praiadeseselle/wire/core/Fieldgroups.php
2022-03-08 15:55:41 +01:00

490 lines
14 KiB
PHP

<?php namespace ProcessWire;
/**
* ProcessWire Fieldgroups
*
* #pw-summary Maintains collections of Fieldgroup object instances and represents the `$fieldgroups` API variable.
* #pw-body For full details on all methods available in a Fieldgroup, be sure to also see the `WireArray` class.
* #pw-var $fieldgroups
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* https://processwire.com
*
* @method int saveContext(Fieldgroup $fieldgroup)
* @method array getExportData(Fieldgroup $fieldgroup)
* @method array setImportData(Fieldgroup $fieldgroup, array $data)
*
*
*/
class Fieldgroups extends WireSaveableItemsLookup {
/**
* Instances of FieldgroupsArray
*
* @var FieldgroupsArray
*
*/
protected $fieldgroupsArray;
public function init() {
$this->fieldgroupsArray = $this->wire(new FieldgroupsArray());
$this->load($this->fieldgroupsArray);
}
/**
* Get the DatabaseQuerySelect to perform the load operation of items
*
* @param Selectors|string|null $selectors Selectors or a selector string to find, or NULL to load all.
* @return DatabaseQuerySelect
*
*/
protected function getLoadQuery($selectors = null) {
$query = parent::getLoadQuery($selectors);
$lookupTable = $this->wire('database')->escapeTable($this->getLookupTable());
$query->select("$lookupTable.data"); // QA
return $query;
}
/**
* Load all the Fieldgroups from the database
*
* The loading is delegated to WireSaveableItems.
* After loaded, we check for any 'global' fields and add them to the Fieldgroup, if not already there.
*
* @param WireArray $items
* @param Selectors|string|null $selectors Selectors or a selector string to find, or NULL to load all.
* @return WireArray Returns the same type as specified in the getAll() method.
*
*/
protected function ___load(WireArray $items, $selectors = null) {
$items = parent::___load($items, $selectors);
return $items;
}
/**
* Per WireSaveableItems interface, return all available Fieldgroup instances
*
* @return FieldgroupsArray
*
*/
public function getAll() {
return $this->fieldgroupsArray;
}
/**
* Per WireSaveableItems interface, create a blank instance of a Fieldgroup
*
* @return Fieldgroup
*
*/
public function makeBlankItem() {
return $this->wire(new Fieldgroup());
}
/**
* Per WireSaveableItems interface, return the name of the table that Fieldgroup instances are stored in
*
* @return string
*
*/
public function getTable() {
return 'fieldgroups';
}
/**
* Per WireSaveableItemsLookup interface, return the name of the table that Fields are linked to Fieldgroups
*
* @return string
*
*/
public function getLookupTable() {
return 'fieldgroups_fields';
}
/**
* Get the number of templates using the given fieldgroup.
*
* Primarily used to determine if the Fieldgroup is deleteable.
*
* @param Fieldgroup $fieldgroup
* @return int
*
*/
public function getNumTemplates(Fieldgroup $fieldgroup) {
return count($this->getTemplates($fieldgroup));
}
/**
* Given a Fieldgroup, return a TemplatesArray of all templates using the Fieldgroup
*
* @param Fieldgroup $fieldgroup
* @return TemplatesArray
*
*/
public function getTemplates(Fieldgroup $fieldgroup) {
$templates = $this->wire(new TemplatesArray());
foreach($this->wire('templates') as $tpl) {
if($tpl->fieldgroup->id == $fieldgroup->id) $templates->add($tpl);
}
return $templates;
}
/**
* Save the Fieldgroup to DB
*
* If fields were removed from the Fieldgroup, then track them down and remove them from the associated field_* tables
*
* @param Saveable $item Fieldgroup to save
* @return bool True on success, false on failure
* @throws WireException
*
*/
public function ___save(Saveable $item) {
$database = $this->wire('database');
/** @var Fieldgroup $item */
if($item->id && $item->removedFields) {
foreach($this->wire('templates') as $template) {
if($template->fieldgroup->id !== $item->id) continue;
foreach($item->removedFields as $field) {
// make sure the field is valid to delete from this template
$error = $this->isFieldNotRemoveable($field, $item, $template);
if($error !== false) throw new WireException("$error Save of fieldgroup changes aborted.");
if($field->type) $field->type->deleteTemplateField($template, $field);
$item->finishRemove($field);
}
}
$item->resetRemovedFields();
}
$contextData = array();
if($item->id) {
// save context data
$query = $database->prepare("SELECT fields_id, data FROM fieldgroups_fields WHERE fieldgroups_id=:item_id");
$query->bindValue(":item_id", (int) $item->id, \PDO::PARAM_INT);
$query->execute();
/** @noinspection PhpAssignmentInConditionInspection */
while($row = $query->fetch(\PDO::FETCH_ASSOC)) {
$contextData[$row['fields_id']] = $row['data'];
}
$query->closeCursor();
}
$result = parent::___save($item);
if(count($contextData)) {
// restore context data
foreach($contextData as $fields_id => $data) {
$fieldgroups_id = (int) $item->id;
$fields_id = (int) $fields_id;
$query = $database->prepare("UPDATE fieldgroups_fields SET data=:data WHERE fieldgroups_id=:fieldgroups_id AND fields_id=:fields_id"); // QA
$query->bindValue(":data", $data, \PDO::PARAM_STR);
$query->bindValue(":fieldgroups_id", $fieldgroups_id, \PDO::PARAM_INT);
$query->bindValue(":fields_id", $fields_id, \PDO::PARAM_INT);
$query->execute();
}
}
return $result;
}
/**
* Delete the given fieldgroup from the database
*
* Also deletes the references in fieldgroups_fields table
*
* @param Saveable|Fieldgroup $item
* @return bool
* @throws WireException
*
*/
public function ___delete(Saveable $item) {
$templates = array();
foreach($this->wire('templates') as $template) {
if($template->fieldgroup->id == $item->id) $templates[] = $template->name;
}
if(count($templates)) {
throw new WireException(
"Can't delete fieldgroup '{$item->name}' because it is in use by template(s): " .
implode(', ', $templates)
);
}
return parent::___delete($item);
}
/**
* Delete the entries in fieldgroups_fields for the given Field
*
* @param Field $field
* @return bool
*
*/
public function deleteField(Field $field) {
$database = $this->wire('database');
$query = $database->prepare("DELETE FROM fieldgroups_fields WHERE fields_id=:fields_id"); // QA
$query->bindValue(":fields_id", $field->id, \PDO::PARAM_INT);
$result = $query->execute();
return $result;
}
/**
* Create and return a cloned copy of this item
*
* If the new item uses a 'name' field, it will contain a number at the end to make it unique
*
* @param Saveable $item Item to clone
* @param string $name
* @return bool|Saveable $item Returns the new clone on success, or false on failure
* @return Saveable|Fieldgroup
*
*/
public function ___clone(Saveable $item, $name = '') {
return parent::___clone($item, $name);
// @TODO clone the field context data
/*
$id = $item->id;
$item = parent::___clone($item);
if(!$item) return false;
return $item;
*/
}
/**
* Save contexts for all fields in the given fieldgroup
*
* @param Fieldgroup $fieldgroup
* @return int Number of field contexts saved
*
*/
public function ___saveContext(Fieldgroup $fieldgroup) {
$contexts = $fieldgroup->getFieldContextArray();
$numSaved = 0;
foreach($contexts as $fieldID => $context) {
$field = $fieldgroup->getFieldContext((int) $fieldID);
if(!$field) continue;
if($this->wire('fields')->saveFieldgroupContext($field, $fieldgroup)) $numSaved++;
}
return $numSaved;
}
/**
* Export config data for the given fieldgroup
*
* @param Fieldgroup $fieldgroup
* @return array
*
*/
public function ___getExportData(Fieldgroup $fieldgroup) {
$data = $fieldgroup->getTableData();
$fields = array();
$contexts = array();
foreach($fieldgroup as $field) {
$fields[] = $field->name;
$fieldContexts = $fieldgroup->getFieldContextArray();
if(isset($fieldContexts[$field->id])) {
$contexts[$field->name] = $fieldContexts[$field->id];
} else {
$contexts[$field->name] = array();
}
}
$data['fields'] = $fields;
$data['contexts'] = $contexts;
return $data;
}
/**
* Given an export data array, import it back to the class and return what happened
*
* Changes are not committed until the item is saved
*
* @param Fieldgroup $fieldgroup
* @param array $data
* @return array Returns array(
* [property_name] => array(
* 'old' => 'old value', // old value, always a string
* 'new' => 'new value', // new value, always a string
* 'error' => 'error message or blank if no error'
* )
* @throws WireException if given invalid data
*
*/
public function ___setImportData(Fieldgroup $fieldgroup, array $data) {
$return = array(
'fields' => array(
'old' => '',
'new' => '',
'error' => array()
),
'contexts' => array(
'old' => '',
'new' => '',
'error' => array()
),
);
$fieldgroup->setTrackChanges(true);
$fieldgroup->errors("clear");
$_data = $this->getExportData($fieldgroup);
$rmFields = array();
if(isset($data['fields'])) {
// field data
$old = "\n" . implode("\n", $_data['fields']) . "\n";
$new = "\n" . implode("\n", $data['fields']) . "\n";
if($old !== $new) {
$return['fields']['old'] = $old;
$return['fields']['new'] = $new;
// figure out which fields should be removed
foreach($fieldgroup as $field) {
$fieldNames[$field->name] = $field->name;
if(!in_array($field->name, $data['fields'])) {
$fieldgroup->remove($field);
$label = "-$field->name";
$return['fields']['new'] .= $label . "\n";;
$rmFields[] = $field->name;
}
}
// figure out which fields should be added
foreach($data['fields'] as $name) {
$field = $this->wire('fields')->get($name);
if(in_array($name, $rmFields)) continue;
if(!$field) {
$error = sprintf($this->_('Unable to find field: %s'), $name);
$return['fields']['error'][] = $error;
$label = str_replace("\n$name\n", "\n?$name\n", $return['fields']['new']);
$return['fields']['new'] = $label;
continue;
}
if(!$fieldgroup->hasField($field)) {
$label = str_replace("\n$field->name\n", "\n+$field->name\n", $return['fields']['new']);
$return['fields']['new'] = $label;
$fieldgroup->add($field);
} else {
$field = $fieldgroup->getField($field->name, true); // in context
$fieldgroup->add($field);
$label = str_replace("\n$field->name\n", "\n$field->name\n", $return['fields']['new']);
$return['fields']['new'] = $label;
}
}
}
$return['fields']['new'] = trim($return['fields']['new']);
$return['fields']['old'] = trim($return['fields']['old']);
}
if(isset($data['contexts'])) {
// context data
foreach($data['contexts'] as $key => $value) {
// remove items where they are both empty
if(empty($value) && empty($_data['contexts'][$key])) {
unset($data['contexts'][$key], $_data['contexts'][$key]);
}
}
foreach($_data['contexts'] as $key => $value) {
// remove items where they are both empty
if(empty($value) && empty($data['contexts'][$key])) {
unset($data['contexts'][$key], $_data['contexts'][$key]);
}
}
$old = wireEncodeJSON($_data['contexts'], true, true);
$new = wireEncodeJSON($data['contexts'], true, true);
if($old !== $new) {
$return['contexts']['old'] = trim($old);
$return['contexts']['new'] = trim($new);
foreach($data['contexts'] as $name => $context) {
$field = $fieldgroup->getField($name, true); // in context
if(!$field) {
if(!empty($context)) $return['contexts']['error'][] = sprintf($this->_('Unable to find field to set field context: %s'), $name);
continue;
}
$id = $field->id;
$fieldContexts = $fieldgroup->getFieldContextArray();
if(isset($fieldContexts[$id]) || !empty($context)) {
$fieldgroup->setFieldContextArray($id, $context);
$fieldgroup->trackChange('fieldContexts');
}
}
}
}
// other data
foreach($data as $key => $value) {
if($key == 'fields' || $key == 'contexts') continue;
$old = isset($_data[$key]) ? $_data[$key] : null;
if(is_array($old)) $old = wireEncodeJSON($old, true, false);
$new = is_array($value) ? wireEncodeJSON($value, true, false) : $value;
if($old == $new) continue;
$fieldgroup->set($key, $value);
$error = (string) $fieldgroup->errors("first clear");
$return[$key] = array(
'old' => $old,
'new' => $value,
'error' => $error,
);
}
if(count($rmFields)) {
$return['fields']['error'][] = sprintf($this->_('Warning, all data in these field(s) will be permanently deleted (please confirm): %s'), implode(', ', $rmFields));
}
$fieldgroup->errors('clear');
return $return;
}
/**
* Is the given Field not allowed to be removed from given Template?
*
* #pw-internal
*
* @param Field $field
* @param Template $template
* @param Fieldgroup $fieldgroup
* @return bool|string Returns error message string if not removeable or boolean false if it is removeable
*
*/
public function isFieldNotRemoveable(Field $field, Fieldgroup $fieldgroup, Template $template = null) {
if(is_null($template)) $template = $this->wire('templates')->get($fieldgroup->name);
if(($field->flags & Field::flagGlobal) && (!$template || !$template->noGlobal)) {
if($template && $template->getConnectedField()) {
// if template has a 1-1 relationship with a field, noGlobal is not enforced
return false;
} else {
return
"Field '$field' may not be removed from fieldgroup '$fieldgroup->name' " .
"because it is globally required (Field::flagGlobal).";
}
}
if($field->flags & Field::flagPermanent) {
return
"Field '$field' may not be removed from fieldgroup '$fieldgroup->name' " .
"because it is permanent (Field::flagPermanent).";
}
return false;
}
}