Menu
«

PHP / Laravel Style Guide

This guide is originally forked from Spaties guidelines. We've made some adjustments to fit our needs and preferences.

About Laravel

First and foremost, Laravel provides the most value when you write things the way Laravel intended you to write. If there's a documented way to achieve something, follow it. Whenever you do something differently, make sure you have a justification for why you didn't follow the defaults.

PHP 8

Use the new shiny features that PHP provides where possible. Read about all the new features here:

Read more about these new things at:

Code style

Code style must follow PSR-12. Generally speaking, everything string-like that's not public-facing should use camelCase. Detailed examples on these are spread throughout the guide in their relevant sections.

We have extended the PSR-12 rulesets and exported the settings in order to make sure that everyone is formatting code in the same way.

Download our PHPStorm config file and go to PHPStorm -> Preferences -> Editor -> Code Style and press the cog wheel and press Import scheme -> IntelliJ IDEA code style XML

When working in legacy code bases, try to avoid re-formatting a complete file in your PRs. This makes it very difficult to properly review the PR.

Making things breathe

Code needs room to breathe. We keep one empty line between methods and functions.

To make conditionals readable we infuse some air there, too.

Bad:

if(!isset($user)){$user=User::find(1);}

Good:

if (! isset($user)) {
    $user = User::find(1);
}

Add one space after type casts:

Bad:

$active = (boolean)request('active');

Good:

$active = (bool) request('active');

String interpolation

When using a variable/property inside a string, use curly braces when needed or sprintf to make it more readable.

Curly braces are generally only needed for when accessing a property on an object with the array syntax.

This is not a hard rule and should not be complained about in PRs.

Bad:

echo "Hello {$user->name}";
echo "Hello {$userName}";

Good:

echo "Hello $user->name";
echo "Hello $userName";
echo "Hello {$user['name']}";
echo sprintf('Hello %s, you have %d new notifications', $user->name, $notifications);

PHPDoc

DocBlocks are ONLY to be used where native types are insufficient.

Bad:

/**
 * @var array
 */
protected $filterable = [
    [
        'column' => ['active'],
        'field' => 'active',
        'operator' => '='
    ],
];

Good:

protected array $filterable = [
    [
        'column' => ['active'],
        'field' => 'active',
        'operator' => '=',
    ],
];

Bad:

/**
 * Will return a user
 *
 * @return User
 */
public function getUser() {
}

Good:

public function getUser(): User {
}

Bad:

<?php
/**
 * Created by PhpStorm.
 * User: viirre
 * Date: 2016-05-04
 * Time: 15:11
 */

Remove the above auto generated comment by PHPStorm:

PHPStorm -> Preferences -> Editor -> File and Code Templates -> Includes-tab -> PHP File Header -> Empty this -> Press Apply

Extended type hinting

For better autocompletion and static code analysis you can declare model properties and specify what types a collection or array consist of in your docblocks. Example:

/**
 * @property int $id
 * @property string $status
 * @property-read string $status_label
 * @property-read Customer|null $customer
 */
interface OrderInterface
{
    public function getStatusLabelAttribute(): string;

    /**
     * @return Collection|Item[]
     */
    public function getItems(): Collection;

    public function customer(): BelongsTo;
}

Facades vs helper methods

We prefer app(ClassName::class) to App::make(ClassName::class) or app()->make(ClassName::class) and try to use the helper methods where available. If you're using a facade, import the full namespaced Facade class rather then using the global alias.

Use-statements for traits

Each individual trait that is imported into a class must be included one-per-line and each inclusion must have its own use import statement.

Bad:

<?php

namespace App;

use Illuminate\Auth\Authenticatable;
use Illuminate\Auth\Passwords\CanResetPassword;

class User extends Model 
{
    use Authenticatable,
        CanResetPassword;
}

Good:

<?php

namespace App;

use Illuminate\Auth\Authenticatable;
use Illuminate\Auth\Passwords\CanResetPassword;

class User extends Model 
{
    use Authenticatable; 
    use CanResetPassword;
}

Comments

Comments should be avoided as much as possible by writing expressive code. A comment should tell why something is as it is (eg. to explain a caveat) , and not what the code does. If you do need to use a comment format it like this:

// There should be space before a single line comment.

// If you need to explain a lot you can use multiple comment lines.
// It's just pretty simple.

Prefixes and Suffixes

