From 250aea41febf9d388c8167b7d9b6e73160b1b8a2 Mon Sep 17 00:00:00 2001 From: Mark Huot Date: Sun, 5 Nov 2023 08:27:35 +0000 Subject: [PATCH] cleaning up eager loading --- src/Keystone.php | 3 +++ src/actions/EagerLoadComponents.php | 37 +++++++++++++++++++++++++++ src/events/AfterPopulateTree.php | 11 ++++++++ src/fields/Keystone.php | 12 ++------- src/helpers/event.php | 17 +++++++++++- src/interfaces/ShouldHandleEvents.php | 7 +++++ src/models/Component.php | 36 ++++++++------------------ src/models/ComponentData.php | 10 ++++---- src/templates/edit.twig | 2 +- tests/ComponentDataTest.php | 8 +++--- 10 files changed, 97 insertions(+), 46 deletions(-) create mode 100644 src/actions/EagerLoadComponents.php create mode 100644 src/events/AfterPopulateTree.php create mode 100644 src/interfaces/ShouldHandleEvents.php diff --git a/src/Keystone.php b/src/Keystone.php index b974fae..409c605 100644 --- a/src/Keystone.php +++ b/src/Keystone.php @@ -8,6 +8,7 @@ use craft\services\Fields; use craft\web\Application as WebApplication; use craft\web\UrlManager; +use markhuot\keystone\actions\EagerLoadComponents; use markhuot\keystone\actions\GetAttributeTypes; use markhuot\keystone\actions\GetComponentType; use markhuot\keystone\base\Plugin; @@ -23,6 +24,7 @@ use markhuot\keystone\listeners\RegisterDefaultComponentTypes; use markhuot\keystone\listeners\RegisterKeystoneFieldType; use markhuot\keystone\listeners\RegisterTwigExtensions; +use markhuot\keystone\models\Component; class Keystone extends Plugin { @@ -38,6 +40,7 @@ protected function getListeners(): array [Element::class, Element::EVENT_DEFINE_BEHAVIORS, AttachElementBehaviors::class], [PlainText::class, PlainText::EVENT_DEFINE_BEHAVIORS, AttachFieldBehavior::class], [Query::class, Query::EVENT_DEFINE_BEHAVIORS, AttachQueryBehaviors::class], + [Component::class, Component::AFTER_POPULATE_TREE, EagerLoadComponents::class], [Plugin::class, Plugin::EVENT_INIT, MarkClassesSafeForTwig::class], [Plugin::class, Plugin::EVENT_INIT, RegisterTwigExtensions::class], [Plugin::class, Plugin::EVENT_INIT, RegisterCollectionMacros::class], diff --git a/src/actions/EagerLoadComponents.php b/src/actions/EagerLoadComponents.php new file mode 100644 index 0000000..cb2e53a --- /dev/null +++ b/src/actions/EagerLoadComponents.php @@ -0,0 +1,37 @@ +getType()->getFieldDefinitions() as $field) { + if ($field->className === Assets::class) { + $assetIds = array_merge($assetIds, $component->data->getRaw($field->handle) ?? []); + } + } + } + + $assets = Asset::find()->id($assetIds)->collect()->keyBy('id'); + + foreach ($components as $component) { + foreach ($component->getType()->getFieldDefinitions() as $field) { + if ($field->className === Assets::class) { + $assets = collect($component->data->getRaw($field->handle) ?? []) + ->map(fn ($id) => $assets->get($id)) + ->filter(); + $component->data->populateRelation($field->handle, $assets); + } + } + } + } +} diff --git a/src/events/AfterPopulateTree.php b/src/events/AfterPopulateTree.php new file mode 100644 index 0000000..2bbfff5 --- /dev/null +++ b/src/events/AfterPopulateTree.php @@ -0,0 +1,11 @@ +with('data') - ->where([ - 'elementId' => $element->id, - 'fieldId' => $this->id, - ]) - ->orderBy('sortOrder'); - $children = $childrenQuery->all(); - $component = new Component; + $component->fieldId = $this->id; + $component->elementId = $element?->id; $component->populateRelation('data', new ComponentData); $component->data->type = 'keystone/fragment'; - $component->setSlotted($children); return $component; } diff --git a/src/helpers/event.php b/src/helpers/event.php index 8cabc6a..ab2560b 100644 --- a/src/helpers/event.php +++ b/src/helpers/event.php @@ -3,6 +3,9 @@ namespace markhuot\keystone\helpers\event; use markhuot\craftai\listeners\ListenerInterface; +use markhuot\keystone\interfaces\ShouldHandleEvents; +use ReflectionClass; +use ReflectionParameter; use yii\base\Event; /** @@ -26,7 +29,19 @@ function listen(...$events): void $handler->init(); } - Event::on($class, $event, fn (...$args) => \Craft::$container->invoke($handler->handle(...), $args)); + Event::on($class, $event, function (...$args) use ($handler) { + $reflect = new ReflectionClass($handler); + if ($reflect->implementsInterface(ShouldHandleEvents::class)) { + $method = $reflect->getMethod('handle'); + $args = collect($method->getParameters()) + ->map(fn (ReflectionParameter $param) => $args[0]->{$param->getName()} ?? null) + ->filter() + ->all(); + + } + + return \Craft::$container->invoke($handler->handle(...), $args); + }); } catch (\Throwable $e) { if (preg_match('/Class ".+" not found/', $e->getMessage())) { continue; diff --git a/src/interfaces/ShouldHandleEvents.php b/src/interfaces/ShouldHandleEvents.php new file mode 100644 index 0000000..116fecb --- /dev/null +++ b/src/interfaces/ShouldHandleEvents.php @@ -0,0 +1,7 @@ + */ protected ?array $slotted = null; @@ -136,35 +139,15 @@ public function setSlotted(array $components): self { $this->slotted = $components; - $this->eagerLoadRelations(); - return $this; } - public function eagerLoadRelations() + public function afterPopulateTree(Collection $components) { - $assetIds = []; + $event = new AfterPopulateTree; + $event->components = $components; - foreach ($this->slotted as $component) { - foreach ($component->getType()->getFieldDefinitions() as $field) { - if ($field->className === Assets::class) { - $assetIds = array_merge($assetIds, $component->data->getRaw($field->handle) ?? []); - } - } - } - - $assets = Asset::find()->id($assetIds)->collect()->keyBy('id'); - - foreach ($this->slotted as $component) { - foreach ($component->getType()->getFieldDefinitions() as $field) { - if ($field->className === Assets::class) { - $assets = collect($component->data->getRaw($field->handle) ?? []) - ->map(fn ($id) => $assets->get($id)) - ->filter(); - $component->data->populateRelation($field->handle, $assets); - } - } - } + Event::trigger(self::class, self::AFTER_POPULATE_TREE, $event); } /** @@ -310,6 +293,7 @@ public function getSlot(string $name = null): SlotCollection ->orderBy('sortOrder') ->collect(); + $this->afterPopulateTree($components); $this->setSlotted($components->all()); } else { $components = collect(); diff --git a/src/models/ComponentData.php b/src/models/ComponentData.php index cd40057..3c5a977 100644 --- a/src/models/ComponentData.php +++ b/src/models/ComponentData.php @@ -116,22 +116,22 @@ public function offsetExists(mixed $offset): bool return true; } - public function get(mixed $offset, bool $raw = false): mixed + public function get(mixed $offset, mixed $default = null): mixed { if ($this->isRelationPopulated($offset)) { return $this->getRelatedRecords()[$offset]; } - $value = $this->getRaw($offset); + $value = $this->getRaw($offset, $default); - if ($raw === false && $this->normalizer) { + if ($this->normalizer) { return ($this->normalizer)($value, $offset); } return $value; } - public function getRaw(string $offset) + public function getRaw(string $offset, mixed $default = null) { if ($this->hasAttribute($offset)) { return $this->getAttribute($offset); @@ -139,7 +139,7 @@ public function getRaw(string $offset) $this->accessed[$offset] = (new FieldDefinition)->handle($offset); - return $this->getData()[$offset] ?? null; + return $this->getData()[$offset] ?? $default; } public function offsetGet(mixed $offset): mixed diff --git a/src/templates/edit.twig b/src/templates/edit.twig index 8dd63cf..e76ed9f 100644 --- a/src/templates/edit.twig +++ b/src/templates/edit.twig @@ -14,7 +14,7 @@ {{ renderField({ id: field.handle, label: field.name, - }, field.getInputHtml(field.normalizeValue(component.data.get(field.handle, true)))) }} + }, field.getInputHtml(field.normalizeValue(component.data.getRaw(field.handle)))) }} {% endfor %} {% endnamespace %} diff --git a/tests/ComponentDataTest.php b/tests/ComponentDataTest.php index 6fd11f7..97901c2 100644 --- a/tests/ComponentDataTest.php +++ b/tests/ComponentDataTest.php @@ -9,18 +9,20 @@ it('loads component data', function () { $entry = Entry::factory()->section('pages')->create(); - $field = Component::factory() + $components = Component::factory() ->elementId($entry->id) ->type('keystone/text') ->count(3) ->create() - ->first()->getField(); + ->each(fn ($c) => $c->data->merge(['text' => 'foo'])->save()); + $field = $components->first()->getField(); $entry = $entry->refresh(); $this->beginBenchmark(); $fragment = $entry->{$field->handle}; // load each of the data relations to make sure we don't incur an N+1 query - $fragment->getSlot()->map(fn ($c) => $c->data); + $data = $fragment->getSlot()->map(fn ($c) => $c->data->get('text')); + expect($data->all())->toMatchArray(['foo', 'foo', 'foo']); $this->endBenchmark()->assertQueryCount(2/* one for the components and one for the data */); expect($fragment->getSlot())