praiadeseselle/wire/core/Field.php

1570 lines
47 KiB
PHP
Raw Normal View History

2022-03-08 15:55:41 +01:00
<?php namespace ProcessWire;
/**
* ProcessWire Field
*
* The Field class corresponds to a record in the fields database table
* and is managed by the 'Fields' class.
*
* #pw-summary Field represents a custom field that is used on a Page.
* #pw-var $field
* #pw-instantiate $field = $fields->get('field_name');
* #pw-body Field objects are managed by the `$fields` API variable.
* #pw-use-constants
*
2022-11-05 18:32:48 +01:00
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
2022-03-08 15:55:41 +01:00
* https://processwire.com
*
* @property int $id Numeric ID of field in the database #pw-group-properties
* @property string $name Name of field #pw-group-properties
* @property string $table Database table used by the field #pw-group-properties
* @property string $prevTable Previously database table (if field was renamed) #pw-group-properties
* @property string $prevName Previously used name (if field was renamed), 3.0.164+ #pw-group-properties
* @property Fieldtype|null $type Fieldtype module that represents the type of this field #pw-group-properties
* @property Fieldtype|null $prevFieldtype Previous Fieldtype, if type was changed #pw-group-properties
* @property int $flags Bitmask of flags used by this field #pw-group-properties
* @property-read string $flagsStr Names of flags used by this field (readonly) #pw-group-properties
* @property string $label Text string representing the label of the field #pw-group-properties
* @property string $description Longer description text for the field #pw-group-properties
* @property string $notes Additional notes text about the field #pw-group-properties
* @property string $icon Icon name used by the field, if applicable #pw-group-properties
* @property string $tags Tags that represent this field, if applicable (space separated string). #pw-group-properties
* @property-read array $tagList Same as $tags property, but as an array. #pw-group-properties
* @property bool $useRoles Whether or not access control is enabled #pw-group-access
* @property array $editRoles Role IDs with edit access, applicable only if access control is enabled. #pw-group-access
* @property array $viewRoles Role IDs with view access, applicable only if access control is enabled. #pw-group-access
* @property array|null $orderByCols Columns that WireArray values are sorted by (default=null), Example: "sort" or "-created". #pw-internal
* @property int|null $paginationLimit Used by paginated WireArray values to indicate limit to use during load. #pw-internal
* @property array $allowContexts Names of settings that are custom configured to be allowed for context. #pw-group-properties
* @property bool|int|null $flagUnique Non-empty value indicates request for, or presence of, Field::flagUnique flag. #pw-internal
* @property Fieldgroup|null $_contextFieldgroup Fieldgroup field is in context for or null if not in context. #pw-internal
*
* Common Inputfield properties that Field objects store:
* @property int|bool|null $required Whether or not this field is required during input #pw-group-properties
* @property string|null $requiredIf A selector-style string that defines the conditions under which input is required #pw-group-properties
* @property string|null $showIf A selector-style string that defines the conditions under which the Inputfield is shown #pw-group-properties
* @property int|null $columnWidth The Inputfield column width (percent) 10-100. #pw-group-properties
* @property int|null $collapsed The Inputfield 'collapsed' value (see Inputfield collapsed constants). #pw-group-properties
* @property int|null $textFormat The Inputfield 'textFormat' value (see Inputfield textFormat constants). #pw-group-properties
*
* @method bool viewable(Page $page = null, User $user = null) Is the field viewable on the given $page by the given $user? #pw-group-access
* @method bool editable(Page $page = null, User $user = null) Is the field editable on the given $page by the given $user? #pw-group-access
* @method Inputfield getInputfield(Page $page, $contextStr = '') Get instance of the Inputfield module that collects input for this field.
* @method InputfieldWrapper getConfigInputfields() Get Inputfields needed to configure this field in the admin.
*
* @todo add modified date property
*
*/
class Field extends WireData implements Saveable, Exportable {
/**
* Field should be automatically joined to the page at page load time
*
* #pw-group-flags
*
*/
const flagAutojoin = 1;
/**
* Field used by all fieldgroups - all fieldgroups required to contain this field
*
* #pw-group-flags
*
*/
const flagGlobal = 4;
/**
* Field is a system field and may not be deleted, have it's name changed, or be converted to non-system
*
* #pw-group-flags
*
*/
const flagSystem = 8;
/**
* Field is permanent in any fieldgroups/templates where it exists - it may not be removed from them
*
* #pw-group-flags
*
*/
const flagPermanent = 16;
/**
* Field is access controlled
*
* #pw-group-flags
*
*/
const flagAccess = 32;
/**
* If field is access controlled, this flag says that values are still front-end API accessible
*
* Without this flag, non-viewable values are made blank when output formatting is ON.
*
* #pw-group-flags
*
*/
const flagAccessAPI = 64;
/**
* If field is access controlled and user has no edit access, they can still view in the editor (if they have view permission)
*
* Without this flag, non-editable values are simply not shown in the editor at all.
*
* #pw-group-flags
*
*/
const flagAccessEditor = 128;
/**
* Field requires that the same value is not repeated more than once in its table 'data' column (when supported by Fieldtype)
*
* When this flag is set and there is a non-empty $flagUnique property on the field, then it indicates a unique index
* is currently present. When only this flag is present (no property), it indicates a request to remove the index and flag.
* When only the property is present (no flag), it indicates a pending request to add unique index and flag.
*
* #pw-group-flags
* @since 3.0.150
*
*/
const flagUnique = 256;
/**
* Field has been placed in a runtime state where it is contextual to a specific fieldgroup and is no longer saveable
*
* #pw-group-flags
*
*/
const flagFieldgroupContext = 2048;
/**
* Set this flag to override system/permanent flags if necessary - once set, system/permanent flags can be removed, but not in the same set().
*
* #pw-group-flags
*
*/
const flagSystemOverride = 32768;
/**
* Prefix for database tables
*
* #pw-internal
*
*/
const tablePrefix = 'field_';
/**
* Permanent/native settings to an individual Field
*
* id: Numeric ID corresponding with id in the fields table.
* type: Fieldtype object or NULL if no Fieldtype assigned.
* label: String text label corresponding to the <label> field during input.
* flags:
* - autojoin: True if the field is automatically joined with the page, or False if it's value is loaded separately.
* - global: Is this field required by all Fieldgroups?
*
*/
protected $settings = array(
'id' => 0,
'name' => '',
'label' => '',
'flags' => 0,
'type' => null,
);
/**
* If the field name changed, this is the name of the previous table so that it can be renamed at save time
*
*/
protected $prevTable;
/**
* If the field name changed, this is the previous name
*
* @var string
*
*/
protected $prevName = '';
/**
* If the field type changed, this is the previous fieldtype so that it can be changed at save time
*
*/
protected $prevFieldtype;
/**
* A specifically set table name by setTable() for override purposes
*
* @var string
*
*/
protected $setTable = '';
/**
* Accessed properties, becomes array when set to true, null when set to false
*
* Used for keeping track of which properties are accessed during a request, to help determine which
* $data properties might no longer be in use.
*
* @var null|array
*
*/
protected $trackGets = null;
/**
* Array of Role IDs referring to roles that are allowed to view contents of this field (on pages)
*
* Applicable only if the flagAccess flag is set
*
* @var array
*
*/
protected $viewRoles = array();
/**
* Array of Role IDs referring to roles that are allowed to edit contents of this field (on pages)
*
* Applicable only if the flagAccess flag is set
*
* @var array
*
*/
protected $editRoles = array();
/**
* Optional key=value runtime settings to provide to Inputfield (see: inputfieldSetting method)
*
* This are runtime only and not stored in the DB.
*
* @var array
*
*/
protected $inputfieldSettings = array();
/**
* Tags assigned to this field, keys are lowercase version of tag, values can possibly contain mixed case
*
* @var null|array
*
*/
protected $tagList = null;
/**
* True if lowercase tables should be enforce, false if not (null = unset). Cached from $config
*
*/
static protected $lowercaseTables = null;
/**
* Set a native setting or a dynamic data property for this Field
*
* This can also be used directly via `$field->name = 'company';`
*
* #pw-group-manipulation
*
* @param string $key Property name to set
* @param mixed $value
* @return Field|WireData
*
*/
public function set($key, $value) {
2022-11-05 18:32:48 +01:00
switch($key) {
case 'id': $this->settings['id'] = (int) $value; return $this;
case 'name': return $this->setName($value);
case 'data': return empty($value) ? $this : parent::set($key, $value);
case 'type': return ($value ? $this->setFieldtype($value) : $this);
case 'label': $this->settings['label'] = $value; return $this;
case 'prevTable': $this->prevTable = $value; return $this;
case 'prevName': $this->prevName = $value; return $this;
case 'prevFieldtype': $this->prevFieldtype = $value; return $this;
case 'flags': $this->setFlags($value); return $this;
case 'flagsAdd': return $this->addFlag($value);
case 'flagsDel': return $this->removeFlag($value);
case 'icon': $this->setIcon($value); return $this;
case 'editRoles': $this->setRoles('edit', $value); return $this;
case 'viewRoles': $this->setRoles('view', $value); return $this;
2022-03-08 15:55:41 +01:00
}
2022-11-05 18:32:48 +01:00
2022-03-08 15:55:41 +01:00
if(isset($this->settings[$key])) {
$this->settings[$key] = $value;
2022-11-05 18:32:48 +01:00
} else if($key === 'useRoles') {
2022-03-08 15:55:41 +01:00
$flags = $this->flags;
if($value) {
$flags = $flags | self::flagAccess; // add flag
} else {
$flags = $flags & ~self::flagAccess; // remove flag
}
$this->setFlags($flags);
} else {
return parent::set($key, $value);
}
return $this;
}
2022-11-05 18:32:48 +01:00
/**
* Set raw setting or other value with no validation/processing
*
* This is for use when a field is loading and needs no validation.
*
* #pw-internal
*
* @param string $key
* @param mixed $value
* @since 3.0.194
*
*/
public function setRawSetting($key, $value) {
if($key === 'data') {
if(!empty($value)) parent::set($key, $value);
} else {
$this->settings[$key] = $value;
}
}
2022-03-08 15:55:41 +01:00
/**
* Set the bitmask of flags for the field
*
* @param int $value
*
*/
protected function setFlags($value) {
// ensure that the system flag stays set
$value = (int) $value;
$override = $this->settings['flags'] & Field::flagSystemOverride;
if(!$override) {
if($this->settings['flags'] & Field::flagSystem) $value = $value | Field::flagSystem;
if($this->settings['flags'] & Field::flagPermanent) $value = $value | Field::flagPermanent;
}
$this->settings['flags'] = $value;
}
/**
* Add the given bitmask flag
*
* #pw-group-flags
*
* @param int $flag
* @return $this
*
*/
public function addFlag($flag) {
$flag = (int) $flag;
$this->setFlags($this->settings['flags'] | $flag);
return $this;
}
/**
* Remove the given bitmask flag
*
* #pw-group-flags
*
* @param int $flag
* @return $this
*
*/
public function removeFlag($flag) {
$flag = (int) $flag;
$this->setFlags($this->settings['flags'] & ~$flag);
return $this;
}
/**
* Does this field have the given bitmask flag?
*
* #pw-group-flags
*
* @param int $flag
* @return bool
*
*/
public function hasFlag($flag) {
$flag = (int) $flag;
return ($this->settings['flags'] & $flag) ? true : false;
}
/**
* Get a Field setting or dynamic data property
*
* This can also be accessed directly, i.e. `$fieldName = $field->name;`.
*
* #pw-group-retrieval
*
* @param string $key
* @return mixed
*
*/
public function get($key) {
2022-11-05 18:32:48 +01:00
if($key === 'type') {
if(!empty($this->settings['type'])) {
$value = $this->settings['type'];
if($value) $value->setLastAccessField($this);
return $value;
}
return null;
}
switch($key) {
case 'id':
case 'name':
case 'type':
case 'flags':
case 'label': return $this->settings[$key];
case 'table': return $this->getTable();
case 'flagsStr': return $this->wire()->fields->getFlagNames($this->settings['flags'], true);
case 'viewRoles':
case 'editRoles': return $this->$key;
case 'useRoles': return ($this->settings['flags'] & self::flagAccess) ? true : false;
case 'prevTable':
case 'prevName':
case 'prevFieldtype': return $this->$key;
case 'icon': return $this->getIcon(true);
case 'tags': return $this->getTags(true);
case 'tagList': return $this->getTags();
2022-03-08 15:55:41 +01:00
}
2022-11-05 18:32:48 +01:00
if(isset($this->settings[$key])) return $this->settings[$key];
2022-03-08 15:55:41 +01:00
$value = parent::get($key);
2022-11-05 18:32:48 +01:00
2022-03-08 15:55:41 +01:00
if($key === 'allowContexts' && !is_array($value)) $value = array();
2022-11-05 18:32:48 +01:00
if($this->trackGets && is_array($this->trackGets)) $this->trackGets($key);
2022-03-08 15:55:41 +01:00
return $value;
}
/**
* Turn on tracking of accessed properties
*
* #pw-internal
*
* @param bool|string $key
* Omit to retrieve current trackGets value.
* Specify true to enable Get tracking.
* Specify false to disable (and reset) Get tracking.
* Specify string key to track.
*
* @return bool|array Returns current state of trackGets when no arguments provided.
* Otherwise it just returns true.
*
*/
public function trackGets($key = null) {
if(is_null($key)) {
// return current value
return array_keys($this->trackGets);
} else if($key === true) {
// enable tracking
if(!is_array($this->trackGets)) $this->trackGets = array();
} else if($key === false) {
// disable tracking
$this->trackGets = null;
} else if(!is_int($key) && is_array($this->trackGets)) {
// track a key
$this->trackGets[$key] = 1;
}
return true;
}
/**
* Return a key=value array of the data associated with the database table per Saveable interface
*
* #pw-internal
*
* @return array
*
*/
public function getTableData() {
$a = $this->settings;
$a['data'] = $this->data;
foreach($a['data'] as $key => $value) {
// remove runtime data (properties beginning with underscore)
if(strpos($key, '_') === 0) unset($a['data'][$key]);
}
if($this->settings['flags'] & self::flagAccess) {
$a['data']['editRoles'] = $this->editRoles;
$a['data']['viewRoles'] = $this->viewRoles;
} else {
unset($a['data']['editRoles'], $a['data']['viewRoles']); // just in case
}
return $a;
}
/**
* Per Saveable interface: return data for external storage
*
* #pw-internal
*
*/
public function getExportData() {
if($this->type) {
$data = $this->getTableData();
$data['type'] = $this->type->className();
} else {
$data['type'] = '';
}
if(isset($data['data'])) $data = array_merge($data, $data['data']); // flatten
unset($data['data']);
if($this->type) {
$typeData = $this->type->exportConfigData($this, $data);
$data = array_merge($data, $typeData);
}
// remove named flags from data since the 'flags' property already covers them
$flagOptions = array('autojoin', 'global', 'system', 'permanent');
foreach($flagOptions as $name) unset($data[$name]);
$data['flags'] = $this->flags;
foreach($data as $key => $value) {
// exclude properties beginning with underscore as they are assumed to be for runtime use only
if(strpos($key, '_') === 0) unset($data[$key]);
}
// convert access roles from IDs to names
if($this->useRoles) {
2022-11-05 18:32:48 +01:00
$roles = $this->wire()->roles;
2022-03-08 15:55:41 +01:00
foreach(array('viewRoles', 'editRoles') as $roleType) {
if(!is_array($data[$roleType])) $data[$roleType] = array();
$roleNames = array();
foreach($data[$roleType] as $key => $roleID) {
2022-11-05 18:32:48 +01:00
$role = $roles->get($roleID);
2022-03-08 15:55:41 +01:00
if(!$role || !$role->id) continue;
$roleNames[] = $role->name;
}
$data[$roleType] = $roleNames;
}
}
return $data;
}
/**
* Given an export data array, import it back to the class and return what happened
*
* #pw-internal
*
* @param array $data
* @return array Returns array(
* [property_name] => array(
*
* // old value (in string comparison format)
* 'old' => 'old value',
*
* // new value (in string comparison format)
* 'new' => 'new value',
*
* // error message (string) or messages (array)
* 'error' => 'error message or blank if no error' ,
* )
*
*/
public function setImportData(array $data) {
$changes = array();
$data['errors'] = array();
$_data = $this->getExportData();
// compare old data to new data to determine what's changed
foreach($data as $key => $value) {
if($key == 'errors') continue;
$data['errors'][$key] = '';
$old = isset($_data[$key]) ? $_data[$key] : '';
if(is_array($old)) $old = wireEncodeJSON($old, true);
$new = is_array($value) ? wireEncodeJSON($value, true) : $value;
if($old === $new || (empty($old) && empty($new)) || (((string) $old) === ((string) $new))) continue;
$changes[$key] = array(
'old' => $old,
'new' => $new,
'error' => '', // to be populated by Fieldtype::importConfigData when applicable
);
}
// prep data for actual import
if(!empty($data['type']) && ((string) $this->type) != $data['type']) {
2022-11-05 18:32:48 +01:00
$this->type = $this->wire()->fieldtypes->get($data['type']);
2022-03-08 15:55:41 +01:00
}
if(!$this->type) {
if(!empty($data['type'])) $this->error("Unable to locate field type: $data[type]");
2022-11-05 18:32:48 +01:00
$this->type = $this->wire()->fieldtypes->get('FieldtypeText');
2022-03-08 15:55:41 +01:00
}
$data = $this->type->importConfigData($this, $data);
// populate import data
foreach($changes as $key => $change) {
$this->errors('clear all');
if(isset($data[$key])) $this->set($key, $data[$key]);
if(!empty($data['errors'][$key])) {
$error = $data['errors'][$key];
// just in case they switched it to an array of multiple errors, convert back to string
if(is_array($error)) $error = implode(" \n", $error);
} else {
$error = $this->errors('last');
}
$changes[$key]['error'] = $error ? $error : '';
}
$this->errors('clear all');
return $changes;
}
/**
* Set the fields name
*
* This method will throw a WireException when field name is a reserved word, is already in use,
* is a system field, or is in some format not accepted for a field name.
*
* #pw-group-manipulation
*
* @param string $name
* @return Field $this
* @throws WireException
*
*/
public function setName($name) {
2022-11-05 18:32:48 +01:00
$fields = $this->wire()->fields;
if($fields) {
if(!ctype_alnum("$name")) {
$name = $this->wire()->sanitizer->fieldName($name);
}
if($fields->isNative($name)) {
throw new WireException("Field may not be named '$name' because it is a reserved word");
}
if(($f = $fields->get($name)) && $f->id != $this->id) {
throw new WireException("Field may not be named '$name' because it is already used by another field ($f->id: $f->name)");
}
if(strpos($name, '__') !== false) {
throw new WireException("Field name '$name' may not have double underscores because this usage is reserved by the core");
}
2022-03-08 15:55:41 +01:00
}
2022-11-05 18:32:48 +01:00
if(!empty($this->settings['name']) && $this->settings['name'] != $name) {
2022-03-08 15:55:41 +01:00
if($this->settings['name'] && ($this->settings['flags'] & Field::flagSystem)) {
throw new WireException("You may not change the name of field '{$this->settings['name']}' because it is a system field.");
}
$this->trackChange('name');
if($this->settings['name']) {
$this->prevName = $this->settings['name'];
$this->prevTable = $this->getTable(); // so that Fields can perform a table rename
}
}
$this->settings['name'] = $name;
return $this;
}
/**
* Set what type of field this is (Fieldtype).
*
* #pw-group-manipulation
*
* @param string|Fieldtype $type Type should be either a Fieldtype object or the string name of a Fieldtype object.
* @return Field $this
* @throws WireException
*
*/
public function setFieldtype($type) {
if(is_object($type) && $type instanceof Fieldtype) {
// good for you
} else if(is_string($type)) {
$typeStr = $type;
2022-11-05 18:32:48 +01:00
$type = $this->wire()->fieldtypes->get($type);
if(!$type) {
2022-03-08 15:55:41 +01:00
$this->error("Fieldtype '$typeStr' does not exist");
return $this;
}
} else {
throw new WireException("Invalid field type in call to Field::setFieldType");
}
2022-11-05 18:32:48 +01:00
$thisType = $this->settings['type'];
if($thisType && "$thisType" != "$type") {
if($this->trackChanges) $this->trackChange("type:$type");
$this->prevFieldtype = $thisType;
2022-03-08 15:55:41 +01:00
}
2022-11-05 18:32:48 +01:00
2022-03-08 15:55:41 +01:00
$this->settings['type'] = $type;
return $this;
}
/**
* Return the Fieldtype module representing this fields type.
*
* Can also be accessed directly via `$field->type`.
*
* #pw-group-retrieval
*
2022-11-05 18:32:48 +01:00
* @return Fieldtype|null|string
2022-03-08 15:55:41 +01:00
* @since 3.0.16 Added for consistency, but all versions can still use $field->type.
*
*/
public function getFieldtype() {
return $this->type;
}
/**
* Get this field in context of a Page/Template
*
* #pw-group-retrieval
*
* @param Page|Template|Fieldgroup|string $for Specify Page, Template, or template name string
* @param string $namespace Optional namespace (internal use)
* @param bool $has Return boolean rather than Field to check if context exists? (default=false)
* @return Field|bool
* @since 3.0.162
* @see Fieldgroup::getFieldContext(), Field::hasContext()
*
*/
public function getContext($for, $namespace = '', $has = false) {
/** @var Fieldgroup|null $fieldgroup */
$fieldgroup = null;
if(is_string($for)) {
$for = $this->wire()->templates->get($for);
}
if($for instanceof Page) {
/** @var Page $context */
$template = $for instanceof NullPage ? null : $for->template;
if(!$template) throw new WireException('Page must have template to get context');
$fieldgroup = $template->fieldgroup;
} else if($for instanceof Template) {
/** @var Template $context */
$fieldgroup = $for->fieldgroup;
} else if($for instanceof Fieldgroup) {
$fieldgroup = $for;
}
if(!$fieldgroup) throw new WireException('Cannot get Fieldgroup for field context');
if($has) return $fieldgroup->hasFieldContext($this->id, $namespace);
return $fieldgroup->getFieldContext($this->id, $namespace);
}
/**
* Does this field have context settings for given Page/Template?
*
* #pw-group-retrieval
*
* @param Page|Template|Fieldgroup|string $for Specify Page, Template, or template name string
* @param string $namespace Optional namespace (internal use)
* @return Field|bool
* @since 3.0.163
* @see Field::getContext()
*
*/
public function hasContext($for, $namespace = '') {
return $this->getContext($for, $namespace, true);
}
/**
* Get all contexts this field is used in
*
* @return array Array of 'fieldgroup-name' => [ contexts ]
* @since 3.0.182
*
*/
public function getContexts() {
$contexts = array();
foreach($this->wire()->fieldgroups as $fieldgroup) {
/** @var Fieldgroup $fieldgroup */
$context = $fieldgroup->getFieldContextArray($this->id);
if(empty($context)) continue;
$contexts[$fieldgroup->name] = $context;
}
return $contexts;
}
/**
* Set the roles that are allowed to view or edit this field on pages.
*
* Applicable only if the `Field::flagAccess` is set to this field's flags.
*
* #pw-group-manipulation
*
* @param string $type Must be either "view" or "edit"
* @param PageArray|array|null $roles May be a PageArray of Role objects or an array of Role IDs.
* @throws WireException if given invalid argument
*
*/
public function setRoles($type, $roles) {
if(empty($roles)) $roles = array();
if(!WireArray::iterable($roles)) {
throw new WireException("setRoles expects PageArray or array of Role IDs");
}
$ids = array();
foreach($roles as $role) {
if(is_int($role) || (is_string($role) && ctype_digit("$role"))) {
$ids[] = (int) $role;
} else if($role instanceof Role) {
$ids[] = (int) $role->id;
} else if(is_string($role) && strlen($role)) {
2022-11-05 18:32:48 +01:00
$rolePage = $this->wire()->roles->get($role);
2022-03-08 15:55:41 +01:00
if($rolePage && $rolePage->id) {
$ids[] = $rolePage->id;
} else {
$this->error("Unknown role '$role'");
}
} else {
// invalid
}
}
if($type == 'view') {
2022-11-05 18:32:48 +01:00
$guestID = $this->wire()->config->guestUserRolePageID;
2022-03-08 15:55:41 +01:00
// if guest is present, then that's inclusive of all, no need to store others in viewRoles
if(in_array($guestID, $ids)) $ids = array($guestID);
if($this->viewRoles != $ids) {
$this->viewRoles = $ids;
$this->trackChange('viewRoles');
}
} else if($type == 'edit') {
if($this->editRoles != $ids) {
$this->editRoles = $ids;
$this->trackChange('editRoles');
}
} else {
throw new WireException("setRoles expects either 'view' or 'edit' (arg 0)");
}
}
/**
* Is this field viewable?
*
* #pw-group-access
*
* - To maximize efficiency check that `$field->useRoles` is true before calling this.
* - If you have already verified that the page is viewable, omit or specify null for $page argument.
* - **Please note:** this does not check that the provided $page itself is viewable. If you want that
* check, then use `$page->viewable($field)` instead.
*
* @param Page|null $page Optionally specify a Page for context (i.e. Is field viewable on $page?)
* @param User|null $user Optionally specify a different user for context (default=current user)
* @return bool True if viewable, false if not
*
*/
public function ___viewable(Page $page = null, User $user = null) {
2022-11-05 18:32:48 +01:00
return $this->wire()->fields->_hasPermission($this, 'view', $page, $user);
2022-03-08 15:55:41 +01:00
}
/**
* Is this field editable?
*
* - To maximize efficiency check that `$field->useRoles` is true before calling this.
* - If you have already verified that the page is editable, omit or specify null for $page argument.
* - **Please note:** this does not check that the provided $page itself is editable. If you want that
* check, then use `$page->editable($field)` instead.
*
* #pw-group-access
*
* @param Page|string|int|null $page Optionally specify a Page for context
* @param User|string|int|null $user Optionally specify a different user (default = current user)
* @return bool
*
*/
public function ___editable(Page $page = null, User $user = null) {
2022-11-05 18:32:48 +01:00
return $this->wire()->fields->_hasPermission($this, 'edit', $page, $user);
2022-03-08 15:55:41 +01:00
}
/**
* Save this fields settings and data in the database.
*
* To hook this save, hook to `Fields::save()` instead.
*
* #pw-group-manipulation
*
* @return bool
*
*/
public function save() {
2022-11-05 18:32:48 +01:00
return $this->wire()->fields->save($this);
2022-03-08 15:55:41 +01:00
}
/**
* Return the number of Fieldgroups this field is used in.
*
* Primarily used to check if the Field is deletable.
*
* #pw-group-retrieval
*
* @return int
*
*/
public function numFieldgroups() {
return $this->getFieldgroups(true);
}
/**
* Return the list of Fieldgroups using this field.
*
* #pw-group-retrieval
*
* @param bool $getCount Get count rather than FieldgroupsArray? (default=false) 3.0.182+
* @return FieldgroupsArray|int WireArray of Fieldgroup objects or count if requested
*
*/
public function getFieldgroups($getCount = false) {
2022-11-05 18:32:48 +01:00
return $this->wire()->fields->getFieldgroups($this, $getCount);
2022-03-08 15:55:41 +01:00
}
/**
* Return the list of of Templates using this field.
*
* #pw-group-retrieval
*
* @param bool $getCount Get count rather than FieldgroupsArray? (default=false) 3.0.182+
* @return TemplatesArray|int WireArray of Template objects or count when requested.
*
*/
public function getTemplates($getCount = false) {
2022-11-05 18:32:48 +01:00
return $this->wire()->fields->getTemplates($this, $getCount);
2022-03-08 15:55:41 +01:00
}
/**
* Return the default value for this field (if set), or null otherwise.
*
* #pw-internal
*
* @deprecated Use $field->type->getDefaultValue($page, $field) instead.
*
*/
public function getDefaultValue() {
$value = $this->get('default');
if($value) return $value;
return null;
}
/**
* Get the Inputfield module used to collect input for this field.
*
* #pw-group-retrieval
*
* @param Page $page Page that the Inputfield is for.
* @param string $contextStr Optional context string to append to the Inputfield's name/id (for repeaters and such).
* @return Inputfield|null
*
*/
public function ___getInputfield(Page $page, $contextStr = '') {
if(!$this->type) return null;
// check access control
$locked = false;
if($this->useRoles && !$this->editable($page)) {
// $this->message("not editable: " . $this->name);
if(($this->flags & self::flagAccessEditor) && $this->viewable($page)) {
// Inputfield is viewable but not editable
$locked = true;
} else {
// Inputfield is neither editable nor viewable
$locked = 'hidden';
}
}
$inputfield = $this->type->getInputfield($page, $this);
if(!$inputfield) return null;
// predefined field settings
$inputfield->attr('name', $this->name . $contextStr);
$inputfield->set('label', $this->label);
// just in case an Inputfield needs to know its Fieldtype/Field context, or lack of it
$inputfield->set('hasFieldtype', $this->type);
$inputfield->set('hasField', $this);
$inputfield->set('hasPage', $page);
// custom field settings
foreach($this->data as $key => $value) {
if($inputfield instanceof InputfieldWrapper) {
$has = $inputfield->hasSetting($key) || $inputfield->hasAttribute($key);
} else {
$has = $inputfield->has($key);
}
if($has) {
if(is_array($this->trackGets)) $this->trackGets($key);
$inputfield->set($key, $value);
}
}
if($locked && $locked === 'hidden') {
// Inputfield should not be shown
$inputfield->collapsed = Inputfield::collapsedHidden;
} else if($locked) {
// Inputfield is locked as a result of access control
$collapsed = $inputfield->getSetting('collapsed');
2022-11-05 18:32:48 +01:00
$ignoreCollapsed = array(
Inputfield::collapsedNoLocked,
Inputfield::collapsedBlankLocked,
Inputfield::collapsedYesLocked,
Inputfield::collapsedHidden
);
2022-03-08 15:55:41 +01:00
if(!in_array($collapsed, $ignoreCollapsed)) {
// Inputfield is not already locked or hidden, convert to locked equivalent
2022-11-05 18:32:48 +01:00
if($collapsed == Inputfield::collapsedYes) {
2022-03-08 15:55:41 +01:00
$collapsed = Inputfield::collapsedYesLocked;
2022-11-05 18:32:48 +01:00
} else if($collapsed == Inputfield::collapsedBlank) {
$collapsed = Inputfield::collapsedBlankLocked;
2022-03-08 15:55:41 +01:00
} else if($collapsed == Inputfield::collapsedNo) {
$collapsed = Inputfield::collapsedNoLocked;
} else {
$collapsed = Inputfield::collapsedYesLocked;
}
$inputfield->collapsed = $collapsed;
}
}
if(count($this->inputfieldSettings)) {
// runtime-only settings to Inputfield (these are not stored in DB)
foreach($this->inputfieldSettings as $name => $value) {
$inputfield->set($name, $value);
}
}
if($contextStr) {
// update dependency strings for the context
foreach(array('showIf', 'requiredIf') as $depType) {
$theIf = $inputfield->getSetting($depType);
if(empty($theIf)) continue;
$inputfield->set($depType, preg_replace('/([_.|a-zA-Z0-9]+)([=!%*<>]+)/', '$1' . $contextStr . '$2', $theIf));
}
}
return $inputfield;
}
/**
* Get or set a runtime-only setting that will be sent to the Inputfield during the getInputfield() call
*
* #pw-internal
*
* @param string $name Specify setting name to get or set, or '*' to get all.
* @param null|mixed $value Specify value, or 'clear' to clear setting(s) described in $name argument.
* @return null|array|bool|mixed Returns setting value, null if not found, true if set or clear requested, or array if all settings requested.
*
*/
public function inputfieldSetting($name, $value = null) {
if($name === '*') {
// get or clear ALL settings
if($value === 'clear') {
$this->inputfieldSettings = array();
return true;
} else {
return $this->inputfieldSettings;
}
} else if(is_null($value)) {
// get a setting, or return null if not found
return isset($this->inputfieldSettings[$name]) ? $this->inputfieldSettings[$name] : null;
} else if($value === 'clear') {
// clear a setting
unset($this->inputfieldSettings[$name]);
return true;
} else {
// set a named setting
$this->inputfieldSettings[$name] = $value;
return true;
}
}
/**
* Get any Inputfields needed to configure the field in the admin.
*
* #pw-group-retrieval
*
* @return InputfieldWrapper
*
*/
public function ___getConfigInputfields() {
$wrapper = $this->wire(new InputfieldWrapper());
$fieldgroupContext = $this->flags & Field::flagFieldgroupContext;
if($fieldgroupContext) {
$allowContext = $this->type->getConfigAllowContext($this);
if(!is_array($allowContext)) $allowContext = array();
$allowContext = array_merge($allowContext, $this->allowContexts);
} else {
$allowContext = array();
}
if(!$fieldgroupContext || count($allowContext)) {
$inputfields = $this->wire(new InputfieldWrapper());
if(!$fieldgroupContext) $inputfields->head = $this->_('Field type details');
$inputfields->attr('title', $this->_('Details'));
$inputfields->attr('id+name', 'fieldtypeConfig');
$remainingNames = array();
foreach($allowContext as $name) $remainingNames[$name] = $name;
try {
$fieldtypeInputfields = $this->type->getConfigInputfields($this);
if(!$fieldtypeInputfields) $fieldtypeInputfields = $this->wire(new InputfieldWrapper());
$configArray = $this->type->getConfigArray($this);
if(count($configArray)) {
$w = $this->wire(new InputfieldWrapper());
$w->importArray($configArray);
$w->populateValues($this);
$fieldtypeInputfields->import($w);
}
foreach($fieldtypeInputfields as $inputfield) {
if($fieldgroupContext && !in_array($inputfield->name, $allowContext)) continue;
$inputfields->append($inputfield);
unset($remainingNames[$inputfield->name]);
}
// now capture those that may have been stuck in a fieldset
if($fieldgroupContext) {
foreach($remainingNames as $name) {
if($inputfields->getChildByName($name)) continue;
$inputfield = $fieldtypeInputfields->getChildByName($name);
if(!$inputfield) continue;
$inputfields->append($inputfield);
unset($remainingNames[$inputfield->name]);
}
}
} catch(\Exception $e) {
$this->trackException($e, false, true);
}
if(count($inputfields)) $wrapper->append($inputfields);
}
$inputfields = $this->wire(new InputfieldWrapper());
2022-11-05 18:32:48 +01:00
$dummyPage = $this->wire()->pages->get('/'); // only using this to satisfy param requirement
2022-03-08 15:55:41 +01:00
$inputfield = $this->getInputfield($dummyPage);
if($inputfield) {
if($fieldgroupContext) {
$allowContext = array('visibility', 'collapsed', 'columnWidth', 'required', 'requiredIf', 'showIf');
$allowContext = array_merge($allowContext, $this->allowContexts, $inputfield->getConfigAllowContext($this));
} else {
$allowContext = array();
$inputfields->head = $this->_('Input field settings');
}
$remainingNames = array();
foreach($allowContext as $name) {
$remainingNames[$name] = $name;
}
$inputfields->attr('title', $this->_('Input'));
$inputfields->attr('id+name', 'inputfieldConfig');
/** @var InputfieldWrapper $inputfieldInputfields */
$inputfieldInputfields = $inputfield->getConfigInputfields();
if(!$inputfieldInputfields) $inputfieldInputfields = $this->wire(new InputfieldWrapper());
$configArray = $inputfield->getConfigArray();
if(count($configArray)) {
$w = $this->wire(new InputfieldWrapper());
$w->importArray($configArray);
$w->populateValues($this);
$inputfieldInputfields->import($w);
}
foreach($inputfieldInputfields as $i) {
if($fieldgroupContext && !in_array($i->name, $allowContext)) continue;
$inputfields->append($i);
unset($remainingNames[$i->name]);
}
if($fieldgroupContext) {
foreach($remainingNames as $name) {
if($inputfields->getChildByName($name)) continue;
$inputfield = $inputfieldInputfields->getChildByName($name);
if(!$inputfield) continue;
$inputfields->append($inputfield);
unset($remainingNames[$inputfield->name]);
}
}
}
$wrapper->append($inputfields);
return $wrapper;
}
/**
* Get the database table used by this field.
*
* #pw-group-retrieval
*
* @return string
* @throws WireException
*
*/
public function getTable() {
2022-11-05 18:32:48 +01:00
if(self::$lowercaseTables === null) {
self::$lowercaseTables = $this->wire()->config->dbLowercaseTables ? true : false;
}
2022-03-08 15:55:41 +01:00
if(!empty($this->setTable)) {
$table = $this->setTable;
} else {
$name = $this->settings['name'];
if(!strlen($name)) throw new WireException("Field 'name' is required");
$table = self::tablePrefix . $name;
}
if(self::$lowercaseTables) $table = strtolower($table);
return $table;
}
/**
* Set an override table name, or omit (or null) to restore default table name
*
* #pw-group-advanced
*
* @param null|string $table
*
*/
public function setTable($table = null) {
2022-11-05 18:32:48 +01:00
$table = empty($table) ? '' : $this->wire()->sanitizer->fieldName($table);
2022-03-08 15:55:41 +01:00
$this->setTable = $table;
}
/**
* The string value of a Field is always it's name
*
*/
public function __toString() {
return $this->settings['name'];
}
2022-11-05 18:32:48 +01:00
/**
* Isset
*
* @param string $key
* @return bool
*
*/
2022-03-08 15:55:41 +01:00
public function __isset($key) {
if(parent::__isset($key)) return true;
return isset($this->settings[$key]);
}
/**
* Return field label, description or notes for language
*
* @param string $property Specify either label, description or notes
* @param Page|Language $language Optionally specify a language. If not specified user's current language is used.
* @return string
*
*/
protected function getText($property, $language = null) {
2022-11-05 18:32:48 +01:00
if(is_null($language)) {
$language = $this->wire()->languages ? $this->wire()->user->language : null;
}
2022-03-08 15:55:41 +01:00
if($language) {
2022-11-05 18:32:48 +01:00
$value = (string) $this->get("$property$language");
if(!strlen($value)) $value = (string) $this->$property;
2022-03-08 15:55:41 +01:00
} else {
2022-11-05 18:32:48 +01:00
$value = (string) $this->$property;
2022-03-08 15:55:41 +01:00
}
2022-11-05 18:32:48 +01:00
if($property === 'label' && !strlen($value)) $value = $this->name;
2022-03-08 15:55:41 +01:00
return $value;
}
/**
* Set a field label, description or notes for language
*
* @param string $property Specify either label, description or notes
* @param string $value Text to set for property
* @param Page|Language $language Optionally specify a language. If not specified default language is used.
*
*/
protected function setText($property, $value, $language = null) {
2022-11-05 18:32:48 +01:00
$languages = $this->wire()->languages;
if($languages && $language != null) {
if(is_string($language) || is_int($language)) $language = $languages->get($language);
2022-03-08 15:55:41 +01:00
if($language && (!$language->id || $language->isDefault())) $language = null;
} else {
$language = null;
}
if(is_null($language)) $language = '';
$this->set("$property$language", $value);
}
/**
* Get field label for current language, or another specified language.
*
* This is different from `$field->label` in that it knows about languages (when installed).
*
* #pw-group-retrieval
*
* @param Page|Language $language Optionally specify a language. If not specified user's current language is used.
* @return string
*
*/
public function getLabel($language = null) {
return $this->getText('label', $language);
}
/**
* Return field description for current language, or another specified language.
*
* This is different from `$field->description` in that it knows about languages (when installed).
*
* #pw-group-retrieval
*
* @param Page|Language $language Optionally specify a language. If not specified user's current language is used.
* @return string
*
*/
public function getDescription($language = null) {
return $this->getText('description', $language);
}
/**
* Return field notes for current language, or another specified language.
*
* This is different from `$field->notes` in that it knows about languages (when installed).
*
* #pw-group-retrieval
*
* @param Page|Language $language Optionally specify a language. If not specified user's current language is used.
* @return string
*
*/
public function getNotes($language = null) {
return $this->getText('notes', $language);
}
/**
* Return the icon used by this field, or blank if none.
*
* #pw-group-retrieval
*
* @param bool $prefix Whether or not you want the icon prefix included (i.e. "fa-")
* @return mixed|string
*
*/
public function getIcon($prefix = false) {
$icon = parent::get('icon');
if(empty($icon)) return '';
if(strpos($icon, 'fa-') === 0) $icon = str_replace('fa-', '', $icon);
if(strpos($icon, 'icon-') === 0) $icon = str_replace('icon-', '', $icon);
return $prefix ? "fa-$icon" : $icon;
}
/**
* Set label, optionally for a specific language
*
* #pw-group-manipulation
*
* @param string $text Text to set
* @param Language|string|int|null $language Language to use
* @since 3.0.16 Added for consistency, all versions can still set property directly.
*
*/
public function setLabel($text, $language = null) {
$this->setText('label', $text, $language);
}
/**
* Set description, optionally for a specific language
*
* #pw-group-manipulation
*
* @param string $text Text to set
* @param Language|string|int|null $language Language to use
* @since 3.0.16 Added for consistency, all versions can still set property directly.
*
*/
public function setDescription($text, $language = null) {
$this->setText('description', $text, $language);
}
/**
* Set notes, optionally for a specific language
*
* #pw-group-manipulation
*
* @param string $text Text to set
* @param Language|string|int|null $language Language to use
* @since 3.0.16 Added for consistency, all versions can still set property directly.
*
*/
public function setNotes($text, $language = null) {
$this->setText('notes', $text, $language);
}
/**
* Set the icon for this field
*
* #pw-group-manipulation
*
* @param string $icon Icon name
* @return $this
*
*/
public function setIcon($icon) {
// store the non-prefixed version
2022-11-05 18:32:48 +01:00
if(strlen("$icon")) {
if(strpos($icon, 'icon-') === 0) $icon = str_replace('icon-', '', $icon);
if(strpos($icon, 'fa-') === 0) $icon = str_replace('fa-', '', $icon);
$icon = $this->wire()->sanitizer->pageName($icon);
}
parent::set('icon', "$icon");
2022-03-08 15:55:41 +01:00
return $this;
}
/**
* Get tags
*
* @param bool|string $getString Optionally specify true for space-separated string, or delimiter string (default=false)
* @return array|string Returns array of tags unless $getString option is requested
* @since 3.0.106
*
*/
public function getTags($getString = false) {
if($this->tagList === null) {
$tagList = $this->setTags(parent::get('tags'));
} else {
$tagList = $this->tagList;
}
if($getString !== false) {
$delimiter = $getString === true ? ' ' : $getString;
return implode($delimiter, $tagList);
}
return $tagList;
}
/**
* Set all tags
*
* #pw-internal
*
* @param array|string $tagList Array of tags to add (or space-separated string)
* @param bool $reindex Set to false to set given $tagsList exactly as-is (assumes it's already in correct format)
* @return array Array of tags that were set
* @since 3.0.106
*
*/
public function setTags($tagList, $reindex = true) {
2022-11-05 18:32:48 +01:00
$textTools = $this->wire()->sanitizer->getTextTools();
2022-03-08 15:55:41 +01:00
if($tagList === null || $tagList === '') {
$tagList = array();
} else if(!is_array($tagList)) {
$tagList = explode(' ', $tagList);
}
if($reindex && count($tagList)) {
$tags = array();
foreach($tagList as $tag) {
$tag = trim($tag);
2022-11-05 18:32:48 +01:00
if(strlen($tag)) $tags[$textTools->strtolower($tag)] = $tag;
2022-03-08 15:55:41 +01:00
}
$tagList = $tags;
}
if($this->tagList !== $tagList) {
$this->tagList = $tagList;
parent::set('tags', implode(' ', $tagList));
2022-11-05 18:32:48 +01:00
$this->wire()->fields->getTags('reset');
2022-03-08 15:55:41 +01:00
}
return $tagList;
}
/**
* Add one or more tags
*
* @param string $tag
* @return array Returns current tag list
* @since 3.0.106
*
*/
public function addTag($tag) {
2022-11-05 18:32:48 +01:00
$textTools = $this->wire()->sanitizer->getTextTools();
2022-03-08 15:55:41 +01:00
$tagList = $this->getTags();
2022-11-05 18:32:48 +01:00
$tagList[$textTools->strtolower($tag)] = $tag;
2022-03-08 15:55:41 +01:00
$this->setTags($tagList, false);
return $tagList;
}
/**
* Return true if this field has the given tag or false if not
*
* @param string $tag
* @return bool
* @since 3.0.106
*
*/
public function hasTag($tag) {
2022-11-05 18:32:48 +01:00
$textTools = $this->wire()->sanitizer->getTextTools();
2022-03-08 15:55:41 +01:00
$tagList = $this->getTags();
2022-11-05 18:32:48 +01:00
return isset($tagList[$textTools->strtolower(trim(ltrim($tag, '-')))]);
2022-03-08 15:55:41 +01:00
}
/**
* Remove a tag
*
* @param string $tag
* @return array Returns current tag list
* @since 3.0.106
*
*/
public function removeTag($tag) {
2022-11-05 18:32:48 +01:00
$textTools = $this->wire()->sanitizer->getTextTools();
2022-03-08 15:55:41 +01:00
$tagList = $this->getTags();
2022-11-05 18:32:48 +01:00
$tag = $textTools->strtolower($tag);
2022-03-08 15:55:41 +01:00
if(!isset($tagList[$tag])) return $tagList;
unset($tagList[$tag]);
return $this->setTags($tagList, false);
}
/**
* Get URL to edit field in the admin
*
* @param array|bool|string $options Specify array of options, string for find option, or bool for http option.
* - `find` (string): Name of field to find in editor form
* - `http` (bool): True to force inclusion of scheme and hostname
* @return string
* @since 3.0.151
*
*/
public function editUrl($options = array()) {
if(is_string($options)) $options = array('find' => $options);
if(is_bool($options)) $options = array('http' => $options);
if(!is_array($options)) $options = array();
2022-11-05 18:32:48 +01:00
$url = $this->wire()->config->urls(empty($options['http']) ? 'admin' : 'httpAdmin');
2022-03-08 15:55:41 +01:00
$url .= "setup/field/edit?id=$this->id";
2022-11-05 18:32:48 +01:00
if(!empty($options['find'])) $url .= '#find-' . $this->wire()->sanitizer->fieldName($options['find']);
2022-03-08 15:55:41 +01:00
return $url;
}
/**
* debugInfo PHP 5.6+ magic method
*
* This is used when you print_r() an object instance.
*
* @return array
*
*/
public function __debugInfo() {
$info = $this->settings;
$info['flags'] = $info['flags'] ? "$this->flagsStr ($info[flags])" : "";
$info = array_merge($info, parent::__debugInfo());
if($this->prevTable) $info['prevTable'] = $this->prevTable;
if($this->prevName) $info['prevName'] = $this->prevName;
if($this->prevFieldtype) $info['prevFieldtype'] = (string) $this->prevFieldtype;
if(!empty($this->trackGets)) $info['trackGets'] = $this->trackGets;
if($this->useRoles) {
$info['viewRoles'] = $this->viewRoles;
$info['editRoles'] = $this->editRoles;
}
return $info;
}
public function debugInfoSmall() {
return array(
'id' => $this->id,
'name' => $this->name,
'label' => $this->getLabel(),
'type' => $this->type ? wireClassName($this->type) : '',
);
}
}