Type of file Prefix Suffix
Interface No TBD
Contract No TBD
Trait No No*
Exception No Exception
Controller No Controller
Transformer No Transformer
Middleware No No
Notification No Notification
Event No No
Listener No No
Console command No No
Policy No Policy
Repository No Repository
Middleware No No
Request No No

* A trait SHOULD BE suffixed with Trait when not in a folder named Traits.

Legacy

"Always leave the campground cleaner than you found it." - The boyscout rule

Since we have older projects built when this guideline was not a thing, it'll undoubtedly be cases where you stumble upon code that are not tolerated by this guideline.

Refactoring and fixing these issues are more than welcome.

Translations

Translations should be rendered with the __ function. We prefer using this over @lang in Blade views because __ can be used in both Blade views and regular PHP code. Here's an example:

<h2>{{ __('newsletter.form.title') }}</h2>

{!! __('newsletter.form.description') !!}

Constructor property promotion

Constructor property promotion should always be used when declaring constructors:

Bad:

class Product 
{
    private string $name;
    private int $price;
    
    public function __construct(string $name, int $price) 
    {
        $this->name = $name;
        $this->price = $price;
    }
}

Good:

class Product 
{
    public function __construct(private string $name, private int $price) 
    {
    }
}

It's allowed to combine property promotion and normal properties:

Good:

class Product 
{
    /** @var Category[] $categories */
    private array $categories
    
    public function __construct(private string $name, array $categories) 
    {
        $this->categories = collect($categories)->map(fn(array $data) => Category::fromArray($data));
    }
}

Good:

class Product 
{    
    private string $formattedName;
    
    public function __construct(private string $name) 
    {
        $this->formattedName = "Foo {$name}";
    }
}

Read more about property promotion at Sticher.io

Composer Rules

Talk to project lead / release manger before doing a general composer update.

If you're in a need to update a specific composer package you're free to do so by running eg. composer update adaptivemedia/atlantis-core.

If you need to re-generate the composer.lock file you can run composer update nothing to do so safely.

If you need to handle a merge conflict in composer.lock where the other person has only required a package, use the ours strategy and re-require the others persons package, eg:

# Use your version of composer.lock
git checkout --ours composer.lock

# Re-require other package
composer require other-persons-package

Folder structure

We try to follow the Laravel default folder structure where possible. There are however cases where we've adopted the folder structure to the projects needs.

In new projects the models should be placed in app\Models so if the projects grows, the models are grouped together in a separate folder.

For Atlantis projects, we have a Modules folder where every module (generally a model) has it's own folder.

Configuration

Configuration files must use kebab-case.

config/
  pdf-generator.php

Configuration keys must use snake_case.

// config/pdf-generator.php
return [
    'chrome_path' => env('CHROME_PATH'),
];

The env helper MUST NOT be used outside of configuration files. Create a configuration value from the env variable like above.

Artisan commands

The names given to artisan commands should all be kebab-cased.

# Good
php artisan delete-old-records

# Bad
php artisan deleteOldRecords

A command should always give some feedback on what the result is. Minimally you should let the handle method spit out a comment at the end indicating that all went well and then return a successfull exit code.

// in a Command
public function handle()
{
    // do some work

    $this->comment('All ok!');
    
    return Command::SUCCESS;
}

If possible use a descriptive success message eg. Old records deleted.

Routing

Public-facing urls must use kebab-case.

https://adaptivemedia.se/open-source
https://adaptivemedia.se/jobs/front-end-developer

Route names must use kebab-case.

Route::get('open-source', 'OpenSourceController@index')->name('open-source.index');
<a href="{{ route('open-source.index') }}">
    Open Source
</a>

Relationships

Avoid using relationship when calling route as this can lead to unnecessary database queries. Instead, use the foreign key.

Bad:

<a href="{{ route('team.show', $user->team) }}">
    Team
</a>

Good:

<a href="{{ route('team.show', $user->team_id) }}">
    Team
</a>

Controllers

Controllers that control a resource must be in singluar form.

class PostController
{
    // ...
}

The main reason for using singular form is to be consistent. Some controllers cannot be named in the plural form (eg. DashboardController) and for some is would just be weird to have them in plural (eg. RedactorMediaController).

Try to keep controllers simple and stick to the default CRUD keywords (index, create, store, show, edit, update, destroy). Extract a new controller if you need other actions.

Don't be afraid to create a Single action controller that only does one thing. In these cases these controllers should only have one public method __invoke.

