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 25, 2025
1 parent 56bd051 commit a0fe974
Show file tree
Hide file tree
Showing 13 changed files with 118 additions and 68 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);

Check failure on line 27 in src/DataPipes/FillRouteParameterPropertiesDataPipe.php

View workflow job for this annotation

GitHub Actions / phpstan

Call to an undefined method Illuminate\Support\Collection<string, object>::getAttribute().

if ($attribute === null) {
continue;
Expand Down
23 changes: 1 addition & 22 deletions src/Normalizers/Normalized/NormalizedModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ 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)) {
if ($dataProperty->attributes->hasAttribute(LoadRelation::class)) {

Check failure on line 40 in src/Normalizers/Normalized/NormalizedModel.php

View workflow job for this annotation

GitHub Actions / phpstan

Call to an undefined method Illuminate\Support\Collection<string, object>::hasAttribute().
if (method_exists($this->model, $name)) {
$this->model->loadMissing($name);
}
Expand All @@ -58,25 +58,4 @@ protected function fetchNewProperty(string $name, DataProperty $dataProperty): m

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);
}
}
15 changes: 8 additions & 7 deletions src/Resolvers/NameMappersResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
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 +22,7 @@ public function __construct(protected array $ignoredMappers = [])
}

public function execute(
Collection $attributes
AttributeCollection $attributes
): array {
return [
'inputNameMapper' => $this->resolveInputNameMapper($attributes),
Expand All @@ -30,11 +31,11 @@ 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 +45,11 @@ 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

Check failure on line 25 in src/RuleInferrers/AttributesRuleInferrer.php

View workflow job for this annotation

GitHub Actions / phpstan

Call to an undefined method Illuminate\Support\Collection<string, object>::getAttributes().
->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
79 changes: 79 additions & 0 deletions src/Support/AttributeCollection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

namespace Spatie\LaravelData\Support;

use Illuminate\Support\Collection;
use ReflectionAttribute;

class AttributeCollection extends Collection
{
private array $groups;

public static function makeFromReflectionAttributes(array $attributes): static
{
return static::make(

Check failure on line 14 in src/Support/AttributeCollection.php

View workflow job for this annotation

GitHub Actions / phpstan

Method Spatie\LaravelData\Support\AttributeCollection::makeFromReflectionAttributes() should return static(Spatie\LaravelData\Support\AttributeCollection) but returns Illuminate\Support\Collection<(int|string), object>.
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
* @param class-string<T> $attributeClass
*
* @return Collection<T>
*/
public function getAttributes(string $attributeClass): Collection
{
$this->maybeProcessItemsIntoGroups();
return collect($this->groups[$attributeClass] ?? []);
}

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

Check failure on line 74 in src/Support/AttributeCollection.php

View workflow job for this annotation

GitHub Actions / phpstan

PHPDoc tag @return with type T|null is not subtype of native type object|null.
{
$this->maybeProcessItemsIntoGroups();
return current($this->groups[$attributeClass] ?? []) ?: null;
}
}
2 changes: 1 addition & 1 deletion src/Support/DataClass.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion src/Support/DataProperty.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public function __construct(
public readonly ?Transformer $transformer,
public readonly ?string $inputMappedName,
public readonly ?string $outputMappedName,
public readonly Collection $attributes,
public readonly AttributeCollection $attributes,
) {
}
}
19 changes: 12 additions & 7 deletions src/Support/Factories/DataClassFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,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 +106,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, static::resolveRecursiveAttributes($parent));

Check failure on line 117 in src/Support/Factories/DataClassFactory.php

View workflow job for this annotation

GitHub Actions / phpstan

Unsafe call to private method Spatie\LaravelData\Support\Factories\DataClassFactory::resolveRecursiveAttributes() through static::.
}

return $attributes;
}

protected function resolveAttributes(
ReflectionClass $reflectionClass
): AttributeCollection {
return AttributeCollection::makeFromReflectionAttributes(static::resolveRecursiveAttributes($reflectionClass));

Check failure on line 126 in src/Support/Factories/DataClassFactory.php

View workflow job for this annotation

GitHub Actions / phpstan

Unsafe call to private method Spatie\LaravelData\Support\Factories\DataClassFactory::resolveRecursiveAttributes() through static::.
}

