1496 lines
44 KiB
PHP
1496 lines
44 KiB
PHP
|
<?php namespace ProcessWire;
|
|||
|
|
|||
|
/**
|
|||
|
* ProcessWire Fields
|
|||
|
*
|
|||
|
* Manages collection of ALL Field instances, not specific to any particular Fieldgroup
|
|||
|
*
|
|||
|
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
|
|||
|
* https://processwire.com
|
|||
|
*
|
|||
|
* #pw-summary Manages all custom fields in ProcessWire, independently of any Fieldgroup.
|
|||
|
* #pw-var $fields
|
|||
|
* #pw-body =
|
|||
|
* Each field returned is an object of type `Field`. The $fields API variable is iterable:
|
|||
|
* ~~~~~
|
|||
|
* foreach($fields as $field) {
|
|||
|
* echo "<p>Name: $field->name, Type: $field->type, Label: $field->label</p>";
|
|||
|
* }
|
|||
|
* ~~~~~
|
|||
|
* #pw-body
|
|||
|
*
|
|||
|
* @method Field|null get($key) Get a field by name or id
|
|||
|
* @method bool changeFieldtype(Field $field1, $keepSettings = false)
|
|||
|
* @method bool saveFieldgroupContext(Field $field, Fieldgroup $fieldgroup, $namespace = '')
|
|||
|
* @method bool deleteFieldDataByTemplate(Field $field, Template $template) #pw-hooker
|
|||
|
* @method void changedType(Saveable $item, Fieldtype $fromType, Fieldtype $toType) #pw-hooker
|
|||
|
* @method void changeTypeReady(Saveable $item, Fieldtype $fromType, Fieldtype $toType) #pw-hooker
|
|||
|
* @method bool|Field clone(Field $item, $name = '') Clone a field and return it or return false on fail.
|
|||
|
* @method array getTags($getFieldNames = false) Get tags for all fields (3.0.179+) #pw-advanced
|
|||
|
* @method bool applySetupName(Field $field, $setupName = '')
|
|||
|
*
|
|||
|
*/
|
|||
|
|
|||
|
class Fields extends WireSaveableItems {
|
|||
|
|
|||
|
/**
|
|||
|
* Instance of FieldsArray
|
|||
|
*
|
|||
|
* @var FieldsArray
|
|||
|
*
|
|||
|
*/
|
|||
|
protected $fieldsArray = null;
|
|||
|
|
|||
|
/**
|
|||
|
* Field names that are native/permanent to the system and thus treated differently in several instances.
|
|||
|
*
|
|||
|
* For example, a Field can't be given one of these names.
|
|||
|
*
|
|||
|
*/
|
|||
|
static protected $nativeNamesSystem = array(
|
|||
|
'child',
|
|||
|
'children',
|
|||
|
'count',
|
|||
|
'check_access',
|
|||
|
'created_users_id',
|
|||
|
'created',
|
|||
|
'createdUser',
|
|||
|
'createdUserID',
|
|||
|
'createdUsersID',
|
|||
|
'data',
|
|||
|
'description',
|
|||
|
'editUrl',
|
|||
|
'end',
|
|||
|
'fieldgroup',
|
|||
|
'fields',
|
|||
|
'find',
|
|||
|
'flags',
|
|||
|
'get',
|
|||
|
'has_parent',
|
|||
|
'hasParent',
|
|||
|
'httpUrl',
|
|||
|
'id',
|
|||
|
'include',
|
|||
|
'isNew',
|
|||
|
'limit',
|
|||
|
'modified_users_id',
|
|||
|
'modified',
|
|||
|
'modifiedUser',
|
|||
|
'modifiedUserID',
|
|||
|
'modifiedUsersID',
|
|||
|
'name',
|
|||
|
'num_children',
|
|||
|
'numChildren',
|
|||
|
'parent_id',
|
|||
|
'parent',
|
|||
|
'parents',
|
|||
|
'path',
|
|||
|
'published',
|
|||
|
'rootParent',
|
|||
|
'siblings',
|
|||
|
'sort',
|
|||
|
'sortfield',
|
|||
|
'start',
|
|||
|
'status',
|
|||
|
'template',
|
|||
|
'templatePrevious',
|
|||
|
'templates_id',
|
|||
|
'url',
|
|||
|
'_custom',
|
|||
|
);
|
|||
|
|
|||
|
/**
|
|||
|
* Flag names in format [ flagInt => 'flagName' ]
|
|||
|
*
|
|||
|
* @var array
|
|||
|
*
|
|||
|
*/
|
|||
|
protected $flagNames = array();
|
|||
|
|
|||
|
/**
|
|||
|
* Field names that are native/permanent to this instance of ProcessWire (configurable at runtime)
|
|||
|
*
|
|||
|
* Array indexes are the names and values are all boolean true.
|
|||
|
*
|
|||
|
*/
|
|||
|
protected $nativeNamesLocal = array();
|
|||
|
|
|||
|
/**
|
|||
|
* Cache of all tags for all fields, populated to array when asked for the first time
|
|||
|
*
|
|||
|
* @var array|null
|
|||
|
*
|
|||
|
*/
|
|||
|
protected $tagList = null;
|
|||
|
|
|||
|
/**
|
|||
|
* @var FieldsTableTools|null
|
|||
|
*
|
|||
|
*/
|
|||
|
protected $tableTools = null;
|
|||
|
|
|||
|
/**
|
|||
|
* @var Fieldtypes|null
|
|||
|
*
|
|||
|
*/
|
|||
|
protected $fieldtypes = null;
|
|||
|
|
|||
|
/**
|
|||
|
* Construct
|
|||
|
*
|
|||
|
*/
|
|||
|
public function __construct() {
|
|||
|
parent::__construct();
|
|||
|
$this->flagNames = array(
|
|||
|
Field::flagAutojoin => 'autojoin',
|
|||
|
Field::flagGlobal => 'global',
|
|||
|
Field::flagSystem => 'system',
|
|||
|
Field::flagPermanent => 'permanent',
|
|||
|
Field::flagAccess => 'access',
|
|||
|
Field::flagAccessAPI => 'access-api',
|
|||
|
Field::flagAccessEditor => 'access-editor',
|
|||
|
Field::flagFieldgroupContext => 'fieldgroup-context',
|
|||
|
Field::flagSystemOverride => 'system-override',
|
|||
|
);
|
|||
|
// convert so that keys are names so that isset() can be used rather than in_array()
|
|||
|
if(isset(self::$nativeNamesSystem[0])) {
|
|||
|
self::$nativeNamesSystem = array_flip(self::$nativeNamesSystem);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
public function getCacheItemName() {
|
|||
|
return array('roles', 'permissions', 'title', 'process');
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Construct and load the Fields
|
|||
|
*
|
|||
|
* #pw-internal
|
|||
|
*
|
|||
|
*/
|
|||
|
public function init() {
|
|||
|
$this->getWireArray();
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Per WireSaveableItems interface, return a blank instance of a Field
|
|||
|
*
|
|||
|
* #pw-internal
|
|||
|
*
|
|||
|
* @return Field
|
|||
|
*
|
|||
|
*/
|
|||
|
public function makeBlankItem() {
|
|||
|
return $this->wire(new Field());
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Make an item and populate with given data
|
|||
|
*
|
|||
|
* @param array $a Associative array of data to populate
|
|||
|
* @return Saveable|Wire
|
|||
|
* @throws WireException
|
|||
|
* @since 3.0.146
|
|||
|
*
|
|||
|
*/
|
|||
|
public function makeItem(array $a = array()) {
|
|||
|
|
|||
|
if(empty($a['type'])) return parent::makeItem($a);
|
|||
|
if($this->fieldtypes === null) $this->fieldtypes = $this->wire()->fieldtypes;
|
|||
|
if(!$this->fieldtypes) return parent::makeItem($a);
|
|||
|
|
|||
|
/** @var Fieldtype $fieldtype */
|
|||
|
$fieldtype = $this->fieldtypes->get($a['type']);
|
|||
|
if(!$fieldtype) {
|
|||
|
if($this->useLazy) {
|
|||
|
$this->error("Fieldtype module '$a[type]' for field '$a[name]' is missing");
|
|||
|
$fieldtype = $this->fieldtypes->get('FieldtypeText');
|
|||
|
} else {
|
|||
|
return parent::makeItem($a);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
$a['type'] = $fieldtype;
|
|||
|
$a['id'] = (int) $a['id'];
|
|||
|
$a['flags'] = (int) $a['flags'];
|
|||
|
|
|||
|
$class = $fieldtype->getFieldClass($a);
|
|||
|
|
|||
|
if(empty($class) || $class === 'Field') {
|
|||
|
$class = '';
|
|||
|
} else if(strpos($class, "\\") === false) {
|
|||
|
$class = wireClassName($class, true);
|
|||
|
if(!class_exists($class)) $class = '';
|
|||
|
}
|
|||
|
|
|||
|
if(empty($class)) {
|
|||
|
$field = new Field();
|
|||
|
} else {
|
|||
|
$field = new $class(); /** @var Field $field */
|
|||
|
}
|
|||
|
|
|||
|
$this->wire($field);
|
|||
|
$field->setTrackChanges(false);
|
|||
|
|
|||
|
foreach($a as $key => $value) {
|
|||
|
$field->setRawSetting($key, $value);
|
|||
|
}
|
|||
|
|
|||
|
$field->resetTrackChanges(true);
|
|||
|
|
|||
|
return $field;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Create a new Saveable item from a raw array ($row) and add it to $items
|
|||
|
*
|
|||
|
* @param array $row
|
|||
|
* @param WireArray|null $items
|
|||
|
* @return Saveable|WireData|Wire
|
|||
|
* @since 3.0.194
|
|||
|
*
|
|||
|
*/
|
|||
|
protected function initItem(array &$row, WireArray $items = null) {
|
|||
|
/** @var Field $item */
|
|||
|
$item = parent::initItem($row, $items);
|
|||
|
$fieldtype = $item ? $item->type : null;
|
|||
|
if($fieldtype) $fieldtype->initField($item);
|
|||
|
return $item;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Per WireSaveableItems interface, return all available Field instances
|
|||
|
*
|
|||
|
* #pw-internal
|
|||
|
*
|
|||
|
* @return FieldsArray|WireArray
|
|||
|
*
|
|||
|
*/
|
|||
|
public function getAll() {
|
|||
|
if($this->useLazy()) $this->loadAllLazyItems();
|
|||
|
return $this->getWireArray();
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Get WireArray container that items are stored in
|
|||
|
*
|
|||
|
* @return WireArray
|
|||
|
* @since 3.0.194
|
|||
|
*
|
|||
|
*/
|
|||
|
public function getWireArray() {
|
|||
|
if($this->fieldsArray === null) {
|
|||
|
$this->fieldsArray = new FieldsArray();
|
|||
|
$this->wire($this->fieldsArray);
|
|||
|
$this->load($this->fieldsArray);
|
|||
|
}
|
|||
|
return $this->fieldsArray;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Per WireSaveableItems interface, return the table name used to save Fields
|
|||
|
*
|
|||
|
* #pw-internal
|
|||
|
*
|
|||
|
*/
|
|||
|
public function getTable() {
|
|||
|
return "fields";
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Return the name that fields should be initially sorted by
|
|||
|
*
|
|||
|
* #pw-internal
|
|||
|
*
|
|||
|
*/
|
|||
|
public function getSort() {
|
|||
|
return $this->getTable() . ".name";
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Save a Field to the database
|
|||
|
*
|
|||
|
* ~~~~~
|
|||
|
* // Modify a field label and save it
|
|||
|
* $field = $fields->get('title');
|
|||
|
* $field->label = 'Title or Headline';
|
|||
|
* $fields->save($field);
|
|||
|
* ~~~~~
|
|||
|
*
|
|||
|
* @param Field|Saveable $item The field to save
|
|||
|
* @return bool True on success, false on failure
|
|||
|
* @throws WireException
|
|||
|
*
|
|||
|
*/
|
|||
|
public function ___save(Saveable $item) {
|
|||
|
|
|||
|
if($item->flags & Field::flagFieldgroupContext) throw new WireException("Field $item is not saveable because it is in a specific context");
|
|||
|
if(!strlen($item->name)) throw new WireException("Field name is required");
|
|||
|
|
|||
|
$database = $this->wire()->database;
|
|||
|
$isNew = $item->id < 1;
|
|||
|
$prevTable = $database->escapeTable($item->prevTable);
|
|||
|
$table = $database->escapeTable($item->getTable());
|
|||
|
|
|||
|
if(!$isNew && $prevTable && $prevTable != $table) {
|
|||
|
// note that we rename the table twice in order to force MySQL to perform the rename
|
|||
|
// even if only the case has changed.
|
|||
|
$schema = $item->type->getDatabaseSchema($item);
|
|||
|
if(!empty($schema)) {
|
|||
|
foreach(array($table, "tmp_$table") as $t) {
|
|||
|
if(!$database->tableExists($t)) continue;
|
|||
|
throw new WireException("Cannot rename to '$item->name' because table `$table` already exists");
|
|||
|
}
|
|||
|
$database->exec("RENAME TABLE `$prevTable` TO `tmp_$table`"); // QA
|
|||
|
$database->exec("RENAME TABLE `tmp_$table` TO `$table`"); // QA
|
|||
|
}
|
|||
|
$item->prevTable = '';
|
|||
|
}
|
|||
|
|
|||
|
if(!$isNew && $item->prevName && $item->prevName != $item->name) {
|
|||
|
$item->type->renamedField($item, $item->prevName);
|
|||
|
$item->prevName = '';
|
|||
|
}
|
|||
|
|
|||
|
if($item->prevFieldtype && $item->prevFieldtype->name != $item->type->name) {
|
|||
|
if(!$this->changeFieldtype($item)) {
|
|||
|
$item->type = $item->prevFieldtype;
|
|||
|
$this->error("Error changing fieldtype for '$item', reverted back to '{$item->type}'");
|
|||
|
} else {
|
|||
|
$item->prevFieldtype = null;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if(!$item->type) throw new WireException("Can't save a Field that doesn't have it's 'type' property set to a Fieldtype");
|
|||
|
$item->type->saveFieldReady($item);
|
|||
|
if(!parent::___save($item)) return false;
|
|||
|
if($isNew) $item->type->createField($item);
|
|||
|
|
|||
|
$setupName = $item->setSetupName();
|
|||
|
if($setupName || $isNew) {
|
|||
|
if($this->applySetupName($item, $setupName)) {
|
|||
|
$item->setSetupName('');
|
|||
|
parent::___save($item);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if($item->flags & Field::flagGlobal) {
|
|||
|
// make sure that all template fieldgroups contain this field and add to any that don't.
|
|||
|
foreach($this->wire()->templates as $template) {
|
|||
|
if($template->noGlobal) continue;
|
|||
|
$fieldgroup = $template->fieldgroup;
|
|||
|
if(!$fieldgroup->hasField($item)) {
|
|||
|
$fieldgroup->add($item);
|
|||
|
$fieldgroup->save();
|
|||
|
$this->message("Added field '{$item->name}' to template/fieldgroup '$fieldgroup->name'", Notice::debug);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if($item->type) $item->type->savedField($item);
|
|||
|
|
|||
|
$this->getTags('reset');
|
|||
|
|
|||
|
return true;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Check that the given Field's table exists and create it if it doesn't
|
|||
|
*
|
|||
|
* @param Field $field
|
|||
|
*
|
|||
|
*/
|
|||
|
protected function checkFieldTable(Field $field) {
|
|||
|
$database = $this->wire()->database;
|
|||
|
$table = $database->escapeTable($field->getTable());
|
|||
|
if(empty($table)) return;
|
|||
|
$exists = $database->query("SHOW TABLES LIKE '$table'")->rowCount() > 0;
|
|||
|
if($exists) return;
|
|||
|
try {
|
|||
|
if($field->type && count($field->type->getDatabaseSchema($field))) {
|
|||
|
if($field->type->createField($field)) $this->message("Created table '$table'");
|
|||
|
}
|
|||
|
} catch(\Exception $e) {
|
|||
|
$this->trackException($e, false, $e->getMessage() . " (checkFieldTable)");
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Check that all fields in the system have their tables installed
|
|||
|
*
|
|||
|
* This enables you to re-create field tables when migrating over entries from the Fields table manually (via SQL dumps or the like)
|
|||
|
*
|
|||
|
* #pw-internal
|
|||
|
*
|
|||
|
*/
|
|||
|
public function checkFieldTables() {
|
|||
|
foreach($this as $field) $this->checkFieldTable($field);
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Delete a Field from the database
|
|||
|
*
|
|||
|
* This method will throw a WireException if you attempt to delete a field that is currently in use (i.e. assigned to one or more fieldgroups).
|
|||
|
*
|
|||
|
* @param Field|Saveable $item Field to delete
|
|||
|
* @return bool True on success, false on failure
|
|||
|
* @throws WireException
|
|||
|
*
|
|||
|
*/
|
|||
|
public function ___delete(Saveable $item) {
|
|||
|
|
|||
|
if(!$this->getWireArray()->isValidItem($item)) {
|
|||
|
throw new WireException("Fields::delete(item) only accepts items of type Field");
|
|||
|
}
|
|||
|
|
|||
|
// if the field doesn't have an ID, so it's not one that came from the DB
|
|||
|
if(!$item->id) {
|
|||
|
$table = $item->getTable();
|
|||
|
throw new WireException("Unable to delete from '$table' for field that doesn't exist in fields table");
|
|||
|
}
|
|||
|
|
|||
|
// if it's in use by any fieldgroups, then we don't allow it to be deleted
|
|||
|
if($item->numFieldgroups()) {
|
|||
|
$names = $item->getFieldgroups()->implode("', '", (string) "name");
|
|||
|
throw new WireException("Unable to delete field '$item->name' because it is in use by these fieldgroups: '$names'");
|
|||
|
}
|
|||
|
|
|||
|
// if it's a system field, it may not be deleted
|
|||
|
if($item->flags & Field::flagSystem) {
|
|||
|
throw new WireException("Unable to delete field '$item->name' because it is a system field.");
|
|||
|
}
|
|||
|
|
|||
|
// delete entries in fieldgroups_fields table. Not really necessary since the above exception prevents this, but here in case that changes.
|
|||
|
$this->wire()->fieldgroups->deleteField($item);
|
|||
|
|
|||
|
// drop the field's table
|
|||
|
if($item->type) $item->type->deleteField($item);
|
|||
|
|
|||
|
return parent::___delete($item);
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
/**
|
|||
|
* Create and return a cloned copy of the given Field
|
|||
|
*
|
|||
|
* @param Field|Saveable $item Field to clone
|
|||
|
* @param string $name Optionally specify name for new cloned item
|
|||
|
* @return Field $item Returns the new clone on success, or false on failure
|
|||
|
*
|
|||
|
*/
|
|||
|
public function ___clone(Saveable $item, $name = '') {
|
|||
|
|
|||
|
$item = $item->type->cloneField($item);
|
|||
|
|
|||
|
// don't clone system flags
|
|||
|
if($item->flags & Field::flagSystem || $item->flags & Field::flagPermanent) {
|
|||
|
$item->flags = $item->flags | Field::flagSystemOverride;
|
|||
|
if($item->flags & Field::flagSystem) $item->flags = $item->flags & ~Field::flagSystem;
|
|||
|
if($item->flags & Field::flagPermanent) $item->flags = $item->flags & ~Field::flagPermanent;
|
|||
|
$item->flags = $item->flags & ~Field::flagSystemOverride;
|
|||
|
}
|
|||
|
|
|||
|
// don't clone the 'global' flag
|
|||
|
if($item->flags & Field::flagGlobal) $item->flags = $item->flags & ~Field::flagGlobal;
|
|||
|
|
|||
|
/** @var Field $item */
|
|||
|
$item = parent::___clone($item, $name);
|
|||
|
if($item) {
|
|||
|
$item->prevTable = null;
|
|||
|
$item->prevName = ''; // prevent renamed hook
|
|||
|
}
|
|||
|
|
|||
|
return $item;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Save the context of the given field for the given fieldgroup
|
|||
|
*
|
|||
|
* #pw-advanced
|
|||
|
*
|
|||
|
* @param Field $field Field to save context for
|
|||
|
* @param Fieldgroup $fieldgroup Context for when field is in this fieldgroup
|
|||
|
* @param string $namespace An optional namespace for additional context
|
|||
|
* @return bool True on success
|
|||
|
* @throws WireException
|
|||
|
*
|
|||
|
*/
|
|||
|
public function ___saveFieldgroupContext(Field $field, Fieldgroup $fieldgroup, $namespace = '') {
|
|||
|
|
|||
|
// get field without context
|
|||
|
$fieldOriginal = $this->get($field->name);
|
|||
|
$data = array();
|
|||
|
|
|||
|
// make sure given field and fieldgroup are valid
|
|||
|
if(!($field->flags & Field::flagFieldgroupContext)) {
|
|||
|
throw new WireException("Field must be in fieldgroup context before its context can be saved");
|
|||
|
}
|
|||
|
|
|||
|
if(!$fieldgroup->has($fieldOriginal)) {
|
|||
|
throw new WireException("Fieldgroup $fieldgroup does not contain field $field");
|
|||
|
}
|
|||
|
|
|||
|
$field_id = (int) $field->id;
|
|||
|
$fieldgroup_id = (int) $fieldgroup->id;
|
|||
|
$database = $this->wire()->database;
|
|||
|
|
|||
|
$newValues = $field->getArray();
|
|||
|
$oldValues = $fieldOriginal->getArray();
|
|||
|
|
|||
|
// 0 is the same as 100 for columnWidth, so we specifically set it just to prevent this from being saved when it doesn't need to be
|
|||
|
if(!isset($oldValues['columnWidth'])) $oldValues['columnWidth'] = 100;
|
|||
|
|
|||
|
// add the built-in fields
|
|||
|
foreach(array('label', 'description', 'notes', 'viewRoles', 'editRoles') as $key) {
|
|||
|
$newValues[$key] = $field->$key;
|
|||
|
$oldValues[$key] = $fieldOriginal->$key;
|
|||
|
}
|
|||
|
|
|||
|
// account for flags that may be applied as part of context
|
|||
|
$flags = $field->flags & ~Field::flagFieldgroupContext;
|
|||
|
if($flags != $fieldOriginal->flags) {
|
|||
|
$flagsAdd = 0;
|
|||
|
$flagsDel = 0;
|
|||
|
// flags that are allowed to be set via context
|
|||
|
$contextFlags = array(
|
|||
|
Field::flagAccess,
|
|||
|
Field::flagAccessAPI,
|
|||
|
Field::flagAccessEditor
|
|||
|
);
|
|||
|
foreach($contextFlags as $flag) {
|
|||
|
if($fieldOriginal->hasFlag($flag) && !$field->hasFlag($flag)) $flagsDel = $flagsDel | $flag;
|
|||
|
if(!$fieldOriginal->hasFlag($flag) && $field->hasFlag($flag)) $flagsAdd = $flagsAdd | $flag;
|
|||
|
}
|
|||
|
if($flagsAdd) $data['flagsAdd'] = $flagsAdd;
|
|||
|
if($flagsDel) $data['flagsDel'] = $flagsDel;
|
|||
|
}
|
|||
|
|
|||
|
// cycle through and determine which values should be saved
|
|||
|
foreach($newValues as $key => $value) {
|
|||
|
$oldValue = empty($oldValues[$key]) ? '' : $oldValues[$key];
|
|||
|
|
|||
|
// if both old and new are empty, then don't store a blank value in the context
|
|||
|
if(empty($oldValue) && empty($value)) continue;
|
|||
|
|
|||
|
// if old and new value are the same, then don't duplicate the value in the context
|
|||
|
if($value == $oldValue) continue;
|
|||
|
|
|||
|
// $value differs from $oldValue and should be saved
|
|||
|
$data[$key] = $value;
|
|||
|
}
|
|||
|
|
|||
|
// remove runtime properties (those that start with underscore)
|
|||
|
foreach($data as $key => $value) {
|
|||
|
if(strpos($key, '_') === 0) unset($data[$key]);
|
|||
|
}
|
|||
|
|
|||
|
// keep all in the same order so that it's easier to compare (by eye) in the DB
|
|||
|
ksort($data);
|
|||
|
|
|||
|
if($namespace) {
|
|||
|
// get existing data and move everything here into a namespace within that data
|
|||
|
$query = $database->prepare('SELECT data FROM fieldgroups_fields WHERE fields_id=:field_id AND fieldgroups_id=:fieldgroup_id');
|
|||
|
$query->bindValue(':field_id', $field_id, \PDO::PARAM_INT);
|
|||
|
$query->bindValue(':fieldgroup_id', $fieldgroup_id, \PDO::PARAM_INT);
|
|||
|
$query->execute();
|
|||
|
list($existingData) = $query->fetch(\PDO::FETCH_NUM);
|
|||
|
$existingData = strlen($existingData) ? json_decode($existingData, true) : array();
|
|||
|
if(!is_array($existingData)) $existingData = array();
|
|||
|
foreach($data as $k => $v) {
|
|||
|
// disallow namespace within namespace
|
|||
|
if(strpos($k, Fieldgroup::contextNamespacePrefix) === 0) unset($data[$k]);
|
|||
|
}
|
|||
|
$existingData[Fieldgroup::contextNamespacePrefix . $namespace] = $data;
|
|||
|
$data = $existingData;
|
|||
|
}
|
|||
|
|
|||
|
// inject updated context back into model
|
|||
|
$fieldgroup->setFieldContextArray($field->id, $data);
|
|||
|
|
|||
|
// if there is something in data, then JSON encode it. If it's empty then make it null.
|
|||
|
$data = count($data) ? wireEncodeJSON($data, true) : null;
|
|||
|
|
|||
|
$query = $database->prepare('UPDATE fieldgroups_fields SET data=:data WHERE fields_id=:field_id AND fieldgroups_id=:fieldgroup_id');
|
|||
|
if(empty($data)) {
|
|||
|
$query->bindValue(':data', null, \PDO::PARAM_NULL);
|
|||
|
} else {
|
|||
|
$query->bindValue(':data', $data, \PDO::PARAM_STR);
|
|||
|
}
|
|||
|
$query->bindValue(':field_id', $field_id, \PDO::PARAM_INT);
|
|||
|
$query->bindValue(':fieldgroup_id', $fieldgroup_id, \PDO::PARAM_INT);
|
|||
|
$result = $query->execute();
|
|||
|
|
|||
|
return $result;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
/**
|
|||
|
* Change a field's type
|
|||
|
*
|
|||
|
* #pw-hooker
|
|||
|
*
|
|||
|
* @param Field $field1 Field with the new type already assigned
|
|||
|
* @param bool $keepSettings Whether or not to keep custom $data settings (default=false)
|
|||
|
* @throws WireException
|
|||
|
* @return bool
|
|||
|
*
|
|||
|
*/
|
|||
|
protected function ___changeFieldtype(Field $field1, $keepSettings = false) {
|
|||
|
|
|||
|
if(!$field1->prevFieldtype) {
|
|||
|
throw new WireException("changeFieldType requires that the given field has had a type change");
|
|||
|
}
|
|||
|
|
|||
|
if( ($field1->type instanceof FieldtypeMulti && !$field1->prevFieldtype instanceof FieldtypeMulti) ||
|
|||
|
($field1->prevFieldtype instanceof FieldtypeMulti && !$field1->type instanceof FieldtypeMulti)) {
|
|||
|
throw new WireException("Cannot convert between single and multiple value field types");
|
|||
|
}
|
|||
|
|
|||
|
$fromType = $field1->prevFieldtype;
|
|||
|
$toType = $field1->type;
|
|||
|
|
|||
|
$this->changeTypeReady($field1, $fromType, $toType);
|
|||
|
|
|||
|
$field2 = clone $field1;
|
|||
|
$flags = $field2->flags;
|
|||
|
if($flags & Field::flagSystem) {
|
|||
|
$field2->flags = $flags | Field::flagSystemOverride;
|
|||
|
$field2->flags = 0; // intentional overwrite after above line
|
|||
|
}
|
|||
|
$field2->name = $field2->name . "_PWTMP";
|
|||
|
$field2->prevFieldtype = $field1->type;
|
|||
|
$field2->type->createField($field2);
|
|||
|
$field1->type = $field1->prevFieldtype;
|
|||
|
|
|||
|
$schema1 = array();
|
|||
|
$schema2 = array();
|
|||
|
|
|||
|
$database = $this->wire()->database;
|
|||
|
$table1 = $database->escapeTable($field1->table);
|
|||
|
$table2 = $database->escapeTable($field2->table);
|
|||
|
|
|||
|
$query = $database->prepare("DESCRIBE `$table1`"); // QA
|
|||
|
$query->execute();
|
|||
|
/** @noinspection PhpAssignmentInConditionInspection */
|
|||
|
while($row = $query->fetch(\PDO::FETCH_ASSOC)) $schema1[] = $row['Field'];
|
|||
|
|
|||
|
$query = $database->prepare("DESCRIBE `$table2`"); // QA
|
|||
|
$query->execute();
|
|||
|
/** @noinspection PhpAssignmentInConditionInspection */
|
|||
|
while($row = $query->fetch(\PDO::FETCH_ASSOC)) $schema2[] = $row['Field'];
|
|||
|
|
|||
|
foreach($schema1 as $key => $value) {
|
|||
|
if(!in_array($value, $schema2)) {
|
|||
|
$this->message("changeFieldType loses table field '$value'", Notice::debug);
|
|||
|
unset($schema1[$key]);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
$sql = "INSERT INTO `$table2` (`" . implode('`,`', $schema1) . "`) " .
|
|||
|
"SELECT `" . implode('`,`', $schema1) . "` FROM `$table1` ";
|
|||
|
|
|||
|
$error = '';
|
|||
|
$exception = null;
|
|||
|
|
|||
|
try {
|
|||
|
$result = $database->exec($sql);
|
|||
|
if($result === false || $query->errorCode() > 0) {
|
|||
|
$errorInfo = $query->errorInfo();
|
|||
|
$error = !empty($errorInfo[2]) ? $errorInfo[2] : 'Unknown Error';
|
|||
|
}
|
|||
|
} catch(\Exception $e) {
|
|||
|
$exception = $e;
|
|||
|
$error = $e->getMessage();
|
|||
|
}
|
|||
|
|
|||
|
if($exception) {
|
|||
|
$this->error("Field type change failed. Database reports: $error");
|
|||
|
$database->exec("DROP TABLE `$table2`"); // QA
|
|||
|
$severe = $this->wire()->process != 'ProcessField';
|
|||
|
$this->trackException($exception, $severe);
|
|||
|
return false;
|
|||
|
}
|
|||
|
|
|||
|
$database->exec("DROP TABLE `$table1`"); // QA
|
|||
|
$database->exec("RENAME TABLE `$table2` TO `$table1`"); // QA
|
|||
|
|
|||
|
$field1->type = $field2->type;
|
|||
|
|
|||
|
if(!$keepSettings) {
|
|||
|
// clear out the custom data, which contains settings specific to the Inputfield and Fieldtype
|
|||
|
foreach($field1->getArray() as $key => $value) {
|
|||
|
// skip fields that may be shared among any fieldtype
|
|||
|
if(in_array($key, array('description', 'required', 'collapsed', 'notes'))) continue;
|
|||
|
// skip over language labels/descriptions
|
|||
|
if(preg_match('/^(description|label|notes)\d+/', $key)) continue;
|
|||
|
// remove the custom field
|
|||
|
$field1->remove($key);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
$this->changedType($field1, $fromType, $toType);
|
|||
|
|
|||
|
return true;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
/**
|
|||
|
* Physically delete all field data (from the database) used by pages of a given template
|
|||
|
*
|
|||
|
* This is for internal API use only. This method is intended only to be called by
|
|||
|
* Fieldtype::deleteTemplateField
|
|||
|
*
|
|||
|
* If you need to remove a field from a Fieldgroup, use Fieldgroup::remove(), and this
|
|||
|
* method will be call automatically at the appropriate time when save the fieldgroup.
|
|||
|
*
|
|||
|
* #pw-hooker
|
|||
|
*
|
|||
|
* @param Field $field
|
|||
|
* @param Template $template
|
|||
|
* @return bool Whether or not it was successful
|
|||
|
* @throws WireException when given a situation where deletion is not allowed
|
|||
|
*
|
|||
|
*/
|
|||
|
public function ___deleteFieldDataByTemplate(Field $field, Template $template) {
|
|||
|
|
|||
|
// first we need to determine if the $field->type module has its own
|
|||
|
// deletePageField method separate from base: Fieldtype/FieldtypeMulti
|
|||
|
$reflector = new \ReflectionClass($field->type->className(true));
|
|||
|
$hasDeletePageField = false;
|
|||
|
|
|||
|
foreach($reflector->getMethods() as $method) {
|
|||
|
$methodName = $method->getName();
|
|||
|
if(strpos($methodName, '___deletePageField') === false) continue;
|
|||
|
try {
|
|||
|
new \ReflectionMethod($reflector->getParentClass()->getName(), $methodName);
|
|||
|
if(!in_array($method->getDeclaringClass()->getName(), array(
|
|||
|
'Fieldtype',
|
|||
|
'FieldtypeMulti',
|
|||
|
__NAMESPACE__ . "\\Fieldtype",
|
|||
|
__NAMESPACE__ . "\\FieldtypeMulti"))) {
|
|||
|
$hasDeletePageField = true;
|
|||
|
}
|
|||
|
|
|||
|
} catch(\Exception $e) {
|
|||
|
// not there
|
|||
|
}
|
|||
|
break;
|
|||
|
}
|
|||
|
|
|||
|
$numPages = $this->getNumPages($field, array('template' => $template));
|
|||
|
$numRows = $this->getNumRows($field, array('template' => $template));
|
|||
|
$success = true;
|
|||
|
|
|||
|
if($numPages <= 200 || $hasDeletePageField) {
|
|||
|
$deleteType = $this->_('page-by-page');
|
|||
|
|
|||
|
// not many pages to operate on, OR fieldtype has a custom deletePageField method,
|
|||
|
// so use verbose/slow method to delete the field from pages
|
|||
|
|
|||
|
$ids = $this->getNumPages($field, array('template' => $template, 'getPageIDs' => true));
|
|||
|
$items = $this->wire()->pages->getById($ids, $template);
|
|||
|
|
|||
|
foreach($items as $page) {
|
|||
|
try {
|
|||
|
$field->type->deletePageField($page, $field);
|
|||
|
// $this->message("Deleted '{$field->name}' from '{$page->path}'", Notice::debug);
|
|||
|
|
|||
|
} catch(\Exception $e) {
|
|||
|
$this->trackException($e, false, true);
|
|||
|
$success = false;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
} else {
|
|||
|
$deleteType = $this->_('single-query');
|
|||
|
|
|||
|
// large number of pages to operate on: use fast method
|
|||
|
|
|||
|
$database = $this->wire()->database;
|
|||
|
$table = $database->escapeTable($field->getTable());
|
|||
|
$sql = "DELETE $table FROM $table " .
|
|||
|
"INNER JOIN pages ON pages.id=$table.pages_id " .
|
|||
|
"WHERE pages.templates_id=:templates_id";
|
|||
|
$query = $database->prepare($sql);
|
|||
|
$query->bindValue(':templates_id', $template->id, \PDO::PARAM_INT);
|
|||
|
try {
|
|||
|
$query->execute();
|
|||
|
} catch(\Exception $e) {
|
|||
|
$this->error($e->getMessage(), Notice::log);
|
|||
|
$this->trackException($e);
|
|||
|
$success = false;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if($success) {
|
|||
|
$this->message(
|
|||
|
sprintf($this->_('Deleted field "%1$s" data in %2$d row(s) from %3$d page(s) using template "%4$s".'),
|
|||
|
$field->name, $numRows, $numPages, $template->name) . " [$deleteType]",
|
|||
|
Notice::log
|
|||
|
);
|
|||
|
} else {
|
|||
|
$this->error(
|
|||
|
sprintf($this->_('Error deleting field "%1$s" data, %2$d row(s), %3$d page(s) using template "%4$s".'),
|
|||
|
$field->name, $numRows, $numPages, $template->name) . " [$deleteType]",
|
|||
|
Notice::log
|
|||
|
);
|
|||
|
}
|
|||
|
|
|||
|
return $success;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Return a count of pages containing populated values for the given field
|
|||
|
*
|
|||
|
* @param Field $field
|
|||
|
* @param array $options Optionally specify one of the following options:
|
|||
|
* - `template` (template|int|string): Specify a Template object, ID or name to isolate returned rows specific to pages using that template.
|
|||
|
* - `page` (Page|int|string): Specify a Page object, ID or path to isolate returned rows specific to that page.
|
|||
|
* - `getPageIDs` (bool): Specify boolean true to make it return an array of matching Page IDs rather than a count.
|
|||
|
* @return int|array Returns array only if getPageIDs option set, otherwise returns count of pages.
|
|||
|
* @throws WireException If given option for page or template doesn't resolve to actual page/template.
|
|||
|
*
|
|||
|
*/
|
|||
|
public function getNumPages(Field $field, array $options = array()) {
|
|||
|
$options['countPages'] = true;
|
|||
|
return $this->getNumRows($field, $options);
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Return a count of database rows populated the given field
|
|||
|
*
|
|||
|
* @param Field $field
|
|||
|
* @param array $options Optionally specify any of the following options:
|
|||
|
* - `template` (Template|int|string): Specify a Template object, ID or name to isolate returned rows specific to pages using that template.
|
|||
|
* - `page` (Page|int|string): Specify a Page object, ID or path to isolate returned rows specific to that page.
|
|||
|
* - `countPages` (bool): Specify boolean true to make it return a page count rather than a row count (default=false).
|
|||
|
* There will only be potential difference between rows and pages counts with multi-value fields.
|
|||
|
* - `getPageIDs` (bool): Specify boolean true to make it return an array of matching Page IDs rather than a count (overrides countPages).
|
|||
|
* @return int|array Returns array only if getPageIDs option set, otherwise returns a count of rows.
|
|||
|
* @throws WireException If given option for page or template doesn't resolve to actual page/template.
|
|||
|
*
|
|||
|
*/
|
|||
|
public function getNumRows(Field $field, array $options = array()) {
|
|||
|
|
|||
|
$defaults = array(
|
|||
|
'template' => 0,
|
|||
|
'page' => 0,
|
|||
|
'countPages' => false,
|
|||
|
'getPageIDs' => false,
|
|||
|
);
|
|||
|
|
|||
|
if(!$field->type) return 0;
|
|||
|
|
|||
|
$options = array_merge($defaults, $options);
|
|||
|
$database = $this->wire()->database;
|
|||
|
$table = $database->escapeTable($field->getTable());
|
|||
|
$useRowCount = false;
|
|||
|
$schema = $field->type->getDatabaseSchema($field);
|
|||
|
|
|||
|
if(empty($schema)) {
|
|||
|
// field has no schema or table (example: FieldtypeConcat)
|
|||
|
if($options['getPageIDs']) return array();
|
|||
|
return 0;
|
|||
|
}
|
|||
|
|
|||
|
if($options['template']) {
|
|||
|
// count by pages using specific template
|
|||
|
|
|||
|
if($options['template'] instanceof Template) {
|
|||
|
$template = $options['template'];
|
|||
|
} else {
|
|||
|
$template = $this->wire()->templates->get($options['template']);
|
|||
|
}
|
|||
|
|
|||
|
if(!$template) throw new WireException("Unknown template: $options[template]");
|
|||
|
|
|||
|
|
|||
|
if($options['getPageIDs']) {
|
|||
|
$sql = "SELECT DISTINCT $table.pages_id FROM $table ".
|
|||
|
"JOIN pages ON pages.templates_id=:templateID AND pages.id=pages_id ";
|
|||
|
|
|||
|
} else if($options['countPages']) {
|
|||
|
$sql = "SELECT COUNT(DISTINCT pages_id) FROM $table ".
|
|||
|
"JOIN pages ON pages.templates_id=:templateID AND pages.id=pages_id ";
|
|||
|
} else {
|
|||
|
$sql = "SELECT COUNT(*) FROM pages " .
|
|||
|
"JOIN $table ON $table.pages_id=pages.id " .
|
|||
|
"WHERE pages.templates_id=:templateID ";
|
|||
|
}
|
|||
|
$query = $database->prepare($sql);
|
|||
|
$query->bindValue(':templateID', $template->id, \PDO::PARAM_INT);
|
|||
|
|
|||
|
} else if($options['page']) {
|
|||
|
// count by specific page
|
|||
|
|
|||
|
if(is_int($options['page'])) {
|
|||
|
$pageID = $options['page'];
|
|||
|
} else {
|
|||
|
$page = $this->wire()->pages->get($options['page']);
|
|||
|
$pageID = $page->id;
|
|||
|
}
|
|||
|
|
|||
|
if(!$pageID) throw new WireException("Unknown page: $options[page]");
|
|||
|
|
|||
|
if($options['countPages']) {
|
|||
|
// is there any the point to this?
|
|||
|
$sql = "SELECT COUNT(DISTINCT pages_id) FROM $table WHERE pages_id=:pageID ";
|
|||
|
} else {
|
|||
|
$sql = "SELECT COUNT(*) FROM $table WHERE pages_id=:pageID ";
|
|||
|
}
|
|||
|
|
|||
|
$query = $database->prepare($sql);
|
|||
|
$query->bindValue(':pageID', $pageID, \PDO::PARAM_INT);
|
|||
|
|
|||
|
} else {
|
|||
|
// overall total count
|
|||
|
|
|||
|
if($options['getPageIDs']) {
|
|||
|
$sql = "SELECT DISTINCT $table.pages_id FROM $table";
|
|||
|
} else if($options['countPages']) {
|
|||
|
$sql = "SELECT COUNT(DISTINCT pages_id) FROM $table";
|
|||
|
} else {
|
|||
|
$sql = "SELECT COUNT(*) FROM $table";
|
|||
|
}
|
|||
|
$query = $database->prepare($sql);
|
|||
|
}
|
|||
|
|
|||
|
$return = $options['getPageIDs'] ? array() : 0;
|
|||
|
|
|||
|
try {
|
|||
|
$query->execute();
|
|||
|
if($options['getPageIDs']) {
|
|||
|
/** @noinspection PhpAssignmentInConditionInspection */
|
|||
|
while($id = $query->fetchColumn()) {
|
|||
|
$return[] = (int) $id;
|
|||
|
}
|
|||
|
} else if($useRowCount) {
|
|||
|
$return = (int) $query->rowCount();
|
|||
|
} else {
|
|||
|
list($return) = $query->fetch(\PDO::FETCH_NUM);
|
|||
|
$return = (int) $return;
|
|||
|
}
|
|||
|
} catch(\Exception $e) {
|
|||
|
$this->error($e->getMessage() . " (getNumRows)");
|
|||
|
$this->trackException($e, false);
|
|||
|
}
|
|||
|
|
|||
|
return $return;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Is the given field name native/permanent to the database?
|
|||
|
*
|
|||
|
* This is deprecated, please us $fields->isNative($name) instead.
|
|||
|
*
|
|||
|
* #pw-internal
|
|||
|
*
|
|||
|
* @param string $name
|
|||
|
* @return bool
|
|||
|
* @deprecated
|
|||
|
*
|
|||
|
*/
|
|||
|
public static function isNativeName($name) {
|
|||
|
$fields = wire()->fields;
|
|||
|
return $fields->isNative($name);
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Is the given field name native/permanent to the database?
|
|||
|
*
|
|||
|
* Such fields are disallowed from being used for custom field names.
|
|||
|
*
|
|||
|
* @param string $name Field name you want to check
|
|||
|
* @return bool True if field is native (and thus should not be used) or false if it's okay to use
|
|||
|
*
|
|||
|
*/
|
|||
|
public function isNative($name) {
|
|||
|
if(isset(self::$nativeNamesSystem[$name])) return true;
|
|||
|
if(isset($this->nativeNamesLocal[$name])) return true;
|
|||
|
return false;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Add a new name to be recognized as a native field name
|
|||
|
*
|
|||
|
* #pw-internal
|
|||
|
*
|
|||
|
* @param string $name
|
|||
|
*
|
|||
|
*/
|
|||
|
public function setNative($name) {
|
|||
|
$this->nativeNamesLocal[$name] = true;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Get list of all tags used by fields
|
|||
|
*
|
|||
|
* - By default it returns an array of tag names where both keys and values are the tag names.
|
|||
|
* - If you specify true for the `$getFields` argument, it returns an array where the keys are
|
|||
|
* tag names and the values are arrays of field names in the tag.
|
|||
|
* - If you specify "reset" for the `$getFields` argument it returns a blank array and resets
|
|||
|
* internal tags cache.
|
|||
|
*
|
|||
|
* #pw-advanced
|
|||
|
*
|
|||
|
* @param bool|string $getFieldNames Specify true to return associative array where keys are tags and values are field names
|
|||
|
* …or specify the string "reset" to force getTags() to reset its cache, forcing it to reload on the next call.
|
|||
|
* @return array Both keys and values are tags in return value
|
|||
|
* @since 3.0.106 + made hookable in 3.0.179
|
|||
|
*
|
|||
|
*/
|
|||
|
public function ___getTags($getFieldNames = false) {
|
|||
|
|
|||
|
if($getFieldNames === 'reset') {
|
|||
|
$this->tagList = null;
|
|||
|
return array();
|
|||
|
}
|
|||
|
|
|||
|
if($this->tagList === null) {
|
|||
|
$tagList = array();
|
|||
|
foreach($this as $field) {
|
|||
|
/** @var Field $field */
|
|||
|
$fieldTags = $field->getTags();
|
|||
|
foreach($fieldTags as $tag) {
|
|||
|
if(!isset($tagList[$tag])) $tagList[$tag] = array();
|
|||
|
$tagList[$tag][] = $field->name;
|
|||
|
}
|
|||
|
}
|
|||
|
ksort($tagList);
|
|||
|
$this->tagList = $tagList;
|
|||
|
}
|
|||
|
|
|||
|
if($getFieldNames) return $this->tagList;
|
|||
|
|
|||
|
$tagList = array();
|
|||
|
foreach($this->tagList as $tag => $fieldNames) {
|
|||
|
$tagList[$tag] = $tag;
|
|||
|
}
|
|||
|
|
|||
|
return $tagList;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Return all fields that have the given $tag
|
|||
|
*
|
|||
|
* Returns an associative array of `['field_name' => 'field_name']` if `$getFieldNames` argument is true,
|
|||
|
* or `['field_name => Field instance]` if not (which is the default).
|
|||
|
*
|
|||
|
* @param string $tag Tag to find fields for
|
|||
|
* @param bool $getFieldNames If true, returns array of field names rather than Field objects (default=false).
|
|||
|
* @return array Array of Field objects, or array of field names if requested. Array keys are always field names.
|
|||
|
* @since 3.0.106
|
|||
|
*
|
|||
|
*/
|
|||
|
public function findByTag($tag, $getFieldNames = false) {
|
|||
|
$tags = $this->getTags(true);
|
|||
|
$items = array();
|
|||
|
if(!isset($tags[$tag])) return $items;
|
|||
|
foreach($tags[$tag] as $fieldName) {
|
|||
|
$items[$fieldName] = ($getFieldNames ? $fieldName : $this->get($fieldName));
|
|||
|
}
|
|||
|
ksort($items);
|
|||
|
return $items;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Find fields by type
|
|||
|
*
|
|||
|
* @param string|Fieldtype $type Fieldtype class name or object
|
|||
|
* @param array $options
|
|||
|
* - `inherit` (bool): Also find types that inherit from given type? (default=true)
|
|||
|
* - `valueType` (string): Value type to return, one of 'field', 'id', or 'name' (default='field')
|
|||
|
* - `indexType` (string): Index type to use, one of 'name', 'id', or '' blank for non-associative array (default='name')
|
|||
|
* @return array|Field[]
|
|||
|
* @since 3.0.194
|
|||
|
*
|
|||
|
*/
|
|||
|
public function findByType($type, array $options = array()) {
|
|||
|
|
|||
|
$defaults = array(
|
|||
|
'inherit' => true, // also find fields using type inherited from given type or interface?
|
|||
|
'valueType' => 'field', // one of 'field', 'id', or 'name'
|
|||
|
'indexType' => 'name', // one of 'name', 'id', or '' blank for non associative array
|
|||
|
);
|
|||
|
|
|||
|
$options = array_merge($defaults, $options);
|
|||
|
$valueType = $options['valueType'];
|
|||
|
$indexType = $options['indexType'];
|
|||
|
$inherit = $options['inherit'];
|
|||
|
$matchTypes = array();
|
|||
|
$matches = array();
|
|||
|
|
|||
|
if($inherit) {
|
|||
|
$typeName = wireClassName($type, true);
|
|||
|
foreach($this->wire()->fieldtypes as $fieldtype) {
|
|||
|
if($fieldtype instanceof $typeName) $matchTypes[$fieldtype->className()] = true;
|
|||
|
}
|
|||
|
} else {
|
|||
|
$typeName = wireClassName($type);
|
|||
|
$matchTypes[$typeName] = true;
|
|||
|
}
|
|||
|
|
|||
|
foreach($this->getWireArray() as $field) {
|
|||
|
/** @var Field $field */
|
|||
|
$fieldtype = $field->type;
|
|||
|
|
|||
|
if(!$fieldtype) continue;
|
|||
|
if(!isset($matchTypes[$fieldtype->className()])) continue;
|
|||
|
|
|||
|
if($valueType === 'field') {
|
|||
|
$value = $field;
|
|||
|
} else if($valueType === 'name') {
|
|||
|
$value = $field->name;
|
|||
|
} else {
|
|||
|
$value = $field->id;
|
|||
|
}
|
|||
|
if($indexType) {
|
|||
|
$index = $field->get($options['indexType']);
|
|||
|
$matches[$index] = $value;
|
|||
|
} else {
|
|||
|
$matches[] = $value;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if($this->useLazy()) {
|
|||
|
foreach(array_keys($this->lazyItems) as $key) {
|
|||
|
if(!isset($this->lazyItems[$key])) continue;
|
|||
|
$row = $this->lazyItems[$key];
|
|||
|
if(empty($row['type'])) continue;
|
|||
|
$type = $row['type'];
|
|||
|
if(!isset($matchTypes[$type])) continue;
|
|||
|
if($valueType === 'field') {
|
|||
|
$value = $this->getLazy((int) $row['id']);
|
|||
|
} else if($valueType === 'name') {
|
|||
|
$value = $row['name'];
|
|||
|
} else {
|
|||
|
$value = $row['id'];
|
|||
|
}
|
|||
|
if($indexType) {
|
|||
|
$index = isset($data[$indexType]) ? $row[$indexType] : $row['id'];
|
|||
|
$matches[$index] = $value;
|
|||
|
} else {
|
|||
|
$matches[] = $value;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
return $matches;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Get all field names
|
|||
|
*
|
|||
|
* @param string $indexType One of 'name', 'id' or blank string for no index (default='')
|
|||
|
* @return array
|
|||
|
* @since 3.0.194
|
|||
|
*
|
|||
|
*/
|
|||
|
public function getAllNames($indexType = '') {
|
|||
|
return $this->getAllValues('name', $indexType);
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Get all flag names or get all flag names for given flags or Field
|
|||
|
*
|
|||
|
* #pw-internal
|
|||
|
*
|
|||
|
* @param int|Field|null $flags Specify flags or Field or omit to get all flag names
|
|||
|
* @param bool $getString Get a string of flag names rather than array? (default=false)
|
|||
|
* @return array|string When array is returned, array is of strings indexed by flag value (int)
|
|||
|
*
|
|||
|
*/
|
|||
|
public function getFlagNames($flags = null, $getString = false) {
|
|||
|
if($flags === null) {
|
|||
|
$a = $this->flagNames;
|
|||
|
} else {
|
|||
|
$a = array();
|
|||
|
if($flags instanceof Field) $flags = $flags->flags;
|
|||
|
foreach($this->flagNames as $flag => $name) {
|
|||
|
if($flags & $flag) $a[$flag] = $name;
|
|||
|
}
|
|||
|
}
|
|||
|
return $getString ? implode(' ', $a) : $a;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Overridden from WireSaveableItems to retain keys with 0 values and remove defaults we don't need saved
|
|||
|
*
|
|||
|
* #pw-internal
|
|||
|
*
|
|||
|
* @param array $value
|
|||
|
* @return string of JSON
|
|||
|
*
|
|||
|
*/
|
|||
|
protected function encodeData(array $value) {
|
|||
|
if(isset($value['collapsed']) && $value['collapsed'] === 0) unset($value['collapsed']);
|
|||
|
if(isset($value['columnWidth']) && (empty($value['columnWidth']) || $value['columnWidth'] == 100)) unset($value['columnWidth']);
|
|||
|
return wireEncodeJSON($value, 0);
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Does user have 'view' or 'edit' permission for this field? (internal use only)
|
|||
|
*
|
|||
|
* PLEASE NOTE: this does not check that the provided $page itself is viewable or editable.
|
|||
|
* If you want that check, then use $page->viewable($field) or $page->editable($field) instead.
|
|||
|
*
|
|||
|
* This provides the back-end to the Field::viewable() and Field::editable() methods.
|
|||
|
* This method is for internal use, please instead use the Field::viewable() or Field::editable() methods.
|
|||
|
*
|
|||
|
* #pw-internal
|
|||
|
*
|
|||
|
* @param Field|int|string Field to check
|
|||
|
* @param string $permission Specify either 'view' or 'edit'
|
|||
|
* @param Page|null $page Optionally specify a page for context
|
|||
|
* @param User|null $user Optionally specify a user for context (default=current user)
|
|||
|
* @return bool
|
|||
|
* @throws WireException if given invalid arguments
|
|||
|
*
|
|||
|
*/
|
|||
|
public function _hasPermission(Field $field, $permission, Page $page = null, User $user = null) {
|
|||
|
if($permission != 'edit' && $permission != 'view') {
|
|||
|
throw new WireException('Specify either "edit" or "view"');
|
|||
|
}
|
|||
|
if(is_null($user)) $user = $this->wire()->user;
|
|||
|
if($user->isSuperuser()) return true;
|
|||
|
if($page && $page->template && $page->template->fieldgroup->hasField($field)) {
|
|||
|
// make sure we have a copy of $field that is in the context of $page
|
|||
|
$_field = $page->template->fieldgroup->getFieldContext($field);
|
|||
|
if($_field) $field = $_field;
|
|||
|
}
|
|||
|
if($field->useRoles) {
|
|||
|
// field is access controlled
|
|||
|
$has = false;
|
|||
|
$roles = $permission == 'edit' ? $field->editRoles : $field->viewRoles;
|
|||
|
if($permission == 'view' && in_array($this->wire()->config->guestUserRolePageID, $roles)) {
|
|||
|
// if guest has view permission, then all have view permission
|
|||
|
$has = true;
|
|||
|
} else {
|
|||
|
foreach($roles as $roleID) {
|
|||
|
if($user->hasRole($roleID)) {
|
|||
|
$has = true;
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
} else {
|
|||
|
// field is not access controlled
|
|||
|
$has = $permission == 'view' ? true : $user->hasPermission("page-edit");
|
|||
|
}
|
|||
|
return $has;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Hook called when a field has changed type
|
|||
|
*
|
|||
|
* #pw-hooker
|
|||
|
*
|
|||
|
* @param Field|Saveable $item
|
|||
|
* @param Fieldtype $fromType
|
|||
|
* @param Fieldtype $toType
|
|||
|
*
|
|||
|
*/
|
|||
|
public function ___changedType(Saveable $item, Fieldtype $fromType, Fieldtype $toType) { }
|
|||
|
|
|||
|
/**
|
|||
|
* Hook called right before a field is about to change type
|
|||
|
*
|
|||
|
* #pw-hooker
|
|||
|
*
|
|||
|
* @param Field|Saveable $item
|
|||
|
* @param Fieldtype $fromType
|
|||
|
* @param Fieldtype $toType
|
|||
|
*
|
|||
|
*/
|
|||
|
public function ___changeTypeReady(Saveable $item, Fieldtype $fromType, Fieldtype $toType) { }
|
|||
|
|
|||
|
/**
|
|||
|
* Get Fieldtypes compatible (for type change) with given Field
|
|||
|
*
|
|||
|
* #pw-internal
|
|||
|
*
|
|||
|
* @param Field $field
|
|||
|
* @return Fieldtypes
|
|||
|
* @since 3.0.140
|
|||
|
*
|
|||
|
*/
|
|||
|
public function getCompatibleFieldtypes(Field $field) {
|
|||
|
$fieldtype = $field->type;
|
|||
|
if($fieldtype) {
|
|||
|
// ask fieldtype what is compatible
|
|||
|
/** @var Fieldtypes $fieldtypes */
|
|||
|
$fieldtypes = $fieldtype->getCompatibleFieldtypes($field);
|
|||
|
if(!$fieldtypes instanceof WireArray) {
|
|||
|
$fieldtypes = $this->wire(new Fieldtypes());
|
|||
|
}
|
|||
|
// ensure original is present
|
|||
|
$fieldtypes->prepend($fieldtype);
|
|||
|
} else {
|
|||
|
// allow all
|
|||
|
$fieldtypes = $this->wire()->fieldtypes;
|
|||
|
}
|
|||
|
return $fieldtypes;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Get FieldsIndexTools instance
|
|||
|
*
|
|||
|
* #pw-internal
|
|||
|
*
|
|||
|
* @return FieldsTableTools
|
|||
|
* @since 3.0.150
|
|||
|
*
|
|||
|
*/
|
|||
|
public function tableTools() {
|
|||
|
if($this->tableTools === null) $this->tableTools = $this->wire(new FieldsTableTools());
|
|||
|
return $this->tableTools;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Return the list of Fieldgroups using given field.
|
|||
|
*
|
|||
|
* #pw-internal
|
|||
|
*
|
|||
|
* @param Field|int|string Field to get fieldgroups for
|
|||
|
* @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($field, $getCount = false) {
|
|||
|
|
|||
|
$fieldId = $this->_fieldId($field);
|
|||
|
$fieldgroups = $this->wire()->fieldgroups;
|
|||
|
/** @var FieldgroupsArray $items */
|
|||
|
$items = $getCount ? null : $this->wire(new FieldgroupsArray());
|
|||
|
$ids = array();
|
|||
|
$count = 0;
|
|||
|
|
|||
|
$sql = "SELECT fieldgroups_id FROM fieldgroups_fields WHERE fields_id=:fields_id";
|
|||
|
$query = $this->wire()->database->prepare($sql);
|
|||
|
$query->bindValue(':fields_id', $fieldId, \PDO::PARAM_INT);
|
|||
|
$query->execute();
|
|||
|
|
|||
|
while($row = $query->fetch(\PDO::FETCH_NUM)) {
|
|||
|
$id = (int) $row[0];
|
|||
|
$ids[$id] = $id;
|
|||
|
}
|
|||
|
|
|||
|
$query->closeCursor();
|
|||
|
|
|||
|
foreach($ids as $id) {
|
|||
|
$fieldgroup = $fieldgroups->get($id);
|
|||
|
if(!$fieldgroup) continue;
|
|||
|
if($items) $items->add($fieldgroup);
|
|||
|
$count++;
|
|||
|
}
|
|||
|
|
|||
|
return $getCount ? $count : $items;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Return the list of of Templates using given field.
|
|||
|
*
|
|||
|
* #pw-internal
|
|||
|
*
|
|||
|
* @param Field|int|string Field to get templates for
|
|||
|
* @param bool $getCount Get count rather than FieldgroupsArray? (default=false)
|
|||
|
* @return TemplatesArray|int WireArray of Template objects or count when requested.
|
|||
|
* @since 3.0.195
|
|||
|
*
|
|||
|
*/
|
|||
|
public function getTemplates($field, $getCount = false) {
|
|||
|
|
|||
|
$fieldId = $this->_fieldId($field);
|
|||
|
$templates = $this->wire()->templates;
|
|||
|
$items = $getCount ? null : $this->wire(new TemplatesArray()); /** @var TemplatesArray $items */
|
|||
|
$count = 0;
|
|||
|
$ids = array();
|
|||
|
|
|||
|
if(!$fieldId) return $items;
|
|||
|
|
|||
|
$sql =
|
|||
|
"SELECT fieldgroups_fields.fieldgroups_id, templates.id AS templates_id " .
|
|||
|
"FROM fieldgroups_fields " .
|
|||
|
"JOIN templates ON templates.fieldgroups_id=fieldgroups_fields.fieldgroups_id " .
|
|||
|
"WHERE fieldgroups_fields.fields_id=:fields_id";
|
|||
|
|
|||
|
$query = $this->wire()->database->prepare($sql);
|
|||
|
$query->bindValue(':fields_id', $fieldId, \PDO::PARAM_INT);
|
|||
|
$query->execute();
|
|||
|
|
|||
|
while($row = $query->fetch(\PDO::FETCH_ASSOC)) {
|
|||
|
$id = (int) $row['templates_id'];
|
|||
|
$ids[$id] = $id;
|
|||
|
}
|
|||
|
|
|||
|
$query->closeCursor();
|
|||
|
|
|||
|
foreach($ids as $id) {
|
|||
|
$template = $templates->get($id);
|
|||
|
if(!$template) continue;
|
|||
|
if($items) $items->add($template);
|
|||
|
$count++;
|
|||
|
}
|
|||
|
|
|||
|
return $getCount ? $count : $items;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Setup a new field using predefined setup name(s) from the Field’s fieldtype
|
|||
|
*
|
|||
|
* If no setupName is provided then this method doesn’t do anything, but hooks to it might.
|
|||
|
*
|
|||
|
* @param Field $field Newly created field
|
|||
|
* @param string $setupName Setup name to apply
|
|||
|
* @return bool True if setup was appled, false if not
|
|||
|
* @since 3.0.213
|
|||
|
*
|
|||
|
*/
|
|||
|
protected function ___applySetupName(Field $field, $setupName = '') {
|
|||
|
|
|||
|
$setups = $field->type->getFieldSetups();
|
|||
|
$setup = isset($setups[$setupName]) ? $setups[$setupName] : null;
|
|||
|
|
|||
|
if(!$setup) return false;
|
|||
|
|
|||
|
$title = isset($setup['title']) ? $setup['title'] : $setupName;
|
|||
|
$func = isset($setup['setup']) ? $setup['setup'] : null;
|
|||
|
|
|||
|
foreach($setup as $property => $value) {
|
|||
|
if($property === 'title' || $property === 'setup') continue;
|
|||
|
$field->set($property, $value);
|
|||
|
}
|
|||
|
|
|||
|
if($func && is_callable($func)) {
|
|||
|
$func($field);
|
|||
|
}
|
|||
|
|
|||
|
$this->message("Applied setup: $title", Notice::debug | Notice::noGroup);
|
|||
|
|
|||
|
return true;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Return field ID for given value (Field, field name, field ID) or 0 if none
|
|||
|
*
|
|||
|
* #pw-internal
|
|||
|
*
|
|||
|
* @param Field|string|int $field
|
|||
|
* @return int
|
|||
|
* @since 3.0.195
|
|||
|
*
|
|||
|
*/
|
|||
|
public function _fieldId($field) {
|
|||
|
if($field instanceof Field) {
|
|||
|
$fieldId = $field->id;
|
|||
|
} else if(ctype_digit("$field")) {
|
|||
|
$fieldId = (int) $field;
|
|||
|
} else {
|
|||
|
$field = $this->get($field);
|
|||
|
$fieldId = $field ? $field->id : 0;
|
|||
|
}
|
|||
|
return $fieldId;
|
|||
|
}
|
|||
|
|
|||
|
}
|