fix: handle native attribute on promoted parameter

Handles race condition when the attribute is affected to a property or 
parameter that was promoted, in this case the attribute will be applied
to both `ParameterReflection` and `PropertyReflection`, but the target
argument inside the attribute class is configured to support only one of
them (parameter or property).

More details: https://wiki.php.net/rfc/constructor_promotion#attributes
This commit is contained in:
Radhi Guennichi 2022-07-31 15:42:58 +02:00 committed by GitHub
parent 17328d86db
commit 897ca9b65e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 59 additions and 3 deletions

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Definition;
use CuyZ\Valinor\Definition\Exception\InvalidReflectionParameter;
use Error;
use ReflectionAttribute;
use ReflectionClass;
use ReflectionMethod;
@ -28,9 +29,23 @@ final class NativeAttributes implements Attributes
{
$this->reflectionAttributes = $this->attributes($reflection);
$attributes = array_map(
static fn (ReflectionAttribute $attribute) => $attribute->newInstance(),
$this->reflectionAttributes
$attributes = array_filter(
array_map(
static function (ReflectionAttribute $attribute) {
try {
return $attribute->newInstance();
} 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
// ParameterReflection AND PropertyReflection, BUT the target arg inside the attribute
// class is configured to support only ONE of them (parameter OR property)
// https://wiki.php.net/rfc/constructor_promotion#attributes for more details.
// Ignore attribute if the instantiation failed.
return null;
}
},
$this->reflectionAttributes,
),
);
$this->delegate = new AttributesContainer(...$attributes);

View File

@ -0,0 +1,10 @@
<?php
namespace CuyZ\Valinor\Tests\Fixture\Attribute;
use Attribute;
#[Attribute(Attribute::TARGET_PROPERTY)]
final class PropertyTargetAttribute
{
}

View File

@ -6,6 +6,7 @@ namespace CuyZ\Valinor\Tests\Fixture\Object;
use CuyZ\Valinor\Tests\Fixture\Attribute\AttributeWithArguments;
use CuyZ\Valinor\Tests\Fixture\Attribute\BasicAttribute;
use CuyZ\Valinor\Tests\Fixture\Attribute\PropertyTargetAttribute;
#[BasicAttribute]
#[AttributeWithArguments('foo', 'bar')]
@ -16,6 +17,10 @@ final class ObjectWithAttributes
#[AttributeWithArguments('foo', 'bar')]
public bool $property;
public function __construct(#[PropertyTargetAttribute] public bool $promotedProperty)
{
}
#[BasicAttribute]
#[AttributeWithArguments('foo', 'bar')]
public function method(#[BasicAttribute] string $parameter): void

View File

@ -14,12 +14,14 @@ use CuyZ\Valinor\Tests\Fixture\Annotation\BasicAnnotation;
use CuyZ\Valinor\Tests\Fixture\Attribute\AttributeWithArguments;
use CuyZ\Valinor\Tests\Fixture\Attribute\BasicAttribute;
use CuyZ\Valinor\Tests\Fixture\Attribute\NestedAttribute;
use CuyZ\Valinor\Tests\Fixture\Attribute\PropertyTargetAttribute;
use CuyZ\Valinor\Tests\Fixture\Object\ObjectWithAttributes;
use CuyZ\Valinor\Tests\Fixture\Object\ObjectWithNestedAttributes;
use Error;
use PHPUnit\Framework\TestCase;
use ReflectionClass;
use ReflectionMethod;
use ReflectionParameter;
use ReflectionProperty;
use stdClass;
@ -322,6 +324,30 @@ final class AttributesCompilerTest extends TestCase
self::assertTrue($attributes->has(NestedAttribute::class));
}
/**
* @requires PHP >= 8
*/
public function test_compiles_native_php_attributes_for_promoted_property_with_property_target_attribute(): void
{
$reflection = new ReflectionProperty(ObjectWithAttributes::class, 'promotedProperty');
$attributes = $this->compile(new NativeAttributes($reflection));
self::assertCount(1, $attributes);
self::assertTrue($attributes->has(PropertyTargetAttribute::class));
}
/**
* @requires PHP >= 8
*/
public function test_compiles_an_empty_attributes_instance_for_promoted_parameter_with_property_target_attribute(): void
{
$reflection = new ReflectionParameter([ObjectWithAttributes::class, '__construct'], 'promotedProperty');
$attributes = $this->compile(new NativeAttributes($reflection));
self::assertCount(0, $attributes);
self::assertInstanceOf(EmptyAttributes::class, $attributes);
}
private function compile(Attributes $attributes): Attributes
{
$code = $this->attributesCompiler->compile($attributes);