getWireArray(); } /** * 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() { if($this->useLazy()) $this->loadAllLazyItems(); return $this->getWireArray(); } /** * @return WireArray|FieldgroupsArray * */ public function getWireArray() { if($this->fieldgroupsArray === null) { $this->fieldgroupsArray = new FieldgroupsArray(); $this->wire($this->fieldgroupsArray); $this->load($this->fieldgroupsArray); } 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) { $templates = $this->wire()->templates; $num = 0; foreach($templates->getAllValues('fieldgroups_id', 'id') as /* $templateId => */ $fieldgroupId) { if($fieldgroupId == $fieldgroup->id) $num++; } return $num; } /** * 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()->templates; $items = $this->wire(new TemplatesArray()); /** @var TemplatesArray $items */ foreach($templates->getAllValues('fieldgroups_id', 'id') as $templateId => $fieldgroupId) { if($fieldgroupId == $fieldgroup->id) { $template = $templates->get($templateId); $items->add($template); } } return $items; } /** * Get all field names used by given fieldgroup * * Use this when you want to identify the field names (or IDs) without loading the fieldgroup or fields in it. * * @param string|int|Fieldgroup $fieldgroup Fieldgroup name, ID or object * @return array Returned array of field names indexed by field ID * @since 3.0.194 * */ public function getFieldNames($fieldgroup) { $fieldNames = array(); $useLazy = $this->useLazy(); if(!$useLazy && !is_object($fieldgroup)) $fieldgroup = $this->get($fieldgroup); if($fieldgroup instanceof Fieldgroup) { foreach($fieldgroup as $field) { /** @var Field $field */ $fieldNames[$field->id] = $field->name; } return $fieldNames; } $fieldIds = array(); if(ctype_digit("$fieldgroup") && $useLazy) { foreach(array_keys($this->lazyItems) as $key) { $row = &$this->lazyItems[$key]; if("$row[id]" === "$fieldgroup" && $row['fields_id']) { $fieldIds[] = (int) $row['fields_id']; } } } else if($fieldgroup) { foreach(array_keys($this->lazyItems) as $key) { $row = &$this->lazyItems[$key]; if("$row[name]" === "$fieldgroup" && $row['fields_id']) { $fieldIds[] = (int) $row['fields_id']; } } } if(count($fieldIds)) { $fieldNames = $this->wire()->fields->getAllValues('name', 'id', 'id', $fieldIds); } return $fieldNames; } /** * 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 $fieldgroup */ $fieldgroup = $item; $datas = array(); $fieldsAdded = array(); $fieldsRemoved = array(); if($fieldgroup->id && $fieldgroup->removedFields) { foreach($this->wire()->templates as $template) { if($template->fieldgroup->id !== $fieldgroup->id) continue; foreach($fieldgroup->removedFields as $field) { /** @var Field $field */ // make sure the field is valid to delete from this template $error = $this->isFieldNotRemoveable($field, $fieldgroup, $template); if($error !== false) throw new WireException("$error Save of fieldgroup changes aborted."); /** @var Fieldtype $fieldtype */ $fieldtype = $field->type; if($fieldtype) $fieldtype->deleteTemplateField($template, $field); $fieldgroup->finishRemove($field); $fieldsRemoved[] = $field; } } $fieldgroup->resetRemovedFields(); } if($fieldgroup->id) { // load context data to populate back after fieldgroup save $sql = 'SELECT fields_id, data FROM fieldgroups_fields WHERE fieldgroups_id=:fieldgroups_id'; $query = $database->prepare($sql); $query->bindValue(':fieldgroups_id', (int) $fieldgroup->id, \PDO::PARAM_INT); $query->execute(); /** @noinspection PhpAssignmentInConditionInspection */ while($row = $query->fetch(\PDO::FETCH_ASSOC)) { $fields_id = (int) $row['fields_id']; $datas[$fields_id] = $row['data']; } $query->closeCursor(); } $result = parent::___save($fieldgroup); // identify any fields added foreach($fieldgroup as $field) { if(!array_key_exists($field->id, $datas)) { $fieldsAdded[] = $field; } } if(count($datas)) { // restore context data $fieldgroups_id = (int) $fieldgroup->id; foreach($datas as $fields_id => $data) { $sql = "UPDATE fieldgroups_fields SET data=:data WHERE fieldgroups_id=:fieldgroups_id AND fields_id=:fields_id"; $query = $database->prepare($sql); if($data === null) { $query->bindValue(":data", null, \PDO::PARAM_NULL); } else { $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(); } } // trigger any fields added foreach($fieldsAdded as $field) { $this->fieldAdded($fieldgroup, $field); } // trigger any fieldsl removed foreach($fieldsRemoved as $field) { $this->fieldRemoved($fieldgroup, $field); } 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) { /** @var Template $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 Fieldgroup|false $item Returns the new clone on success, or false on failure * */ public function ___clone(Saveable $item, $name = '') { if(!$item instanceof Fieldgroup) return false; $database = $this->wire()->database; /** @var Fieldgroup|false $fieldgroup */ $fieldgroup = parent::___clone($item, $name); if(!$fieldgroup) return false; $sql = 'SELECT fields_id, sort, data FROM fieldgroups_fields ' . 'WHERE fieldgroups_id=:fieldgroups_id ' . 'AND data IS NOT NULL'; $query = $this->wire()->database->prepare($sql); $query->bindValue(':fieldgroups_id', $item->id, \PDO::PARAM_INT); $query->execute(); $rows = $query->fetchAll(\PDO::FETCH_ASSOC); $query->closeCursor(); $sql = 'UPDATE fieldgroups_fields SET data=:data ' . 'WHERE fieldgroups_id=:fieldgroups_id ' . 'AND fields_id=:fields_id AND sort=:sort'; $query = $database->prepare($sql); foreach($rows as $row) { $query->bindValue(':data', $row['data']); $query->bindValue(':fieldgroups_id', (int) $fieldgroup->id, \PDO::PARAM_INT); $query->bindValue(':fields_id', (int) $row['fields_id'], \PDO::PARAM_INT); $query->bindValue(':sort', (int) $row['sort'], \PDO::PARAM_INT); $query->execute(); } return $fieldgroup; } /** * 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) { /** @var Field $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) { /** @var Field $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; } /** * Hook called when field has been added to fieldgroup * * #pw-hooker * * @param Fieldgroup $fieldgroup * @param Field $field * @since 3.0.193 * */ public function ___fieldAdded(Fieldgroup $fieldgroup, Field $field) { } /** * Hook called when field has been removed from fieldgroup * * #pw-hooker * * @param Fieldgroup $fieldgroup * @param Field $field * @since 3.0.193 * */ public function ___fieldRemoved(Fieldgroup $fieldgroup, Field $field) { } }