skip to content

In PHP, what do Attribute::TARGET_* and Attribute::IS_REPEATABLE control on a custom attribute, and when is a violation actually reported?

level: middleimportance: should knowfreq 28%

answer

  1. bitmask passed to #[Attribute(...)]
  2. bare #[Attribute] = TARGET_ALL, not repeatable
  3. userland: checked in newInstance()
  4. built-in attributes: fatal at compile time
  5. getTarget() and isRepeated() on ReflectionAttribute

basics

~20 s

The 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 s

The 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
<?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

for a junior

Know that #[Attribute] can take flags that limit placement and allow repetition, and that the default allows any target once.

for a middle

Explain that userland flags are enforced only inside newInstance(), with the exact Error it throws, while built-in attributes fail at compile time.

for a senior

Make placement errors fail in CI by instantiating every framework attribute in a test, and handle repeatable attributes by iterating all occurrences.

for a principal

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.