# Laravel
# Fundamental
# Follow the official Laravel documentation
Laravel has very good documentation that you can use to reference on developing a Laravel application.
# Use the latest stable version
Be mindful of the Laravel release and support cycles. Update often. Don't use obsolete versions. Ideally, bring your application to the latest version along with the right PHP version.
# Follow PHP Best Practices
When you develop a Laravel application, you use PHP. Therefore, follow PHP Best Practices.
# Use Composer
Use Composer to install Laravel itself and get other packages. It also helps you update packages and manage dependencies smoothly.
# Basic
# Naming convention
The standard for naming elements in Laravel is as follows:
Element | Convention | Example |
---|---|---|
Class | PascalCase | PostCreatedEvent |
Class constant | UPPER_SNAKE_CASE | CREATED_AT |
Class method | camelCase | publishPost() |
Model | PascalCase, singular | Post |
Controller | PascalCase, singular | PostController |
Route | lowercase, plural, RESTful | posts , posts/1 , posts/1/edit |
Named route | snake_case, dotted | posts.index , posts.publish_all |
View | follows named route | posts/index.blade.php , posts/publish_all.blade.php |
Model properties | snake_case | updated_at |
Table | snake_case, plural | posts , post_comments |
Pivot table | snake_case, singular, alphabetical | post_user |
Config key | snake_case | comment_relations.enabled |
Variable | camelCase | $postComments |
# Suffix class names with types
For classes that belong in standard types, such as commands, jobs, events, and so on, suffix their names with the types.
PostController
PostInterface
PostPolicy
PublishPostCommand
PublishPostJob
PostPublishedEvent
# Use named routes
Name all your routes. Output the route by using the route()
function.
# Use resource controllers
Resource controllers are controllers that have standard resourceful methods that handle well-defined RESTful routes for a model. This table below gives a good example of a resource controller that handles all RESTful routes for the Photo model:
URI | Method | Route Name |
---|---|---|
GET /photos | index() | photos.index |
GET /photos/create | create() | photos.create |
POST /photos | store() | photos.store |
GET /photos/{photo} | show() | photos.show |
GET /photos/{photo}/edit | edit() | photos.edit |
PATCH/PUT /photos/{photo} | update() | photos.update |
DELETE /photos/{photo} | destroy() | photos.destroy |
A good controller should only have these 7 resourceful methods.
There could be 1 or 2 additional methods when they handle closely related operations to these resourceful methods, and thus can belong in the same controller. When you start to have more methods in a controller, it's a good time to refactor these non-resourceful methods out to another controller.
# Put all sensitive information in the .env
file
And declare them in config, then use config()
in logic to get the config data.
WARNING
Never commit the .env
file in version control.
# Don't read environment values directly in code
Never read data from the .env
file directly. Reference the data with env()
in a config file instead, then use config()
to get the data in logic code.
TIP
The only place where env()
should be used is in config files.
Bad:
// In a controller
PaymentGateway::init('ABCD1234'); // API key
Bad:
// In a controller
PaymentGateway::init(env('PAYMENT_API_KEY')); // API key
Good:
# In .env file
PAYMENT_API_KEY=ABCD1234
// In config/payment.php config file
[
'api_key' => env('PAYMENT_API_KEY'),
]
// In a controller
PaymentGateway::init(config('payment.api_key'));
# Use separate config files
Keep Laravel's default config structure as is. When you want to introduce new config, make a separate file such as config/payment.php
.
Keeping the default configs helps you upgrade Laravel later easily by simply replacing with the new version. You won't have to diff each file to see what changes and what needs to be kept, which creates a lot of work for no extra benefit.
# Use migrations
Don't change your database, especially production, directly. Use migrations for it.
# Use seeders to quickly set up and reset data for your project
A good project should be able to run php artisan migrate:fresh --seed
to have sample data for you to immediately work on from the last point. A good first example is a user seeder.
# Don't put any logic in route files
Extract all logic handling in controllers and reference them in routes. As a general rule of thumb, don't ever have any Closure in route files. This also helps enable route caching which makes your application extremely fast in handling routes.
# Use cast for boolean attributes of Eloquent models
Many database engines don't have a native boolean type, so many schema designs use integer for this. In fact, Laravel migrations define a boolean field as TINYINT
in MySQL. However, to use the attributes as a PHP boolean, you need to specify the $cast
property on the Eloquent model.
// In migration
Schema::table('users', function (Blueprint $table) {
// ...
$table->boolean('is_admin');
});
class User
{
protected $casts = [
'is_admin' => 'boolean',
];
public function isAdmin()
{
return $this->is_admin; // This will return true/false instead of 1/0
}
}
# Use shorthand to check for existence with Eloquent
There are different ways to check if an Eloquent object exists after a query, such as empty()
, is_null
, and === null
in the if
statement. In fact, a simple Not Operator !
suffices. Laravel uses this itself.
$product = Product::where('code', $code)->first();
if (! $product) {
Log::error('Product does not exist.');
}
When you want to check if an Eloquent collection is empty, use ->isEmpty()
.
$products = Product::where('name', $name)->get();
if ($products->isEmpty()) {
Log::error('No products match name.');
}
# Intermediate
# Create the Models directory
Create a Models
directory under the app
directory, then put all Eloquent models in it. This adheres to the principle of separation of concerns and will make your code much more maintainable in the long run.
# Move request validation to Form Requests
Bad:
public function store()
{
// Validate input
request()->validate([
'title' => 'required|string|max:255',
'body' => 'required|string',
]);
// Retrieve the validated input data
$data = request()->only(['title', 'body']);
}
Good:
class PostRequest extends FormRequest
{
public function rules()
{
return [
'title' => 'required|string|max:255',
'body' => 'required|string',
];
}
}
public function store(PostRequest $request)
{
// Retrieve the validated input data
$data = $request->validated();
}
# Declare middlewares in routes, not controllers
When you have a middleware that can be declared in both the controller and its associated route, declare on the route or the route group for better organization.
# Don't execute queries in Blade templates
Instead, get and prepare all your data before passing to the view, such as in the controller.
# Always use Dependency Injection
Laravel powerfully supports dependency injection, so use it whenever you can.
For example, in controller methods, declare Request $request
and use the variable to gain access to the request.
public function update(Request $request, Post $post)
{
$title = $request->input('title'); // Get an input named 'title'
$user = $request->user(); // Get the current authenticated user, instead of `Auth::user()`
}
Using $request
has the obvious benefits of having a real Request
object to utilize in an OOP fashion and also enables convenient unit testing later on. If you need validation, promote the Request
object into a FormRequest
.
# Prefer Helper Functions over Facades
When injected dependency is not an option, reach for Helper Functions next. Use Facades only when there is no equivalent helper function.
Another ideal place to use helper functions is in views, where Facades cannot make namespace and usage declarations, thus you will lose out on IDE discovery.
Here are the list of Helper Functions to gain access to Laravel functionalities as opposed to Facades:
Facade | Helper Function | Example |
---|---|---|
Request | request() | request('title') gets input named title |
Response | response() | response('yes') outputs yes in the HTTP response |
View | view() | view('posts.index') displays the view template |
Cache | cache() | cache('timeout') gets cache value of timeout |
Config | config() | config('app.url') gets the value at app.url in config |
Session | session() | session()->flash('key', 'value') flashes a session value |
Log | logger() | logger('Job starts') writes to log at debug level |
App | app() | app('router') gets the Router instance in the container |
Validator | validator() | validator($array, ['age' => 'numeric']) validates an array |
Auth | auth() | auth()->user() gets the authenticated user |
Cookie | cookie() | cookie('name', 'Leo') sets a Laravel cookie |
# Specify relationships in migrations
This helps you use a database tool to easily visualize schema modeling later.
$table->foreign('user_id')
->references('id')
->on('users');
// Laravel 7
$table->foreignId('user_id');
# Respect the primary key id
in models
Always try have the primary key id
for your models.
Sometimes, you might think you don't need an id
field, or you have another field that looks temptingly like it could be a primary key, like a unique code
in a tickets
table. Don't fall for it. Have id
as a dedicated primary key anyway.
id
has been the primary key for models in most design conventions. Having it helps you automatically have an indexed table for free and follow RESTful and other design patterns easily. There's a reason Laravel has id
as the default primary key for models and tables.
In practice, just use id()
in your table migrations and use foreignId()
to declare foreign keys. This is the battle-tested method in the Laravel world.
If a big integer ID doesn't work for your application, make it a UUID string instead by using uuid()
. There is likely no scenario where you need any other method.
# Use Eloquent
Use Eloquent by default for all database operations when possible. Eloquent is the most powerful feature of Laravel. Most of your data operations can and should be done with Eloquent.
If you can't use Eloquent, such as in a complicated queries with joins, use Query Builder.
Using Eloquent and Query Builder give you absolute defense against SQL injection out of the box. It also allows you to swap out the database engine for restructuring or testing anytime.
In the rare case when you can't use Query Builder, then use raw queries with great care.
# Stream between Filesystems
When you need to copy a file from an origin Filesystem to a target Filesystem, stream instead of reading the whole content of the file with get()
.
Bad:
$fileContent = Storage::disk('s3')->get('from/file.jpg');
Storage::disk('ftp')->put('to/file.jpg', $fileContent);
Good:
Storage::disk('ftp')->writeStream(
'to/file.jpg', Storage::disk('s3')->readStream('from/file.jpg')
);
# Use each() on collections instead of foreach
When you have a collection of objects, use the each()
method on the collection. It gives you type hinting.
Bad:
foreach ($emails as $email) {
$email->send();
}
Good:
$emails->each(function (Email $email) {
$email->send();
});
# Advanced
# Thin models, thin controllers
Both your models and controllers should be thin, containing only necessary high-level business logic. Refactor complicated logic into other modules such as Jobs, Commands, and Services.
Models in Laravel are Eloquent models. They should contain only logic that pertains to Eloquent features, such as reserved properties, relationships, query scopes, mutators/accessors, and so on. For logic that goes beyond basic Eloquent features, separate the concern in another module like a Service, Repository, or Trait.
Similarly, controllers should follow a resourceful pattern, where you will only need 8 resource methods. In general, there should be at most 2 more methods for extraordinarily related operations. Anything further should be packaged into another controller.
# Don't instantiate objects in Command and Job constructors
Avoid creating new objects in the constructors of Commands. All of them are run no matter what when the application boots up from the CLI, thus creating unnecessary footprint.
Instead, place them in the handle()
method, which will run only when the Command is executed. You can also inject the dependencies by type-hinting in the method's declaration.
Also avoid creating new instances in the constructors of Jobs, if you can. Laravel serializes all properties of a Job when sending to queues, and having less data is always good for a faster storage holding, deserialization, and performance.
# Cache data
Cache all the data that are frequently queried, especially static data.
You can also cache dynamic data related to Models, and use Observers to refresh the cache.
Some developers even use the request URL as the key for caching.
# When you can, break a Command down and refactor logic into Jobs
If a Command feels like it does too much, refactor logic into Jobs. Queuing jobs takes only milliseconds. Moving logic this way helps you manage execution on a queue (with Horizon, for example) and minimize the command's likelihood of hitting a timeout.
Bad:
// ProcessImagesCommand.php
public function handle()
{
foreach ($this->images as $image) {
$image->process(); // Heavy operation
}
}
Good:
// ProcessImagesCommand.php
public function handle()
{
foreach ($this->images as $image) {
ProcessImageJob::dispatch($image); // Refactor into job
}
}
// ProcessImageJob.php
public function handle()
{
$this->image->process();
}
PHP →