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
- PHP 8
- Code style
- Composer Rules
- Folder structure
- Configuration
- Artisan commands
- Routing
- Controllers
- Testing
- Validation
- Views
- Blade Templates
- Authorization
- Exceptions
- Enums
- Actions
- Policies
- User usage
- Models
- Query scopes
- Migrations
- Alert messages
- Interfaces
- Traits
- Boolean methods and property names
- Database External identifiers
- Naming for Company Customers
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
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
- Separation of Concerns: Action classes encapsulate specific pieces of business logic, making it easier to maintain and test individual components of the application.
- Reusability: Since action classes are self-contained, they can be reused across different parts of the application, reducing code duplication and improving maintainability.
- 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.
- 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:
- Queue jobs
- Model scopes
- Event listeners
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);
});
}
};
down()
Don't add 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
.