pages = $pages; $this->debug = $pages->debug(); } /** * Set whether loaded pages have their outputFormatting turned on or off * * By default, it is turned on. * * @param bool $outputFormatting * */ public function setOutputFormatting($outputFormatting = true) { $this->outputFormatting = $outputFormatting ? true : false; } /** * Get whether loaded pages have their outputFormatting turned on or off * * @return bool * */ public function getOutputFormatting() { return $this->outputFormatting; } /** * Enable or disable use of autojoin for all queries * * Default should always be true, and you may use this to turn it off temporarily, but * you should remember to turn it back on * * @param bool $autojoin * */ public function setAutojoin($autojoin = true) { $this->autojoin = $autojoin ? true : false; } /** * Get whether autojoin is enabled for page loading queries * * @return bool * */ public function getAutojoin() { return $this->autojoin; } /** * Normalize a selector string * * @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 normalizeSelectorString($selector, $convertIDs = true) { $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"; } 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 $sanitizer = $this->wire()->sanitizer; if($sanitizer->pageNameUTF8($selector) === $selector) { // sanitized value consistent with a page name // optimize selector rather than determining value here $selector = 'name=' . $sanitizer->selectorValue($selector); } } } if(is_int($selector) || ctype_digit("$selector")) { // page ID integer if($convertIDs) { $selector = (int) $selector; } else { $selector = "id=$selector"; } } /** @var array|int|string $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) { $includeMode = isset($options['include']) ? $options['include'] : ''; $value = $this->filterListable($value, $includeMode, $loadOptions); } if($this->debug) { $this->pages->debugLog('find', $selector . " [optimized]", $value); } } return $value; } /** * Given a Selector string, return the Page objects that match in a PageArray. * * Non-visible pages are excluded unless an include=hidden|unpublished|all mode is specified in the selector string, * or in the $options array. If 'all' mode is specified, then non-accessible pages (via access control) can also be included. * * @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` (bool): Apply optimizations for finding a single page. * - `findAll` (bool): Find all pages with no exclusions (same as include=all option). * - `findIDs` (bool|int): Makes method return raw array rather than PageArray, specify one of the following: * • `true` (bool): return array of [ [id, templates_id, parent_id] ] for each page. * • `1` (int): Return just array of just page IDs, [id, id, id] * • `2` (int): Return all pages table columns in associative array for each page (3.0.153+). * • `3` (int): Same as 2 + dates are unix timestamps + has 'pageArray' key w/blank PageArray for pagination info (3.0.172+). * • `4` (int): Same as 3 + return PageArray instead if one is available in cache (3.0.172+). * - `getTotal` (bool): Whether to set returning PageArray's "total" property (default: true except when findOne=true) * - `cache` (bool): Allow caching of selectors and pages loaded (default=true). Also sets loadOptions[cache]. * - `allowCustom` (bool): Whether to allow use of "_custom=new selector" in selectors (default=false). * - `lazy` (bool): Makes find() return Page objects that don't have any data populated to them (other than id and template). * - `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 $selectorString argument is an array. * - `caller` (string): Name of calling function, for debugging purposes, i.e. pages.count * - `include` (string): Inclusion mode of 'hidden', 'unpublished' or 'all'. Default=none. Typically you would specify this * directly in the selector string, so the option is mainly useful if your first argument is not a string. * - `stopBeforeID` (int): Stop loading pages once page matching this ID is found (default=0). * - `startAfterID` (int): Start loading pages once page matching this ID is found (default=0). * - `loadOptions` (array): Assoc array of options to pass to getById() load options. (does not apply when 'findIds' > 3). * - `joinFields` (array): Names of fields to autojoin, or empty array to join none; overrides field autojoin settings (default=null) 3.0.172+ * @return PageArray|array * */ public function find($selector, $options = array()) { if(is_string($options)) $options = Selectors::keyValueStringToArray($options); $loadOptions = isset($options['loadOptions']) && is_array($options['loadOptions']) ? $options['loadOptions'] : array(); $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; $allowShortcuts = $loadPages && !$lazy && (!$findIDs || $findIDs === 4); $joinFields = isset($options['joinFields']) ? $options['joinFields'] : array(); $cachePages = isset($options['cache']) ? $options['cache'] : true; if($cachePages) { $options['cache'] = $cachePages; $loadOptions['cache'] = $cachePages; } else if(!isset($loadOptions['cache'])) { $loadOptions['cache'] = false; } if($allowShortcuts) { $pages = $this->findShortcut($selector, $options, $loadOptions); if($pages) return $pages; } if($selector instanceof Selectors) { $selectors = $selector; } else { $selector = $this->normalizeSelector($selector, false); $selectors = $this->wire(new Selectors()); /** @var Selectors $selectors */ $selectors->init($selector); } if(isset($options['include']) && in_array($options['include'], array('hidden', 'unpublished', 'all'))) { $selectors->add(new SelectorEqual('include', $options['include'])); } $selectorString = is_string($selector) ? $selector : (string) $selectors; // check whether the joinFields option will be used if(!$lazy && !$findIDs) { $fields = $this->wire()->fields; // support the joinFields option when selector contains 'field=a|b|c' or 'join=a|b|c' foreach(array('field', 'join') as $name) { if(strpos($selectorString, "$name=") === false || $fields->get($name)) continue; foreach($selectors as $selector) { if($selector->field() !== $name) continue; $joinFields = array_merge($joinFields, $selector->values()); $selectors->remove($selector); } } if(count($joinFields)) { unset($options['include']); // because it was moved into $selectors earlier return $this->findMin($selectors, array_merge($options, array('joinFields' => $joinFields))); } } // see if this has been cached and return it if so if($allowShortcuts) { $pages = $this->pages->cacher()->getSelectorCache($selectorString, $options); if($pages !== null) { if($debug) $this->pages->debugLog('find', $selectorString, $pages . ' [from-cache]'); return $pages; } } $pageFinder = $this->pages->getPageFinder(); $pagesInfo = array(); $pagesIDs = array(); if($debug) Debug::timer("$caller($selectorString)", true); $profiler = $this->wire()->profiler; $profilerEvent = $profiler ? $profiler->start("$caller($selectorString)", "Pages") : null; if(($lazy || $findIDs) && strpos($selectorString, 'limit=') === false) $options['getTotal'] = false; if($lazy) { // [ pageID => templateID ] $pagesIDs = $pageFinder->findTemplateIDs($selectors, $options); } else if($findIDs === 1) { // [ pageID ] $pagesIDs = $pageFinder->findIDs($selectors, $options); } else if($findIDs === 2) { // [ pageID => [ all pages columns ] ] $pagesInfo = $pageFinder->findVerboseIDs($selectors, $options); } else if($findIDs === 3 || $findIDs === 4) { // [ pageID => [ all pages columns + sortfield + dates as unix timestamps ], // 'pageArray' => PageArray(blank but with pagination info populated) ] ] $options['joinSortfield'] = true; $options['getNumChildren'] = true; $options['unixTimestamps'] = true; $pagesInfo = $pageFinder->findVerboseIDs($selectors, $options); } else { // [ [ 'id' => 3, 'templates_id' => 2, 'parent_id' => 1, 'score' => 1.123 ] $pagesInfo = $pageFinder->find($selectors, $options); } if($debug && empty($loadOptions['caller'])) { $loadOptions['caller'] = "$caller($selectorString)"; } // note that we save this pagination state here and set it at the end of this method // because it's possible that more find operations could be executed as the pages are loaded $total = $pageFinder->getTotal(); $limit = $pageFinder->getLimit(); $start = $pageFinder->getStart(); if($lazy) { // lazy load: create empty pages containing only id and template $templates = $this->wire()->templates; $pages = $this->pages->newPageArray($loadOptions); $pages->finderOptions($options); $pages->setDuplicateChecking(false); $loadPages = false; $cachePages = false; $template = null; $templatesByID = array(); $loading = $this->loading; if(!$loading) $this->loading = true; foreach($pagesIDs as $id => $templateID) { if(isset($templatesByID[$templateID])) { $template = $templatesByID[$templateID]; } else { $template = $templates->get($templateID); $templatesByID[$templateID] = $template; } $page = $this->pages->newPage($template); $page->_lazy($id); $page->loaderCache = false; $pages->add($page); } if(!$loading) $this->loading = false; $pages->setDuplicateChecking(true); if(count($pagesIDs)) $pages->_lazy(true); unset($template, $templatesByID); } else if($findIDs) { $loadPages = false; $cachePages = false; // PageArray for hooks or for findIDs==3 option $pages = $this->pages->newPageArray($loadOptions); } else if($loadPages) { // parent_id is null unless a single parent was specified in the selectors $templates = $this->wire()->templates; $parent_id = $pageFinder->getParentID(); $idsSorted = array(); $idsByTemplate = array(); $scores = array(); // organize the pages by template ID foreach($pagesInfo as $page) { $tpl_id = (int) $page['templates_id']; $id = (int) $page['id']; if(!isset($idsByTemplate[$tpl_id])) $idsByTemplate[$tpl_id] = array(); $idsByTemplate[$tpl_id][] = $id; $idsSorted[] = $id; if(!empty($page['score'])) $scores[$id] = (float) $page['score']; } 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; $opt['template'] = $templates->get($tpl_id); $opt['parent_id'] = $parent_id; $unsortedPages->import($this->getById($ids, $opt)); } // put pages back in the order that the selectorEngine returned them in, while double checking that the selector matches $pages = $this->pages->newPageArray($loadOptions); foreach($idsSorted as $id) { foreach($unsortedPages as $page) { if($page->id == $id) { $pages->add($page); break; } } } } else { // there is only one template used, so no resorting is necessary $pages = $this->pages->newPageArray($loadOptions); reset($idsByTemplate); $opt = $loadOptions; $opt['template'] = $templates->get(key($idsByTemplate)); $opt['parent_id'] = $parent_id; $pages->import($this->getById($idsSorted, $opt)); } $sortsAfter = $pageFinder->getSortsAfter(); if(count($sortsAfter)) $pages->sort($sortsAfter); if(count($scores)) { foreach($pages as $page) { $score = isset($scores[$page->id]) ? $scores[$page->id] : 0; $page->setQuietly('_pfscore', $score); } } } else { $pages = $this->pages->newPageArray($loadOptions); } $pageFinder->getPageArrayData($pages); $pages->setTotal($total); $pages->setLimit($limit); $pages->setStart($start); $pages->setSelectors($selectorString); $pages->setTrackChanges(true); $this->lastPageFinder = $pageFinder; if($loadPages && $cachePages) { if(strpos($selectorString, 'sort=random') !== false) { if($selectors->getSelectorByFieldValue('sort', 'random')) $cachePages = false; } if($cachePages) { $this->pages->cacher()->selectorCache($selectorString, $options, $pages); } } if($debug) { $this->pages->debugLog('find', $selectorString, $pages); $count = $pages->count(); $note = ($count == $total ? $count : $count . "/$total") . " page(s)"; if($count) { $note .= ": " . $pages->first()->path; if($count > 1) $note .= " ... " . $pages->last()->path; } if(substr($caller, -1) !== ')') $caller .= "($selectorString)"; Debug::saveTimer($caller, $note); foreach($pages as $item) { if($item->_debug_loader) continue; $item->setQuietly('_debug_loader', $caller); } } if($profilerEvent) $profiler->stop($profilerEvent); if($this->pages->hasHook('found()')) $this->pages->found($pages, array( 'pageFinder' => $pageFinder, 'pagesInfo' => $pagesInfo, 'options' => $options )); if($findIDs) { if($findIDs === 3 || $findIDs === 4) $pagesInfo['pageArray'] = $pages; return $findIDs === 1 ? $pagesIDs : $pagesInfo; } return $pages; } /** * Minimal find for reduced or delayed overload in some circumstances * * This combines the page finding and page loading operation into a single operation * and single query, unlike a regular find() which finds matching page IDs in one * query and then loads them in a separate query. As a result this method does not * need to call the getByIds() method to load pages, as it is able to load them itself. * * This strategy may eventually replace the “find() + getByIds()” strategy, but for the * moment is only used when the `$pages->find()` method specifies `field=name` in * the selector. In that selector, `name` can be any field name, or group of them, i.e. * `title|date|summary`, or a non-existing field like `none` to specify that no fields * should be autojoin (for fastest performance). * * Note that while this might reduce overhead in some cases, it can also increase the * overall request time if you omit fields that are actually used on the resulting pages. * For instance, if the `title` field is an autojoin field (as it is by default), and * we do a `$pages->find('template=blog-post, field=none');` and then render a list of * blog post titles, then we have just increased overhead because PW would have to * perform a separate query to load each blog-post page’s title. On the other hand, if * we render a list of blog post titles with date and summary, and the date and summary * fields are not configured as autojoin fields, then we can specify all those that we * use in our rendered list to greatly improve performance, like this: * `$pages->find('template=blog-post, field=title|date|summary');`. * * While this method combines what find() and getById() do in one query, there does not * appear to be any overhead benefit when the two strategies are dealing with identical * conditions, like the same autojoin fields. * * @param string|array|Selectors $selector * @param array $options * - `cache` (bool): Allow pulling from and saving results to cache? (default=true) * - `joinFields` (array): Names of fields to also join into the page load * @return PageArray * @throws WireException * @since 3.0.172 * */ public function findMin($selector, array $options = array()) { $useCache = isset($options['cache']) ? $options['cache'] : true; $templates = $this->wire()->templates; $languages = $this->wire()->languages; $languageIds = array(); $templatesById = array(); $tmpAutojoinFields = array(); // fields to autojoin temporarily, just during this method call if($languages) foreach($languages as $language) $languageIds[$language->id] = $language->id; $options['findIDs'] = $useCache ? 4 : 3; $joinFields = isset($options['joinFields']) ? $options['joinFields'] : array(); $rows = $this->find($selector, $options); // if PageArray was already available in cache, return it now if($rows instanceof PageArray) return $rows; /** @var PageArray $pageArray */ $pageArray = $rows['pageArray']; $pageArray->setTrackChanges(false); $paginationTotal = $pageArray->getTotal(); /** @var array $joinResults PageFinder sets which fields supported autojoin true|false */ $joinResults = $pageArray->data('joinFields'); unset($rows['pageArray']); foreach($rows as $row) { $page = $useCache ? $this->pages->getCache($row['id']) : null; $tid = (int) $row['templates_id']; if($page) { $pageArray->add($page); continue; } if(isset($templatesById[$tid])) { $template = $templatesById[$tid]; } else { $template = $templates->get($tid); if(!$template) continue; $templatesById[$tid] = $template; } $sortfield = $template->sortfield; if(empty($sortfield) && isset($row['sortfield'])) $sortfield = $row['sortfield']; $set = array( 'pageClass' => $template->getPageClass(), 'isLoaded' => false, 'id' => $row['id'], 'template' => $template, 'parent_id' => $row['parent_id'], 'sortfield' => $sortfield, ); unset($row['templates_id'], $row['parent_id'], $row['id'], $row['sortfield']); $page = $this->pages->newPage($set); $page->instanceID = ++self::$pageInstanceID; if($languages) { foreach($languageIds as $id) { $key = "name$id"; if(isset($row[$key]) && strpos($row[$key], 'xn-') === 0) { $page->setName($row[$key], $key); unset($row[$key]); } } } foreach($row as $key => $value) { if(strpos($key, '__')) { if($value === null) { $row[$key] = 'null'; // ensure detected by later isset in foreach($joinFields) } else { $page->setFieldValue($key, $value, false); } } else { $page->setForced($key, $value); } } foreach($joinFields as $joinField) { if(empty($joinResults[$joinField])) continue; // field did not support autojoin if(!$template->fieldgroup->hasField($joinField)) continue; $field = $page->getField($joinField); if(!$field || !$field->type) continue; if(isset($row["{$joinField}__data"])) { if(!$field->hasFlag(Field::flagAutojoin)) { $field->addFlag(Field::flagAutojoin); $tmpAutojoinFields[$field->id] = $field; } } else { // set blank values where joinField didn't appear on page row $blankValue = $field->type->getBlankValue($page, $field); $page->setFieldValue($field->name, $blankValue, false); } } $page->setIsLoaded(true); $page->setIsNew(false); $page->resetTrackChanges(true); $page->setOutputFormatting($this->outputFormatting); $this->totalPagesLoaded++; $pageArray->add($page); if($useCache) $this->pages->cache($page); } $pageArray->setTotal($paginationTotal); $pageArray->resetTrackChanges(true); foreach($tmpAutojoinFields as $field) { /** @var Field $field */ $field->removeFlag(Field::flagAutojoin)->untrackChange('flags'); } if($useCache) { $selectorString = $pageArray->getSelectors(true); $this->pages->cacher()->selectorCache($selectorString, $options, $pageArray); } return $pageArray; } /** * Like find() but returns only the first match as a Page object (not PageArray) * * This is functionally similar to the get() method except that its default behavior is to * filter for access control and hidden/unpublished/etc. states, in the same way that the * find() method does. You can add an `include=` to your selector with value `hidden`, * `unpublished` or `all` to change this behavior, just like with find(). * * Unlike the find() method, this method performs a secondary runtime access check by calling * `$page->viewable()` with the found $page, and returns a `NullPage` if the page is not * viewable with that call. In 3.0.142+, an `include=` mode of `all` or `unpublished` will * override this, where appropriate. * * This method also accepts an `$options` array, whereas `Pages::get()` does not. * * @param string|int|array|Selectors $selector * @param array|string $options See $options for `Pages::find` * @return Page|NullPage * */ public function findOne($selector, $options = array()) { if(empty($selector)) return $this->pages->newNullPage(); if(is_string($options)) $options = Selectors::keyValueStringToArray($options); $defaults = array( 'findOne' => true, // find only one page 'getTotal' => false, // don't count totals 'caller' => 'pages.findOne' ); $options = array_merge($defaults, $options); $items = $this->pages->find($selector, $options); $page = $items->first(); if($page && !$page->viewable(false)) { // page found but is not viewable, check if include mode was specified and would allow the page $selectors = $items->getSelectors(); if($selectors) { $include = $selectors->getSelectorByField('include'); $checkAccess = $selectors->getSelectorByField('check_access'); if(!$checkAccess) $checkAccess = $selectors->getSelectorByField('checkAccess'); $checkAccess = $checkAccess ? (bool) $checkAccess->value() : true; } else { $include = null; $checkAccess = true; } if(!$include) { // there was no “include=” selector present if($checkAccess === true) $page = null; } else if($include->value() === 'all') { // allow $page to pass through with include=all mode } else if($include->value() === 'unpublished' && $page->hasStatus(Page::statusUnpublished) && $checkAccess) { // check if user would have access without unpublished status $status = $page->status; $page->setQuietly('status', $status & ~Page::statusUnpublished); $viewable = $page->viewable(false); $page->setQuietly('status', $status); // restore if(!$viewable) $page = null; } else { if($checkAccess === true) $page = null; } } return $page && $page->id ? $page : $this->pages->newNullPage(); } /** * Find pages and cache the result for specified period of time * * Use this when you want to cache a slow or complex page finding operation so that it doesn’t * have to be repated for every web request. Note that this only caches the find operation * and not the loading of the found pages. * * ~~~~~ * $items = $pages->findCache("title%=foo"); // 60 seconds (default) * $items = $pages->findCache("title%=foo", 3600); // 1 hour * $items = $pages->findCache("title%=foo", "+1 HOUR"); // same as above * ~~~~~ * * @param string|array|Selectors $selector * @param int|string|bool|null $expire When the cache should expire, one of the following: * - Max age integer (in seconds). * - Any string accepted by PHP’s `strtotime()` that specifies when the cache should be expired. * - Any `WireCache::expire…` constant or anything accepted by the `WireCache::get()` $expire argument. * @param array $options Options to pass to `$pages->getByIDs()`, or: * - `findIDs` (bool): Return just the page IDs rather then the actual pages? (default=false) * @return PageArray|array * @since 3.0.218 * */ public function findCache($selector, $expire = 60, $options = array()) { $user = $this->wire()->user; $cache = $this->wire()->cache; $ns = 'pages.findCache'; $items = null; if(is_string($selector)) { $selectorStr = $selector; $selectors = $selector; } else { $selectors = $this->wire(new Selectors($selector)); $selectorStr = (string) $selectors; } $rolesStr = (string) $user->roles; if(strpos($rolesStr, '|')) { $rolesArray = explode('|', $rolesStr); sort($rolesArray); $rolesStr = implode('|', $rolesArray); } $optionsStr = ''; foreach($options as $key => $value) { if(!is_string($value)) { if(is_array($value)) $value = print_r($value, true); $value = (string) $value; } $optionsStr .= "$key==$value,"; } $cacheName = "$rolesStr\r$selectorStr\r$optionsStr"; $pageNum = $this->wire()->input->pageNum(); if($pageNum > 1 && Selectors::selectorHasField($selectors, 'limit')) { if(!Selectors::selectorHasField($selectors, 'start')) $cacheName .= "\r$pageNum"; } $cacheName = md5($cacheName); $data = $cache->getFor($ns, $cacheName, $expire); if(!empty($data) && $data['selector'] === $selectorStr && $data['roles'] === $rolesStr) { $ids = $data['pages']; } else { $ids = null; if(strpos($selectorStr, 'template') !== false && empty($options['template'])) { $info = Selectors::selectorHasField($selectors, array('template', 'templates_id'), array('verbose' => true)); if($info['result']) $options['template'] = $this->wire()->templates->get($info['value']); echo "template=$options[template]\n"; } } if($ids === null) { if(empty($options['findIDs'])) { $items = $this->find($selectors, $options); $ids = $items->explode('id'); } else { $ids = $this->pages->findIDs($selectors, $options); } $data = array( 'selector' => $selectorStr, 'roles' => $rolesStr, 'pages' => $ids ); $cache->saveFor($ns, $cacheName, $data, $expire); } else if(empty($options['findIDs'])) { $items = $this->pages->getByIDs($ids, $options); } if(!empty($options['findIDs'])) return $ids; foreach($items as $item) { if($item instanceof NullPage || $item->status & Page::statusTrash) { $items->remove($item); } } return $items; } /** * Returns the first page matching the given selector with no exclusions * * @param string|int|array|Selectors $selector * @param array $options See Pages::find method for options * @return Page|NullPage Always returns a Page object, but will return NullPage (with id=0) when no match found * */ public function get($selector, $options = array()) { if(empty($selector)) return $this->pages->newNullPage(); if(is_int($selector)) { $getCache = true; } else if(is_string($selector) && (ctype_digit($selector) || strpos($selector, 'id=') === 0)) { $getCache = true; } else { $getCache = false; } if($getCache) { // if cache is possible, allow user-specified options to dictate whether cache is allowed if(isset($options['loadOptions']) && isset($options['loadOptions']['getFromCache'])) { $getCache = (bool) $options['loadOptions']['getFromCache']; } if($getCache) { $page = $this->pages->getCache($selector); // selector is either 123 or id=123 if($page) return $page; } } $defaults = array( 'findOne' => true, // find only one page 'findAll' => true, // no exclusions 'getTotal' => false, // don't count totals 'caller' => 'pages.get' ); $options = count($options) ? array_merge($defaults, $options) : $defaults; $page = $this->pages->find($selector, $options)->first(); 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 all columns * from the matching row in the pages table. * * @param string|int|array|Selectors $selector * @param bool $verbose Return verbose array with all pages columns rather than just page id? (default=false) * @param array $options Additional options to pass in find() $options argument (not currently applicable) * @return array|int * @since 3.0.153 * */ public function has($selector, $verbose = false, array $options = array()) { $defaults = array( 'findOne' => true, // find only one page 'findAll' => true, // no exclusions 'findIDs' => $verbose ? 2 : 1, // 2=all cols, 1=IDs only 'getTotal' => false, // don't count totals 'caller' => 'pages.has', ); $options = count($options) ? array_merge($defaults, $options) : $defaults; 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 * * Optionally specify an $options array rather than a template for argument 2. When present, the 'template' and 'parent_id' arguments may be provided * in the given $options array. These options may be specified: * * LOAD OPTIONS (argument 2 array): * - cache: boolean, default=true. place loaded pages in memory cache? * - getFromCache: boolean, default=true. Allow use of previously cached pages in memory (rather than re-loading it from DB)? * - template: instance of Template (see $template argument) * - parent_id: integer (see $parent_id argument) * - getNumChildren: boolean, default=true. Specify false to disable retrieval and population of 'numChildren' Page property. * - getOne: boolean, default=false. Specify true to return just one Page object, rather than a PageArray. * - autojoin: boolean, default=true. Allow use of autojoin option? * - joinFields: array, default=empty. Autojoin the field names specified in this array, regardless of field settings (requires autojoin=true). * - joinSortfield: boolean, default=true. Whether the 'sortfield' property will be joined to the page. * - findTemplates: boolean, default=true. Determine which templates will be used (when no template specified) for more specific autojoins. * - pageClass: string, default=auto-detect. Class to instantiate Page objects with. Leave blank to determine from template. * - pageArrayClass: string, default=PageArray. PageArray-derived class to store pages in (when 'getOne' is false). * - pageArray: PageArray, default=null. Optional predefined PageArray to populate to. * - page (Page|null): Existing Page object to populate (also requires the getOne option to be true). (default=null) * - caller (string): Name of calling function, for debugging purposes (default=blank). * * Use the $options array for potential speed optimizations: * - Specify a 'template' with your call, when possible, so that this method doesn't have to determine it separately. * - Specify false for 'getNumChildren' for potential speed optimization when you know for certain pages will not have children. * - Specify false for 'autojoin' for potential speed optimization in certain scenarios (can also be a bottleneck, so be sure to test). * - Specify false for 'joinSortfield' for potential speed optimization when you know the Page will not have children or won't need to know the order. * - Specify false for 'findTemplates' so this method doesn't have to look them up. Potential speed optimization if you have few autojoin fields globally. * - Note that if you specify false for 'findTemplates' the pageClass is assumed to be 'Page' unless you specify something different for the 'pageClass' option. * * @param array|WireArray|string|int $_ids Array of page IDs, comma or pipe-separated string of IDs, or single page ID (string or int) * or in 3.0.156+ array of associative arrays where each in format: [ 'id' => 123, 'templates_id' => 456 ] * @param Template|array|string|int|null $template Specify a template to make the load faster, because it won't have to attempt to join all possible fields... * just those used by the template. Optionally specify an $options array instead, see the method notes above. * @param int|null $parent_id Specify a parent to make the load faster, as it reduces the possibility for full table scans. * This argument is ignored when an options array is supplied for the $template. * @return PageArray|Page|NullPage Returns Page only if the 'getOne' option is specified, otherwise always returns a PageArray. * @throws WireException * */ public function getById($_ids, $template = null, $parent_id = null) { $options = array( 'cache' => true, 'getFromCache' => true, 'template' => null, 'parent_id' => null, 'getNumChildren' => true, 'getOne' => false, 'autojoin' => true, 'findTemplates' => true, 'joinSortfield' => true, 'joinFields' => array(), 'page' => null, 'pageClass' => '', // blank = auto detect 'pageArray' => null, // PageArray to populate to 'pageArrayClass' => 'PageArray', 'caller' => '', ); $templates = $this->wire()->templates; $database = $this->wire()->database; $idsByTemplate = array(); $loading = $this->loading; if(is_array($template)) { // $template property specifies an array of options $options = array_merge($options, $template); $template = $options['template']; $parent_id = $options['parent_id']; if("$options[cache]" === "1") $options['cache'] = true; } else if(!is_null($template) && !$template instanceof Template) { throw new WireException('getById argument 2 must be Template or $options array'); } if(!is_null($parent_id) && !is_int($parent_id)) { // convert Page object or string to integer id $parent_id = (int) ((string) $parent_id); } if(!is_null($template) && !is_object($template)) { // convert template string or id to Template object $template = $templates->get($template); } if(is_string($_ids)) { // convert string of IDs to array $_ids = trim($_ids, '|, '); if(ctype_digit($_ids)) { $_ids = array((int) $_ids); // single ID: "123" } else if(strpos($_ids, '|')) { $_ids = explode('|', $_ids); // pipe-separated IDs: "123|456|789" } else if(strpos($_ids, ',')) { $_ids = explode(',', $_ids); // comma-separated IDs: "123,456,789" } else { $_ids = array(); // unrecognized ID string: fail } } else if(is_int($_ids)) { $_ids = array($_ids); } if(!WireArray::iterable($_ids) || !count($_ids)) { // return blank if $_ids isn't iterable or is empty return $options['getOne'] ? $this->pages->newNullPage() : $this->pages->newPageArray($options); } if(is_object($_ids)) $_ids = $_ids->getArray(); // ArrayObject or the like $loaded = array(); // array of id => Page objects that have been loaded $ids = array(); // sanitized version of $_ids // sanitize ids and determine which pages we can pull from cache foreach($_ids as $key => $id) { if(!is_int($id)) { if(is_array($id)) { if(!isset($id['id'])) continue; $tid = isset($id['templates_id']) ? (int) $id['templates_id'] : 0; $id = (int) $id['id']; if($tid) { if(!isset($idsByTemplate[$tid])) $idsByTemplate[$tid] = array(); $idsByTemplate[$tid][] = $id; } } else { $id = trim($id); if(!ctype_digit($id)) continue; $id = (int) $id; } } if($id < 1) continue; $key = (int) $key; if($options['getOne'] && is_object($options['page'])) { // single page that will be populated directly $loaded[$id] = ''; $ids[$key] = $id; } else if($options['getFromCache'] && $page = $this->pages->getCache($id)) { // page is already available in the cache if($template && $page->template->id != $template->id) { // do not load: does not match specified template } else if($parent_id && $page->parent_id != $parent_id) { // do not load: does not match specified parent_id } else { $loaded[$id] = $page; } } else if(isset(Page::$loadingStack[$id])) { // if the page is already in the process of being loaded, point to it rather than attempting to load again. // the point of this is to avoid a possible infinite loop with autojoin fields referencing each other. $p = Page::$loadingStack[$id]; if($p) { $loaded[$id] = $p; // cache the pre-loaded version so that other pages referencing it point to this instance rather than loading again $this->pages->cache($loaded[$id]); } } else { $loaded[$id] = ''; // reserve the spot, in this order $ids[$key] = $id; // queue id to be loaded } } $idCnt = count($ids); // idCnt contains quantity of remaining page ids to load if(!$idCnt) { // if there are no more pages left to load, we can return what we've got if($options['getOne']) { $page = count($loaded) ? reset($loaded) : null; return $page instanceof Page ? $page : $this->pages->newNullPage(); } $pages = $this->pages->newPageArray($options); $pages->setDuplicateChecking(false); $pages->import($loaded); $pages->setDuplicateChecking(true); return $pages; } if(!$loading) $this->loading = true; if(count($idsByTemplate)) { // ok } else if($template === null && $options['findTemplates']) { // template was not defined with the function call, so we determine // which templates are used by each of the pages we have to load $sql = 'SELECT id, templates_id FROM pages'; if($idCnt == 1) { $query = $database->prepare("$sql WHERE id=:id"); $query->bindValue(':id', (int) reset($ids), \PDO::PARAM_INT); } else { $ids = array_map('intval', $ids); $sql = "$sql WHERE id IN(" . implode(',', $ids) . ")"; $query = $database->prepare($sql); } $result = $database->execute($query); if($result) { /** @noinspection PhpAssignmentInConditionInspection */ while($row = $query->fetch(\PDO::FETCH_NUM)) { list($id, $templates_id) = $row; $id = (int) $id; $templates_id = (int) $templates_id; if(!isset($idsByTemplate[$templates_id])) $idsByTemplate[$templates_id] = array(); $idsByTemplate[$templates_id][] = $id; } } $query->closeCursor(); } else if($template === null) { // no template provided, and autojoin not needed (so we don't need to know template) $idsByTemplate = array(0 => $ids); } else { // template was provided $idsByTemplate = array($template->id => $ids); } foreach($idsByTemplate as $templates_id => $ids) { if($templates_id && (!$template || $template->id != $templates_id)) { $template = $templates->get($templates_id); } if($template) { $fields = $template->fieldgroup; } else { $fields = $this->wire()->fields; } /** @var DatabaseQuerySelect $query */ $query = $this->wire(new DatabaseQuerySelect()); $sortfield = $template ? $template->sortfield : ''; $joinSortfield = empty($sortfield) && $options['joinSortfield']; // note that "false AS isLoaded" triggers the setIsLoaded() function in Page intentionally $select = 'false AS isLoaded, pages.templates_id AS templates_id, pages.*, '; if($joinSortfield) { $select .= 'pages_sortfields.sortfield, '; } if($options['getNumChildren']) { $select .= "\n(SELECT COUNT(*) FROM pages AS children WHERE children.parent_id=pages.id) AS numChildren"; } $query->select(rtrim($select, ', ')); $query->from('pages'); if($joinSortfield) $query->leftjoin('pages_sortfields ON pages_sortfields.pages_id=pages.id'); if($options['autojoin'] && $this->autojoin) { foreach($fields as $field) { /** @var Field $field */ if(!empty($options['joinFields']) && in_array($field->name, $options['joinFields'])) { // joinFields option specified to force autojoin this field } else { // check if autojoin not enabled for field if(!($field->flags & Field::flagAutojoin)) continue; // non-fieldgroup, autojoin only if global flag is set if($fields instanceof Fields && !($field->flags & Field::flagGlobal)) continue; } $table = $database->escapeTable($field->table); // check autojoin not allowed, otherwise merge in the autojoin query $fieldtype = $field->type; if(!$fieldtype || !$fieldtype->getLoadQueryAutojoin($field, $query)) continue; // complete autojoin $query->leftjoin("$table ON $table.pages_id=pages.id"); // QA } } if(count($ids) > 1) { $ids = array_map('intval', $ids); $query->where('pages.id IN(' . implode(',', $ids) . ')'); } else { $id = reset($ids); $query->where('pages.id=:id'); $query->bindValue(':id', (int) $id, \PDO::PARAM_INT); } if(!is_null($parent_id)) { $query->where('pages.parent_id=:parent_id'); $query->bindValue(':parent_id', (int) $parent_id, \PDO::PARAM_INT); } if($template) { $query->where('pages.templates_id=:templates_id'); $query->bindValue(':templates_id', (int) $template->id, \PDO::PARAM_INT); } $query->groupby('pages.id'); $stmt = $query->prepare(); $database->execute($stmt); $class = $options['pageClass']; if(empty($class)) $class = $template ? $template->getPageClass() : __NAMESPACE__ . "\\Page"; // page to populate, if provided in 'getOne' mode /** @var Page|null $_page */ $_page = $options['getOne'] && $options['page'] instanceof Page ? $options['page'] : null; try { // while($page = $stmt->fetchObject($_class, array($template))) { /** @noinspection PhpAssignmentInConditionInspection */ while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { if($_page) { // populate provided Page object $page = $_page; $page->set('template', $template ? $template : (int) $row['templates_id']); if(!$page->get('parent_id')) $page->set('parent_id', (int) $row['parent_id']); } else { // create new Page object $pageTemplate = $template ? $template : $templates->get((int) $row['templates_id']); $pageClass = empty($options['pageClass']) && $pageTemplate ? $pageTemplate->getPageClass() : $class; $page = $this->pages->newPage(array( 'pageClass' => $pageClass, 'template' => $pageTemplate ? $pageTemplate : $row['templates_id'], 'parent' => $row['parent_id'], )); } unset($row['templates_id'], $row['parent_id']); $page->loaderCache = $options['cache']; foreach($row as $key => $value) $page->set($key, $value); $page->instanceID = ++self::$pageInstanceID; $page->setIsLoaded(true); $page->setIsNew(false); $page->resetTrackChanges(true); $page->setOutputFormatting($this->outputFormatting); $loaded[$page->id] = $page; if($options['cache'] === true) { $this->pages->cache($page); } else if($options['cache']) { $this->pages->cacher()->cacheGroup($page, $options['cache']); } $this->totalPagesLoaded++; } } catch(\Exception $e) { $error = $e->getMessage() . " [pageClass=$class, template=$template]"; $user = $this->wire()->user; if($user && $user->isSuperuser()) $this->error($error); $this->wire()->log->error($error); $this->trackException($e, false); } $stmt->closeCursor(); $template = null; } if($options['getOne']) { if(!$loading) $this->loading = false; $page = count($loaded) ? reset($loaded) : null; return $page instanceof Page ? $page : $this->pages->newNullPage(); } $pages = $this->pages->newPageArray($options); $pages->setDuplicateChecking(false); $pages->import($loaded); $pages->setDuplicateChecking(true); if(!$loading) $this->loading = false; // debug mode only if($this->debug) { $page = $this->wire()->page; if($page && $page->template == 'admin') { if(empty($options['caller'])) { $_template = is_null($template) ? '' : ", $template"; $_parent_id = is_null($parent_id) ? '' : ", $parent_id"; if(count($_ids) > 10) { $_ids = '[' . reset($_ids) . '…' . end($_ids) . ', ' . count($_ids) . ' pages]'; } else { $_ids = count($_ids) > 1 ? "[" . implode(',', $_ids) . "]" : implode('', $_ids); } $options['caller'] = "pages.getById($_ids$_template$_parent_id)"; } foreach($pages as $item) { $item->setQuietly('_debug_loader', $options['caller']); } } } return $pages; } /** * Find page(s) by name * * This method is optimized just for finding pages by name and it does * not perform any filtering or access checking. * * @param string $name Match this page name * @param array $options * - `parent' (int|Page): Match this parent ID (default=0) * - `parentName` (string): Match this parent name (default='') * - `getArray` (bool): Get PHP info array rather than Page|NullPage|PageArray? (default=false) * - `getOne` (bool|int): Get just one match of Page or NullPage? (default=false) * When true, if multiple pages match then NullPage will be returned. To instead return * the first match, specify int `1` instead of boolean true. * @return array|NullPage|Page|PageArray * */ public function findByName($name, array $options = array()) { $defaults = array( 'parent' => 0, 'parentName' => '', 'getArray' => false, 'getOne' => false, ); $options = array_merge($defaults, $options); $getArray = $options['getArray']; $getOne = $options['getOne']; $blankRow = array( 'id' => 0, 'templates_id' => 0, 'parent_id' => 0, ); $joins = array(); $selects = array( 'pages.id', 'pages.parent_id', 'pages.templates_id', ); $wheres = array( 'pages.name=:name', ); $binds = array( 'name' => $name, ); if($options['parent']) { $wheres[] = 'pages.parent_id=:parentId'; $binds['parentId'] = (int) "$options[parent]"; } if($options['parentName']) { $joins[] = 'JOIN pages AS parent ON pages.parent_id=parent.id AND parent.name=:parentName'; $binds['parentName'] = $options['parentName']; } $sql = 'SELECT ' . implode(', ', $selects) . ' ' . 'FROM pages ' . implode(' ', $joins) . ' ' . 'WHERE ' . implode(' AND ', $wheres) . ' '; if($getOne) $sql .= 'LIMIT 2'; $query = $this->wire()->database->prepare($sql); foreach($binds as $bindKey => $bindValue) { $query->bindValue(":$bindKey", $bindValue); } $query->execute(); $rowCount = (int) $query->rowCount(); $rows = array(); while($row = $query->fetch(\PDO::FETCH_ASSOC)) { $rows[] = $row; } $query->closeCursor(); if($getOne === 1 && $rowCount > 1) { // multiple rows found but only first one requested $rowCount = 1; } if($rowCount === 0) { // no rows matched if($getOne) { return $getArray ? $blankRow : $this->pages->newNullPage(); } else { return $getArray ? array() : $this->pages->newPageArray(); } } else if($rowCount === 1) { // one row matched if($getOne) { return $getArray ? reset($rows) : $this->pages->getByIDs($rows, array('getOne' => true)); } else { return $getArray ? $rows : $this->pages->getByIDs($rows); } } else { // multiple rows matched if($getOne) { // return blank (multiple not allowed here) return $getArray ? $blankRow : $this->pages->newNullPage(); } else { // return all return $getArray ? $rows : $this->pages->getByIDs($rows); } } } /** * Given an ID return a path to a page, without loading the actual page * * Please note * =========== * 1) Always returns path in default language, unless a language argument/option is specified. * 2) Path may be different from 'url' as it doesn't include $config->urls->root at the beginning. * 3) In most cases, it's preferable to use $page->path() rather than this method. This method is * here just for cases where a path is needed without loading the page. * 4) It's possible for there to be Page::path() hooks, and this method completely bypasses them, * which is another reason not to use it unless you know such hooks aren't applicable to you. * * @param int|Page $id ID of the page you want the path to * @param null|array|Language|int|string $options Specify $options array or Language object, id or name. Allowed options: * - language (int|string|anguage): To retrieve in non-default language, specify language object, ID or name (default=null) * - useCache (bool): Allow pulling paths from already loaded pages? (default=true) * - usePagePaths (bool): Allow pulling paths from PagePaths module, if installed? (default=true) * @return string Path to page or blank on error/not-found * */ public function getPath($id, $options = array()) { $modules = $this->wire()->modules; $database = $this->wire()->database; $languages = $this->wire()->languages; $config = $this->wire()->config; $defaults = array( 'language' => null, 'useCache' => true, 'usePagePaths' => true ); if(!is_array($options)) { // language was specified rather than $options $defaults['language'] = $options; $options = array(); } $options = array_merge($defaults, $options); if($id instanceof Page) { if($options['useCache']) return $id->path(); $id = $id->id; } $id = (int) $id; if(!$id || $id < 0) return ''; if($languages && !$languages->hasPageNames()) $languages = null; $language = $options['language']; $languageID = 0; $homepageID = (int) $config->rootPageID; if(!empty($language) && $languages) { if(is_string($language) || is_int($language)) $language = $languages->get($language); if(!$language->isDefault()) $languageID = (int) $language->id; } // if page is already loaded and cache allowed, then get the path from it if($options['useCache'] && $page = $this->pages->getCache($id)) { /** @var Page $page */ if($languageID) $languages->setLanguage($language); $path = $page->path(); if($languageID) $languages->unsetLanguage(); return $path; } else if($id === $homepageID && $languages && !$languageID) { // default language in multi-language environment, let $page handle it since there is additional // hooked logic there provided by LanguageSupportPageNames $page = $this->pages->get($homepageID); $languages->setDefault(); $path = $page->path(); $languages->unsetDefault(); return $path; } // if PagePaths module is installed, and not in multi-language environment, attempt to get from PagePaths module if(!$languages && !$languageID && $options['usePagePaths'] && $modules->isInstalled('PagePaths')) { /** @var PagePaths $pagePaths */ $pagePaths = $modules->get('PagePaths'); $path = $pagePaths->getPath($id); if($path) return $path; } $path = ''; $templatesID = 0; $parentID = $id; $maxParentID = $language ? 0 : 1; $cols = 'parent_id, templates_id, name'; if($languageID) $cols .= ", name$languageID"; // col=3 $query = $database->prepare("SELECT $cols FROM pages WHERE id=:parent_id"); do { $query->bindValue(":parent_id", (int) $parentID, \PDO::PARAM_INT); $database->execute($query); $row = $query->fetch(\PDO::FETCH_NUM); if(!$row) { $path = ''; break; } $parentID = (int) $row[0]; $templatesID = (int) $row[1]; $name = empty($row[3]) ? $row[2] : $row[3]; if($parentID) { // non-homepage $path = $name . '/' . $path; } else { // homepage if($name !== Pages::defaultRootName && !empty($name)) { $path = $name . '/' . $path; } } } while($parentID > $maxParentID); if(!strlen($path) || $path === '/') return $path; $path = trim($path, '/'); if($templatesID) { $template = $this->wire()->templates->get($templatesID); if($template->slashUrls) $path .= '/'; } return '/' . ltrim($path, '/'); } /** * Get a page by its path, similar to $pages->get('/path/to/page/') but with more options * * Please note * =========== * 1) There are no exclusions for page status or access. If needed, you should validate access * on any page returned from this method. * 2) In a multi-language environment, you must specify the $useLanguages option to be true, if you * want a result for a $path that is (or might be) a multi-language path. Otherwise, multi-language * paths will make this method return a NullPage (or 0 if getID option is true). * 3) Partial paths may also match, so long as the partial path is completely unique in the site. * If you don't want that behavior, double check the path of the returned page. * 4) See also the newer/more capable `$pages->pathFinder()` methods `get('/path/')` and `getPage('/path/')`. * * @param string $path * @param array|bool $options array of options (below), or specify boolean for $useLanguages option only. * - `getID` (bool): Specify true to just return the page ID (default=false) * - `useLanguages` (bool): Specify true to allow retrieval by language-specific paths (default=false) * - `useHistory` (bool): Allow use of previous paths used by the page, if PagePathHistory module is installed (default=false) * - `allowUrl` (bool): Allow getting page by path OR url? Specify false to find only by path. This option only applies if * the site happens to run from a subdirectory. (default=true) 3.0.184+ * - `allowPartial` (bool): Allow partial paths to match? (default=true) 3.0.184+ * - `allowUrlSegments` (bool): Allow paths with URL segments to match? When true and page match cannot be found, the closest * parent page that allows URL segments will be returned. Found URL segments are populated to a `_urlSegments` array * property on the returned page object. This also cancels the allowPartial setting. (default=false) 3.0.184+ * @return Page|int * @see PagesPathFinder::get(), PagesPathFinder::getPage() * */ public function getByPath($path, $options = array()) { $modules = $this->wire()->modules; $sanitizer = $this->wire()->sanitizer; $config = $this->wire()->config; $database = $this->wire()->database; $defaults = array( 'getID' => false, 'useLanguages' => false, 'useHistory' => false, 'allowUrl' => true, 'allowPartial' => true, 'allowUrlSegments' => false, '_isRecursive' => false, ); if(!is_array($options)) { $defaults['useLanguages'] = (bool) $options; $options = array(); } $options = array_merge($defaults, $options); if(isset($options['getId'])) $options['getID'] = $options['getId']; // case alternate $homepageID = (int) $config->rootPageID; $rootUrl = $this->wire()->config->urls->root; if($options['allowUrl'] && $rootUrl !== '/' && strpos($path, $rootUrl) === 0) { // root URL is subdirectory and path has that subdirectory $rootName = trim($rootUrl, '/'); if(strpos($rootName, '/')) { // root URL has multiple levels of subdirectories, remove them from path list(,$path) = explode(rtrim($rootUrl, '/'), $path, 2); } else { // one subdirectory, see if a page has the same name $query = $database->prepare('SELECT id FROM pages WHERE parent_id=1 AND name=:name'); $query->bindValue(':name', $rootName); $query->execute(); if($query->rowCount() > 0) { // leave subdirectory in path because page in site also matches subdirectory name } else { // remove root URL subdirectory from path list(,$path) = explode(rtrim($rootUrl, '/'), $path, 2); } $query->closeCursor(); } } if($path === '/') { // this can only be homepage return $options['getID'] ? $homepageID : $this->getById($homepageID, array('getOne' => true)); } else if(empty($path)) { // path is empty and cannot match anything return $options['getID'] ? 0 : $this->pages->newNullPage(); } $_path = $path; $path = $sanitizer->pagePathName($path, Sanitizer::toAscii); $pathParts = explode('/', trim($path, '/')); $_pathParts = $pathParts; $languages = $options['useLanguages'] ? $this->wire()->languages : null; if($languages && !$languages->hasPageNames()) $languages = null; $langKeys = array(':name' => 'name'); if($languages) { foreach($languages as $language) { if($language->isDefault()) continue; $languageID = (int) $language->id; $langKeys[":name$languageID"] = "name$languageID"; } } $pageID = 0; $templatesID = 0; $parentID = 0; if($options['allowPartial'] && !$options['allowUrlSegments']) { // first see if we can find a single page just having the name that's the last path part // this is an optimization if the page name happens to be globally unique in the system, which is often the case $name = end($pathParts); $binds = array(':name' => $name); $wheres = array(); $numParts = count($pathParts); // can match 'name' or 'name123' cols where 123 is language ID foreach($langKeys as $bindKey => $colName) { $wheres[] = "$colName=$bindKey"; $binds[$bindKey] = $name; } $sql = 'SELECT id, templates_id, parent_id FROM pages WHERE (' . implode(' OR ', $wheres) . ') '; if($numParts == 1) { $sql .= ' AND (parent_id=:parent_id '; $binds[':parent_id'] = $homepageID; if($languages) { $sql .= 'OR id=:homepage_id '; $binds[':homepage_id'] = $homepageID; } $sql .= ') '; } $sql .= 'LIMIT 2'; $query = $database->prepare($sql); foreach($binds as $key => $value) $query->bindValue($key, $value); $database->execute($query); $numRows = $query->rowCount(); if($numRows == 1) { // if only 1 page matches then we’ve found what we’re looking for list($pageID, $templatesID, $parentID) = $query->fetch(\PDO::FETCH_NUM); } else if($numRows == 0) { // no page can possibly match last segment } else if($numRows > 1) { // multiple pages match } $query->closeCursor(); } if(!$pageID) { // multiple pages have the name or partial path match is not allowed // build a query joining all the path parts $joins = array(); $wheres = array(); $binds = array(); $n = 0; $lastAlias = "pages"; $lastPart = array_pop($pathParts); while(count($pathParts)) { $n++; $alias = "_pages$n"; $part = array_pop($pathParts); $whereORs = array(); foreach($langKeys as $bindKey => $colName) { $bindKey .= "_$n"; $whereORs[] = "$alias.$colName=$bindKey"; $binds[$bindKey] = $part; } $where = '(' . implode(' OR ', $whereORs) . ')'; $joins[] = "\nJOIN pages AS $alias ON $lastAlias.parent_id=$alias.id AND $where"; //$wheres[] = $where; // appears to be redundant as where only needed in join $lastAlias = $alias; } $isRootParent = !$n; // there were no pathParts, so we are matching just a rootParent if($isRootParent) $wheres[] = "pages.parent_id=1"; $whereORs = array(); foreach($langKeys as $bindKey => $colName) { $whereORs[] = "pages.$colName=$bindKey"; $binds[$bindKey] = $lastPart; } $wheres[] = '(' . implode(' OR ', $whereORs) . ')'; $sql = 'SELECT pages.id, pages.templates_id, pages.parent_id, pages.name ' . 'FROM pages ' . implode(' ', $joins) . " \n" . 'WHERE (' . implode(' AND ', $wheres) . ') '; $query = $database->prepare($sql); foreach($binds as $key => $value) $query->bindValue($key, $value); $database->execute($query); $rowCount = $query->rowCount(); if($rowCount === 1) { // just one page matched $row = $query->fetch(\PDO::FETCH_NUM); list($pageID, $templatesID, $parentID, ) = $row; } else if($rowCount > 1 && $isRootParent) { // multiple pages matched off root // use either 'default' language match or first matching language $rows = array(); while($row = $query->fetch(\PDO::FETCH_ASSOC)) { $rows[] = $row; if($row['name'] !== $lastPart) continue; $rows = array($row); // force use of only this row (default language) break; } $row = reset($rows); list($pageID, $templatesID, $parentID) = array($row['id'], $row['templates_id'], $row['parent_id']); } else if($rowCount > 1) { // multiple pages matched somewhere in site, we need a stronger tool (pagesPathFinder) $pathFinder = $this->pages->pathFinder(); $info = $pathFinder->get($_path, array( 'useLanguages' => $options['useLanguages'], 'useHistory' => $options['useHistory'], )); if(!empty($info['page']['id'])) { // pathFinder found a match if(count($info['urlSegments']) && !$options['allowUrlSegments']) { // found URL segments and they weren't allowed by options } else { $pageID = $info['page']['id']; $templatesID = $info['page']['templates_id']; $parentID = $info['page']['parent_id']; } } } else if($isRootParent) { // no page matches possible, maybe a URL segment for homepage? } else { // no match found yet } $query->closeCursor(); } if(!$pageID && $options['useHistory'] && $modules->isInstalled('PagePathHistory')) { // if finding failed, check if there is a previous path it lived at, if history module available $pph = $modules->get('PagePathHistory'); /** @var PagePathHistory $pph */ $page = $pph->getPage($sanitizer->pagePathNameUTF8($_path)); if($page->id) return $options['getID'] ? $page->id : $page; } if(!$pageID && $options['allowUrlSegments'] && !$options['_isRecursive'] && count($_pathParts)) { // attempt to match parent pages that allow URL segments $pathParts = $_pathParts; $urlSegments = array(); $recursiveOptions = array_merge($options, array( 'getID' => false, 'allowUrlSegments' => false, 'allowPartial' => false, '_isRecursive' => true )); do { $urlSegment = array_pop($pathParts); array_unshift($urlSegments, $urlSegment); $path = '/' . implode('/', $pathParts); $page = $this->getByPath($path, $recursiveOptions); } while(count($pathParts) && !$page->id); if($page->id) { if($page->template->urlSegments) { // matched page template allows URL segments $page->setQuietly('_urlSegments', $urlSegments); if(!$options['getID']) return $page; $pageID = $page->id; } else { // page template does not allow URL segments, so path cannot match $pageID = 0; } } } if($options['getID']) return (int) $pageID; if(!$pageID) return $this->pages->newNullPage(); return $this->getById((int) $pageID, array( 'template' => $templatesID ? $this->wire()->templates->get((int) $templatesID) : null, 'parent_id' => (int) $parentID, 'getOne' => true )); } /** * Get a fresh, non-cached copy of a Page from the database * * This method is the same as `$pages->get()` except that it skips over all memory caches when loading a Page. * Meaning, if the Page is already in memory, it doesn’t use the one in memory and instead reloads from the DB. * Nor does it place the Page it loads in any memory cache. Use this method to load a fresh copy of a page * that you might need to compare to an existing loaded copy, or to load a copy that won’t be seen or touched * by anything in ProcessWire other than your own code. * * ~~~~~ * $p1 = $pages->get(1234); * $p2 = $pages->get($p1->path); * $p1 === $p2; // true: same Page instance * * $p3 = $pages->getFresh($p1); * $p1 === $p3; // false: same Page but different instance * ~~~~~ * * #pw-advanced * * @param Page|string|array|Selectors|int $selectorOrPage Specify Page to get copy of, selector or ID * @param array $options Options to modify behavior * @return Page|NullPage * @since 3.0.172 * */ public function getFresh($selectorOrPage, $options = array()) { if(!isset($options['cache'])) $options['cache'] = false; if(!isset($options['loadOptions'])) $options['loadOptions'] = array(); if(!isset($options['caller'])) $options['caller'] = 'pages.loader.getFresh'; $options['loadOptions']['getFromCache'] = false; if(!isset($options['loadOptions']['cache'])) $options['loadOptions']['cache'] = false; $selector = $selectorOrPage instanceof Page ? $selectorOrPage->id : $selectorOrPage; return $this->get($selector, $options); } /** * Load total number of children from DB for given page * * @param int|Page $page Page or Page ID * @return int * @throws WireException * @since 3.0.172 * */ public function getNumChildren($page) { $pageId = $page instanceof Page ? $page->id : (int) $page; $sql = 'SELECT COUNT(*) FROM pages WHERE parent_id=:id'; $query = $this->wire()->database->prepare($sql); $query->bindValue(':id', $pageId, \PDO::PARAM_INT); $query->execute(); $numChildren = (int) $query->fetchColumn(); $query->closeCursor(); return $numChildren; } /** * Count and return how many pages will match the given selector string * * @param string|array|Selectors $selector Specify selector, or omit to retrieve a site-wide count. * @param array|string $options See $options in Pages::find * @return int * */ public function count($selector = '', $options = array()) { if(is_string($options)) $options = Selectors::keyValueStringToArray($options); if(empty($selector)) { if(empty($options)) { // optimize away a simple site-wide total count $query = $this->wire()->database->query("SELECT COUNT(*) FROM pages"); $count = (int) $query->fetch(\PDO::FETCH_COLUMN); $query->closeCursor(); return (int) $count; } else { // no selector string, but options specified $selector = "id>0"; } } $options['loadPages'] = false; $options['getTotal'] = true; $options['caller'] = 'pages.count'; $options['returnVerbose'] = false; //if($this->wire('config')->debug) $options['getTotalType'] = 'count'; // test count method when in debug mode if(is_string($selector)) { $selector .= ", limit=1"; } else if(is_array($selector)) { $selector['limit'] = 1; } else if($selector instanceof Selectors) { $selector->add(new SelectorEqual('limit', 1)); } return $this->pages->find($selector, $options)->getTotal(); } /** * Remove pages from already-loaded PageArray aren't visible or accessible * * @param PageArray $items * @param string $includeMode Optional inclusion mode: * - 'hidden': Allow pages with 'hidden' status' * - 'unpublished': Allow pages with 'unpublished' or 'hidden' status * - 'all': Allow all pages (not much point in calling this method) * @param array $options loadOptions * @return PageArray * */ protected function filterListable(PageArray $items, $includeMode = '', array $options = array()) { if($includeMode === 'all') return $items; $itemsAllowed = $this->pages->newPageArray($options); foreach($items as $item) { if($includeMode === 'unpublished') { $allow = $item->status < Page::statusTrash; } else if($includeMode === 'hidden') { $allow = $item->status < Page::statusUnpublished; } else { $allow = $item->status < Page::statusHidden; } if($allow) $allow = $item->listable(); // confirm access if($allow) $itemsAllowed->add($item); } $itemsAllowed->resetTrackChanges(true); return $itemsAllowed; } /** * Returns an array of all columns native to the pages table * * @return array of column names, also indexed by column name * */ public function getNativeColumns() { if(empty($this->nativeColumns)) { $query = $this->wire()->database->prepare("SELECT * FROM pages WHERE id=:id"); $query->bindValue(':id', $this->wire()->config->rootPageID, \PDO::PARAM_INT); $query->execute(); $row = $query->fetch(\PDO::FETCH_ASSOC); foreach(array_keys($row) as $colName) { $this->nativeColumns[$colName] = $colName; } $query->closeCursor(); } return $this->nativeColumns; } /** * Get value of of a native column in pages table for given page ID * * @param int|Page $id Page ID * @param string $column * @return int|string|bool Returns int/string value on success or boolean false if no matching row * @since 3.0.156 * @throws \PDOException|WireException * */ public function getNativeColumnValue($id, $column) { $id = (is_object($id) ? (int) "$id" : (int) $id); if($id < 1) return false; $database = $this->wire()->database; if($database->escapeCol($column) !== $column) throw new WireException("Invalid column name: $column"); $query = $database->prepare("SELECT `$column` FROM pages WHERE id=:id"); $query->bindValue(':id', $id, \PDO::PARAM_INT); $query->execute(); $value = $query->fetchColumn(); $query->closeCursor(); if(ctype_digit("$value") && strpos($column, 'name') !== 0) $value = (int) $value; return $value; } /** * Is the given column name native to the pages table? * * @param $columnName * @return bool * */ public function isNativeColumn($columnName) { $nativeColumns = $this->getNativeColumns(); return isset($nativeColumns[$columnName]); } /** * Get or set debug state * * @param bool|null $debug * @return bool * */ public function debug($debug = null) { $value = $this->debug; if(!is_null($debug)) $this->debug = (bool) $debug; return $value; } /** * Return the total quantity of pages loaded by getById() * * @return int * */ public function getTotalPagesLoaded() { return $this->totalPagesLoaded; } /** * Get last used instance of PageFinder (for debugging purposes) * * @return PageFinder|null * @since 3.0.146 * */ public function getLastPageFinder() { return $this->lastPageFinder; } /** * Are we currently loading pages? * * @return bool * @since 3.0.195 * * */ public function isLoading() { return $this->loading; } }