Most of this document is borrowed from Spatie code style guidelines regarding Laravel. We have changed some things we didn't agree with and added a few new rules.
Code style must follow PSR-1, PSR-2 and 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.
By default, we don't use final. For our open source stuff, we assume that all our users know they are responsible for writing tests for any overwritten behaviour. This is not a hard and fast rule as there may be cases when final classes are required.
If a method returns nothing, it should be indicated with void.
This makes it more clear to the users of your code what your intention was when writing it.
You should type a property whenever possible. Don't use a docblock.
// good
class Foo
{
public string $bar;
}
// bad
class Foo
{
/** @var string */
public $bar;
}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.
// Good
class Url
{
public static function fromString(string $url): Url
{
// ...
}
}
// Bad: The description is redundant, and the method is fully type-hinted.
class Url
{
/**
* Create a url from a string.
*
* @param string $url
*
* @return Url
*/
public static function fromString(string $url): Url
{
// ...
}
}Don't use fully qualified class names in docblocks.
// Good
/**
* @param string $foo
*
* @return Url
*/
// Bad
/**
* @param string $url
*
* @return Url
*/When possible, docblocks should be written on one line.
// Good
/** @var string */
/** @test */
// Bad
/**
* @test
*/If a variable has multiple types, the most common occurring type should be first.
If one of the returned types is null, use |null syntax. Always put null last.
// Good
/** @var Bar|null */
// Bad
/** @var null|Bar */Use Type|null instead of ?Type for type hinting
When possible prefer string interpolation above sprintf and the . operator.
// Good
$greeting = "Hi, I am {$name}.";// Bad
$greeting = 'Hi, I am ' . $name . '.';Don't use curly braces when possible.
Every portion of a ternary expression should be on its own line unless it's a really short expression.
// Good
$result = $object instanceof Model
? $object->name
: 'A default value';
$name = $isFoo ? 'foo' : 'bar';
// Bad
$result = $object instanceof Model ?
$object->name :
'A default value';Always use curly brackets.
// Good
if ($condition) {
...
}
// Bad
if ($condition) ...Generally a function should have its unhappy path first and its happy path last. In most cases this will cause the happy path being in an unindented part of the function which makes it more readable.
// Good
if (! $goodCondition) {
throw new Exception;
}
// do work// Bad
if ($goodCondition) {
// do work
}
throw new Exception;In general, else should be avoided because it makes code less readable. In most cases it can be refactored using early returns. This will also cause the happy path to go last, which is desirable.
// Good
if (! $conditionBA) {
// conditionB A failed
return;
}
if (! $conditionB) {
// conditionB A passed, B failed
return;
}
// condition A and B passed// Bad
if ($conditionA) {
if ($conditionB) {
// condition A and B passed
}
else {
// conditionB A passed, B failed
}
}
else {
// conditionB A failed
}In general, separate if statements should be preferred over a compound condition. This makes debugging code easier.
// Good
if (! $conditionA) {
return;
}
if (! $conditionB) {
return;
}
if (! $conditionC) {
return;
}
// do stuff// bad
if ($conditionA && $conditionB && $conditionC) {
// do stuff
}Expressive code is better than hard to read code with comments. If you do need to use a comment, format it like this:
// There should be a space before a single line comment.
/*
* If you need to explain a lot you can use a comment block. Notice the
* single * on the first line. Comment blocks don't need to be three
* lines long or three characters shorter than the previous line.
*/Statements should be able to breathe. In general always add blank lines between statements, unless they're a sequence of single-line equivalent operations. This isn't something enforceable, it's a matter of what looks best in its context.
// Good
public function getPage($url)
{
$page = $this->pages()->where('slug', $url)->first();
if (! $page) {
return null;
}
if ($page['private'] && ! Auth::check()) {
return null;
}
return $page;
}
// Bad: Everything's cramped together.
public function getPage($url)
{
$page = $this->pages()->where('slug', $url)->first();
if (! $page) {
return null;
}
if ($page['private'] && ! Auth::check()) {
return null;
}
return $page;
}// Good: A sequence of single-line equivalent operations.
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->string('email')->unique();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}Don't add any extra empty lines between {} brackets.
// Good
if ($foo) {
$this->foo = $foo;
}
// Bad
if ($foo) {
$this->foo = $foo;
}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'),
];Avoid using the env helper outside of configuration files. Create a configuration value from the env variable like above.
The names given to artisan commands should all be kebab-cased.
# Good
php artisan delete-old-records
# Bad
php artisan deleteOldRecordsA 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.
// in a Command
public function handle()
{
// do some work
$this->comment('All ok!');
}If possible use a descriptive success message eg. Old records deleted.
Public-facing urls must use kebab-case.
Route names must use camelCase.
Route::get('open-source', 'OpenSourceController@index')->name('openSource');<a href="{{ route('openSource') }}">
Open Source
</a>All routes have an http verb, that's why we like to put the verb first when defining a route. It makes a group of routes very readable. Any other route options should come after it.
// good: all http verbs come first
Route::get('/', 'HomeController@index')->name('home');
Route::get('open-source', 'OpenSourceController@index')->name('openSource');
// bad: http verbs not easily scannable
Route::name('home')->get('/', 'HomeController@index');
Route::name('openSource')->get('OpenSourceController@index');Route parameters should use camelCase.
Route::get('news/{newsItem}', 'NewsItemsController@index');A route url should not start with / unless the url would be an empty string.
// good
Route::get('/', 'HomeController@index');
Route::get('open-source', 'OpenSourceController@index');
//bad
Route::get('', 'HomeController@index');
Route::get('/open-source', 'OpenSourceController@index');Controllers that control a resource must use the singular resource name.
class PostController
{
// ...
}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.
In the following example, we could have PostController@favorite, and PostController@unfavorite, or we could extract it to a separate FavoritePostController.
class PostController
{
public function create()
{
// ...
}
// ...
public function favorite(Post $post)
{
request()->user()->favorites()->attach($post);
return response(null, 200);
}
public function unfavorite(Post $post)
{
request()->user()->favorites()->detach($post);
return response(null, 200);
}
}Here we fall back to default CRUD words, store and destroy.
class FavoritePostController
{
public function store(Post $post)
{
request()->user()->favorites()->attach($post);
return response(null, 200);
}
public function destroy(Post $post)
{
request()->user()->favorites()->detach($post);
return response(null, 200);
}
}This is a loose guideline that doesn't need to be enforced.
View files must use camelCase.
resources/
views/
openSource.blade.php
class OpenSourceController
{
public function index() {
return view('openSource');
}
}When using multiple rules for one field in a form request, avoid using |, always use array notation. Using array notation will make it easier to apply custom rule classes to a field without changing all other rules fro string format to array format.
// good
public function rules()
{
return [
'email' => ['required', 'email'],
];
}
// bad
public function rules()
{
return [
'email' => 'required|email',
];
}Prefer class syntax for custom validation rules instead of string syntax. Dont use Validator::extend().
Indent using four spaces.
<a href="/open-source">
Open Source
</a>Don't add spaces after control structures.
@if($condition)
Something
@endifPolicies 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>
@endcanTry to name abilities using default CRUD words. One exception: replace show with view. A server shows a resource, a user views it.
Translations must 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') !!}Use translation strings, don't hard-code strings even messages in controllers, responses and exceptions. This also applies to data like the app name, company name, company address, etc. Things may change over time.
Naming things is often seen as one of the harder things in programming. That's why we've established some high level guidelines for naming classes.
Following Laravel guidelines use singular resource names in controller names
e.g. UserController or EventDayController
When writing non-resourceful controllers you might come across invokable controllers that perform a single action. These can be named by the action they perform again suffixed by Controller.
e.g. PerformCleanupController
Both Eloquent resources and Fractal transformers are plural resources suffixed with Resource or Transformer accordingly. This is to avoid naming collisions with models.
A job's name should describe its action.
E.g. CreateUser or PerformDatabaseCleanup
Events will often be fired before or after the actual event. This should be very clear by the tense used in their name.
E.g. ApprovingLoan before the action is completed and LoanApproved after the action is completed.
Listeners will perform an action based on an incoming event. Their name should reflect that action with a Listener suffix. This might seem strange at first but will avoid naming collisions with jobs.
E.g. SendInvitationMailListener
To avoid naming collisions we'll suffix commands with Command, so they are easiliy distinguisable from jobs.
e.g. PublishScheduledPostsCommand
Again to avoid naming collisions we'll suffix mailables with Mail, as they're often used to convey an event, action or question.
e.g. AccountActivatedMail or NewEventMail
Present the user clear and informative error messages. Never assume the user knows why something fails.
Include unique codes and a user friendly message. Developers using you APIs should be able to rely on status codes and exception codes.
{
"message": "Invalid Vendor for Promotion",
"code": 1003
}Each module error codes should start with a specific number. For example all error codes between 1001 and 1999 belong to OrderModule and all error codes between 2001 and 2999 belong to ProductModule.
If you need to display a set of database records, always use pagination.
Never leak Eloquent Models into the front-end. Always use Response classes.
Prefer timestamps over booleans. For example, published_at instead of is_published.
Always prevent the lazy loading of relationships.
When declaring enums that correspond to array of constants or cases of an enum DO NOT use it in migration. Write out the concrete values for that enum at the time of writing the migration. Think about adding new cases and constants in the future and you will know the reason.
// bad
$table->enum('type', array_map(fn (Type $t) => $t->value, Type::cases()));
// good
$table->enum('type', [Type::One->value, Type::Two->value, Type::Three->value]);Favor PascalCase over UPPERCASE notation for enums because the RFC showed them in this form.
- When returning only one result use find followed by the conditions for example
findById()orfindByPhoneNumber()and throw exception instead of returning null in case no result is found for exampleUserNotFoundExceptionorOrderNotFoundException. - When returning multiple results use get followed by the conditions for example
getByType()orget(int $page, int $perPage)and returnEnumerable. - When creating many use
persistMany.
class UserRepository {
//bulk insert
public function persistMany(Enumerable $users) {
$users = array_map(
function (User $u) {
$output = $u->getAttributes();
$output['created_at'] = Carbon::now()->toDateTimeString();
$output['updated_at'] = Carbon::now()->toDateTimeString();
return $output;
},
$users->all(),
);
DB::table('users')->insert($users);
}
}- When creating or updating one use
save.