PHP attributes are a useful documentation tool, but they are a terrible documentation strategy on their own. That is the short version.
The appeal is obvious. Attributes live next to the code. They are structured, machine-readable, and hard to ignore during implementation. For Laravel and PHP teams building APIs, policies, commands, or internal frameworks, that sounds like the perfect answer to documentation drift.
It is not.
Attributes are excellent for capturing metadata close to the code. They are weak at explaining intent, tradeoffs, workflow, and reader context. If you treat them as the whole documentation system, you usually end up with reference output that is technically populated but editorially useless.
My recommendation is simple: use attributes as a source layer, not as the finished docs. They are best when they feed a stronger documentation system that still has structure, narrative, and human judgment.
What attributes are actually good at
PHP attributes were built for structured metadata, not prose. The PHP manual describes them as machine-readable metadata attached to classes, methods, properties, parameters, and more, accessible through reflection: PHP attributes overview.
That makes them very good at things code already knows for sure:
- route-level metadata
- validation or schema hints
- authorization intent
- serialization rules
- OpenAPI field and response definitions
- command signatures or handler registration
In other words, attributes shine when the documentation value is declarative and local.
A basic example looks clean for a reason:
<?php
use OpenApi\Attributes as OA;
use App\Http\Resources\UserResource;
use Illuminate\Http\Request;
#[OA\Get(
path: '/api/users/{user}',
summary: 'Fetch a single user',
tags: ['Users']
)]
#[OA\Parameter(name: 'user', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))]
#[OA\Response(
response: 200,
description: 'User returned successfully',
content: new OA\JsonContent(ref: UserResource::class)
)]
public function show(Request $request, int $user)
{
// ...
}
That is a good use of attributes because the metadata is tightly coupled to the code surface. The route shape, parameter location, and basic response contract belong close to the handler. If the endpoint changes, the attribute should change with it.
This is also why tools like swagger-php lean hard into attributes now. Its docs treat attributes as a first-class documentation path, and its annotation model is already moving toward deprecation in favor of attributes: swagger-php and attribute reference.
So the pro-attribute case is real. They reduce one kind of drift: the drift between implementation details and machine-readable reference data.
Where attribute-driven docs become a trap
The problem starts when teams confuse structured metadata with useful documentation.
A generated reference can be technically complete and still fail its reader.
That usually happens in three predictable ways.
1. The docs become local but not meaningful
Attributes are great at saying what exists. They are bad at saying why it matters.
A generated API page may tell you an endpoint accepts status, page, and sort, and still never answer the questions developers actually care about:
- Which filters are stable API contracts versus convenience helpers?
- What combinations are slow or discouraged?
- What defaults are business-critical?
- What failure cases should clients design around?
- When should this endpoint not be used at all?
Attributes do not naturally answer those questions because those answers are not local facts. They are editorial explanations.
2. The reference starts lying through omission
Teams often trust generated docs too much because they look official. But a clean generated page can hide serious gaps:
- examples are missing
- response semantics are underspecified
- auth behavior is implied, not stated
- pagination edge cases are absent
- business rules live in service code, not in the documented contract
This is the dangerous version of drift. The docs are not obviously stale. They are plausibly incomplete, which is worse because people trust them longer.
3. Attributes accumulate faster than they get curated
Attributes feel cheap to add, so teams add them everywhere. That is fine at first. Then the codebase gets noisy.
A controller method ends up carrying routing metadata, OpenAPI metadata, security metadata, response metadata, and internal framework metadata all in one stack. At that point, you did not create self-documenting code. You created a metadata wall.
That wall has two costs:
- developers stop reading it carefully
- the generated docs inherit the same lack of focus
The output may still validate. That does not mean it communicates.
The real comparison: attributes versus editorial docs
The useful comparison is not attributes versus no attributes. It is attributes-only docs versus attributes feeding an editorial documentation layer.
Attributes-only documentation wins on:
- proximity to code
- machine readability
- generation speed
- lower risk of obvious schema drift
- better tooling automation
Editorial documentation wins on:
- decision-making context
- examples with intent
- warning readers about failure modes
- explaining patterns across endpoints or modules
- helping new developers understand how the system is supposed to be used
That is why attributes are a shortcut and a drift trap at the same time.
If you stay purely manual, the docs drift because humans forget. If you stay purely attribute-driven, the docs drift because the generated output captures only the pieces the generator can see.
The best systems split the job.
- Attributes own structured truth that should stay near code.
- Generated references expose that truth consistently.
- Editorial docs explain how to use the system well.
This is the same pattern that works in Laravel apps generally. Route definitions are not architecture docs. Validation rules are not onboarding docs. Resource classes are not product guidance. They are inputs into a bigger understanding.
Documentation should be designed the same way.
Where attributes help most in Laravel codebases
Laravel teams get the best results from attributes when they use them on contract surfaces, not as a universal writing system.
Good targets include:
- API endpoint metadata
- request and response schema mapping
- policy or permission registration
- event, listener, or command discovery
- internal package hooks where reflection already exists
Bad targets are usually the places where explanation matters more than declaration.
For example, this is a reasonable attribute shape because it captures local truth:
<?php
#[RequiresPermission('users.view')]
#[AuditAction('user.viewed')]
#[CacheTtl(seconds: 60)]
final class ShowUserAction
{
public function __invoke(int $userId): UserData
{
// ...
}
}
Those attributes tell your framework or tooling something concrete. They can also be surfaced in generated internal reference material.
But they still do not answer bigger questions:
- Why is
users.viewseparated fromusers.list? - Why is the cache only 60 seconds?
- What user states are intentionally hidden?
- What audit guarantees should downstream systems expect?
Those answers belong in docs written for humans, not in ever-growing attribute payloads.
A useful rule of thumb
If a fact is:
- enforced by code,
- introspectable by reflection,
- and meaningful as structured metadata,
then an attribute is a good home for it.
If a fact requires:
- rationale,
- examples,
- sequencing,
- tradeoff discussion,
- or warnings about misuse,
then it should not live only in attributes.
That line saves teams from turning code into a documentation dumping ground.
How to keep generated docs honest
The failure mode is not using attributes. The failure mode is shipping generated output with no editorial contract around it.
The easiest fix is to define a small documentation architecture instead of hoping generators produce finished work.
1. Decide what attributes are allowed to own
Be explicit. Do not let every team invent its own doctrine.
For example:
- attributes own route, schema, auth, and response metadata
- markdown docs own tutorials, workflows, migration notes, and decision rules
- changelogs own release deltas
- examples live in tested fixtures or example requests
That sounds obvious, but most teams skip it. Then they wonder why half the documentation lives in controllers and the other half in Confluence gravesites.
2. Generate references, then curate entry points
A generated OpenAPI page is not a developer experience. It is a reference artifact.
You still need curated entry points such as:
- “Start here” guides
- common workflows
- auth setup
- pagination and rate-limit behavior
- versioning rules
- examples that reflect real client use
Generated reference should support those pages, not replace them.
3. Treat examples as first-class documentation
This is where attribute-driven systems are usually weak. They capture schemas well and examples poorly.
If your docs generator supports examples, use them aggressively. If it does not, keep tested examples close to the codebase and pull them into editorial docs. A technically correct schema without one realistic example is far less useful than teams admit.
4. Review attributes like API surface, not like decoration
Attribute changes should be treated as contract changes when they affect external behavior. That means code review should ask questions like:
- Did the response contract change?
- Is the status code still correct?
- Are nullable fields still truthful?
- Does the summary reflect actual use, not just implementation detail?
If reviewers treat attributes like harmless syntax garnish, the docs will rot even though they are generated.
5. Add drift checks that matter
The best attribute-based documentation setups fail loudly when the generated output and committed artifacts diverge.
In practice, that means things like:
- generating OpenAPI in CI
- diffing generated spec files
- rejecting undocumented breaking changes
- testing examples when possible
Automation cannot create good prose, but it can stop bad reference drift from slipping through quietly.
The shape I would recommend
For a Laravel or broader PHP team, I would use a three-layer model.
Layer 1: attributes for structured source truth
Use PHP attributes for metadata that is local, machine-readable, and tied directly to code.
That includes route contracts, schemas, auth hints, and other declarative surface details.
Layer 2: generated reference for consistency
Use tools like swagger-php to produce API reference artifacts from that source truth. Let generators handle the repetitive shape of endpoint docs so humans do not waste effort retyping stable contract data.
Layer 3: editorial docs for meaning
Write short, opinionated docs that explain how the system is actually used:
- which endpoints matter most
- common integration flows
- business caveats
- performance traps
- migration notes
- examples that reflect reality
That last layer is the one teams try to skip. It is also the layer that separates “technically documented” from “actually usable.”
The practical recommendation
PHP attributes are absolutely worth using for documentation-related metadata. They reduce one of the most annoying forms of drift by keeping structured facts close to the implementation. For Laravel teams especially, that is a real win.
But they are not a substitute for documentation design.
If you want docs that developers trust, use attributes to capture reference truth, use generation to keep that truth consistent, and use editorial writing to explain how the system should be used in practice. That is the balance that works.
The decision rule is simple: attributes should document what the code can assert confidently; humans should document what readers still need help understanding.
Read the full post on QCode: https://qcode.in/php-attributes-as-documentation-useful-shortcut-or-drift-trap/