Attributes were first introduced in attributes_v2, with the current syntax adopted in shorter_attribute_syntax_change. Enums were added in enumerations, with enums targeted the same as classes and enum cases targeted the same as class constants. With the addition of compile-time non-class constants from attributes-on-constants, attributes can now target:
The targets that an attribute supports are indicated in the declaration, e.g. the #[\Override]
attribute is declared with
#[Attribute(Attribute::TARGET_METHOD)] final class Override { public function __construct() {} }
which indicates that the attribute can be used only for methods.
Although some of the implementation details changed since the attributes_v2 RFC, a key distinction that that RFC drew still remains:
This proposal differentiates between two different kinds of attributes:
Compiler Attributes (validated at compile time) Userland Attributes (validated during Reflection API access)
This validation includes ensuring that an attribute is only used in the places that it supports - e.g. the #[\Override]
attribute cannot be used on a non-class function.
However, the compile-time validation means that any errors cannot be caught, nor code gated behind version flags. Accordingly, this RFC proposes a new attribute, #[\DelayedTargetValidation]
, that, when applied, delays any errors from compiler attributes until runtime.
Let there be a new attribute:
#[\Attribute(\Attribute::TARGET_ALL)] final class DelayedTargetValidation { public function __construct() {} }
When this attribute is present on a target, then for any other attributes provided by PHP (as opposed to defined in userland) on the same target, any errors from a bad target are ignored at compile time, and instead emitted at runtime the same way userland attributes have targets validated.
Consider, for example, the #[\Override]
attribute introduced in PHP 8.3. Since PHP 5.3, it has been possible for child classes to override the class constants of parent classes. However, the attribute cannot currently be used for class constants.
Additionally, since at least PHP 5.0 it has been possible for child classes to override the class properties of parent classes (e.g. to expand visibility). However, the #[\Override]
attribute cannot be used for class properties either.
Let us suppose that in PHP 8.6, it became possible to use #[\Override]
on class constants, and in PHP 8.7 it also became possible to use #[\Override]
on class properties. To be clear, this RFC does not propose expanding the targets of #[\Override]
; the point of #[\DelayedTargetValidation]
if to allow for future compatability if a later RFC expands the targets. For the sake of demonstration, the example below assumes that the targets of #[\Override]
are expanded in 8.6 and 8.7.
If a library author wanted to annotate that their class constant, method, and property all override the parent class:
class Base { public const NAME = '<base>'; protected int $id; protected function getName(): string { return static::NAME; } } class Child extends Base { #[\Override] public const NAME = 'Child'; // Just increasing visibility #[\Override] public int $id; // Add the ID to the name #[\Override] protected function getName(): string { return parent::getName() . $this->id; } }
If the author wanted to make use of #[\Override]
in this manner, the library would require PHP 8.7, because in PHP 8.6 and below, the compile-time validation of the attribute would complain.
However, the author may not want to drop support for PHP 8.5 and 8.6, and thus refrain from using the attribute for a few years, even when the code is being run on PHP 8.7.
Instead, under this RFC, the author could use:
class Child extends Base { #[\DelayedTargetValidation] #[\Override] public const NAME = 'Child'; // Just increasing visibility #[\DelayedTargetValidation] #[\Override] public int $id; // Add the ID to the name #[\Override] protected function getName(): string { return parent::getName() . $this->id; } }
Which would result in the following:
This new attribute is intended for backward compatibility. The runtime errors from Reflection can be caught if needed, but otherwise the code will run and the inapplicable attributes will just be ignored.
To be clear, this RFC does not propose that #[\Override]
be usable on class constants or properties. The inspiration for this RFC was the discussion in the internals mailing list about expanding the places where #[\Deprecated]
could be used - if that attribute gets new targets for PHP 8.6, having the #[\DelayedTargetValidation]
attribute in 8.5 would allow libraries to use the #[\Deprecated]
attribute in more places (that PHP 8.6 supports) without triggering errors on PHP 8.5.
The DelayedTargetValidation
class in the global namespace is now provided by PHP and cannot be used in userland; a GitHub search for "class DelayedTargetValidation"
revealed 0 hits as of 2025.06.12, so the impact should be minimal, but noted here for completion.
Code that expects ReflectionAttribute::newInstance()
to never throw if the attribute is provided by PHP rather than userland may need to be adjusted.
Next version of PHP (8.5).
ReflectionAttribute::newInstance()
currently skips target validation for internal attributes, on the assumption that the validation was done at compile-time; it will need to be updated.
Make sure there are no open issues when the vote starts!
The places where core attributes actually provide functionality (e.g. verifying that a method really overrides a parent methods when using #[\Override]
) are unaffected by this attribute; it only delays the errors that would be reported from using the attribute in the wrong places.
In the case of #[\Override]
, for example, it does not suppress errors about a method not actually overriding a parent method - that is the functionality of the attribute, rather than the target validation.
This section details areas where the feature might be improved in future, but that are not currently proposed in this RFC.
Implement the #[\DelayedTargetValidation] attribute as described?
After the project is implemented, this section should contain
#[\Deprecated]
RFC, see https://news-web.php.net/php.internals/127486#[\Deprecated]
uses, https://news-web.php.net/php.internals/127305#[\Override]
attribute RFC, marking_overriden_methods#[\Deprecated]
attribute RFC, deprecated_attribute
On the mailing list, some contributors suggested that target validation for compile-time attributes be by default moved from compile time to run time for all internal attributes, without requiring this new #[\DelayedTargetValidation]
attribute to be used. However, changing this would be problematic.
There is a fundamental difference between the way that internal attributes and userland attributes are used. Internal attributes are used to hook into the engine and modify the engine behavior. While they can be instantiated with ReflectionAttribute::getInstance()
, accessing the internal attributes via reflection is not required and is not usually done. The presence of the attribute triggers the desired engine behavior, and accessing it via reflection is orthogonal to the attribute's functionality.
Userland attributes, on the other hand, are entirely used for metadata, and are only usable via reflection. For example, when symfony started adding support for using attributes to configure things, the attributes were used as an alternative to parsing doc comments. See e.g. https://symfony.com/blog/new-in-symfony-5-2-php-8-attributes. Userland attributes are not validated at compile time because they do not need to be valid, the engine doesn't care about them. They are meant to be used by userland code. Attributes can be applied that don't even exist, and the error isn't raised until you try to use ReflectionAttribute::getInstance()
with them.
Because internal attributes are applied with the intention of modifying engine behavior rather than being accessed via reflection, it makes sense that the attempt to modify engine behavior triggers validation at compile time. Otherwise, an error about misusing an attribute would likely never be raised, but the desired engine behavior would not be modified. However, there are times (e.g. for backward compatibility) where the developer wants to make a conscious choice to delay the validation from compile time to runtime, and that is what the proposed #[\DelayedTargetValidation]
attribute allows developers to do.