Skip to main content

Laravel Guidelines

Attribution

These guidelines are adapted from spatie.be/guidelines/laravel and tailored for the Saucebase stack.

General PHP Rules

Code style must follow PSR-1, PSR-2, and PSR-12. Formatting is enforced automatically by Laravel Pint — run composer lint before committing.

Nullable and Union Types

Whenever possible use the short nullable notation of a type, instead of using a union with null.

// Good
public function getUser(?int $id): ?User

// Bad
public function getUser(int|null $id): User|null

Void Return Types

If a method returns nothing, it should be indicated with void. This makes it more clear to the reader that the method intentionally returns nothing.

// Good
public function handle(): void
{
// ...
}

Typed Properties

You should type a class property whenever possible.

class Foo
{
public string $bar;
}

Enums

Enum cases should use TitleCase. Enum values should use snake_case or kebab-case for string-backed enums.

// Good
enum Status: string
{
case Active = 'active';
case SoftDeleted = 'soft_deleted';
}

// Bad
enum Status: string
{
case active = 'active';
case SOFT_DELETED = 'soft_deleted';
}

Docblocks

Don't use docblocks for methods that can be fully type hinted (unless you need a description).

Only add a description when it provides more context than the method signature itself. Use full sentences for descriptions, including a period at the end.

// Good
class Url
{
public static function fromString(string $url): Url
{
// ...
}
}

// Bad: The description is redundant and the `@param` and `@return` are duplicated.
class Url
{
/**
* Create a url from a string.
*
* @param string $url
*
* @return \Spatie\Url\Url
*/
public static function fromString(string $url): Url
{
// ...
}
}

When dealing with arrays of objects, add the correct type hint via PHPDoc:

/** @param User[] $users */
public function notifyUsers(array $users): void
{
// ...
}

Constructor Property Promotion

Use constructor property promotion for simple dependencies.

// Good
class Foo
{
public function __construct(
protected readonly Bar $bar,
protected readonly Baz $baz,
) {}
}

// Bad
class Foo
{
protected Bar $bar;
protected Baz $baz;

public function __construct(Bar $bar, Baz $baz)
{
$this->bar = $bar;
$this->baz = $baz;
}
}

Traits

Each applied trait should go on its own line, and the use keyword must be used for each trait.

// Good
class MyModel extends Model
{
use HasFactory;
use SoftDeletes;
}

// Bad
class MyModel extends Model
{
use HasFactory, SoftDeletes;
}

Strings

When possible, prefer string interpolation over sprintf or concatenation with ..

// Good
$greeting = "Hello, {$user->name}!";

// Okay
$greeting = 'Hello, ' . $user->name . '!';

If Statements

Happy Path Last

Generally a function should have its unhappy path first and its happy path last. This keeps the main logic unindented and easier to read.

// Good
if (! $goodCondition) {
throw new Exception;
}

// do work
// Bad
if ($goodCondition) {
// do work
}

throw new Exception;

Avoid Else

Avoid else after a return or throw. This keeps nesting flat.

// Good
if ($conditionA) {
// if body
return;
}

if ($conditionB) {
// if body
return;
}

// default behavior

// Bad
if ($conditionA) {
// if body
} elseif ($conditionB) {
// if body
} else {
// default behavior
}

Compound Ifs

In general, separate if statements should be preferred over a compound condition. This makes debugging easier.

// Good
if (! $conditionA) {
return;
}

if (! $conditionB) {
return;
}

if (! $conditionC) {
return;
}

// do stuff

// Bad
if (! $conditionA || ! $conditionB || ! $conditionC) {
return;
}

// do stuff

Whitespace

Statements should have to breathe. Add blank lines between groups of statements, but don't add blank lines inside a single block.

// Good
public function getPage(string $url): ?Page
{
$page = $this->pages()->where('url', $url)->first();

if (! $page) {
return null;
}

if ($page->deleted_at) {
return null;
}

return $page;
}

// Bad: too compressed
public function getPage(string $url): ?Page
{
$page = $this->pages()->where('url', $url)->first();
if (! $page) {
return null;
}
if ($page->deleted_at) {
return null;
}
return $page;
}

Don't add any extra empty lines between a { and the first statement in a block, or between the last statement and }.

Configuration

Configuration values must never be used directly in code via env(). Always proxy them through a config file.

// Good
$apiKey = config('services.mailgun.key');

// Bad
$apiKey = env('MAILGUN_KEY');

Artisan Commands

Command names should use kebab-case and should be namespaced with a colon.

# Good
php artisan export:users
php artisan generate:api-keys

# Bad
php artisan exportUsers
php artisan export_users

Commands should always provide feedback on what they're doing, using $this->info(), $this->warn(), etc.

Routing

Public-facing URLs must use kebab-case.

// Good
/open-source
/jobs-at-spatie

// Bad
/openSource
/jobs_at_spatie

Route names must use camelCase.

// Good
Route::get('open-source', [OpenSourceController::class, 'index'])->name('openSource');

// Bad
Route::get('open-source', [OpenSourceController::class, 'index'])->name('open-source');

All routes have an HTTP verb. Route names for resource controllers follow Laravel conventions: index, create, store, show, edit, update, destroy.

Controllers

Controllers should use PascalCase and end with Controller. Controllers that handle a single resource should be named after that resource. Use resource controllers whenever possible, limiting methods to the seven standard actions.

// Good
class UsersController extends Controller {}
class UserSettingsController extends Controller {}

// Bad
class UserControllerNew extends Controller {}
class ManageUsersController extends Controller {}

Views

View file names should use camelCase and must correspond to the controller method or Inertia page.

// Controller
return inertia('Blog::Index'); // modules/Blog/resources/js/pages/Index.vue
return inertia('Blog::PostShow'); // modules/Blog/resources/js/pages/PostShow.vue

Validation

Always use a dedicated Form Request class. Never inline $request->validate() in a controller.

// Good
class StoreUserRequest extends FormRequest
{
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'unique:users'],
];
}
}

public function store(StoreUserRequest $request): RedirectResponse
{
// ...
}

Authorization

Prefer policies over inline $this->authorize() calls in controllers. Register them in the model's $policies array or via AuthServiceProvider.

Naming Classes

Naming things is often seen as one of the harder things in programming. That's why we've established some high-level guidelines for how to name various "types" of classes.

SuffixExampleWhen to use
ControllerUsersControllerHandles HTTP requests for a resource
RequestStoreUserRequestValidates and authorises a form submission
ResourceUserResourceAPI JSON transformer
PolicyUserPolicyAuthorisation rules for a model
ObserverUserObserverReacts to model events
JobSendWelcomeEmailJobQueued task
MailWelcomeMailMailable class
NotificationInvoicePaidNotificationNotification class
SeederUsersDatabaseSeederDatabase seeder
FactoryUserFactoryModel factory
CommandGenerateApiKeysCommandArtisan command
MiddlewareEnsureUserIsAdminHTTP middleware
ServiceProviderNavigationServiceProviderService provider
PluginBillingPluginFilament plugin