protected function resolveMethods(
ReflectionClass $reflectionClass,
): Collection {
Expand Down
21 changes: 7 additions & 14 deletions src/Support/Factories/DataPropertyFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Spatie\LaravelData\Optional;
use Spatie\LaravelData\Resolvers\NameMappersResolver;
use Spatie\LaravelData\Support\Annotations\DataIterableAnnotation;
use Spatie\LaravelData\Support\AttributeCollection;
use Spatie\LaravelData\Support\DataProperty;

class DataPropertyFactory
Expand All @@ -35,9 +36,7 @@ public function build(
?DataIterableAnnotation $classDefinedDataIterableAnnotation = null,
?AutoLazy $classAutoLazy = null,
): DataProperty {
$attributes = collect($reflectionProperty->getAttributes())
->filter(fn (ReflectionAttribute $reflectionAttribute) => class_exists($reflectionAttribute->getName()))
->map(fn (ReflectionAttribute $reflectionAttribute) => $reflectionAttribute->newInstance());
$attributes = AttributeCollection::makeFromReflectionAttributes($reflectionProperty->getAttributes());

$type = $this->typeFactory->buildProperty(
$reflectionProperty->getType(),
Expand All @@ -61,17 +60,11 @@ public function build(
default => null,
};

$computed = $attributes->contains(
fn (object $attribute) => $attribute instanceof Computed
);
$computed = $attributes->hasAttribute(Computed::class);

$hidden = $attributes->contains(
fn (object $attribute) => $attribute instanceof Hidden
);
$hidden = $attributes->hasAttribute(Hidden::class);

$validate = ! $attributes->contains(
fn (object $attribute) => $attribute instanceof WithoutValidation
) && ! $computed;
$validate = ! $attributes->hasAttribute(WithoutValidation::class) && ! $computed;

if (! $reflectionProperty->isPromoted()) {
$hasDefaultValue = $reflectionProperty->hasDefaultValue();
Expand Down Expand Up @@ -103,8 +96,8 @@ className: $reflectionProperty->class,
autoLazy: $autoLazy,
hasDefaultValue: $hasDefaultValue,
defaultValue: $defaultValue,
cast: $attributes->first(fn (object $attribute) => $attribute instanceof GetsCast)?->get(),
transformer: $attributes->first(fn (object $attribute) => $attribute instanceof WithTransformer || $attribute instanceof WithCastAndTransformer)?->get(),
cast: $attributes->getAttribute(GetsCast::class)?->get(),
transformer: ($attributes->getAttribute(WithTransformer::class) ?? $attributes->getAttribute(WithCastAndTransformer::class))?->get(),
inputMappedName: $inputMappedName,
outputMappedName: $outputMappedName,
attributes: $attributes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,7 @@ protected function transformProperties(
): string {
$dataClass = app(DataConfig::class)->getDataClass($class->getName());

$isOptional = $dataClass->attributes->contains(
fn (object $attribute) => $attribute instanceof TypeScriptOptional
);
$isOptional = $dataClass->attributes->hasAttribute(TypeScriptOptional::class);

Check failure on line 56 in src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php

View workflow job for this annotation

GitHub Actions / phpstan

Call to an undefined method Illuminate\Support\Collection<string, object>::hasAttribute().

return array_reduce(
$this->resolveProperties($class),
Expand All @@ -76,9 +74,7 @@ function (string $carry, ReflectionProperty $property) use ($isOptional, $dataCl
}

$isOptional = $isOptional
|| $dataProperty->attributes->contains(
fn (object $attribute) => $attribute instanceof TypeScriptOptional
)
|| $dataProperty->attributes->hasAttribute(TypeScriptOptional::class)

Check failure on line 77 in src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php

View workflow job for this annotation

GitHub Actions / phpstan

Call to an undefined method Illuminate\Support\Collection<string, object>::hasAttribute().
|| ($dataProperty->type->lazyType && $dataProperty->type->lazyType !== ClosureLazy::class)
|| $dataProperty->type->isOptional
|| ($dataProperty->type->isNullable && $this->config->shouldConsiderNullAsOptional());
Expand Down
2 changes: 1 addition & 1 deletion tests/Normalizers/ModelNormalizerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
->old_accessor->toEqual($data->old_accessor);
});

it('it will only call model accessors when required', function () {
it('will only call model accessors when required', function () {
$dataClass = new class () extends Data {
public string $accessor;

Expand Down
6 changes: 3 additions & 3 deletions tests/Resolvers/NameMappersResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
use Spatie\LaravelData\Mappers\StudlyCaseMapper;
use Spatie\LaravelData\Resolvers\NameMappersResolver;
use Spatie\LaravelData\Support\AttributeCollection;

function getAttributes(object $class): Collection
function getAttributes(object $class): AttributeCollection
{
return collect((new ReflectionProperty($class, 'property'))->getAttributes())
->map(fn (ReflectionAttribute $attribute) => $attribute->newInstance());
return AttributeCollection::makeFromReflectionAttributes((new ReflectionProperty($class, 'property'))->getAttributes());
}

beforeEach(function () {
Expand Down
2 changes: 1 addition & 1 deletion tests/ValidationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ public static function rules(): array
]);
})->skip('Add a new ruleinferrer to rule them all and make these cases better');

it('it will take care of mapping', function () {
it('will take care of mapping', function () {
DataValidationAsserter::for(new class () extends Data {
#[MapInputName('some_property')]
public string $property;
Expand Down

0 comments on commit a0fe974

Please sign in to comment.