From 81f6766212b67fb20b8e55e64ae8f43479af8c92 Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Sun, 10 Dec 2023 04:34:38 -0800 Subject: [PATCH] Defer field instantiation until actually needed Resolves #13992 --- CHANGELOG.md | 1 + src/services/Fields.php | 77 ++++++++++++++++++++++++++++------------- 2 files changed, 53 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31cc3e43ff8..48206b7eb13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased - It’s no longer possible to dismiss asset conflict resolution modals by pressing Esc or clicking outside of the modal. ([#14002](https://github.com/craftcms/cms/issues/14002)) +- Improved performance for sites with lots of custom fields in non-global contexts. ([#13992](https://github.com/craftcms/cms/issues/13992)) - Added `craft\db\Connection::onAfterTransaction()`. - Added `craft\fieldlayoutelements\TextField::$inputType`. ([#13988](https://github.com/craftcms/cms/issues/13988)) - Deprecated `craft\fieldlayoutelements\TextField::$type`. `$inputType` should be used instead. ([#13988](https://github.com/craftcms/cms/issues/13988)) diff --git a/src/services/Fields.php b/src/services/Fields.php index b9ae373791e..d7d8bcc6d14 100644 --- a/src/services/Fields.php +++ b/src/services/Fields.php @@ -196,10 +196,16 @@ class Fields extends Component private ?MemoizableArray $_groups = null; /** - * @var MemoizableArray|null - * @see _fields() + * @var MemoizableArray|null + * @see _fieldConfigs() */ - private ?MemoizableArray $_fields = null; + private ?MemoizableArray $_fieldConfigs = null; + + /** + * @var FieldInterface[] + * @see _field() + */ + private array $_fields = []; /** * @var FieldLayout[]|null[] @@ -620,36 +626,55 @@ public function createField(mixed $config): FieldInterface } /** - * Returns a memoizable array of all fields. + * Returns a memoizable array of field configs. * * @param string|string[]|false|null $context The field context(s) to fetch fields from. Defaults to [[\craft\services\Content::$fieldContext]]. * Set to `false` to get all fields regardless of context. * - * @return MemoizableArray + * @return MemoizableArray */ - private function _fields(mixed $context = null): MemoizableArray + private function _fieldConfigs(mixed $context = null): MemoizableArray { - if (!isset($this->_fields)) { - $fields = []; - foreach ($this->_createFieldQuery()->all() as $result) { - $fields[] = $this->createField($result); - } - $this->_fields = new MemoizableArray($fields); + $context ??= Craft::$app->getContent()->fieldContext; + + if (!isset($this->_fieldConfigs)) { + $this->_fieldConfigs = new MemoizableArray($this->_createFieldQuery()->all()); } if ($context === false) { - return $this->_fields; + return $this->_fieldConfigs; } - if ($context === null) { - $context = Craft::$app->getContent()->fieldContext; + if (is_array($context)) { + return $this->_fieldConfigs->whereIn('context', $context, true); } - if (is_array($context)) { - return $this->_fields->whereIn('context', $context, true); + return $this->_fieldConfigs->where('context', $context, true); + } + + /** + * @param array[] $configs + * @return FieldInterface[] + */ + private function _fields(array $configs): array + { + return array_map(fn(array $config) => $this->_field($config), $configs); + } + + /** + * @param array|null $config + * @return FieldInterface|null + */ + private function _field(?array $config): ?FieldInterface + { + if ($config === null) { + return null; } - return $this->_fields->where('context', $context, true); + if (!isset($this->_fields[$config['id']])) { + $this->_fields[$config['id']] = $this->createField($config); + } + return $this->_fields[$config['id']]; } /** @@ -661,7 +686,7 @@ private function _fields(mixed $context = null): MemoizableArray */ public function getAllFields(mixed $context = null): array { - return $this->_fields($context)->all(); + return $this->_fields($this->_fieldConfigs($context)->all()); } /** @@ -720,7 +745,7 @@ public function getFieldsByType(string $type, mixed $context = null): array */ public function getFieldById(int $fieldId): ?FieldInterface { - return $this->_fields(false)->firstWhere('id', $fieldId); + return $this->_field($this->_fieldConfigs(false)->firstWhere('id', $fieldId)); } /** @@ -731,7 +756,7 @@ public function getFieldById(int $fieldId): ?FieldInterface */ public function getFieldByUid(string $fieldUid): ?FieldInterface { - return $this->_fields(false)->firstWhere('uid', $fieldUid, true); + return $this->_field($this->_fieldConfigs(false)->firstWhere('uid', $fieldUid, true)); } /** @@ -754,7 +779,7 @@ public function getFieldByUid(string $fieldUid): ?FieldInterface */ public function getFieldByHandle(string $handle, mixed $context = null): ?FieldInterface { - return $this->_fields($context)->firstWhere('handle', $handle, true); + return $this->_field($this->_fieldConfigs($context)->firstWhere('handle', $handle, true)); } /** @@ -777,7 +802,7 @@ public function doesFieldWithHandleExist(string $handle, ?string $context = null */ public function getFieldsByGroupId(int $groupId): array { - return $this->_fields(false)->where('groupId', $groupId)->all(); + return $this->_fields($this->_fieldConfigs(false)->where('groupId', $groupId)->all()); } /** @@ -1028,7 +1053,8 @@ public function applyFieldDelete(string $fieldUid): void } // Clear caches - $this->_fields = null; + $this->_fieldConfigs = null; + $this->_fields = []; // Update the field version $this->updateFieldVersion(); @@ -1089,7 +1115,8 @@ private function _dropOldFieldColumns(string $handle, ?string $columnSuffix, arr */ public function refreshFields(): void { - $this->_fields = null; + $this->_fieldConfigs = null; + $this->_fields = []; $this->updateFieldVersion(); }