Skip to content

Commit

Permalink
fix: load attributes lazily during runtime and cache access
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
romm committed Aug 19, 2023
1 parent 1617930 commit 02102ed
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 23 deletions.
32 changes: 25 additions & 7 deletions src/Definition/AttributesContainer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<object> */
/** @var list<AttributeParam> */
private array $attributes;

public function __construct(object ...$attributes)
/**
* @no-named-arguments
* @param AttributeParam ...$attributes
*/
public function __construct(array ...$attributes)
{
$this->attributes = $attributes;
}
Expand All @@ -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;
}
}
Expand All @@ -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
));
}

Expand All @@ -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']();
}
}
}
7 changes: 6 additions & 1 deletion src/Definition/NativeAttributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 3 additions & 9 deletions src/Definition/Repository/Cache/Compiler/AttributesCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down
38 changes: 32 additions & 6 deletions tests/Unit/Definition/AttributesContainerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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));
Expand All @@ -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
];
}
}

0 comments on commit 02102ed

Please sign in to comment.