In PHP, what do Attribute::TARGET_* and Attribute::IS_REPEATABLE control on a custom attribute, and when is a violation actually reported?
answer
- bitmask passed to #[Attribute(...)]
- bare #[Attribute] = TARGET_ALL, not repeatable
- userland: checked in newInstance()
- built-in attributes: fatal at compile time
- getTarget() and isRepeated() on ReflectionAttribute
basics
~20 sThe TARGET_* flags restrict which declarations an attribute may sit on and IS_REPEATABLE allows it more than once per declaration. For userland attributes PHP enforces both only when ReflectionAttribute::newInstance() runs, by throwing an Error; built-in attributes fail at compile time.
solid answer
~30 sThe bitmask passed to `#[Attribute(...)]` combines `TARGET_CLASS`, `TARGET_FUNCTION`, `TARGET_METHOD`, `TARGET_PROPERTY`, `TARGET_CLASS_CONSTANT`, `TARGET_PARAMETER`, `TARGET_CONSTANT` (8.5, global constants) or `TARGET_ALL`, plus `IS_REPEATABLE`. A bare `#[Attribute]` means `TARGET_ALL` and not repeatable. For a **userland** attribute, compilation accepts any placement; `getAttributes()`, `getName()` and `getArguments()` still work, and only `newInstance()` throws an `Error` such as `Attribute "Route" cannot target class (allowed targets: method)` or `Attribute "Route" must not be repeated`. **Built-in** attributes like `#[\SensitiveParameter]` are checked by the compiler, so a misplaced one is a fatal error when the file loads; PHP 8.5's `#[\DelayedTargetValidation]` postpones that to `newInstance()`.
code
php · 21 lines<?php
declare(strict_types=1);
#[Attribute(Attribute::TARGET_METHOD)]
final class Route
{
public function __construct(public readonly string $path) {}
}
#[Route('/oops')] // wrong target: compiles fine
final class HomeController {}
$attr = (new ReflectionClass(HomeController::class))->getAttributes()[0];
var_dump($attr->getArguments()); // array(1) { [0]=> string(5) "/oops" }
try {
$attr->newInstance();
} catch (Error $e) {
echo $e->getMessage(), PHP_EOL;
// Attribute "Route" cannot target class (allowed targets: method)
}go deeper
Know that #[Attribute] can take flags that limit placement and allow repetition, and that the default allows any target once.
Explain that userland flags are enforced only inside newInstance(), with the exact Error it throws, while built-in attributes fail at compile time.
Make placement errors fail in CI by instantiating every framework attribute in a test, and handle repeatable attributes by iterating all occurrences.
Decide how strict a framework's attribute contract should be, trading early build failures against flexibility for third-party code that reuses the attributes.
## The flags When you declare an attribute class, the argument to the `#[Attribute]` marker is an integer **bitmask** built from class constants of the built-in `Attribute` class: | Constant | Allows the attribute on | |---|---| | `Attribute::TARGET_CLASS` | classes, including enums and anonymous classes | | `Attribute::TARGET_FUNCTION` | functions and closures | | `Attribute::TARGET_METHOD` | methods | | `Attribute::TARGET_PROPERTY` | properties | | `Attribute::TARGET_CLASS_CONSTANT` | class constants | | `Attribute::TARGET_PARAMETER` | function and method parameters | | `Attribute::TARGET_CONSTANT` | global `const` declarations (PHP 8.5) | | `Attribute::TARGET_ALL` | all of the above | | `Attribute::IS_REPEATABLE` | more than one occurrence per declaration | Flags are combined with `|`: `#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]` lets a `Route` attribute appear several times on one method, which suits a controller action reachable at two paths. A **bare** `#[Attribute]` uses the constructor default, `Attribute::TARGET_ALL`, which does *not* include `IS_REPEATABLE` — so by default an attribute may go anywhere but only once per declaration. ## When userland violations surface This is the part candidates most often get wrong. For an attribute class **you** wrote, the compiler does not know the class's flags — it has not even loaded the class. So: - a `Route` placed on a class, or written twice when not repeatable, **compiles without complaint**; - `getAttributes()` still lists it, and `getName()`, `getArguments()`, `getTarget()` and `isRepeated()` all work; - only `ReflectionAttribute::newInstance()` loads the class, compares the flags with where the attribute was found, and throws an `Error`: - `Attribute "Route" cannot target class (allowed targets: method)` - `Attribute "Route" must not be repeated` The consequence: a wrongly placed attribute is harmless and invisible until the consumer instantiates it. If your router reads only `getArguments()`, the flags are never enforced at all. ## Built-in attributes are different Attributes that the engine itself implements — `#[\SensitiveParameter]`, `#[\Deprecated]`, `#[\NoDiscard]`, `#[\AllowDynamicProperties]` and others — are registered with the compiler. Their target and repetition rules are checked **while the file compiles**, and a violation is a fatal error. Some also run extra validators: `#[\Deprecated]` on a class other than a trait fails with `Cannot apply #[\Deprecated] to class ...`. PHP 8.5 added `#[\DelayedTargetValidation]` for forward compatibility. Placed on a declaration, it turns an invalid-target error for the built-in attributes there into an `Error` thrown by `newInstance()`, so a library can use an attribute on a target that only a newer PHP version supports without breaking older ones. ## Related 8.5 change Before 8.5, putting `#[Attribute]` itself on an abstract class, interface, trait or enum was accepted and failed later at `newInstance()`. PHP 8.5 reports it at compile time. ## Inspecting flags and targets in code The marker itself is an ordinary attribute, so a tool can read an attribute class's own configuration: `(new ReflectionClass(Route::class))->getAttributes(Attribute::class)[0]->newInstance()->flags` returns the integer bitmask, and a bitwise test such as `$flags & Attribute::IS_REPEATABLE` answers whether repetition is allowed. On the usage side, `ReflectionAttribute::getTarget()` returns the single `TARGET_*` constant describing where that occurrence was found, and `isRepeated()` reports whether the same attribute appears more than once on the declaration. | Situation | Userland attribute | Built-in attribute | |---|---|---| | Wrong target | `Error` from `newInstance()` | fatal error at compile time | | Repeated without `IS_REPEATABLE` | `Error` from `newInstance()` | fatal error at compile time | | Class missing | `Error` from `newInstance()` | not applicable | | With `#[\DelayedTargetValidation]` | no change | wrong target moves to `newInstance()` | ## Practical guidance 1. Always pass explicit targets for framework attributes; `TARGET_ALL` hides placement mistakes. 2. Add `IS_REPEATABLE` only when repetition has a meaning, and read every occurrence rather than index `[0]`. 3. Instantiate every attribute your framework owns during a build step or test, so placement errors fail CI instead of a request. 4. Use `getTarget()` when one attribute class supports several targets and must behave differently on each.
- How does a PHP router read a repeatable #[Route] correctly?It loops over every `ReflectionAttribute` returned by `getAttributes(Route::class)` and instantiates each one, registering a route per occurrence. Code that takes only `[0]` silently drops the extra paths; `isRepeated()` tells you whether more than one exists.
- What does #[\DelayedTargetValidation] change in PHP 8.5?On the same declaration, an invalid target for a built-in attribute no longer stops compilation; instead `newInstance()` on that attribute throws. It exists so libraries can use a built-in attribute on a target a newer PHP allows. It does not skip the attribute's other checks.
saying these in an interview costs you the question
- A misplaced custom attribute stops the file from compiling.
- A bare #[Attribute] makes the attribute repeatable by default.
- getAttributes() throws when an attribute sits on a forbidden target.
- TARGET_* flags are hints for IDEs and PHP never enforces them.
- Built-in and custom attributes are validated at the same moment.