Rev 22 | Blame | Compare with Previous | Last modification | View Log | Download
<?php namespace ProcessWire;require_once(dirname(__FILE__) . '/FieldtypeRepeater.module');/*** ProcessWire Fieldset (Page)** Maintains a collection of fields as a fieldset via an independent Page.** ProcessWire 3.x, Copyright 2023 by Ryan Cramer* https://processwire.com** @property int $repeatersRootPageID**/class FieldtypeFieldsetPage extends FieldtypeRepeater implements ConfigurableModule {public static function getModuleInfo() {return array('title' => __('Fieldset (Page)', __FILE__), // Module Title'summary' => __('Fieldset with fields isolated to separate namespace (page), enabling re-use of fields.', __FILE__), // Module Summary'version' => 1,'autoload' => true,'requires' => 'FieldtypeRepeater');}/*** Construct**/public function __construct() {parent::__construct();require_once(dirname(__FILE__) . '/FieldsetPage.php');}/*** Get the FieldsetPage object for the given $page and $field, or NullPage if field is not on page** @param Page $page* @param Field $field* @param bool $createIfNotExists* @return FieldsetPage|NullPage**/public function getFieldsetPage(Page $page, Field $field, $createIfNotExists = true) {/** @var FieldsetPage|NullPage $readyPage */if(!$page->hasField($field)) return new NullPage();$parent = $this->getRepeaterPageParent($page, $field);if($page->id) {$name = self::repeaterPageNamePrefix . $page->id;$readyPage = $parent->child("name=$name, include=all");} else {$name = '';$readyPage = new NullPage();}if(!$readyPage->id) {$class = $this->getPageClass();$readyPage = $this->wire(new $class());$readyPage->template = $this->getRepeaterTemplate($field);if($parent->id) $readyPage->parent = $parent;$readyPage->addStatus(Page::statusOn);if($name) $readyPage->name = $name;if($name && $createIfNotExists) {$readyPage->save();$readyPage->setQuietly('_repeater_new', 1);$this->readyPageSaved($readyPage, $page, $field);} else {$readyPage->setQuietly('_repeater_new', 1);}}if($readyPage instanceof FieldsetPage) {$readyPage->setForPage($page);$readyPage->setForField($field);$readyPage->setTrackChanges(true);}return $readyPage;}/*** Convert a repeater Page to a PageArray for use with methods/classes expecting RepeaterPageArray** @param Page $page* @param Field $field* @param RepeaterPage|FieldsetPage $value Optionally add this item to it* @return RepeaterPageArray|PageArray**/public function getRepeaterPageArray(Page $page, Field $field, $value = null) {if($value instanceof PageArray) return $value;$pageArray = parent::getBlankValue($page, $field);if($value instanceof Page) $pageArray->add($value);$pageArray->resetTrackChanges();return $pageArray;}/*** Get the class used for repeater Page objects** @return string**/public function getPageClass() {return __NAMESPACE__ . "\\FieldsetPage";}/*** Get a blank value of this type, i.e. return a blank PageArray** @param Page $page* @param Field $field* @return FieldsetPage**/public function getBlankValue(Page $page, Field $field) {return $this->getFieldsetPage($page, $field, false);}/*** Return an InputfieldRepeater, ready to be used** @param Page $page Page being edited* @param Field $field Field that needs an Inputfield* @return Inputfield**/public function getInputfield(Page $page, Field $field) {$hasField = $page->hasField($field->name);/** @var InputfieldRepeater $inputfield */$inputfield = $this->wire()->modules->get($this->getInputfieldClass());$inputfield->set('page', $page);$inputfield->set('field', $field);$inputfield->set('repeaterDepth', 0);$inputfield->set('repeaterReadyItems', 0); // ready items deprecatedif($hasField) {$inputfield->set('repeaterMaxItems', 1);$inputfield->set('repeaterMinItems', 1);$inputfield->set('singleMode', true);}if($page->id && $hasField) {$item = $page->get($field->name);if(!$item->id) $item = null;} else {$item = null;}$value = $this->getRepeaterPageArray($page, $field, $item);$inputfield->val($value);return $inputfield;}/*** Returns a page ready for use as a repeater** @param Field $field* @param Page $page The page that the repeater field lives on* @return Page**/public function getBlankRepeaterPage(Page $page, Field $field) {return $this->getFieldsetPage($page, $field, true);}/*** Get next page ready to be used as a new repeater item, creating it if it doesn't already exist** @param Page $page* @param Field $field* @param PageArray|Page $value* @param array $notIDs Optional Page IDs that should be excluded from the next ready page* @return Page**/public function getNextReadyPage(Page $page, Field $field, $value = null, array $notIDs = array()) {return $this->getFieldsetPage($page, $field, true);}/*** Given a raw value (value as stored in DB), return the value as it would appear in a Page object** Something to note is that this wakeup function is different than most in that the $value it is given* is just an array like array('data' => 123, 'parent_id' => 456) -- it doesn't actually contain any of the* repeater page data other than saying how many there are and the parent where they are stored. So this* wakeup function can technically do its job without even having the $value, unlike most other fieldtypes.** @param Page $page* @param Field $field* @param array|Page $value* @return FieldsetPage|Page $value**/public function ___wakeupValue(Page $page, Field $field, $value) {if($value instanceof Page) return $value;return $this->getFieldsetPage($page, $field, true);}/*** Return the database schema in predefined format** @param Field $field* @return array**/public function getDatabaseSchema(Field $field) {$schema = parent::getDatabaseSchema($field);unset($schema['parent_id'],$schema['count'],$schema['keys']['parent_id'],$schema['keys']['count'],$schema['keys']['data_exact']);$schema['data'] = 'int UNSIGNED NOT NULL';$schema['keys']['data'] = 'KEY `data` (`data`)';return $schema;}/*** Update a DatabaseQuerySelect object to match a Page** @param DatabaseQuerySelect $query* @param string $table* @param string $subfield* @param string $operator* @param string $value* @return DatabaseQuerySelect* @throws WireException**/public function getMatchQuery($query, $table, $subfield, $operator, $value) {$field = $query->field;if(in_array($subfield, array('count', 'parent', 'parent_id'))) {throw new WireException("The '$subfield' subfield option is not available for field '$field'");}if($subfield == 'data' || $subfield == 'id' || !$subfield) {if(!in_array($operator, array('=', '!=', '<', '>', '<=', '>='))) {throw new WireException("Operator $operator not supported for $field." . ($subfield ? $subfield : 'data'));}$value = (int) "$value";$query->where("($table.data{$operator}$value)");} else {$f = $this->wire()->fields->get($subfield);if(!$f) return $query; // unknown subfield// match fields from the repeater template, perform a separate find() operation for the subfield$templateID = $field->get('template_id');$value = $this->wire()->sanitizer->selectorValue($value);$ids = $this->wire()->pages->findIDs("templates_id=$templateID, include=all, $f->name$operator$value");// use the IDs found from the separate find() as our getMatchQueryif(count($ids)) {$query->where("$table.data IN(" . implode(',', $ids) . ")");} else {$query->where("1>2"); // force a non-match}}return $query;}/*** Given an 'awake' value, as set by wakeupValue, convert the value back to a basic type for storage in DB.** In this case, the sleepValue doesn't represent the actual value as they are stored in pages.** @param Page $page* @param Field $field* @param string|int|array|object $value* @return int**/public function ___sleepValue(Page $page, Field $field, $value) {if($value instanceof PageArray) $value = $value->first();$sleepValue = $value instanceof Page ? (int) $value->id : 0;/*$sleepValue = array('data' => "$id",'count' => $id > 0 ? 1 : 0,'parent_id' => $id);*/return $sleepValue;}/*** Export repeater value** @param Page $page* @param Field $field* @param RepeaterPageArray$value* @param array $options* @return array**/public function ___exportValue(Page $page, Field $field, $value, array $options = array()) {$value = $this->getRepeaterPageArray($page, $field, $value); // export as PageArrayreturn parent::___exportValue($page, $field, $value, $options);}/*** Return the parent used by the repeater pages for the given Page and Field** Unlike regular repeaters, all page items for a given field share the same parent, regardless of owning page.** i.e. /processwire/repeaters/for-field-123/** @param Page $page* @param Field $field* @param bool $create* @return Page**/public function getRepeaterPageParent(Page $page, Field $field, $create = true) {return $this->getRepeaterParent($field);}/*** Handles the sanitization and convertion to RepeaterPage value** @param Page $page* @param Field $field* @param mixed $value* @return FieldsetPage|Page**/public function sanitizeValue(Page $page, Field $field, $value) {if(is_string($value) || is_array($value)) {$value = parent::sanitizeValue($page, $field, $value);}if($value instanceof PageArray) {if($value->count()) {$value = $value->first();} else {$value = $this->getFieldsetPage($page, $field);}}if(!$value instanceof Page) {$value = $this->getFieldsetPage($page, $field);}return $value;}/*** Perform output formatting on the value delivered to the API** This method is only used when $page->outputFormatting is true.** @param Page $page* @param Field $field* @param Page|PageArray $value* @return Page**/public function ___formatValue(Page $page, Field $field, $value) {return $value;}/*** Load the given page field from the database table and return the value.** - Return NULL if the value is not available.* - Return the value as it exists in the database, without further processing.* - This is intended only to be called by Page objects on an as-needed basis.* - Typically this is only called for fields that don't have 'autojoin' turned on.* - Any actual conversion of the value should be handled by the `Fieldtype::wakeupValue()` method.** #pw-group-loading** @param Page $page Page object to save.* @param Field $field Field to retrieve from the page.* @return FieldsetPage**/public function ___loadPageField(Page $page, Field $field) {return $this->getFieldsetPage($page, $field, true);}/*** Save the given field from page** @param Page $page Page object to save.* @param Field $field Field to retrieve from the page.* @return bool True on success, false on DB save failure.**/public function ___savePageField(Page $page, Field $field) {$pages = $this->wire()->pages;if(!$page->id || !$field->id) return false;if($page->get('_cloning') instanceof Page) {// $page is being cloned, so lets also clone the FieldsetPage$source = $page->get('_cloning')->get($field->name);if(!$source->id) return false;$target = $pages->clone($source, null, false, array('uncacheAll' => false,'set' => array('name' => self::repeaterPageNamePrefix . $page->id)));if(!$target->id) return false;$page->set($field->name, $target);}$value = $page->get($field->name);if(!$value instanceof Page) return false;// make the FieldsetPage mirror unpublished/hidden state from its ownerif($page->isUnpublished()) {if(!$value->isUnpublished()) $value->addStatus(Page::statusUnpublished);} else {if($value->isUnpublished()) $value->removeStatus(Page::statusUnpublished);}if($page->isHidden()) {if(!$value->isHidden()) $value->addStatus(Page::statusHidden);} else {if($value->isHidden()) $value->removeStatus(Page::statusHidden);}$pages->save($value, array('uncacheAll' => false));$database = $this->wire()->database;$table = $database->escapeTable($field->table);$sql ="INSERT INTO `$table` (pages_id, data) VALUES(:pages_id, :data) " ."ON DUPLICATE KEY UPDATE `data`=VALUES(`data`)";$query = $database->prepare($sql);$query->bindValue(':pages_id', $page->id, \PDO::PARAM_INT);$query->bindValue(':data', $value->id, \PDO::PARAM_INT);$result = $query->execute();return $result;}/*** Return configuration fields definable for each FieldtypePage** @param Field|RepeaterField $field* @return InputfieldWrapper**/public function ___getConfigInputfields(Field $field) {$inputfields = parent::___getConfigInputfields($field);/** @var InputfieldAsmSelect $f */$f = $inputfields->getChildByName('repeaterFields');$f->label = $this->_('Fields in fieldset');$f->description = $this->_('Define the fields that are used by this fieldset. You may also drag and drop fields to the desired order.');$f->notes = '';$field->repeaterLoading = FieldtypeRepeater::loadingOff;$field->repeaterMaxItems = 1;$field->repeaterMinItems = 1;include_once(dirname(__FILE__) . '/FieldsetPageInstructions.php');$inputfields->add(FieldsetPageInstructions($field));return $inputfields;}/*** Populate the settings for a newly created repeater template** @param Template $template**/protected function populateRepeaterTemplateSettings(Template $template) {parent::populateRepeaterTemplateSettings($template);$template->pageLabelField = 'for_page_path';}}