php-static-analysis/attributes
PHP attributes that let you annotate classes, methods, and properties with metadata for static analysis tools, improving type inference, purity checks, and IDE support. Lightweight, framework-agnostic, and easy to drop into existing codebases.
composer require --dev php-static-analysis/attributes
Install it as a dev dependency: the attributes are metadata for analyzers, not runtime behavior, so most projects do not need them in production require. If you are publishing a library whose public API uses these attributes, install them in require instead and keep the constraint loose, or consumers may not be able to autoload the attribute classes.
Where to look first:
src/ — the attribute class list is the actual API surface. This package is small; the value is knowing exactly which attribute names exist and what each one is intended to signal.README.md — check the support matrix. A common issue with attribute packages is that the attribute is provided here but only consumed by a specific analyzer version (or only by a plugin). Do not assume PHPStan, Psalm, and PhpStorm all honor the same set./** @pure */ → #[Pure] on a method, or /** @internal */ → #[Internal] on a class.First use case: annotate a stateless utility method as #[Pure] and a value object as #[Immutable], then run PHPStan/Psalm at your current level and confirm the analyzer actually changes its inference. If output does not change, the attribute is not wired into your toolchain yet — fix that before rolling it out.
Annotate at the narrowest scope. Put #[Pure] on individual methods rather than whole classes unless every method qualifies. Same for #[Internal] — class-level is fine for a whole implementation namespace, method-level is better for mixed public APIs.
Replace, don't duplicate. If the analyzer already understands the docblock equivalent, do not keep both. Pick attributes as the canonical form, and update code review rules so contributors don't reintroduce @pure/@internal docblocks that silently drift.
Use it as an API contract.
#[Pure] / #[NoReturn] on factory and guard methods so the analyzer can reason about control flow.#[Immutable] / #[ReadOnly] on DTOs and value objects to prevent accidental mutation in calling code.#[Internal] on classes outside your supported public API, then enforce with a rule that flags cross-package usage.#[Deprecated] on public methods before removal; combine with phpstan-deprecation-rules if available in your stack so internal call sites fail CI.Suppress narrowly. If the package exposes a suppression attribute, prefer it over inline @phpstan-ignore-line / @psalm-suppress because it keeps suppression metadata discoverable and greppable. Always include the specific identifier and a reason: a suppression without a code and a comment is the same as disabling the rule.
Layer with existing extensions. Use the attributes to express intent, and reach for first-party extensions for types. Attributes cannot express generics or template bounds; keep @template, @param, and @return docblocks for that and use attributes only for purity, immutability, deprecation, and visibility hints.
CI pattern. Pin the analyzer and this package together. Add a job that runs PHPStan/Psalm at a fixed level so attribute changes are validated continuously; otherwise a version bump to the attributes package can change analyzer results without any code change in your repo.
Migration pattern. Convert one module at a time, run the analyzer, and diff the reported errors. Attributes are declarations; if converting a docblock to an attribute removes an error, the analyzer was reading it — if it does not, it wasn't.
(new ReflectionMethod(Foo::class, 'bar'))->getAttributes() should return the attribute, and the class must be autoloadable for it to instantiate.#[Pure] is a promise, not a check. If you mark a method pure that reads a global or mutates state, the analyzer will trust you and produce false negatives. Wrong annotations are worse than none — they hide bugs. Keep purity annotations close to review and add a test that exercises the method.#[Immutable] semantics vary between tools. One analyzer may treat it as "no public setters," another as "all properties readonly." Read the definition your analyzer uses before annotating aggregate objects with nested mutable state.require-dev but your shipping code references them, consumers get "class not found" errors the moment reflection touches the class. Either move to require or keep attributes strictly in code paths only your own toolchain scans.composer dump-autoload -o and verify with composer why php-static-analysis/attributes that exactly one version is installed.composer.json and upgrade deliberately, reviewing the CI diff.TypeSpecifier/DynamicMethodReturnTypeExtension or a Psalm handler that reads your own attribute. That keeps semantics honest and lets the analyzer do real reasoning rather than trusting a misused marker.readonly-related annotations may require 8.1+. Check composer.json before adopting if you support older runtimes in libraries.How can I help you explore Laravel packages today?