Skip to content

Commit a8770fd

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

15 files changed

+144
-100
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

+15-33
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,25 @@
55
use Illuminate\Database\Eloquent\MissingAttributeException;
66
use Illuminate\Database\Eloquent\Model;
77
use Illuminate\Support\Str;
8-
use ReflectionProperty;
98
use Spatie\LaravelData\Attributes\LoadRelation;
109
use Spatie\LaravelData\Support\DataProperty;
1110

1211
class NormalizedModel implements Normalized
1312
{
1413
protected array $properties = [];
1514

16-
protected ReflectionProperty $castsProperty;
17-
18-
protected ReflectionProperty $attributesProperty;
19-
2015
public function __construct(
2116
protected Model $model,
2217
) {
2318
}
2419

2520
public function getProperty(string $name, DataProperty $dataProperty): mixed
2621
{
27-
$value = array_key_exists($name, $this->properties)
28-
? $this->properties[$name]
29-
: $this->fetchNewProperty($name, $dataProperty);
22+
$propertyName = $this->model::$snakeAttributes ? Str::snake($name) : $name;
23+
24+
$value = array_key_exists($propertyName, $this->properties)
25+
? $this->properties[$propertyName]
26+
: $this->fetchNewProperty($propertyName, $dataProperty);
3027

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

3835
protected function fetchNewProperty(string $name, DataProperty $dataProperty): mixed
3936
{
40-
if ($dataProperty->attributes->contains(fn (object $attribute) => $attribute::class === LoadRelation::class)) {
37+
$camelName = Str::camel($name);
38+
39+
if ($dataProperty->attributes->hasAttribute(LoadRelation::class)) {
4140
if (method_exists($this->model, $name)) {
4241
$this->model->loadMissing($name);
42+
} elseif (method_exists($this->model, $camelName)) {
43+
$this->model->loadMissing($camelName);
4344
}
4445
}
4546

4647
if ($this->model->relationLoaded($name)) {
4748
return $this->properties[$name] = $this->model->getRelation($name);
4849
}
50+
if ($this->model->relationLoaded($camelName)) {
51+
return $this->properties[$name] = $this->model->getRelation($camelName);
52+
}
4953

50-
if (!$this->model->isRelation($name)) {
54+
if (! $this->model->isRelation($name) && ! $this->model->isRelation($camelName)) {
5155
try {
52-
$propertyName = $this->model::$snakeAttributes ? Str::snake($name) : $name;
53-
return $this->properties[$name] = $this->model->getAttribute($propertyName);
56+
return $this->properties[$name] = $this->model->getAttribute($name);
5457
} catch (MissingAttributeException) {
5558
// Fallback if missing Attribute
5659
}
5760
}
5861

5962
return $this->properties[$name] = UnknownProperty::create();
6063
}
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-
}
8264
}

src/Resolvers/DataValidationRulesResolver.php

+1-3
Original file line numberDiff line numberDiff line change
@@ -242,9 +242,7 @@ protected function resolveOverwrittenRules(
242242
);
243243

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

249247
foreach ($overwrittenRules as $key => $rules) {
250248
if (in_array($key, $withoutValidationProperties)) {

src/Resolvers/NameMappersResolver.php

+8-9
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22

33
namespace Spatie\LaravelData\Resolvers;
44

5-
use Illuminate\Support\Collection;
65
use Spatie\LaravelData\Attributes\MapInputName;
76
use Spatie\LaravelData\Attributes\MapName;
87
use Spatie\LaravelData\Attributes\MapOutputName;
98
use Spatie\LaravelData\Mappers\NameMapper;
109
use Spatie\LaravelData\Mappers\ProvidedNameMapper;
10+
use Spatie\LaravelData\Support\AttributeCollection;
1111

1212
class NameMappersResolver
1313
{
@@ -21,7 +21,7 @@ public function __construct(protected array $ignoredMappers = [])
2121
}
2222

2323
public function execute(
24-
Collection $attributes
24+
AttributeCollection $attributes
2525
): array {
2626
return [
2727
'inputNameMapper' => $this->resolveInputNameMapper($attributes),
@@ -30,11 +30,10 @@ public function execute(
3030
}
3131

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

3938
if ($mapper) {
4039
return $this->resolveMapper($mapper->input);
@@ -44,11 +43,11 @@ protected function resolveInputNameMapper(
4443
}
4544

4645
protected function resolveOutputNameMapper(
47-
Collection $attributes
46+
AttributeCollection $attributes
4847
): ?NameMapper {
4948
/** @var MapOutputName|MapName|null $mapper */
50-
$mapper = $attributes->first(fn (object $attribute) => $attribute instanceof MapOutputName)
51-
?? $attributes->first(fn (object $attribute) => $attribute instanceof MapName);
49+
$mapper = $attributes->getAttribute(MapOutputName::class)
50+
?? $attributes->getAttribute(MapName::class);
5251

5352
if ($mapper) {
5453
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

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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): self
13+
{
14+
return new self(
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+
26+
return parent::add($item);
27+
}
28+
29+
public function offsetSet($key, $value): void
30+
{
31+
unset($this->groups);
32+
parent::offsetSet($key, $value);
33+
}
34+
35+
private function maybeProcessItemsIntoGroups(): void
36+
{
37+
if (! isset($this->groups)) {
38+
foreach ($this->items as $item) {
39+
$implements = class_implements($item);
40+
$parents = class_parents($item);
41+
foreach (array_merge([get_class($item)], $implements, $parents) as $parent) {
42+
$this->groups[$parent][] = $item;
43+
}
44+
}
45+
}
46+
}
47+
48+
/**
49+
* @param class-string $attributeClass
50+
*/
51+
public function hasAttribute(string $attributeClass): bool
52+
{
53+
$this->maybeProcessItemsIntoGroups();
54+
55+
return ! empty($this->groups[$attributeClass]);
56+
}
57+
58+
/**
59+
* @template T of object
60+
* @param class-string<T> $attributeClass
61+
*
62+
* @return Collection<T>
63+
*/
64+
public function getAttributes(string $attributeClass): Collection
65+
{
66+
$this->maybeProcessItemsIntoGroups();
67+
68+
return collect($this->groups[$attributeClass] ?? []);
69+
}
70+
71+
/**
72+
* @template T of object
73+
* @param class-string<T> $attributeClass
74+
*
75+
* @return ?T
76+
*/
77+
public function getAttribute(string $attributeClass): ?object
78+
{
79+
$this->maybeProcessItemsIntoGroups();
80+
81+
return current($this->groups[$attributeClass] ?? []) ?: null;
82+
}
83+
}

src/Support/DataClass.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* @property class-string $name
99
* @property Collection<string, DataProperty> $properties
1010
* @property Collection<string, DataMethod> $methods
11-
* @property Collection<string, object> $attributes
11+
* @property AttributeCollection<string, object> $attributes
1212
* @property array<string, \Spatie\LaravelData\Support\Annotations\DataIterableAnnotation> $dataCollectablePropertyAnnotations
1313
*/
1414
class DataClass
@@ -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

+2-3
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@
22

33
namespace Spatie\LaravelData\Support;
44

5-
use Illuminate\Support\Collection;
65
use Spatie\LaravelData\Attributes\AutoLazy;
76
use Spatie\LaravelData\Casts\Cast;
87
use Spatie\LaravelData\Transformers\Transformer;
98

109
/**
11-
* @property Collection<string, object> $attributes
10+
* @property AttributeCollection<string, object> $attributes
1211
*/
1312
class DataProperty
1413
{
@@ -28,7 +27,7 @@ public function __construct(
2827
public readonly ?Transformer $transformer,
2928
public readonly ?string $inputMappedName,
3029
public readonly ?string $outputMappedName,
31-
public readonly Collection $attributes,
30+
public readonly AttributeCollection $attributes,
3231
) {
3332
}
3433
}

src/Support/Factories/DataClassFactory.php

+12-8
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
namespace Spatie\LaravelData\Support\Factories;
44

55
use Illuminate\Support\Collection;
6-
use ReflectionAttribute;
76
use ReflectionClass;
87
use ReflectionMethod;
98
use ReflectionParameter;
@@ -21,6 +20,7 @@
2120
use Spatie\LaravelData\Mappers\ProvidedNameMapper;
2221
use Spatie\LaravelData\Resolvers\NameMappersResolver;
2322
use Spatie\LaravelData\Support\Annotations\DataIterableAnnotationReader;
23+
use Spatie\LaravelData\Support\AttributeCollection;
2424
use Spatie\LaravelData\Support\DataClass;
2525
use Spatie\LaravelData\Support\DataProperty;
2626
use Spatie\LaravelData\Support\LazyDataStructureProperty;
@@ -105,22 +105,26 @@ public function build(ReflectionClass $reflectionClass): DataClass
105105
);
106106
}
107107

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());
108+
private function resolveRecursiveAttributes(ReflectionClass $reflectionClass): array
109+
{
110+
111+
$attributes = $reflectionClass->getAttributes();
114112

115113
$parent = $reflectionClass->getParentClass();
116114

117115
if ($parent !== false) {
118-
$attributes = $attributes->merge(static::resolveAttributes($parent));
116+
$attributes = array_merge($attributes, $this->resolveRecursiveAttributes($parent));
119117
}
120118

121119
return $attributes;
122120
}
123121

122+
protected function resolveAttributes(
123+
ReflectionClass $reflectionClass
124+
): AttributeCollection {
125+
return AttributeCollection::makeFromReflectionAttributes($this->resolveRecursiveAttributes($reflectionClass));
126+
}
127+
124128
protected function resolveMethods(
125129
ReflectionClass $reflectionClass,
126130
): Collection {

0 commit comments

Comments
 (0)