Skip to content

Commit

Permalink
Implement Attribute caching
Browse files Browse the repository at this point in the history
  • Loading branch information
Tofandel committed Jan 27, 2025
1 parent 91c3782 commit 363330e
Show file tree
Hide file tree
Showing 15 changed files with 144 additions and 101 deletions.
5 changes: 1 addition & 4 deletions src/DataPipes/FillRouteParameterPropertiesDataPipe.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,7 @@ public function handle(
}

foreach ($class->properties as $dataProperty) {
/** @var FromRouteParameter|null $attribute */
$attribute = $dataProperty->attributes->first(
fn (object $attribute) => $attribute instanceof FromRouteParameter
);
$attribute = $dataProperty->attributes->getAttribute(FromRouteParameter::class);

if ($attribute === null) {
continue;
Expand Down
48 changes: 15 additions & 33 deletions src/Normalizers/Normalized/NormalizedModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,25 @@
use Illuminate\Database\Eloquent\MissingAttributeException;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
use ReflectionProperty;
use Spatie\LaravelData\Attributes\LoadRelation;
use Spatie\LaravelData\Support\DataProperty;

class NormalizedModel implements Normalized
{
protected array $properties = [];

protected ReflectionProperty $castsProperty;

protected ReflectionProperty $attributesProperty;

public function __construct(
protected Model $model,
) {
}

public function getProperty(string $name, DataProperty $dataProperty): mixed
{
$value = array_key_exists($name, $this->properties)
? $this->properties[$name]
: $this->fetchNewProperty($name, $dataProperty);
$propertyName = $this->model::$snakeAttributes ? Str::snake($name) : $name;

$value = array_key_exists($propertyName, $this->properties)
? $this->properties[$propertyName]
: $this->fetchNewProperty($propertyName, $dataProperty);

if ($value === null && ! $dataProperty->type->isNullable) {
return UnknownProperty::create();
Expand All @@ -37,46 +34,31 @@ public function getProperty(string $name, DataProperty $dataProperty): mixed

protected function fetchNewProperty(string $name, DataProperty $dataProperty): mixed
{
if ($dataProperty->attributes->contains(fn (object $attribute) => $attribute::class === LoadRelation::class)) {
$camelName = Str::camel($name);

if ($dataProperty->attributes->hasAttribute(LoadRelation::class)) {
if (method_exists($this->model, $name)) {
$this->model->loadMissing($name);
} elseif (method_exists($this->model, $camelName)) {
$this->model->loadMissing($camelName);
}
}

if ($this->model->relationLoaded($name)) {
return $this->properties[$name] = $this->model->getRelation($name);
}
if ($this->model->relationLoaded($camelName)) {
return $this->properties[$name] = $this->model->getRelation($camelName);
}

if (!$this->model->isRelation($name)) {
if (! $this->model->isRelation($name) && ! $this->model->isRelation($camelName)) {
try {
$propertyName = $this->model::$snakeAttributes ? Str::snake($name) : $name;
return $this->properties[$name] = $this->model->getAttribute($propertyName);
return $this->properties[$name] = $this->model->getAttribute($name);
} catch (MissingAttributeException) {
// Fallback if missing Attribute
}
}

return $this->properties[$name] = UnknownProperty::create();
}

protected function hasModelAttribute(string $name): bool
{
if (method_exists($this->model, 'hasAttribute')) {
return $this->model->hasAttribute($name);
}

// TODO: remove this once we stop supporting Laravel 10
if (! isset($this->attributesProperty)) {
$this->attributesProperty = new ReflectionProperty($this->model, 'attributes');
}

if (! isset($this->castsProperty)) {
$this->castsProperty = new ReflectionProperty($this->model, 'casts');
}

return array_key_exists($name, $this->attributesProperty->getValue($this->model)) ||
array_key_exists($name, $this->castsProperty->getValue($this->model)) ||
$this->model->hasGetMutator($name) ||
$this->model->hasAttributeMutator($name);
}
}
4 changes: 1 addition & 3 deletions src/Resolvers/DataValidationRulesResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -242,9 +242,7 @@ protected function resolveOverwrittenRules(
);

$overwrittenRules = app()->call([$class->name, 'rules'], ['context' => $validationContext]);
$shouldMergeRules = $class->attributes->contains(
fn (object $attribute) => $attribute::class === MergeValidationRules::class
);
$shouldMergeRules = $class->attributes->hasAttribute(MergeValidationRules::class);

foreach ($overwrittenRules as $key => $rules) {
if (in_array($key, $withoutValidationProperties)) {
Expand Down
18 changes: 8 additions & 10 deletions src/Resolvers/NameMappersResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

namespace Spatie\LaravelData\Resolvers;

use Illuminate\Support\Collection;
use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapName;
use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Mappers\NameMapper;
use Spatie\LaravelData\Mappers\ProvidedNameMapper;
use Spatie\LaravelData\Support\AttributeCollection;

class NameMappersResolver
{
Expand All @@ -21,7 +21,7 @@ public function __construct(protected array $ignoredMappers = [])
}

public function execute(
Collection $attributes
AttributeCollection $attributes
): array {
return [
'inputNameMapper' => $this->resolveInputNameMapper($attributes),
Expand All @@ -30,11 +30,10 @@ public function execute(
}

protected function resolveInputNameMapper(
Collection $attributes
AttributeCollection $attributes
): ?NameMapper {
/** @var MapInputName|MapName|null $mapper */
$mapper = $attributes->first(fn (object $attribute) => $attribute instanceof MapInputName)
?? $attributes->first(fn (object $attribute) => $attribute instanceof MapName);
$mapper = $attributes->getAttribute(MapInputName::class)
?? $attributes->getAttribute(MapName::class);

if ($mapper) {
return $this->resolveMapper($mapper->input);
Expand All @@ -44,11 +43,10 @@ protected function resolveInputNameMapper(
}

protected function resolveOutputNameMapper(
Collection $attributes
AttributeCollection $attributes
): ?NameMapper {
/** @var MapOutputName|MapName|null $mapper */
$mapper = $attributes->first(fn (object $attribute) => $attribute instanceof MapOutputName)
?? $attributes->first(fn (object $attribute) => $attribute instanceof MapName);
$mapper = $attributes->getAttribute(MapOutputName::class)
?? $attributes->getAttribute(MapName::class);

if ($mapper) {
return $this->resolveMapper($mapper->output);
Expand Down
2 changes: 1 addition & 1 deletion src/RuleInferrers/AttributesRuleInferrer.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public function handle(
): PropertyRules {
$property
->attributes
->filter(fn (object $attribute) => $attribute instanceof ValidationRule)
->getAttributes(ValidationRule::class)
->each(function (ValidationRule $rule) use ($rules) {
if ($rule instanceof Present && $rules->hasType(RequiringRule::class)) {
$rules->removeType(RequiringRule::class);
Expand Down
83 changes: 83 additions & 0 deletions src/Support/AttributeCollection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

namespace Spatie\LaravelData\Support;

use Illuminate\Support\Collection;
use ReflectionAttribute;

class AttributeCollection extends Collection
{
private array $groups;

public static function makeFromReflectionAttributes(array $attributes): self
{
return new self(
array_map(
fn (ReflectionAttribute $attribute) => $attribute->newInstance(),
array_filter($attributes, fn (ReflectionAttribute $attribute) => class_exists($attribute->getName()))
)
);
}

public function add($item): static
{
unset($this->groups);

return parent::add($item);
}

public function offsetSet($key, $value): void
{
unset($this->groups);
parent::offsetSet($key, $value);
}

private function maybeProcessItemsIntoGroups(): void
{
if (! isset($this->groups)) {
foreach ($this->items as $item) {
$implements = class_implements($item);
$parents = class_parents($item);
foreach (array_merge([get_class($item)], $implements, $parents) as $parent) {
$this->groups[$parent][] = $item;
}
}
}
}

/**
* @param class-string $attributeClass
*/
public function hasAttribute(string $attributeClass): bool
{
$this->maybeProcessItemsIntoGroups();

return ! empty($this->groups[$attributeClass]);
}

/**
* @template T of object
* @param class-string<T> $attributeClass
*
* @return Collection<T>
*/
public function getAttributes(string $attributeClass): Collection
{
$this->maybeProcessItemsIntoGroups();

return collect($this->groups[$attributeClass] ?? []);
}

/**
* @template T of object
* @param class-string<T> $attributeClass
*
* @return ?T
*/
public function getAttribute(string $attributeClass): ?object
{
$this->maybeProcessItemsIntoGroups();

return current($this->groups[$attributeClass] ?? []) ?: null;
}
}
4 changes: 2 additions & 2 deletions src/Support/DataClass.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* @property class-string $name
* @property Collection<string, DataProperty> $properties
* @property Collection<string, DataMethod> $methods
* @property Collection<string, object> $attributes
* @property AttributeCollection<string, object> $attributes
* @property array<string, \Spatie\LaravelData\Support\Annotations\DataIterableAnnotation> $dataCollectablePropertyAnnotations
*/
class DataClass
Expand All @@ -27,7 +27,7 @@ public function __construct(
public readonly bool $validateable,
public readonly bool $wrappable,
public readonly bool $emptyData,
public readonly Collection $attributes,
public readonly AttributeCollection $attributes,
public readonly array $dataIterablePropertyAnnotations,
public DataStructureProperty $allowedRequestIncludes,
public DataStructureProperty $allowedRequestExcludes,
Expand Down
5 changes: 2 additions & 3 deletions src/Support/DataProperty.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@

namespace Spatie\LaravelData\Support;

use Illuminate\Support\Collection;
use Spatie\LaravelData\Attributes\AutoLazy;
use Spatie\LaravelData\Casts\Cast;
use Spatie\LaravelData\Transformers\Transformer;

/**
* @property Collection<string, object> $attributes
* @property AttributeCollection<string, object> $attributes
*/
class DataProperty
{
Expand All @@ -28,7 +27,7 @@ public function __construct(
public readonly ?Transformer $transformer,
public readonly ?string $inputMappedName,
public readonly ?string $outputMappedName,
public readonly Collection $attributes,
public readonly AttributeCollection $attributes,
) {
}
}
20 changes: 12 additions & 8 deletions src/Support/Factories/DataClassFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
namespace Spatie\LaravelData\Support\Factories;

use Illuminate\Support\Collection;
use ReflectionAttribute;
use ReflectionClass;
use ReflectionMethod;
use ReflectionParameter;
Expand All @@ -21,6 +20,7 @@
use Spatie\LaravelData\Mappers\ProvidedNameMapper;
use Spatie\LaravelData\Resolvers\NameMappersResolver;
use Spatie\LaravelData\Support\Annotations\DataIterableAnnotationReader;
use Spatie\LaravelData\Support\AttributeCollection;
use Spatie\LaravelData\Support\DataClass;
use Spatie\LaravelData\Support\DataProperty;
use Spatie\LaravelData\Support\LazyDataStructureProperty;
Expand Down Expand Up @@ -105,22 +105,26 @@ public function build(ReflectionClass $reflectionClass): DataClass
);
}

protected function resolveAttributes(
ReflectionClass $reflectionClass
): Collection {
$attributes = collect($reflectionClass->getAttributes())
->filter(fn (ReflectionAttribute $reflectionAttribute) => class_exists($reflectionAttribute->getName()))
->map(fn (ReflectionAttribute $reflectionAttribute) => $reflectionAttribute->newInstance());
private function resolveRecursiveAttributes(ReflectionClass $reflectionClass): array
{

$attributes = $reflectionClass->getAttributes();

$parent = $reflectionClass->getParentClass();

if ($parent !== false) {
$attributes = $attributes->merge(static::resolveAttributes($parent));
$attributes = array_merge($attributes, $this->resolveRecursiveAttributes($parent));
}

return $attributes;
}

protected function resolveAttributes(
ReflectionClass $reflectionClass
): AttributeCollection {
return AttributeCollection::makeFromReflectionAttributes($this->resolveRecursiveAttributes($reflectionClass));
}

protected function resolveMethods(
ReflectionClass $reflectionClass,
): Collection {
Expand Down
Loading

0 comments on commit 363330e

Please sign in to comment.