Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

eager loading assets #10

Merged
merged 24 commits into from
Nov 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1 +1,27 @@
# Read about configuration, here:
# https://craftcms.com/docs/4.x/config/

# The application ID used to to uniquely store session and cache data, mutex locks, and more
CRAFT_APP_ID=

# The environment Craft is currently running in (dev, staging, production, etc.)
CRAFT_ENVIRONMENT=dev

# The secure key Craft will use for hashing and encrypting data
CRAFT_SECURITY_KEY=

# Database connection settings
CRAFT_DB_DRIVER=mysql
CRAFT_DB_SERVER=127.0.0.1
CRAFT_DB_PORT=3306
CRAFT_DB_DATABASE=
CRAFT_DB_USER=root
CRAFT_DB_PASSWORD=
CRAFT_DB_SCHEMA=public
CRAFT_DB_TABLE_PREFIX=

# General settings (see config/general.php)
DEV_MODE=true
ALLOW_ADMIN_CHANGES=true
DISALLOW_ROBOTS=true
CRAFT_RUN_QUEUE_AUTOMATICALLY=false
4 changes: 0 additions & 4 deletions .github/workflows/php.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ jobs:
with:
path: vendor
key: ${{ runner.os }}-php-${{ hashFiles('**/composer.json') }}
restore-keys: |
${{ runner.os }}-php-
- name: Install PHP dependencies
run: composer install --prefer-dist --no-progress --no-ansi
if: steps.composer-cache.outputs.cache-hit != 'true'
Expand Down Expand Up @@ -134,8 +132,6 @@ jobs:
with:
path: vendor
key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-php-
- name: Setup
run: ./bin/post-clone.sh
- name: Remove Craft DB from .env
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@
.idea
.phpunit.result.cache
/node_modules
/volumes
1 change: 0 additions & 1 deletion .run/Pest.run.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Pest" type="PestRunConfigurationType">
<CommandLine parameters="-d pcov.enabled=0" />
<option name="pestRunnerSettings">
<PestRunner directory="$PROJECT_DIR$/tests" method="" />
</option>
Expand Down
5 changes: 3 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"minimum-stability": "dev",
"autoload": {
"psr-4": {
"markhuot\\keystone\\": "src/"
"markhuot\\keystone\\": "src/",
"markhuot\\keystone\\tests\\": "tests/"
},
"files": [
"src/helpers/event.php",
Expand All @@ -21,7 +22,7 @@
}
],
"require-dev": {
"markhuot/craft-pest-core": "dev-sequence",
"markhuot/craft-pest-core": "dev-main",
"phpstan/phpstan": "^1.10",
"laravel/pint": "^1.13",
"craftcms/craft": "dev-main"
Expand Down
8 changes: 1 addition & 7 deletions phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,9 @@
<php>
<env name="CRAFT_TEMPLATES_PATH" value="./tests/templates/" />
</php>
<coverage>
<include>
<directory suffix=".php">./src</directory>
</include>
</coverage>
<source>
<include>
<directory suffix=".php">./modules</directory>
<directory suffix=".php">./templates</directory>
<directory suffix=".php">./src</directory>
</include>
</source>
</phpunit>
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,10 +24,11 @@
use markhuot\keystone\listeners\RegisterDefaultComponentTypes;
use markhuot\keystone\listeners\RegisterKeystoneFieldType;
use markhuot\keystone\listeners\RegisterTwigExtensions;
use markhuot\keystone\models\Component;

class Keystone extends Plugin
{
protected function getListeners(): array

Check failure on line 31 in src/Keystone.php

View workflow job for this annotation

GitHub Actions / Run PHPStan

Method markhuot\keystone\Keystone::getListeners() return type has no value type specified in iterable type array.
{
return [
[WebApplication::class, WebApplication::EVENT_BEFORE_REQUEST, AttachPerRequestBehaviors::class],
Expand All @@ -38,6 +40,7 @@
[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
2 changes: 1 addition & 1 deletion src/actions/CompileTwigComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public function handle($force = false)
$compiledClassesPath = rtrim(Craft::$app->getPath()->getCompiledClassesPath(), '/').'/';
$hash = sha1($this->handle);
$className = 'ComponentType'.$hash.$filemtime;
$fqcn = '\\keystone\\cache\\'.$className;
$fqcn = 'keystone\\cache\\'.$className;

// Bail early if the cache already exists
if (! $force && file_exists($compiledClassesPath.$className.'.php')) {
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) {
$componentData = collect($component->data->getRaw($field->handle) ?? [])
->map(fn ($id) => $assets->get($id))
->filter();
$component->data->populateRelation($field->handle, $componentData);
}
}
}
}
}
35 changes: 24 additions & 11 deletions src/actions/GetComponentType.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace markhuot\keystone\actions;

use Craft;
use craft\base\Event;
use Illuminate\Support\Collection;
use markhuot\keystone\base\ComponentType;
Expand All @@ -12,36 +13,48 @@ class GetComponentType
{
const EVENT_REGISTER_COMPONENT_TYPES = 'registerKeystoneComponentTypes';

protected static ?RegisterComponentTypes $_types = null;

public function __construct(
protected ?Component $context = null
) {
}

protected function getTypes()
{
if (static::$_types !== null) {
return static::$_types;
}

$event = new RegisterComponentTypes;
Event::trigger(static::class, static::EVENT_REGISTER_COMPONENT_TYPES, $event);

return static::$_types = $event;
}

/**
* @return Collection<ComponentType>
*/
public function all(): Collection
{
$event = new RegisterComponentTypes;
Event::trigger(static::class, static::EVENT_REGISTER_COMPONENT_TYPES, $event);

return $event->getTwigComponents()
return $this->getTypes()->getTwigComponents()
->mapInto(CompileTwigComponent::class)->map->handle()
->merge($event->getClassComponents())
->merge($this->getTypes()->getClassComponents())
->map(fn ($className) => new $className($this->context));
}

public function byType(string $type): ComponentType
{
$event = new RegisterComponentTypes;
Event::trigger(static::class, static::EVENT_REGISTER_COMPONENT_TYPES, $event);
if ($twigPath = $this->getTypes()->getTwigComponents()->get($type)) {
$fqcn = (new CompileTwigComponent($twigPath, $type))->handle();
}

if ($twigPath = $event->getTwigComponents()->get($type)) {
return new ((new CompileTwigComponent($twigPath, $type))->handle())($this->context);
if ($className = $this->getTypes()->getClassComponents()->get($type)) {
$fqcn = $className;
}

if ($className = $event->getClassComponents()->get($type)) {
return new $className($this->context);
if ($fqcn) {
return Craft::$container->get($fqcn, ['context' => $this->context]);
}

throw new \RuntimeException('Could not find a component type definition for '.$type);
Expand Down
18 changes: 17 additions & 1 deletion src/actions/MakeModelFromArray.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,14 @@ protected function load(\yii\base\Model $model, mixed $data): void

$reflect = new \ReflectionClass($model);

foreach ($data as $key => &$value) {
foreach ($data as $key => $value) {
// If we were passed something like elementId over the wire, but want to deal with
// a property of `->element` then check for that here and swap the keys out.
if (str_ends_with($key, 'Id') && ! $reflect->hasProperty($key)) {
unset($data[$key]);
$key = substr($key, 0, -2);
}

if ($reflect->hasProperty($key)) {
$property = $reflect->getProperty($key);
$type = $property->getType()->getName();
Expand All @@ -107,8 +114,17 @@ className: $type,
);
}
}

$data[$key] = $value;
}

// We can't set a non-null property to null, or it would error out here during the load
// phase instead of during the validation phase. So, if a value is null but the model doesn't
// support that remove the null value from the data array. This will leave the value unset
// in the model and it will later fail validation.
//
// Basically, we're punting null errors further down in the processing so it can catch during
// validation and not throw a runtime error here during load.
$reflect = new \ReflectionClass($model);
foreach ($reflect->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) {
if (! $property->getType()?->allowsNull() && ($data[$property->getName()] ?? null) === null) {
Expand Down
32 changes: 10 additions & 22 deletions src/actions/MoveComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,28 +40,16 @@ public function handleAboveOrBelow(Component $source, Component $target, MoveCom
$target->refresh();

// make room for the insertion
if ($position === MoveComponentPosition::BEFORE) {
Component::updateAll([
'sortOrder' => new Expression('sortOrder + 1'),
], ['and',
['=', 'elementId', $target->elementId],
['=', 'fieldId', $target->fieldId],
['slot' => $target->slot],
['path' => $target->path],
['>=', 'sortOrder', $target->sortOrder],
]);
}
if ($position === MoveComponentPosition::AFTER) {
Component::updateAll([
'sortOrder' => new Expression('sortOrder + 1'),
], ['and',
['=', 'elementId', $target->elementId],
['=', 'fieldId', $target->fieldId],
['slot' => $target->slot],
['path' => $target->path],
['>', 'sortOrder', $target->sortOrder],
]);
}
$operator = ($position === MoveComponentPosition::BEFORE) ? '>=' : '>';
Component::updateAll([
'sortOrder' => new Expression('sortOrder + 1'),
], ['and',
['=', 'elementId', $target->elementId],
['=', 'fieldId', $target->fieldId],
['slot' => $target->slot],
['path' => $target->path],
[$operator, 'sortOrder', $target->sortOrder],
]);

// Refresh the target again, in case it changed, so we're setting the correct
// sort order
Expand Down
10 changes: 5 additions & 5 deletions src/actions/NormalizeFieldDataForComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ public function handle(mixed $value, string $handle)
}

// If the field is editable, return an editable div
if ($field?->getBehavior(AttachFieldBehavior::INTERACTS_WITH_KEYSTONE)) {
if ($field->isEditableInLivePreview() && Craft::$app->getRequest()->getQueryParam('x-craft-live-preview') !== null) {
return new InlineEditData($this->component, $field, $value);
}
}
// if ($field?->getBehavior(AttachFieldBehavior::INTERACTS_WITH_KEYSTONE)) {
// if ($field->isEditableInLivePreview() && Craft::$app->getRequest()->getQueryParam('x-craft-live-preview') !== null) {
// return new InlineEditData($this->component, $field, $value);
// }
// }

return $value;
}
Expand Down
4 changes: 4 additions & 0 deletions src/attributes/Custom.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ public function getInputHtml(): string

public function toAttributeArray(): array
{
if ($this->value === null) {
return [];
}

$className = \Craft::$app->getView()->registerCssRule($this->value);

return ['class' => $className];
Expand Down
16 changes: 8 additions & 8 deletions src/base/ComponentType.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ abstract class ComponentType

protected array $_accessedSlots = [];

protected ?array $_schema = null;
protected static ?array $_schema = null;

public function __construct(
protected ?Component $context = null
Expand Down Expand Up @@ -130,7 +130,7 @@ public function getExports($dumb = false): array
'exports' => $exports = new Exports,
]);

$exports = ['exports' => $exports->exports, 'props' => $props];
$exports = ['exports' => $exports, 'props' => $props];

if ($dumb) {
return $exports;
Expand All @@ -141,30 +141,30 @@ public function getExports($dumb = false): array

public function getExport(string $name, mixed $default = null)
{
return $this->getExports()['exports'][$name] ?? $default;
return $this->getExports()['exports']->get($name) ?? $default;
}

public function defineSlot(string $slotName = null): SlotDefinition
{
return $this->_accessedSlots[$slotName] ??= new SlotDefinition($this->context, $slotName);
}

protected function getSchema(): array
public function getSchema(): array
{
if ($this->_schema !== null) {
return $this->_schema;
if (static::$_schema !== null) {
return static::$_schema;
}

['exports' => $exports, 'props' => $props] = $this->getExports(true);

$slotDefinitions = collect($this->_accessedSlots);

$exportedFieldDefinitions = collect($exports['propTypes'] ?? [])
$exportedFieldDefinitions = collect($exports->get('propTypes', []))
->map(fn (FieldDefinition $defn, string $key) => $defn->handle($key));

$fieldDefinitions = $props->getAccessed()
->merge($exportedFieldDefinitions);

return $this->_schema = [$fieldDefinitions, $slotDefinitions];
return static::$_schema = [$fieldDefinitions, $slotDefinitions];
}
}
Loading
Loading