This is a loose guideline that doesn't need to be enforced.

Testing

We like to keep our tests as clean as possible and there's no difference from the other parts of the system. Code written in tests can and should also be a subject to refactoring.

When writing tests, try to keep them as separated as possible, rather use two or more test-methods than testing 10 different cases in the same method.

Feature vs Unit tests

We prefer feature tests where possible. Unit tests can be used where testing in isolation is a good complement, for an example if you need to know that a calculation is done in a very specific way.

We only test the open API of our classes. Private and protected methods should be tested through one of the open methods. We're not interested in how a class is implemented, only that the code produces the outcome we want.

Mocking

Always mock external services. Prefer $this->createMock over Mockery::mock().

Prefixing

Prefixing test-methods with test_ should be avoided and instead either the doc-block variant or the #[Test] attribute should be used.

Bad:

public function test_can_activate_user() {
    //
}

Good (old PHPUnit syntax):

/** @test */
public function can_activate_user() {
    //
}

Good (new PHPUnit syntax):

#[Test]
public function can_activate_user() {
    //
}

Factories

We have previously preferred to use ModelFactory::new() creation over Model::new() to avoid having to use the HasFactory trait on the model and to keep all factory files in the factories folder (must otherwise be nested to match or Modules-structure).

In Atlantis projects, we have now fixed this so Model factories inside the Modules/ folder are auto discovered, so we now prefer to use the HasFactory syntax.

Setup

If you need to set up models in the same way over and over again in your test-file, you can create your own set up method when needed.

private function createActiveUsersWithCategories() {
    UserFactory::new()->times(5)->create();
    CategoryFactory::new()->times(2)->create();
}

The above method can be used when needed in the tests, but should not be placed in setUp-methods that automatically apply to all tests in the class.

Logging a user in

Authenticating a user can be done by using the methods actingAs or be. The only difference between the two is that actingAs returns $this while be returns void.

Logging in a user should be done within the test case itself and never be hidden away in a setup method.

Validation

Can be done in two ways:

On the request object

request()->validate([
    'title' => 'required'
]);

Request Form Validation

See Laravel documentation

Views

View files must use kebab-case.

resources/
  views/
    open-source.blade.php
class OpenSourceController
{
    public function index() {
        return view('open-source');
    }
}

Blade Templates

Checking for existence of a relation

When checking for the existence of a relation in an @if statement, use the id column of the relation instead of the relation itself. This avoids unnecessary database queries.

Bad:

@if ($user->team)
    <a href="{{ route('team.show', $user->team) }}">
        Team
    </a>
@endif

Good:

@if ($user->team_id)
    <a href="{{ route('team.show', $user->team_id) }}">
        Team
    </a>
@endif

Authorization

Policies must use camelCase.

Gate::define('editPost', function ($user, $post) {
    return $user->id == $post->user_id;
});
@can('editPost', $post)
    <a href="{{ route('posts.edit', $post) }}">
        Edit
    </a>
@endcan

Try to name abilities using default CRUD words. One exception: replace show with view. A server shows a resource, a user views it.

Exceptions

Try to throw as specific exceptions as possible and avoid both throwing and catching \Exception if possible. If there is a standard SPL exception that fits your case, use that. Create exceptions with human readable names and a message which explains exactly what is wrong. Include any invalid values in the message if applicable.

Bad:

throw new \Exception('File missing');

Good:

throw new FileNotFoundException(sprintf(
    'Configuration file "%s" for awesome component not found',
    $relativeFilePath
));

Read Tomas Votruba's Tips to Write Exceptions Everyone Will Love.

Enums

Use an Enum when you have an enumaration of something. Basic example of a "backed" enum:

<?php

namespace App\Enums;

enum Status: string
{
    case Draft = 'draft';
    case Ongoing = 'ongoing';
    case Finished = 'finished';

    public static function asOptions(): array
    {
        return [
            self::Draft->value => 'Utkast',
            self::Ongoing->value => 'Pågående',
            self::Finished->value => 'Klar',
        ];
    }
    
    public function color(): string
    {
        return match($this) 
        {
            Status::Draft => 'gray',   
            Status::Ongoing => 'green',   
            Status::Finished => 'red',   
        };
    }
}

// Usage:
$status = \App\Enums\Status::Draft;
echo $status->value; // "draft"
echo $status->color(); // "gray"
dump($status->cases()); // Prints an array with "name" and "value" for each enum value

