Spatie Guidelines
Apply these guidelines when writing code for Spatie projects or contributing to Spatie packages.
Core principle: write things the way Laravel intended. If Laravel has a documented way to do something, use it. Deviate only with a clear justification.
PHP Style
Type System
- Type properties, parameters, and return types. Skip docblocks for fully typed methods.
- Use
?Type (short nullable), not Type|null.
- Always use the
void return type when a method returns nothing.
- Use constructor property promotion when all properties can be promoted. One per line, trailing comma:
class MyClass {
public function __construct(
protected string $firstArgument,
protected string $secondArgument,
) {}
}
Docblocks
- Skip docblocks for fully type-hinted methods unless you need a description.
- Use full sentences with a period for descriptions.
- Always import classnames, then reference the short name in the docblock. Never write a fully qualified name inside a docblock:
use Spatie\Url\Url;
/** @return Url */
- Use one-line docblocks when possible:
/** @var string */
- Always add types for iterables, specifying both key and value:
/**
* @param array<int, MyObject> $myArray
* @param int $typedArgument
*/
function someFunction(array $myArray, int $typedArgument) {}
- Put the most common type first in a multi-type docblock:
/** @var Collection|SomeWeirdVendor\Collection */
- Use array shape notation for fixed keys, with each key on its own line:
/** @return array{
first: SomeClass,
second: SomeClass
} */
- If one parameter needs a docblock, add docblocks for all the other parameters too.
Code Style
- Follow PSR-1, PSR-2, and PSR-12.
- Use camelCase for non-public-facing strings.
- Don't use
final by default.
- Prefer string interpolation over concatenation:
"Hi, I am {$name}."
- Enum values use PascalCase:
case Diamonds;
- Each trait on its own line with its own
use:
class MyClass
{
use TraitA;
use TraitB;
}
- Always import namespaces with
use statements. Never use inline fully qualified class names such as \Exception or \Illuminate\Support\Facades\Http.
- Never use single-letter variable names. Write
$exception instead of $e, $request instead of $r.
Avoid private const
Don't introduce private const. Replace each one:
- Used once: inline it. When a raw literal would obscure the meaning the name carried, assign it to a descriptively named local variable instead.
- Used more than once: turn it into a private property. Use
private static when the methods using it are static.
- Referenced from a default parameter value or a PHP attribute, where no property can be used: inline the literal there.
Control Flow
- Happy path last. Handle failure conditions first and return early.
- Avoid
else. Refactor to early returns or ternaries.
- Separate compound ifs. Prefer nested
if statements over && chains.
- Always use curly brackets, even for a single statement.
- Ternary operators: keep each part on its own line unless the expression is very short.
- Add blank lines between statements so the code can breathe. The exception is a sequence of equivalent single-line operations.
- No extra empty lines between
{} brackets.
// Happy path last
if (! $user) {
return null;
}
if (! $user->isActive()) {
return null;
}
// Process active user...
// Short ternary
$name = $isFoo ? 'foo' : 'bar';
// Multi-line ternary
$result = $object instanceof Model ?
$object->name :
'A default value';
// Ternary instead of else
$condition
? $this->doSomething()
: $this->doSomethingElse();
// Bad: compound condition with &&
if ($user->isActive() && $user->hasPermission('edit')) {
$user->edit();
}
// Good: nested ifs
if ($user->isActive()) {
if ($user->hasPermission('edit')) {
$user->edit();
}
}
Comments
Be very critical about adding comments. They often become outdated and mislead over time. Code should be self-documenting through descriptive variable and function names. Adding a comment should never be the first tactic for making code readable.
Instead of this:
// Get the failed checks for this site
$checks = $site->checks()->where('status', 'failed')->get();
Do this:
$failedChecks = $site->checks()->where('status', 'failed')->get();
- Don't add comments that describe what the code does. Make the code describe itself.
- Short, readable code doesn't need comments explaining it.
- Use descriptive variable names instead of generic names plus a comment.
- Only add a comment to explain why something non-obvious is done, never what is being done.
- Refactor explanatory comments into descriptively named methods.
- Never add comments to tests. Test names should be descriptive enough.
Laravel Conventions
Configuration
- Config filenames: kebab-case (
media-library.php, pdf-generator.php)
- Config keys: snake_case (
'chrome_path' => env('CHROME_PATH'))
- Never use
env() outside config files. Use the config() helper.
- Service-specific config goes in
config/services.php, not a new file.
Routing
- URLs: kebab-case (
/open-source, /front-end-developer)
- Route names: camelCase (
->name('openSource'))
- Route parameters: camelCase (
{newsItem}, {userId})
- HTTP verb first:
Route::get('open-source', [OpenSourceController::class, 'index'])
- Use tuple notation
[Controller::class, 'method'], not the string 'Controller@method'
- Don't prefix URLs with
/, except the root /
API Routing
- Plural resource names:
/errors, /error-occurrences
- Kebab-case resources
- Limit deep nesting. Prefer
/error-occurrences/1 over /projects/1/errors/1/error-occurrences/1
- Nest only when the context is necessary:
/errors/1/occurrences
Controllers
- Plural resource name plus a
Controller suffix: PostsController
- Stick to the CRUD keywords:
index, create, store, show, edit, update, destroy
- Extract new controllers for non-CRUD actions (for example
FavoritePostsController with store and destroy)
- Use invokable controllers for single actions:
PerformCleanupController
Views & Blade
- View files: camelCase (
openSource.blade.php)
- Indent with 4 spaces.
- No spaces after directives:
@if($condition)
- Use
__() for translations, not @lang
Validation
- Always use array notation:
['required', 'email'], never the pipe form 'required|email'. Array notation is easier to combine with custom rule classes.
public function rules() {
return [
'email' => ['required', 'email'],
];
}
- Custom rules use snake_case:
Validator::extend('organisation_type', function ($attribute, $value) {
return OrganisationType::isValid($value);
});
Authorization
- Policies use camelCase:
Gate::define('editPost', ...)
- Use CRUD words, but replace
show with view
Migrations
- Only write
up methods in migrations. Don't write down methods.
Artisan Commands
- Command names: kebab-case (
delete-old-records)
- Always output feedback. At minimum, a
$this->comment('All ok!') at the end.
- For batch processing, output progress per item and a summary at the end.
- Put the output before processing the item, which makes debugging a failure easier:
$items->each(function(Item $item) {
$this->info("Processing item id `{$item->id}`...");
$this->processItem($item);
});
$this->comment("Processed {$items->count()} items.");
Naming Classes
| Type |
Convention |
Example |
| Controller |
Plural + Controller |
PostsController |
| Invokable Controller |
Action + Controller |
PerformCleanupController |
| Model |
Singular |
Post |
| Job |
Action description |
CreateUser, SendEmailNotification |
| Event |
Tense indicates timing |
UserRegistering / UserRegistered |
| Listener |
Action + Listener |
SendInvitationMailListener |
| Command |
Action + Command |
PublishScheduledPostsCommand |
| Mailable |
Event/action + Mail |
AccountActivatedMail |
| Resource |
Plural + Resource |
UsersResource |
| Enum |
Descriptive, no prefix |
OrderStatus, BookingType |
Common Mistakes to Avoid
- Using
env() outside config files
- Using pipe notation for validation rules (
'required|email')
- Using
$fillable instead of $guarded = [] in package models
- Adding spaces after Blade directives (
@if ($condition))
- Putting extra empty lines inside
{} brackets
- Using
final on classes (Spatie doesn't by default)
- Docblocks on fully type-hinted methods without descriptions
- Fully qualified class names inside docblocks instead of an imported short name
- Inline fully qualified class names in code instead of a
use statement
- Single-letter variable names such as
$e
- String controller references (
'Controller@method') instead of tuple notation
- Using
else where early returns work
- Deep API route nesting when a flat route suffices
- Creating new config files for service credentials (use
services.php)
- Forgetting the
void return type on methods that return nothing
- Writing
down methods in migrations
- Introducing a
private const instead of inlining it or using a private property
Detailed references
Load these as needed:
- Package architecture:
references/package-architecture.md. Structure, service providers, contracts, model and config patterns.
- Testing with Pest:
references/testing-pest.md. Testbench setup, test style, what to test, helpers, composer stack.
- Git workflow:
references/version-control.md. Branch naming, PR workflow, commit conventions.
- Laravel and PHP style, in depth:
references/laravel-php.md.
- JavaScript style:
references/javascript.md.
- New project setup:
references/new-project-setup.md.
1---2name: spatie-guidelines3description: Spatie's PHP, Laravel, JavaScript and Vue coding guidelines and conventions. Use when writing or reviewing PHP, Laravel, JavaScript, or Vue code for Spatie projects or packages. Covers code style, type declarations, docblocks, control flow, naming, routing, controllers, Blade, validation, comments, testing (Pest), package structure, service providers, and Git workflow. Triggers include "follow Spatie guidelines", "Spatie style", "Spatie package", or any code review for Spatie packages and projects.4license: MIT5---67# Spatie Guidelines89Apply these guidelines when writing code for Spatie projects or contributing to Spatie packages.1011**Core principle:** write things the way Laravel intended. If Laravel has a documented way to do something, use it. Deviate only with a clear justification.1213---1415## PHP Style1617### Type System1819- Type properties, parameters, and return types. Skip docblocks for fully typed methods.20- Use `?Type` (short nullable), not `Type|null`.21- Always use the `void` return type when a method returns nothing.22- Use constructor property promotion when all properties can be promoted. One per line, trailing comma:2324```php25class MyClass {26 public function __construct(27 protected string $firstArgument,28 protected string $secondArgument,29 ) {}30}31```3233### Docblocks3435- Skip docblocks for fully type-hinted methods unless you need a description.36- Use full sentences with a period for descriptions.37- **Always import classnames, then reference the short name in the docblock.** Never write a fully qualified name inside a docblock:3839```php40use Spatie\Url\Url;4142/** @return Url */43```4445- Use one-line docblocks when possible: `/** @var string */`46- Always add types for iterables, specifying both key and value:4748```php49/**50 * @param array<int, MyObject> $myArray51 * @param int $typedArgument52 */53function someFunction(array $myArray, int $typedArgument) {}54```5556- Put the most common type first in a multi-type docblock:5758```php59/** @var Collection|SomeWeirdVendor\Collection */60```6162- Use array shape notation for fixed keys, with each key on its own line:6364```php65/** @return array{66 first: SomeClass,67 second: SomeClass68} */69```7071- If one parameter needs a docblock, add docblocks for all the other parameters too.7273### Code Style7475- Follow PSR-1, PSR-2, and PSR-12.76- Use camelCase for non-public-facing strings.77- Don't use `final` by default.78- Prefer string interpolation over concatenation: `"Hi, I am {$name}."`79- Enum values use PascalCase: `case Diamonds;`80- Each trait on its own line with its own `use`:8182```php83class MyClass84{85 use TraitA;86 use TraitB;87}88```8990- Always import namespaces with `use` statements. Never use inline fully qualified class names such as `\Exception` or `\Illuminate\Support\Facades\Http`.91- Never use single-letter variable names. Write `$exception` instead of `$e`, `$request` instead of `$r`.9293### Avoid `private const`9495Don't introduce `private const`. Replace each one:9697- **Used once:** inline it. When a raw literal would obscure the meaning the name carried, assign it to a descriptively named local variable instead.98- **Used more than once:** turn it into a private property. Use `private static` when the methods using it are static.99- **Referenced from a default parameter value or a PHP attribute**, where no property can be used: inline the literal there.100101### Control Flow102103- **Happy path last.** Handle failure conditions first and return early.104- **Avoid `else`.** Refactor to early returns or ternaries.105- **Separate compound ifs.** Prefer nested `if` statements over `&&` chains.106- Always use curly brackets, even for a single statement.107- Ternary operators: keep each part on its own line unless the expression is very short.108- Add blank lines between statements so the code can breathe. The exception is a sequence of equivalent single-line operations.109- No extra empty lines between `{}` brackets.110111```php112// Happy path last113if (! $user) {114 return null;115}116117if (! $user->isActive()) {118 return null;119}120121// Process active user...122123// Short ternary124$name = $isFoo ? 'foo' : 'bar';125126// Multi-line ternary127$result = $object instanceof Model ?128 $object->name :129 'A default value';130131// Ternary instead of else132$condition133 ? $this->doSomething()134 : $this->doSomethingElse();135136// Bad: compound condition with &&137if ($user->isActive() && $user->hasPermission('edit')) {138 $user->edit();139}140141// Good: nested ifs142if ($user->isActive()) {143 if ($user->hasPermission('edit')) {144 $user->edit();145 }146}147```148149### Comments150151Be very critical about adding comments. They often become outdated and mislead over time. Code should be self-documenting through descriptive variable and function names. Adding a comment should never be the first tactic for making code readable.152153*Instead of this:*154155```php156// Get the failed checks for this site157$checks = $site->checks()->where('status', 'failed')->get();158```159160*Do this:*161162```php163$failedChecks = $site->checks()->where('status', 'failed')->get();164```165166- Don't add comments that describe what the code does. Make the code describe itself.167- Short, readable code doesn't need comments explaining it.168- Use descriptive variable names instead of generic names plus a comment.169- Only add a comment to explain *why* something non-obvious is done, never *what* is being done.170- Refactor explanatory comments into descriptively named methods.171- Never add comments to tests. Test names should be descriptive enough.172173---174175## Laravel Conventions176177### Configuration178179- Config filenames: **kebab-case** (`media-library.php`, `pdf-generator.php`)180- Config keys: **snake_case** (`'chrome_path' => env('CHROME_PATH')`)181- Never use `env()` outside config files. Use the `config()` helper.182- Service-specific config goes in `config/services.php`, not a new file.183184### Routing185186- URLs: **kebab-case** (`/open-source`, `/front-end-developer`)187- Route names: **camelCase** (`->name('openSource')`)188- Route parameters: **camelCase** (`{newsItem}`, `{userId}`)189- HTTP verb first: `Route::get('open-source', [OpenSourceController::class, 'index'])`190- Use tuple notation `[Controller::class, 'method']`, not the string `'Controller@method'`191- Don't prefix URLs with `/`, except the root `/`192193### API Routing194195- Plural resource names: `/errors`, `/error-occurrences`196- Kebab-case resources197- Limit deep nesting. Prefer `/error-occurrences/1` over `/projects/1/errors/1/error-occurrences/1`198- Nest only when the context is necessary: `/errors/1/occurrences`199200### Controllers201202- **Plural** resource name plus a `Controller` suffix: `PostsController`203- Stick to the CRUD keywords: `index`, `create`, `store`, `show`, `edit`, `update`, `destroy`204- Extract new controllers for non-CRUD actions (for example `FavoritePostsController` with `store` and `destroy`)205- Use invokable controllers for single actions: `PerformCleanupController`206207### Views & Blade208209- View files: **camelCase** (`openSource.blade.php`)210- Indent with 4 spaces.211- No spaces after directives: `@if($condition)`212- Use `__()` for translations, not `@lang`213214### Validation215216- Always use array notation: `['required', 'email']`, never the pipe form `'required|email'`. Array notation is easier to combine with custom rule classes.217218```php219public function rules() {220 return [221 'email' => ['required', 'email'],222 ];223}224```225226- Custom rules use **snake_case**:227228```php229Validator::extend('organisation_type', function ($attribute, $value) {230 return OrganisationType::isValid($value);231});232```233234### Authorization235236- Policies use **camelCase**: `Gate::define('editPost', ...)`237- Use CRUD words, but replace `show` with `view`238239### Migrations240241- Only write `up` methods in migrations. Don't write `down` methods.242243### Artisan Commands244245- Command names: **kebab-case** (`delete-old-records`)246- Always output feedback. At minimum, a `$this->comment('All ok!')` at the end.247- For batch processing, output progress per item and a summary at the end.248- Put the output *before* processing the item, which makes debugging a failure easier:249250```php251$items->each(function(Item $item) {252 $this->info("Processing item id `{$item->id}`...");253 $this->processItem($item);254});255256$this->comment("Processed {$items->count()} items.");257```258259### Naming Classes260261| Type | Convention | Example |262|------|-----------|---------|263| Controller | Plural + `Controller` | `PostsController` |264| Invokable Controller | Action + `Controller` | `PerformCleanupController` |265| Model | Singular | `Post` |266| Job | Action description | `CreateUser`, `SendEmailNotification` |267| Event | Tense indicates timing | `UserRegistering` / `UserRegistered` |268| Listener | Action + `Listener` | `SendInvitationMailListener` |269| Command | Action + `Command` | `PublishScheduledPostsCommand` |270| Mailable | Event/action + `Mail` | `AccountActivatedMail` |271| Resource | Plural + `Resource` | `UsersResource` |272| Enum | Descriptive, no prefix | `OrderStatus`, `BookingType` |273274---275276## Common Mistakes to Avoid277278- Using `env()` outside config files279- Using pipe notation for validation rules (`'required|email'`)280- Using `$fillable` instead of `$guarded = []` in package models281- Adding spaces after Blade directives (`@if ($condition)`)282- Putting extra empty lines inside `{}` brackets283- Using `final` on classes (Spatie doesn't by default)284- Docblocks on fully type-hinted methods without descriptions285- Fully qualified class names inside docblocks instead of an imported short name286- Inline fully qualified class names in code instead of a `use` statement287- Single-letter variable names such as `$e`288- String controller references (`'Controller@method'`) instead of tuple notation289- Using `else` where early returns work290- Deep API route nesting when a flat route suffices291- Creating new config files for service credentials (use `services.php`)292- Forgetting the `void` return type on methods that return nothing293- Writing `down` methods in migrations294- Introducing a `private const` instead of inlining it or using a private property295296---297298## Detailed references299300Load these as needed:301302- **Package architecture**: `references/package-architecture.md`. Structure, service providers, contracts, model and config patterns.303- **Testing with Pest**: `references/testing-pest.md`. Testbench setup, test style, what to test, helpers, composer stack.304- **Git workflow**: `references/version-control.md`. Branch naming, PR workflow, commit conventions.305- **Laravel and PHP style, in depth**: `references/laravel-php.md`.306- **JavaScript style**: `references/javascript.md`.307- **New project setup**: `references/new-project-setup.md`.