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), notType|null. - Always use the
voidreturn 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
finalby 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
usestatements. Never use inline fully qualified class names such as\Exceptionor\Illuminate\Support\Facades\Http. - Never use single-letter variable names. Write
$exceptioninstead of$e,$requestinstead 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 staticwhen 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
ifstatements 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 theconfig()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/1over/projects/1/errors/1/error-occurrences/1 - Nest only when the context is necessary:
/errors/1/occurrences
Controllers
- Plural resource name plus a
Controllersuffix:PostsController - Stick to the CRUD keywords:
index,create,store,show,edit,update,destroy - Extract new controllers for non-CRUD actions (for example
FavoritePostsControllerwithstoreanddestroy) - 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
showwithview
Migrations
- Only write
upmethods in migrations. Don't writedownmethods.
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
$fillableinstead of$guarded = []in package models - Adding spaces after Blade directives (
@if ($condition)) - Putting extra empty lines inside
{}brackets - Using
finalon 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
usestatement - Single-letter variable names such as
$e - String controller references (
'Controller@method') instead of tuple notation - Using
elsewhere early returns work - Deep API route nesting when a flat route suffices
- Creating new config files for service credentials (use
services.php) - Forgetting the
voidreturn type on methods that return nothing - Writing
downmethods in migrations - Introducing a
private constinstead 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.