diff --git a/wire/core/Pages.php b/wire/core/Pages.php index 12f1b052..4640fb8a 100644 --- a/wire/core/Pages.php +++ b/wire/core/Pages.php @@ -217,15 +217,15 @@ class Pages extends Wire { * * @param string|int|array|Selectors $selector Specify selector (standard usage), but can also accept page ID or array of page IDs. * @param array|string $options One or more options that can modify certain behaviors. May be associative array or "key=value" selector string. - * - `findOne` (boolean): Apply optimizations for finding a single page (default=false). - * - `findAll` (boolean): Find all pages with no exclusions, same as "include=all" option (default=false). - * - `findIDs` (boolean|int): Specify 1 to return array of only page IDs, or true to return verbose array (default=false). - * - `getTotal` (boolean): Whether to set returning PageArray's "total" property (default=true, except when findOne=true). - * - `loadPages` (boolean): Whether to populate the returned PageArray with found pages (default=true). + * - `findOne` (bool): Apply optimizations for finding a single page (default=false). + * - `findAll` (bool): Find all pages with no exclusions, same as "include=all" option (default=false). + * - `findIDs` (bool|int): 1 to get array of page IDs, true to return verbose array, 2 to return verbose array with all cols in 3.0.153+. (default=false). + * - `getTotal` (bool): Whether to set returning PageArray's "total" property (default=true, except when findOne=true). + * - `loadPages` (bool): Whether to populate the returned PageArray with found pages (default=true). * The only reason why you'd want to change this to false would be if you only needed the count details from * the PageArray: getTotal(), getStart(), getLimit, etc. This is intended as an optimization for $pages->count(). * Does not apply if $selector argument is an array. - * - `cache` (boolean): Allow caching of selectors and loaded pages? (default=true). Also sets loadOptions[cache]. + * - `cache` (bool): Allow caching of selectors and loaded pages? (default=true). Also sets loadOptions[cache]. * - `allowCustom` (boolean): Allow use of _custom="another selector" in given $selector? For specific uses. (default=false) * - `caller` (string): Optional name of calling function, for debugging purposes, i.e. "pages.count" (default=blank). * - `include` (string): Optional inclusion mode of 'hidden', 'unpublished' or 'all'. (default=none). Typically you would specify this @@ -342,7 +342,7 @@ class Pages extends Wire { * @param string|array|Selectors $selector Selector to find page IDs. * @param array|bool $options Options to modify behavior. * - `verbose` (bool): Specify true to make return value array of associative arrays, each with verbose info. - * - The verbose option above can also be specified by providing boolean true as the $options argument. + * - The verbose option above can also be specified as alternative to the $options argument. * - See `Pages::find()` $options argument for additional options. * @return array Array of page IDs, or in verbose mode: array of arrays, each with id, parent_id and templates_id keys. * @since 3.0.46 @@ -350,13 +350,20 @@ class Pages extends Wire { */ public function findIDs($selector, $options = array()) { $verbose = false; - if($options === true) $verbose = true; - if(!is_array($options)) $options = array(); + if(!is_array($options)) { + // verbose option specified in $options array + $verbose = $options; + $options = array(); + } if(isset($options['verbose'])) { $verbose = $options['verbose']; unset($options['verbose']); } - $options['findIDs'] = $verbose ? true : 1; + if($verbose === 2 || $verbose === '*') { + $options['findIDs'] = 2; + } else { + $options['findIDs'] = $verbose ? true : 1; + } /** @var array $ids */ $ids = $this->find($selector, $options); return $ids; @@ -389,7 +396,29 @@ class Pages extends Wire { public function get($selector, $options = array()) { return $this->loader->get($selector, $options); } - + + /** + * Is there any page that matches the given $selector in the system? (with no exclusions) + * + * - This can be used as an “exists” or “getID” type of method. + * - Returns ID of first matching page if any exist, or 0 if none exist (returns array if `$verbose` is true). + * - Like with the `get()` method, no pages are excluded, so an `include=all` is not necessary in selector. + * - If you need to quickly check if something exists, this method is preferable to using a count() or get(). + * + * When `$verbose` option is used, an array is returned instead. Verbose return array includes page `id`, + * `parent_id` and `templates_id` indexes. + * + * @param string|int|array|Selectors $selector + * @param bool $verbose Return verbose array with page id, parent_id, templates_id rather than just page id? (default=false) + * @return array|int + * @since 3.0.153 + * @see Pages::count(), Pages::get() + * + */ + public function has($selector, $verbose = false) { + return $this->loader->has($selector, $verbose); + } + /** * Save a page object and its fields to database. * diff --git a/wire/core/PagesLoader.php b/wire/core/PagesLoader.php index 97e55dad..5ca82aeb 100644 --- a/wire/core/PagesLoader.php +++ b/wire/core/PagesLoader.php @@ -109,62 +109,44 @@ class PagesLoader extends Wire { } /** - * Helper for find() method to attempt to shortcut the find when possible + * Normalize a selector string * - * @param string|array|Selectors $selector - * @param array $options - * @param array $loadOptions - * @return bool|Page|PageArray Returns boolean false when no shortcut available + * @param string $selector + * @param bool $convertIDs Normalize to integer ID or array of integer IDs when possible (default=true) + * @return array|int|string * */ - protected function findShortcut(&$selector, $options, $loadOptions) { + protected function normalizeSelectorString($selector, $convertIDs = true) { - if(empty($selector)) return $this->pages->newPageArray($loadOptions); - if(!empty($options['lazy'])) return false; - - $value = false; - $filter = empty($options['findAll']); - - if(is_string($selector)) { - $selector = trim($selector, ', '); - if(ctype_digit($selector)) { - // normalize to page ID (int) - $selector = (int) $selector; - } else if($selector === '/' || $selector === 'path=/') { - // normalize selectors that indicate homepage to just be ID 1 - $selector = (int) $this->wire('config')->rootPageID; - } else if($selector[0] == '/') { - // if selector begins with a slash, it is referring to a path - $selector = "path=$selector"; - } - } + $selector = trim($selector, ', '); - if(is_array($selector)) { - // array that is .... not associative .................. not selector array ........ consists of only numbers - if(ctype_digit(implode('', array_keys($selector))) && !is_array(reset($selector)) && ctype_digit(implode('', $selector))) { - // regular array of page IDs, we delegate that to getById() method, but with access/visibility control - foreach($selector as $k => $v) $selector[$k] = (int) $v; - $value = $this->getById($selector, $loadOptions); - $filter = true; - } + if(ctype_digit($selector)) { + // normalize to page ID (int) + $selector = (int) $selector; - } else if(is_int($selector)) { - // page ID integer - $value = $this->getById(array($selector), $loadOptions); + } else if($selector === '/' || $selector === 'path=/') { + // normalize selectors that indicate homepage to just be ID 1 + $selector = (int) $this->wire('config')->rootPageID; + + } else if($selector[0] === '/') { + // if selector begins with a slash, it is referring to a path + $selector = "path=$selector"; - } else if(is_string($selector) && strpos($selector, ',') === false) { - // there is just one “key=value” or “value” selector - if(strpos($selector, 'id=') === 0) { - // string like id=123 or id=123|456|789 - $s = substr($selector, 3); // skip over 'id=' - if(ctype_digit($s)) { - // id=123 - $value = $this->getById((int) $s, $loadOptions); - } else if(strpos($selector, '|') && ctype_digit(str_replace('|', '', $s))) { - // id=123|456|789 - $a = explode('|', $s); - foreach($a as $k => $v) $a[$k] = (int) $v; - $value = $this->getById($a, $loadOptions); + } else if(strpos($selector, ',') === false) { + // there is just one “key=value” or “value” selector that needs further processing + if(strpos($selector, 'id=')) { + if($convertIDs) { + // string like id=123 or id=123|456|789 converted to int or int-array + $s = substr($selector, 3); // skip over 'id=' + if(ctype_digit($s)) { + // id=123 + $selector = (int) $s; + } else if(strpos($selector, '|') && ctype_digit(str_replace('|', '', $s))) { + // id=123|456|789 + $a = explode('|', $s); + foreach($a as $k => $v) $a[$k] = (int) $v; + $selector = $a; + } } } else if(!Selectors::stringHasOperator($selector)) { // no operator indicates this is just referring to a page name @@ -176,6 +158,91 @@ class PagesLoader extends Wire { } } } + + if(is_int($selector) || ctype_digit("$selector")) { + // page ID integer + if($convertIDs) { + $selector = (int) $selector; + } else { + $selector = "id=$selector"; + } + } + + return $selector; + } + + /** + * Normalize a selector + * + * @param string|int|array $selector + * @param bool $convertIDs Convert ID-only selectors to integers or arrays of integers? + * @return array|int|string + * + */ + protected function normalizeSelector($selector, $convertIDs = true) { + + if(empty($selector)) return ''; + + if(is_int($selector)) { + if(!$convertIDs) $selector = "id=$selector"; + } else if(is_string($selector)) { + $selector = $this->normalizeSelectorString($selector, $convertIDs); + } else if(is_array($selector)) { + // array that is not associative, not selector array, and consists of only numbers + if($this->isIdArray($selector)) { + if(!$convertIDs) $selector = 'id=' . implode('|', $selector); + } + } + + return $selector; + } + + /** + * Is this an array of IDs? Also sanitizes to all integers when true + * + * @param array $a + * @return bool + * + */ + protected function isIdArray(array &$a) { + if(ctype_digit(implode('', array_keys($a))) && !is_array(reset($a)) && ctype_digit(implode('', $a))) { + // regular array of page IDs, we delegate that to getById() method, but with access/visibility control + foreach($a as $k => $v) $a[$k] = (int) $v; + return true; + } else { + return false; + } + } + + /** + * Helper for find() method to attempt to shortcut the find when possible + * + * @param string|array|Selectors $selector + * @param array $options + * @param array $loadOptions + * @return bool|Page|PageArray Returns boolean false when no shortcut available + * + */ + protected function findShortcut($selector, $options, $loadOptions) { + + if(empty($selector)) { + return $this->pages->newPageArray($loadOptions); + } + + $value = false; + $filter = empty($options['findAll']); + $selector = $this->normalizeSelector($selector, true); + + if(is_array($selector)) { + if($this->isIdArray($selector)) { + $value = $this->getById($selector, $loadOptions); + $filter = true; + } + + } else if(is_int($selector)) { + // page ID integer + $value = $this->getById(array($selector), $loadOptions); + } if($value) { if($filter) { @@ -200,7 +267,7 @@ class PagesLoader extends Wire { * @param array|string $options Optional one or more options that can modify certain behaviors. May be assoc array or key=value string. * - findOne: boolean - apply optimizations for finding a single page * - findAll: boolean - find all pages with no exclusions (same as include=all option) - * - findIDs: boolean|int - true=return array of [id, template_id, parent_id], or 1=return just page IDs. + * - findIDs: boolean|int - true=return array of [id, template_id, parent_id], or 1=return just page IDs, 2=return all columns (3.0.153+). * - getTotal: boolean - whether to set returning PageArray's "total" property (default: true except when findOne=true) * - cache: boolean - Allow caching of selectors and pages loaded (default=true). Also sets loadOptions[cache]. * - allowCustom: boolean - Whether to allow use of "_custom=new selector" in selectors (default=false). @@ -239,6 +306,7 @@ class PagesLoader extends Wire { if($selector instanceof Selectors) { $selectors = $selector; } else { + $selector = $this->normalizeSelector($selector, false); $selectors = $this->wire(new Selectors()); $selectors->init($selector); } @@ -273,7 +341,10 @@ class PagesLoader extends Wire { $pagesIDs = $pageFinder->findTemplateIDs($selectors, $options); } else if($findIDs === 1) { // [ pageID ] - $pagesIDs = $pageFinder->findIDs($selectors, $options); + $pagesIDs = $pageFinder->findIDs($selectors, $options); + } else if($findIDs === 2) { + // [ pageID => [ all pages columns ] ] + $pagesInfo = $pageFinder->findVerboseIDs($selectors, $options); } else { // [ [ 'id' => 3, 'templates_id' => 2, 'parent_id' => 1 ] $pagesInfo = $pageFinder->find($selectors, $options); @@ -504,6 +575,52 @@ class PagesLoader extends Wire { if(!$page) $page = $this->pages->newNullPage(); return $page; } + + /** + * Is there any page that matches the given $selector in the system? (with no exclusions) + * + * - This can be used as an “exists” or “getID” type of method. + * - Returns ID of first matching page if any exist, or 0 if none exist (returns array if `$verbose` is true). + * - Like with the `get()` method, no pages are excluded, so an `include=all` is not necessary in selector. + * - If you need to quickly check if something exists, this method is preferable to using a count() or get(). + * + * When `$verbose` option is used, an array is returned instead. Verbose return array includes page `id`, + * `parent_id` and `templates_id` indexes. + * + * @param string|int|array|Selectors $selector + * @param bool $verbose Return verbose array with all pages columns rather than just page id? (default=false) + * @return array|int + * @since 3.0.153 + * + */ + public function has($selector, $verbose = false) { + + $options = array( + 'findOne' => true, // find only one page + 'findAll' => true, // no exclusions + 'findIDs' => $verbose ? 2 : 1, // 1=find IDs, true=find verbose all cols + 'getTotal' => false, // don't count totals + 'caller' => 'pages.has', + ); + + if(empty($selector)) return $verbose ? array() : 0; + + if((is_string($selector) || is_int($selector)) && !$verbose) { + // see if any matching page is already in the cache + $page = $this->pages->getCache($selector); + if($page) return $page->id; + } + + $items = $this->pages->find($selector, $options); + + if($verbose) { + $value = count($items) ? reset($items) : array(); + } else { + $value = count($items) ? (int) reset($items) : 0; + } + + return $value; + } /** * Given an array or CSV string of Page IDs, return a PageArray