DEV Community

Cover image for PHP Attributes Can Help Your Docs, But They Won’t Save Them
Saqueib Ansari
Saqueib Ansari

Posted on • Originally published at qcode.in

PHP Attributes Can Help Your Docs, But They Won’t Save Them

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)
{
    // ...
}
Enter fullscreen mode Exit fullscreen mode

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
    {
        // ...
    }
}
Enter fullscreen mode Exit fullscreen mode

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.view separated from users.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/

Top comments (0)