praiadeseselle/wire/core/PageValues.php

1123 lines
35 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php namespace ProcessWire;
/**
* ProcessWire Page Values
*
* Provides implementation for several Page value get() functions.
*
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
* https://processwire.com
*
* @since 3.0.205
*
*/
class PageValues extends Wire {
/**
* Given a 'field.subfield' type string traverse properties and return value
*
* @param Page $page
* @param string $key
* @return mixed|null
*
*/
public function getDotValue(Page $page, $key) {
$keys = explode('.', $key);
if(count($keys) === 1) return $page->get($key);
// test if any parts of key can potentially refer to API variables
foreach($keys as $key) {
if($page->wire($key) || $key === 'pass') return null;
}
$value = $page;
$values = array();
$wireArrayProperties = array('first', 'last', 'count', 'keys', 'values');
do {
$key = array_shift($keys);
$k = $key;
$index = '';
// k is key without brackets (if any were present)
if(strpos($k, '[')) {
list($k, $index) = explode('[', $k, 2);
$index = rtrim($index, ']');
if(ctype_digit($index)) $index = (int) $index;
}
if($value instanceof Page && !in_array($key, $wireArrayProperties)) {
// value is a Page
if(isset(PageProperties::$traversalReturnTypes[$k])) {
// traversal property: Page or PageArray
// parent, rootParent, child, next, prev, children, parents, siblings
$value = $value->$key();
} else {
// native base property or custom field value
$value = $value->get($key);
}
} else if($value instanceof WireArray) {
if(in_array($k, $wireArrayProperties)) {
$value = $value->getProperty($k);
} else {
$value = $value->each($k);
// convert PHP array to WireArray if there are keys remaining (for next round)
if(count($keys)) $value = $page->wire(WireArray($value));
}
if(is_int($index)) {
// index is integer
if(WireArray::iterable($value)) {
$value = isset($value[$index]) ? $value[$index] : null;
}
} else if($index) {
// index is selector
if($value instanceof WireArray) $value = $value->find($index);
}
} else if($value instanceof WireData) {
$v = $value->get($key);
if($v === null) switch($key) {
// self-generated equivalents for WireArray properties/methods
case 'first':
case 'last': $v = $value; break;
case 'count': $v = 1; break;
case 'values': $v = array($value); break;
case 'keys': $v = ("$value" === $value->className() ? array(0) : array("$value")); break;
}
$value = $v;
} else if(is_array($value)) {
foreach($value as $kk => $vv) {
$value[$kk] = $vv instanceof Wire ? $vv->$k : $vv;
}
} else {
$value = null;
}
$values[] = $value;
} while($value !== null && count($keys));
// if($value === null && count($values) && $values[0] !== null) {
// if first key didn't return null then try again with WireData native method
// $value = $page->getDot($_key);
// }
return $value;
}
/**
* Get value that ends with square brackets to get iterable value, filtered value or property value
*
* ~~~~~
* $iterableValue = $page->get('field_name[]');
* ~~~~~
* Note: When requesting an iterable value, this method will return an empty array in cases where
* the Page::get() method would return null.
*
* @param Page $page
* @param string $key
* @param mixed $value Value to use rather than pulling from $page
* @return array|mixed|Page|PageArray|Wire|WireArray|WireData|string|\Traversable
*
*/
public function getBracketValue(Page $page, $key, $value = null) {
if(strpos($key, '.')) return $this->getDotValue($page, $key);
if(substr($key, -1) !== ']') return null;
$property = '';
$selector = '';
$getIterable = true;
$key = rtrim($key, ']');
list($key, $index) = explode('[', $key, 2);
if(strpos($index, '][')) {
// i.e. field[selector][0]
list($selector, $index) = explode('][', $index);
}
if(ctype_digit($index)) {
$index = (int) $index;
$getIterable = false;
}
if($index !== '' && !is_int($index)) {
if(ctype_alnum(str_replace('_', '', $index))) {
$property = $index;
} else {
$selector = $index;
}
$index = '';
}
if($value === null) {
if($selector) {
// filter FieldtypeMulti values at DB level
// using $page->field_name($selector) method feature
$value = $page->$key($selector);
$selector = '';
} else {
$value = $page->get($key);
}
}
if($value === null) {
return $getIterable ? array() : null;
}
if(is_object($value)) {
if($value instanceof Page) {
$value = $page->wire()->pages->newPageArray()->add($value);
} else if($value instanceof WireArrayItem) {
$value = $value->getWireArray();
} else if($value instanceof WireData) {
$value = $page->wire(WireArray([$value]));
} else if($value instanceof \Traversable) {
// WireArray or other
} else {
$value = array($value);
}
} else if($getIterable) {
$value = is_array($value) ? $value : array($value);
}
if($property !== '') {
if($value instanceof WireArray) {
$value = $value->each($property);
}
} else if($selector !== '') {
if($value instanceof WireArray) {
$value = $value->find($selector);
$value->resetTrackChanges();
}
} else if(is_int($index)) {
if($value instanceof WireArray){
$value = $value->eq($index);
} else if(is_array($value) || $value instanceof \ArrayAccess) {
$value = isset($value[$index]) ? $value[$index] : null;
} else if(WireArray::iterable($value)) {
$n = 0;
$found = false;
foreach($value as $v) {
if($n === $index) {
$value = $v;
$found = true;
break;
}
$n++;
}
if(!$found) $value = null;
}
}
return $value;
}
/**
* Get multiple Page property/field values in an array
*
* This method works exactly the same as the `get()` method except that it accepts an
* array (or CSV string) of properties/fields to get, and likewise returns an array
* of those property/field values. By default it returns a regular (non-indexed) PHP
* array in the same order given. To instead get an associative array indexed by the
* property/field names given, specify `true` for the `$assoc` argument.
*
* ~~~~~
* // returns regular array i.e. [ 'foo val', 'bar val' ]
* $a = $page->getMultiple([ 'foo', 'bar' ]);
* list($foo, $bar) = $a;
*
* // returns associative array i.e. [ 'foo' => 'foo val', 'bar' => 'bar val' ]
* $a = $page->getMultiple([ 'foo', 'bar' ], true);
* $foo = $a['foo'];
* $bar = $a['bar'];
*
* // CSV string can also be used instead of array
* $a = $page->getMultiple('foo,bar');
* ~~~~~
*
* @param page $page
* @param array|string $keys Array or CSV string of properties to get.
* @param bool $assoc Get associative array indexed by given properties? (default=false)
* @return array
*
*/
public function getMultiple(Page $page, $keys, $assoc = false) {
if(!is_array($keys)) {
$keys = (string) $keys;
if(strpos($keys, ',') !== false) {
$keys = explode(',', $keys);
} else {
$keys = array($keys);
}
}
$values = array();
foreach($keys as $key) {
$key = trim("$key");
$value = strlen($key) ? $page->get($key) : null;
if($assoc) {
$values[$key] = $value;
} else {
$values[] = $value;
}
}
return $values;
}
/**
* 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 page $page
* @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
*
*/
public function getFieldFirstValue(Page $page, $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) {
$v = $page->getUnformatted($key);
if(is_array($v) || $v instanceof WireArray) {
// array or WireArray
if(!count($v)) continue;
} else if(is_object($v)) {
// like LanguagesPageFieldValue
$str = trim((string) $v);
if(!strlen($str)) continue;
} else if(is_string($v)) {
$v = trim($v);
}
if($v) {
if($page->of()) {
$v = $page->get($key);
}
if($v) {
$value = $getKey ? $key : $v;
break;
}
}
}
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.
*
* @param Page $page
* @param string $key Field name or markup string with field {name} tags in it
* @return string
* @see Page::getText()
*
*/
public function getMarkup(Page $page, $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, $page);
}
if(strpos($key, '|') !== false) {
$key = $this->getFieldFirstValue($page, $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 = $page;
do {
$name = array_shift($parts);
$field = $page->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($page, $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
*
* If no `$entities` argument is provided, returned value is entity encoded when output formatting
* is on, and not entity encoded when output formatting is off.
*
* @param Page $page
* @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(Page $page, $key, $oneLine = false, $entities = null) {
$value = $page->getMarkup($key);
$length = strlen($value);
if(!$length) return '';
$options = array(
'entities' => ($entities === null ? $page->of() : (bool) $entities)
);
$sanitizer = $this->wire()->sanitizer;
if($oneLine) {
$value = $sanitizer->markupToLine($value, $options);
} else {
$value = $sanitizer->markupToText($value, $options);
}
// if stripping tags from non-empty value made it empty, just indicate that it was markup and length
if(!strlen(trim($value))) $value = "markup($length)";
return $value;
}
/**
* 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);
* ~~~~~
*
* @param Page $page
* @param int|array|string Status value, array of status names or values, or status name string.
* @return Page
* @see Page::addStatus(), Page::removeStatus()
*
*/
public function setStatus(Page $page, $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(PageProperties::$statuses[$v])) { // string (status name)
$status = $status | PageProperties::$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;
$status = $page->_getSetting('status');
$override = $status & Page::statusSystemOverride;
if(!$override) {
if($status & Page::statusSystemID) $value = $value | Page::statusSystemID;
if($status & Page::statusSystem) $value = $value | Page::statusSystem;
}
$page->_setSetting('status', $value);
if($value & Page::statusDeleted) {
// disable any instantiated filesManagers after page has been marked deleted
// example: uncache method polls filesManager
$page->__unset('filesManager');
}
return $page;
}
/**
* Remove the specified status from this page
*
* This is the preferred way to remove a status from a page. There is also a corresponding `Page::addStatus()` method.
*
* ~~~~~
* // Remove hidden status from the page using status name
* $page->removeStatus('hidden');
*
* // Remove hidden status from the page using status constant
* $page->removeStatus(Page::statusHidden);
* ~~~~~
*
* @param Page $page
* @param int|string $statusFlag Status flag constant or string representation (hidden, locked, unpublished, etc.)
* @return Page
* @throws WireException If you attempt to remove `Page::statusSystem` or `Page::statusSystemID` statuses without first adding `Page::statusSystemOverride` status.
* @see Page::addStatus(), Page::hasStatus()
*
*/
public function removeStatus(Page $page, $statusFlag) {
if(is_string($statusFlag) && isset(PageProperties::$statuses[$statusFlag])) {
$statusFlag = PageProperties::$statuses[$statusFlag];
}
$statusFlag = (int) $statusFlag;
$status = $page->_getSetting('status');
$override = $status & Page::statusSystemOverride;
if($statusFlag == Page::statusSystem || $statusFlag == Page::statusSystemID) {
if(!$override) throw new WireException('Cannot remove statusSystem from page without statusSystemOverride');
}
$page->status = $status & ~$statusFlag;
return $page;
}
/**
* 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');
* ~~~~~
*
* @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 Page
*
*/
public function setName(Page $page, $value, $language = null) {
$key = 'name';
$charset = $this->wire()->config->pageNameCharset;
$sanitizer = $this->wire()->sanitizer;
$isLoaded = $page->isLoaded();
if($isLoaded) {
if(is_int($language)) {
$key .= $language;
$existingValue = $page->get($key);
} else if($language && $language !== 'name') {
// update $key to contain language ID when applicable
$languages = $this->wire()->languages;
if($languages) {
if(!is_object($language)) {
if(strpos($language, 'name') === 0) $language = (int) substr($language, 4);
$language = $languages->getLanguage($language);
if(!$language || !$language->id || $language->isDefault()) $language = '';
}
if(!$language) return $page;
$key .= $language->id;
}
$existingValue = $page->get($key);
} else {
$existingValue = $page->_getSetting($key);
if($existingValue === null) $existingValue = '';
}
// 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);
}
} 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($language) {
if(ctype_digit("$language")) {
$key = "name$language";
} else if(is_string($language)) {
$key = $language; // i.e. name1234
}
}
}
if($key === 'name') {
$page->_setSetting($key, $value);
} else if(!$isLoaded || $page->_getSetting('quietMode')) {
$page->_parentSet($key, $value);
} else {
$this->setFieldValue($page, $key, $value, $isLoaded); // i.e. name1234
}
return $page;
}
/**
* 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(Page $page, $fieldName = '') {
$of = $page->of();
if($of) $page->of(false);
$template = $page->template();
$fieldgroup = $template ? $template->fieldgroup : null;
if($fieldgroup) {
if(is_array($fieldName) && !ctype_digit(implode('', array_keys($fieldName)))) {
// fieldName is an associative array of options for Fieldgroup::getPageInputfields
$wrapper = $fieldgroup->getPageInputfields($page, $fieldName);
} else {
$wrapper = $fieldgroup->getPageInputfields($page, '', $fieldName);
}
} else {
$wrapper = null;
}
if($of) $page->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(Page $page, $fieldName) {
$inputfields = $this->getInputfields($page, $fieldName);
if($inputfields) {
$field = $this->wire()->fields->get($fieldName);
if($field && $field->type 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;
}
}
/**
* Get the icon name associated with this Page (if applicable)
*
* #pw-internal
*
* @param Page $page
* @return string
*
*/
public function getIcon(Page $page) {
$template = $page->template();
if(!$template) return '';
if($page->hasField('process')) {
$process = $page->getUnformatted('process');
if($process) {
$info = $this->wire()->modules->getModuleInfoVerbose($process);
if(!empty($info['icon'])) return $info['icon'];
}
}
return $template->getIcon();
}
/**
* Process and instantiate any data in the fieldDataQueue
*
* This happens after setIsLoaded(true) is called
*
* #pw-internal
*
* @param Page $page
* @param array $fieldDataQueue
* @return bool
*
*/
public function processFieldDataQueue(Page $page, array $fieldDataQueue) {
$template = $page->template();
if(!$template) return false;
$fieldgroup = $template->fieldgroup;
if(!$fieldgroup) return false;
foreach($fieldDataQueue as $key => $value) {
$field = $fieldgroup->get($key);
if(!$field) continue;
// check for autojoin multi fields, which may have multiple values bundled into one string
// as a result of an sql group_concat() function
if($field->type instanceof FieldtypeMulti && ($field->flags & Field::flagAutojoin)) {
foreach($value as $k => $v) {
if(is_string($v) && strpos($v, FieldtypeMulti::multiValueSeparator) !== false) {
$value[$k] = explode(FieldtypeMulti::multiValueSeparator, $v);
}
}
}
// if all there is in the array is 'data', then we make that the value rather than keeping an array
// this is so that Fieldtypes that only need to interact with a single value don't have to receive an array of data
if(count($value) == 1 && array_key_exists('data', $value)) $value = $value['data'];
$this->setFieldValue($page, $key, $value, false);
}
$page->fieldDataQueue(array());
return true;
}
/**
* 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 Page $page
* @param string|int|Field $field
* @return Field|null
* @todo determine if we can always retrieve in context regardless of output formatting.
*
*/
public function getField(Page $page, $field) {
$template = $page->template();
$fieldgroup = $template ? $template->fieldgroup : null;
if(!$fieldgroup) return null;
if($page->of() && $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
*
* @param Page $page
* @return FieldsArray of Field objects
*
*/
public function getFields(Page $page) {
$template = $page->template();
$fields = new FieldsArray();
$this->wire($fields);
if(!$template) return $fields;
$fieldgroup = $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 given $field name, ID or object is valid for this Page
*
* Note that this only indicates validity, not whether the field is populated.
*
* #pw-advanced
*
* @param Page $page
* @param int|string|Field|array $field Field name, object or ID to check.
* - In 3.0.126+ this may also be an array or pipe "|" separated string of field names to check.
* @return bool|string True if valid, false if not.
* - In 3.0.126+ returns first matching field name if given an array of field names or pipe separated string of field names.
*
*/
public function hasField(Page $page, $field) {
$template = $page->template();
if(!$template) return false;
if(is_string($field) && strpos($field, '|') !== false) {
$field = explode('|', $field);
}
if(is_array($field)) {
$result = false;
foreach($field as $f) {
$f = trim($f);
if(!empty($f) && $this->hasField($page, $f)) $result = $f;
if($result) break;
}
} else {
$result = $template->fieldgroup->hasField($field);
}
return $result;
}
/**
* 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|null
*
*/
public function output(Page $page, $forceNew = false) {
$template = $page->template();
if(!$template) return null;
/** @var TemplateFile $output */
$output = $this->wire(new TemplateFile());
$output->setThrowExceptions(false);
$output->setFilename($template->filename);
$fuel = $this->wire()->fuel->getArray();
$output->set('wire', $this->wire());
foreach($fuel as $key => $value) $output->set($key, $value);
$output->set('page', $page);
return $output;
}
/**
* 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
*
*/
public function getFieldValue(Page $page, $key, $selector = '') {
$template = $page->template();
if(!$template) return $page->_parentGet($key);
$field = $this->getField($page, $key);
$value = $page->_parentGet($key);
if(!$field) return $value; // likely a runtime field, not part of our data
/** @var Fieldtype $fieldtype */
$fieldtype = $field->type;
$invokeArgument = '';
if($value !== null && $page->wakeupNameQueue($key)) {
$value = $fieldtype->_callHookMethod('wakeupValue', array($page, $field, $value));
$value = $fieldtype->sanitizeValue($page, $field, $value);
$trackChanges = $page->trackChanges(true);
$page->setTrackChanges(false);
$page->_parentSet($key, $value);
$page->setTrackChanges($trackChanges);
$page->wakeupNameQueue($key, false);
}
if($field->useRoles && $page->of()) {
// API access may be limited when output formatting is ON
if($field->flags & Field::flagAccessAPI) {
// API access always allowed because of flag
} else if($page->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 = $fieldtype->getBlankValue($page, $field);
return $this->formatFieldValue($page, $field, $value);
}
}
if($value !== null && empty($selector)) {
// if the non-filtered value is already loaded, return it
return $this->formatFieldValue($page, $field, $value);
}
$track = $page->trackChanges();
$page->setTrackChanges(false);
if(!$fieldtype) return null;
if($selector && !Selectors::stringHasSelector($selector)) {
// if selector argument provided, but isn't valid, we assume it
// to instead be an argument for the value's __invoke() method
$invokeArgument = $selector;
$selector = '';
}
if($selector) {
$value = $fieldtype->loadPageFieldFilter($page, $field, $selector);
} else {
$value = $fieldtype->_callHookMethod('loadPageField', array($page, $field));
}
if($value === null) {
$value = $fieldtype->getDefaultValue($page, $field);
} else {
$value = $fieldtype->_callHookMethod('wakeupValue', array($page, $field, $value));
}
// turn off output formatting and set the field value, which may apply additional changes
$of = $page->of();
if($of) $page->of(false);
$this->setFieldValue($page, $key, $value, false);
if($of) $page->of(true);
$value = $page->_parentGet($key);
// prevent storage of value if it was filtered when loaded
if(!empty($selector)) $page->__unset($key);
if($value instanceof Wire && !$value instanceof Page) $value->resetTrackChanges(true);
if($track) $page->setTrackChanges(true);
$value = $this->formatFieldValue($page, $field, $value);
if($invokeArgument && is_object($value) && method_exists($value, '__invoke')) {
$value = $value->__invoke($invokeArgument);
}
return $value;
}
/**
* Return a value consistent with the pages output formatting state
*
* This is primarily for use as a helper to the getFieldValue() method.
*
* @param Page $page
* @param Field $field
* @param mixed $value
* @return mixed
*
*/
public function formatFieldValue(Page $page, Field $field, $value) {
$hasInterface = $value instanceof PageFieldValueInterface;
if($hasInterface) {
$value->setPage($page);
$value->setField($field);
}
if($page->of()) {
// output formatting is enabled so return a formatted value
//$value = $field->type->formatValue($this, $field, $value);
$value = $field->type->_callHookMethod('formatValue', array($page, $field, $value));
// check again for interface since value may now be different
if($hasInterface) $hasInterface = $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
$page->__unset($field->name);
$value = $this->getFieldValue($page, $field->name);
}
return $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 Page $page
* @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 Returns reference to this Page
* @throws WireException
*
*/
public function setFieldValue(Page $page, $key, $value, $load = true) {
if(!$page->template()) {
throw new WireException("You must assign a template to the page before setting field values ($key)");
}
$isLoaded = $page->isLoaded();
// 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(!$isLoaded && strpos($key, '__')) {
list($key, $subKey) = explode('__', $key, 2);
$fieldData = $page->fieldDataQueue($key);
if($fieldData === null) $fieldData = array();
$fieldData[$subKey] = $value;
$page->fieldDataQueue($key, $fieldData);
return $page;
}
// check if the given key resolves to a Field or not
$field = $this->getField($page, $key);
if(!$field) {
// not a known/saveable field, let them use it for runtime storage
$valPrevious = $page->_parentGet($key);
if($valPrevious !== null && $page->_parentGet("-$key") === null && $valPrevious !== $value) {
// store previous value (if set) in a "-$key" version
$page->setQuietly("-$key", $valPrevious);
}
$page->_parentSet($key, $value);
return $page;
}
/** @var Fieldtype $fieldtype */
$fieldtype = $field->type;
// if a null value is set, then ensure the proper blank type is set to the field
if($value === null) {
$page->_parentSet($key, $fieldtype->getBlankValue($page, $field));
return $page;
}
// if the page is currently loading from the database, we assume that any set values are 'raw' and need to be woken up
if(!$page->isLoaded()) {
// queue for wakeup and sanitize on first field access
$page->wakeupNameQueue($key, true);
// page is currently loading, so we don't need to continue any further
$page->_parentSet($key, $value);
return $page;
}
// check if the field hasn't been already loaded
if($page->_parentGet($key) === null) {
// 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) $page->get($key);
} else if($page->wakeupNameQueue($key)) {
// autoload value: we don't yet have a "woke" value suitable for change detection, so let it wakeup
if($page->trackChanges() && $load) {
// if changes are being tracked, load existing value for comparison
$this->getFieldValue($page, $key);
} else {
// if changes aren't being tracked, the existing value can be discarded
$page->wakeupNameQueue($key, false);
}
} else {
// check if the field is corrupted
$isCorrupted = false;
if($value instanceof PageFieldValueInterface) {
// value indicates it is already formatted, so would corrupt the page for saving
if($value->formatted()) $isCorrupted = true;
} else if($page->of()) {
// check if value is modified by being formatted
$result = $fieldtype->_callHookMethod('formatValue', array($page, $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.
$page->set('status', $page->status | Page::statusCorrupted);
$corruptedFields = $page->get('_statusCorruptedFields');
if(!is_array($corruptedFields)) $corruptedFields = array();
$corruptedFields[$field->name] = $field->name;
$page->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 = $page->isLoaded();
if(!$load) $page->setIsLoaded(false, true); // true=set quietly
// ensure that the value is in a safe format and set it
$value = $fieldtype->sanitizeValue($page, $field, $value);
// Silently restore isLoaded state
if(!$load) $page->setIsLoaded($isLoaded, true);
$page->_parentSet($key, $value);
return $page;
}
}