Appearance
Laravel
Fundamental
Get the essentials right
- Follow the official Laravel documentation.
- Use the latest stable version. Update often. Don't use obsolete versions. Aim to bring your application to the latest Laravel version whenever possible.
- Follow PHP best practices. You can refer to this guide for more details.
Naming convention
The standard for naming elements in Laravel is as follows:
Element | Convention | Example |
---|---|---|
Class | PascalCase | PostCreatedEvent |
Class constant | UPPER_SNAKE_CASE | STATUS_COMPLETED |
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 property | snake_case | support_email |
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 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.
DANGER
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.
Therefore, the only place where env()
should be used is in config files.
Bad:
php
// In controller
PaymentGateway::init('ABCD1234'); // API key
Bad:
php
// In controller
PaymentGateway::init(env('PAYMENT_API_KEY')); // API key
Good:
# In .env
PAYMENT_API_KEY=ABCD1234
php
// In config/payment.php
[
'api_key' => env('PAYMENT_API_KEY'),
]
php
// In 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 every time there is an update 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 directly, especially production. 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 work on immediately from the latest codebase.
Don't put any logic in route files
Put logic in controllers and reference it from routes. As a rule of thumb, don't put any Closure in route files.
Use cast for boolean attributes of Eloquent models
Many database engines don't have a native boolean type, so most schema designs use integer for this. In fact, Laravel migrations define a boolean field as TINYINT
for MySQL. To use an attribute as a boolean in PHP, you need to specify it in the Eloquent model's $cast
property.
php
// In migration
Schema::table('users', function (Blueprint $table) {
// ...
$table->boolean('is_admin');
});
class User
{
protected $casts = [
'is_admin' => 'boolean',
];
public function isAdmin(): bool
{
return $this->is_admin; // This will return true/false instead of 1/0
}
}
Use shorthand to check for existence with Eloquent
After a query, there are different ways to check if an Eloquent object exists, such as empty()
, is_null
, and === null
. In fact, a simple Not Operator !
suffices. Laravel uses this itself.
php
$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()
.
php
$products = Product::where('name', $name)->get();
if ($products->isEmpty()) {
Log::error('No products match name.');
}
Prevent Lazy Loading
Lazy loading is bad and harms performance. Disable it during development so Laravel will throw an exception noticing you about a lazy loading so you can fix it right away.
php
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
Model::preventLazyLoading(! $this->app->isProduction());
}
}
Intermediate
Move request validation to Form Requests
Bad:
php
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:
php
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 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.
php
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 usage declarations and lose you 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.
php
$table->foreignId('user_id');
Use the primary key id
in models
Respect the primary key id
for your models.
Sometimes, a table may feel like it doesn't need an id
field. Perhaps it has a field that looks like a primary key, such as a unique code
in a tickets
table. Don't make it primary key. Have id
as a dedicated primary key anyway.
id
has been the primary key for models in most design conventions. It gives you an indexed table for free, and follows RESTful and other design patterns. 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 an 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 testing and potential restructuring easily.
In the rare cases when you can't use Query Builder, use raw queries with great care.
Stream between Filesystems
When you need to copy a file from a Filesystem to another, stream instead of reading the whole content of the file.
Bad:
php
$fileContent = Storage::disk('s3')->get('from/file.jpg');
Storage::disk('ftp')->put('to/file.jpg', $fileContent);
Good:
php
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 and type hint the variable.
Bad:
php
foreach ($users as $user) {
$user->invite();
}
Good:
php
$users->each(function (User $user) {
$user->invite();
});
Advanced
Thin models, thin controllers
Both your models and controllers should be thin, containing only necessary business logic specific to the class. Refactor complicated logic into 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 trait, repository, or service.
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 directly 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 carried 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. Laravel serializes all properties of a job when sending to queue. Less data is always good for efficient storage holding, deserialization, and performance.
Cache data
Cache all the data that are frequently queried, especially static data that don't change often.
You can also cache dynamic data related to models, and use Observers to refresh the cache.
TIP
Some developers even use the request URL as the key for caching.
Break a command down into jobs
If a command feels like it does too much, refactor its 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:
php
// ProcessImagesCommand.php
public function handle()
{
$this->images->each(function (Image as $image) {
$image->process(); // Heavy operation
});
}
Good:
php
// ProcessImagesCommand.php
public function handle()
{
$this->images->each(function (Image $image) {
ProcessImageJob::dispatch($image); // Refactor into job
});
}
// ProcessImageJob.php
public function handle()
{
$this->image->process();
}