// Usage in a model:
protected $casts = [
    'status' => \App\Enums\Status::class
];

\App\Models\Project::create([
    'status' => \App\Enums\Status::Draft,
    ...
]);

Make sure you use correct casing (PascalCase):

Bad:

enum Status: string
{
    case DRAFT = 'draft';
}

Good:

enum Status: string
{
    case Draft = 'draft';
}

We use PascalCase since that is used in the original RFC, and it seems to be the most common casing. Also, it differs visually from constants, so you can easily spot the difference.

Read more at Sticher.io

Actions

We often prefer to use "Action classes" to extract behaviour that performs a certain action. In some projects, we use the package lorisleiva/laravel-actions. This is how we use it:

BookFreightAction::make()->handle($this->order);

The constructor of the action class should take any services that we want Laravel to auto-inject for us.

Naming Convention

Action classes should be named clearly and descriptively to indicate their purpose and the action they perform. The class name should typically follow the format: Verb + Noun + “Action”. For example, CalculateTotalDistanceAction, SendWelcomeEmailAction, or GenerateReportAction. This naming convention helps maintain consistency across the codebase and makes it easier to understand what each action does at a glance.

An action class can return a value, but most often it will return void.

Prefer to execute the action statically via ActionClass::make()->handle() because of its simplicity.

In project's we don't use this package, prefer to mimic this package and execute the action with e.g: (new SomeAction)->handle($param))

Benefits of Using Action Classes

  1. Separation of Concerns: Action classes encapsulate specific pieces of business logic, making it easier to maintain and test individual components of the application.
  2. Reusability: Since action classes are self-contained, they can be reused across different parts of the application, reducing code duplication and improving maintainability.
  3. Testability: Isolating logic in action classes allows for more focused and easier unit testing, ensuring that each action works as expected without needing to set up extensive application context.
  4. Consistency: Following a standardized approach to handling business logic helps maintain a consistent code structure, which is easier to navigate and understand for all developers in the team.

Policies

Relationships

Avoid using relationships in policies, when possible, as they can lead to unnecessary database queries. Instead, use the foreign key.

Bad:

class UserPolicy {
    public function showTeam(User $loggedInUser): bool
    {
        if ($loggedInUser->team) {
            return true;
        }
        
        return false;
    }
}

Good:

class UserPolicy {
    public function showTeam(User $loggedInUser): bool
    {
        if ($loggedInUser->team_id) {
            return true;
        }
        
        return false;
    }
}

Usage of user()

Avoid using the user() helper in application code that does not explicitly begin in a controller or view model. This is to avoid the risk of the code being used in a non http context where user() would return null.

Instead pass a $user instance to the code that needs it.

Instances where this is relevant where you may not think about it:

Bad:

// When in some class
class SomeModel
{
    public function someMethod(): void
    {
        user()->doSomething();
    }
}

// When in a queue job
class SomeJob implements ShouldQueue
{
    public function handle(): void
    {
        user()->doSomething();
    }
}

Good:

// When in some class
class SomeModel
{
    public function someMethod(User $user): void
    {
        $user->doSomething();
    }
}

// When in a queue job
class SomeJob implements ShouldQueue
{
    public function handle(User $user): void
    {
        $user->doSomething();
    }
}

Models

Bool casting

Cast boolean attributes to bool, without it the attribute will be of type int

Good:

<?php

class User extends Model {
    protected $casts = [
        'is_active' => 'bool',
    ];
}

Query scopes

We should aim to use dedicated query builders instead of local query scopes when we can. Local query scopes are too magic and does not work 100% with IDE autocompletion and static analysis tools.

Notice the @method static TeamBuilder query() annotation on the model. This is needed for the IDE to know that the query() method returns a TeamBuilder instance.

See more at https://timacdonald.me/dedicated-eloquent-model-query-builders/

Bad:

<?php
class Team extends Model
{
    public function scopeActive(Builder $query): void
    {
        $query->where('active', true);
    }
}

Good:

// App\Models\Team.php
<?php

namespace App\Modules\Team;

use Illuminate\Database\Eloquent\Builder;

/**
 * @method static TeamBuilder query()
 */
class Team extends Model
{
    public function newEloquentBuilder(Builder $query): TeamBuilder
    {
        return new TeamBuilder($query);
    }
}

// App\Modules\Team\TeamBuilder.php
<?php

namespace App\Modules\Team;

