/** * 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 $selectorString Specify selector string (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 and include pages with 'hidden' status (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). * 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 - optional name of calling function, for debugging purposes, i.e. pages.count * - include: string - Optional 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. * - loadOptions: array - Optional assoc array of options to pass to getById() load options. * @return PageArray * */ public function ___find($selectorString, $options = array()) { if (is_string($options)) { $options = Selectors::keyValueStringToArray($options); } if (is_array($selectorString)) { if (ctype_digit(implode('', array_keys($selectorString))) && ctype_digit(implode('', $selectorString))) { // if given a regular array of page IDs, we delegate that to getById() method, but with access/visibility control return $this->filterListable($this->getById($selectorString), isset($options['include']) ? $options['include'] : ''); } else { // some other type of array/values that we don't yet recognize // @todo add support for array selectors, per Selectors::arrayToSelectorString() return new PageArray(); } } $loadPages = true; $loadOptions = isset($options['loadOptions']) && is_array($options['loadOptions']) ? $options['loadOptions'] : array(); $debug = $this->wire('config')->debug; if (array_key_exists('loadPages', $options)) { $loadPages = (bool) $options['loadPages']; } if (!strlen($selectorString)) { return new PageArray(); } if ($selectorString === '/' || $selectorString === 'path=/') { $selectorString = 1; } if ($selectorString[0] == '/') { // if selector begins with a slash, then we'll assume it's referring to a path $selectorString = "path={$selectorString}"; } else { if (strpos($selectorString, ",") === false && strpos($selectorString, "|") === false) { // there is just one param. Lets see if we can find a shortcut. if (ctype_digit("{$selectorString}") || strpos($selectorString, "id=") === 0) { // if selector is just a number, or a string like "id=123" then we're going to do a shortcut $s = str_replace("id=", '', $selectorString); if (ctype_digit("{$s}")) { $value = $this->getById(array((int) $s), $loadOptions); if (empty($options['findOne'])) { $value = $this->filterListable($value, isset($options['include']) ? $options['include'] : ''); } if ($debug) { $this->debugLog('find', $selectorString . " [optimized]", $value); } return $value; } } } } if (isset($options['include']) && in_array($options['include'], array('hidden', 'unpublished', 'all'))) { $selectorString .= ", include={$options['include']}"; } // see if this has been cached and return it if so $pages = $this->getSelectorCache($selectorString, $options); if (!is_null($pages)) { if ($debug) { $this->debugLog('find', $selectorString, $pages . ' [from-cache]'); } return $pages; } // check if this find has already been executed, and return the cached results if so // if(null !== ($pages = $this->getSelectorCache($selectorString, $options))) return clone $pages; // if a specific parent wasn't requested, then we assume they don't want results with status >= Page::statusUnsearchable // if(strpos($selectorString, 'parent_id') === false) $selectorString .= ", status<" . Page::statusUnsearchable; $caller = isset($options['caller']) ? $options['caller'] : 'pages.find'; $selectors = new Selectors($selectorString); $pageFinder = $this->getPageFinder(); if ($debug) { Debug::timer("{$caller}({$selectorString})", true); } $pagesInfo = $pageFinder->find($selectors, $options); // 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 ($loadPages) { // parent_id is null unless a single parent was specified in the selectors $parent_id = $pageFinder->getParentID(); $idsSorted = array(); $idsByTemplate = array(); // organize the pages by template ID foreach ($pagesInfo as $page) { $tpl_id = $page['templates_id']; if (!isset($idsByTemplate[$tpl_id])) { $idsByTemplate[$tpl_id] = array(); } $idsByTemplate[$tpl_id][] = $page['id']; $idsSorted[] = $page['id']; } if (count($idsByTemplate) > 1) { // perform a load for each template, which results in unsorted pages $unsortedPages = new PageArray(); foreach ($idsByTemplate as $tpl_id => $ids) { $opt = $loadOptions; $opt['template'] = $this->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 = new PageArray(); 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 = new PageArray(); reset($idsByTemplate); $opt = $loadOptions; $opt['template'] = $this->templates->get(key($idsByTemplate)); $opt['parent_id'] = $parent_id; $pages->import($this->getById($idsSorted, $opt)); } } else { $pages = new PageArray(); } $pages->setTotal($total); $pages->setLimit($limit); $pages->setStart($start); $pages->setSelectors($selectors); $pages->setTrackChanges(true); if ($loadPages) { $this->selectorCache($selectorString, $options, $pages); } if ($this->config->debug) { $this->debugLog('find', $selectorString, $pages); } if ($debug) { $count = $pages->count(); $note = ($count == $total ? $count : $count . "/{$total}") . " page(s)"; if ($count) { $note .= ": " . $pages->first()->path; if ($count > 1) { $note .= " ... " . $pages->last()->path; } } Debug::saveTimer("{$caller}({$selectorString})", $note); } if ($this->hasHook('found()')) { $this->found($pages, array('pageFinder' => $pageFinder, 'pagesInfo' => $pagesInfo, 'options' => $options)); } return $pages; }
/** * Get the role pages that are part of this template * * This method returns a blank PageArray if roles haven't yet been loaded into the template. * If the roles have previously been loaded as an array, then this method converts that array to a PageArray and returns it. * * @return PageArray * */ public function getRoles() { if (is_null($this->_roles)) { return new PageArray(); } else { if ($this->_roles instanceof PageArray) { return $this->_roles; } else { if (is_array($this->_roles)) { $errors = array(); $roles = new PageArray(); if (count($this->_roles)) { $test = implode('0', $this->_roles); // test to see if it's all digits (IDs) if (ctype_digit("{$test}")) { $roles->import($this->pages->getById($this->_roles)); } else { // role names foreach ($this->_roles as $name) { $role = $this->wire('roles')->get($name); if ($role->id) { $roles->add($role); } else { $errors[] = $name; } } } } if (count($errors) && $this->useRoles) { $this->error("Unable to load role(s): " . implode(', ', $errors)); } $this->_roles = $roles; return $this->_roles; } else { return new PageArray(); } } } }
/** * Get the role pages that are part of this template * * This method returns a blank PageArray if roles haven't yet been loaded into the template. * If the roles have previously been loaded as an array, then this method converts that array to a PageArray and returns it. * * @return PageArray * */ protected function getRoles() { if (is_null($this->_roles)) { return new PageArray(); } else { if ($this->_roles instanceof PageArray) { return $this->_roles; } else { if (is_array($this->_roles)) { $roles = new PageArray(); if (count($this->_roles)) { $roles->import($this->pages->getById($this->_roles)); } $this->_roles = $roles; return $this->_roles; } else { return new PageArray(); } } } }
/** * Given an array or CSV string of Page IDs, return a PageArray * * @param array|WireArray|string $ids Array of IDs or CSV string of IDs * @param Template $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. * @param int $parent_id Specify a parent to make the load faster, as it reduces the possibility for full table scans * @return PageArray * */ public function getById($ids, Template $template = null, $parent_id = null) { static $instanceID = 0; $pages = new PageArray(); if (is_string($ids)) { $ids = explode(",", $ids); } if (!WireArray::iterable($ids) || !count($ids)) { return $pages; } if (is_object($ids)) { $ids = $ids->getArray(); } $loaded = array(); foreach ($ids as $key => $id) { $id = (int) $id; $ids[$key] = $id; if ($page = $this->getCache($id)) { $loaded[$id] = $page; unset($ids[$key]); } 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. $loaded[$id] = Page::$loadingStack[$id]; // cache the pre-loaded version so that other pages referencing it point to this instance rather than loading again $this->cache($loaded[$id]); unset($ids[$key]); } else { $loaded[$id] = ''; // reserve the spot, in this order } } } $idCnt = count($ids); if (!$idCnt) { return $pages->import($loaded); } $idsByTemplate = array(); if (is_null($template)) { $sql = "SELECT id, templates_id FROM pages WHERE "; if ($idCnt == 1) { $sql .= "id=" . (int) reset($ids); } else { $sql .= "id IN(" . implode(",", $ids) . ")"; } $result = $this->db->query($sql); if ($result && $result->num_rows) { while ($row = $result->fetch_row()) { list($id, $templates_id) = $row; if (!isset($idsByTemplate[$templates_id])) { $idsByTemplate[$templates_id] = array(); } $idsByTemplate[$templates_id][] = $id; } } $result->free(); } else { $idsByTemplate = array($template->id => $ids); } foreach ($idsByTemplate as $templates_id => $ids) { if (!$template || $template->id != $templates_id) { $template = $this->fuel('templates')->get($templates_id); } $fields = $template->fieldgroup; $query = new DatabaseQuerySelect(); $query->select("false AS isLoaded, pages.templates_id AS templates_id, pages.*, pages_sortfields.sortfield, " . "(SELECT COUNT(*) FROM pages AS children WHERE children.parent_id=pages.id) AS numChildren"); $query->leftjoin("pages_sortfields ON pages_sortfields.pages_id=pages.id"); $query->groupby("pages.id"); foreach ($fields as $field) { if (!($field->flags & Field::flagAutojoin)) { continue; } $table = $field->table; if (!$field->type->getLoadQueryAutojoin($field, $query)) { continue; } // autojoin not allowed $query->leftjoin("{$table} ON {$table}.pages_id=pages.id"); } if (!is_null($parent_id)) { $query->where("pages.parent_id=" . (int) $parent_id); } $query->where("pages.templates_id={$template->id}"); $query->where("pages.id IN(" . implode(',', $ids) . ") "); $query->from("pages"); if (!($result = $query->execute())) { throw new WireException($this->db->error); } $class = $template->pageClass && class_exists($template->pageClass) ? $template->pageClass : 'Page'; while ($page = $result->fetch_object($class, array($template))) { $page->instanceID = ++$instanceID; $page->setIsLoaded(true); $page->setIsNew(false); $page->setTrackChanges(true); $page->setOutputFormatting($this->outputFormatting); $loaded[$page->id] = $page; $this->cache($page); } $template = null; $result->free(); } return $pages->import($loaded); }
/** * Get the role pages that are part of this template * * This method returns a blank PageArray if roles haven't yet been loaded into the template. * If the roles have previously been loaded as an array, then this method converts that array to a PageArray and returns it. * * @param string $type Default is 'view', but you may also specify 'edit', 'create' or 'add' to retrieve those * @return PageArray * @throws WireException if given an unknown roles type * */ public function getRoles($type = 'view') { if (strpos($type, 'page-') === 0) { $type = str_replace('page-', '', $type); } if ($type != 'view') { $roles = new PageArray(); $roleIDs = null; if ($type == 'edit') { $roleIDs = $this->editRoles; } else { if ($type == 'create') { $roleIDs = $this->createRoles; } else { if ($type == 'add') { $roleIDs = $this->addRoles; } else { throw new WireException("Unknown roles type: {$type}"); } } } if (empty($roleIDs)) { return $roles; } return $this->wire('pages')->getById($roleIDs); } // type=view assumed from this point forward if (is_null($this->_roles)) { return new PageArray(); } else { if ($this->_roles instanceof PageArray) { return $this->_roles; } else { if (is_array($this->_roles)) { $errors = array(); $roles = new PageArray(); if (count($this->_roles)) { $test = implode('0', $this->_roles); // test to see if it's all digits (IDs) if (ctype_digit("{$test}")) { $roles->import($this->pages->getById($this->_roles)); } else { // role names foreach ($this->_roles as $name) { $role = $this->wire('roles')->get($name); if ($role->id) { $roles->add($role); } else { $errors[] = $name; } } } } if (count($errors) && $this->useRoles) { $this->error("Unable to load role(s): " . implode(', ', $errors)); } $this->_roles = $roles; return $this->_roles; } else { return new PageArray(); } } } }
/** * 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: * * - 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. * * @param array|WireArray|string $ids Array of IDs or CSV string of IDs * @param Template|array|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 * @throws WireException * */ public function getById($ids, $template = null, $parent_id = null) { $options = array('template' => null, 'parent_id' => null, 'getNumChildren' => true); if (is_array($template)) { // $template property specifies an array of options $options = array_merge($options, $template); $template = $options['template']; $parent_id = $options['parent_id']; } else { if (!is_null($template) && !$template instanceof Template) { throw new WireException('getById argument 2 must be Template or $options array'); } } static $instanceID = 0; $database = $this->wire('database'); $pages = new PageArray(); if (is_string($ids)) { $ids = explode(",", $ids); } if (!WireArray::iterable($ids) || !count($ids)) { return $pages; } if (is_object($ids)) { $ids = $ids->getArray(); } $loaded = array(); foreach ($ids as $key => $id) { $id = (int) $id; $ids[$key] = $id; if ($page = $this->getCache($id)) { $loaded[$id] = $page; unset($ids[$key]); } 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. $loaded[$id] = Page::$loadingStack[$id]; // cache the pre-loaded version so that other pages referencing it point to this instance rather than loading again $this->cache($loaded[$id]); unset($ids[$key]); } else { $loaded[$id] = ''; // reserve the spot, in this order } } } $idCnt = count($ids); if (!$idCnt) { return $pages->import($loaded); } $idsByTemplate = array(); if (is_null($template)) { $sql = "SELECT id, templates_id FROM pages WHERE "; if ($idCnt == 1) { $sql .= "id=" . (int) reset($ids); } else { $sql .= "id IN(" . implode(",", $ids) . ")"; } $query = $database->prepare($sql); $result = $query->execute(); if ($result) { while ($row = $query->fetch(PDO::FETCH_NUM)) { list($id, $templates_id) = $row; if (!isset($idsByTemplate[$templates_id])) { $idsByTemplate[$templates_id] = array(); } $idsByTemplate[$templates_id][] = $id; } } $query->closeCursor(); } else { $idsByTemplate = array($template->id => $ids); } foreach ($idsByTemplate as $templates_id => $ids) { if (!$template || $template->id != $templates_id) { $template = $this->wire('templates')->get($templates_id); } $fields = $template->fieldgroup; $query = new DatabaseQuerySelect(); $joinSortfield = empty($template->sortfield); $query->select("false AS isLoaded, pages.templates_id AS templates_id, pages.*, " . ($joinSortfield ? 'pages_sortfields.sortfield, ' : '') . ($options['getNumChildren'] ? '(SELECT COUNT(*) FROM pages AS children WHERE children.parent_id=pages.id) AS numChildren' : '')); if ($joinSortfield) { $query->leftjoin('pages_sortfields ON pages_sortfields.pages_id=pages.id'); } $query->groupby('pages.id'); foreach ($fields as $field) { if (!($field->flags & Field::flagAutojoin)) { continue; } $table = $database->escapeTable($field->table); if (!$field->type || !$field->type->getLoadQueryAutojoin($field, $query)) { continue; } // autojoin not allowed $query->leftjoin("{$table} ON {$table}.pages_id=pages.id"); // QA } if (!is_null($parent_id)) { $query->where("pages.parent_id=" . (int) $parent_id); } $query->where("pages.templates_id=" . (int) $template->id); // QA $query->where("pages.id IN(" . implode(',', $ids) . ") "); // QA $query->from("pages"); $stmt = $query->execute(); if ($stmt->errorCode() > 0) { $errorInfo = $result->errorInfo(); throw new WireException($errorInfo[2]); } $class = $template->pageClass && class_exists($template->pageClass) ? $template->pageClass : 'Page'; while ($page = $stmt->fetchObject($class, array($template))) { $page->instanceID = ++$instanceID; $page->setIsLoaded(true); $page->setIsNew(false); $page->setTrackChanges(true); $page->setOutputFormatting($this->outputFormatting); $loaded[$page->id] = $page; $this->cache($page); } $stmt->closeCursor(); $template = null; } return $pages->import($loaded); }
/** * Given an array or CSV string of Page IDs, return a PageArray * * @param array|WireArray|string $ids Array of IDs or CSV string of IDs * @param Template $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. * @param int $parent_id Specify a parent to make the load faster, as it reduces the possibility for full table scans * @return PageArray * */ public function getById($ids, Template $template = null, $parent_id = null) { static $instanceID = 0; $pages = new PageArray(); if (is_string($ids)) { $ids = explode(",", $ids); } if (!WireArray::iterable($ids) || !count($ids)) { return $pages; } if (is_object($ids)) { $ids = $ids->getArray(); } $loaded = array(); foreach ($ids as $key => $id) { $id = (int) $id; $ids[$key] = $id; if ($page = $this->getCache($id)) { $loaded[$id] = $page; unset($ids[$key]); } else { $loaded[$id] = ''; // reserve the spot, in this order } } $idCnt = count($ids); $fields = is_null($template) ? $this->fuel->fields : $template->fieldgroup; if ($idCnt) { // Optimization to only load the fields specific to the page, if just one page. // Without it, all autojoin fields have to be attempted, whether they are applicable to the pages loaded or not. // Even though this increases queries, it does result in a slight overall speed and memory improvement. if (is_null($template) && $idCnt == 1) { $result = $this->db->query("SELECT templates_id FROM pages WHERE id=" . (int) reset($ids)); if ($result) { list($tpl_id) = $result->fetch_row(); $template = $this->fuel('templates')->get($tpl_id); if ($template) { $fields = $template->fieldgroup; } $result->free(); } } $query = new DatabaseQuerySelect(); $query->select("false AS isLoaded, pages.templates_id AS templates_id, pages.*, pages_sortfields.sortfield, " . "(SELECT COUNT(*) FROM pages AS children WHERE children.parent_id=pages.id) AS numChildren"); $query->leftjoin("pages_sortfields ON pages_sortfields.pages_id=pages.id"); $query->groupby("pages.id"); foreach ($fields as $field) { if (!($field->flags & Field::flagAutojoin)) { continue; } //if($field->type instanceof FieldtypeMulti) continue; $table = $field->table; // if($field->type instanceof FieldtypeMulti) { // $sel .= "(SELECT COUNT(*) FROM $table WHERE $table.pages_id=pages.id) AS {$field->name}, "; // } else { if (!$field->type->getLoadQueryAutojoin($field, $query)) { continue; } // autojoin not allowed // $query->select("$table.data AS {$field->name}"); $query->leftjoin("{$table} ON {$table}.pages_id=pages.id"); } if (!is_null($parent_id)) { $query->where("pages.parent_id=" . (int) $parent_id); } if (!is_null($template)) { $query->where("pages.templates_id={$template->id}"); } $query->where("pages.id IN(" . implode(',', $ids) . ") "); $query->from("pages"); if (!($result = $query->execute())) { throw new WireException($this->db->error); } while ($page = $result->fetch_object('Page', array($template))) { $page->instanceID = ++$instanceID; $page->setIsLoaded(true); $page->setIsNew(false); $page->setTrackChanges(true); $page->setOutputFormatting($this->outputFormatting); $loaded[$page->id] = $page; $this->cache($page); } $result->free(); } $pages->import($loaded); return $pages; }