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

[5.x] Augmentation performance improvements #9636

Merged
merged 45 commits into from
May 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
fe2c56f
Makes method-backed augmented values lazy/deferrable
JohnathonKoster Feb 22, 2024
1df8569
A tiny, but not nothing difference
JohnathonKoster Feb 22, 2024
af03d7e
Merge branch '4.x' into deferred-method-augmentation
JohnathonKoster Feb 22, 2024
d220d16
🧹
JohnathonKoster Feb 23, 2024
9cf6cd1
Reduce calls to blueprintFields
JohnathonKoster Feb 26, 2024
20c376a
Use deferred augmentation in more places internally
JohnathonKoster Feb 28, 2024
e7eafa9
Delay the resolution of regular values
JohnathonKoster Mar 2, 2024
7b7cfa4
Refactors shared logic across deferred values
JohnathonKoster Mar 2, 2024
b10b6c5
Need to account for relationships
JohnathonKoster Mar 2, 2024
ee66acd
Adds transient values
JohnathonKoster Mar 2, 2024
eedbb88
Refactor select to allow for known fields to be supplied
JohnathonKoster Mar 2, 2024
86d22d3
Adds the Augmentor 50000; uses it inside Antlers augmentation
JohnathonKoster Mar 2, 2024
24d7878
Some key adjustments
JohnathonKoster Mar 2, 2024
477541a
Update nav tag to use bulk augmentor
JohnathonKoster Mar 2, 2024
2ede1a2
Restore a familiar experience to dump, dd, and its friends
JohnathonKoster Mar 2, 2024
f5eee7c
Ensure fieldtype materializes values
JohnathonKoster Mar 3, 2024
9c67cf1
Merge branch 'master' into deferred-all-augmentation
jasonvarga Mar 12, 2024
41aa519
Merge branch 'master' into deferred-all-augmentation
JohnathonKoster Mar 13, 2024
a529524
Merge branch 'master' into fork/deferred-all-augmentation
jasonvarga Apr 17, 2024
f07e257
Merge branch 'master' into deferred-all-augmentation
jasonvarga Apr 30, 2024
c576286
Merge branch 'master' into deferred-all-augmentation
jasonvarga Apr 30, 2024
8b35960
Some nitpicks that help me understand it all a bit better, plus just …
jasonvarga May 1, 2024
6379697
Refactor out the trait by putting resolves into Value
jasonvarga May 1, 2024
018fde1
Just always materialize values. It'll know not to bother doing anythi…
jasonvarga May 1, 2024
1f989dd
Clear this up
jasonvarga May 1, 2024
bcaa0f4
Remove the 3 Value class inheritors by using a closure based solution…
jasonvarga May 1, 2024
015368c
oops
jasonvarga May 1, 2024
8dd6b79
isSelecting is never used since transient values were introduced
jasonvarga May 1, 2024
828e3a5
null coalesce
jasonvarga May 1, 2024
b1ec703
Get rid of materialize and toValue ...
jasonvarga May 1, 2024
1ec54a1
refactor to single fieldtype method
jasonvarga May 1, 2024
3509be9
Avoid passing around fields argument
jasonvarga May 1, 2024
81a8547
docblock for clickthrough fun
jasonvarga May 1, 2024
e3ad099
undo some viz changes
jasonvarga May 1, 2024
d85fac3
not that
jasonvarga May 1, 2024
2811e9c
Get augmented once
jasonvarga May 1, 2024
9fb80bd
avoid passing around a fieldtype
jasonvarga May 1, 2024
db30f96
Nitpicks ...
jasonvarga May 2, 2024
37f13c1
rename
jasonvarga May 2, 2024
5c4e66d
resolve when serializing (so it works in nocache)
jasonvarga May 2, 2024
0a2836a
apply to terms
jasonvarga May 2, 2024
fecfc5b
viz
jasonvarga May 2, 2024
bf5c321
nitpick
jasonvarga May 2, 2024
6bac97b
contract
jasonvarga May 2, 2024
8a5bb26
Nitpick dumper ...
jasonvarga May 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/Contracts/Data/Augmentable.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@

