diff --git a/wire/core/Page.php b/wire/core/Page.php index ab11be3e..39f4b34a 100644 --- a/wire/core/Page.php +++ b/wire/core/Page.php @@ -8,7 +8,7 @@ * 1. Providing get/set access to the Page's properties * 2. Accessing the related hierarchy of pages (i.e. parents, children, sibling pages) * - * ProcessWire 3.x, Copyright 2019 by Ryan Cramer + * ProcessWire 3.x, Copyright 2021 by Ryan Cramer * https://processwire.com * * #pw-summary Class used by all Page objects in ProcessWire. @@ -318,34 +318,6 @@ class Page extends WireData implements \Countable, WireMatchable { */ const statusMax = 9999999; - /** - * Status string shortcuts, so that status can be specified as a word - * - * See also: self::getStatuses() method. - * - * @var array - * - */ - static protected $statuses = array( - 'reserved' => self::statusReserved, - 'locked' => self::statusLocked, - 'systemID' => self::statusSystemID, - 'system' => self::statusSystem, - 'unique' => self::statusUnique, - 'draft' => self::statusDraft, - 'flagged' => self::statusFlagged, - 'internal' => self::statusInternal, - 'temp' => self::statusTemp, - 'hidden' => self::statusHidden, - 'unpublished' => self::statusUnpublished, - 'trash' => self::statusTrash, - 'deleted' => self::statusDeleted, - 'systemOverride' => self::statusSystemOverride, - 'corrupted' => self::statusCorrupted, - 'max' => self::statusMax, - 'on' => self::statusOn, - ); - /** * The Template this page is using (object) * @@ -540,14 +512,6 @@ class Page extends WireData implements \Countable, WireMatchable { */ static public $loadingStack = array(); - /** - * Page class helper object instances (one of each helper per ProcessWire instance, lazy loaded) - * - * @var array - * - */ - static protected $helpers = array(); - /** * Controls the behavior of Page::__isset function (no longer in use) * @@ -567,14 +531,6 @@ class Page extends WireData implements \Countable, WireMatchable { */ protected $pageNum = 1; - /** - * Reference to main config, optimization so that get() method doesn't get called - * - * @var Config|null - * - */ - protected $config = null; - /** * When true, exceptions won't be thrown when values are set before templates * @@ -627,144 +583,6 @@ class Page extends WireData implements \Countable, WireMatchable { */ protected $_meta = null; - /** - * Properties that can be accessed, mapped to method of access (excluding custom fields of course) - * - * Keys are base property name, values are one of: - * - [methodName]: method name that it maps to ([methodName]=actual method name) - * - "s": property name is accessible in $this->settings using same key - * - "p": Property name maps to same property name in $this - * - "m": Property name maps to same method name in $this - * - "n": Property name maps to same method name in $this, but may be overridden by custom field - * - "t": Property name maps to PageTraversal method with same name, if not overridden by custom field - * - [blank]: needs additional logic to be handled ([blank]='') - * - * @var array - * - */ - static $baseProperties = array( - 'accessTemplate' => 'getAccessTemplate', - 'addable' => 'm', - 'child' => 'm', - 'children' => 'm', - 'created' => 's', - 'createdStr' => '', - 'createdUser' => '', - 'created_users_id' => 's', - 'deletable' => 'm', - 'deleteable' => 'm', - 'editable' => 'm', - 'editUrl' => 'm', - 'fieldgroup' => '', - 'filesManager' => 'm', - 'filesPath' => 'm', - 'filesUrl' => 'm', - 'hasChildren' => 'm', - 'hasFiles' => 'm', - 'hasFilesPath' => 'm', - 'hasLinks' => 't', - 'hasParent' => 'parents', - 'hasReferences' => 't', - 'httpUrl' => 'm', - 'id' => 's', - 'index' => 'n', - 'instanceID' => 'p', - 'isHidden' => 'm', - 'isLoaded' => 'm', - 'isLocked' => 'm', - 'isNew' => 'm', - 'isPublic' => 'm', - 'isTrash' => 'm', - 'isUnpublished' => 'm', - 'links' => 'n', - 'listable' => 'm', - 'modified' => 's', - 'modifiedStr' => '', - 'modifiedUser' => '', - 'modified_users_id' => 's', - 'moveable' => 'm', - 'name' => 's', - 'namePrevious' => 'p', - 'next' => 'm', - 'numChildren' => 's', - 'numParents' => 'm', - 'numDescendants' => 'm', - 'numLinks' => 't', - 'numReferences' => 't', - 'output' => 'm', - 'outputFormatting' => 'p', - 'parent' => 'm', - 'parent_id' => '', - 'parentPrevious' => 'p', - 'parents' => 'm', - 'path' => 'm', - 'prev' => 'm', - 'publishable' => 'm', - 'published' => 's', - 'publishedStr' => '', - 'quietMode' => 'p', - 'references' => 'n', - 'referencing' => 't', - 'render' => '', - 'rootParent' => 'm', - 'siblings' => 'm', - 'sort' => 's', - 'sortable' => 'm', - 'sortfield' => 's', - 'status' => 's', - 'statusPrevious' => 'p', - 'statusStr' => '', - 'template' => 'p', - 'templates_id' => '', - 'templatePrevious' => 'p', - 'trashable' => 'm', - 'url' => 'm', - 'urls' => 'm', - 'viewable' => 'm' - ); - - /** - * Alternate names accepted for base properties - * - * Keys are alternate property name and values are base property name - * - * @var array - * - */ - static $basePropertiesAlternates = array( - 'createdUserID' => 'created_users_id', - 'createdUsersID' => 'created_users_id', - 'created_user_id' => 'created_users_id', - 'editURL' => 'editUrl', - 'fields' => 'fieldgroup', - 'has_parent' => 'hasParent', - 'httpURL' => 'httpUrl', - 'modifiedUserID' => 'modified_users_id', - 'modifiedUsersID' => 'modified_users_id', - 'modified_user_id' => 'modified_users_id', - 'num_children' => 'numChildren', - 'numChildrenVisible' => 'hasChildren', - 'numVisibleChildren' => 'hasChildren', - 'of' => 'outputFormatting', - 'out' => 'output', - 'parentID' => 'parent_id', - 'subpages' => 'children', - 'template_id' => 'templates_id', - 'templateID' => 'templates_id', - 'templatesID' => 'templates_id', - ); - - /** - * Method alternates/aliases (alias => actual) - * - * @var array - * - */ - static $baseMethodAlternates = array( - 'descendants' => 'find', - 'descendant' => 'findOne', - ); - /** * Create a new page in memory. * @@ -849,12 +667,14 @@ class Page extends WireData implements \Countable, WireMatchable { */ public function set($key, $value) { - if(isset(self::$basePropertiesAlternates[$key])) $key = self::$basePropertiesAlternates[$key]; + if(isset(PageProperties::$basePropertiesAlternates[$key])) $key = PageProperties::$basePropertiesAlternates[$key]; - if(($key == 'id' || $key == 'name') && $this->settings[$key] && $value != $this->settings[$key]) - if( ($key == 'id' && (($this->settings['status'] & Page::statusSystem) || ($this->settings['status'] & Page::statusSystemID))) || - ($key == 'name' && (($this->settings['status'] & Page::statusSystem)))) { - throw new WireException("You may not modify '$key' on page '{$this->path}' because it is a system page"); + if($this->isLoaded && ($key === 'id' || $key === 'name') && $this->settings[$key] && $value != $this->settings[$key]) { + $sys = $this->settings['status'] & Page::statusSystem; + $sysID = $this->settings['status'] & Page::statusSystemID; + if(($key === 'id' && ($sys || $sysID)) || ($key === 'name' && $sys)) { + throw new WireException("You may not modify '$key' on page #$this->id ($this->path) because it is a system page"); + } } switch($key) { @@ -865,7 +685,7 @@ class Page extends WireData implements \Countable, WireMatchable { case 'sort': case 'numChildren': $value = (int) $value; - if($this->settings[$key] !== $value) $this->trackChange($key, $this->settings[$key], $value); + if($this->isLoaded && $this->settings[$key] !== $value) $this->trackChange($key, $this->settings[$key], $value); $this->settings[$key] = $value; break; case 'status': @@ -904,16 +724,16 @@ class Page extends WireData implements \Countable, WireMatchable { case 'created': case 'modified': case 'published': - if(is_null($value)) $value = 0; - if(!ctype_digit("$value")) $value = strtotime($value); + if($value === null) $value = 0; + if($value && !ctype_digit("$value")) $value = strtotime($value); $value = (int) $value; - if($this->settings[$key] !== $value) $this->trackChange($key, $this->settings[$key], $value); + if($this->isLoaded && $this->settings[$key] !== $value) $this->trackChange($key, $this->settings[$key], $value); $this->settings[$key] = $value; break; case 'created_users_id': case 'modified_users_id': $value = (int) $value; - if($this->settings[$key] !== $value) $this->trackChange($key, $this->settings[$key], $value); + if($this->isLoaded && $this->settings[$key] !== $value) $this->trackChange($key, $this->settings[$key], $value); $this->settings[$key] = $value; break; case 'createdUser': @@ -922,8 +742,8 @@ class Page extends WireData implements \Countable, WireMatchable { break; case 'sortfield': if($this->template && $this->template->sortfield) break; - $value = $this->wire('pages')->sortfields()->decode($value); - if($this->settings[$key] != $value) $this->trackChange($key, $this->settings[$key], $value); + $value = $this->wire()->pages->sortfields()->decode($value); + if($this->isLoaded && $this->settings[$key] != $value) $this->trackChange($key, $this->settings[$key], $value); $this->settings[$key] = $value; break; case 'isLoaded': @@ -942,9 +762,13 @@ class Page extends WireData implements \Countable, WireMatchable { $this->loaderCache = (bool) $value; break; default: - if(strpos($key, 'name') === 0 && ctype_digit(substr($key, 5)) && $this->wire('languages')) { - // i.e. name1234 - $this->setName($value, $key); + if(isset(PageProperties::$languageProperties[$key])) { + list($property, $languageId) = PageProperties::$languageProperties[$key]; + if($property === 'name') { + $this->setName($value, $languageId); // i.e. name1234 + } else if($property === 'status') { + parent::set($key, (int) $value); // i.e. status1234 + } } else { if($this->quietMode && !$this->template) return parent::set($key, $value); $this->setFieldValue($key, $value, $this->isLoaded); @@ -1027,7 +851,8 @@ class Page extends WireData implements \Countable, WireMatchable { } // check if the given key resolves to a Field or not - if(!$field = $this->getField($key)) { + $field = $this->getField($key); + if(!$field) { // not a known/saveable field, let them use it for runtime storage $valPrevious = parent::get($key); if($valPrevious !== null && is_null(parent::get("-$key")) && $valPrevious !== $value) { @@ -1038,7 +863,7 @@ class Page extends WireData implements \Countable, WireMatchable { } // if a null value is set, then ensure the proper blank type is set to the field - if(is_null($value)) { + if($value === null) { return parent::set($key, $field->type->getBlankValue($this, $field)); } @@ -1140,9 +965,11 @@ class Page extends WireData implements \Countable, WireMatchable { if(is_int($this->lazyLoad) && $this->lazyLoad && $key != 'id') $this->_lazy(true); if(is_array($key)) $key = implode('|', $key); - if(isset(self::$basePropertiesAlternates[$key])) $key = self::$basePropertiesAlternates[$key]; - if(isset(self::$baseProperties[$key])) { - $type = self::$baseProperties[$key]; + if(isset(PageProperties::$basePropertiesAlternates[$key])) { + $key = PageProperties::$basePropertiesAlternates[$key]; + } + if(isset(PageProperties::$baseProperties[$key])) { + $type = PageProperties::$baseProperties[$key]; if($type === 'p') { // local property return $this->$key; @@ -1501,7 +1328,7 @@ class Page extends WireData implements \Countable, WireMatchable { } } - if(!is_null($value) && empty($selector)) { + if($value !== null && empty($selector)) { // if the non-filtered value is already loaded, return it return $this->formatFieldValue($field, $value); } @@ -1524,7 +1351,7 @@ class Page extends WireData implements \Countable, WireMatchable { $value = $field->type->_callHookMethod('loadPageField', array($this, $field)); } - if(is_null($value)) { + if($value === null) { $value = $field->type->getDefaultValue($this, $field); } else { $value = $field->type->_callHookMethod('wakeupValue', array($this, $field, $value)); @@ -1934,8 +1761,8 @@ class Page extends WireData implements \Countable, WireMatchable { } else { return $this->get($method); } - } else if(isset(self::$baseMethodAlternates[$method])) { - return call_user_func_array(array($this, self::$baseMethodAlternates[$method]), $arguments); + } else if(isset(PageProperties::$baseMethodAlternates[$method])) { + return call_user_func_array(array($this, PageProperties::$baseMethodAlternates[$method]), $arguments); } else { return parent::___callUnknown($method, $arguments); } @@ -1961,6 +1788,7 @@ class Page extends WireData implements \Countable, WireMatchable { * #pw-group-manipulation * * @param int|array|string Status value, array of status names or values, or status name string. + * @return self * @see Page::addStatus(), Page::removeStatus() * */ @@ -1979,8 +1807,8 @@ class Page extends WireData implements \Countable, WireMatchable { foreach($value as $v) { if(is_int($v) || ctype_digit("$v")) { // integer $status = $status | ((int) $v); - } else if(is_string($v) && isset(self::$statuses[$v])) { // string (status name) - $status = $status | self::$statuses[$v]; + } else if(is_string($v) && isset(PageProperties::$statuses[$v])) { // string (status name) + $status = $status | PageProperties::$statuses[$v]; } } if($status) $value = $status; @@ -2006,6 +1834,7 @@ class Page extends WireData implements \Countable, WireMatchable { // example: uncache method polls filesManager $this->filesManager = null; } + return $this; } /** @@ -2032,27 +1861,30 @@ class Page extends WireData implements \Countable, WireMatchable { public function setName($value, $language = null) { $key = 'name'; - $charset = $this->wire('config')->pageNameCharset; - $sanitizer = $this->wire('sanitizer'); - - if($language) { - // update $key to contain language ID when applicable - $languages = $this->wire('languages'); - if($languages) { - if(!is_object($language)) { - if(strpos($language, 'name') === 0) $language = (int) substr($language, 4); - $language = $languages->get($language); - if(!$language || !$language->id || $language->isDefault()) $language = ''; - } - if(!$language) return $this; - $key .= $language->id; - } - $existingValue = $this->get($key); - } else { - $existingValue = isset($this->settings[$key]) ? $this->settings[$key] : ''; - } + $charset = $this->wire()->config->pageNameCharset; + $sanitizer = $this->wire()->sanitizer; if($this->isLoaded) { + if(is_int($language)) { + $key .= $language; + $existingValue = $this->get($key); + } else if($language && $language !== 'name') { + // update $key to contain language ID when applicable + $languages = $this->wire()->languages; + if($languages) { + if(!is_object($language)) { + if(strpos($language, 'name') === 0) $language = (int) substr($language, 4); + $language = $languages->getLanguage($language); + if(!$language || !$language->id || $language->isDefault()) $language = ''; + } + if(!$language) return $this; + $key .= $language->id; + } + $existingValue = $this->get($key); + } else { + $existingValue = isset($this->settings[$key]) ? $this->settings[$key] : ''; + } + // name is being set after page has already been loaded if($charset === 'UTF8') { // UTF8 page names allowed but decoding not allowed @@ -2082,11 +1914,18 @@ class Page extends WireData implements \Countable, WireMatchable { } else { // regular ascii page name while page is loading, do nothing to it } + if($language) { + if(ctype_digit("$language")) { + $key = "name$language"; + } else if(is_string($language)) { + $key = $language; // i.e. name1234 + } + } } if($key === 'name') { $this->settings[$key] = $value; - } else if($this->quietMode) { + } else if($this->quietMode || !$this->isLoaded) { parent::set($key, $value); } else { $this->setFieldValue($key, $value, $this->isLoaded); // i.e. name1234 @@ -2354,7 +2193,7 @@ class Page extends WireData implements \Countable, WireMatchable { * */ public function numChildren($selector = null) { - if(!$this->settings['numChildren'] && is_null($selector)) return $this->settings['numChildren']; + if(!$this->settings['numChildren'] && $selector === null) return 0; return $this->traversal()->numChildren($this, $selector); } @@ -3443,7 +3282,7 @@ class Page extends WireData implements \Countable, WireMatchable { */ public function sortfield() { $sortfield = $this->template ? $this->template->sortfield : ''; - if(!$sortfield) $sortfield = $this->sortfield; + if(!$sortfield) $sortfield = $this->settings['sortfield']; if(!$sortfield) $sortfield = 'sort'; return $sortfield; } @@ -3703,7 +3542,9 @@ class Page extends WireData implements \Countable, WireMatchable { * */ public function hasStatus($status) { - if(is_string($status) && isset(self::$statuses[$status])) $status = self::$statuses[$status]; + if(is_string($status) && isset(PageProperties::$statuses[$status])) { + $status = PageProperties::$statuses[$status]; + } return (bool) ($this->status & $status); } @@ -3729,10 +3570,11 @@ class Page extends WireData implements \Countable, WireMatchable { * */ public function addStatus($statusFlag) { - if(is_string($statusFlag) && isset(self::$statuses[$statusFlag])) $statusFlag = self::$statuses[$statusFlag]; + if(is_string($statusFlag) && isset(PageProperties::$statuses[$statusFlag])) { + $statusFlag = PageProperties::$statuses[$statusFlag]; + } $statusFlag = (int) $statusFlag; - $this->setStatus($this->status | $statusFlag); - return $this; + return $this->setStatus($this->status | $statusFlag); } /** @@ -3758,14 +3600,13 @@ class Page extends WireData implements \Countable, WireMatchable { * */ public function removeStatus($statusFlag) { - if(is_string($statusFlag) && isset(self::$statuses[$statusFlag])) $statusFlag = self::$statuses[$statusFlag]; + if(is_string($statusFlag) && isset(PageProperties::$statuses[$statusFlag])) { + $statusFlag = PageProperties::$statuses[$statusFlag]; + } $statusFlag = (int) $statusFlag; $override = $this->settings['status'] & Page::statusSystemOverride; if($statusFlag == Page::statusSystem || $statusFlag == Page::statusSystemID) { - if(!$override) throw new WireException( - "You may not remove the 'system' status from a page unless it also has system override " . - "status (Page::statusSystemOverride)" - ); + if(!$override) throw new WireException('Cannot remove statusSystem from page without statusSystemOverride'); } $this->status = $this->status & ~$statusFlag; return $this; @@ -3802,7 +3643,9 @@ class Page extends WireData implements \Countable, WireMatchable { * */ public function is($status) { - if(is_string($status) && isset(self::$statuses[$status])) $status = self::$statuses[$status]; + if(is_string($status) && isset(PageProperties::$statuses[$status])) { + $status = PageProperties::$statuses[$status]; + } return $this->comparison()->is($this, $status); } @@ -3955,23 +3798,10 @@ class Page extends WireData implements \Countable, WireMatchable { * */ public function status($value = false, $status = null) { - if(!is_bool($value)) { - $this->setStatus($value); - return $this; - } - if(is_null($status)) $status = $this->status; + if($value !== true) return $this->setStatus($value); + if($status === null) $status = $this->status; if($value === false) return $status; - $names = array(); - $remainder = $status; - foreach(self::$statuses as $name => $value) { - if($value <= self::statusOn || $value >= self::statusMax) continue; - if($status & $value) { - $names[$value] = $name; - $remainder = $remainder & ~$value; - } - } - if($remainder > 1) $names[$remainder] = "unknown-$remainder"; - return $names; + return PageProperties::statusToNames($status); } /** @@ -4288,8 +4118,8 @@ class Page extends WireData implements \Countable, WireMatchable { if($this->isLoaded) { return $this->get($key) !== null; } else { - if(isset(self::$baseProperties[$key])) return true; - if(isset(self::$basePropertiesAlternates[$key])) return true; + if(isset(PageProperties::$baseProperties[$key])) return true; + if(isset(PageProperties::$basePropertiesAlternates[$key])) return true; if($this->hasField($key)) return true; return false; } @@ -4407,24 +4237,24 @@ class Page extends WireData implements \Countable, WireMatchable { * Return a Page helper class instance that’s common among all Page (and derived) objects in this ProcessWire instance * * @param string $className - * @return object|PageComparison|PageAccess|PageTraversal|PageFamily + * @return object|PageComparison|PageAccess|PageTraversal * */ protected function getHelperInstance($className) { $instanceID = $this->wire()->getProcessWireInstanceID(); - if(!isset(self::$helpers[$instanceID])) { + if(!isset(PageProperties::$helpers[$instanceID])) { // no helpers yet for this ProcessWire instance - self::$helpers[$instanceID] = array(); + PageProperties::$helpers[$instanceID] = array(); } - if(!isset(self::$helpers[$instanceID][$className])) { + if(!isset(PageProperties::$helpers[$instanceID][$className])) { // helper not yet loaded, so load it $nsClassName = __NAMESPACE__ . "\\$className"; $helper = new $nsClassName(); if($helper instanceof WireFuelable) $this->wire($helper); - self::$helpers[$instanceID][$className] = $helper; + PageProperties::$helpers[$instanceID][$className] = $helper; } else { // helper already ready to use - $helper = self::$helpers[$instanceID][$className]; + $helper = PageProperties::$helpers[$instanceID][$className]; } return $helper; } @@ -4454,7 +4284,7 @@ class Page extends WireData implements \Countable, WireMatchable { } /** - * @return PageFamily + * return PageFamily * * Coming soon * @@ -4474,7 +4304,7 @@ class Page extends WireData implements \Countable, WireMatchable { * */ static public function getStatuses() { - return self::$statuses; + return PageProperties::$statuses; } /** diff --git a/wire/core/PageFinder.php b/wire/core/PageFinder.php index d88e4b9b..f9a00ce6 100644 --- a/wire/core/PageFinder.php +++ b/wire/core/PageFinder.php @@ -96,7 +96,19 @@ class PageFinder extends Wire { * @since 3.0.153 * */ - 'returnAllCols' => false, + 'returnAllCols' => false, + + /** + * Additional options when when 'returnAllCols' option is true + * @since 3.0.172 + * + */ + 'returnAllColsOptions' => array( + 'joinFields' => array(), // names of additional fields to join + 'joinSortfield' => false, // include 'sortfield' in returned columns? (joined from pages_sortfields table) + 'getNumChildren' => false, // include 'numChildren' in returned columns? (sub-select from pages table) + 'unixTimestamps' => false, // return dates as unix timestamps? + ), /** * When true, only the DatabaseQuery object is returned by find(), for internal use. @@ -594,7 +606,7 @@ class PageFinder extends Wire { * - `getTotalType` (string): Method to use to get total, specify 'count' or 'calc' (default='calc'). * - `returnQuery` (bool): When true, only the DatabaseQuery object is returned by find(), for internal use. (default=false) * - `loadPages` (bool): This is an optimization used by the Pages::find() method, but we observe it here as we - * may be able to apply some additional optimizations in certain cases. For instance, if loadPages=false, then + * may be able to apply some additional optimizations in certain cases. For instance, if loadPages=false, then * we can skip retrieval of IDs and omit sort fields. (default=true) * - `stopBeforeID` (int): Stop loading pages once a page matching this ID is found. Page having this ID will be * excluded as well (default=0). @@ -781,13 +793,27 @@ class PageFinder extends Wire { * * @param Selectors|string|array $selectors Selectors object, selector string or selector array * @param array $options + * - `joinFields` (array): Names of additional fields to join (default=[]) 3.0.172+ + * - `joinSortfield` (bool): Include 'sortfield' in returned columns? Joined from pages_sortfields table. (default=false) 3.0.172+ + * - `getNumChildren` (bool): Include 'numChildren' in returned columns? Calculated in query. (default=false) 3.0.172+ + * - `unixTimestamps` (bool): Return created/modified/published dates as unix timestamps rather than ISO-8601? (default=false) 3.0.172+ * @return array|DatabaseQuerySelect * @since 3.0.153 * */ public function findVerboseIDs($selectors, $options = array()) { + $hasCustomOptions = count($options) > 0; $options['returnVerbose'] = false; $options['returnAllCols'] = true; + $options['returnAllColsOptions'] = $this->defaultOptions['returnAllColsOptions']; + if($hasCustomOptions) { + // move some from $options into $options['returnAllColsOptions'] + foreach($options['returnAllColsOptions'] as $name => $default) { + if(!isset($options[$name])) continue; + $options['returnAllColsOptions'][$name] = $options[$name]; + unset($options[$name]); + } + } return $this->find($selectors, $options); } @@ -1454,11 +1480,42 @@ class PageFinder extends Wire { $subqueries = array(); $joins = array(); $database = $this->database; + $autojoinTables = array(); $this->preProcessSelectors($selectors, $options); $this->numAltOperators = 0; + + /** @var DatabaseQuerySelect $query */ + $query = $this->wire(new DatabaseQuerySelect()); + if(!empty($options['bindOptions'])) { + foreach($options['bindOptions'] as $k => $v) $query->bindOption($k, $v); + } if($options['returnAllCols']) { - $columns = array('pages.*'); + $opts = $this->defaultOptions['returnAllColsOptions']; + if(!empty($options['returnAllColsOptions'])) $opts = array_merge($opts, $options['returnAllColsOptions']); + $columns = array('pages.*'); + if($opts['unixTimestamps']) { + $columns[] = 'UNIX_TIMESTAMP(pages.created) AS created'; + $columns[] = 'UNIX_TIMESTAMP(pages.modified) AS modified'; + $columns[] = 'UNIX_TIMESTAMP(pages.published) AS published'; + } + if($opts['joinSortfield']) { + $columns[] = 'pages_sortfields.sortfield AS sortfield'; + $query->leftjoin('pages_sortfields ON pages_sortfields.pages_id=pages.id'); + } + if($opts['getNumChildren']) { + $query->select('(SELECT COUNT(*) FROM pages AS children WHERE children.parent_id=pages.id) AS numChildren'); + } + if(!empty($opts['joinFields'])) { + foreach($opts['joinFields'] as $joinField) { + $joinField = $this->wire()->fields->get($joinField); + if(!$joinField || !$joinField instanceof Field) continue; + $joinTable = $database->escapeTable($joinField->getTable()); + if(!$joinTable || !$joinField->type) continue; + if(!$joinField->type->getLoadQueryAutojoin($joinField, $query)) continue; + $autojoinTables[$joinTable] = $joinTable; // added at end if not already joined + } + } } else if($options['returnVerbose']) { $columns = array('pages.id', 'pages.parent_id', 'pages.templates_id'); } else if($options['returnParentIDs']) { @@ -1469,11 +1526,6 @@ class PageFinder extends Wire { $columns = array('pages.id'); } - /** @var DatabaseQuerySelect $query */ - $query = $this->wire(new DatabaseQuerySelect()); - if(!empty($options['bindOptions'])) { - foreach($options['bindOptions'] as $k => $v) $query->bindOption($k, $v); - } $query->select($columns); $query->from("pages"); $query->groupby($options['returnParentIDs'] ? 'pages.parent_id' : 'pages.id'); @@ -1715,6 +1767,11 @@ class PageFinder extends Wire { $joinType = $j['joinType']; $query->$joinType("$j[table] AS $j[tableAlias] ON $j[tableAlias].pages_id=pages.id AND ($j[join])"); } + + foreach($autojoinTables as $table) { + if(isset($fieldCnt[$table])) continue; // already joined + $query->leftjoin("$table ON $table.pages_id=pages.id"); + } if(count($sortSelectors)) { foreach(array_reverse($sortSelectors) as $s) { diff --git a/wire/core/PageProperties.php b/wire/core/PageProperties.php new file mode 100644 index 00000000..0292c0bb --- /dev/null +++ b/wire/core/PageProperties.php @@ -0,0 +1,230 @@ + Page::statusReserved, + 'locked' => Page::statusLocked, + 'systemID' => Page::statusSystemID, + 'system' => Page::statusSystem, + 'unique' => Page::statusUnique, + 'draft' => Page::statusDraft, + 'flagged' => Page::statusFlagged, + 'internal' => Page::statusInternal, + 'temp' => Page::statusTemp, + 'hidden' => Page::statusHidden, + 'unpublished' => Page::statusUnpublished, + 'trash' => Page::statusTrash, + 'deleted' => Page::statusDeleted, + 'systemOverride' => Page::statusSystemOverride, + 'corrupted' => Page::statusCorrupted, + 'max' => Page::statusMax, + 'on' => Page::statusOn, + ); + + /** + * Properties that can be accessed, mapped to method of access (excluding custom fields of course) + * + * Keys are base property name, values are one of: + * - [methodName]: method name that it maps to ([methodName]=actual method name) + * - "s": property name is accessible in $this->settings using same key + * - "p": Property name maps to same property name in $this + * - "m": Property name maps to same method name in $this + * - "n": Property name maps to same method name in $this, but may be overridden by custom field + * - "t": Property name maps to PageTraversal method with same name, if not overridden by custom field + * - [blank]: needs additional logic to be handled ([blank]='') + * + * @var array + * + */ + public static $baseProperties = array( + 'accessTemplate' => 'getAccessTemplate', + 'addable' => 'm', + 'child' => 'm', + 'children' => 'm', + 'created' => 's', + 'createdStr' => '', + 'createdUser' => '', + 'created_users_id' => 's', + 'deletable' => 'm', + 'deleteable' => 'm', + 'editable' => 'm', + 'editUrl' => 'm', + 'fieldgroup' => '', + 'filesManager' => 'm', + 'filesPath' => 'm', + 'filesUrl' => 'm', + 'hasChildren' => 'm', + 'hasFiles' => 'm', + 'hasFilesPath' => 'm', + 'hasLinks' => 't', + 'hasParent' => 'parents', + 'hasReferences' => 't', + 'httpUrl' => 'm', + 'id' => 's', + 'index' => 'n', + 'instanceID' => 'p', + 'isHidden' => 'm', + 'isLoaded' => 'm', + 'isLocked' => 'm', + 'isNew' => 'm', + 'isPublic' => 'm', + 'isTrash' => 'm', + 'isUnpublished' => 'm', + 'links' => 'n', + 'listable' => 'm', + 'modified' => 's', + 'modifiedStr' => '', + 'modifiedUser' => '', + 'modified_users_id' => 's', + 'moveable' => 'm', + 'name' => 's', + 'namePrevious' => 'p', + 'next' => 'm', + 'numChildren' => 's', + 'numParents' => 'm', + 'numDescendants' => 'm', + 'numLinks' => 't', + 'numReferences' => 't', + 'output' => 'm', + 'outputFormatting' => 'p', + 'parent' => 'm', + 'parent_id' => '', + 'parentPrevious' => 'p', + 'parents' => 'm', + 'path' => 'm', + 'prev' => 'm', + 'publishable' => 'm', + 'published' => 's', + 'publishedStr' => '', + 'quietMode' => 'p', + 'references' => 'n', + 'referencing' => 't', + 'render' => '', + 'rootParent' => 'm', + 'siblings' => 'm', + 'sort' => 's', + 'sortable' => 'm', + 'sortfield' => 's', + 'status' => 's', + 'statusPrevious' => 'p', + 'statusStr' => '', + 'template' => 'p', + 'templates_id' => '', + 'templatePrevious' => 'p', + 'trashable' => 'm', + 'url' => 'm', + 'urls' => 'm', + 'viewable' => 'm' + ); + + /** + * Alternate names accepted for base properties + * + * Keys are alternate property name and values are base property name + * + * @var array + * + */ + public static $basePropertiesAlternates = array( + 'createdUserID' => 'created_users_id', + 'createdUsersID' => 'created_users_id', + 'created_user_id' => 'created_users_id', + 'editURL' => 'editUrl', + 'fields' => 'fieldgroup', + 'has_parent' => 'hasParent', + 'httpURL' => 'httpUrl', + 'modifiedUserID' => 'modified_users_id', + 'modifiedUsersID' => 'modified_users_id', + 'modified_user_id' => 'modified_users_id', + 'num_children' => 'numChildren', + 'numChildrenVisible' => 'hasChildren', + 'numVisibleChildren' => 'hasChildren', + 'of' => 'outputFormatting', + 'out' => 'output', + 'parentID' => 'parent_id', + 'subpages' => 'children', + 'template_id' => 'templates_id', + 'templateID' => 'templates_id', + 'templatesID' => 'templates_id', + ); + + /** + * Method alternates/aliases (alias => actual) + * + * @var array + * + */ + public static $baseMethodAlternates = array( + 'descendants' => 'find', + 'descendant' => 'findOne', + ); + + /** + * Name and status language properties (populated by LanguagesSupport module when applicable) + * + * Keys are language property, values array where index 0 is property name and index 1 is language ID. + * ~~~~~ + * [ + * 'name1234' => [ 'name', 1234 ], + * 'status1234' => [ 'status', 1234 ], + * ... + * ] + * ~~~~~ + * + * @var array|null + * + */ + public static $languageProperties = array(); + + /** + * Given a status (flags int) return array of status names + * + * @param int $status + * @return array + * + */ + public static function statusToNames($status) { + $names = array(); + $remainder = $status; + foreach(self::$statuses as $name => $value) { + if($value <= Page::statusOn || $value >= Page::statusMax) continue; + if($status & $value) { + $names[$value] = $name; + $remainder = $remainder & ~$value; + } + } + if($remainder > 1) $names[$remainder] = "unknown-$remainder"; + return $names; + } +} \ No newline at end of file diff --git a/wire/core/Pages.php b/wire/core/Pages.php index c4e729a1..79c6550d 100644 --- a/wire/core/Pages.php +++ b/wire/core/Pages.php @@ -332,6 +332,86 @@ class Pages extends Wire { return $matches; } + /** + * Find pages and specify which fields to join (overriding configured autojoin settings) + * + * This is a useful optimization when you know exactly which fields you will be using from the returned + * pages and you want to have their values joined into the page loading query to reduce overhead. Note + * that this overrides the configured autojoin settings in ProcessWire fields. + * + * If a particular page in the returned set of pages was already loaded before this method call, + * then the one already in memory will be used rather than this method loading another copy of it. + * + * ~~~~~ + * // 1. Example of loading blog posts where we want to join title, date, summary: + * $posts = $pages->findJoin("template=blog-post", [ 'title', 'date', 'summary' ]); + * + * // 2. You can also specify the join fields as a CSV string: + * $posts = $pages->findJoin("template=blog-post", 'title, date, summary'); + * + * // 3. You can also use the join functionality on a regular $pages->find() by specifying + * // property 'join' or 'field' in the selector. The words 'join' and 'field' are aliases + * // of each other here, just in case you have an existing field with one of those names. + * // Otherwise, use whichever makes more sense to you. The following examples demonstrate + * // this and all do exactly the same thing as examples 1 and 2 above: + * $posts = $pages->find("template=blog-post, join=title|date|summary"); + * $posts = $pages->find("template=blog-post, field=title|date|summary"); + * $posts = $pages->find("template=blog-post, join=title, join=date, join=summary"); + * $posts = $pages->find("template=blog-post, field=title, field=date, field=summary"); + * + * // 4. Let’s say you want to load pages with NO autojoin fields, here is how. + * // The following loads all blog-post pages and prevents ANY fields from being joined, + * // even if they are configured to be autojoin in ProcessWire: + * $posts = $pages->findJoin("template=blog-post", false); + * $posts = $pages->find("template=blog-post, join=none"); // same as above + * ~~~~~ + * + * #pw-group-retrieval + * + * @param string|array|Selectors $selector + * @param array|string|bool $joinFields Array or CSV string of field names to autojoin, or false to join none. + * @param array $options + * @return PageArray + * @since 3.0.172 + * + */ + public function findJoin($selector, $joinFields, $options = array()) { + + $fields = $this->wire()->fields; + + if($joinFields === false) { + $name = 'none'; + while($fields->get($name)) $name .= 'X'; + $joinFields = array($name); + } else if(empty($joinFields)) { + $joinFields = array(); + } else if(!is_array($joinFields)) { + $joinFields = (string) $joinFields; + if(strpos($joinFields, ',') !== false) { + $joinFields = explode(',', $joinFields); + } else if(strpos($joinFields, '|') !== false) { + $joinFields = explode('|', $joinFields); + } else { + $joinFields = array($joinFields); + } + } + + foreach($joinFields as $key => $name) { + if(is_int($name) || ctype_digit($name)) { + $field = $fields->get($name); + if(!$field) continue; + $name = $field->name; + } else if(strpos($name, '.') !== false) { + list($name,) = explode('.', $name, 2); // subfields not allowed + } + $joinFields[$key] = trim($name); + } + + $options['joinFields'] = $joinFields; + + return $this->find($selector, $options); + } + /** * Like find() except returns array of IDs rather than Page objects * @@ -401,46 +481,40 @@ class Pages extends Wire { * as it exists in the database. In most cases you should use `$pages->find()` instead, * but this method provides a convenient alternative for some cases. * - * The `$selector` argument can any page-finding selector that you would provide + * The `$selector` argument can be any page-finding selector that you would provide * to a regular `$pages->find()` call. The most interesting stuff relates to the * `$field` argument though, which is what the rest of this section looks at: * * If you omit the `$field` argument, it will return all data for the found pages in * an array where the keys are the page IDs and the values are associative arrays - * containing all of the page’s raw field and property values indexed by name… - * ~~~~~ - * $a = $pages->findRaw("template=blog"); - * ~~~~~ - * …but findRaw() is more useful for cases where you want to retrieve specific things - * without having to load the entire page (or its data). Below are a few examples of - * how you can do this. + * containing all of each page raw field and property values indexed by name… + * `$a = $pages->findRaw("template=blog");` …but findRaw() is more useful for cases + * where you want to retrieve specific things without having to load the entire page + * (or its data). Below are a few examples of how you can do this. * - * If you provide a string (field name) for `$field`, then it will return an array with - * the values of the `data` column of that field. The `$field` can also be the name of - * a native pages table property like `id` or `name`. * ~~~~~ + * // If you provide a string (field name) for `$field`, then it will return an + * // array with the values of the `data` column of that field. The `$field` can + * // also be the name of a native pages table property like `id` or `name`. * $a = $pages->findRaw("template=blog", "title"); - * ~~~~~ - * The above would return an array of blog page titles indexed by page ID. If you - * provide an array for `$field` then it will return an array for each page, where each - * of those arrays is indexed by the field names you requested. - * ~~~~~ + * + * // The above would return an array of blog page titles indexed by page ID. If + * // you provide an array for `$field` then it will return an array for each page, + * // where each of those arrays is indexed by the field names you requested. * $a = $pages->findRaw("template=blog", [ "title", "date" ]); - * ~~~~~ - * You may specify field name(s) like `field.subfield` to retrieve a specific column/subfield. - * When it comes to Page references or Repeaters, the subfield can also be the name of a field - * that exists on the Page reference or repeater pages. - * ~~~~~ + * + * // You may specify field name(s) like `field.subfield` to retrieve a specific + * // column/subfield. When it comes to Page references or Repeaters, the subfield + * // can also be the name of a field that exists on the Page reference or repeater. * $a = $pages->findRaw("template=blog", [ "title", "categories.title" ]); - * ~~~~~ - * You can also use this format below to retrieve multiple subfields from one field: - * ~~~~~ + * + * // You can also use this format below to get multiple subfields from one field: * $a = $pages->findRaw("template=blog", [ "title", "categories" => [ "id", "title" ] ]); - * ~~~~~ - * You may specify wildcard field name(s) like `field.*` to return all columns for `field`. - * This retrieves all columns from the field’s table. This is especially useful with fields - * like Table or Combo that might have several different columns: - * ~~~~~ + * + * // You may specify wildcard field name(s) like `field.*` to return all columns + * // for `field`. This retrieves all columns from the field’s table. This is + * // especially useful with fields like Table or Combo that might have several + * // different columns: * $a = $pages->findRaw("template=villa", "rates_table.*" ); * ~~~~~ * diff --git a/wire/core/PagesLoader.php b/wire/core/PagesLoader.php index 138d396f..e3bcada0 100644 --- a/wire/core/PagesLoader.php +++ b/wire/core/PagesLoader.php @@ -64,6 +64,14 @@ class PagesLoader extends Wire { */ protected $debug = false; + /** + * Page instance ID + * + * @var int + * + */ + static protected $pageInstanceID = 0; + /** * Construct * @@ -265,23 +273,29 @@ 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 exclusions (same as include=all option) - * - findIDs: boolean|int - true=return array of [id, template_id, parent_id], or 1=return just page IDs, 2=return all columns (3.0.153+). - * - getTotal: boolean - whether to set returning PageArray's "total" property (default: true except when findOne=true) - * - cache: boolean - Allow caching of selectors and pages loaded (default=true). Also sets loadOptions[cache]. - * - allowCustom: boolean - Whether to allow use of "_custom=new selector" in selectors (default=false). - * - lazy: boolean - makes find() return Page objects that don't have any data populated to them (other than id and template). - * - 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. + * - `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 - Optional assoc array of options to pass to getById() load options. + * - `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 * */ @@ -295,10 +309,12 @@ class PagesLoader extends Wire { $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']) ? (bool) $options['cache'] : true; if(!$cachePages && !isset($loadOptions['cache'])) $loadOptions['cache'] = false; - if($loadPages && !$lazy && !$findIDs) { + if($allowShortcuts) { $pages = $this->findShortcut($selector, $options, $loadOptions); if($pages) return $pages; } @@ -316,11 +332,29 @@ class PagesLoader extends Wire { } $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($loadPages && !$findIDs && !$lazy) { + if($allowShortcuts) { $pages = $this->pages->cacher()->getSelectorCache($selectorString, $options); - if(!is_null($pages)) { + if($pages !== null) { if($debug) $this->pages->debugLog('find', $selectorString, $pages . ' [from-cache]'); return $pages; } @@ -339,12 +373,23 @@ class PagesLoader extends Wire { 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); @@ -391,7 +436,8 @@ class PagesLoader extends Wire { $loadPages = false; $cachePages = false; - $pages = $this->pages->newPageArray($loadOptions); // only for hooks to see + // 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 @@ -496,11 +542,159 @@ class PagesLoader extends Wire { 'options' => $options )); - if($findIDs) return $findIDs === 1 ? $pagesIDs : $pagesInfo; + 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 + * + */ + protected 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(); + + 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); + 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, '__')) { + $page->setFieldValue($key, $value, false); + } else { + $page->setForced($key, $value); + } + } + + // set blank values where joinField didn't appear on page row + foreach($joinFields as $joinField) { + if(isset($row["{$joinField}__data"])) continue; + if(!$template->fieldgroup->hasField($joinField)) continue; + $field = $page->getField($joinField); + if(!$field || !$field->type) continue; + $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->resetTrackChanges(true); + + 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) * @@ -696,8 +890,6 @@ class PagesLoader extends Wire { */ public function getById($_ids, $template = null, $parent_id = null) { - static $instanceID = 0; - $options = array( 'cache' => true, 'getFromCache' => true, @@ -970,7 +1162,7 @@ class PagesLoader extends Wire { unset($row['templates_id']); foreach($row as $key => $value) $page->set($key, $value); if($options['cache'] === false) $page->loaderCache = false; - $page->instanceID = ++$instanceID; + $page->instanceID = ++self::$pageInstanceID; $page->setIsLoaded(true); $page->setIsNew(false); $page->resetTrackChanges(true); @@ -1338,6 +1530,26 @@ class PagesLoader extends Wire { $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 diff --git a/wire/core/PagesSortfields.php b/wire/core/PagesSortfields.php index fae05555..b20eae6b 100644 --- a/wire/core/PagesSortfields.php +++ b/wire/core/PagesSortfields.php @@ -5,13 +5,37 @@ * * Manages the table for the sortfield property for Page children. * - * ProcessWire 3.x, Copyright 2016 by Ryan Cramer + * ProcessWire 3.x, Copyright 2021 by Ryan Cramer * https://processwire.com * */ class PagesSortfields extends Wire { + /** + * Get sortfield for given Page from DB + * + * @param int|Page $page Page or page ID + * @return string + * @since 3.0.172 + * + */ + public function get($page) { + $pageId = $page instanceof Page ? $page->id : (int) $page; + $sql = 'SELECT sortfield FROM pages_sortfields WHERE pages_id=:id'; + $query = $this->wire()->database->prepare($sql); + $query->bindValue(':id', $pageId, \PDO::PARAM_INT); + $query->execute(); + if($query->rowCount()) { + $sortfield = $query->fetchColumn(); + $sortfield = $this->decode($sortfield); + } else { + $sortfield = ''; + } + $query->closeCursor(); + return $sortfield; + } + /** * Save the sortfield for a given Page * diff --git a/wire/modules/LanguageSupport/LanguageSupport.module b/wire/modules/LanguageSupport/LanguageSupport.module index b086541a..69fe697c 100644 --- a/wire/modules/LanguageSupport/LanguageSupport.module +++ b/wire/modules/LanguageSupport/LanguageSupport.module @@ -157,6 +157,8 @@ class LanguageSupport extends WireData implements Module, ConfigurableModule { $_default = $language; // backup plan } else { $numOtherLanguages++; + PageProperties::$languageProperties["name$language->id"] = array('name', $language->id); + PageProperties::$languageProperties["status$language->id"] = array('status', $language->id); } }