From a5c70a4e7d14caac7e9e444be85590da8710828e Mon Sep 17 00:00:00 2001 From: Ryan Cramer Date: Fri, 4 Feb 2022 14:51:11 -0500 Subject: [PATCH] Upgrade core to have lazy-loading option for Fields, Templates, Fieldgroups. Collaboration with @thetuningspoon for boot performance improvement on installations with large quantities of fields/templates/fieldgroups. Lazy loading option is enabled by default but can be disabled by setting $config->useLazyLoading=false; in your /site/config.php file. Co-authored-by: thetuningspoon --- wire/config.php | 24 +- wire/core/Config.php | 1 + wire/core/Field.php | 230 +++++----- wire/core/Fieldgroup.php | 25 +- wire/core/Fieldgroups.php | 87 +++- wire/core/Fields.php | 216 ++++++++-- wire/core/Fieldtype.php | 13 + wire/core/ProcessWire.php | 3 + wire/core/Template.php | 2 + wire/core/Templates.php | 81 +++- wire/core/WireSaveableItems.php | 400 ++++++++++++++++-- wire/core/WireSaveableItemsLookup.php | 115 +++-- .../FieldtypeRepeater.module | 152 ++++--- .../LanguageSupport/LanguageSupport.module | 62 +-- .../LanguageSupportFields.module | 100 +++-- wire/templates-admin/debug.inc | 44 +- 16 files changed, 1229 insertions(+), 326 deletions(-) diff --git a/wire/config.php b/wire/config.php index 411c1ece3..8248dcbd5 100644 --- a/wire/config.php +++ b/wire/config.php @@ -12,7 +12,7 @@ * You may also make up your own configuration options by assigning them * in /site/config.php * - * ProcessWire 3.x, Copyright 2016 by Ryan Cramer + * ProcessWire 3.x, Copyright 2022 by Ryan Cramer * https://processwire.com * * @@ -76,12 +76,15 @@ /** * Tools, and their order, to show in debug mode (admin) * - * Options include: pages, api, session, modules, hooks, database, db, timers, user, input, cache, autoload + * Options include: pages, api, session, modules, hooks, database, db, timers, user, input, cache, autoload, lazyload + * + * Note: when used, lazyload option should be placed first beause its results can be affected by later debug tools. * * @var array * */ $config->debugTools = array( + //'lazyload', 'pages', 'api', 'session', @@ -194,6 +197,23 @@ */ $config->usePageClasses = false; +/** + * Use lazy loading of fields (plus templates and fieldgroups) for faster boot times? + * + * This delays loading of fields, templates and fieldgroups until they are requested. + * This can improve performance on systems with hundreds of fields or templates, as + * individual fields, templates/fieldgroups won't get constructed until they are needed. + * + * Specify `true` to use lazy loading for all types, `false` to disable all lazy loading, + * or specify array with one or more of the following for lazy loading only certain types: + * `[ 'fields', 'templates', 'fieldgroups' ]` + * + * @var bool|array + * @since 3.0.194 + * + */ +$config->useLazyLoading = true; + /** * Disable all HTTPS requirements? * diff --git a/wire/core/Config.php b/wire/core/Config.php index 78051c92c..5e42d4b7f 100644 --- a/wire/core/Config.php +++ b/wire/core/Config.php @@ -148,6 +148,7 @@ * @property bool $usePoweredBy Use the x-powered-by header? Set to false to disable. #pw-group-system * @property bool $useFunctionsAPI Allow most API variables to be accessed as functions? (see /wire/core/FunctionsAPI.php) #pw-group-system * @property bool $useMarkupRegions Enable support for front-end markup regions? #pw-group-system + * @property bool|array $useLazyLoading Delay loading of fields (and templates/fieldgroups) till requested? Can improve performance on systems with lots of fields or templates. #pw-group-system @since 3.0.193 * @property bool $usePageClasses Use custom Page classes in `/site/classes/[TemplateName]Page.php`? #pw-group-system @since 3.0.152 * @property int $lazyPageChunkSize Chunk size for for $pages->findMany() calls. #pw-group-system * diff --git a/wire/core/Field.php b/wire/core/Field.php index 913f6e5c2..5df222528 100644 --- a/wire/core/Field.php +++ b/wire/core/Field.php @@ -12,7 +12,7 @@ * #pw-body Field objects are managed by the `$fields` API variable. * #pw-use-constants * - * ProcessWire 3.x, Copyright 2019 by Ryan Cramer + * ProcessWire 3.x, Copyright 2022 by Ryan Cramer * https://processwire.com * * @property int $id Numeric ID of field in the database #pw-group-properties @@ -256,7 +256,6 @@ class Field extends WireData implements Saveable, Exportable { */ static protected $lowercaseTables = null; - /** * Set a native setting or a dynamic data property for this Field * @@ -270,40 +269,27 @@ class Field extends WireData implements Saveable, Exportable { * */ public function set($key, $value) { - - if($key == 'name') { - return $this->setName($value); - } else if($key == 'type' && $value) { - return $this->setFieldtype($value); - } else if($key == 'prevTable') { - $this->prevTable = $value; - return $this; - } else if($key == 'prevName') { - $this->prevName = $value; - return $this; - } else if($key == 'prevFieldtype') { - $this->prevFieldtype = $value; - return $this; - } else if($key == 'flags') { - $this->setFlags($value); - return $this; - } else if($key == 'flagsAdd') { - return $this->addFlag($value); - } else if($key == 'flagsDel') { - return $this->removeFlag($value); - } else if($key == 'id') { - $value = (int) $value; + + switch($key) { + case 'id': $this->settings['id'] = (int) $value; return $this; + case 'name': return $this->setName($value); + case 'data': return empty($value) ? $this : parent::set($key, $value); + case 'type': return ($value ? $this->setFieldtype($value) : $this); + case 'label': $this->settings['label'] = $value; return $this; + case 'prevTable': $this->prevTable = $value; return $this; + case 'prevName': $this->prevName = $value; return $this; + case 'prevFieldtype': $this->prevFieldtype = $value; return $this; + case 'flags': $this->setFlags($value); return $this; + case 'flagsAdd': return $this->addFlag($value); + case 'flagsDel': return $this->removeFlag($value); + case 'icon': $this->setIcon($value); return $this; + case 'editRoles': $this->setRoles('edit', $value); return $this; + case 'viewRoles': $this->setRoles('view', $value); return $this; } - + if(isset($this->settings[$key])) { $this->settings[$key] = $value; - } else if($key == 'icon') { - $this->setIcon($value); - } else if($key == 'editRoles') { - $this->setRoles('edit', $value); - } else if($key == 'viewRoles') { - $this->setRoles('view', $value); - } else if($key == 'useRoles') { + } else if($key === 'useRoles') { $flags = $this->flags; if($value) { $flags = $flags | self::flagAccess; // add flag @@ -318,6 +304,26 @@ public function set($key, $value) { return $this; } + /** + * Set raw setting or other value with no validation/processing + * + * This is for use when a field is loading and needs no validation. + * + * #pw-internal + * + * @param string $key + * @param mixed $value + * @since 3.0.194 + * + */ + public function setRawSetting($key, $value) { + if($key === 'data') { + if(!empty($value)) parent::set($key, $value); + } else { + $this->settings[$key] = $value; + } + } + /** * Set the bitmask of flags for the field * @@ -391,29 +397,41 @@ public function hasFlag($flag) { * */ public function get($key) { - - if($key === 'type' && isset($this->settings['type'])) { - $value = $this->settings['type']; - if($value) $value->setLastAccessField($this); - return $value; + + if($key === 'type') { + if(!empty($this->settings['type'])) { + $value = $this->settings['type']; + if($value) $value->setLastAccessField($this); + return $value; + } + return null; + } + + switch($key) { + case 'id': + case 'name': + case 'type': + case 'flags': + case 'label': return $this->settings[$key]; + case 'table': return $this->getTable(); + case 'flagsStr': return $this->wire()->fields->getFlagNames($this->settings['flags'], true); + case 'viewRoles': + case 'editRoles': return $this->$key; + case 'useRoles': return ($this->settings['flags'] & self::flagAccess) ? true : false; + case 'prevTable': + case 'prevName': + case 'prevFieldtype': return $this->$key; + case 'icon': return $this->getIcon(true); + case 'tags': return $this->getTags(true); + case 'tagList': return $this->getTags(); } - if($key == 'viewRoles') return $this->viewRoles; - else if($key == 'editRoles') return $this->editRoles; - else if($key == 'table') return $this->getTable(); - else if($key == 'prevTable') return $this->prevTable; - else if($key == 'prevName') return $this->prevName; - else if($key == 'prevFieldtype') return $this->prevFieldtype; - else if(isset($this->settings[$key])) return $this->settings[$key]; - else if($key == 'icon') return $this->getIcon(true); - else if($key == 'useRoles') return ($this->settings['flags'] & self::flagAccess) ? true : false; - else if($key == 'flags') return $this->settings['flags']; - else if($key == 'flagsStr') return $this->wire('fields')->getFlagNames($this->settings['flags'], true); - else if($key == 'tagList') return $this->getTags(); - else if($key == 'tags') return $this->getTags(true); + if(isset($this->settings[$key])) return $this->settings[$key]; $value = parent::get($key); + if($key === 'allowContexts' && !is_array($value)) $value = array(); - if(is_array($this->trackGets)) $this->trackGets($key); + if($this->trackGets && is_array($this->trackGets)) $this->trackGets($key); + return $value; } @@ -610,21 +628,25 @@ public function setImportData(array $data) { * */ public function setName($name) { - $name = $this->wire('sanitizer')->fieldName($name); - - if($this->wire('fields')->isNative($name)) { - throw new WireException("Field may not be named '$name' because it is a reserved word"); - } - if($this->wire('fields') && ($f = $this->wire('fields')->get($name)) && $f->id != $this->id) { - throw new WireException("Field may not be named '$name' because it is already used by another field"); - } - - if(strpos($name, '__') !== false) { - throw new WireException("Field name '$name' may not have double underscores because this usage is reserved by the core"); + $fields = $this->wire()->fields; + + if($fields) { + if(!ctype_alnum("$name")) { + $name = $this->wire()->sanitizer->fieldName($name); + } + if($fields->isNative($name)) { + throw new WireException("Field may not be named '$name' because it is a reserved word"); + } + if(($f = $fields->get($name)) && $f->id != $this->id) { + throw new WireException("Field may not be named '$name' because it is already used by another field ($f->id: $f->name)"); + } + if(strpos($name, '__') !== false) { + throw new WireException("Field name '$name' may not have double underscores because this usage is reserved by the core"); + } } - - if($this->settings['name'] != $name) { + + if(!empty($this->settings['name']) && $this->settings['name'] != $name) { if($this->settings['name'] && ($this->settings['flags'] & Field::flagSystem)) { throw new WireException("You may not change the name of field '{$this->settings['name']}' because it is a system field."); } @@ -657,8 +679,8 @@ public function setFieldtype($type) { } else if(is_string($type)) { $typeStr = $type; - $fieldtypes = $this->wire('fieldtypes'); /** @var Fieldtypes $fieldtypes */ - if(!$type = $fieldtypes->get($type)) { + $type = $this->wire()->fieldtypes->get($type); + if(!$type) { $this->error("Fieldtype '$typeStr' does not exist"); return $this; } @@ -666,10 +688,13 @@ public function setFieldtype($type) { throw new WireException("Invalid field type in call to Field::setFieldType"); } - if(!$this->type || ($this->type->name != $type->name)) { - $this->trackChange("type:{$type->name}"); - if($this->type) $this->prevFieldtype = $this->type; + $thisType = $this->settings['type']; + + if($thisType && "$thisType" != "$type") { + if($this->trackChanges) $this->trackChange("type:$type"); + $this->prevFieldtype = $thisType; } + $this->settings['type'] = $type; return $this; @@ -682,7 +707,7 @@ public function setFieldtype($type) { * * #pw-group-retrieval * - * @return Fieldtype|null + * @return Fieldtype|null|string * @since 3.0.16 Added for consistency, but all versions can still use $field->type. * */ @@ -796,7 +821,7 @@ public function setRoles($type, $roles) { } } if($type == 'view') { - $guestID = $this->wire('config')->guestUserRolePageID; + $guestID = $this->wire()->config->guestUserRolePageID; // if guest is present, then that's inclusive of all, no need to store others in viewRoles if(in_array($guestID, $ids)) $ids = array($guestID); if($this->viewRoles != $ids) { @@ -829,7 +854,7 @@ public function setRoles($type, $roles) { * */ public function ___viewable(Page $page = null, User $user = null) { - return $this->wire('fields')->_hasPermission($this, 'view', $page, $user); + return $this->wire()->fields->_hasPermission($this, 'view', $page, $user); } /** @@ -848,7 +873,7 @@ public function ___viewable(Page $page = null, User $user = null) { * */ public function ___editable(Page $page = null, User $user = null) { - return $this->wire('fields')->_hasPermission($this, 'edit', $page, $user); + return $this->wire()->fields->_hasPermission($this, 'edit', $page, $user); } /** @@ -862,11 +887,9 @@ public function ___editable(Page $page = null, User $user = null) { * */ public function save() { - $fields = $this->wire('fields'); - return $fields->save($this); + return $this->wire()->fields->save($this); } - /** * Return the number of Fieldgroups this field is used in. * @@ -891,18 +914,29 @@ public function numFieldgroups() { * */ public function getFieldgroups($getCount = false) { - $fieldgroups = $getCount ? null : $this->wire(new FieldgroupsArray()); + + $fieldgroups = $this->wire()->fieldgroups; + $items = $getCount ? null : $this->wire(new FieldgroupsArray()); /** @var FieldgroupsArray $items */ $count = 0; - foreach($this->wire()->fieldgroups as $fieldgroup) { + + /* + * note: all fieldgroups load on the foreach($fieldgroups) so this code doesn't seem necessary? + if($fieldgroups->useLazy()) { + $fieldgroups->loadLazyItemsByValue('fields_id', $this->settings['id']); + } + */ + + foreach($fieldgroups as $fieldgroup) { foreach($fieldgroup as $field) { if($field->id == $this->id) { - if($fieldgroups) $fieldgroups->add($fieldgroup); + if($items) $items->add($fieldgroup); $count++; break; } } } - return $getCount ? $count : $fieldgroups; + + return $getCount ? $count : $items; } /** @@ -915,24 +949,26 @@ public function getFieldgroups($getCount = false) { * */ public function getTemplates($getCount = false) { + $templates = $this->wire()->templates; if($getCount) { $count = 0; - foreach($this->templates as $template) { + foreach($templates as $template) { if($template->hasField($this)) $count++; } return $count; } - $templates = $this->wire(new TemplatesArray()); + /** @var TemplatesArray $items */ + $items = $this->wire(new TemplatesArray()); $fieldgroups = $this->getFieldgroups(); - foreach($this->templates as $template) { + foreach($templates as $template) { foreach($fieldgroups as $fieldgroup) { if($template->fieldgroups_id == $fieldgroup->id) { - $templates->add($template); + $items->add($template); break; } } } - return $templates; + return $items; } @@ -1196,7 +1232,9 @@ public function ___getConfigInputfields() { * */ public function getTable() { - if(is_null(self::$lowercaseTables)) self::$lowercaseTables = $this->config->dbLowercaseTables ? true : false; + if(self::$lowercaseTables === null) { + self::$lowercaseTables = $this->wire()->config->dbLowercaseTables ? true : false; + } if(!empty($this->setTable)) { $table = $this->setTable; } else { @@ -1217,7 +1255,7 @@ public function getTable() { * */ public function setTable($table = null) { - $table = empty($table) ? '' : $this->wire('sanitizer')->fieldName($table); + $table = empty($table) ? '' : $this->wire()->sanitizer->fieldName($table); $this->setTable = $table; } @@ -1390,10 +1428,12 @@ public function setNotes($text, $language = null) { */ public function setIcon($icon) { // store the non-prefixed version - if(strpos($icon, 'icon-') === 0) $icon = str_replace('icon-', '', $icon); - if(strpos($icon, 'fa-') === 0) $icon = str_replace('fa-', '', $icon); - $icon = $this->wire('sanitizer')->pageName($icon); - parent::set('icon', $icon); + if(strlen("$icon")) { + if(strpos($icon, 'icon-') === 0) $icon = str_replace('icon-', '', $icon); + if(strpos($icon, 'fa-') === 0) $icon = str_replace('fa-', '', $icon); + $icon = $this->wire()->sanitizer->pageName($icon); + } + parent::set('icon', "$icon"); return $this; } @@ -1446,7 +1486,7 @@ public function setTags($tagList, $reindex = true) { if($this->tagList !== $tagList) { $this->tagList = $tagList; parent::set('tags', implode(' ', $tagList)); - $this->wire('fields')->getTags('reset'); + $this->wire()->fields->getTags('reset'); } return $tagList; } @@ -1509,9 +1549,9 @@ public function editUrl($options = array()) { if(is_string($options)) $options = array('find' => $options); if(is_bool($options)) $options = array('http' => $options); if(!is_array($options)) $options = array(); - $url = $this->wire('config')->urls(empty($options['http']) ? 'admin' : 'httpAdmin'); + $url = $this->wire()->config->urls(empty($options['http']) ? 'admin' : 'httpAdmin'); $url .= "setup/field/edit?id=$this->id"; - if(!empty($options['find'])) $url .= '#find-' . $this->wire('sanitizer')->fieldName($options['find']); + if(!empty($options['find'])) $url .= '#find-' . $this->wire()->sanitizer->fieldName($options['find']); return $url; } diff --git a/wire/core/Fieldgroup.php b/wire/core/Fieldgroup.php index be0b0ca07..7a8b1da96 100644 --- a/wire/core/Fieldgroup.php +++ b/wire/core/Fieldgroup.php @@ -13,7 +13,7 @@ * are separated in the API in case want want to have fieldgroups used by * multiple templates in the future (like ProcessWire 1.x). * - * ProcessWire 3.x, Copyright 2016 by Ryan Cramer + * ProcessWire 3.x, Copyright 2022 by Ryan Cramer * https://processwire.com * * @property int $id Fieldgroup database ID #pw-group-retrieval @@ -37,7 +37,7 @@ class Fieldgroup extends WireArray implements Saveable, Exportable, HasLookupIte protected $settings = array( 'id' => 0, 'name' => '', - ); + ); /** * Any fields that were removed from this instance are noted so that Fieldgroups::save() can delete unused data @@ -122,10 +122,12 @@ public function makeBlankItem() { * */ public function add($field) { - if(!is_object($field)) $field = $this->wire('fields')->get($field); + if(!is_object($field)) $field = $this->wire()->fields->get($field); if($field && $field instanceof Field) { - if(!$field->id) throw new WireException("You must save field '$field' before adding to Fieldgroup '{$this->name}'"); + if(!$field->id) { + throw new WireException("You must save field '$field' before adding to Fieldgroup '$this->name'"); + } parent::add($field); } else { // throw new WireException("Unable to add field '$field' to Fieldgroup '{$this->name}'"); @@ -371,7 +373,10 @@ public function addLookupItem($item, array &$row) { if($item) $this->add($item); if(!empty($row['data'])) { // set field context for this fieldgroup - $this->fieldContexts[(int)$item] = wireDecodeJSON($row['data']); + $data = $row['data']; + if(is_string($data)) $data = wireDecodeJSON($data); + if(!is_array($data)) $row['data'] = array(); + $this->fieldContexts[(int) "$item"] = $data; } return $this; } @@ -392,15 +397,17 @@ public function set($key, $value) { if($key == 'data') return $this; // we don't have a data field here - if($key == 'id') { + if($key === 'id') { $value = (int) $value; - } else if($key == 'name') { - $value = $this->wire('sanitizer')->name($value); + } else if($key === 'name') { + $value = $this->wire()->sanitizer->templateName($value); } if(isset($this->settings[$key])) { - if($this->settings[$key] !== $value) $this->trackChange($key, $this->settings[$key], $value); + if($this->trackChanges && $this->settings[$key] !== $value) { + $this->trackChange($key, $this->settings[$key], $value); + } $this->settings[$key] = $value; } else { diff --git a/wire/core/Fieldgroups.php b/wire/core/Fieldgroups.php index d209ca37e..8a50c1709 100644 --- a/wire/core/Fieldgroups.php +++ b/wire/core/Fieldgroups.php @@ -23,20 +23,19 @@ class Fieldgroups extends WireSaveableItemsLookup { /** - * Instances of FieldgroupsArray + * Instance of FieldgroupsArray * * @var FieldgroupsArray * */ - protected $fieldgroupsArray; + protected $fieldgroupsArray = null; /** * Init * */ public function init() { - $this->fieldgroupsArray = $this->wire(new FieldgroupsArray()); - $this->load($this->fieldgroupsArray); + $this->getWireArray(); } /** @@ -48,7 +47,7 @@ public function init() { */ protected function getLoadQuery($selectors = null) { $query = parent::getLoadQuery($selectors); - $lookupTable = $this->wire('database')->escapeTable($this->getLookupTable()); + $lookupTable = $this->wire()->database->escapeTable($this->getLookupTable()); $query->select("$lookupTable.data"); // QA return $query; } @@ -76,6 +75,19 @@ protected function ___load(WireArray $items, $selectors = null) { * */ public function getAll() { + 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; } @@ -119,7 +131,14 @@ public function getLookupTable() { * */ public function getNumTemplates(Fieldgroup $fieldgroup) { - return count($this->getTemplates($fieldgroup)); + $templates = $this->wire()->templates; + $num = 0; + + foreach($templates->getAllValues('fieldgroups_id', 'id') as $templateId => $fieldgroupId) { + if($fieldgroupId == $fieldgroup->id) $num++; + } + + return $num; } /** @@ -130,11 +149,59 @@ public function getNumTemplates(Fieldgroup $fieldgroup) { * */ 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); + $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) { + $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 $templates; + return $fieldNames; } /** diff --git a/wire/core/Fields.php b/wire/core/Fields.php index d5d1a1570..091594a2b 100644 --- a/wire/core/Fields.php +++ b/wire/core/Fields.php @@ -5,7 +5,7 @@ * * Manages collection of ALL Field instances, not specific to any particular Fieldgroup * - * ProcessWire 3.x, Copyright 2018 by Ryan Cramer + * ProcessWire 3.x, Copyright 2022 by Ryan Cramer * https://processwire.com * * #pw-summary Manages all custom fields in ProcessWire, independently of any Fieldgroup. @@ -34,6 +34,8 @@ class Fields extends WireSaveableItems { /** * Instance of FieldsArray + * + * @var FieldsArray * */ protected $fieldsArray = null; @@ -96,6 +98,12 @@ class Fields extends WireSaveableItems { '_custom', ); + /** + * Flag names in format [ flagInt => 'flagName' ] + * + * @var array + * + */ protected $flagNames = array(); /** @@ -120,12 +128,18 @@ class Fields extends WireSaveableItems { */ protected $tableTools = null; + /** + * @var Fieldtypes|null + * + */ + protected $fieldtypes = null; + /** * Construct * */ public function __construct() { - $this->fieldsArray = new FieldsArray(); + parent::__construct(); $this->flagNames = array( Field::flagAutojoin => 'autojoin', Field::flagGlobal => 'global', @@ -138,7 +152,9 @@ public function __construct() { 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); + if(isset(self::$nativeNamesSystem[0])) { + self::$nativeNamesSystem = array_flip(self::$nativeNamesSystem); + } } /** @@ -148,8 +164,7 @@ public function __construct() { * */ public function init() { - $this->wire($this->fieldsArray); - $this->load($this->fieldsArray); + $this->getWireArray(); } /** @@ -174,44 +189,94 @@ public function makeBlankItem() { public function makeItem(array $a = array()) { if(empty($a['type'])) return parent::makeItem($a); - - /** @var Fieldtypes $fieldtypes */ - $fieldtypes = $this->wire('fieldtypes'); - if(!$fieldtypes) return parent::makeItem($a); + if($this->fieldtypes === null) $this->fieldtypes = $this->wire()->fieldtypes; + if(!$this->fieldtypes) return parent::makeItem($a); /** @var Fieldtype $fieldtype */ - $fieldtype = $fieldtypes->get($a['type']); - if(!$fieldtype) return parent::makeItem($a); + $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') return parent::makeItem($a); - if(strpos($class, "\\") === false) $class = wireClassName($class, true); - if(!class_exists($class)) return parent::makeItem($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 */ + } - /** @var Field $field */ - $field = new $class(); $this->wire($field); + $field->setTrackChanges(false); foreach($a as $key => $value) { - $field->$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 + * @return FieldsArray|WireArray * */ public function getAll() { - return $this->fieldsArray; + 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; } /** @@ -354,22 +419,29 @@ public function checkFieldTables() { */ public function ___delete(Saveable $item) { - if(!$this->fieldsArray->isValidItem($item)) throw new WireException("Fields::delete(item) only accepts items of type Field"); + 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) throw new WireException("Unable to delete from '" . $item->getTable() . "' for field that doesn't exist in fields table"); + 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'"); + 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."); + 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); + $this->wire()->fieldgroups->deleteField($item); // drop the field's table if($item->type) $item->type->deleteField($item); @@ -990,6 +1062,102 @@ public function findByTag($tag, $getFieldNames = false) { 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) { + $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 * diff --git a/wire/core/Fieldtype.php b/wire/core/Fieldtype.php index a6ca454fe..f6d03fe20 100644 --- a/wire/core/Fieldtype.php +++ b/wire/core/Fieldtype.php @@ -99,6 +99,7 @@ public function __construct() { } * */ public function init() { } + public function ready() { } /** * Set last access field @@ -774,6 +775,18 @@ public function getMatchQuerySort(Field $field, $query, $table, $subfield, $desc if($query && $table && $field && $subfield && $desc) {} return false; } + + /** + * Called when field of this type is initialized at boot or after lazy loaded + * + * #pw-internal + * + * @param Field $field + * @since 3.0.194 + * + */ + public function initField(Field $field) { + } /** * Create a new field table in the database. diff --git a/wire/core/ProcessWire.php b/wire/core/ProcessWire.php index d702de541..1ba679cd3 100644 --- a/wire/core/ProcessWire.php +++ b/wire/core/ProcessWire.php @@ -291,6 +291,7 @@ public function __construct($config = null, $rootURL = '/') { $config->setWire($this); $this->debug = $config->debug; + if($this->debug) Debug::timer('all'); $this->instanceID = self::addInstance($this); $this->setWire($this); @@ -552,9 +553,11 @@ public function load(Config $config) { $pages = $this->wire('pages', new Pages($this), true); $this->initVar('fieldtypes', $fieldtypes); + if($this->debug) Debug::timer('init.fields.templates.fieldgroups'); $this->initVar('fields', $fields); $this->initVar('fieldgroups', $fieldgroups); $this->initVar('templates', $templates); + if($this->debug) Debug::saveTimer('init.fields.templates.fieldgroups'); $this->initVar('pages', $pages); if($this->debug) Debug::timer('boot.load.permissions'); diff --git a/wire/core/Template.php b/wire/core/Template.php index 097f73fa8..e6704c203 100644 --- a/wire/core/Template.php +++ b/wire/core/Template.php @@ -781,6 +781,8 @@ protected function setSetting($key, $value) { /** * Set setting value without processing * + * #pw-internal + * * @param string $key * @param mixed $value * @since 3.0.194 diff --git a/wire/core/Templates.php b/wire/core/Templates.php index 262c3312f..133b6e8d5 100644 --- a/wire/core/Templates.php +++ b/wire/core/Templates.php @@ -5,7 +5,7 @@ * * Manages and provides access to all the Template instances * - * ProcessWire 3.x, Copyright 2019 by Ryan Cramer + * ProcessWire 3.x, Copyright 2022 by Ryan Cramer * https://processwire.com * * #pw-summary Manages and provides access to all the Templates. @@ -36,7 +36,7 @@ class Templates extends WireSaveableItems { * @var TemplatesArray * */ - protected $templatesArray; + protected $templatesArray = null; /** * Templates that had changed files during this request @@ -61,9 +61,9 @@ class Templates extends WireSaveableItems { * */ public function __construct(Fieldgroups $fieldgroups) { + parent::__construct(); $fieldgroups->wire($this); - $this->fieldgroups = $fieldgroups; - $this->templatesArray = $this->wire(new TemplatesArray()); + $this->fieldgroups = $fieldgroups; } /** @@ -73,8 +73,7 @@ public function __construct(Fieldgroups $fieldgroups) { * */ public function init() { - $this->wire($this->templatesArray); - $this->load($this->templatesArray); + $this->getWireArray(); } /** @@ -84,6 +83,23 @@ public function init() { * */ public function getAll() { + return $this->getWireArray(); + } + + /** + * Get WireArray container that items are stored in + * + * #pw-internal + * + * @return WireArray + * @since 3.0.194 + * + */ + public function getWireArray() { + if($this->templatesArray === null) { + $this->templatesArray = $this->wire(new TemplatesArray()); + $this->load($this->templatesArray); + } return $this->templatesArray; } @@ -99,18 +115,47 @@ public function getAll() { */ public function makeItem(array $a = array()) { - /** @var Template $item */ + /** @var Template $template */ $template = $this->wire(new Template()); $template->loaded(false); + + if(!empty($a['data'])) { + if(is_string($a['data'])) $a['data'] = $this->decodeData($a['data']); + } else { + unset($a['data']); + } + + foreach(array('id', 'name', 'fieldgroups_id', 'flags', 'cache_time') as $key) { + if(!isset($a[$key])) continue; + $value = $key === 'name' ? $a[$key] : (int) $a[$key]; + $template->setRaw($key, $value); + unset($a[$key]); + } + foreach($a as $key => $value) { $template->set($key, $value); } + $template->loaded(true); $template->resetTrackChanges(true); return $template; } + /** + * Load all lazy items + * + * #pw-internal + * + * @since 3.0.194 + * + */ + public function loadAllLazyItems() { + if(!$this->useLazy()) return; + $this->wire()->fieldgroups->loadAllLazyItems(); + parent::loadAllLazyItems(); + } + /** * Return a new blank item * @@ -200,8 +245,8 @@ public function add($name, array $properties = array()) { * */ public function get($key) { - if($key == 'path') return $this->wire('config')->paths->templates; - $value = $this->templatesArray->get($key); + if($key === 'path') return $this->wire()->config->paths->templates; + $value = $this->getWireArray()->get($key); if(is_null($value)) $value = parent::get($key); return $value; } @@ -224,14 +269,22 @@ public function ___save(Saveable $item) { $isNew = $item->id < 1; - if(!$item->fieldgroup) throw new WireException("Template '$item' cannot be saved because it has no fieldgroup assigned"); - if(!$item->fieldgroup->id) throw new WireException("You must save Fieldgroup '{$item->fieldgroup->name}' before adding to Template '{$item}'"); + if(!$item->fieldgroup) { + throw new WireException("Template '$item' cannot be saved because it has no fieldgroup assigned"); + } + if(!$item->fieldgroup->id) { + throw new WireException("You must save Fieldgroup '{$item->fieldgroup->name}' before adding to Template '{$item}'"); + } $rolesChanged = $item->isChanged('useRoles'); - if($this->wire('pages')->get("/")->template->id == $item->id) { - if(!$item->useRoles) throw new WireException("Template '{$item}' is used by the homepage and thus must manage access"); - if(!$item->hasRole("guest")) throw new WireException("Template '{$item}' is used by the homepage and thus must have the 'guest' role assigned."); + if($this->wire()->pages->get('/')->template->id == $item->id) { + if(!$item->useRoles) { + throw new WireException("Template '{$item}' is used by the homepage and thus must manage access"); + } + if(!$item->hasRole('guest')) { + throw new WireException("Template '{$item}' is used by the homepage and thus must have the 'guest' role assigned."); + } } if(!$item->isChanged('modified')) $item->modified = time(); diff --git a/wire/core/WireSaveableItems.php b/wire/core/WireSaveableItems.php index 76d709f0a..870c56252 100644 --- a/wire/core/WireSaveableItems.php +++ b/wire/core/WireSaveableItems.php @@ -44,11 +44,27 @@ abstract public function getAll(); */ abstract public function makeBlankItem(); + /** + * Get WireArray container that items are stored in + * + * This is the same as the getAll() method except that it is guaranteed not to load + * additional items as part of the call. + * + * #pw-internal + * + * @return WireArray + * @since 3.0.194 + * + */ + public function getWireArray() { + return $this->getAll(); + } + /** * Make an item and populate with given data * * @param array $a Associative array of data to populate - * @return Saveable|Wire + * @return Saveable|WireData|Wire * @throws WireException * @since 3.0.146 * @@ -71,7 +87,6 @@ public function makeItem(array $a = array()) { */ abstract public function getTable(); - /** * Return the default name of the field that load() should sort by (default is none) * @@ -93,7 +108,7 @@ public function getSort() { return ''; } */ protected function getLoadQuerySelectors($selectors, DatabaseQuerySelect $query) { - $database = $this->wire('database'); + $database = $this->wire()->database; if(is_object($selectors) && $selectors instanceof Selectors) { // iterable selectors @@ -165,7 +180,7 @@ protected function getLoadQuery($selectors = null) { $item = $this->makeBlankItem(); $fields = array_keys($item->getTableData()); - $database = $this->wire('database'); + $database = $this->wire()->database; $table = $database->escapeTable($this->getTable()); @@ -195,23 +210,24 @@ protected function getLoadQuery($selectors = null) { */ protected function ___load(WireArray $items, $selectors = null) { - /** @var WireDatabasePDO $database */ - $database = $this->wire('database'); + $useLazy = $this->useLazy(); + $database = $this->wire()->database; $sql = $this->getLoadQuery($selectors)->getQuery(); - - $query = $database->prepare($sql); + + $query = $database->prepare($sql); $query->execute(); - - while($row = $query->fetch(\PDO::FETCH_ASSOC)) { - if(isset($row['data'])) { - if($row['data']) { - $row['data'] = $this->decodeData($row['data']); - } else { - unset($row['data']); - } + $rows = $query->fetchAll(\PDO::FETCH_ASSOC); + $n = 0; + + foreach($rows as $row) { + if($useLazy) { + $this->lazyItems[$n] = $row; + $this->lazyNameIndex[$row['name']] = $n; + $this->lazyIdIndex[$row['id']] = $n; + $n++; + } else { + $this->initItem($row, $items); } - $item = $this->makeItem($row); - if($item) $items->add($item); } $query->closeCursor(); @@ -220,6 +236,35 @@ protected function ___load(WireArray $items, $selectors = null) { return $items; } + /** + * 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) { + + if(!empty($row['data'])) { + if(is_string($row['data'])) $row['data'] = $this->decodeData($row['data']); + } else { + unset($row['data']); + } + + if($items === null) $items = $this->getWireArray(); + + $item = $this->makeItem($row); + + if($item) { + if($this->useLazy() && $item->id) $this->unsetLazy($item); + $items->add($item); + } + + return $item; + } + /** * Should the given item key/field be saved in the database? * @@ -230,7 +275,7 @@ protected function ___load(WireArray $items, $selectors = null) { * */ protected function saveItemKey($key) { - if($key == 'id') return false; + if($key === 'id') return false; return true; } @@ -299,7 +344,7 @@ public function ___save(Saveable $item) { $result = $query->execute(); if($result) { $item->id = (int) $database->lastInsertId(); - $this->getAll()->add($item); + $this->getWireArray()->add($item); $this->added($item); } } @@ -334,7 +379,7 @@ public function ___delete(Saveable $item) { $database = $this->wire('database'); $this->deleteReady($item); - $this->getAll()->remove($item); + $this->getWireArray()->remove($item); $table = $database->escapeTable($this->getTable()); $query = $database->prepare("DELETE FROM `$table` WHERE id=:id LIMIT 1"); @@ -397,32 +442,143 @@ public function ___clone(Saveable $item, $name = '') { * */ public function ___find($selectors) { + if($this->useLazy()) $this->loadAllLazyItems(); return $this->getAll()->find($selectors); } #[\ReturnTypeWillChange] public function getIterator() { + if($this->useLazy()) $this->loadAllLazyItems(); return $this->getAll(); } + /** + * Get an item + * + * @param string|int $key + * @return array|mixed|null|Page|Saveable|Wire|WireData + * + */ public function get($key) { - return $this->getAll()->get($key); + $value = $this->getWireArray()->get($key); + if($value === null && $this->useLazy() && $key !== null) $value = $this->getLazy($key); + return $value; } public function __get($key) { $value = $this->get($key); - if(is_null($value)) $value = parent::__get($key); + if($value === null) $value = parent::__get($key); return $value; } + /** + * Do we have the given item or item by given key? + * + * @param string|int|Saveable|WireData $item + * @return bool + * + */ public function has($item) { - return $this->getAll()->has($item); + if($this->useLazy() && !empty($this->lazyItems)) $this->get($item); // ensure lazy item present + return $this->getAll()->has($item); } + /** + * Isset + * + * @param string|int $key + * @return bool + * + */ public function __isset($key) { return $this->get($key) !== null; } + /** + * Get all property values for items + * + * This is useful for getting all property values without triggering lazy loaded items to load. + * + * #pw-internal + * + * @param string $valueType|array Name of property value you want to get, or array of them, i.e. 'id', 'name', etc. (default='id') + * @param string $indexType One of 'name', 'id' or blank string for no index (default='') + * @param string $matchType Optionally match this property, also requires $matchValue argument (default='') + * @param string|int|array $matchValue Match this value for $matchType property, use array for OR values (default=null) + * @return array + * @since 3.0.194 + * + */ + public function getAllValues($valueType = 'id', $indexType = '', $matchType = '', $matchValue = null) { + + $values = array(); + $useValueArray = is_array($valueType); + $matchArray = is_array($matchValue) ? array_flip($matchValue) : false; + $items = $this->getWireArray(); + + if($this->useLazy()) { + foreach($this->lazyItems as $row) { + $index = null; + if($matchValue !== null) { + if($matchArray) { + $v = isset($row[$matchType]) ? $row[$matchType] : null; + if(!$v === null || !isset($matchArray[$v])) continue; + } else { + if($row[$matchType] != $matchValue) continue; + } + } + if($indexType) { + $index = isset($row[$indexType]) ? $row[$indexType] : $row['id']; + } + if($useValueArray) { + /** @var array $valueType */ + $value = array(); + foreach($valueType as $key) { + $value[$key] = isset($row[$key]) ? $row[$key] : null; + } + } else { + $value = isset($row[$valueType]) ? $row[$valueType] : null; + } + if($index !== null) { + $values[$index] = $value; + } else { + $values[] = $value; + } + } + } + + foreach($items as $field) { + $index = null; + if($matchValue !== null) { + if($matchArray) { + $v = $field->get($matchType); + if($v === null || !isset($matchArray[$v])) continue; + } else { + if($field->get($matchType) != $matchValue) continue; + } + } + if($indexType) { + $index = $field->get($indexType); + } + if($useValueArray) { + /** @var array $valueType */ + $value = array(); + foreach($valueType as $key) { + $value[$key] = $field->get($key); + } + } else { + $value = $field->get($valueType); + } + if($index !== null) { + $values[$index] = $value; + } else { + $values[] = $value; + } + } + + return $values; + } + /** * Encode the 'data' portion of the table. * @@ -459,6 +615,11 @@ protected function decodeData($value) { public function useFuel($useFuel = null) { return false; } + + /************************************************************************************** + * HOOKERS + * + */ /** * Hook that runs right before item is to be saved. @@ -561,6 +722,12 @@ public function ___renamed(Saveable $item, $oldName, $newName) { $this->log("Renamed $oldName to $newName", $item); } + + /************************************************************************************** + * OTHER + * + */ + /** * Enables use of $apivar('name') or wire()->apivar('name') * @@ -603,5 +770,190 @@ public function error($text, $flags = 0) { return parent::error($text, $flags); } + /** + * debugInfo PHP 5.6+ magic method + * + * This is used when you print_r() an object instance. + * + * @return array + * + */ + public function __debugInfo() { + $info = array(); // parent::__debugInfo(); + $info['loaded'] = array(); + $info['notLoaded'] = array(); + foreach($this->getWireArray() as $item) { + /** @var WireData|Saveable $item */ + $when = $item->get('_lazy'); + $value = $item->get('name|id'); + $value = $value ? "$value ($when)" : $item; + $info['loaded'][] = $value; + } + foreach($this->lazyItems as $row) { + $value = null; + if(isset($row['name'])) $value = $row['name']; + if(!$value && isset($row['id'])) $value = $row['id']; + if(!$value) $value = &$row; + $info['notLoaded'][] = $value; + } + return $info; + } + + /************************************************************************************** + * LAZY LOADING + * + */ + + /** + * Lazy loaded raw item data from database + * + * @var array + * + */ + protected $lazyItems = array(); // [ 0 => [ ... ], 1 => [ ... ], etc. ] + protected $lazyNameIndex = array(); // [ 'name' => 123 ] where 123 is key in $lazyItems + protected $lazyIdIndex = array(); // [ 3 => 123 ] where 3 is ID and 123 is key in $lazyItems + + /** + * @var bool|null + * + */ + protected $useLazy = null; + + + /** + * Use lazy loading for this type? + * + * @return bool + * @since 3.0.194 + * + */ + public function useLazy() { + if($this->useLazy !== null) return $this->useLazy; + $this->useLazy = $this->wire()->config->useLazyLoading; + if(is_array($this->useLazy)) $this->useLazy = in_array(strtolower($this->className()), $this->useLazy); + return $this->useLazy; + } + + /** + * Remove item from lazy loading data/indexes + * + * @param Saveable $item + * @return bool + * + */ + protected function unsetLazy(Saveable $item) { + if(!isset($this->lazyIdIndex[$item->id])) return false; + $key = $this->lazyIdIndex[$item->id]; + unset($this->lazyItems[$key], $this->lazyNameIndex[$item->name], $this->lazyIdIndex[$item->id]); + return true; + } + + /** + * Load all pending lazy-loaded items + * + * #pw-internal + * + */ + public function loadAllLazyItems() { + + if(!$this->useLazy()) return; + + $debug = $this->wire()->config->debug; + $items = $this->getWireArray(); + + foreach(array_keys($this->lazyItems) as $key) { + if(!isset($this->lazyItems[$key])) continue; // required + $row = &$this->lazyItems[$key]; + $item = $this->initItem($row, $items); + if($debug) $item->setQuietly('_lazy', '*'); + } + + $this->lazyItems = array(); + $this->lazyNameIndex = array(); + $this->lazyIdIndex = array(); + + // if you want to identify what triggered a “load all”, uncomment below: + // bd(Debug::backtrace()); + } + + /** + * Lazy load items by property value + * + * #pw-internal + * + * @param string $key i.e. fieldgroups_id + * @param string|int $value + * @todo I don't think we need this method, but leaving it here temporarily for reference + * @deprecated + * + */ + private function loadLazyItemsByValue($key, $value) { + + $debug = $this->wire()->config->debug; + $items = $this->getWireArray(); + + foreach($this->lazyItems as $lazyKey => $lazyItem) { + if($lazyItem[$key] != $value) continue; + $item = $this->initItem($lazyItem, $items); + unset($this->lazyItems[$lazyKey]); + if($debug) $item->setQuietly('_lazy', '='); + } + } + + /** + * Get a lazy loaded item, companion to get() method + * + * #pw-internal + * + * @param string|int $value + * @return Saveable|Wire|WireData|null + * @since 3.0.194 + * + */ + protected function getLazy($value) { + + $property = ctype_digit("$value") ? 'id' : 'name'; + $value = $property === 'id' ? (int) $value : "$value"; + $item = null; + $lazyItem = null; + $lazyKey = null; + + if(!empty($this->lazyIdIndex)) { + if($property === 'id') { + $index = &$this->lazyIdIndex; + } else { + $index = &$this->lazyNameIndex; + } + if(isset($index[$value])) { + $lazyKey = $index[$value]; + $lazyItem = $this->lazyItems[$lazyKey]; + } + } else { + foreach($this->lazyItems as $key => $row) { + if(!isset($row[$property]) || $row[$property] != $value) continue; + $lazyKey = $key; + $lazyItem = $row; + break; + } + } + + if($lazyItem) { + $item = $this->initItem($lazyItem); + $this->getWireArray()->add($item); + unset($this->lazyItems[$lazyKey]); + if($this->wire()->config->debug) $item->setQuietly('_lazy', '1'); + } + + if($item === null && $property === 'name' && !ctype_alnum($value)) { + if(Selectors::stringHasOperator("$value") || strpos("$value", '|')) { + $this->loadAllLazyItems(); + $item = $this->getWireArray()->get($value); + } + } + + return $item; + } + } diff --git a/wire/core/WireSaveableItemsLookup.php b/wire/core/WireSaveableItemsLookup.php index 7c75486fc..fb91cf507 100644 --- a/wire/core/WireSaveableItemsLookup.php +++ b/wire/core/WireSaveableItemsLookup.php @@ -18,6 +18,14 @@ abstract class WireSaveableItemsLookup extends WireSaveableItems { */ abstract public function getLookupTable(); + /** + * Cache of value returned from getLookupField() method + * + * @var string|null + * + */ + protected $lookupField = null; + /** * If a lookup table should be left joined, this method returns the name of the array field in $data that contains multiple values * @@ -26,9 +34,11 @@ abstract public function getLookupTable(); * */ public function getLookupField() { + if($this->lookupField) return $this->lookupField; $lookupTable = $this->getLookupTable(); if(!$lookupTable) return ''; - return preg_replace('/_?' . $this->getTable() . '_?/', '', $lookupTable) . '_id'; + $this->lookupField = preg_replace('/_?' . $this->getTable() . '_?/', '', $lookupTable) . '_id'; + return $this->lookupField; } /** @@ -62,43 +72,84 @@ protected function getLoadQuery($selectors = null) { */ protected function ___load(WireArray $items, $selectors = null) { + $useLazy = $this->useLazy(); $database = $this->wire()->database; $query = $this->getLoadQuery($selectors); $sql = $query->getQuery(); + + $this->getLookupField(); // preload + $stmt = $database->prepare($sql); $stmt->execute(); - $lookupField = $this->getLookupField(); - - while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { - - /** @var HasLookupItems $item */ - $item = $this->makeBlankItem(); - $lookupValue = $row[$lookupField]; - unset($row[$lookupField]); - $item->addLookupItem($lookupValue, $row); - - foreach($row as $field => $value) { - $item->$field = $value; - } - - if($items->has($item)) { - // LEFT JOIN is adding more elements of the same item, i.e. from lookup table - // if the item is already present in $items, then use the existing one rather - // and throw out the one we just created - $item = $items->get($item); - $item->addLookupItem($lookupValue, $row); + $rows = $stmt->fetchAll(\PDO::FETCH_ASSOC); + + // note: non-use of lazyNameIndex/lazyIdIndex is intentional + + foreach($rows as $row) { + if($useLazy) { + $this->lazyItems[] = $row; } else { - // add a new item - $items->add($item); + /** @var HasLookupItems $item */ + $this->initItem($row, $items); } } - + $stmt->closeCursor(); $items->setTrackChanges(true); return $items; } + /** + * Create a new Saveable/Lookup item from a raw array ($row) and add it to $items + * + * @param array $row + * @param WireArray|null $items + * @return Saveable|HasLookupItems|WireData|Wire + * @since 3.0.194 + * + */ + protected function initItem(array &$row, WireArray $items = null) { + + $lookupField = $this->getLookupField(); + $lookupValue = $row[$lookupField]; + $item = $this->makeBlankItem(); /** @var HasLookupItems $item */ + + if($items === null) $items = $this->getWireArray(); + + unset($row[$lookupField]); + + $item->addLookupItem($lookupValue, $row); + + foreach($row as $key => $value) { + $item->$key = $value; + } + + if($this->useLazy) { + $items->add($item); + foreach($this->lazyItems as $key => $a) { + if($a['id'] != $row['id']) continue; + if(!isset($a[$lookupField])) continue; + $lookupValue = $a[$lookupField]; + unset($a[$lookupField]); + $item->addLookupItem($lookupValue, $a); + unset($this->lazyItems[$key]); + } + + } else if($items->has($item)) { + // LEFT JOIN is adding more elements of the same item, i.e. from lookup table + // if the item is already present in $items, then use the existing one rather + // and throw out the one we just created + $item = $items->get($item); + $item->addLookupItem($lookupValue, $row); + } else { + // add a new item + $items->add($item); + } + + return $item; + } + /** * Should the given item key/field be saved in the database? * @@ -178,4 +229,20 @@ public function ___delete(Saveable $item) { $query->execute(); return parent::___delete($item); } + + /** + * debugInfo PHP 5.6+ magic method + * + * This is used when you print_r() an object instance. + * + * @return array + * + */ + public function __debugInfo() { + $info = parent::__debugInfo(); + $info['loaded'] = array_unique($info['loaded']); + $info['notLoaded'] = array_unique($info['notLoaded']); + return $info; + } + } diff --git a/wire/modules/Fieldtype/FieldtypeRepeater/FieldtypeRepeater.module b/wire/modules/Fieldtype/FieldtypeRepeater/FieldtypeRepeater.module index c1153c69b..e4bdd36f2 100644 --- a/wire/modules/Fieldtype/FieldtypeRepeater/FieldtypeRepeater.module +++ b/wire/modules/Fieldtype/FieldtypeRepeater/FieldtypeRepeater.module @@ -9,7 +9,7 @@ * /wire/core/Fieldtype.php * /wire/core/FieldtypeMulti.php * - * ProcessWire 3.x, Copyright 2021 by Ryan Cramer + * ProcessWire 3.x, Copyright 2022 by Ryan Cramer * https://processwire.com * * @todo: automatic sorting. @@ -53,6 +53,30 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { const loadingAll = 1; const loadingOff = 2; + /** + * Field names used by repeaters in format [ PW_instanceNum => [ 'field_name', 'field_name2' ] ]; + * + * @var array + * + */ + static protected $fieldsUsedInRepeaters = array(); + + /** + * Template IDs used by repeaters in format [ PW_instanceNum => [ 123, 456, 789 ] ] + * + * @var array + * + */ + static protected $templatesUsedByRepeaters = array(); + + /** + * Has ready method been called? [ PW_instanceNum => true | false ] + * + * @var bool + * + */ + static protected $isReady = false; + /** * When non-zero, a deletePageField function call occurred and we shouldn't re-create any repeater parents * @@ -75,6 +99,14 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { */ protected $ajaxFieldName = ''; + /** + * Use lazy loading mode? + * + * @var null|bool + * + */ + protected $useLazy = null; + /** * Construct the Repeater Fieldtype * @@ -93,8 +125,8 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { */ public function init() { $this->wire()->pages->addHookAfter('deleteReady', $this, 'hookPagesDelete'); + $this->useLazy = $this->wire()->config->useLazyLoading; parent::init(); - // $this->initFields(); } /** @@ -102,23 +134,26 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { * */ public function ready() { - // if(in_array('WirePageEditor', wireClassImplements((string) $this->wire('page')->process))) { // @todo - // $this->initFields(); // intentionally repeated from init() - - // make sure that all templates used by repeater pages enforce a Page type of RepeaterPage - foreach($this->wire()->fields as $field) { - $fieldtype = $field->type; - if(!$fieldtype || !$fieldtype instanceof FieldtypeRepeater) continue; - /** @var FieldtypeRepeater $fieldtype */ - $template = $fieldtype->getRepeaterTemplate($field); - if(!$template) continue; - $class = $fieldtype->getPageClass(); - if(__NAMESPACE__ && $class) $class = wireClassName($class); - $_class = $template->get('pageClass'); - if($class === $_class) continue; - $template->set('pageClass', $class); - $template->save(); - } + parent::ready(); + + $instanceNum = $this->wire()->getInstanceNum(); + if(!empty(self::$isReady[$instanceNum])) return; // ensures everything below only runs only once (for extending types) + self::$isReady[$instanceNum] = true; + + if(!$this->useLazy) { + // make sure that all templates used by repeater pages enforce a Page type of RepeaterPage + // this was necessary when lazy loading option was disabled + $this->useLazy = true; + $repeaterFields = $this->wire()->fields->findByType('FieldtypeRepeater', array( + 'inherit' => true, + 'valueType' => 'field', + 'indexType' => '', + )); + foreach($repeaterFields as $field) { + $this->initField($field); + } + $this->useLazy = false; + } $page = $this->wire()->page; $process = $page->process; /** @var Process|null $process */ @@ -161,35 +196,32 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { $this->addHookBefore('PageFinder::getQuery', $this, 'hookPageFinderGetQuery'); } - + /** - * Initialize repeater fields at boot - * - protected function initFields() { - - static $initFields = array(); - $className = $this->className(); - if(isset($initFields[$className])) return; - - $fields = $this->wire('fields'); - if(!$fields) return; - - // make sure that all templates used by repeater pages enforce a Page type of RepeaterPage - foreach($fields as $field) { - if(!$field->type || $field->type->className() != $className) continue; - $template = $this->getRepeaterTemplate($field); - if(!$template) continue; - if(__NAMESPACE__) { - $template->setQuietly('pageClass', wireClassName($this->getPageClass())); - } else { - $template->setQuietly('pageClass', $this->getPageClass()); - } - } - - $initFields[$className] = 1; - } + * Called when field of this type is initialized at boot or after lazy loaded + * + * #pw-internal + * + * @param Field $field + * @since 3.0.194 + * */ - + public function initField(Field $field) { + if(!$this->useLazy) return; + parent::initField($field); + /** @var FieldtypeRepeater $fieldtype */ + $fieldtype = $field->type; + if(!$fieldtype instanceof FieldtypeRepeater) return; + $template = $fieldtype->getRepeaterTemplate($field); + if(!$template) return; + $class = $fieldtype->getPageClass(); + if(__NAMESPACE__ && $class) $class = wireClassName($class); + $_class = $template->get('pageClass'); + if($class === $_class) return; + $template->set('pageClass', $class); + $template->save(); + } + /** * Get class name to use Field objects of this type (must be class that extends Field class) * @@ -305,27 +337,33 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { */ public function hookPageFinderGetQuery(HookEvent $event) { - static $fieldsUsedInRepeaters = null; - static $templatesUsedByRepeaters = array(); - /** @var Selectors $selectors */ $selectors = $event->arguments[0]; /** @var PageFinder $pageFinder */ $pageFinder = $event->object; $pageFinderOptions = $pageFinder->getOptions(); + $instanceNum = $this->wire()->getInstanceNum(); // determine which fields are used in repeaters - if(is_null($fieldsUsedInRepeaters)) { - $fieldsUsedInRepeaters = array('title'); // title used by admin template (repeater parents) - foreach($this->wire()->templates as $template) { - if(strpos($template->name, self::templateNamePrefix) === 0) { - $templatesUsedByRepeaters[] = $template->id; - foreach($template->fieldgroup as $field) { - $fieldsUsedInRepeaters[] = $field->name; - } + if(!isset(self::$fieldsUsedInRepeaters[$instanceNum])) { + $fieldNames = array('title' => 'title'); // title used by admin template (repeater parents) + $templates = $this->wire()->templates; + $templateIds = array(); + $allTemplateNames = $templates->getAllValues('name', 'id'); + $fieldgroups = $this->wire()->fieldgroups; + foreach($allTemplateNames as $templateId => $templateName) { + if(strpos($templateName, self::templateNamePrefix) !== 0) continue; + $templateIds[$templateName] = $templateId; + foreach($fieldgroups->getFieldNames($templateName) as $fieldId => $fieldName) { + $fieldNames[$fieldName] = $fieldName; } } + self::$fieldsUsedInRepeaters[$instanceNum] = array_values($fieldNames); + self::$templatesUsedByRepeaters[$instanceNum] = array_values($templateIds); } + + $fieldsUsedInRepeaters = self::$fieldsUsedInRepeaters[$instanceNum]; + $templatesUsedByRepeaters = self::$templatesUsedByRepeaters[$instanceNum]; // did we find a field used by a repeater in the selector? $found = false; diff --git a/wire/modules/LanguageSupport/LanguageSupport.module b/wire/modules/LanguageSupport/LanguageSupport.module index 1345108dc..18b42e9d9 100644 --- a/wire/modules/LanguageSupport/LanguageSupport.module +++ b/wire/modules/LanguageSupport/LanguageSupport.module @@ -5,7 +5,7 @@ * * This module is the front door to all the other language modules and files. * - * ProcessWire 3.x, Copyright 2016 by Ryan Cramer + * ProcessWire 3.x, Copyright 2022 by Ryan Cramer * https://processwire.com * * @property int $languagesPageID @@ -120,10 +120,16 @@ class LanguageSupport extends WireData implements Module, ConfigurableModule { * */ public function init() { + + $pages = $this->wire()->pages; + $user = $this->wire()->user; + $config = $this->wire()->config; + $templates = $this->wire()->templates; + $modules = $this->wire()->modules; // document which pages were already cached at this point, as their values may need // to be reloaded to account for language fields. - foreach($this->wire('pages')->getCache() as $id => $value) $this->earlyCachedPages[$id] = $value; + foreach($pages->getCache() as $id => $value) $this->earlyCachedPages[$id] = $value; // prevent possible double init if($this->initialized) return; @@ -134,17 +140,18 @@ class LanguageSupport extends WireData implements Module, ConfigurableModule { $defaultLanguagePageID = $this->defaultLanguagePageID; // create the $languages API var - $languageTemplate = $this->templates->get('language'); + $languageTemplate = $templates->get('language'); if(!$languageTemplate) return; if(!$this->languagesPageID) { // fallback if LanguageSupport config lost or not accessible for some reason - $this->languagesPageID = $this->wire('pages')->get("template=admin, name=languages"); + $this->languagesPageID = $pages->get("template=admin, name=languages"); } // prevent fields like 'title' from autojoining until languages are fully loaded - $this->wire('pages')->setAutojoin(false); - + $pages->setAutojoin(false); + + /** @var Languages $languages */ $languages = $this->wire(new Languages($this->wire('wire'), $languageTemplate, $this->languagesPageID)); $_default = null; // just in case @@ -153,7 +160,7 @@ class LanguageSupport extends WireData implements Module, ConfigurableModule { foreach($languages as $language) { if($language->id == $defaultLanguagePageID) { $this->defaultLanguagePage = $language; - } else if($language->name == 'default') { + } else if($language->name === 'default') { $_default = $language; // backup plan } else { $numOtherLanguages++; @@ -169,6 +176,7 @@ class LanguageSupport extends WireData implements Module, ConfigurableModule { $this->defaultLanguagePage = $languages->getAll()->first(); } } + $this->defaultLanguagePage->setIsDefaultLanguage(); $languages->setDefault($this->defaultLanguagePage); @@ -176,14 +184,14 @@ class LanguageSupport extends WireData implements Module, ConfigurableModule { $this->wire('languages', $languages); // identify the current language from the user, or set one if it's not already - if($this->user->language && $this->user->language->id) { + if($user->language && $user->language->id) { // $language = $this->user->language; } else { $language = $this->defaultLanguagePage; - $this->user->language = $language; + $user->language = $language; } - $this->wire('config')->dateFormat = $this->_('Y-m-d H:i:s'); // Sortable date format used in the admin + $config->dateFormat = $this->_('Y-m-d H:i:s'); // Sortable date format used in the admin $locale = $this->_('C'); // Value to pass to PHP's setlocale(LC_ALL, 'value') function when initializing this language // Default is 'C'. Specify '0' to skip the setlocale() call (and carry on system default). Specify CSV string of locales to try multiple locales in order. if($locale != '0') $languages->setLocale(LC_ALL, $locale); @@ -201,10 +209,10 @@ class LanguageSupport extends WireData implements Module, ConfigurableModule { $this->addHook('Page::getLanguageValue', $this, 'hookPageGetLanguageValue'); - if($this->wire('modules')->isInstalled('LanguageSupportFields')) { - $this->LanguageSupportFields = $this->wire('modules')->get('LanguageSupportFields'); + if($modules->isInstalled('LanguageSupportFields')) { + $this->LanguageSupportFields = $modules->get('LanguageSupportFields'); $this->LanguageSupportFields->LS_init(); - if($languages->getPageEditPermissions('none') && !$this->user->hasPermission('page-edit-lang-none')) { + if($languages->getPageEditPermissions('none') && !$user->hasPermission('page-edit-lang-none')) { $this->addHookBefore('InputfieldWrapper::renderInputfield', $this, 'hookInputfieldWrapperBeforeRenderInputfield'); } } @@ -214,7 +222,7 @@ class LanguageSupport extends WireData implements Module, ConfigurableModule { } // restore autojoin state for pages - $this->wire('pages')->setAutojoin(true); + $pages->setAutojoin(true); } /** @@ -222,29 +230,31 @@ class LanguageSupport extends WireData implements Module, ConfigurableModule { * */ public function ready() { + $page = $this->wire()->page; + // styles used by our Inputfield hooks - if($this->wire('page')->template == 'admin') { - $this->config->styles->add($this->config->urls('LanguageSupport') . "LanguageSupport.css"); - $language = $this->wire('user')->language; - if($language) $this->config->js('LanguageSupport', array( + if($page->template->name === 'admin') { + $config = $this->wire()->config; + $config->styles->add($config->urls('LanguageSupport') . "LanguageSupport.css"); + $language = $this->wire()->user->language; + if($language) $config->js('LanguageSupport', array( 'language' => array( 'id' => $language->id, 'name' => $language->name, 'title' => (string) $language->title, - ) + ) )); - if($this->wire('modules')->isInstalled('LanguageTabs')) { - $this->languageTabs = $this->wire('modules')->get('LanguageTabs'); + $modules = $this->wire()->modules; + if($modules->isInstalled('LanguageTabs')) { + $this->languageTabs = $modules->get('LanguageTabs'); } } // if languageSupportFields is here, then we have to deal with pages that loaded before this module did if($this->LanguageSupportFields) { - $fieldNames = array(); // save the names of all fields that support languages - foreach($this->wire('fields') as $field) { - if($field->type instanceof FieldtypeLanguageInterface) $fieldNames[] = $field->name; - } + $fieldNames = $this->LanguageSupportFields->getMultilangFieldNames(); + // unset the values from all the early cached pages since they didn't recognize languages // this will force them to reload when accessed foreach($this->earlyCachedPages as $id => $p) { @@ -254,11 +264,11 @@ class LanguageSupport extends WireData implements Module, ConfigurableModule { if($t) $p->setTrackChanges(true); } } + // release this as we don't need it anymore $this->earlyCachedPages = array(); if($this->LanguageSupportFields) $this->LanguageSupportFields->LS_ready(); - } /** diff --git a/wire/modules/LanguageSupport/LanguageSupportFields.module b/wire/modules/LanguageSupport/LanguageSupportFields.module index 6e10a23d9..03b2643a2 100644 --- a/wire/modules/LanguageSupport/LanguageSupportFields.module +++ b/wire/modules/LanguageSupport/LanguageSupportFields.module @@ -3,7 +3,7 @@ /** * Multi-language support fields module * - * ProcessWire 3.x, Copyright 2021 by Ryan Cramer + * ProcessWire 3.x, Copyright 2022 by Ryan Cramer * https://processwire.com * * @method void languageAdded(Page $language) #pw-hooker @@ -29,13 +29,13 @@ class LanguageSupportFields extends WireData implements Module { 'singular' => true, 'requires' => array( 'LanguageSupport' - ), + ), 'installs' => array( 'FieldtypePageTitleLanguage', 'FieldtypeTextareaLanguage', 'FieldtypeTextLanguage', - ) - ); + ) + ); } /** @@ -57,7 +57,6 @@ class LanguageSupportFields extends WireData implements Module { * */ public function __construct() { - // load other required classes $dirname = dirname(__FILE__); require_once($dirname . '/LanguagesValueInterface.php'); @@ -81,31 +80,32 @@ class LanguageSupportFields extends WireData implements Module { * */ public function LS_init() { + + $fields = $this->wire()->fields; $this->addHookBefore('FieldtypeLanguageInterface::sleepValue', $this, 'fieldtypeSleepValue'); $this->addHookBefore('PageFinder::getQuery', $this, 'pageFinderGetQuery'); $this->addHookBefore('Fieldtype::formatValue', $this, 'hookFieldtypeFormatValue'); $languageNames = array(); - $fieldNames = array(); + $fieldNames = $fields->getAllNames('name'); - foreach($this->wire('languages') as $language) { + foreach($this->wire()->languages as $language) { $languageNames[] = $language->name; } // keep track of which fields are multilanguage - foreach($this->wire('fields') as $field) { - if($field->type instanceof FieldtypeLanguageInterface) { - $this->multilangFields[] = $field->name; - } - $fieldNames[] = $field->name; - } - + $this->multilangFields = $fields->findByType('FieldtypeLanguageInterface', array( + 'inherit' => true, + 'valueType' => 'name', + 'indexType' => '', + )); + // determine which fields have language alternates, i.e. 'title_es' is an alternate for 'title' foreach($fieldNames as $fieldName) { foreach($languageNames as $languageName) { $altName = $fieldName . '_' . $languageName; - if(in_array($altName, $fieldNames)) { + if(isset($fieldNames[$altName])) { if(!isset($this->multilangAlternateFields[$fieldName])) $this->multilangAlternateFields[$fieldName] = array(); $this->multilangAlternateFields[$fieldName][] = $altName; } @@ -118,8 +118,9 @@ class LanguageSupportFields extends WireData implements Module { * */ public function LS_ready() { - $this->languages->addHook('added', $this, 'hookLanguageAdded'); - $this->languages->addHook('deleted', $this, 'hookLanguageDeleted'); + $languages = $this->wire()->languages; + $languages->addHook('added', $this, 'hookLanguageAdded'); + $languages->addHook('deleted', $this, 'hookLanguageDeleted'); } /** @@ -135,9 +136,9 @@ class LanguageSupportFields extends WireData implements Module { /** @var Field $field */ $field = $event->arguments[1]; - if($field->name == 'language') return; + if($field->name === 'language') return; - $language = $this->wire('user')->get('language'); + $language = $this->wire()->user->get('language'); if(!$language || !$language->id || $language->isDefault()) return; // exit quickly if we can determine now we don't need to continue @@ -179,12 +180,13 @@ class LanguageSupportFields extends WireData implements Module { */ public function hookLanguageAdded(HookEvent $event) { + $fields = $this->wire()->fields; $language = $event->arguments[0]; if($language->template->name != LanguageSupport::languageTemplateName) return; foreach($this->multilangFields as $name) { - $field = $this->wire('fields')->get($name); + $field = $fields->get($name); if($field) $this->fieldLanguageAdded($field, $language); } @@ -209,11 +211,12 @@ class LanguageSupportFields extends WireData implements Module { */ public function hookLanguageDeleted(HookEvent $event) { + $fields = $this->wire()->fields; $language = $event->arguments[0]; if($language->template->name != LanguageSupport::languageTemplateName) return; foreach($this->multilangFields as $name) { - $field = $this->wire('fields')->get($name); + $field = $fields->get($name); if($field) $this->fieldLanguageRemoved($field, $language); } @@ -245,7 +248,7 @@ class LanguageSupportFields extends WireData implements Module { if(!($field->type instanceof FieldtypeLanguageInterface)) return; $schema = $field->type->getDatabaseSchema($field); - $database = $this->wire('database'); + $database = $this->wire()->database; $table = $database->escapeTable($field->table); foreach($schema as $name => $value) { @@ -284,7 +287,7 @@ class LanguageSupportFields extends WireData implements Module { if(!($field->type instanceof FieldtypeLanguageInterface)) return; $schema = $field->type->getDatabaseSchema($field); - $database = $this->wire('database'); + $database = $this->wire()->database; $table = $database->escapeTable($field->table); foreach($schema as $name => $value) { @@ -309,9 +312,9 @@ class LanguageSupportFields extends WireData implements Module { */ public function pageFinderGetQuery(HookEvent $event) { - $user = $this->wire('user'); + $user = $this->wire()->user; $language = $user->language; - $database = $this->wire('database'); + $database = $this->wire()->database; if(!$language || !$language->id || $language->isDefault()) return; @@ -373,6 +376,7 @@ class LanguageSupportFields extends WireData implements Module { $value = $event->return; if($value instanceof LanguagesPageFieldValue) return; $v = new LanguagesPageFieldValue($page, $field, $value); + $this->wire($v); $event->return = $v; } @@ -396,6 +400,7 @@ class LanguageSupportFields extends WireData implements Module { // good } else if(is_array($value)) { $value = new LanguagesPageFieldValue($page, $field, $value); + $this->wire($value); $value->setTrackChanges(true); $value->setField($field); $event->return = $value; @@ -421,7 +426,7 @@ class LanguageSupportFields extends WireData implements Module { if(!$value instanceof LanguagesPageFieldValue) return; - foreach($this->wire('languages') as $language) { + foreach($this->wire()->languages as $language) { if($language->isDefault()) $key = 'data'; else $key = 'data' . $language->id; $values[$key] = $value->getLanguageValue($language->id); @@ -444,17 +449,21 @@ class LanguageSupportFields extends WireData implements Module { public function fieldtypeGetConfigInputfields(HookEvent $event) { + /** @var Field $field */ $field = $event->arguments(0); - $inputfields = $event->return; - $f = $this->wire('modules')->get('InputfieldRadios'); + /** @var InputfieldWrapper $inputfields */ + $inputfields = $event->return; + + /** @var InputfieldRadios $f */ + $f = $this->wire()->modules->get('InputfieldRadios'); $f->attr('name', 'langBlankInherit'); $f->label = $this->_('Language Support / Blank Behavior'); $f->description = $this->_("What should happen when this field's value is blank?"); $f->notes = $this->_('Applies only to non-default language values on the front-end of your site.'); $f->addOption(LanguagesPageFieldValue::langBlankInheritDefault, $this->_('Inherit value from default language')); $f->addOption(LanguagesPageFieldValue::langBlankInheritNone, $this->_('Remain blank')); - $f->attr('value', (int) $field->langBlankInherit); + $f->attr('value', (int) $field->get('langBlankInherit')); $f->collapsed = Inputfield::collapsedBlank; $inputfields->add($f); } @@ -469,7 +478,9 @@ class LanguageSupportFields extends WireData implements Module { * */ public function getAlternateFields($fieldName) { - if(isset($this->multilangAlternateFields[$fieldName])) return $this->multilangAlternateFields[$fieldName]; + if(isset($this->multilangAlternateFields[$fieldName])) { + return $this->multilangAlternateFields[$fieldName]; + } return array(); } @@ -485,13 +496,11 @@ class LanguageSupportFields extends WireData implements Module { $pos = strrpos($altFieldName, '_'); if(!$pos) return ''; $parentName = substr($altFieldName, 0, $pos); - // $this->message($parentName); - if(isset($this->multilangAlternateFields[$parentName]) && in_array($altFieldName, $this->multilangAlternateFields[$parentName])) { - if(!$returnLanguage) return $parentName; - $languageName = substr($altFieldName, $pos+1); - return $this->wire('languages')->get($languageName); - } - return ''; + if(!isset($this->multilangAlternateFields[$parentName])) return ''; + if(!in_array($altFieldName, $this->multilangAlternateFields[$parentName])) return ''; + if(!$returnLanguage) return $parentName; + $languageName = substr($altFieldName, $pos+1); + return $this->wire()->languages->get($languageName); } /** @@ -505,6 +514,17 @@ class LanguageSupportFields extends WireData implements Module { return $this->getAlternateFieldParent($altFieldName, true); } + /** + * Get multi-language field names (fields that implement FieldtypeLanguageInterface) + * + * @return array + * @since 3.0.194 + * + */ + public function getMultilangFieldNames() { + return $this->multilangFields; + } + /** * Is the given field name a language alternate field? * @@ -544,10 +564,12 @@ class LanguageSupportFields extends WireData implements Module { public function ___uninstall() { // first check if there are any fields using the LanguageInterface $errors = ''; - foreach($this->wire('fields') as $field) { + foreach($this->wire()->fields as $field) { if($field->type instanceof FieldtypeLanguageInterface) $errors .= $field->name . ", "; } - if($errors) throw new WireException("Can't uninstall because these fields use the language interface: " . rtrim($errors, ", ")); + if($errors) { + throw new WireException("Can't uninstall because these fields use the language interface: " . rtrim($errors, ", ")); + } } } diff --git a/wire/templates-admin/debug.inc b/wire/templates-admin/debug.inc index 2034e8b91..2f18b0ecf 100644 --- a/wire/templates-admin/debug.inc +++ b/wire/templates-admin/debug.inc @@ -1,5 +1,6 @@ - + useLazyLoading): ?> + ClassLoadedNot Loaded"; + $numTotal = 0; + $numLoadedTotal = 0; + $numNotLoadedTotal = 0; + foreach(array('fields', 'templates', 'fieldgroups') as $key) { + /** @var WireSaveableItems $var */ + $var = $wire->$key; + if(empty($var)) continue; + $debugInfo = $var->__debugInfo(); + $numLoaded = count($debugInfo['loaded']); + $numNotLoaded = count($debugInfo['notLoaded']); + $numEither = $numLoaded + $numNotLoaded; + $numTotal += $numEither; + $numLoadedTotal += $numLoaded; + $numNotLoadedTotal += $numNotLoaded; + sort($debugInfo['loaded']); + sort($debugInfo['notLoaded']); + $o .= + "" . + "" . $var->className() . " ($numLoaded/$numNotLoaded/$numEither)" . + "" . implode("
", $debugInfo['loaded']) . "" . + "" . implode("
", $debugInfo['notLoaded']) . "" . + ""; + } + $o .= "
"; + ?> + + + +