diff --git a/wire/core/PageFinder.php b/wire/core/PageFinder.php index 7b5d4702..f1b527a5 100644 --- a/wire/core/PageFinder.php +++ b/wire/core/PageFinder.php @@ -130,6 +130,18 @@ class PageFinder extends Wire { */ 'allowCustom' => false, + /** + * Use sortsAfter feature where PageFinder lets you perform the sorting manually after the find() + * + * When in use, you can access the PageFinder::getSortsAfter() method to retrieve an array of sort + * fields that should be sent to PageArray::sort() + * + * So far this option seems to add more overhead in most cases (rather than save it) so recommend not + * using it. Kept for further experimenting. + * + */ + 'useSortsAfter' => false, + ); protected $fieldgroups; @@ -144,6 +156,7 @@ class PageFinder extends Wire { protected $getQueryNumChildren = 0; // number of times the function has been called protected $lastOptions = array(); protected $extraOrSelectors = array(); // one from each field must match + protected $sortsAfter = array(); // apply these sorts after pages loaded // protected $extraSubSelectors = array(); // subselectors that are added in after getQuery() // protected $extraJoins = array(); @@ -342,6 +355,7 @@ class PageFinder extends Wire { * template ID and score. When false, returns only an array of page IDs. True is required by most usage from * Pages class. False is only for specific cases. * - `allowCustom` (bool): Whether or not to allow _custom='selector string' type values (default=false). + * - `useSortsAfter` (bool): When true, PageFinder may ask caller to perform sort manually in some cases (default=false). * @return array|DatabaseQuerySelect * @throws PageFinderException * @@ -357,8 +371,6 @@ class PageFinder extends Wire { $this->fieldgroups = $this->wire('fieldgroups'); $options = array_merge($this->defaultOptions, $options); - $this->start = 0; // reset for new find operation - $this->limit = 0; $this->parent_id = null; $this->templates_id = null; $this->checkAccess = true; @@ -490,16 +502,62 @@ class PageFinder extends Wire { * */ protected function preProcessSelectors(Selectors $selectors, $options = array()) { - if(!empty($options['allowCustom'])) { - foreach($selectors as $selector) { - $field = $selector->field; - if(!is_string($field) || $field !== '_custom') continue; + + $sortSelectors = array(); + $start = null; + $limit = null; + + foreach($selectors as $selector) { + $field = $selector->field; + + if($field === '_custom') { $selectors->remove($selector); - $_selectors = $this->wire(new Selectors($selector->value())); - /** @var Selectors $_selectors */ - foreach($_selectors as $s) $selectors->add($s); + if(!empty($options['allowCustom'])) { + $_selectors = $this->wire(new Selectors($selector->value())); + /** @var Selectors $_selectors */ + foreach($_selectors as $s) $selectors->add($s); + } + + } else if($field === 'sort') { + if(!empty($options['useSortsAfter']) && $selector->operator == '=' && strpos($selector->value, '.') === false) { + $sortSelectors[] = $selector; + } + + } else if($field === 'limit') { + $limit = (int) $selector->value; + + } else if($field === 'start') { + $start = (int) $selector->value; } } + + if(!$limit && !$start && count($sortSelectors) + && $options['returnVerbose'] && !empty($options['useSortsAfter']) + && empty($options['startAfterID']) && empty($options['stopBeforeID'])) { + // the `useSortsAfter` option is enabled and potentially applicable + $sortsAfter = array(); + foreach($sortSelectors as $n => $selector) { + if(!$n && $this->wire('pages')->loader()->isNativeColumn($selector->value)) { + // first iteration only, see if it's a native column and prevent sortsAfter if so + break; + } + if(strpos($selector->value, '.') !== false) { + // we don't supports sortsAfter for subfields, so abandon entirely + $sortsAfter = array(); + break; + } + if($selector->operator != '=') { + // sort property being used for something else that we don't recognize + continue; + } + $sortsAfter[] = $selector->value; + $selectors->remove($selector); + } + $this->sortsAfter = $sortsAfter; + } + + $this->limit = $limit; + $this->start = $start; } @@ -802,8 +860,8 @@ class PageFinder extends Wire { if(count($fields) > 1) $fields = $this->arrangeFields($fields); $fieldsStr = ':' . implode(':', $fields) . ':'; // for strpos $field = reset($fields); // first field + $subfield = ''; if(strpos($field, '.')) list($field, $subfield) = explode('.', $field); - else $subfield = ''; // TODO Make native fields and path/url multi-field and multi-value aware if($field == 'sort' && $selector->operator === '=' && !$subfield) { @@ -811,7 +869,7 @@ class PageFinder extends Wire { continue; } else if($field == 'limit' || $field == 'start') { - if(!$startLimit) $this->getQueryStartLimit($query, $selectors); + if(!$startLimit) $this->getQueryStartLimit($query); $startLimit = true; continue; @@ -1488,20 +1546,13 @@ class PageFinder extends Wire { } } - protected function getQueryStartLimit(DatabaseQuerySelect $query, $selectors) { + protected function getQueryStartLimit(DatabaseQuerySelect $query) { - $start = null; - $limit = null; - $sql = ''; - - foreach($selectors as $selector) { - if($selector->field == 'start') $start = (int) $selector->value; - else if($selector->field == 'limit') $limit = (int) $selector->value; - } + $start = $this->start; + $limit = $this->limit; if($limit) { - - $this->limit = $limit; + $sql = ''; if(is_null($start) && ($input = $this->wire('input'))) { // if not specified in the selector, assume the 'start' property from the default page's pageNum @@ -1511,15 +1562,13 @@ class PageFinder extends Wire { if(!is_null($start)) { $sql .= "$start,"; - $this->start = $start; } $sql .= "$limit"; - if($this->getTotal && $this->getTotalType != 'count') $query->select("SQL_CALC_FOUND_ROWS"); + if($this->getTotal && $this->getTotalType != 'count') $query->select("SQL_CALC_FOUND_ROWS"); + if($sql) $query->limit($sql); } - - if($sql) $query->limit($sql); } @@ -1951,7 +2000,7 @@ class PageFinder extends Wire { * */ public function getLimit() { - return $this->limit; + return $this->limit === null ? 0 : $this->limit; } /** @@ -1961,7 +2010,7 @@ class PageFinder extends Wire { * */ public function getStart() { - return $this->start; + return $this->start === null ? 0 : $this->start; } /** @@ -1994,6 +2043,20 @@ class PageFinder extends Wire { return $this->lastOptions; } + /** + * Returns array of sortfields that should be applied to resulting PageArray after loaded + * + * See the `useSortsAfter` option which must be enabled to use this. + * + * #pw-internal + * + * @return array + * + */ + public function getSortsAfter() { + return $this->sortsAfter; + } + /** * Does the given field or fieldName resolve to a field that uses Page or PageArray values? * diff --git a/wire/core/Pages.php b/wire/core/Pages.php index cba3b829..78980608 100644 --- a/wire/core/Pages.php +++ b/wire/core/Pages.php @@ -298,6 +298,34 @@ class Pages extends Wire { return $matches; } + /** + * Like $pages->find() except returns array of IDs rather than Page objects. + * + * This is a faster method to use when you only need to know the matching page IDs. + * + * #pw-group-retrieval + * + * @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 arrays with [ id, parent_id, templates_id ] for each page. + * - The verbose option above can also be specified by providing boolean true as the $options argument. + * - See `Pages::find()` $options argument for additional options. + * @return array Array of page IDs, or in verbose mode: array of arrays with id, parent_id and templates_id. + * @since 3.0.46 + * + */ + public function findIDs($selector, $options = array()) { + $verbose = false; + if($options === true) $verbose = true; + if(!is_array($options)) $options = array(); + if(isset($options['verbose'])) { + $verbose = $options['verbose']; + unset($options['verbose']); + } + $options['findIDs'] = $verbose ? true : 1; + return $this->find($selector, $options); + } + /** * Returns the first page matching the given selector with no exclusions * diff --git a/wire/core/PagesLoader.php b/wire/core/PagesLoader.php index 9596c31a..265148db 100644 --- a/wire/core/PagesLoader.php +++ b/wire/core/PagesLoader.php @@ -173,7 +173,8 @@ class PagesLoader 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 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 exculsions (same as include=all option) + * - 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. * - 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). @@ -199,11 +200,12 @@ class PagesLoader extends Wire { $loadPages = array_key_exists('loadPages', $options) ? (bool) $options['loadPages'] : true; $caller = isset($options['caller']) ? $options['caller'] : 'pages.find'; $lazy = empty($options['lazy']) ? false : true; + $findIDs = isset($options['findIDs']) ? $options['findIDs'] : false; $debug = $this->debug && !$lazy; $cachePages = isset($options['cache']) ? (bool) $options['cache'] : true; if(!$cachePages && !isset($loadOptions['cache'])) $loadOptions['cache'] = false; - if($loadPages) { + if($loadPages && !$lazy && !$findIDs) { $pages = $this->findShortcut($selector, $options, $loadOptions); if($pages) return $pages; } @@ -222,10 +224,12 @@ class PagesLoader extends Wire { $selectorString = is_string($selector) ? $selector : (string) $selectors; // see if this has been cached and return it if so - $pages = $this->pages->cacher()->getSelectorCache($selectorString, $options); - if(!is_null($pages)) { - if($debug) $this->pages->debugLog('find', $selectorString, $pages . ' [from-cache]'); - return $pages; + if($loadPages && !$findIDs && !$lazy) { + $pages = $this->pages->cacher()->getSelectorCache($selectorString, $options); + if(!is_null($pages)) { + if($debug) $this->pages->debugLog('find', $selectorString, $pages . ' [from-cache]'); + return $pages; + } } $pageFinder = $this->pages->getPageFinder(); @@ -236,8 +240,9 @@ class PagesLoader extends Wire { $profiler = $this->wire('profiler'); $profilerEvent = $profiler ? $profiler->start("$caller($selectorString)", "Pages") : null; - if($lazy) { - if(strpos($selectorString, 'limit=') === false) $options['getTotal'] = false; + if(($lazy || $findIDs) && strpos($selectorString, 'limit=') === false) $options['getTotal'] = false; + + if($lazy || $findIDs === 1) { $pagesIDs = $pageFinder->findIDs($selectors, $options); } else { $pagesInfo = $pageFinder->find($selectors, $options); @@ -261,7 +266,7 @@ class PagesLoader extends Wire { $loadPages = false; $cachePages = false; $template = null; - + foreach($pagesIDs as $id) { $page = $this->pages->newPage(); $page->_lazy($id); @@ -272,6 +277,12 @@ class PagesLoader extends Wire { $pages->setDuplicateChecking(true); if(count($pagesIDs)) $pages->_lazy(true); + } else if($findIDs) { + + $loadPages = false; + $cachePages = false; + $pages = $this->pages->newPageArray($loadOptions); // only for hooks to see + } else if($loadPages) { // parent_id is null unless a single parent was specified in the selectors $parent_id = $pageFinder->getParentID(); @@ -288,6 +299,7 @@ class PagesLoader extends Wire { if(count($idsByTemplate) > 1) { // perform a load for each template, which results in unsorted pages + // @todo use $idsUnsorted array rather than $unsortedPages PageArray $unsortedPages = $this->pages->newPageArray($loadOptions); foreach($idsByTemplate as $tpl_id => $ids) { $opt = $loadOptions; @@ -315,6 +327,9 @@ class PagesLoader extends Wire { $opt['parent_id'] = $parent_id; $pages->import($this->getById($idsSorted, $opt)); } + + $sortsAfter = $pageFinder->getSortsAfter(); + if(count($sortsAfter)) $pages->sort($sortsAfter); } else { $pages = $this->pages->newPageArray($loadOptions); @@ -353,6 +368,8 @@ class PagesLoader extends Wire { 'pagesInfo' => $pagesInfo, 'options' => $options )); + + if($findIDs) return $findIDs === 1 ? $pagesIDs : $pagesInfo; return $pages; }