skip to content

In PHP 8, how do native attributes written as #[...] differ from docblock annotations such as @Route inside a /** */ comment?

level: juniorimportance: should knowfreq 45%

answer

  1. one is grammar, one is comment text
  2. names resolved against use imports
  3. getAttributes() vs getDocComment()
  4. opcache.save_comments=0 strips docblocks
  5. #[ started a comment before PHP 8.0

basics

~20 s

Attributes are real PHP syntax: the engine parses them, resolves their names like class names and returns them through Reflection's getAttributes(). Docblock annotations are comment text a userland library must fetch with getDocComment() and parse itself.

solid answer

~40 s

Since PHP 8.0 an attribute such as `#[Route('/users')]` is part of the grammar: a malformed one is a parse error, its name is resolved against the file's namespace and `use` imports, and its arguments must be constant expressions. Reflection hands it back as `ReflectionAttribute` objects via `getAttributes()`. A docblock annotation like `@Route("/users")` is only text inside `/** */`: PHP never checks it, `getDocComment()` returns the raw string or `false`, and a library must tokenize it and resolve names itself. Docblocks also vanish when OPcache runs with `opcache.save_comments=0`; attributes are kept. What the engine does *not* check is that the attribute class exists: that only surfaces when `newInstance()` is called. And neither form acts on its own — both are inert metadata until code reads them.

code

php · 23 lines
php
<?php
declare(strict_types=1);

namespace App\Http;

use Attribute;

#[Attribute(Attribute::TARGET_METHOD)]
final class Route
{
    public function __construct(public readonly string $path) {}
}

final class UserController
{
    /** @Route("/legacy-users") */
    #[Route('/users')]
    public function list(): void {}
}

$m = new \ReflectionMethod(UserController::class, 'list');
var_dump($m->getDocComment());                 // raw string: a library must parse it
var_dump($m->getAttributes()[0]->getName());   // string(14) "App\Http\Route"

go deeper

for a junior

Recognise the #[Name(args)] syntax, know it arrived in PHP 8.0, and say that attributes are read through Reflection while docblocks are only comment text.

for a middle

Explain name resolution through use imports, the constant-expression limit on arguments, and that the attribute class is looked up only at newInstance().

for a senior

Point out the operational difference: opcache.save_comments=0 silently breaks docblock-driven code, and attribute typos stay invisible until something instantiates them, so tests should.

for a principal

Weigh migrating a codebase from annotation parsing to attributes: removed parser dependency and caching versus the effort of converting every annotation and the PHP 8 floor it imposes.

## Two ways to attach metadata to PHP code Frameworks have long wanted to say things *about* a declaration: this method answers `GET /users`, this property must be an e-mail, this class is a database entity. Before PHP 8.0 the only place to write that was a **docblock** — a `/** ... */` comment — using an annotation convention such as `@Route("/users")`. PHP 8.0 added **attributes**, written `#[Route('/users')]`, as native syntax for the same job. The two look similar on the page, but they live in different worlds: one is part of the language, the other is a comment that happens to contain structured text. ## What the engine does with an attribute When PHP compiles a file containing `#[Route('/users', methods: ['GET'])]`, it: - **parses** it as grammar, so `#[Route('/users'` without the closing bracket is a parse error, not a silently ignored comment; - **resolves the name** exactly like a class name: `Route` becomes `App\Http\Route` through the file's `namespace` and `use` statements; - **checks the arguments** are constant expressions — literals, constants, `new` (since 8.1), arrays, arithmetic, and since 8.5 static closures and first-class callables. A function call such as `strtolower('/X')` fails with "Constant expression contains invalid operations"; - **stores** the attribute on the declaration, where `ReflectionClass`, `ReflectionFunctionAbstract` (functions and methods), `ReflectionProperty`, `ReflectionClassConstant`, `ReflectionParameter` and, in 8.5, `ReflectionConstant` expose it through `getAttributes()`. What the engine deliberately does **not** do at compile time is look up the attribute class. `#[Rout('/x')]` with a typo compiles fine; the error `Attribute class "Rout" not found` only appears when something calls `ReflectionAttribute::newInstance()`. ## What a docblock annotation is A docblock is a comment. PHP keeps its text and `getDocComment()` returns it as one string (or `false` when there is none). Everything else is the library's job: 1. fetch the raw comment string; 2. tokenize the `@Name(...)` syntax the library invented; 3. resolve `Name` against the file's `use` imports, which means reading the source file again; 4. build objects from the parsed arguments, usually caching the result because the parsing is slow. A typo in a docblock annotation is invisible to PHP. And because docblocks are comments, they are at the mercy of configuration: with `opcache.save_comments=0` (the default is `1`) OPcache drops doc comments and `getDocComment()` returns `false`, so annotation-driven code silently stops working. Attributes are persisted by OPcache regardless of that setting. ## Side by side | Aspect | `#[Route('/users')]` attribute | `@Route("/users")` docblock | |---|---|---| | Parsed by | the PHP engine | a userland library | | Syntax error | parse error at compile time | ignored, or a library error at run time | | Name resolution | engine, via `use` imports | library re-reads the source file | | Arguments | constant expressions, named args allowed | whatever the library's mini-language accepts | | Read with | `getAttributes()` returning `ReflectionAttribute` | `getDocComment()` returning a string | | `opcache.save_comments=0` | unaffected | comment removed | | Class must exist | only when `newInstance()` runs | depends on the library | ## What stays the same - **Both are inert.** A custom attribute does nothing by being present; a router, validator or container has to read it. The exceptions are the few built-in attributes the engine itself consumes, such as `#[\Deprecated]` or `#[\SensitiveParameter]`. - **Both describe, neither enforces a type contract.** Attributes are not interfaces: a class carrying `#[Entity]` gains no methods. - **Docblocks keep a role** for prose documentation and for PHPDoc types read by static analysers, which attributes did not replace. ## The PHP 7 to PHP 8 transition Before 8.0, `#` began a single-line comment, so a line such as `#[Route('/users')]` was a comment to PHP 7 and an attribute to PHP 8. Libraries used that to ship attribute syntax while still supporting PHP 7. The flip side is a backward-compatibility break: PHP 8.0 no longer treats `#[` as the start of a comment, so old code with comments beginning `#[` had to change. In current PHP 8.5 codebases, new metadata goes into attributes; annotation parsing survives mostly in older libraries.

  • Does a custom PHP attribute change behaviour just by being on a method?
    No. A userland attribute is stored metadata and nothing more; the method runs exactly the same until a router, validator or container calls `getAttributes()` and acts on the result. Only a handful of built-in attributes, such as `#[\Deprecated]`, `#[\NoDiscard]` or `#[\SensitiveParameter]`, are consumed by the engine itself.
  • Why could some libraries write attributes on their own line while still supporting PHP 7?
    Before PHP 8.0, `#` started a single-line comment, so `#[Route('/users')]` alone on a line was ignored by PHP 7 and read as an attribute by PHP 8. The same change was a small break: since 8.0, `#[` no longer starts a comment.

A shipping label with a printed barcode versus a handwritten note taped to the box: the sorting machine can read and check the barcode's format, while the note needs a clerk to interpret it. Either one moves the parcel only when something at the depot actually reads it.

saying these in an interview costs you the question

  • Attributes are just a shorter syntax for docblock comments.
  • PHP checks that the attribute class exists when the file compiles.
  • Attribute arguments can call any function, like an ordinary constructor call.
  • A custom attribute changes a method's behaviour simply by being present.
  • getDocComment() returns the same text under every OPcache configuration.