From 02102ed073659f847d75106daeaef40c2516431c Mon Sep 17 00:00:00 2001 From: Romain Canon Date: Sat, 19 Aug 2023 15:13:57 +0200 Subject: [PATCH] fix: load attributes lazily during runtime and cache access Attributes are now loaded in a lazy way, preventing an error from happening when attributes were compiled in the cache, but not present when using the mapper during the runtime. This could happen for instance with attributes used to generate documentation of an API: they are loaded as a development dependency, then the cache is warmed up during deployment, so they are present in the mapper compiled cache; but these attributes are missing from the live version of the application, so they cannot be accessed. This commit helps to prevent a fatal error from happening in this kind of situation. --- src/Definition/AttributesContainer.php | 32 ++++++++++++---- src/Definition/NativeAttributes.php | 7 +++- .../Cache/Compiler/AttributesCompiler.php | 12 ++---- .../Definition/AttributesContainerTest.php | 38 ++++++++++++++++--- 4 files changed, 66 insertions(+), 23 deletions(-) diff --git a/src/Definition/AttributesContainer.php b/src/Definition/AttributesContainer.php index ba6ae279..f60353c8 100644 --- a/src/Definition/AttributesContainer.php +++ b/src/Definition/AttributesContainer.php @@ -7,18 +7,28 @@ use Traversable; use function array_filter; +use function array_map; use function array_values; use function count; +use function is_a; -/** @internal */ +/** + * @phpstan-type AttributeParam = array{class: class-string, callback: callable(): object} + * + * @internal + */ final class AttributesContainer implements Attributes { private static self $empty; - /** @var array */ + /** @var list */ private array $attributes; - public function __construct(object ...$attributes) + /** + * @no-named-arguments + * @param AttributeParam ...$attributes + */ + public function __construct(array ...$attributes) { $this->attributes = $attributes; } @@ -31,7 +41,7 @@ public static function empty(): self public function has(string $className): bool { foreach ($this->attributes as $attribute) { - if ($attribute instanceof $className) { + if (is_a($attribute['class'], $className, true)) { return true; } } @@ -41,9 +51,15 @@ public function has(string $className): bool public function ofType(string $className): array { - return array_values(array_filter( + $attributes = array_filter( $this->attributes, - static fn (object $attribute): bool => $attribute instanceof $className + static fn (array $attribute): bool => is_a($attribute['class'], $className, true) + ); + + /** @phpstan-ignore-next-line */ + return array_values(array_map( + fn (array $attribute) => $attribute['callback'](), + $attributes )); } @@ -57,6 +73,8 @@ public function count(): int */ public function getIterator(): Traversable { - return yield from $this->attributes; + foreach ($this->attributes as $attribute) { + yield $attribute['callback'](); + } } } diff --git a/src/Definition/NativeAttributes.php b/src/Definition/NativeAttributes.php index f4a7b980..064702ae 100644 --- a/src/Definition/NativeAttributes.php +++ b/src/Definition/NativeAttributes.php @@ -32,7 +32,12 @@ public function __construct(ReflectionClass|ReflectionProperty|ReflectionMethod| array_map( static function (ReflectionAttribute $attribute) { try { - return $attribute->newInstance(); + $instance = $attribute->newInstance(); + + return [ + 'class' => $attribute->getName(), + 'callback' => fn () => $instance + ]; } catch (Error) { // Race condition when the attribute is affected to a property/parameter // that was PROMOTED, in this case the attribute will be applied to both diff --git a/src/Definition/Repository/Cache/Compiler/AttributesCompiler.php b/src/Definition/Repository/Cache/Compiler/AttributesCompiler.php index d04e49b1..88706307 100644 --- a/src/Definition/Repository/Cache/Compiler/AttributesCompiler.php +++ b/src/Definition/Repository/Cache/Compiler/AttributesCompiler.php @@ -34,21 +34,15 @@ public function compile(Attributes $attributes): string private function compileNativeAttributes(NativeAttributes $attributes): string { - $attributes = $attributes->definition(); - - if (count($attributes) === 0) { - return '[]'; - } - $attributesListCode = []; - foreach ($attributes as $className => $arguments) { + foreach ($attributes->definition() as $className => $arguments) { $argumentsCode = $this->compileAttributeArguments($arguments); - $attributesListCode[] = "new $className($argumentsCode)"; + $attributesListCode[] = "['class' => '$className', 'callback' => fn () => new $className($argumentsCode)]"; } - return '...[' . implode(",\n", $attributesListCode) . ']'; + return implode(', ', $attributesListCode); } /** diff --git a/tests/Unit/Definition/AttributesContainerTest.php b/tests/Unit/Definition/AttributesContainerTest.php index 4a2365f4..73a841c9 100644 --- a/tests/Unit/Definition/AttributesContainerTest.php +++ b/tests/Unit/Definition/AttributesContainerTest.php @@ -11,6 +11,9 @@ use PHPUnit\Framework\TestCase; use stdClass; +/** + * @phpstan-import-type AttributeParam from AttributesContainer + */ final class AttributesContainerTest extends TestCase { public function test_empty_attributes_is_empty_and_remains_the_same_instance(): void @@ -25,22 +28,34 @@ public function test_empty_attributes_is_empty_and_remains_the_same_instance(): public function test_attributes_are_countable(): void { - $attributes = new AttributesContainer(new stdClass(), new stdClass(), new stdClass()); + $attributes = [ + $this->attribute(new stdClass()), + $this->attribute(new stdClass()), + $this->attribute(new stdClass()), + ]; - self::assertCount(3, $attributes); + $container = new AttributesContainer(...$attributes); + + self::assertCount(3, $container); } public function test_attributes_are_traversable(): void { - $attributes = [new stdClass(), new stdClass(), new stdClass()]; + $objects = [new stdClass(), new stdClass(), new stdClass()]; + $attributes = [ + $this->attribute($objects[0]), + $this->attribute($objects[1]), + $this->attribute($objects[2]), + ]; + $container = new AttributesContainer(...$attributes); - self::assertSame($attributes, iterator_to_array($container)); + self::assertSame($objects, iterator_to_array($container)); } public function test_attributes_has_type_checks_all_attributes(): void { - $attributes = new AttributesContainer(new stdClass()); + $attributes = new AttributesContainer($this->attribute(new stdClass())); self::assertTrue($attributes->has(stdClass::class)); self::assertFalse($attributes->has(DateTimeInterface::class)); @@ -51,11 +66,22 @@ public function test_attributes_of_type_filters_on_given_class_name(): void $object = new stdClass(); $date = new DateTime(); - $attributes = new AttributesContainer($object, $date); + $attributes = new AttributesContainer($this->attribute($object), $this->attribute($date)); $filteredAttributes = $attributes->ofType(DateTimeInterface::class); self::assertContainsEquals($date, $filteredAttributes); self::assertNotContains($object, $filteredAttributes); self::assertSame($date, $filteredAttributes[0]); } + + /** + * @return AttributeParam + */ + private function attribute(object $object): array + { + return [ + 'class'=> $object::class, + 'callback' => fn () => $object + ]; + } }