Skip to content

Commit

Permalink
cleaning up eager loading
Browse files Browse the repository at this point in the history
  • Loading branch information
markhuot committed Nov 5, 2023
1 parent 5d8eb91 commit 250aea4
Show file tree
Hide file tree
Showing 10 changed files with 97 additions and 46 deletions.
3 changes: 3 additions & 0 deletions src/Keystone.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
{
Expand All @@ -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],
Expand Down
37 changes: 37 additions & 0 deletions src/actions/EagerLoadComponents.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace markhuot\keystone\actions;

use craft\elements\Asset;
use craft\fields\Assets;
use Illuminate\Support\Collection;
use markhuot\keystone\interfaces\ShouldHandleEvents;

class EagerLoadComponents implements ShouldHandleEvents
{
public function handle(Collection $components)
{
$assetIds = [];

foreach ($components 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 ($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);
}
}
}
}
}
11 changes: 11 additions & 0 deletions src/events/AfterPopulateTree.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace markhuot\keystone\events;

use craft\base\Event;
use Illuminate\Support\Collection;

class AfterPopulateTree extends Event
{
public Collection $components;
}
12 changes: 2 additions & 10 deletions src/fields/Keystone.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,11 @@ public static function hasContentColumn(): bool
*/
protected function getFragment(ElementInterface $element)
{
$childrenQuery = Component::find()
->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;
}
Expand Down
17 changes: 16 additions & 1 deletion src/helpers/event.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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;
Expand Down
7 changes: 7 additions & 0 deletions src/interfaces/ShouldHandleEvents.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace markhuot\keystone\interfaces;

interface ShouldHandleEvents
{
}
36 changes: 10 additions & 26 deletions src/models/Component.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
use craft\base\ElementInterface;
use craft\base\FieldInterface;
use craft\db\ActiveQuery;
use craft\elements\Asset;
use craft\fields\Assets;
use Illuminate\Support\Collection;
use markhuot\keystone\actions\GetComponentType;
use markhuot\keystone\actions\NormalizeFieldDataForComponent;
use markhuot\keystone\base\AttributeBag;
Expand All @@ -15,6 +14,8 @@
use markhuot\keystone\collections\SlotCollection;
use markhuot\keystone\db\ActiveRecord;
use markhuot\keystone\db\Table;
use markhuot\keystone\events\AfterPopulateTree;
use yii\base\Event;

use function markhuot\keystone\helpers\base\app;
use function markhuot\keystone\helpers\base\throw_if;
Expand All @@ -31,6 +32,8 @@
*/
class Component extends ActiveRecord
{
const AFTER_POPULATE_TREE = 'afterPopulateTree';

/** @var array<Component> */
protected ?array $slotted = null;

Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -310,6 +293,7 @@ public function getSlot(string $name = null): SlotCollection
->orderBy('sortOrder')
->collect();

$this->afterPopulateTree($components);
$this->setSlotted($components->all());
} else {
$components = collect();
Expand Down
10 changes: 5 additions & 5 deletions src/models/ComponentData.php
Original file line number Diff line number Diff line change
Expand Up @@ -116,30 +116,30 @@ 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);
}

$this->accessed[$offset] = (new FieldDefinition)->handle($offset);

return $this->getData()[$offset] ?? null;
return $this->getData()[$offset] ?? $default;
}

public function offsetGet(mixed $offset): mixed
Expand Down
2 changes: 1 addition & 1 deletion src/templates/edit.twig
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}
</div>
Expand Down
8 changes: 5 additions & 3 deletions tests/ComponentDataTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down

0 comments on commit 250aea4

Please sign in to comment.