use Illuminate\Database\Eloquent\Builder;

class TeamBuilder extends Builder
{
    public function active(): self
    {
        $this->where('is_active', true);
        
        return $this;
    }
}

// Somewhere else..
Team::query()->active()->get();

Migrations

Use anonymous class migration files

Bad:

class AddColumnToTable extends Migration
{
    public function up()
    {
        Schema::table('table', function (Blueprint $table) {
            $table->string('column', 2);
        });
    }

}

Good:

return new class extends Migration {
    public function up(): void
    {
        Schema::table('table', function (Blueprint $table) {
            $table->string('column', 2);
        });
    }
};

Don't add down()

In general, if something goes wrong, it's easier to fix the problem and deploy a new release than to roll back the migration.

Bad:

return new class extends Migration {
    public function up(): void
    {
        Schema::create('table', function (Blueprint $table) {
            $table->unsignedInteger('id');
        });
    }
    
    public function down(): void
    {
        Schema::dropIfExists('table');
    }
};

Good:

return new class extends Migration {
    public function up(): void
    {
        Schema::create('table', function (Blueprint $table) {
            $table->unsignedInteger('id');
        });
    }
};

Avoid importing dependencies in migrations

Don't use constants, classes or any other dependency in the migration. The migration will fail if the dependency is removed from the application. Use scalar values and the DB-facade instead.

Bad:

return new class extends Migration {
    public function up(): void
    {
        Address::query()
            ->where('sales_status', SalesStatus::UNSIGNED)
            ->update(['sales_status' => SalesStatus::SIGNED])
    }
};

Good:

return new class extends Migration {
    public function up(): void
    {
        DB::table('addresses')
            ->where('sales_status', 'unsigned')
            ->update(['sales_status' => 'signed']);
    }
};

Alert and messages

Finish all alerts and messages with a period (.) except short flash messages (max 2-3 words). Short flash messages should not have have any ending character at all.

Interfaces

Interfaces should be named descriptively, reflecting their capabilities rather than using the 'Interface' suffix. For instance, an interface named 'Searchable' indicates that any class implementing it can perform search operations. This approach enhances code readability and comprehension by clearly signaling the expected functionality or behavior of the interface. Adhering to such naming conventions is crucial for writing clean and maintainable code, a practice widely recognized across various programming languages.

Concerning the organization of interfaces, they can be either placed in a dedicated 'Interfaces' folder or if it's specific to a certain module and that module is small, within that module's folder.

Bad:

interface SearchInterface

Good:

interface Searchable
interface Meetable

Traits

A trait should not be suffixed with Trait if it's placed within a folder called Traits. It should have a clear name that describes what the trait is adding to the class in the form of "This class has X".

Bad:

// File: app/Traits/MeetingTrait.php
trait MeetingTrait

Good:

// File: app/Modules/Meeting/HasMeetingsTrait.php
trait HasMeetingsTrait

Good:

// File: app/Traits/HasMeetings.php
trait HasMeetings
trait HasWidgets

Boolean methods and property names

When defining a method or property that returns a boolean value, the name should be prefixed with e.g. has, should or can or have is in the middle to make it readable. In some cases, it doesn't make sense to use any of these prefixes, but in most cases it does.

Bad:

if (isUserActive()) {
}

if ($isUserActive) {
}

Bad:

if ($isAlreadyCreated) {
}

Good:

if (userIsActive()) {
}

if ($userIsActive) {
}

Good:

if (hasTemplates()) {
}

if (shouldNotify()) {
}

Good:

if ($alreadyCreated) {
}

Database External identifiers

When naming database columns that reference identifiers from external systems, it's important to avoid confusion with foreign keys that reference other columns within our own database. To ensure clarity, such columns should not use the naming convention "x_id" (where "x" represents the column's context). For instance, a column referencing an identifier from Salesforce should not be named "sales_force_id" as it may be misconstrued as a foreign key to another column in our database.

Bad:

sales_force_id

This suggests it is a foreign key to another table in our database, which is misleading.

Good:

sales_force_external_identifier

This clearly indicates that the column contains an identifier from an external system (Salesforce), distinguishing it from internal foreign keys. By adhering to this naming convention, we maintain consistency and avoid ambiguity in our database schema, enhancing the clarity and maintainability of our codebase.

Naming for Company Customers

When naming something that can be either a "private customer" or "company customer", prefer to use Company instead of e.g. Business or Corporate.