Skip to content

Commit a33e411

Browse files
committed
Implement Attribute caching
1 parent 91c3782 commit a33e411

13 files changed

+118
-68
lines changed

src/DataPipes/FillRouteParameterPropertiesDataPipe.php

+1-4
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,7 @@ public function handle(
2424
}
2525

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

3229
if ($attribute === null) {
3330
continue;

src/Normalizers/Normalized/NormalizedModel.php

+1-22
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public function getProperty(string $name, DataProperty $dataProperty): mixed
3737

3838
protected function fetchNewProperty(string $name, DataProperty $dataProperty): mixed
3939
{
40-
if ($dataProperty->attributes->contains(fn (object $attribute) => $attribute::class === LoadRelation::class)) {
40+
if ($dataProperty->attributes->hasAttribute(LoadRelation::class)) {
4141
if (method_exists($this->model, $name)) {
4242
$this->model->loadMissing($name);
4343
}
@@ -58,25 +58,4 @@ protected function fetchNewProperty(string $name, DataProperty $dataProperty): m
5858

5959
return $this->properties[$name] = UnknownProperty::create();
6060
}
61-
62-
protected function hasModelAttribute(string $name): bool
63-
{
64-
if (method_exists($this->model, 'hasAttribute')) {
65-
return $this->model->hasAttribute($name);
66-
}
67-
68-
// TODO: remove this once we stop supporting Laravel 10
69-
if (! isset($this->attributesProperty)) {
70-
$this->attributesProperty = new ReflectionProperty($this->model, 'attributes');
71-
}
72-
73-
if (! isset($this->castsProperty)) {
74-
$this->castsProperty = new ReflectionProperty($this->model, 'casts');
75-
}
76-
77-
return array_key_exists($name, $this->attributesProperty->getValue($this->model)) ||
78-
array_key_exists($name, $this->castsProperty->getValue($this->model)) ||
79-
$this->model->hasGetMutator($name) ||
80-
$this->model->hasAttributeMutator($name);
81-
}
8261
}

src/Resolvers/NameMappersResolver.php

+8-7
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Spatie\LaravelData\Attributes\MapOutputName;
99
use Spatie\LaravelData\Mappers\NameMapper;
1010
use Spatie\LaravelData\Mappers\ProvidedNameMapper;
11+
use Spatie\LaravelData\Support\AttributeCollection;
1112

1213
class NameMappersResolver
1314
{
@@ -21,7 +22,7 @@ public function __construct(protected array $ignoredMappers = [])
2122
}
2223

2324
public function execute(
24-
Collection $attributes
25+
AttributeCollection $attributes
2526
): array {
2627
return [
2728
'inputNameMapper' => $this->resolveInputNameMapper($attributes),
@@ -30,11 +31,11 @@ public function execute(
3031
}
3132

3233
protected function resolveInputNameMapper(
33-
Collection $attributes
34+
AttributeCollection $attributes
3435
): ?NameMapper {
3536
/** @var MapInputName|MapName|null $mapper */
36-
$mapper = $attributes->first(fn (object $attribute) => $attribute instanceof MapInputName)
37-
?? $attributes->first(fn (object $attribute) => $attribute instanceof MapName);
37+
$mapper = $attributes->getAttribute(MapInputName::class)
38+
?? $attributes->getAttribute(MapName::class);
3839

3940
if ($mapper) {
4041
return $this->resolveMapper($mapper->input);
@@ -44,11 +45,11 @@ protected function resolveInputNameMapper(
4445
}
4546

4647
protected function resolveOutputNameMapper(
47-
Collection $attributes
48+
AttributeCollection $attributes
4849
): ?NameMapper {
4950
/** @var MapOutputName|MapName|null $mapper */
50-
$mapper = $attributes->first(fn (object $attribute) => $attribute instanceof MapOutputName)
51-
?? $attributes->first(fn (object $attribute) => $attribute instanceof MapName);
51+
$mapper = $attributes->getAttribute(MapOutputName::class)
52+
?? $attributes->getAttribute(MapName::class);
5253

5354
if ($mapper) {
5455
return $this->resolveMapper($mapper->output);

src/RuleInferrers/AttributesRuleInferrer.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public function handle(
2424
): PropertyRules {
2525
$property
2626
->attributes
27-
->filter(fn (object $attribute) => $attribute instanceof ValidationRule)
27+
->getAttributes(ValidationRule::class)
2828
->each(function (ValidationRule $rule) use ($rules) {
2929
if ($rule instanceof Present && $rules->hasType(RequiringRule::class)) {
3030
$rules->removeType(RequiringRule::class);

src/Support/AttributeCollection.php

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
namespace Spatie\LaravelData\Support;
4+
5+
use Illuminate\Support\Collection;
6+
use ReflectionAttribute;
7+
8+
class AttributeCollection extends Collection
9+
{
10+
private array $groups;
11+
12+
public static function makeFromReflectionAttributes(array $attributes): static
13+
{
14+
return static::make(
15+
array_map(
16+
fn (ReflectionAttribute $attribute) => $attribute->newInstance(),
17+
array_filter($attributes, fn (ReflectionAttribute $attribute) => class_exists($attribute->getName()))
18+
)
19+
);
20+
}
21+
22+
public function add($item): static
23+
{
24+
unset($this->groups);
25+
return parent::add($item);
26+
}
27+
28+
public function offsetSet($key, $value): void
29+
{
30+
unset($this->groups);
31+
parent::offsetSet($key, $value);
32+
}
33+
34+
private function maybeProcessItemsIntoGroups(): void
35+
{
36+
if (!isset($this->groups)) {
37+
foreach ($this->items as $item) {
38+
$implements = class_implements($item);
39+
$parents = class_parents($item);
40+
foreach (array_merge([get_class($item)], $implements, $parents) as $parent) {
41+
$this->groups[$parent][] = $item;
42+
}
43+
}
44+
}
45+
}
46+
47+
/**
48+
* @param class-string $attributeClass
49+
*/
50+
public function hasAttribute(string $attributeClass): bool
51+
{
52+
$this->maybeProcessItemsIntoGroups();
53+
return !empty($this->groups[$attributeClass]);
54+
}
55+
56+
/**
57+
* @template T
58+
* @param class-string<T> $attributeClass
59+
*
60+
* @return Collection<T>
61+
*/
62+
public function getAttributes(string $attributeClass): Collection
63+
{
64+
$this->maybeProcessItemsIntoGroups();
65+
return collect($this->groups[$attributeClass] ?? []);
66+
}
67+
68+
/**
69+
* @template T
70+
* @param class-string<T> $attributeClass
71+
*
72+
* @return ?T
73+
*/
74+
public function getAttribute(string $attributeClass): ?object
75+
{
76+
$this->maybeProcessItemsIntoGroups();
77+
return current($this->groups[$attributeClass] ?? []) ?: null;
78+
}
79+
}

src/Support/DataClass.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public function __construct(
2727
public readonly bool $validateable,
2828
public readonly bool $wrappable,
2929
public readonly bool $emptyData,
30-
public readonly Collection $attributes,
30+
public readonly AttributeCollection $attributes,
3131
public readonly array $dataIterablePropertyAnnotations,
3232
public DataStructureProperty $allowedRequestIncludes,
3333
public DataStructureProperty $allowedRequestExcludes,

src/Support/DataProperty.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public function __construct(
2828
public readonly ?Transformer $transformer,
2929
public readonly ?string $inputMappedName,
3030
public readonly ?string $outputMappedName,
31-
public readonly Collection $attributes,
31+
public readonly AttributeCollection $attributes,
3232
) {
3333
}
3434
}

src/Support/Factories/DataClassFactory.php

+12-7
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use Spatie\LaravelData\Mappers\ProvidedNameMapper;
2222
use Spatie\LaravelData\Resolvers\NameMappersResolver;
2323
use Spatie\LaravelData\Support\Annotations\DataIterableAnnotationReader;
24+
use Spatie\LaravelData\Support\AttributeCollection;
2425
use Spatie\LaravelData\Support\DataClass;
2526
use Spatie\LaravelData\Support\DataProperty;
2627
use Spatie\LaravelData\Support\LazyDataStructureProperty;
@@ -105,22 +106,26 @@ public function build(ReflectionClass $reflectionClass): DataClass
105106
);
106107
}
107108

108-
protected function resolveAttributes(
109-
ReflectionClass $reflectionClass
110-
): Collection {
111-
$attributes = collect($reflectionClass->getAttributes())
112-
->filter(fn (ReflectionAttribute $reflectionAttribute) => class_exists($reflectionAttribute->getName()))
113-
->map(fn (ReflectionAttribute $reflectionAttribute) => $reflectionAttribute->newInstance());
109+
private function resolveRecursiveAttributes(ReflectionClass $reflectionClass): array
110+
{
111+
112+
$attributes = $reflectionClass->getAttributes();
114113

115114
$parent = $reflectionClass->getParentClass();
116115

117116
if ($parent !== false) {
118-
$attributes = $attributes->merge(static::resolveAttributes($parent));
117+
$attributes = array_merge($attributes, static::resolveRecursiveAttributes($parent));
119118
}
120119

121120
return $attributes;
122121
}
123122

123+
protected function resolveAttributes(
124+
ReflectionClass $reflectionClass
125+
): AttributeCollection {
126+
return AttributeCollection::makeFromReflectionAttributes(static::resolveRecursiveAttributes($reflectionClass));
127+
}
128+
124129
protected function resolveMethods(
125130
ReflectionClass $reflectionClass,
126131
): Collection {

src/Support/Factories/DataPropertyFactory.php

+7-14
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Spatie\LaravelData\Optional;
1717
use Spatie\LaravelData\Resolvers\NameMappersResolver;
1818
use Spatie\LaravelData\Support\Annotations\DataIterableAnnotation;
19+
use Spatie\LaravelData\Support\AttributeCollection;
1920
use Spatie\LaravelData\Support\DataProperty;
2021

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

4241
$type = $this->typeFactory->buildProperty(
4342
$reflectionProperty->getType(),
@@ -61,17 +60,11 @@ public function build(
6160
default => null,
6261
};
6362

64-
$computed = $attributes->contains(
65-
fn (object $attribute) => $attribute instanceof Computed
66-
);
63+
$computed = $attributes->hasAttribute(Computed::class);
6764

68-
$hidden = $attributes->contains(
69-
fn (object $attribute) => $attribute instanceof Hidden
70-
);
65+
$hidden = $attributes->hasAttribute(Hidden::class);
7166

72-
$validate = ! $attributes->contains(
73-
fn (object $attribute) => $attribute instanceof WithoutValidation
74-
) && ! $computed;
67+
$validate = ! $attributes->hasAttribute(WithoutValidation::class) && ! $computed;
7568

7669
if (! $reflectionProperty->isPromoted()) {
7770
$hasDefaultValue = $reflectionProperty->hasDefaultValue();
@@ -103,8 +96,8 @@ className: $reflectionProperty->class,
10396
autoLazy: $autoLazy,
10497
hasDefaultValue: $hasDefaultValue,
10598
defaultValue: $defaultValue,
106-
cast: $attributes->first(fn (object $attribute) => $attribute instanceof GetsCast)?->get(),
107-
transformer: $attributes->first(fn (object $attribute) => $attribute instanceof WithTransformer || $attribute instanceof WithCastAndTransformer)?->get(),
99+
cast: $attributes->getAttribute(GetsCast::class)?->get(),
100+
transformer: ($attributes->getAttribute(WithTransformer::class) ?? $attributes->getAttribute(WithCastAndTransformer::class))?->get(),
108101
inputMappedName: $inputMappedName,
109102
outputMappedName: $outputMappedName,
110103
attributes: $attributes,

src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php

+2-6
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,7 @@ protected function transformProperties(
5353
): string {
5454
$dataClass = app(DataConfig::class)->getDataClass($class->getName());
5555

56-
$isOptional = $dataClass->attributes->contains(
57-
fn (object $attribute) => $attribute instanceof TypeScriptOptional
58-
);
56+
$isOptional = $dataClass->attributes->hasAttribute(TypeScriptOptional::class);
5957

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

7876
$isOptional = $isOptional
79-
|| $dataProperty->attributes->contains(
80-
fn (object $attribute) => $attribute instanceof TypeScriptOptional
81-
)
77+
|| $dataProperty->attributes->hasAttribute(TypeScriptOptional::class)
8278
|| ($dataProperty->type->lazyType && $dataProperty->type->lazyType !== ClosureLazy::class)
8379
|| $dataProperty->type->isOptional
8480
|| ($dataProperty->type->isNullable && $this->config->shouldConsiderNullAsOptional());

tests/Normalizers/ModelNormalizerTest.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
->old_accessor->toEqual($data->old_accessor);
7373
});
7474

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

tests/Resolvers/NameMappersResolverTest.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
1010
use Spatie\LaravelData\Mappers\StudlyCaseMapper;
1111
use Spatie\LaravelData\Resolvers\NameMappersResolver;
12+
use Spatie\LaravelData\Support\AttributeCollection;
1213

13-
function getAttributes(object $class): Collection
14+
function getAttributes(object $class): AttributeCollection
1415
{
15-
return collect((new ReflectionProperty($class, 'property'))->getAttributes())
16-
->map(fn (ReflectionAttribute $attribute) => $attribute->newInstance());
16+
return AttributeCollection::makeFromReflectionAttributes((new ReflectionProperty($class, 'property'))->getAttributes());
1717
}
1818

1919
beforeEach(function () {

tests/ValidationTest.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ public static function rules(): array
291291
]);
292292
})->skip('Add a new ruleinferrer to rule them all and make these cases better');
293293

294-
it('it will take care of mapping', function () {
294+
it('will take care of mapping', function () {
295295
DataValidationAsserter::for(new class () extends Data {
296296
#[MapInputName('some_property')]
297297
public string $property;

0 commit comments

Comments
 (0)