· via dev.to (home feed)
Laravel 13 pushes model config into attributes PHP never checks until newInstance()
Laravel 13 moves Eloquent model settings into attributes such as #[Table]. Because PHP resolves attribute classes lazily, framework-free code can carry them without failing at runtime.

Laravel 13, released on March 17, 2026 with PHP 8.3 as its minimum version, moves a large share of framework configuration into PHP attributes. Alongside that shift sits a subtlety that a recent dev.to post by Gabriel Anhaia argues developers should grasp before upgrading: PHP does not load or verify an attribute's class when the annotated class is compiled or instantiated. A model attribute like #[Table] is stored as a name plus arguments, and the referenced class is only resolved if something calls newInstance() on it through reflection.
What moved into attributes
According to the post, the Eloquent documentation for 13.x now presents model configuration through attributes from the Illuminate\Database\Eloquent\Attributes namespace. #[Table] accepts the table name together with named arguments including key, keyType, incrementing, timestamps and dateFormat. #[Fillable] can stand in for the $fillable property, and the serialization docs position #[Hidden] and #[Visible] as replacements for $hidden and $visible. The release notes extend the pattern beyond models: #[Middleware] and #[Authorize] on controllers, #[Tries], #[Backoff] and #[Timeout] on jobs, and further attributes spread across Eloquent, events, notifications, validation, testing and resource serialization.
On a class extending Model, this changes little architecturally, since inheritance already binds the class to Eloquent and PHP cannot load it without the framework. The case the author singles out is a plain class with no framework parent that carries one of these attributes anyway.
How PHP handles attributes
The attributes RFC that landed in PHP 8.0 made a deliberate trade-off. At compile time PHP resolves an attribute's name against the file's imports and records that name and its argument expressions, but it does not autoload the attribute class or check that the arguments match a constructor. Per the RFC, validation happens only when ReflectionAttribute::newInstance() is invoked, declaring the class is technically optional, and getAttributes() is specified never to throw. Compiler-defined attributes are the exception; userland ones are never checked at compile time.
The import statement is equally inert. The PHP manual describes importing as a compile-time operation, a name alias resolved without involving the autoloader.
The scenario that slips through
The post walks through a domain package with its own composer.json, no Laravel among its requirements, and tests that pass with the framework absent. A pull request adds #[Table('orders')] above an entity plus the matching use line, and nothing breaks: the class compiles, instantiating it works, getAttributes() returns a reflection object, getName() reports the resolved class name and getArguments() returns the stored arguments. Only newInstance(), the single call a framework would make, throws an Error, because no autoloader can supply the attribute class. The author adds a caveat that he did not execute that exact script himself, so the walkthrough reflects documented behaviour rather than captured output.
Why the quiet dependency still costs you
Even though the code runs, the import is real and the domain namespace now names a class from Illuminate\. The post lays out three consequences. The entity now encodes storage details, since the table name is exactly what hexagonal architecture pushes into an adapter, yet the next person renaming the table edits a domain class. Nothing at runtime surfaces the problem, because domain tests never reflect over attributes and Laravel never reads the attribute on a non-model; only something that reflects over every attribute, such as a generic mapper, a serializer, or a later refactor promoting the class to a model, would hit the failure. And the pattern spreads easily, because attributes are compact and appear innocuous, and an AI coding agent that has just read several models using them will reach for the same pattern on the next class it writes.
Guarding with static analysis
Because PHP treats this behaviour as correct, the post recommends catching it in tooling that reads the AST, where the attribute name sits in plain sight. Deptrac's use analyser examines import statements, so a single use line is enough for it to register the dependency; placing the framework in its own layer and denying the domain layer any dependencies blocks it. Deptrac also ships an attribute collector useful for the reverse check, such as grouping classes carrying a persistence attribute. For an error message that names the exact offending line, the post suggests a custom PHPStan rule.
Why it matters
Attribute-based configuration makes framework code terser, but it also makes accidental coupling invisible to both the runtime and ordinary tests. A framework-free package can drift into depending on Laravel through a line that no test executes and no constructor requires. Teams upgrading to Laravel 13, or letting AI agents write code against it, should treat static analysis as the enforcement point for architectural boundaries, because PHP itself will never raise the alarm.
- #laravel
- #php
- #eloquent
- #attributes
- #static-analysis