1
0
mirror of https://github.com/processwire/processwire.git synced 2025-08-11 09:14:58 +02:00

Add new $pages->findJoin() method and $pages->find("selector, field=title|summary"); option that lets you specify which fields to autojoin in a page finding operation. Also added is an experimental PagesLoader::findMin() method that finds and loads pages in one query rather than splitting them out into separate find() and getById() calls. For now, we just use this method to handle the findJoin() and joinFields options, but may expand its use later. This commit also contains numerous performance optimizations to the Page class, and several updates to the PageFinder class to support the new autojoin options.

This commit is contained in:
Ryan Cramer
2021-02-05 11:28:48 -05:00
parent a697795c08
commit 7fe8e5f1fb
7 changed files with 757 additions and 328 deletions

View File

@@ -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 thats 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;
}
/**

View File

@@ -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) {

View File

@@ -0,0 +1,230 @@
<?php namespace ProcessWire;
/**
* ProcessWire Page properties helper
*
* For static runtime property detection by the base Page class.
* The properties/methods in this class were originally in the base Page class
* but have been moved here for Page class load time optimization purposes.
* Except where indicated, please treat these properties as private to the
* Page class.
*
* ProcessWire 3.x, Copyright 2021 by Ryan Cramer
* https://processwire.com
*
*/
abstract class PageProperties {
/**
* Page class helper object instances (one of each helper per ProcessWire instance, lazy loaded)
*
* @var array
*
*/
public static $helpers = array();
/**
* Status string shortcuts, so that status can be specified as a word
*
* See also: Page::getStatuses() method.
*
* @var array
*
*/
public static $statuses = array(
'reserved' => 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;
}
}

View File

@@ -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. Lets 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 pages 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 fields 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 fields table. This is
* // especially useful with fields like Table or Combo that might have several
* // different columns:
* $a = $pages->findRaw("template=villa", "rates_table.*" );
* ~~~~~
*

View File

@@ -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 pages 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

View File

@@ -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
*

View File

@@ -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);
}
}