sinemacula / coding-standards-laravel
Laravel-specific coding standards, static-analysis rules, and code-quality tooling for Sine Macula's Laravel repositories.
Package info
github.com/sinemacula/coding-standards-laravel
Type:phpcodesniffer-standard
pkg:composer/sinemacula/coding-standards-laravel
Requires
- php: ^8.3
- composer/semver: ^3.0
- friendsofphp/php-cs-fixer: ^3.0
- sinemacula/coding-standards: ^1.0
Requires (Dev)
- dealerdirect/phpcodesniffer-composer-installer: ^1.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan: ^2.1
- phpstan/phpstan-phpunit: ^2.0
- phpunit/phpunit: ^12
- slevomat/coding-standard: ^8.0
- squizlabs/php_codesniffer: ^3.13
- symfony/console: ^7.4
Suggests
- phpstan/extension-installer: Auto-registers the Laravel PHPStan rules (required for the shared php/phpstan-laravel.neon to load).
- phpstan/phpstan-phpunit: PHPUnit-aware PHPStan rules. Auto-registers via phpstan/extension-installer.
This package is auto-updated.
Last update: 2026-06-25 21:37:43 UTC
README
Laravel-specific coding standards, static-analysis rules, and code-quality tooling for Sine Macula's Laravel repositories.
Install this only in Laravel projects. Framework-agnostic, language-wide standards live in
sinemacula/coding-standards; this package adds the
Laravel-specific layer. Non-Laravel repos simply don't install it - that is how the Laravel rules stay
scoped to Laravel projects (no runtime framework detection).
Installation
composer require --dev sinemacula/coding-standards-laravel
This brings sinemacula/coding-standards with it. You also need squizlabs/php_codesniffer,
dealerdirect/phpcodesniffer-composer-installer, and slevomat/coding-standard in your dev deps (as you
already do for the base standard).
Usage
Wire it into the PHP tools through the same Qlty plugin setup you already use for the base standard
(package_file = "composer.json" with package_filters = ["sinemacula", ...] in .qlty/qlty.toml - the
"sinemacula" filter already matches this package, so Qlty installs it into the linter tool environments
automatically).
PHPCS
Two standards ship; reference exactly one (each pulls in SineMacula, so it replaces it - don't reference both):
SineMaculaLaravel- for applications. The full standard, including the role-based structure rules (placement and naming of controllers, models, providers, …) and the controller rules.SineMaculaLaravelPackage- for libraries and packages. The same standard with the app-skeleton rules (Structure.*,Controllers.*) excluded, since a package is organised by domain rather than Laravel's app directory layout. Composition only - it redefines nothing.
<?xml version="1.0"?> <ruleset name="Project"> <rule ref="SineMaculaLaravel"/> <file>src</file> <file>tests</file> </ruleset>
PHPStan
The Laravel rules are auto-included via this package's extra.phpstan.includes (resolved by
phpstan/extension-installer), alongside the base config. Your project's phpstan.neon only needs its
own level / paths.
PHP CS Fixer
If Laravel-specific fixer rules are present, reference the Laravel factory from your
.php-cs-fixer.dist.php; otherwise keep using the base PhpCsFixerConfig::make().
Rules
These are the Laravel-specific rules this package adds on top of the base sinemacula/coding-standards.
A deliberate exception can be bypassed with the native directive - // phpcs:ignore <code> for a sniff,
@phpstan-ignore <identifier> for a rule.
PHPCS sniffs
| Sniff | Enforces |
|---|---|
SineMaculaLaravel.Architecture.DisallowServiceLocation |
No service location (app(), resolve(), App::make()) inside a class body - inject collaborators instead. Targets production code: test files, container-wiring classes (service providers and registrars), and dynamic resolution of a runtime variable (app($class), a factory that cannot be injected) are exempt. |
SineMaculaLaravel.Configuration.DisallowEnvOutsideConfig |
env() only inside config/ files (test code exempt); use config() everywhere else. |
SineMaculaLaravel.Controllers.DisallowDatabaseAccess |
No DB:: facade or direct Eloquent model queries in a controller - read through a repository. |
SineMaculaLaravel.Controllers.DisallowInlineValidation |
No inline validation ($request->validate(), Validator::make()) in a controller - use a form request. |
SineMaculaLaravel.Controllers.DisallowNonRestActions |
A controller's candidate actions (public, non-static instance methods) must be REST verbs or __invoke; statics, the constructor and framework overrides are auto-exempt. Mark a method @non-rest-action (a deliberate non-CRUD action) or @utility (not an action) to allow it. |
SineMaculaLaravel.Debug.DisallowDebugStatements |
No debug calls (dd, dump, ray, var_dump, print_r) in committed code. |
SineMaculaLaravel.Eloquent.DisallowLegacyAttributeAccessor |
No legacy getXAttribute() / setXAttribute() accessors on an Eloquent model - use Attribute::make(). Gated on both model identity (base class) and accessor arity, so look-alike methods on other classes are clean. |
SineMaculaLaravel.Services.DisallowHttpAbort |
No abort() / abort_if / abort_unless / HttpException in a service - throw a domain exception. |
SineMaculaLaravel.Structure.RequireBladeLocation |
A *.blade.php template must live under a resources/views (or module Resources/views) directory. |
SineMaculaLaravel.Structure.RequireRoleDirectory |
A class whose role is recognised by identity (what it extends/implements) must live under that role's directory - a controller under Http/Controllers; an entry-point provider may sit at the package root. |
SineMaculaLaravel.Structure.RequireRoleNaming |
A class is named for its role: controllers/providers/form-requests/resources/policies require a suffix, models forbid Model/Entity, and the rest (jobs, listeners, events, mailables, middleware, commands, casts, rules) stay bare. |
SineMaculaLaravel.Structure.RoutesLocation |
A routes.php file, if present, must sit at the root of an Http directory. |
SineMaculaLaravel.TypeHints.PropertyTypeHint |
A class property declares a native type - except the framework-magic properties ($table, $fillable, $signature, …, the configurable magicProperties set) that override an untyped parent and so cannot be typed. |
SineMaculaLaravel.TypeHints.ParameterTypeHint |
A function or method parameter declares a native type - except where a parent fixes the signature: a method carrying #[\Override], or a non-private trait method (whose effective parent is the consuming class's, invisible to a token sniff). |
SineMaculaLaravel.TypeHints.ReturnTypeHint |
A function, method or closure declares a native return type - except constructors/destructors/clone handlers and methods carrying #[\Override]. |
Role-based structure
RequireRoleNaming and RequireRoleDirectory resolve a class's role by identity first - what it
extends, implements, uses or is attributed with - and fall back to its location (a concrete
class under a role directory, minus exempt sub-namespaces such as Concerns/Support/Contracts). A
class with neither is unconstrained, so genuine domain classes are never flagged.
The default role table is convention-correct for Laravel: it never requires a suffix the framework
leaves bare, and the idiomatic bare User model is honoured (the Model role's identity covers
Authenticatable and Pivot). Every list - roleIdentities, roleLocations, requireSuffix,
forbidSuffix, exemptNamespaces, moduleRootRoles - is a public sniff property a ruleset can
override. Identity is matched on the immediate base by short name, so a project's own intermediate
base (e.g. a BaseController) is supported by adding it to roleIdentities.
Opt a class out entirely with an @role-exempt docblock tag or a #[NotARole] attribute.
Type hints
The TypeHints.* sniffs replace the base standard's Slevomat native-type requirements with
Laravel-aware equivalents (the base Slevomat MissingNativeTypeHint / MissingAnyTypeHint codes are
excluded; the traversable-specification checks are kept). They are needed because the Slevomat sniffs
are inheritance-blind: PHP forbids typing a property that overrides an untyped parent ($table,
$fillable, …) or changing an inherited method signature, so the original rules force types that
fatal at class load. Native types are still required everywhere else. The exempt property set is the
configurable magicProperties list on PropertyTypeHint, and an overriding method opts its
signature out with the native #[\Override] attribute. A non-private trait method is also exempt
from the parameter requirement, since its effective parent is whatever the consuming class extends
and a token sniff cannot resolve that.
Readonly properties
The base standard requires every public property to be readonly. An Eloquent model is exempt: it
overrides the framework's public magic properties ($timestamps, $incrementing, …), which are
declared non-readonly on the base class, and PHP forbids making an inherited property readonly. This
standard sets the base sniff's ignoredParentClasses to the model bases (Model, Authenticatable,
Pivot), matched by the immediate parent name as written.
PHPStan rules
| Identifier | Enforces |
|---|---|
sineMaculaLaravel.castsProperty |
No $casts property on an Eloquent model (a class extending Model/Authenticatable/Pivot) - use the casts() method. A non-model class with its own $casts is left alone. |
sineMaculaLaravel.datesProperty |
No $dates property on a model (deprecated) - cast dates via casts(). |
sineMaculaLaravel.massAssignment |
Every concrete production model declares mass assignment explicitly via $fillable/$guarded or the #[Fillable]/#[Guarded] attribute; models declared in tests are exempt. |
sineMaculaLaravel.relationshipReturnType |
A relationship method declares a return-type hint. |
sineMaculaLaravel.modelAttribute |
Prefer a model attribute over its legacy property/method form, for the attributes a project enables (default #[Table]/#[Fillable]/#[Hidden], configurable via sineMaculaLaravel.modelAttributes). The 13.2-only attributes are enforced only when the project's Laravel floor reaches 13.2 - taken from sineMaculaLaravel.minLaravelVersion or detected from the nearest composer.json; below that, or when undetectable, the property form is left alone. |
sineMaculaLaravel.migrationMethods |
A migration defines both up() and down(). |
sineMaculaLaravel.schemaNaming |
Table and column names in a migration use snake_case. Inspects the literal name arguments of the Schema table calls and the Blueprint column/index methods (value arguments and dynamic names are left alone). Digits are allowed (line_1, oauth2); only casing is enforced. |
sineMaculaLaravel.formRequestRules |
A form request (under Http\Requests) defines a rules() method. |
sineMaculaLaravel.factoryTimestamps |
A factory definition() must not set created_at / updated_at. |
sineMaculaLaravel.resourceFieldNaming |
Field keys in a resource's toArray() result use snake_case, nested arrays included. Inspects the string-literal keys of the array returned directly from toArray() on a JsonResource; computed keys, a non-literal return, and non-resource classes are left alone. Digits are allowed (line_1); only casing is enforced. |
Requirements
- PHP ^8.3
Testing
composer test # PHPUnit sniff/rule suite composer test:coverage # suite with Clover coverage output (requires Xdebug) composer analyse # PHPStan over the package's own sniffs and rules composer check # static analysis and lint via qlty composer format # format via qlty composer smells # duplication / complexity smells via qlty
Changelog
See CHANGELOG.md for a list of notable changes.
Contributing
Contributions are welcome. Please read CONTRIBUTING.md for guidelines on branching, commits, code quality, and pull requests.
Security
If you discover a security vulnerability, please report it responsibly. See SECURITY.md for the disclosure policy and contact details.
License
Licensed under the Apache License, Version 2.0.