interface Augmentable extends \JsonSerializable
{
public function augmented(): Augmented;

public function augmentedValue($key);

public function toAugmentedArray($keys = null);

public function toDeferredAugmentedArray($keys = null);

public function toDeferredAugmentedArrayUsingFields($keys, $fields);

public function toAugmentedCollection($keys = null);

public function toShallowAugmentedArray();
Expand Down
8 changes: 8 additions & 0 deletions src/Contracts/Data/BulkAugmentable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Statamic\Contracts\Data;

interface BulkAugmentable
{
public function getBulkAugmentationReferenceKey(): ?string;
}
89 changes: 72 additions & 17 deletions src/Data/AbstractAugmented.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ abstract class AbstractAugmented implements Augmented
protected $data;
protected $blueprintFields;
protected $relations = [];
private $fieldtype;

public function __construct($data)
{
Expand All @@ -36,39 +37,53 @@ public function select($keys = null)
$keys = $this->filterKeys(Arr::wrap($keys ?: $this->keys()));

foreach ($keys as $key) {
$arr[$key] = $this->get($key);
$arr[$key] = $this->transientValue($key);
}

return (new AugmentedCollection($arr))->withRelations($this->relations);
}

private function transientValue($key)
{
$fields = $this->blueprintFields();

$callback = function (Value $value) use ($key, $fields) {
$this->fieldtype = $fields->get($key)?->fieldtype();
$deferred = $this->get($key);
$this->fieldtype = null;

$value->setFieldtype($deferred->fieldtype());
$value->setAugmentable($deferred->augmentable());

return $deferred->raw();
};

return new Value($callback, $key);
}

abstract public function keys();

public function get($handle): Value
{
$method = Str::camel($handle);

if ($this->methodExistsOnThisClass($method)) {
$value = $this->$method();

return $value instanceof Value
? $value
: new Value($value, $method, null, $this->data);
}

if (method_exists($this->data, $method) && collect($this->keys())->contains(Str::snake($handle))) {
return $this->wrapValue($this->data->$method(), $handle);
$value = $this->wrapAugmentedMethodInvokable($method, $handle);
} elseif (method_exists($this->data, $method) && collect($this->keys())->contains(Str::snake($handle))) {
$value = $this->wrapDataMethodInvokable($method, $handle);
} else {
$value = $this->wrapDeferredValue($handle);
}

return $this->wrapValue($this->getFromData($handle), $handle);
return $value->resolve();
}

protected function filterKeys($keys)
private function filterKeys($keys)
{
return array_diff($keys, $this->excludedKeys());
}

protected function excludedKeys()
private function excludedKeys()
{
return Statamic::isApiRoute()
? config('statamic.api.excluded_keys', [])
Expand All @@ -93,19 +108,52 @@ protected function getFromData($handle)
return $value;
}

protected function wrapValue($value, $handle)
private function wrapDeferredValue($handle)
{
$fields = $this->blueprintFields();
return new Value(
fn () => $this->getFromData($handle),
$handle,
$this->fieldtype($handle),
$this->data
);
}

private function wrapAugmentedMethodInvokable(string $method, string $handle)
{
return new Value(
fn () => $this->$method(),
$handle,
null,
$this->data,
);
}

private function wrapDataMethodInvokable(string $method, string $handle)
{
return new Value(
fn () => $this->data->$method(),
$handle,
$this->fieldtype($handle),
$this->data
);
}

protected function wrapValue($value, $handle)
{
return new Value(
$value,
$handle,
optional($fields->get($handle))->fieldtype(),
$this->fieldtype($handle),
$this->data
);
}

protected function blueprintFields()
private function fieldtype($handle)
{
return $this->fieldtype ?? optional($this->blueprintFields()->get($handle))->fieldtype();
}

public function blueprintFields()
{
if (! isset($this->blueprintFields)) {
$this->blueprintFields = (method_exists($this->data, 'blueprint') && $blueprint = $this->data->blueprint())
Expand All @@ -116,6 +164,13 @@ protected function blueprintFields()
return $this->blueprintFields;
}

public function withBlueprintFields($fields)
{
$this->blueprintFields = $fields;

return $this;
}

public function withRelations($relations)
{
$this->relations = $relations;
Expand Down
12 changes: 12 additions & 0 deletions src/Data/AugmentedCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,18 @@ public function withoutEvaluation()
return $this;
}

public function all()
{
return collect($this->items)
->map(fn ($item) => $item instanceof Value ? $item->resolve() : $item)
->all();
}

public function deferredAll()
{
return parent::all();
}

public function toArray()
{
return $this->map(function ($value) {
Expand Down
111 changes: 111 additions & 0 deletions src/Data/BulkAugmentor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php

namespace Statamic\Data;

use Statamic\Contracts\Data\Augmentable;
use Statamic\Contracts\Data\BulkAugmentable;

class BulkAugmentor
{
private $isTree = false;
private $originalValues = [];
private $augmentedValues = [];

private function getAugmentationReference($item)
{
if ($item instanceof BulkAugmentable && $key = $item->getBulkAugmentationReferenceKey()) {
return $key;
}

return 'Ref::'.get_class($item).spl_object_hash($item);
}

public static function make($items)
{
return (new static)->augment($items);
}

public static function tree($tree)
{
return (new static)->augmentTree($tree);
}

/**
* @param array<Augmentable> $items
* @return $this
*/
private function augment($items)
{
$count = count($items);

$referenceKeys = [];
$referenceFields = [];

for ($i = 0; $i < $count; $i++) {
$item = $items[$i];
$reference = $this->getAugmentationReference($item);

if (! $this->isTree) {
$this->originalValues[$i] = $item;
}

if (array_key_exists($reference, $referenceKeys)) {
continue;
}

$augmented = $item->augmented();
$referenceKeys[$reference] = $augmented->keys();
$referenceFields[$reference] = $augmented->blueprintFields();
}

for ($i = 0; $i < $count; $i++) {
$item = $items[$i];
$reference = $this->getAugmentationReference($item);
$fields = $referenceFields[$reference];
$keys = $referenceKeys[$reference];

$this->augmentedValues[$i] = $item->toDeferredAugmentedArrayUsingFields($keys, $fields);
}

return $this;
}

private function augmentTree($tree)
{
$this->isTree = true;

if (! $tree) {
return $this;
}

$items = [];

for ($i = 0; $i < count($tree); $i++) {
$item = $tree[$i];

$items[] = $item['page'];
$this->originalValues[$i] = $item;
}

return $this->augment($items);
}

public function map(callable $callable)
{
$items = [];

for ($i = 0; $i < count($this->originalValues); $i++) {
$original = $this->originalValues[$i];
$augmented = $this->augmentedValues[$i];

$items[] = call_user_func_array($callable, [$original, $augmented, $i]);
}

return collect($items);
}

public function toArray()
{
return $this->augmentedValues;
}
}
20 changes: 18 additions & 2 deletions src/Data/HasAugmentedInstance.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,34 @@ public function augmentedValue($key)
return $this->augmented()->get($key);
}

public function toAugmentedCollection($keys = null)
private function toAugmentedCollectionWithFields($keys, $fields = null)
{
return $this->augmented()
->withRelations($this->defaultAugmentedRelations())
->withBlueprintFields($fields)
->select($keys ?? $this->defaultAugmentedArrayKeys());
}

public function toAugmentedCollection($keys = null)
{
return $this->toAugmentedCollectionWithFields($keys);
}

public function toAugmentedArray($keys = null)
{
return $this->toAugmentedCollection($keys)->all();
}

public function toDeferredAugmentedArray($keys = null)
{
return $this->toAugmentedCollectionWithFields($keys)->deferredAll();
}

public function toDeferredAugmentedArrayUsingFields($keys, $fields)
{
return $this->toAugmentedCollectionWithFields($keys, $fields)->deferredAll();
}

public function toShallowAugmentedCollection()
{
return $this->augmented()->select($this->shallowAugmentedArrayKeys())->withShallowNesting();
Expand All @@ -39,7 +55,7 @@ public function toShallowAugmentedArray()
return $this->toShallowAugmentedCollection()->all();
}

public function augmented()
public function augmented(): Augmented
{
return $this->runHooks('augmented', $this->newAugmentedInstance());
}
Expand Down
16 changes: 14 additions & 2 deletions src/Entries/Entry.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use Statamic\Contracts\Auth\Protect\Protectable;
use Statamic\Contracts\Data\Augmentable;
use Statamic\Contracts\Data\Augmented;
use Statamic\Contracts\Data\BulkAugmentable;
use Statamic\Contracts\Data\Localization;
use Statamic\Contracts\Entries\Entry as Contract;
use Statamic\Contracts\Entries\EntryRepository;
Expand Down Expand Up @@ -43,7 +44,6 @@
use Statamic\Facades\Collection;
use Statamic\Facades\Site;
use Statamic\Facades\Stache;
use Statamic\Fields\Value;
use Statamic\GraphQL\ResolvesValues;
use Statamic\Revisions\Revisable;
use Statamic\Routing\Routable;
Expand All @@ -53,7 +53,7 @@
use Statamic\Support\Str;
use Statamic\Support\Traits\FluentlyGetsAndSets;

class Entry implements Arrayable, ArrayAccess, Augmentable, ContainsQueryableValues, Contract, Localization, Protectable, ResolvesValuesContract, Responsable, SearchableContract
class Entry implements Arrayable, ArrayAccess, Augmentable, BulkAugmentable, ContainsQueryableValues, Contract, Localization, Protectable, ResolvesValuesContract, Responsable, SearchableContract
{
use ContainsComputedData, ContainsData, ExistsAsFile, FluentlyGetsAndSets, HasAugmentedInstance, Localizable, Publishable, Revisable, Searchable, TracksLastModified, TracksQueriedColumns, TracksQueriedRelations;

Expand All @@ -78,6 +78,7 @@ class Entry implements Arrayable, ArrayAccess, Augmentable, ContainsQueryableVal
protected $withEvents = true;
protected $template;
protected $layout;
private $augmentationReferenceKey;
private $computedCallbackCache;
private $siteCache;

Expand All @@ -92,6 +93,17 @@ public function id($id = null)
return $this->fluentlyGetOrSet('id')->args(func_get_args());
}

public function getBulkAugmentationReferenceKey(): ?string
{
if ($this->augmentationReferenceKey) {
return $this->augmentationReferenceKey;
}

$dataPart = implode('|', $this->data->keys()->sort()->all());

return $this->augmentationReferenceKey = 'Entry::'.$this->blueprint()->namespace().'::'.$dataPart;
}

public function locale($locale = null)
{
return $this
Expand Down