Remember that “conventions” are just conventions and you could do whatever you want just be constant, however it’s better follow the documentation:
Must visit for all the references and good practices:
https://github.com/alexeymezenin/laravel-best-practices#follow-laravel-naming-conventions
Details:
How to read this guide
- MUST This word, or the terms “REQUIRED” or “SHALL”, mean that the definition is an absolute requirement of the specification.
- MUST NOT This phrase, or the phrase “SHALL NOT”, mean that the definition is an absolute prohibition of the specification.
- SHOULD This word, or the adjective “RECOMMENDED”, mean that there may exist valid reasons in particular circumstances to ignore a particular item, but the full implications must be understood and carefully weighed before choosing a different course.
- SHOULD NOT This phrase, or the phrase “NOT RECOMMENDED” mean that there may exist valid reasons in particular circumstances when the particular behavior is acceptable or even useful, but the full implications should be understood and the case carefully weighed before implementing any behavior described with this label.
- MAY this word, or the adjective “OPTIONAL”, mean that an item is truly optional.
The above specifications is derived from RFC 2119 Environment
Local Environment
There are multiple ways to run Laravel in local environment as outlined below:
You are free to choose which local environment that works for you and your team as long as the following conditions are met:
- Your local environment MUST be able to duplicate the production server or close to its specifications such as PHP version, extensions installed and MySQL version
· .env file MUST NOT be committed into repository
- You SHOULD NOT be connecting to your any production server when you are debugging locally, to prevent accidental corruption of data, unintended API call or similar incident.
- You SHOULD NOT be using personally identiable information (PII) of your end-users data or any data that could potentially identify a specific individual such as first/last name, address, medical condition so on and so forth, unless you are explicitly authorized by your company or client to do so.
- You MUST update the readme.md file for any special instruction on how to run the app in local environment, so that other developers who will setup the app in their local machine can follow them properly.
- While it is possible to WAMP or XAMP for Laravel, this is un-common practice so you SHOULD try to familiarize yourself on how server components works and be comfortable in dealing with them.
Staging Environment
Staging servers is a type of server that is used to test a software, website or service in a production-similar environment before being set live. It is the role of a staging environment or staging site, to serve as a temporary hosting and testing server for any new software or feature.
Production Environment
You MUST regularly rotate your APP_KEY
APP_KEYS are set when you initialized a new Laravel application or executed the following command
Laravel uses the key for all encrypted cookies, including the session cookie, before handing them off to the user’s browser, and it uses it to decrypt cookies read from the browser. This prevents the client from making changes to their cookies and granting themselves admin privileges or impersonating another user in your application. Encrypted cookies are an important security feature in Laravel
Configuration
Environment Variables
You MUST put sensitive information into .env files
Use .env files to store any secure information and retrieve it via env function. There should be no instance on which you will put it inside models/controllers and commit it to Git.
Good
// .env
API_HOST=https://example.com/api
API_USERNAME=myuser
API_PASSWORD=secret
// access the value from app/config.php file
return [
...
'api_host' => env('API_HOST', 'https://defaultdomain.com')
'api_username' => env('API_USER', 'defaultuser')
'api_password' => env('API_USER', 'defaultpassword')
...
]
Bad
define('API_HOST', 'https://defaultdomain.com');
define('API_USERNAME', 'defaultuser');
define('API_PASSWORD', 'defaultpassword');
class DomainController extends Controller
{
public function index()
{
$api_username
}
Your application key MUST be set. This is the APP_KEY variable in your .env file. You can generate one via
Application Namespace
It is recommended to give your application a name. That is, instead of using the default App root namespace given by Laravel install, set it to what the application is all about. This can be set via
php artisan app:name YourAppName
This makes your controllers/models etc. resolve into YourAppName\Controllers and YourAppName\Models
Package Configuration
Custom or Package configuration filename MUST be in snake_case
Good
Bad
Config and language files indexes SHOULD be in snake-case
Good
// config/myconfig.php
return [
'my_api' => [
'domain' => env('API_DOMAIN'),
'secret' => env('API_SECRET'),
],
Bad
// config/myconfig.php
return [
'MyApi' => [
'DOMAIN' => env('API_DOMAIN'),
'SECRET' => env('API_SECRET'),
],
The best way to figure out if you have implemented best-practice in configuring your app, is if the codebase could be made open source at any moment without compromising any credentials
Functions
Laravel comes with a lot of useful helper functions, but you can also define your own helper functions, given the following conditions:
You SHOULD place your custom helper functions by creating a file called helper.php
Good
project_folder/app/helper.php
project_folder/app/Http/helper.php
Bad
project_folder/functions.php
You MUST use Composer’s autolading capability to load your functions
Good
// file composer.json
...
"autoload": {
"files": [
"app/helpers.php"
],
...
Bad
// file app/Http/Controllers/HomeController.php
class HomeController.php
{
function index(){
require_once(app_path("helpers.php"));
}
}
You MUST check if the the function exists before defining it
Good
if (! function_exists('my_custom_helper')) {
function my_custom_helper($key, $default = null) {
// ...
}
}
Bad
function my_custom_helper($key, $default = null) {
// ...
}
Other General guides with functions
- If the function length exceeds 25 lines, you SHOULD break it down to multiple functions
- Each function SHOULD have a Unit Test associated with it
Views
You SHOULD use snake_case as file name of your Blade templates
Good
Bad
showFiltered.blade.php
show-filtered.blade.php
You MUST not make non UI-related operations inside blade templates
Good
// $api_results is passed by controller
<ul>
@foreach($api_results as $result)
<li>{{ $result->name }}</li>
@endforeach
</ul>
Bad
@php
$api_results = json_decode(file_get_contents("https://api.example.com"));
@endphp
<ul>
@foreach($api_results as $result)
<li>{{ $result->name }}</li>
@endforeach
</ul>
Database Conventions
Table and Fields Naming
Table names MUST be in plural form and MUST be all lower-case
Good
class CreateFlightsTable extends Migration
{
public function up()
{
Schema::create('flights', function (Blueprint $table) {
Bad
class CreateFlightsTable extends Migration
{
public function up()
{
Schema::create('flight', function (Blueprint $table) {
class CreateUsersTable extends Migration
{
public function up()
{
Schema::create('MyUsers', function (Blueprint $table) {
Pivot table names MUST be in singular model names in alphabetical order
Good
post_user
articles_user
photo_post
Bad
posts_users
user_articles
post_photos
Table column names SHOULD be in snake_case without the model name
Good
Bad
UserName
_title
ThumbUrl
post_title
Foreign keys MUST be singular model name with _id suffix
Good
Bad
userid
siteid
Memberid
TransactionID
Primary Keys SHOULD be “id”
Good
Bad
Database Alterations
You MUST not be changing the database schema directly, use Database Migrations instead
Good
Bad
- use of PHPMyAdmin
- directly executing ALTER statement in mysql console / cli
- using sql file to change the db
Migration filenames MUST follow to following pattern
creation of table
yyyy_mm_dd_<timestamp>_create_<table name>_table
Good
2019_06_06_164210_create_domains_table.php
Bad
2019_06_06_164210_domains.php
Database Choice
Polyglot Persistence
Is a practice of using different data storage technologies for different kinds of data. Eloquent ORM can support multiple database for a reason, so don’t limit yourself to MySQL.
- It is RECOMMENDED to use MongoDB for records that have attributes that vary a lot. For example, in an inventory system, an office supplies product might have a different set of fields compared to vehicle and auto supplies.
- It is RECOMMENDED to use ElasticSearch for high volume data searching and indexing.
- It is RECOMMENDED to use Neo4J for applications that require complex relationships between models. For example a multi-level networking application, social network site and similar apps.
From this article, here is a sample breakdown of different databases being used by a retailer company Design Patterns
SOLID
SOLID is five design principles intended to make software designs more understandable, flexible and maintainable.
Single responsibility principle
A class should only have a single responsibility, that is, only changes to one part of the software’s specification should be able to affect the specification of the class.
Open–closed principle
Software entities … should be open for extension, but closed for modification.
Liskov substitution principle
Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.
Interface segregation principle
Many client-specific interfaces are better than one general-purpose interface.
Dependency inversion principle
One should depend upon abstractions, [not] concretions.
Repository Pattern
The idea with this pattern is to have a generic abstract way for the app to work with the data layer without being bothered what storage technology is used when saving/retrieving the data.
When reading/writing data, it is RECOMMENDED to wrap it into Repository Object
Testing
Unit Testing
Methods in test classes MUST start with “test” then a camelCased name of the test
Good
class ExampleTest extends TestCase
{
public function testBasicTest()
{
Bad
class ExampleTest extends TestCase
{
public function test_basic_test()
{
Single responsibility principle
A class and a method should have only one responsibility.
Bad:
public function getFullNameAttribute()
{
if (auth()->user() && auth()->user()->hasRole('client') && auth()->user()->isVerified()) {
return 'Mr. ' . $this->first_name . ' ' . $this->middle_name . ' ' . $this->last_name;
} else {
return $this->first_name[0] . '. ' . $this->last_name;
}
}
Good:
public function getFullNameAttribute()
{
return $this->isVerifiedClient() ? $this->getFullNameLong() : $this->getFullNameShort();
}
public function isVerifiedClient()
{
return auth()->user() && auth()->user()->hasRole('client') && auth()->user()->isVerified();
}
public function getFullNameLong()
{
return 'Mr. ' . $this->first_name . ' ' . $this->middle_name . ' ' . $this->last_name;
}
public function getFullNameShort()
{
return $this->first_name[0] . '. ' . $this->last_name;
}
Fat models, skinny controllers
Put all DB related logic into Eloquent models or into Repository classes if you're using Query Builder or raw SQL queries.
Bad:
public function index()
{
$clients = Client::verified()
->with(['orders' => function ($q) {
$q->where('created_at', '>', Carbon::today()->subWeek());
}])
->get();
return view('index', ['clients' => $clients]);
}
Good:
public function index()
{
return view('index', ['clients' => $this->client->getWithNewOrders()]);
}
class Client extends Model
{
public function getWithNewOrders()
{
return $this->verified()
->with(['orders' => function ($q) {
$q->where('created_at', '>', Carbon::today()->subWeek());
}])
->get();
}
}
Validation
Move validation from controllers to Request classes.
Bad:
public function store(Request $request)
{
$request->validate([
'title' => 'required|unique:posts|max:255',
'body' => 'required',
'publish_at' => 'nullable|date',
]);
....
}
Good:
public function store(PostRequest $request)
{
....
}
class PostRequest extends Request
{
public function rules()
{
return [
'title' => 'required|unique:posts|max:255',
'body' => 'required',
'publish_at' => 'nullable|date',
];
}
}
Business logic should be in service class
A controller must have only one responsibility, so move business logic from controllers to service classes.
Bad:
public function store(Request $request)
{
if ($request->hasFile('image')) {
$request->file('image')->move(public_path('images') . 'temp');
}
....
}
Good:
public function store(Request $request)
{
$this->articleService->handleUploadedImage($request->file('image'));
....
}
class ArticleService
{
public function handleUploadedImage($image)
{
if (!is_null($image)) {
$image->move(public_path('images') . 'temp');
}
}
}
Don't repeat yourself (DRY)
Reuse code when you can. SRP is helping you to avoid duplication. Also, reuse Blade templates, use Eloquent scopes etc.
Bad:
public function getActive()
{
return $this->where('verified', 1)->whereNotNull('deleted_at')->get();
}
public function getArticles()
{
return $this->whereHas('user', function ($q) {
$q->where('verified', 1)->whereNotNull('deleted_at');
})->get();
}
Good:
public function scopeActive($q)
{
return $q->where('verified', 1)->whereNotNull('deleted_at');
}
public function getActive()
{
return $this->active()->get();
}
public function getArticles()
{
return $this->whereHas('user', function ($q) {
$q->active();
})->get();
}
Prefer to use Eloquent over using Query Builder and raw SQL queries. Prefer collections over arrays
Eloquent allows you to write readable and maintainable code. Also, Eloquent has great built-in tools like soft deletes, events, scopes etc.
Bad:
SELECT *
FROM `articles`
WHERE EXISTS (SELECT *
FROM `users`
WHERE `articles`.`user_id` = `users`.`id`
AND EXISTS (SELECT *
FROM `profiles`
WHERE `profiles`.`user_id` = `users`.`id`)
AND `users`.`deleted_at` IS NULL)
AND `verified` = '1'
AND `active` = '1'
ORDER BY `created_at` DESC
Good:
Article::has('user.profile')->verified()->latest()->get();
Mass assignment
Bad:
$article = new Article;
$article->title = $request->title;
$article->content = $request->content;
$article->verified = $request->verified;
// Add category to article
$article->category_id = $category->id;
$article->save();
Good:
$category->article()->create($request->validated());
Do not execute queries in Blade templates and use eager loading (N + 1 problem)
Bad (for 100 users, 101 DB queries will be executed):
@foreach (User::all() as $user)
{{ $user->profile->name }}
@endforeach
Good (for 100 users, 2 DB queries will be executed):
$users = User::with('profile')->get();
...
@foreach ($users as $user)
{{ $user->profile->name }}
@endforeach
Comment your code, but prefer descriptive method and variable names over comments
Bad:
if (count((array) $builder->getQuery()->joins) > 0)
Better:
// Determine if there are any joins.
if (count((array) $builder->getQuery()->joins) > 0)
Good:
if ($this->hasJoins())
Do not put JS and CSS in Blade templates and do not put any HTML in PHP classes
Bad:
let article = `{{ json_encode($article) }}`;
Better:
<input id="article" type="hidden" value='@json($article)'>
Or
<button class="js-fav-article" data-article='@json($article)'>{{ $article->name }}<button>
In a Javascript file:
let article = $('#article').val();
The best way is to use specialized PHP to JS package to transfer the data.
Use config and language files, constants instead of text in the code
Bad:
public function isNormal()
{
return $article->type === 'normal';
}
return back()->with('message', 'Your article has been added!');
Good:
public function isNormal()
{
return $article->type === Article::TYPE_NORMAL;
}
return back()->with('message', __('app.article_added'));
Use standard Laravel tools accepted by community
Prefer to use built-in Laravel functionality and community packages instead of using 3rd party packages and tools. Any developer who will work with your app in the future will need to learn new tools. Also, chances to get help from the Laravel community are significantly lower when you're using a 3rd party package or tool. Do not make your client pay for that.
Task | Standard tools | 3rd party tools |
Authorization | Policies | Entrust, Sentinel and other packages |
Compiling assets | Laravel Mix | Grunt, Gulp, 3rd party packages |
Development Environment | Homestead | Docker |
Deployment | Laravel Forge | Deployer and other solutions |
Unit testing | PHPUnit, Mockery | Phpspec |
Browser testing | Laravel Dusk | Codeception |
DB | Eloquent | SQL, Doctrine |
Templates | Blade | Twig |
Working with data | Laravel collections | Arrays |
Form validation | Request classes | 3rd party packages, validation in controller |
Authentication | Built-in | 3rd party packages, your own solution |
API authentication | Laravel Passport | 3rd party JWT and OAuth packages |
Creating API | Built-in | Dingo API and similar packages |
Working with DB structure | Migrations | Working with DB structure directly |
Localization | Built-in | 3rd party packages |
Realtime user interfaces | Laravel Echo, Pusher | 3rd party packages and working with WebSockets directly |
Generating testing data | Seeder classes, Model Factories, Faker | Creating testing data manually |
Task scheduling | Laravel Task Scheduler | Scripts and 3rd party packages |
DB | MySQL, PostgreSQL, SQLite, SQL Server | MongoDB |
Follow Laravel naming conventions
Also, follow naming conventions accepted by Laravel community:
What | How | Good | Bad |
Controller | singular | ArticleController | ArticlesController |
Route | plural | articles/1 | article/1 |
Named route | snake_case with dot notation | users.show_active | users.show-active, show-active-users |
Model | singular | User | Users |
hasOne or belongsTo relationship | singular | articleComment | articleComments, article_comment |
All other relationships | plural | articleComments | articleComment, article_comments |
Table | plural | article_comments | article_comment, articleComments |
Pivot table | singular model names in alphabetical order | article_user | user_article, articles_users |
Table column | snake_case without model name | meta_title | MetaTitle; article_meta_title |
Model property | snake_case | $model->created_at | $model->createdAt |
Foreign key | singular model name with _id suffix | article_id | ArticleId, id_article, articles_id |
Primary key | - | id | custom_id |
Migration | - | 2017_01_01_000000_create_articles_table | 2017_01_01_000000_articles |
Method | camelCase | getAll | get_all |
Method in resource controller | | store | saveArticle |
Method in test class | camelCase | testGuestCannotSeeArticle | test_guest_cannot_see_article |
Variable | camelCase | $articlesWithAuthor | $articles_with_author |
Collection | descriptive, plural | $activeUsers = User::active()->get() | $active, $data |
Object | descriptive, singular | $activeUser = User::active()->first() | $users, $obj |
Config and language files index | snake_case | articles_enabled | ArticlesEnabled; articles-enabled |
View | kebab-case | show-filtered.blade.php | showFiltered.blade.php, show_filtered.blade.php |
Config | snake_case | google_calendar.php | googleCalendar.php, google-calendar.php |
Contract (interface) | adjective or noun | Authenticatable | AuthenticationInterface, IAuthentication |
Trait | adjective | Notifiable | NotificationTrait |
Use shorter and more readable syntax where possible
Bad:
$request->session()->get('cart');
$request->input('name');
Good:
session('cart');
$request->name;
More examples:
Common syntax | Shorter and more readable syntax |
Session::get('cart') | session('cart') |
$request->session()->get('cart') | session('cart') |
Session::put('cart', $data) | session(['cart' => $data]) |
$request->input('name'), Request::get('name') | $request->name, request('name') |
return Redirect::back() | return back() |
is_null($object->relation) ? null : $object->relation->id | optional($object->relation)->id |
return view('index')->with('title', $title)->with('client', $client) | return view('index', compact('title', 'client')) |
$request->has('value') ? $request->value : 'default'; | $request->get('value', 'default') |
Carbon::now(), Carbon::today() | now(), today() |
App::make('Class') | app('Class') |
->where('column', '=', 1) | ->where('column', 1) |
->orderBy('created_at', 'desc') | ->latest() |
->orderBy('age', 'desc') | ->latest('age') |
->orderBy('created_at', 'asc') | ->oldest() |
->select('id', 'name')->get() | ->get(['id', 'name']) |
->first()->name | ->value('name') |
Use IoC container or facades instead of new Class
new Class syntax creates tight coupling between classes and complicates testing. Use IoC container or facades instead.
Bad:
$user = new User;
$user->create($request->validated());
Good:
public function __construct(User $user)
{
$this->user = $user;
}
....
$this->user->create($request->validated());
Do not get data from the .env file directly
Pass the data to config files instead and then use the config() helper function to use the data in an application.
Bad:
$apiKey = env('API_KEY');
Good:
// config/api.php
'key' => env('API_KEY'),
// Use the data
$apiKey = config('api.key');
Store dates in the standard format. Use accessors and mutators to modify date format
Bad:
{{ Carbon::createFromFormat('Y-d-m H-i', $object->ordered_at)->toDateString() }}
{{ Carbon::createFromFormat('Y-d-m H-i', $object->ordered_at)->format('m-d') }}
Good:
// Model
protected $dates = ['ordered_at', 'created_at', 'updated_at'];
public function getSomeDateAttribute($date)
{
return $date->format('m-d');
}
// View
{{ $object->ordered_at->toDateString() }}
{{ $object->ordered_at->some_date }}
Other good practices
Never put any logic in routes files.