Permalink
Fetching contributors…
Cannot retrieve contributors at this time
3885 lines (3630 sloc) 129 KB
<?php namespace ProcessWire;
/**
* ProcessWire Page
*
* Page is the class used by all instantiated pages and it provides functionality for:
*
* 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 2017 by Ryan Cramer
* https://processwire.com
*
* #pw-summary Class used by all Page objects in ProcessWire.
* #pw-summary-languages Multi-language methods require these core modules: `LanguageSupport`, `LanguageSupportFields`, `LanguageSupportPageNames`.
* #pw-summary-system Most system properties directly correspond to columns in the `pages` database table.
* #pw-order-groups common,traversal,manipulation,date-time,access,output-rendering,status,constants,languages,system,advanced,hooks
* #pw-use-constants
* #pw-var $page
* #pw-body =
* The `$page` API variable represents the current page being viewed. However, the documentation
* here also applies to all Page objects that you may work with in the API. We use `$page` as the most common example
* throughout the documentation, but you can substitute that with any variable name representing a Page.
* #pw-body
*
* @link http://processwire.com/api/ref/page/ Offical $page Documentation
* @link http://processwire.com/api/selectors/ Official Selectors Documentation
*
* @property int $id The numbered ID of the current page #pw-group-system
* @property string $name The name assigned to the page, as it appears in the URL #pw-group-system #pw-group-common
* @property string $namePrevious Previous name, if changed. Blank if not. #pw-advanced
* @property string $title The page's title (headline) text
* @property string $path The page's URL path from the homepage (i.e. /about/staff/ryan/)
* @property string $url The page's URL path from the server's document root
* @property string $httpUrl Same as $page->url, except includes scheme (http or https) and hostname.
* @property Page|string|int $parent The parent Page object or a NullPage if there is no parent. For assignment, you may also use the parent path (string) or id (integer). #pw-group-traversal
* @property Page|null $parentPrevious Previous parent, if parent was changed. #pw-group-traversal
* @property int $parent_id The numbered ID of the parent page or 0 if homepage or not assigned. #pw-group-system
* @property int $templates_id The numbered ID of the template usedby this page. #pw-group-system
* @property PageArray $parents All the parent pages down to the root (homepage). Returns a PageArray. #pw-group-common #pw-group-traversal
* @property Page $rootParent The parent page closest to the homepage (typically used for identifying a section) #pw-group-traversal
* @property Template|string $template The Template object this page is using. The template name (string) may also be used for assignment.
* @property Template|null $templatePrevious Previous template, if template was changed. #pw-advanced
* @property Fieldgroup $fields All the Fields assigned to this page (via its template). Returns a Fieldgroup. #pw-advanced
* @property int $numChildren The number of children (subpages) this page has, with no exclusions (fast). #pw-group-traversal
* @property int $hasChildren The number of visible children this page has. Excludes unpublished, no-access, hidden, etc. #pw-group-traversal
* @property int $numVisibleChildren Verbose alias of $hasChildren #pw-internal
* @property PageArray $children All the children of this page. Returns a PageArray. See also $page->children($selector). #pw-group-traversal
* @property Page|NullPage $child The first child of this page. Returns a Page. See also $page->child($selector). #pw-group-traversal
* @property PageArray $siblings All the sibling pages of this page. Returns a PageArray. See also $page->siblings($selector). #pw-group-traversal
* @property Page $next This page's next sibling page, or NullPage if it is the last sibling. See also $page->next($pageArray). #pw-group-traversal
* @property Page $prev This page's previous sibling page, or NullPage if it is the first sibling. See also $page->prev($pageArray). #pw-group-traversal
* @property int $created Unix timestamp of when the page was created. #pw-group-common #pw-group-date-time #pw-group-system
* @property string $createdStr Date/time when the page was created (formatted date/time string). #pw-group-date-time
* @property int $modified Unix timestamp of when the page was last modified. #pw-group-common #pw-group-date-time #pw-group-system
* @property string $modifiedStr Date/time when the page was last modified (formatted date/time string). #pw-group-date-time
* @property int $published Unix timestamp of when the page was published. #pw-group-common #pw-group-date-time #pw-group-system
* @property string $publishedStr Date/time when the page was published (formatted date/time string). #pw-group-date-time
* @property int $created_users_id ID of created user. #pw-group-system
* @property User $createdUser The user that created this page. Returns a User or a NullUser.
* @property int $modified_users_id ID of last modified user. #pw-group-system
* @property User $modifiedUser The user that last modified this page. Returns a User or a NullUser.
* @property PagefilesManager $filesManager The object instance that manages files for this page. #pw-advanced
* @property bool $outputFormatting Whether output formatting is enabled or not. #pw-advanced
* @property int $sort Sort order of this page relative to siblings (applicable when manual sorting is used). #pw-group-system
* @property int $index Index of this page relative to its siblings, regardless of sort (starting from 0). #pw-group-traversal
* @property string $sortfield Field that a page is sorted by relative to its siblings (default="sort", which means drag/drop manual) #pw-group-system
* @property null|array _statusCorruptedFields Field names that caused the page to have Page::statusCorrupted status. #pw-internal
* @property int $status Page status flags. #pw-group-system #pw-group-status
* @property int|null $statusPrevious Previous status, if status was changed. #pw-group-status
* @property string statusStr Returns space-separated string of status names active on this page. #pw-group-status
* @property Fieldgroup $fieldgroup Fieldgroup used by page template. Shorter alias for $page->template->fieldgroup (same as $page->fields) #pw-advanced
* @property string $editUrl URL that this page can be edited at. #pw-group-advanced
* @property string $editURL Alias of $editUrl. #pw-internal
* @property PageRender $render May be used for field markup rendering like $page->render->title. #pw-advanced
* @property bool $loaderCache Whether or not pages loaded as a result of this one may be cached by PagesLoaderCache. #pw-internal
*
* @property Page|null $_cloning Internal runtime use, contains Page being cloned (source), when this Page is the new copy (target). #pw-internal
* @property bool|null $_hasAutogenName Internal runtime use, set by Pages class when page as auto-generated name. #pw-internal
* @property bool|null $_forceSaveParents Internal runtime/debugging use, force a page to refresh its pages_parents DB entries on save(). #pw-internal
*
* Methods added by PageRender.module:
* -----------------------------------
* @method string|mixed render($fieldName = '') Returns rendered page markup. If given a $fieldName argument, it behaves same as the renderField() method. #pw-group-output-rendering
*
* Methods added by PagePermissions.module:
* ----------------------------------------
* @method bool viewable($field = '', $checkTemplateFile = true) Returns true if the page (and optionally field) is viewable by the current user, false if not. #pw-group-access
* @method bool editable($field = '', $checkPageEditable = true) Returns true if the page (and optionally field) is editable by the current user, false if not. #pw-group-access
* @method bool publishable() Returns true if the page is publishable by the current user, false if not. #pw-group-access
* @method bool listable() Returns true if the page is listable by the current user, false if not. #pw-group-access
* @method bool deleteable() Returns true if the page is deleteable by the current user, false if not. #pw-group-access
* @method bool deletable() Alias of deleteable(). #pw-group-access
* @method bool trashable($orDeleteable = false) Returns true if the page is trashable by the current user, false if not. #pw-group-access
* @method bool addable($pageToAdd = null) Returns true if the current user can add children to the page, false if not. Optionally specify the page to be added for additional access checking. #pw-group-access
* @method bool moveable($newParent = null) Returns true if the current user can move this page. Optionally specify the new parent to check if the page is moveable to that parent. #pw-group-access
* @method bool sortable() Returns true if the current user can change the sort order of the current page (within the same parent). #pw-group-access
* @property bool $viewable #pw-group-access
* @property bool $editable #pw-group-access
* @property bool $publishable #pw-group-access
* @property bool $deleteable #pw-group-access
* @property bool $deletable #pw-group-access
* @property bool $trashable #pw-group-access
* @property bool $addable #pw-group-access
* @property bool $moveable #pw-group-access
* @property bool $sortable #pw-group-access
* @property bool $listable #pw-group-access
*
* Methods added by LanguageSupport.module (not installed by default)
* -----------------------------------------------------------------
* @method Page setLanguageValue($language, $field, $value) Set value for field in language (requires LanguageSupport module). $language may be ID, language name or Language object. Field should be field name (string). #pw-group-languages
* @method Page getLanguageValue($language, $field) Get value for field in language (requires LanguageSupport module). $language may be ID, language name or Language object. Field should be field name (string). #pw-group-languages
*
* Methods added by LanguageSupportPageNames.module (not installed by default)
* ---------------------------------------------------------------------------
* @method string localName($language = null, $useDefaultWhenEmpty = false) Return the page name in the current user’s language, or specify $language argument (Language object, name, or ID), or TRUE to use default page name when blank (instead of 2nd argument). #pw-group-languages
* @method string localPath($language = null) Return the page path in the current user's language, or specify $language argument (Language object, name, or ID). #pw-group-languages
* @method string localUrl($language = null) Return the page URL in the current user's language, or specify $language argument (Language object, name, or ID). #pw-group-languages
* @method string localHttpUrl($language = null) Return the page URL (including scheme and hostname) in the current user's language, or specify $language argument (Language object, name, or ID). #pw-group-languages
*
* Methods added by ProDrafts.module (if installed)
* ------------------------------------------------
* @method ProDraft|\ProDraft|int|string|Page|array draft($key = null, $value = null) Helper method for drafts (added by ProDrafts). #pw-advanced
*
* Hookable methods
* ----------------
* @method mixed getUnknown($key) Last stop to find a property that we haven't been able to locate. Hook this method to provide a handler. #pw-hooker
* @method Page rootParent() Get parent closest to homepage. #pw-internal
* @method void loaded() Called when page is loaded. #pw-internal
* @method void setEditor(WirePageEditor $editor) #pw-internal
* @method string getIcon() #pw-internal
* @method string getMarkup($key) Return the markup value for a given field name or {tag} string. #pw-internal
* @method string|mixed renderField($fieldName, $file = '') Returns rendered field markup, optionally with file relative to templates/fields/. #pw-internal
* @method string|mixed renderValue($value, $file) Returns rendered markup for $value using $file relative to templates/fields/. #pw-internal
*
*/
class Page extends WireData implements \Countable, WireMatchable {
/*
* The following constant flags are specific to a Page's 'status' field. A page can have 1 or more flags using bitwise logic.
* Status levels 1024 and above are excluded from search by the core. Status levels 16384 and above are runtime only and not
* stored in the DB unless for logging or page history.
*
* If the under 1024 status flags are expanded in the future, it must be ensured that the combined value of the searchable flags
* never exceeds 1024, otherwise issues in Pages::find() will need to be considered.
*
* The status levels 16384 and above can safely be changed as needed as they are runtime only.
*
* Please note that statuses 2, 32, 256, and 4096 are reserved for future use.
*
*/
/**
* Base status for pages in use (assigned automatically)
* #pw-internal
*
*/
const statusOn = 1;
/**
* Indicates page is locked for changes (name: "locked")
*
*/
const statusLocked = 4;
/**
* Page is for the system and may not be deleted or have its id changed (name: "system-id").
* #pw-internal
*
*/
const statusSystemID = 8;
/**
* Page is for the system and may not be deleted or have its id, name, template or parent changed (name: "system").
* #pw-internal
*
*/
const statusSystem = 16;
/**
* Page has pending draft changes (name: "draft").
* #pw-internal
*
*/
const statusDraft = 64;
/**
* Page has version data available (name: "versions").
* #pw-internal
*
*/
const statusVersions = 128;
/**
* Page is temporary. 1+ day old unpublished pages with this status may be automatically deleted (name: "temp").
* #pw-internal
*
*/
const statusTemp = 512;
/**
* Page is hidden and excluded from page finding methods unless overridden by selector (name: "hidden").
*
*/
const statusHidden = 1024;
/**
* Page is unpublished (not publicly visible) and excluded from page finding methods unless overridden (name: "unpublished").
*
*/
const statusUnpublished = 2048;
/**
* Page is in the trash.
* #pw-internal
*
*/
const statusTrash = 8192; // page is in the trash
/**
* Page is deleted (runtime status only, as deleted pages aren't saved in the database)
* #pw-internal
*
*/
const statusDeleted = 16384;
/**
* Page is in a state where system flags may be overridden (runtime only)
* #pw-internal
*
*/
const statusSystemOverride = 32768;
/**
* Page was corrupted at runtime and is NOT saveable.
* #pw-internal
*
*/
const statusCorrupted = 131072;
/**
* Maximum possible page status, to use only for runtime comparisons - do not assign this to a page.
* #pw-internal
*
*/
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(
'locked' => self::statusLocked,
'systemID' => self::statusSystemID,
'system' => self::statusSystem,
'draft' => self::statusDraft,
'versions' => self::statusVersions,
'temp' => self::statusTemp,
'hidden' => self::statusHidden,
'unpublished' => self::statusUnpublished,
'trash' => self::statusTrash,
'deleted' => self::statusDeleted,
'systemOverride' => self::statusSystemOverride,
'corrupted' => self::statusCorrupted,
);
/**
* The Template this page is using (object)
*
* @var Template|null
*
*/
protected $template;
/**
* The previous template used by the page, if it was changed during runtime.
*
* Allows Pages::save() to delete data that's no longer used.
*
* @var Template|null
*
*/
private $templatePrevious;
/**
* Parent Page - Instance of Page
*
* @var Page|null
*
*/
protected $_parent = null;
/**
* Parent ID for lazy loading purposes
*
* @var int
*
*/
protected $_parent_id = 0;
/**
* The previous parent used by the page, if it was changed during runtime.
*
* Allows Pages::save() to identify when the parent has changed
*
* @var Page|null
*
*/
private $parentPrevious;
/**
* The previous name used by this page, if it changed during runtime.
*
* @var string
*
*/
private $namePrevious;
/**
* The previous status used by this page, if it changed during runtime.
*
* @var int
*
*/
private $statusPrevious;
/**
* Reference to the Page's template file, used for output. Instantiated only when asked for.
*
* @var TemplateFile|null
*
*/
private $output;
/**
* Instance of PagefilesManager, which manages and migrates file versions for this page
*
* Only instantiated upon request, so access only from filesManager() method in Page class.
* Outside API can use $page->filesManager.
*
* @var PagefilesManager|null
*
*/
private $filesManager = null;
/**
* Field data that queues while the page is loading.
*
* Once setIsLoaded(true) is called, this data is processed and instantiated into the Page and the fieldDataQueue is emptied (and no longer relevant)
*
* @var array
*
*/
protected $fieldDataQueue = array();
/**
* Field names that should wakeup and sanitize on first access (populated when isLoaded==false)
*
* These are most likely field names designated as autoload for this page.
*
* @var array of (field name => raw field value)
*
*/
protected $wakeupNameQueue = array();
/**
* Is this a new page (not yet existing in the database)?
*
* @var bool
*
*/
protected $isNew = true;
/**
* Is this Page finished loading from the DB (i.e. Pages::getById)?
*
* When false, it is assumed that any values set need to be woken up.
* When false, it also assumes that built-in properties (like name) don't need to be sanitized.
*
* Note: must be kept in the 'true' state. Pages::getById sets it to false before populating data and then back to true when done.
*
* @var bool
*
*/
protected $isLoaded = true;
/**
* Lazy load state of page
*
* - int: Page is pending lazy loading, and not yet populated.
* - false: Page is not lazy loading.
* - true: Page was lazy loading and has already loaded.
*
* @var bool
*
*/
protected $lazyLoad = false;
/**
* Whether or not pages loaded by this one are allowed to be cached by PagesLoaderCache class
*
* @var bool
*
*/
protected $loaderCache = true;
/**
* Is this page allowing it's output to be formatted?
*
* If so, the page may not be saveable because calls to $page->get(field) are returning versions of
* variables that may have been formatted at runtime for output. An exception will be thrown if you
* attempt to set the value of a formatted field when $outputFormatting is on.
*
* Output formatting should be turned off for pages that you are manipulating and saving.
* Whereas it should be turned on for pages that are being used for output on a public site.
* Having it on means that Textformatters and any other output formatters will be executed
* on any values returned by this page. Likewise, any values you set to the page while outputFormatting
* is set to true are considered potentially corrupt.
*
* @var bool
*
*/
protected $outputFormatting = false;
/**
* A unique instance ID assigned to the page at the time it's loaded (for debugging purposes only)
*
* @var int
*
*/
protected $instanceID = 0;
/**
* IDs for all the instances of pages, used for debugging and testing.
*
* Indexed by $instanceID => $pageID
*
* @var array
*
*/
static public $instanceIDs = array();
/**
* Stack of ID indexed Page objects that are currently in the loading process.
*
* Used to avoid possible circular references when multiple pages referencing each other are being populated at the same time.
*
* @var array
*
*/
static public $loadingStack = array();
/**
* Controls the behavior of Page::__isset function (no longer in use)
*
* @var bool
* @deprecated No longer in use
*
*/
static public $issetHas = false;
/**
* The current page number, starting from 1
*
* @deprecated, use $input->pageNum instead.
*
* @var int
*
*/
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
*
* @var bool
*
*/
protected $quietMode = false;
/**
* Cached User that created this page
*
* @var User|null
*
*/
protected $createdUser = null;
/**
* Cached User that last modified the page
*
* @var User|null
*
*/
protected $modifiedUser = null;
/**
* Page-specific settings which are either saved in pages table, or generated at runtime.
*
* @var array
*
*/
protected $settings = array(
'id' => 0,
'name' => '',
'status' => 1,
'numChildren' => 0,
'sort' => -1,
'sortfield' => 'sort',
'modified_users_id' => 0,
'created_users_id' => 0,
'created' => 0,
'modified' => 0,
'published' => 0,
);
/**
* 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
* - [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',
'hasParent' => 'parents',
'hasChildren' => 'm',
'httpUrl' => 'm',
'id' => 's',
'index' => 'n',
'instanceID' => 'p',
'isHidden' => 'm',
'isLoaded' => 'm',
'isLocked' => 'm',
'isNew' => 'm',
'isPublic' => 'm',
'isTrash' => 'm',
'isUnpublished' => 'm',
'listable' => 'm',
'modified' => 's',
'modifiedStr' => '',
'modifiedUser' => '',
'modified_users_id' => 's',
'moveable' => 'm',
'name' => 's',
'namePrevious' => 'p',
'next' => 'm',
'numChildren' => 's',
'output' => 'm',
'outputFormatting' => 'p',
'parent' => 'm',
'parent_id' => '',
'parentPrevious' => 'p',
'parents' => 'm',
'path' => 'm',
'prev' => 'm',
'publishable' => 'm',
'published' => 's',
'publishedStr' => '',
'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',
'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',
);
/**
* Create a new page in memory.
*
* @param Template $tpl Template object this page should use.
*
*/
public function __construct(Template $tpl = null) {
if(!is_null($tpl)) {
$tpl->wire($this);
$this->template = $tpl;
}
$this->useFuel(false); // prevent fuel from being in local scope
$this->parentPrevious = null;
$this->templatePrevious = null;
$this->statusPrevious = null;
}
/**
* Destruct this page instance
*
*/
public function __destruct() {
if($this->instanceID) {
// remove from the record of instanceID, so that we have record of page's that HAVEN'T been destructed.
unset(self::$instanceIDs[$this->instanceID]);
}
}
/**
* Clone this page instance
*
*/
public function __clone() {
$track = $this->trackChanges();
$this->setTrackChanges(false);
if($this->filesManager) {
$this->filesManager = clone $this->filesManager;
$this->filesManager->setPage($this);
}
foreach($this->template->fieldgroup as $field) {
$name = $field->name;
if(!$field->type->isAutoload() && !isset($this->data[$name])) continue; // important for draft loading
$value = $this->get($name);
// no need to clone non-objects, as they've already been cloned
// no need to clone Page objects as we still want to reference the original page
if(!is_object($value) || $value instanceof Page) continue;
$value2 = clone $value;
$this->set($name, $value2); // commit cloned value
// if value is Pagefiles, then tell it the new page
if($value2 instanceof PageFieldValueInterface) $value2->setPage($this);
}
$this->instanceID .= ".clone";
if($track) $this->setTrackChanges(true);
parent::__clone();
}
/**
* Set the value of a page property
*
* You can set properties to a page using either `$page->set('property', $value);` or `$page->property = $value;`.
*
* ~~~~~
* // Set the page title using set() method
* $page->set('title', 'About Us');
*
* // Set the page title directly (equivalent to the above)
* $page->title = 'About Us';
* ~~~~~
*
* #pw-group-common
* #pw-group-manipulation
*
* @param string $key Name of property to set
* @param mixed $value Value to set
* @return Page|WireData Reference to this Page
* @see __set
* @throws WireException
*
*/
public function set($key, $value) {
if(isset(self::$basePropertiesAlternates[$key])) $key = self::$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");
}
switch($key) {
/** @noinspection PhpMissingBreakStatementInspection */
case 'id':
if(!$this->isLoaded) Page::$loadingStack[(int) $value] = $this;
// no break is intentional
case 'sort':
case 'numChildren':
$value = (int) $value;
if($this->settings[$key] !== $value) $this->trackChange($key, $this->settings[$key], $value);
$this->settings[$key] = $value;
break;
case 'status':
$this->setStatus($value);
break;
case 'statusPrevious':
$this->statusPrevious = is_null($value) ? null : (int) $value;
break;
case 'name':
$this->setName($value);
break;
case 'parent':
case 'parent_id':
if(is_object($value) && $value instanceof Page) {
// ok
$this->setParent($value);
} else if($value && !$this->_parent &&
($key == 'parent_id' || is_int($value) || (is_string($value) && ctype_digit("$value")))) {
// store only parent ID so that parent is lazy loaded,
// but only if parent hasn't already been previously loaded
$this->_parent_id = (int) $value;
} else if($value && (is_string($value) || is_int($value))) {
$value = $this->_pages('get', $value);
$this->setParent($value);
}
break;
case 'parentPrevious':
if(is_null($value) || $value instanceof Page) $this->parentPrevious = $value;
break;
case 'template':
case 'templates_id':
if($key == 'templates_id' && $this->template && $this->template->id == $value) break;
if($key == 'templates_id') $value = $this->wire('templates')->get((int)$value);
$this->setTemplate($value);
break;
case 'created':
case 'modified':
case 'published':
if(is_null($value)) $value = 0;
if(!ctype_digit("$value")) $value = strtotime($value);
$value = (int) $value;
if($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);
$this->settings[$key] = $value;
break;
case 'createdUser':
case 'modifiedUser':
$this->setUser($value, str_replace('User', '', $key));
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);
$this->settings[$key] = $value;
break;
case 'isLoaded':
$this->setIsLoaded($value);
break;
case 'pageNum':
// note: pageNum is deprecated, use $input->pageNum instead
/** @noinspection PhpDeprecationInspection */
$this->pageNum = ((int) $value) > 1 ? (int) $value : 1;
break;
case 'instanceID':
$this->instanceID = $value;
self::$instanceIDs[$value] = $this->settings['id'];
break;
case 'loaderCache':
$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);
} else {
if($this->quietMode && !$this->template) return parent::set($key, $value);
$this->setFieldValue($key, $value, $this->isLoaded);
}
}
return $this;
}
/**
* Quietly set the value of a page property.
*
* Set a value to a page without tracking changes and without exceptions.
* Otherwise same as set().
*
* #pw-advanced
* #pw-group-manipulation
*
* @param string $key
* @param mixed $value
* @return $this
*
*/
public function setQuietly($key, $value) {
$this->quietMode = true;
parent::setQuietly($key, $value);
$this->quietMode = false;
return $this;
}
/**
* Force setting a value, skipping over any checks or errors
*
* Enables setting a value when page has no template assigned, for example.
*
* #pw-internal
*
* @param string $key Name of field/property to set
* @param mixed $value Value to set
* @return Page|WireData Returns reference to this page
*
*/
public function setForced($key, $value) {
return parent::set($key, $value);
}
/**
* Set the value of a field that is defined in the page's Fieldgroup
*
* This may not be called when outputFormatting is on.
*
* This is for internal use. API should generally use the set() method, but this is kept public for the minority of instances where it's useful.
*
* #pw-internal
*
* @param string $key
* @param mixed $value
* @param bool $load Should the existing value be loaded for change comparisons? (applicable only to non-autoload fields)
* @return Page|WireData Returns reference to this Page
* @throws WireException
*
*/
public function setFieldValue($key, $value, $load = true) {
if(!$this->template) {
throw new WireException("You must assign a template to the page before setting custom field values ($key)");
}
// if the page is not yet loaded and a '__' field was set, then we queue it so that the loaded() method can
// instantiate all those fields knowing that all parts of them are present for wakeup.
if(!$this->isLoaded && strpos($key, '__')) {
list($key, $subKey) = explode('__', $key);
if(!isset($this->fieldDataQueue[$key])) $this->fieldDataQueue[$key] = array();
$this->fieldDataQueue[$key][$subKey] = $value;
return $this;
}
// check if the given key resolves to a Field or not
if(!$field = $this->getField($key)) {
// 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) {
// store previous value (if set) in a "-$key" version
parent::setQuietly("-$key", $valPrevious);
}
return parent::set($key, $value);
}
// if a null value is set, then ensure the proper blank type is set to the field
if(is_null($value)) {
return parent::set($key, $field->type->getBlankValue($this, $field));
}
// if the page is currently loading from the database, we assume that any set values are 'raw' and need to be woken up
if(!$this->isLoaded) {
// queue for wakeup and sanitize on first field access
$this->wakeupNameQueue[$key] = $key;
// page is currently loading, so we don't need to continue any further
return parent::set($key, $value);
}
// check if the field hasn't been already loaded
if(is_null(parent::get($key))) {
// this field is not currently loaded. if the $load param is true, then ...
// retrieve old value first in case it's not autojoined so that change comparisons and save's work
if($load) $this->get($key);
} else if(isset($this->wakeupNameQueue[$key])) {
// autoload value: we don't yet have a "woke" value suitable for change detection, so let it wakeup
if($this->trackChanges() && $load) {
// if changes are being tracked, load existing value for comparison
$this->getFieldValue($key);
} else {
// if changes aren't being tracked, the existing value can be discarded
unset($this->wakeupNameQueue[$key]);
}
} else {
// check if the field is corrupted
$isCorrupted = false;
if(is_object($value) && $value instanceof PageFieldValueInterface) {
if($value->formatted()) $isCorrupted = true;
} else if($this->outputFormatting) {
$result = $field->type->_callHookMethod('formatValue', array($this, $field, $value));
if($result != $value) $isCorrupted = true;
}
if($isCorrupted) {
// The field has been loaded or dereferenced from the API, and this field changes when formatters are applied to it.
// There is a good chance they are trying to set a formatted value, and we don't allow this situation because the
// possibility of data corruption is high. We set the Page::statusCorrupted status so that Pages::save() can abort.
$this->set('status', $this->status | self::statusCorrupted);
$corruptedFields = $this->get('_statusCorruptedFields');
if(!is_array($corruptedFields)) $corruptedFields = array();
$corruptedFields[$field->name] = $field->name;
$this->set('_statusCorruptedFields', $corruptedFields);
}
}
// isLoaded so sanitizeValue can determine if it can perform a typecast rather than a full sanitization (when helpful)
// we don't use setIsLoaded() so as to avoid triggering any other functions
$isLoaded = $this->isLoaded;
if(!$load) $this->isLoaded = false;
// ensure that the value is in a safe format and set it
$value = $field->type->sanitizeValue($this, $field, $value);
// Silently restore isLoaded state
if(!$load) $this->isLoaded = $isLoaded;
return parent::set($key, $value);
}
/**
* Get the value of a Page property (see details for several options)
*
* This method can accept a simple property name, and also much more:
*
* - You can retrieve a value using either `$page->get('property');` or `$page->property`.
* - Get the first populated property by specifying multiple properties separated by a pipe, i.e. `headline|title`.
* - Get multiple properties in a string by specifying a string `{property}` tags, i.e. `{title}: {summary}`.
* - Specify a selector string to get the first matching child page, i.e. `created>=today`.
* - This method can also retrieve sub-properties of object properties, i.e. `parent.title`.
*
* ~~~~~
* // retrieve the title using get…
* $title = $page->get('title');
*
* // …or retrieve using direct access
* $title = $page->title;
*
* // retrieve headline if populated, otherwise title
* $headline = $page->get('headline|title');
*
* // retrieve title, created date, and summary, formatted in a string
* $str = $page->get('{createdStr}: {title} - {summary}');
*
* // example of getting a sub-property: title of parent page
* $title = $page->get('parent.title');
* ~~~~~
*
* @param string $key Name of property, format string or selector, per the details above.
* @return mixed Value of found property, or NULL if not found.
* @see __get()
*
*/
public function get($key) {
// if lazy load pending, load the page now
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($type == 'p') {
// local property
return $this->$key;
} else if($type == 'm') {
// local method
return $this->{$key}();
} else if($type == 'n') {
// local method, possibly overridden by $field
if(!$this->wire('fields')->get($key)) return $this->{$key}();
} else if($type == 's') {
// settings property
return $this->settings[$key];
} else if($type) {
// defined local method
return $this->{$type}();
}
}
switch($key) {
case 'parent':
$value = $this->_parent ? $this->_parent : $this->parent();
break;
case 'parent_id':
$value = $this->_parent ? $this->_parent->id : 0;
if(!$value) $value = $this->_parent_id;
break;
case 'templates_id':
$value = $this->template ? $this->template->id : 0;
break;
case 'fieldgroup':
$value = $this->template->fieldgroup;
break;
case 'modifiedUser':
case 'createdUser':
if(!$this->$key) {
$_key = str_replace('User', '', $key) . '_users_id';
$u = $this->wire('user');
if($this->settings[$_key] == $u->id) {
$this->set($key, $u); // prevent possible recursion loop
} else {
$u = $this->wire('users')->get((int) $this->settings[$_key]);
$this->set($key, $u);
}
}
$value = $this->$key;
if($value) $value->of($this->of());
break;
case 'urlSegment':
// deprecated, but kept for backwards compatibility
$value = $this->wire('input')->urlSegment1;
break;
case 'statusStr':
$value = implode(' ', $this->status(true));
break;
case 'modifiedStr':
case 'createdStr':
case 'publishedStr':
$value = $this->settings[str_replace('Str', '', $key)];
$value = $value ? wireDate($this->wire('config')->dateFormat, $value) : '';
break;
case 'render':
$value = $this->wire('modules')->get('PageRender');
$value->setPropertyPage($this);
break;
case 'loaderCache':
$value = $this->loaderCache;
break;
default:
if($key && isset($this->settings[(string)$key])) return $this->settings[$key];
// populate a formatted string with {tag} vars
if(strpos($key, '{') !== false && strpos($key, '}')) return $this->getMarkup($key);
// populate a markup requested field like '_fieldName_'
if(strpos($key, '_') === 0 && substr($key, -1) == '_' && !$this->wire('fields')->get($key)) {
if($this->wire('sanitizer')->fieldName($key) == $key) return $this->renderField(substr($key, 1, -1));
}
if(($value = $this->getFieldFirstValue($key)) !== null) return $value;
if(($value = $this->getFieldValue($key)) !== null) return $value;
// if there is a selector, we'll assume they are using the get() method to get a child
if(Selectors::stringHasOperator($key)) return $this->child($key);
// check if it's a field.subfield property
if(strpos($key, '.') && ($value = $this->getFieldSubfieldValue($key)) !== null) return $value;
if(strpos($key, '_OR_')) {
// convert '_OR_' to '|'
$value = $this->getFieldFirstValue(str_replace('_OR_', '|', $key));
if($value !== null) return $value;
}
// optionally let a hook look at it
if($this->wire('hooks')->isHooked('Page::getUnknown()')) $value = $this->getUnknown($key);
}
return $value;
}
/**
* Get a Field object in context or NULL if not valid for this page
*
* Field in context is only returned when output formatting is on.
*
* #pw-advanced
*
* @param string|int|Field $field
* @return Field|null
* @throws WireException if given invalid argument
* @todo determine if we can always retrieve in context regardless of output formatting.
*
*/
public function getField($field) {
$template = $this->template;
$fieldgroup = $template ? $template->fieldgroup : null;
if(!$fieldgroup) return null;
if($this->outputFormatting && $fieldgroup->hasFieldContext($field)) {
$value = $fieldgroup->getFieldContext($field);
} else {
$value = $fieldgroup->getField($field);
}
return $value;
}
/**
* Returns a FieldsArray of all Field objects in the context of this Page
*
* Unlike $page->fieldgroup (or its alias $page->fields), the fields returned from
* this method are in the context of this page/template. Meaning returned Field
* objects may have some properties that are different from the Field outside of
* the context of this page.
*
* #pw-advanced
*
* @return FieldsArray of Field objects
*
*/
public function getFields() {
if(!$this->template) return new FieldsArray();
$fields = new FieldsArray();
$fieldgroup = $this->template->fieldgroup;
foreach($fieldgroup as $field) {
if($fieldgroup->hasFieldContext($field)) {
$field = $fieldgroup->getFieldContext($field);
}
if($field) $fields->add($field);
}
return $fields;
}
/**
* Returns whether or not $field is valid for this Page
*
* Note that this only indicates validity, not whether the field is populated.
*
* #pw-advanced
*
* @param int|string|Field $field Field name, object or ID to chck
* @return bool True if valid, false if not.
*
*/
public function hasField($field) {
return $this->template ? $this->template->fieldgroup->hasField($field) : false;
}
/**
* If given a field.subfield string, returns the associated value
*
* This is like the getDot() method, but with additional protection during output formatting.
*
* @param $key
* @return mixed|null
*
*/
protected function getFieldSubfieldValue($key) {
$value = null;
if(!strpos($key, '.')) return null;
if($this->outputFormatting()) {
// allow limited access to field.subfield properties when output formatting is on
// we only allow known custom fields, and only 1 level of subfield
list($key1, $key2) = explode('.', $key);
$field = $this->getField($key1);
if($field && !($field->flags & Field::flagSystem)) {
// known custom field, non-system
// if neither is an API var, then we'll allow it
if(!$this->wire($key1) && !$this->wire($key2)) $value = $this->getDot("$key1.$key2");
}
} else {
// we allow any field.subfield properties when output formatting is off
$value = $this->getDot($key);
}
return $value;
}
/**
* Hookable method called when a request to a field was made that didn't match anything
*
* Hooks that want to inject something here should hook after and modify the $event->return.
*
* #pw-hooker
*
* @param string $key Name of property.
* @return null|mixed Returns null if property not known, or a value if it is.
*
*/
public function ___getUnknown($key) {
// $key unused is intentional, for access by hooks
if($key) {}
return null;
}
/**
* Handles get() method requests for properties that include a period like "field.subfield"
*
* Typically these resolve to objects, and the subfield is pulled from the object.
* Currently we only allow this dot syntax when output formatting is off. This limitation may be removed
* but we have to consider potential security implications before doing so.
*
* #pw-internal
*
* @param string $key Property name in field.subfield format
* @return null|mixed Returns null if not found or invalid. Returns property value on success.
*
*/
public function getDot($key) {
if(strpos($key, '.') === false) return $this->get($key);
$of = $this->outputFormatting();
if($of) $this->setOutputFormatting(false);
$value = self::_getDot($key, $this);
if($of) $this->setOutputFormatting(true);
return $value;
}
/**
* Given a Multi Key, determine if there are multiple keys requested and return the first non-empty value
*
* A Multi Key is a string with multiple field names split by pipes, i.e. headline|title
*
* Example: browser_title|headline|title - Return the value of the first field that is non-empty
*
* @param string $multiKey
* @param bool $getKey Specify true to get the first matching key (name) rather than value
* @return null|mixed Returns null if no values match, or if there aren't multiple keys split by "|" chars
*
*/
protected function getFieldFirstValue($multiKey, $getKey = false) {
// looking multiple keys split by "|" chars, and not an '=' selector
if(strpos($multiKey, '|') === false || strpos($multiKey, '=') !== false) return null;
$value = null;
$keys = explode('|', $multiKey);
foreach($keys as $key) {
$value = $this->getUnformatted($key);
if(is_object($value)) {
// like LanguagesPageFieldValue or WireArray
$str = trim((string) $value);
if(!strlen($str)) continue;
} else if(is_array($value)) {
// array with no items
if(!count($value)) continue;
} else if(is_string($value)) {
$value = trim($value);
}
if($value) {
if($this->outputFormatting) $value = $this->get($key);
if($value) {
if($getKey) $value = $key;
break;
}
}
}
return $value;
}
/**
* Get the value for a non-native page field, and call upon Fieldtype to join it if not autojoined
*
* @param string $key Name of field to get
* @param string $selector Optional selector to filter load by...
* ...or, if not in selector format, it becomes an __invoke() argument for object values .
* @return null|mixed
*
*/
protected function getFieldValue($key, $selector = '') {
if(!$this->template) return parent::get($key);
$field = $this->getField($key);
$value = parent::get($key);
if(!$field) return $value; // likely a runtime field, not part of our data
$invokeArgument = '';
if($value !== null && isset($this->wakeupNameQueue[$key])) {
$value = $field->type->_callHookMethod('wakeupValue', array($this, $field, $value));
$value = $field->type->sanitizeValue($this, $field, $value);
$trackChanges = $this->trackChanges(true);
$this->setTrackChanges(false);
parent::set($key, $value);
$this->setTrackChanges($trackChanges);
unset($this->wakeupNameQueue[$key]);
}
if($field->useRoles && $this->outputFormatting) {
// API access may be limited when output formatting is ON
if($field->flags & Field::flagAccessAPI) {
// API access always allowed because of flag
} else if($this->viewable($field)) {
// User has view permission for this field
} else {
// API access is denied when output formatting is ON
// so just return a blank value as defined by the Fieldtype
// note: we do not store this blank value in the Page, so that
// the real value can potentially be loaded later without output formatting
$value = $field->type->getBlankValue($this, $field);
return $this->formatFieldValue($field, $value);
}
}
if(!is_null($value) && empty($selector)) {
// if the non-filtered value is already loaded, return it
return $this->formatFieldValue($field, $value);
}
$track = $this->trackChanges();
$this->setTrackChanges(false);
if(!$field->type) return null;
if($selector && !Selectors::stringHasSelector($selector)) {
// if selector argument provdied, but isn't valid, we assume it
// to instead be an argument for the value's __invoke() method
$invokeArgument = $selector;
$selector = '';
}
if($selector) {
$value = $field->type->loadPageFieldFilter($this, $field, $selector);
} else {
// $value = $field->type->loadPageField($this, $field);
$value = $field->type->_callHookMethod('loadPageField', array($this, $field));
}
if(is_null($value)) {
$value = $field->type->getDefaultValue($this, $field);
} else {
$value = $field->type->_callHookMethod('wakeupValue', array($this, $field, $value));
//$value = $field->type->wakeupValue($this, $field, $value);
}
// turn off output formatting and set the field value, which may apply additional changes
$outputFormatting = $this->outputFormatting;
if($outputFormatting) $this->setOutputFormatting(false);
$this->setFieldValue($key, $value, false);
if($outputFormatting) $this->setOutputFormatting(true);
$value = parent::get($key);
// prevent storage of value if it was filtered when loaded
if(!empty($selector)) $this->__unset($key);
if(is_object($value) && $value instanceof Wire) $value->resetTrackChanges(true);
if($track) $this->setTrackChanges(true);
$value = $this->formatFieldValue($field, $value);
if($invokeArgument && is_object($value) && method_exists($value, '__invoke')) {
$value = $value->__invoke($invokeArgument);
}
return $value;
}
/**
* Return a value consistent with the page’s output formatting state
*
* This is primarily for use as a helper to the getFieldValue() method.
*
* @param Field $field
* @param mixed $value
* @return mixed
*
*/
protected function formatFieldValue(Field $field, $value) {
$hasInterface = is_object($value) && $value instanceof PageFieldValueInterface;
if($hasInterface) {
$value->setPage($this);
$value->setField($field);
}
if($this->outputFormatting) {
// output formatting is enabled so return a formatted value
//$value = $field->type->formatValue($this, $field, $value);
$value = $field->type->_callHookMethod('formatValue', array($this, $field, $value));
// check again for interface since value may now be different
if($hasInterface) $hasInterface = is_object($value) && $value instanceof PageFieldValueInterface;
if($hasInterface) $value->formatted(true);
} else if($hasInterface && $value->formatted()) {
// unformatted requested, and value is already formatted so load a fresh copy
$this->__unset($field->name);
$value = $this->getFieldValue($field->name);
}
return $value;
}
/**
* Return the markup value for a given field name or {tag} string
*
* 1. If given a field name (or `name.subname` or `name1|name2|name3`) it will return the
* markup value as defined by the fieldtype.
* 2. If given a string with field names referenced in `{tags}`, it will populate those
* tags and return the populated string.
*
* #pw-advanced
*
* @param string $key Field name or markup string with field {name} tags in it
* @return string
* @see Page::getText()
*
*/
public function ___getMarkup($key) {
if(strpos($key, '{') !== false && strpos($key, '}')) {
// populate a string with {tags}
// note that the wirePopulateStringTags() function calls back on this method
// to retrieve the markup values for each of the found field names
return wirePopulateStringTags($key, $this);
}
if(strpos($key, '|') !== false) {
$key = $this->getFieldFirstValue($key, true);
if(!$key) return '';
}
if($this->wire('sanitizer')->name($key) != $key) {
// not a possible field name
return '';
}
$parts = strpos($key, '.') ? explode('.', $key) : array($key);
$value = $this;
do {
$name = array_shift($parts);
$field = $this->getField($name);
if(!$field && $this->wire($name)) {
// disallow API vars
$value = '';
break;
}
if($value instanceof Page) {
$value = $value->getFormatted($name);
} else if($value instanceof WireData) {
$value = $value->get($name);
} else {
$value = $value->$name;
}
if($field && count($parts) < 2) {
// this is a field that will provide its own formatted value
$subname = count($parts) == 1 ? array_shift($parts) : '';
if(!$subname || !$this->wire($subname)) {
$value = $field->type->markupValue($this, $field, $value, $subname);
}
}
} while(is_object($value) && count($parts));
if(is_object($value)) {
if($value instanceof Page) $value = $value->getFormatted('title|name');
if($value instanceof PageArray) $value = $value->getMarkup();
}
if(!is_string($value)) $value = (string) $value;
return $value;
}
/**
* Same as getMarkup() except returned value is plain text
*
* Returned value is entity encoded, unless $entities argument is false.
*
* #pw-advanced
*
* @param string $key Field name or string with field {name} tags in it.
* @param bool $oneLine Specify true if returned value must be on single line.
* @param bool|null $entities True to entity encode, false to not. Null for auto, which follows page's outputFormatting state.
* @return string
* @see Page::getMarkup()
*
*/
public function getText($key, $oneLine = false, $entities = null) {
$value = $this->getMarkup($key);
if(!strlen($value)) return '';
$options = array(
'entities' => (is_null($entities) ? $this->outputFormatting() : (bool) $entities)
);
if($oneLine) {
$value = $this->wire('sanitizer')->markupToLine($value, $options);
} else {
$value = $this->wire('sanitizer')->markupToText($value, $options);
}
return $value;
}
/**
* Get the unformatted value of a field, regardless of output formatting state
*
* When a page's output formatting state is off, `$page->get('property')` or `$page->property` will
* produce the same result as this method call.
*
* ~~~~~
* // Get the 'body' field without any text formatters applied
* $body = $page->getUnformatted('body');
* ~~~~~
*
* #pw-advanced
*
* @param string $key Field or property name to retrieve
* @return mixed
* @see Page::getFormatted(), Page::of()
*
*/
public function getUnformatted($key) {
$outputFormatting = $this->outputFormatting;
if($outputFormatting) $this->setOutputFormatting(false);
$value = $this->get($key);
if($outputFormatting) $this->setOutputFormatting(true);
return $value;
}
/**
* Get the formatted value of a field, regardless of output formatting state
*
* When a page's output formatting state is on, `$page->get('property')` or `$page->property` will
* produce the same result as this method call.
*
* ~~~~~
* // Get the formatted 'body' field (text formatters applied)
* $body = $page->getFormatted('body');
* ~~~~~
*
* #pw-advanced
*
* @param string $key Field or property name to retrieve
* @return mixed
* @see Page::getUnformatted(), Page::of()
*
*/
public function getFormatted($key) {
$outputFormatting = $this->outputFormatting;
if(!$outputFormatting) $this->setOutputFormatting(true);
$value = $this->get($key);
if(!$outputFormatting) $this->setOutputFormatting(false);
return $value;
}
/**
* Direct access get method
*
* @param string $key
* @return mixed
* @see get()
*
*/
public function __get($key) {
return $this->get($key);
}
/**
* Direct access set method
*
* @param string $key
* @param mixed $value
* @see set()
*
*/
public function __set($key, $value) {
$this->set($key, $value);
}
/**
* If method call resulted in no handler, this hookable method is called.
*
* If you want to override this method with a hook, see the example below.
* ~~~~~
* $wire->addHookBefore('Wire::callUnknown', function(HookEvent $event) {
* // Get information about unknown method that was called
* $methodObject = $event->object;
* $methodName = $event->arguments(0); // string
* $methodArgs = $event->arguments(1); // array
* // The replace option replaces the method and blocks the exception
* $event->replace = true;
* // Now do something with the information you have, for example
* // you might want to populate a value to $event->return if
* // you want the unknown method to return a value.
* });
* ~~~~~
*
* #pw-hooker
*
* @param string $method Requested method name
* @param array $arguments Arguments provided
* @return null|mixed Return value of method (if applicable)
* @throws WireException
* @see Wire::callUnknown()
*
*/
protected function ___callUnknown($method, $arguments) {
if($this->hasField($method)) {
if(count($arguments)) {
return $this->getFieldValue($method, $arguments[0]);
} else {
return $this->get($method);
}
} else {
return parent::___callUnknown($method, $arguments);
}
}
/**
* Set the status setting, with some built-in protections
*
* This method is also used when you set status directly, i.e. `$page->status = $value;`.
*
* ~~~~~
* // set status to unpublished
* $page->setStatus('unpublished');
*
* // set status to hidden and unpublished
* $page->setStatus('hidden, unpublished');
*
* // set status to hidden + unpublished using Page constant bitmask
* $page->setStatus(Page::statusHidden | Page::statusUnpublished);
* ~~~~~
*
* #pw-advanced
* #pw-group-manipulation
*
* @param int|array|string Status value, array of status names or values, or status name string.
* @see Page::addStatus(), Page::removeStatus()
*
*/
protected function setStatus($value) {
if(!is_int($value)) {
// status provided as something other than integer
if(is_string($value) && !ctype_digit($value)) {
// string of one or more status names
if(strpos($value, ',') !== false) $value = str_replace(array(', ', ','), ' ', $value);
$value = explode(' ', strtolower($value));
}
if(is_array($value)) {
// array of status names or numbers
$status = 0;
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];
}
}
if($status) $value = $status;
}
// note if $value started as an integer string, i.e. "123", it gets passed through to below
}
$value = (int) $value;
$override = $this->settings['status'] & Page::statusSystemOverride;
if(!$override) {
if($this->settings['status'] & Page::statusSystemID) $value = $value | Page::statusSystemID;
if($this->settings['status'] & Page::statusSystem) $value = $value | Page::statusSystem;
}
if($this->settings['status'] != $value) {
$this->trackChange('status', $this->settings['status'], $value);
$this->statusPrevious = $this->settings['status'];
}
$this->settings['status'] = $value;
if($value & Page::statusDeleted) {
// disable any instantiated filesManagers after page has been marked deleted
// example: uncache method polls filesManager
$this->filesManager = null;
}
}
/**
* Set the page name, optionally for specific language
*
* ~~~~~
* // Set page name (default language)
* $page->setName('my-page-name');
*
* // This is equivalent to the above
* $page->name = 'my-page-name';
*
* // Set page name for Spanish language
* $page->setName('la-cerveza', 'es');
* ~~~~~
*
* #pw-group-manipulation
*
* @param string $value Page name that you want to set
* @param Language|string|int|null $language Set language for name (can also be language name or string in format "name1234")
* @return $this
*
*/
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] : '';
}
if($this->isLoaded) {
// name is being set after page has already been loaded
if($charset === 'UTF8') {
// UTF8 page names allowed but decoding not allowed
$value = $sanitizer->pageNameUTF8($value);
} else if(empty($existingValue)) {
// ascii, and beautify if there is no existing value
$value = $sanitizer->pageName($value, true);
} else {
// ascii page name and do not beautify
$value = $sanitizer->pageName($value, false);
}
if($existingValue !== $value && !$this->quietMode) {
// set the namePrevious property when the main 'name' has changed
if($key === 'name' && $existingValue && empty($this->namePrevious)) {
$this->namePrevious = $existingValue;
}
// track the change
$this->trackChange($key, $existingValue, $value);
}
} else {
// name being set while page is loading
if($charset === 'UTF8' && strpos($value, 'xn-') === 0) {
// allow decode of UTF8 name while page is loading
$value = $sanitizer->pageName($value, Sanitizer::toUTF8);
} else {
// regular ascii page name while page is loading, do nothing to it
}
}
if($key === 'name') {
$this->settings[$key] = $value;
} else if($this->quietMode) {
parent::set($key, $value);
} else {
$this->setFieldValue($key, $value, $this->isLoaded); // i.e. name1234
}
return $this;
}
/**
* Set this Page's Template
*
* ~~~~~
* // The following 3 lines are equivalent
* $page->setTemplate('basic-page');
* $page->template = 'basic-page';
* $page->templates = $templates->get('basic-page');
* ~~~~~
*
* #pw-internal
*
* @param Template|int|string $tpl May be Template object, name or ID.
* @return $this
* @throws WireException if given invalid arguments or template not allowed for page
*
*/
protected function setTemplate($tpl) {
if(!is_object($tpl)) $tpl = $this->wire('templates')->get($tpl);
if(!$tpl instanceof Template) throw new WireException("Invalid value sent to Page::setTemplate");
if($this->template && $this->template->id != $tpl->id && $this->isLoaded) {
if($this->settings['status'] & Page::statusSystem) {
throw new WireException("Template changes are disallowed on this page");
}
if(is_null($this->templatePrevious)) $this->templatePrevious = $this->template;
$this->trackChange('template', $this->template, $tpl);
}
if($tpl->sortfield) $this->settings['sortfield'] = $tpl->sortfield;
$this->template = $tpl;
return $this;
}
/**
* Set this page's parent Page
*
* #pw-internal
*
* @param Page $parent
* @return $this
* @throws WireException if given impossible $parent or parent changes aren't allowed
*
*/
public function setParent(Page $parent) {
if($this->_parent && $this->_parent->id == $parent->id) return $this;
if($parent->id && $this->id == $parent->id || $parent->parents->has($this)) {
throw new WireException("Page cannot be its own parent");
}
if($this->isLoaded) {
if(!$this->_parent) $this->parent(); // force it to load
$this->trackChange('parent', $this->_parent, $parent);
if(($this->_parent && $this->_parent->id) && $this->_parent->id != $parent->id) {
if($this->settings['status'] & Page::statusSystem) {
throw new WireException("Parent changes are disallowed on this page");
}
if(is_null($this->parentPrevious)) $this->parentPrevious = $this->_parent;
}
}
$this->_parent = $parent;
$this->_parent_id = $parent->id;
return $this;
}
/**
* Set either the createdUser or the modifiedUser
*
* @param User|int|string $user User object or integer/string representation of User
* @param string $userType Must be either 'created' or 'modified'
* @return $this
* @throws WireException
*
*/
protected function setUser($user, $userType) {
if(!$user instanceof User) {
if(is_object($user)) {
$user = null;
} else {
$user = $this->wire('users')->get($user);
}
}
// if they are setting an invalid user or unknown user, then the Page defaults to the super user
if(!$user || !$user->id || !$user instanceof User) {
$user = $this->wire('users')->get($this->wire('config')->superUserPageID);
}
if($userType == 'created') {
$field = 'created_users_id';
$this->createdUser = $user;
} else if($userType == 'modified') {
$field = 'modified_users_id';
$this->modifiedUser = $user;
} else {
throw new WireException("Unknown user type in Page::setUser(user, type)");
}
$existingUserID = $this->settings[$field];
if($existingUserID != $user->id) $this->trackChange($field, $existingUserID, $user->id);
$this->settings[$field] = $user->id;
return $this;
}
/**
* Find pages matching given selector in the descendent hierarchy
*
* This is the same as `Pages::find()` except that the results are limited to descendents of this Page.
*
* ~~~~~
* // Find all unpublished pages underneath the current page
* $items = $page->find("status=unpublished");
* ~~~~~
*
* #pw-group-common
* #pw-group-traversal
*
* @param string|array $selector Selector string or array
* @param array $options Same as the $options array passed to $pages->find().
* @return PageArray
* @see Pages::find()
*
*/
public function find($selector = '', $options = array()) {
if(!$this->numChildren) return $this->wire('pages')->newPageArray();
if(is_string($selector)) {
$selector = trim("has_parent={$this->id}, $selector", ", ");
} else if(is_array($selector)) {
$selector["has_parent"] = $this->id;
}
return $this->_pages('find', $selector, $options);
}
/**
* Return this page’s children, optionally filtered by a selector
*
* By default, hidden, unpublished and no-access pages are excluded unless `include=x` (where "x" is desired status) is specified.
* If a selector isn't needed, children can also be accessed directly by property with `$page->children`.
*
* ~~~~~
* // Render navigation for all child pages below this one
* foreach($page->children() as $child) {
* echo "<li><a href='$child->url'>$child->title</a></li>";
* }
* ~~~~~
* ~~~~~
* // Retrieve just the 3 newest children
* $newest = $page->children("limit=3, sort=-created");
* ~~~~~
*
* #pw-group-common
* #pw-group-traversal
*
* @param string $selector Selector to use, or omit to return all children.
* @param array $options Optional options to modify behavior, the same as those provided to Pages::find.
* @return PageArray|array Returns PageArray for most cases. Returns regular PHP array if using the findIDs option.
* @see Page::child(), Page::find(), Page::numChildren(), Page::hasChildren()
*
*/
public function children($selector = '', $options = array()) {
return $this->traversal()->children($this, $selector, $options);
}
/**
* Return number of all children, optionally with conditions
*
* Use this over the `$page->numChildren` property when you want to specify a selector, or when you want the result to
* include only visible children. See the options for the $selector argument.
*
* When you want to retrieve all children with no exclusions or conditions, use the `$page->numChildren` property instead.
*
* ~~~~~
* // Find how many children were modified in the last week
* $qty = $page->numChildren("modified>='-1 WEEK'");
* ~~~~~
*
* #pw-group-common
* #pw-group-traversal
*
* @param bool|string|array $selector
* - When not specified, result includes all children without conditions, same as $page->numChildren property.
* - When a string or array, a selector is assumed and quantity will be counted based on selector.
* - When boolean true, number includes only visible children (excludes unpublished, hidden, no-access, etc.)
* - When boolean false, number includes all children without conditions, including unpublished, hidden, no-access, etc.
* @return int Number of children
* @see Page::hasChildren(), Page::children(), Page::child()
*
*/
public function numChildren($selector = null) {
if(!$this->settings['numChildren'] && is_null($selector)) return $this->settings['numChildren'];
return $this->traversal()->numChildren($this, $selector);
}
/**
* Return the number of visible children, optionally with conditions
*
* This method is similar to `$page->numChildren()` except that the default behavior is to exclude non-visible children.
*
* This method may be more convenient for front-end navigation use than the `$page->numChildren()` method because
* it only includes the count of visible children. By visible, we mean children that are not hidden, unpublished,
* or non-accessible due to access control.
*
* ~~~~~
* // Determine if we should show navigation to children
* if($page->hasChildren()) {
* // Yes, we should show navigation to children
* }
* ~~~~~
*
* #pw-group-common
* #pw-group-traversal
*
* @param bool|string|array $selector
* - When not specified, result is quantity of visible children (excludes unpublished, hidden, no-access, etc.)
* - When a string or array, a selector is assumed and quantity will be counted based on selector.
* - When boolean true, number includes only visible children (this is the default behavior, so no need to specify this value).
* - When boolean false, number includes all children without conditions, including unpublished, hidden, no-access, etc.
* @return int Number of children
*
*/
public function hasChildren($selector = true) {
return $this->numChildren($selector);
}
/**
* Return the page’s first single child that matches the given selector.
*
* Same as `$page->children()` but returns a single Page object or NullPage (with id=0) rather than a PageArray.
* Meaning, this method will only ever return one Page.
*
* ~~~~~
* // Get the newest created child page
* $newestChild = $page->child("sort=-created");
* ~~~~~
*
* #pw-group-common
* #pw-group-traversal
*
* @param string|array|int $selector Selector to use, or blank to return the first child.
* @param array $options Optional options per Pages::find
* @return Page|NullPage
* @see Page::children()
*
*/
public function child($selector = '', $options = array()) {
return $this->traversal()->child($this, $selector, $options);
}
/**
* Return this page’s parent Page, or–if given a selector–the closest matching parent.
*
* Omit all arguments if you just want to retrieve the parent of this page, which would be the same as the
* `$page->parent` property. To retrieve the closest parent matching your selector, specify either a selector
* string or array.
*
* ~~~~~
* // Retrieve the parent
* $parent = $page->parent();
*
* // Retrieve the closest parent using template "products"
* $parent = $page->parent("template=products");
* ~~~~~
*
* #pw-group-common
* #pw-group-traversal
*
* @param string|array $selector Optional selector. When used, it returns the closest parent matching the selector.
* @return Page Returns a Page or a NullPage when there is no parent or the selector string did not match any parents.
*
*/
public function parent($selector = '') {
if(!$this->_parent) {
if($this->_parent_id) {
$this->_parent = $this->_pages('get', (int) $this->_parent_id);
} else {
return $this->wire('pages')->newNullPage();
}
}
if(empty($selector)) return $this->_parent;
if($this->_parent->matches($selector)) return $this->_parent;
if($this->_parent->parent_id) return $this->_parent->parent($selector); // recursive, in a way
return $this->wire('pages')->newNullPage();
}
/**
* Return this page’s parent pages, or the parent pages matching the given selector.
*
* This method returns all parents of this page, in order. If a selector is specified, they
* will be filtered by the selector.
*
* ~~~~~
* // Render breadcrumbs
* foreach($page->parents() as $parent) {
* echo "<li><a href='$parent->url'>$parent->title</a></li>";
* }
* ~~~~~
* ~~~~~
* // Return all parents, excluding the homepage
* $parents = $page->parents("template!=home");
* ~~~~~
*
* #pw-group-common
* #pw-group-traversal
*
* @param string|array $selector Optional selector string to filter parents by.
* @return PageArray All parent pages, or those matching the given selector.
*
*/
public function parents($selector = '') {
return $this->traversal()->parents($this, $selector);
}
/**
* Return all parents from current page till the one matched by $selector
*
* This duplicates the jQuery parentsUntil() function in ProcessWire.
*
* #pw-group-traversal
*
* @param string|Page|array $selector May either be a selector sor Page to stop at. Results will not include this.
* @param string|array $filter Optional selector to filter matched pages by
* @return PageArray
*
*/
public function parentsUntil($selector = '', $filter = '') {
return $this->traversal()->parentsUntil($this, $selector, $filter);
}
/**
* Find the closest parent page matching your selector
*
* This is like `$page->parent()` but includes the current Page in the possible pages that can be matched,
* and the $selector argument is required.
*
* #pw-group-traversal
*
* @param string|array $selector Selector string to match.
* @return Page|NullPage $selector Returns the current Page or closest parent matching the selector. Returns NullPage when no match.
*
*/
public function closest($selector) {
if(empty($selector) || $this->matches($selector)) return $this;
return $this->parent($selector);
}
/**
* Get the lowest-level, non-homepage parent of this page
*
* The rootParents typically comprise the first level of navigation on a site, and in many cases are considered
* the "section" pages of the site.
*
* ~~~~~
* // Determine if we are in the "products" section of the site
* if($page->rootParent()->template == 'products') {
* // we are in the products section
* } else {
* // we are in some other section of the site
* }
* ~~~~~
*
* #pw-group-common
* #pw-group-traversal
*
* @return Page
*
*/
public function ___rootParent() {
return $this->traversal()->rootParent($this);
}
/**
* Return this Page’s sibling pages, optionally filtered by a selector.
*
* To exclude the current page in list of siblings, specify boolean false for first or second argument.
*
* ~~~~~
* // Get all sibling pages
* $siblings = $page->siblings();
*
* // Get all sibling pages, and exclude current page from the returned value
* $siblings = $page->siblings(false);
*
* // Get all siblings having the "product-featured" template, sorted by name
* $featured = $page->siblings("template=product-featured, sort=name");
*
* // Same as above, while excluding current page
* $featured = $page->siblings("template=product-featured, sort=name", false);
* ~~~~~
*
* #pw-group-traversal
*
* @param string|array|bool $selector Optional selector to filter siblings by, or omit for all siblings.
* @param bool $includeCurrent Specify false to exclude current page in the returned siblings (default=true).
* If no $selector argument is given, this argument may optionally be specified as the first argument.
* @return PageArray
*
*/
public function siblings($selector = '', $includeCurrent = true) {
if(is_bool($selector)) {
$includeCurrent = $selector;
$selector = '';
}
if(!$includeCurrent) {
if(is_array($selector)) {
$selector[] = array('id', '!=', $this->id);
} else {
if(strlen($selector)) $selector .= ", ";
$selector .= "id!=$this->id";
}
}
return $this->traversal()->siblings($this, $selector);
}
/**
* Return the next sibling page
*
* By default, hidden, unpublished and non-viewable pages are excluded. If you want them included,
* be sure to specify `include=` with hidden, unpublished or all, in your selector.
*
* ~~~~~
* // Get the next sibling
* $sibling = $page->next();
*
* // Get the next newest sibling
* $sibling = $page->next("created>$page->created");
*
* // Get the next sibling, even if it isn't viewable
* $sibling = $page->next("include=all");
* ~~~~~
*
* #pw-group-traversal
*
* @param string|array $selector Optional selector. When specified, will find nearest next sibling that matches.
* @param PageArray $siblings DEPRECATED: Optional siblings to use instead of the default. Avoid using this argument
* as it forces this method to use the older/slower functions.
* @return Page|NullPage Returns the next sibling page, or a NullPage if none found.
*
*/
public function next($selector = '', PageArray $siblings = null) {
if(is_object($selector) && $selector instanceof PageArray) {
$siblings = $selector;
$selector = '';
}
if($siblings) return $this->traversal()->nextSibling($this, $selector, $siblings);
return $this->traversal()->next($this, $selector);
}
/**
* Return all sibling pages after this one, optionally matching a selector
*
* #pw-group-traversal
*
* @param string|array|bool $selector Optional selector. When specified, will filter the found siblings.
* @param bool|PageArray $getQty Return a count instead of PageArray? (boolean)
* - If no $selector argument is needed, this may be specified as the first argument.
* - Legacy support: You may specify a PageArray of siblings to use instead of the default (deprecated, avoid it).
* @param bool $getPrev For internal use, makes this method implement the prevAll() behavior instead.
* @return PageArray|int Returns all matching pages after this one, or integer if $count option specified.
*
*/
public function nextAll($selector = '', $getQty = false, $getPrev = false) {
$siblings = null;
if(is_object($selector) && $selector instanceof PageArray) {
$siblings = $selector;
$selector = '';
}
if(is_object($getQty) && $getQty instanceof PageArray) {
$siblings = $getQty;
$getQty = false;
}
if(is_bool($selector)) {
$getQty = $selector;
$selector = '';
}
if($getPrev) {
if($siblings) return $this->traversal()->prevAllSiblings($this, $selector, $siblings);
return $this->traversal()->prevAll($this, $selector, array('qty' => $getQty));
}
if($siblings) return $this->traversal()->nextAllSiblings($this, $selector, $siblings);
return $this->traversal()->nextAll($this, $selector, array('qty' => $getQty));
}
/**
* Return all sibling pages after this one until matching the one specified
*
* #pw-group-traversal
*
* @param string|Page|array $selector May either be a selector or Page to stop at. Results will not include this.
* @param string|array $filter Optional selector to filter matched pages by
* @param PageArray $siblings DEPRECATED: Optional PageArray of siblings to use instead (avoid).
* @return PageArray
*
*/
public function nextUntil($selector = '', $filter = '', PageArray $siblings = null) {
if($siblings) return $this->traversal()->nextUntilSiblings($this, $selector, $filter, $siblings);
return $this->traversal()->nextUntil($this, $selector, $filter);
}
/**
* Return the previous sibling page
*
* ~~~~~
* // Get the previous sibling
* $sibling = $page->prev();
*
* // Get the previous sibling having field "featured" with value of "1"
* $sibling = $page->prev("featured=1");
* ~~~~~
*
* #pw-group-traversal
*
* @param string|array $selector Optional selector. When specified, will find nearest previous sibling that matches.
* @param PageArray|null $siblings DEPRECATED: $siblings Optional siblings to use instead of the default.
* @return Page|NullPage Returns the previous sibling page, or a NullPage if none found.
*
*/
public function prev($selector = '', PageArray $siblings = null) {
if(is_object($selector) && $selector instanceof PageArray) {
$siblings = $selector;
$selector = '';
}
if($siblings) return $this->traversal()->prevSibling($this, $selector, $siblings);
return $this->traversal()->prev($this, $selector);
}
/**
* Return all sibling pages before this one, optionally matching a selector
*
* #pw-group-traversal
*
* @param string|array|bool $selector Optional selector. When specified, will filter the found siblings.
* @param bool|PageArray $getQty Return a count instead of PageArray? (boolean)
* - If no $selector argument is needed, this may be specified as the first argument.
* - Legacy support: You may specify a PageArray of siblings to use instead of the default (deprecated, avoid it).
* @return Page|NullPage|int Returns all matching pages before this one, or integer if $getQty requested.
*
*/
public function prevAll($selector = '', $getQty = false) {
return $this->nextAll($selector, $getQty, true);
}
/**
* Return all sibling pages before this one until matching the one specified
*
* #pw-group-traversal
*
* @param string|Page|array $selector May either be a selector or Page to stop at. Results will not include this.
* @param string|array $filter Optional selector to filter matched pages by
* @param PageArray|null $siblings DEPRECATED: Optional PageArray of siblings to use instead of default.
* @return PageArray
*
*/
public function prevUntil($selector = '', $filter = '', PageArray $siblings = null) {
if($siblings) return $this->traversal()->prevUntilSiblings($this, $selector, $filter, $siblings);
return $this->traversal()->prevUntil($this, $selector, $filter);
}
/**
* Get languages active for this page and viewable by current user
*
* #pw-group-languages
*
* @return PageArray|null Returns PageArray of languages, or null if language support is not active.
*
*/
public function getLanguages() {
$languages = $this->wire('pages')->newPageArray();
$templateLanguages = $this->template->getLanguages();
if(!$templateLanguages) return null;
foreach($templateLanguages as $language) {
if($this->viewable($language, false)) $languages->add($language);
}
return $languages;
}
/**
* Save the entire page to the database, or just a field from it
*
* This is the same as calling `$pages->save($page);` or `$pages->saveField($page, $field)`, but calling directly
* on the $page like this may be more convenient in many instances.
*
* If you want to hook into the save operation, hook into one of the many Pages class hooks referenced in the 'See Also' section.
*
* ~~~~~
* // Save the page
* $page->save();
*
* // Save just the 'title' field from the page
* $page->save('title');
* ~~~~~
*
* #pw-group-common
* #pw-group-manipulation
*
* @param Field|string $field Optional field to save (name of field or Field object)
* @param array $options See Pages::save() documentation for options. You may also specify $options as the first argument if no $field is needed.
* @return bool Returns true on success false on fail
* @throws WireException on database error
* @see Pages::save(), Pages::saveField(), Pages::saveReady(), Pages::saveFieldReady(), Pages::saved(), Pages::fieldSaved()
*
*/
public function save($field = null, array $options = array()) {
if(is_array($field) && empty($options)) {
$options = $field;
$field = null;
}
if(!is_null($field)) {
if($this->hasField($field)) {
return $this->wire('pages')->saveField($this, $field, $options);
} else if(is_string($field) && (isset($this->settings[$field]) || parent::get($field) !== null)) {
$options['noFields'] = true;
return $this->wire('pages')->save($this, $options);
} else {
return false;
}
}
return $this->wire('pages')->save($this, $options);
}
/**
* Quickly set field value(s) and save to database
*
* You can specify a single vield and value, or an array of fields and values.
*
* This method does not need output formatting to be turned off first, so make sure that whatever
* value(s) you set are not formatted values.
*
* ~~~~~
* // Set and save the summary field
* $page->setAndSave('summary', 'When nothing is done, nothing is left undone.');
* ~~~~~
* ~~~~~
* // Set and save multiple fields
* $page->setAndSave([
* 'title' => 'It is Friday again',
* 'subtitle' => 'Here is another new blog post',
* 'body' => 'Hope you all have a great weekend!'
* ]);
* ~~~~~
* ~~~~~
* // Update a 'last_login' field after every user login
* $session->addHookAfter('loginSuccess', function($event) {
* $user = $event->arguments(0);
* $user->setAndSave('last_login', time());
* });
* ~~~~~
*
* #pw-group-manipulation
* #pw-links [Blog post about setAndSave](https://processwire.com/blog/posts/processwire-2.6.9-core-updates-and-new-procache-version/)
*
* @param array|string $key Field or property name to set, or array of one or more ['property' => $value].
* @param string|int|bool|object $value Value to set, or omit if you provided an array in first argument.
* @param array $options See Pages::save() for additional $options that may be specified.
* @return bool Returns true on success, false on failure
* @see Pages::save()
*
*/
public function setAndSave($key, $value = null, array $options = array()) {
if(is_array($key)) {
$values = $key;
$property = count($values) == 1 ? key($values) : '';
} else {
$property = $key;
$values = array($key => $value);
}
$of = $this->of();
if($of) $this->of(false);
foreach($values as $k => $v) {
$this->set($k, $v);
}
if($property) {
$result = $this->save($property, $options);
} else {
$result = $this->save($options);
}
if($of) $this->of(true);
return $result;
}
/**
* Delete this page from the database
*
* This is the same as calling `$pages->delete($page)`.
*
* ~~~~~
* // Delete pages named "delete-me" that don't have children
* $items = $pages->find("name=delete-me, numChildren=0");
* foreach($items as $item) {
* $item->delete();
* }
* ~~~~~
* ~~~~~
* // Delete a page and recursively all of its children, grandchildren, etc.
* $item = $pages->get('/some-page/');
* $item->delete(true);
* ~~~~~
*
* #pw-group-manipulation
*
* @param bool $recursive If set to true, then this will attempt to delete all children too.
* @return bool True on success, false on failure.
* @throws WireException when attempting to delete a page with children and $recursive option is not specified.
* @see Pages::delete()
*
*/
public function delete($recursive = false) {
return $this->wire('pages')->delete($this, $recursive);
}
/**
* Move this page to the trash
*
* This is the same as calling `$pages->trash($page)`.
*
* ~~~~~
* // Trash a page
* $item = $pages->get('/some-page/');
* $item->trash();
* ~~~~~
*
* #pw-group-manipulation
*
* @return bool True on success, false on failure
* @throws WireException
*
*/
public function trash() {
return $this->wire('pages')->trash($this);
}
/**
* Returns number of children page has, affected by output formatting mode.
*
* - When output formatting is on, returns only number of visible children,
* making the return value the same as the `Page::hasChildren()` method.
*
* - When output formatting is off, returns number of all children without exclusion,
* making the return value the same as the `Page::numChildren()` method.
*
* ~~~~~
* // Get number of visible children, like $page->hasChildren()
* $page->of(true); // enable output formatting
* $numVisible = $page->count();
*
* // Get number of all children, like $page->numChildren()
* $page->of(false); // disable output formatting
* $numTotal = $page->count();
* ~~~~~
*
* #pw-advanced
*
* @return int Quantity of children
* @see Page::hasChildren(), Page::numChildren()
*
*/
public function count() {
if($this->outputFormatting) return $this->numChildren(true);
return $this->numChildren(false);
}
/**
* Enables iteration of the page's properties and fields with PHP’s foreach()
*
* This fulfills PHP's IteratorAggregate interface, enabling you to interate all of the page's properties and fields.
*
* ~~~~~
* // List all properties and fields from the page
* foreach($page as $name => $value) {
* echo "<h3>$name</h3>";
* echo "<p>$value</p>";
* }
* ~~~~~
*
* #pw-advanced
*
* @return \ArrayObject
*
*/
public function getIterator() {
$a = $this->settings;
if($this->template && $this->template->fieldgroup) {
foreach($this->template->fieldgroup as $field) {
$a[$field->name] = $this->get($field->name);
}
}
return new \ArrayObject($a);
}
/**
* Has the Page changed since it was loaded?
*
* To check if only a specific property on the page has changed, specify the property/field name as the first argument.
* This method assumes that change tracking is enabled for the Page (as it is by default).
* Pages that are new (i.e. don't yet exist in the database) always return true.
*
* #pw-group-manipulation
*
* ~~~~~
* // Check if page has any changes
* if($page->isChanged()) {
* // There are changes to this page
* $changes = $page->getChanges();
* }
* ~~~~~
* ~~~~~
* // When page is about to be saved, update summary when body has changed
* $this->addHookBefore('Pages::saveReady', function($event) {
* $page = $event->arguments('page');
* if($page->isChanged('body')) {
* // get first 300 chars from body
* $summary = substr($page->body, 0, 300);
* // truncate to position of last period
* $period = strrpos($summary, '.');
* if($period) $summary = substr($summary, 0, $period);
* // populate to the page, so that summary is also saved
* $page->summary = $summary;
* }
* });
* ~~~~~
*
* @param string $what If specified, only checks the given property for changes rather than the whole page.
* @return bool
* @see Wire::setTrackChanges(), Wire::getChanges(), Wire::trackChange()
*
*/
public function isChanged($what = '') {
if($this->isNew()) return true;
if(parent::isChanged($what)) return true;
$changed = false;
if($what) {
$data = array_key_exists($what, $this->data) ? array($this->data[$what]) : array();
} else {
$data = &$this->data;
}
foreach($data as $key => $value) {
if(is_object($value) && $value instanceof Wire) {
$changed = $value->isChanged();
}
if($changed) break;
}
return $changed;
}
/**
* Clears out any tracked changes and turns change tracking ON or OFF
*
* Use this method when you want to clear a list of tracked changes on the page. Note that any changes are still
* present, but ProcessWire no longer knows they had been changed. Meaning, the changes won't be available to
* the `$page->isChanged()` and `$page->getChanges()` methods, and the changes might be skipped over if/when
* the page is saved.
*
* #pw-group-manipulation
*
* @param bool $trackChanges True to turn change tracking ON, or false to turn OFF. Default of true is assumed.
* @return $this
* @see Page::isChanged(), Page::getChanges(), Page::trackChanges()
*
*/
public function resetTrackChanges($trackChanges = true) {
parent::resetTrackChanges($trackChanges);
foreach($this->data as $key => $value) {
if(is_object($value) && $value instanceof Wire && $value !== $this) $value->resetTrackChanges($trackChanges);
}
return $this;
}
/**
* Returns the Page's ID in a string
*
* @return string
*
*/
public function __toString() {
return "{$this->id}";
}
/**
* Returns the Page’s path from the ProcessWire installation root.
*
* The path is always indicated from the ProcessWire installation root. Meaning, if the installation is
* running from a subdirectory, then the path does not include that subdirectory, whereas the url does.
* Note that path and url are identical if installation is not running from a subdirectory.
*
* #pw-hookable
* #pw-group-common
*
* ~~~~~
* // Difference between path and url on site running from subdirectory /my-site/
* echo $page->path(); // outputs: /about/contact/
* echo $page->url(); // outputs: /my-site/about/contact/
* ~~~~~
*
* @return string Returns the page path, for example: `/about/contact/`
* @see Page::url(), Page::httpUrl()
*
*/
public function path() {
return $this->wire('hooks')->isHooked('Page::path()') ? $this->__call('path', array()) : $this->___path();
}
/**
* Provides the hookable implementation for the path() method.
*
* The method we're using here by having a real path() function above is slightly quicker than just letting
* PW's hook handler handle it all. We're taking this approach since path() is a function that can feasibly
* be called hundreds or thousands of times in a request, so we want it as optimized as possible.
*
* #pw-internal
*
*/
protected function ___path() {
if($this->id === 1) return '/';
$path = '';
$parents = $this->parents();
foreach($parents as $parent) if($parent->id > 1) $path .= "/{$parent->name}";
return $path . '/' . $this->name . '/';
}
/**
* Returns the URL to the page (optionally with additional $options)
*
* - This method can also be accessed by property `$page->url` (without parenthesis).
*
* - Like `$page->path()` but comes from server document root. Path and url are identical if
* installation is not running from a subdirectory.
*
* - Use `$page->httpUrl()` if you need the URL to include scheme and hostname.
*
* - **Need to hook this method?** While it's not directly hookable, it does use the `$page->path()`
* method, which *is* hookable. As a result, you can affect the output of the url() method by
* hooking the path() method instead.
*
* ## $options argument
*
* You can specify an `$options` argument to this method with any of the following:
*
* - `pageNum` (int|string): Specify pagination number, or "+" for next pagination, or "-" for previous pagination.
* - `urlSegmentStr` (string): Specify a URL segment string to append.
* - `urlSegments` (array): Specify array of URL segments to append (may be used instead of urlSegmentStr).
* - `data` (array): Array of key=value variables to form a query string.
* - `http` (bool): Specify true to make URL include scheme and hostname (default=false).
* - `language` (Language): Specify Language object to return URL in that Language.
*
* You can also specify any of the following for `$options` as shortcuts:
*
* - If you specify an `int` for options it is assumed to be the `pageNum` option.
* - If you specify `+` or `-` for options it is assumed to be the `pageNum` “next/previous pagination” option.
* - If you specify any other `string` for options it is assumed to be the `urlSegmentStr` option.
* - If you specify a `boolean` (true) for options it is assumed to be the `http` option.
*
* Please also note regarding `$options`:
*
* - This method honors template slash settings for page, URL segments and page numbers.
* - Any passed in URL segments are automatically sanitized with `Sanitizer::pageNameUTF8()`.
* - If using the `pageNum` or URL segment options please also make sure these are enabled on the page’s template.
* - The query string generated by any `data` variables is entity encoded when output formatting is on.
* - The `language` option requires that the `LanguageSupportPageNames` module is installed.
* - The prefix for page numbers honors `$config->pageNumUrlPrefix` and multi-language prefixes as well.
*
* ~~~~~
* // Using $page->url to output navigation
* foreach($page->children as $child) {
* echo "<li><a href='$child->url'>$child->title</a></li>";
* }
* ~~~~~
* ~~~~~
* // Difference between url() and path() on site running from subdirectory /my-site/
* echo $page->url(); // outputs: /my-site/about/contact/
* echo $page->path(); // outputs: /about/contact/
* ~~~~~
* ~~~~~
* // Specify that you want a specific pagination (output: /example/page2)
* echo $page->url(2);
*
* // Get URL for next and previous pagination
* echo $page->url('+'); // next
* echo $page->url('-'); // prev
*
* // Get a URL with scheme and hostname (output: http://domain.com/example/)
* echo $page->url(true);
*
* // Specify a URL segment string (output: /example/photos/1)
* echo $page->url('photos/1');
*
* // Use a URL segment array (output: /example/photos/1)
* echo $page->url([
* 'urlSegments' => [ 'photos', '1' ]
* ]);
*
* // Get URL in a specific language
* $fr = $languages->get('fr');
* echo $page->url($fr);
*
* // Include data/query vars (output: /example/?action=view&type=photos)
* echo $page->url([
* 'data' => [
* 'action' => 'view',
* 'type' => 'photos'
* ]
* ]);
*
* // Specify multiple options (output: http://domain.com/example/foo/page3?bar=baz)
* echo $page->url([
* 'http' => true,
* 'pageNum' => 3,
* 'urlSegmentStr' => 'foo',
* 'data' => [ 'bar' => 'baz' ]
* ]);
* ~~~~~
*
* @param array|int|string|bool|Language|null $options Optionally specify options to modify default behavior (see method description).
* @return string Returns page URL, for example: `/my-site/about/contact/`
* @see Page::path(), Page::httpUrl(), Page::editUrl(), Page::localUrl()
*
*/
public function url($options = null) {
if($options !== null) return $this->traversal()->urlOptions($this, $options);
$url = rtrim($this->wire('config')->urls->root, "/") . $this->path();
if($this->template->slashUrls === 0 && $this->settings['id'] > 1) $url = rtrim($url, '/');
return $url;
}
/**
* Returns the URL to the page, including scheme and hostname
*
* - This method is just like the `$page->url()` method except that it also includes scheme and hostname.
*
* - This method can also be accessed at the property `$page->httpUrl` (without parenthesis).
*
* - It is desirable to use this method when some page templates require https while others don't.
* This ensures local links will always point to pages with the proper scheme. For other cases, it may
* be preferable to use `$page->url()` since it produces shorter output.
*
* ~~~~~
* // Generating a link to this page using httpUrl
* echo "<a href='$page->httpUrl'>$page->title</a>";
* ~~~~~
*
* @param array $options For details on usage see `Page::url()` options argument.
* @return string Returns full URL to page, for example: `https://processwire.com/about/`
* @see Page::url(), Page::localHttpUrl()
*
*/
public function httpUrl($options = array()) {
if(!$this->template) return '';
switch($this->template->https) {
case -1: $protocol = 'http'; break;
case 1: $protocol = 'https'; break;
default: $protocol = $this->wire('config')->https ? 'https' : 'http';
}
if(is_array($options)) unset($options['http']);
else if(is_bool($options)) $options = array();
return "$protocol://" . $this->wire('config')->httpHost . $this->url($options);
}
/**
* Return the URL necessary to edit this page
*
* - We recommend checking that the page is editable before outputting the editUrl().
* - If user opens URL in their browser and is not logged in, they must login to account with edit permission.
* - This method can also be accessed by property at `$page->editUrl` (without parenthesis).
*
* ~~~~~~
* if($page->editable()) {
* echo "<a href='$page->editUrl'>Edit this page</a>";
* }
* ~~~~~~
*
* #pw-group-advanced
*
* @param array|bool $options Specify boolean true to force URL to include scheme and hostname, or use $options array:
* - `http` (bool): True to force scheme and hostname in URL (default=auto detect).
* @return string URL for editing this page
*
*/
public function editUrl($options = array()) {
$adminTemplate = $this->wire('templates')->get('admin');
$https = $adminTemplate && ($adminTemplate->https > 0);
$url = ($https && !$this->wire('config')->https) ? 'https://' . $this->wire('config')->httpHost : '';
$url .= $this->wire('config')->urls->admin . "page/edit/?id=$this->id";
if($options === true || (is_array($options) && !empty($options['http']))) {
if(strpos($url, '://') === false) {
$url = ($https ? 'https://' : 'http://') . $this->wire('config')->httpHost . $url;
}
}
$append = $this->wire('session')->getFor($this, 'appendEditUrl');
if($append) $url .= $append;
return $url;
}
/**
* Return the field name by which children are sorted
*
* - If sort is descending, then field name is prepended with a "-".
* - Returns the value "sort" if pages are unsorted or sorted manually.
* - Note the return value from this method may be different from the `Page::sortfield` (lowercase) property,
* as this method considers the sort field specified with the template as well.
*
* #pw-group-internal
*
* @return string
*
*/
public function sortfield() {
$sortfield = $this->template ? $this->template->sortfield : '';
if(!$sortfield) $sortfield = $this->sortfield;
if(!$sortfield) $sortfield = 'sort';
return $sortfield;
}
/**
* Return the index/position of this page relative to siblings.
*
* ~~~~~
* $i = $page->index();
* $n = $page->parent->numChildren();
* echo "This page is $i out of $n total pages";
* ~~~~~
*
* #pw-group-traversal
*
* @return int Returns index number (zero-based)
* @since 3.0.24
*
*/
public function index() {
return $this->traversal()->index($this);
}
/**
* Get the output TemplateFile object for rendering this page (internal use only)
*
* You can retrieve the results of this by calling $page->out or $page->output
*
* #pw-internal
*
* @param bool $forceNew Forces it to return a new (non-cached) TemplateFile object (default=false)
* @return TemplateFile
*
*/
public function output($forceNew = false) {
if($this->output && !$forceNew) return $this->output;
if(!$this->template) return null;
$this->output = $this->wire(new TemplateFile());
$this->output->setThrowExceptions(false);
$this->output->setFilename($this->template->filename);
$fuel = $this->wire('fuel')->getArray();
$this->output->set('wire', $this->wire());
foreach($fuel as $key => $value) $this->output->set($key, $value);
$this->output->set('page', $this);
return $this->output;
}
/**
* Render given $fieldName using site/templates/fields/ markup file
*
* Shorter aliases of this method include:
*
* - `$page->render('fieldName', $file);`
* - `$page->render->fieldName;`
* - `$page->_fieldName_;`
*
* This method expects that there is a file in `/site/templates/fields/` to render the field with:
*
* - `/site/templates/fields/fieldName.php`
* - `/site/templates/fields/fieldName.templateName.php`
* - `/site/templates/fields/fieldName/$file.php` (using $file argument)
* - `/site/templates/fields/$file.php` (using $file argument)
* - `/site/templates/fields/$file/fieldName.php` (using $file argument, must have trailing slash)
* - `/site/templates/fields/$file.fieldName.php` (using $file argument, must have trailing period)
*
* Note that the examples above showing $file require that the `$file` argument is specified.
*
* ~~~~~
* // Render output for the 'images' field (assumes you have implemented an output file)
* echo $page->renderField('images');
* ~~~~~
*
* #pw-group-output-rendering
*
* @param string $fieldName May be any custom field name or native page property.
* @param string $file Optionally specify file (in site/templates/fields/) to render with (may omit .php extension).
* @param mixed|null $value Optionally specify value to render, otherwise it will be pulled from this $page.
* @return mixed|string Returns the rendered value of the field
* @see Page::render(), Page::renderValue()
*
*/
public function ___renderField($fieldName, $file = '', $value = null) {
/** @var PageRender $pageRender */
$pageRender = $this->wire('modules')->get('PageRender');
return $pageRender->renderField($this, $fieldName, $file, $value);
}
/**
* Render given $value using /site/templates/fields/ markup file
*
* See the documentation for the `Page::renderField()` method for information about the `$file` argument.
*
* ~~~~~
* // Render a value using site/templates/fields/my-images.php custom output template
* $images = $page->images;
* echo $page->renderValue($images, 'my-images');
* ~~~~~
*
* #pw-group-output-rendering
*
* @param mixed $value Value to render
* @param string $file Optionally specify file (in site/templates/fields/) to render with (may omit .php extension)
* @return mixed|string Returns rendered value
*
*/
public function ___renderValue($value, $file = '') {
return $this->___renderField('', $file, $value);
}
/**
* Return all Inputfield objects necessary to edit this page
*
* This method returns an InputfieldWrapper object that contains all the custom Inputfield objects
* required to edit this page. You may also specify a `$fieldName` argument to limit what is contained
* in the returned InputfieldWrapper.
*
* Please note this method deals only with custom fields, not system fields name 'name' or 'status', etc.,
* as those are exclusive to the ProcessPageEdit page editor.
*
* #pw-advanced
*
* @param string|array $fieldName Optional field to limit to, typically the name of a fieldset or tab.
* - Or optionally specify array of $options (See `Fieldgroup::getPageInputfields()` for options).
* @return null|InputfieldWrapper Returns an InputfieldWrapper array of Inputfield objects, or NULL on failure.
*
*/
public function getInputfields($fieldName = '') {
$of = $this->of();
if($of) $this->of(false);
if($this->template) {
if(is_array($fieldName) && !ctype_digit(implode('', array_keys($fieldName)))) {
// fieldName is an associative array of options for Fieldgroup::getPageInputfields
$wrapper = $this->template->fieldgroup->getPageInputfields($this, $fieldName);
} else {
$wrapper = $this->template->fieldgroup->getPageInputfields($this, '', $fieldName);
}
} else {
$wrapper = null;
}
if($of) $this->of(true);
return $wrapper;
}
/**
* Get a single Inputfield for the given field name
*
* - If requested field name refers to a single field, an Inputfield object is returned.
* - If requested field name refers to a fieldset or tab, then an InputfieldWrapper representing will be returned.
* - Returned Inputfield already has values populated to it.
* - Please note this method deals only with custom fields, not system fields name 'name' or 'status', etc.,
* as those are exclusive to the ProcessPageEdit page editor.
*
* #pw-advanced
*
* @param string $fieldName
* @return Inputfield|InputfieldWrapper|null Returns Inputfield, or null if given field name doesn't match field for this page.
*
*/
public function getInputfield($fieldName) {
$inputfields = $this->getInputfields($fieldName);
if($inputfields) {
$field = $this->wire('fields')->get($fieldName);
if($field && $field instanceof FieldtypeFieldsetOpen) {
// requested field name is a fieldset, returns InputfieldWrapper
return $inputfields;
} else {
// requested field name is a single field, return Inputfield
return $inputfields->children()->first();
}
} else {
// requested field name is not applicable to this page
return null;
}
}
/**
* Does this page have the given status?
*
* This method is the preferred way to check if a page has a particular status.
* The status may be specified as one of the `Page::status` constants or a string representing
* one of the constants, i.e. `hidden`, `unpublished`, `locked`, and so on.
*
* ~~~~~
* // check if page has hidden status using status name
* if($page->hasStatus('hidden')) { ... }
*
* // check if page has hidden status using status constant
* if($page->hasStatus(Page::statusHidden)) { ... }
*
* // There are also method shortcuts, i.e.
* if($page->isHidden()) { ... }
* if($page->isUnpublished()) { ... }
* if($page->isLocked()) { ... }