Laravel inner power resides in details. Let me give you these 7 tips to keep your code maintainable.
- 1. Controllers should... well, control
- 2. Cleaner validation with Requests Forms
- 3. Type hint your models in your controllers
- 4. Centralized error handling
- 5. Collections... collections everywhere
- 6. Factorize your routes with Resources
- 7. Lean code with mutators
1. Controllers should... well, control
Ever seen/wrote a big boy like this?
use Validator;
use Kreait\Firebase\Factory;
use Kreait\Firebase\ServiceAccount;
class AuthenticationController extends Controller {
public function index(Request $request) {
if ($request->wantsJson()) {
$validator = Validator::validate($request->all(), [
'token' => 'required|filled|string'
]);
if ($validator->fails()) {
return response()->json('wrong token', 400);
}
$serviceAccount = ServiceAccount::fromArray(config('firebase'));
$firebase = (new Factory)->withServiceAccount($serviceAccount)
->create()
->getAuth();
try {
$firebase->verifyIdToken($token);
return response()->json('ok');
} catch(Exception $exception) {
app('sentry')->captureException($exception);
return response()->json('unauthorized', 403);
}
} else {
abort(403);
}
}
}
What about this:
use App\Firebase;
use App\Http\Requests\AuthenticationRequest;
class AuthenticationController extends Controller {
public function index(AuthenticationRequest $request) {
if ($request->wantsJson()) {
if (Firebase::tokenValid($request->input('token'))) {
return response()->json('ok');
} else {
return response()->json('unauthorized', 403);
}
} else {
abort(403);
}
}
}
We use several things to help us free the business logic out of the rest. I will cover Request forms, which helped me here to remove the validation step out of the controller, and Error handling in this post.
But must important, do not put business logic in your controllers. Controllers should remain light and only use the tools it needs to take decisions on how to reroute the request. In the end, it is just a request controller, nothing more. Any business decisions should be located on separated Models.
And yes, you can create classes that are not necessarily related to a table. In my example, I modelized a "problem" using a class: the Firebase authentication.
2. Cleaner validation with Form requests
I love validation with Laravel. I like the clean and expressive way they are used. Typically an example of validating a user cart.
use App\Product;
use App\Order;
use App\Session;
class CartController extends Controller {
public function store(Request $request) {
$request->validate([
'id' => 'required|exists:product,id'
]);
// Request passed, time to save the data
$productId = $request->input('id');
$product = Product::find($productId);
Order::insert([
'productId' => $product->id
]);
Session::flash('success', 'Order completed!');
return response()->view('product');
}
}
This is nice, but we could simplify more. First you will need to create a Form Request.
php artisan make:request CartRequest
This will create a file on app/Http/Requests/CartRequest.php
. Open it, it looks like this:
use Illuminate\Foundation\Http\FormRequest;
class CartRequest extends FormRequest {
public function authorize() {
return true; // Unless you have Guards, set it to true.
}
public function rules() {
return [
'id' => 'required|exists:product,id'
];
}
}
So now we can use it in our controller:
use App\Product;
use App\Order;
use App\Session;
use App\Http\Requests\CartRequest;
class CartController extends Controller {
public function store(CartRequest $request) {
// The request is validated using our CartRequest
$productId = $request->input('id');
$product = Product::find($productId);
Order::insert([
'productId' => $product->id
]);
Session::flash('success', 'Order completed!');
return response()->view('product');
}
}
This is a great way to strip up the fuzz and focus on the business algorithm. And the best, you can reuse form requests for models that share similar validation.
use App\Http\Requests\ProductRequest;
class Shoe extends Controller {
public function index(ProductRequest $request) {
// ...
}
}
class Clothing extends Controller {
public function index(ProductRequest $request) {
// ...
}
}
3. Type hint your models in your controllers
Sometimes I wrote 3 kilometers long routes, and my controllers follow along...
Route::get('/customer/{customerId}/contract/{contractId}/invoice/{invoiceId}', 'CustomerContractInvoiceController@show');
use App\Customer;
use App\Contract;
use App\Invoice;
class CustomerContractInvoiceController extends Controller {
public function show(Request $request, int $customerId, int $contractId, int $invoiceId) {
$customer = Customer::findOrFail($customerId);
$contractId = Contract::findOrFail($contractId);
$invoice = Invoice::findOrFail($invoiceId);
// ...
}
}
Laravel can handle the model fetching for you using Route model binding. Resistance is futile...
use App\Customer;
use App\Contract;
use App\Invoice;
class CustomerContractInvoiceController extends Controller {
public function show(Request $request, Customer $customer, Contract $contract, Invoice $invoice) {
$customerName = $customer->name;
// ...
}
}
This way you can directly use the Eloquent models. To do this, Laravel needs you to properly specify the $primaryKey
property on your model in case this is not id
:
use Illuminate\Database\Eloquent\Model;
class Customer extends Model {
protected $primaryKey = 'customerId';
}
4. Centralized error handling
I am exhausted of having to check for errors in my controllers.
use Validator;
use Session;
class NewsletterController extends Controller {
public function store(Request $request) {
$validator = new Validator($request->all(), [
'email' => 'required|email'
]);
if ($validator->fails) {
return redirect()->back()->withInput()->withErrors($validator);
}
Session::flash('success', 'We publish each week so stay tuned!');
return redirect()->route('newsletter.success');
}
}
So I configure my global Error handling to take advantage of exceptions thrown by Form Requests. To do so, go to app/Exceptions/Handler.php
.
class Handler extends ExceptionHandler {
public function report(Exception $exception) {
// This is where you log to your error dashboard like Sentry, ...
parent::report($exception);
}
public function render($request, Exception $exception) {
// Here we manage errors globally
}
}
So the method that we want to customize is render
. Report is great to push a notification to an error management app like Sentry.
Let us handle validation errors globally.
use Illuminate\Validation\ValidationException;
class Handler extends ExceptionHandler {
public function report(Exception $exception) {
// This is where you log to your error dashboard like Sentry, ...
parent::report($exception);
}
public function render($request, Exception $exception) {
if ($exception instanceof ValidationException) {
return redirect()->back()->withInput()->withErrors($exception->errors());
}
}
}
You can even make it manage navigation and AJAX errors in a single shot.
use Illuminate\Validation\ValidationException;
class Handler extends ExceptionHandler {
public function report(Exception $exception) {
// This is where you log to your error dashboard like Sentry, ...
parent::report($exception);
}
public function render($request, Exception $exception) {
// The request have a "Accept: application/json"
// This is an AJAX request
if ($request->wantsJson()) {
if ($exception instanceof ValidationException) {
return response()->json($exception->errors(), 400);
}
}
// This is a normal form validation
else {
if ($exception instanceof ValidationException) {
return redirect()->back()->withInput()->withErrors($exception->errors());
}
}
}
}
5. Collections... collections everywhere
What I regret with PHP is the lack of OOP. Javascript is so cool with its POP.
const popularPosts = posts.filter(post => post.views > 5000)->map(post => post.title);
In Laravel, one of the best feature is Collections. Eloquent models becomes so handy and we can perform a lot of things fluently.
use App\Post;
$popularPosts = Post::all()->filter(function($post) {
return $post->views > 5000;
})->map(function($post) {
return $post->title;
});
And you can even use it outside Eloquent usage!
$menus = [
[ 'placement' => 'left', 'text' => 'Home' ],
[ 'placement' => 'left', 'text' => 'About us' ],
[ 'placement' => 'right', 'text' => 'Contact' ]
];
$rightMenus = collect($menus)->filter(function($menu) {
return $menu['placement'] === 'right';
});
6. Factorize your routes with resources
I do not know if it happened to you, but one time I built a web app at my job (still in production), and my routes/web.php
became to look like this:
// routes/web.php
Route::get('/customer', 'CustomerController@index');
Route::get('/customer/create', 'CustomerController@create');
Route::get('/customer/{id}', 'CustomerController@show');
Route::get('/customer/{id}/edit', 'CustomerController@edit');
Route::get('/customer/{id}/delete', 'CustomerController@delete');
Route::get('/customer/{id}/phone', 'CustomerPhoneController@index');
Route::get('/customer/{id}/phone/create', 'CustomerPhoneController@create');
Route::get('/customer/{customerId}/phone/{phoneId}', 'CustomerPhoneController@show');
Route::get('/customer/{customerId}/phone/{phoneId}/edit', 'CustomerPhoneController@edit');
Route::post('/customer', 'CustomerController@store');
Route::post('/customer/{id}/phone', 'CustomerPhoneController@store');
Route::put('/customer/{id}', 'CustomerController@update');
Route::put('/customer/{customerId}/phone/{phoneId}', 'CustomerPhoneController@update');
Route::delete('/customer/{id}', 'CustomerController@destroy');
Route::delete('/customer/{customerId}/phone/{phoneId}', 'CustomerPhoneController@destroy');
... And this is an excerpt. I also manage customer addresses, customer emails, customer companies, customer contracts, customer contracts invoices, ...
Fortunately Laravel have our backs with Resources controllers.
// routes/web.php
Route::resource('customer', 'CustomerController');
Route::resource('customer.phone', 'CustomerPhoneController');
Finally some fresh air...
Resources works by automatically binding your first argument to the 4 common HTTP methods. In fact if you run
php artisan route:list
You would have the following output.
Domain | Method | URI | Name | Action | Middleware |
---|---|---|---|---|---|
GET,HEAD | /customer | customer.index | App\Http\Controllers\CustomerController@index | web | |
GET,HEAD | /customer/{customer} | customer.show | App\Http\Controllers\CustomerController@show | web | |
GET,HEAD | /customer/{customer]/edit | customer.edit | App\Http\Controllers\CustomerController@edit | web | |
GET,HEAD | /customer/{customer}/phone | customer.phone.index | App\Http\Controllers\CustomerPhoneController@index | web | |
GET,HEAD | /customer/{customer/phone/{phone} | customer.phone.show | App\Http\Controllers\CustomerPhoneController@show | web | |
GET,HEAD | /customer/{customer}/phone/{phone}/edit | customer.phone.edit | App\Http\Controllers\CustomerPhoneController@edit | web | |
POST | /customer | customer.store | App\Http\Controllers\CustomerController@store | web | |
POST | /customer/{customer}/phone | customer.phone.store | App\Http\Controllers\CustomerPhoneController@store | web | |
PUT,PATCH | /customer/{customer} | customer.update | App\Http\Controllers\CustomerController@update | web | |
PUT,PATCH | /customer/{customer}/phone/{phone} | customer.phone.update | App\Http\Controllers\CustomerPhoneController@update | web | |
DELETE | /customer/{customer} | customer.destroy | App\Http\Controllers\CustomerController@destroy | web | |
DELETE | /customer/{customer}/phone/{phone} | customer.phone.destroy | App\Http\Controllers\CustomerPhoneController@destroy | web |
And again, Laravel got your covered, so you can use this command line to create your resource controllers:
php artisan make:controller CustomerController --resource
Which will automatically create the methods above for you!
// app/Http/Controllers/CustomerController.php
use Illuminate\Http\Request;
class CustomerController extends Controller
{
public function index() {}
public function create() {}
public function store(Request $request) {}
public function show($id) {}
public function edit($id) {}
public function update(Request $request, $id) {}
public function destroy($id) {}
}
And... if you create a model, you can also create its associated controller in resource mode, so that Laravel will type hint your controller for you.
php artisan make:model Customer --resource --controller
// app/Http/Controllers/CustomerController.php
use App\Customer;
use Illuminate\Http\Request;
class CustomerController extends Controller
{
public function index() {}
public function create() {}
public function store(Request $request) {}
public function show(Customer $customer) {}
public function edit(Customer $customer) {}
public function update(Request $request, Customer $customer) {}
public function destroy(Customer $customer) {}
}
7. Lean code with mutators
Sometimes, mixing multiple field of a record is useful to create meaningful content. For example, in a customer view:
<!-- resources/views/customer/show.blade.php -->
@extends('layout/logged')
@section('content')
<h1>viewing customer {{ $customer->firstName }} {{ $customer->lastName }}</h1>
@endsection
Mutators will help you abstracting your business logic.
// app/Customer.php
use Illuminate\Database\Eloquent\Model;
class Customer extends Model {
protected $table = 'customer';
public function getNameAttribute() {
return "{$this->firstName} {$this->lastName}";
}
}
To have access to a $customer->name
attribute, you need to create a function like above, in the form public function get<yourattribute>Attribute() {}
. Laravel will then provide you this attribute when you collect an Eloquent model.
<!-- resources/views/customer/show.blade.php -->
@extends('layout/logged')
@section('content')
<h1>{{ $customer->name }}</h1>
@endsection
Even better, you can also cast the data comming from your database.
Imaging you have a boolean field in your customer table named wantsNewsletter
. As you know, boolean type in MySQL does not exists, so it simulates it with this column:
wantsNewsletter TINYINT(1) NOT NULL DEFAULT 0
0 for false
, 1 for true
. In your eloquent model, you can ask Laravel to cast it to a boolean:
// app/Customer.php
use Illuminate\Database\Eloquent\Model;
class Customer extends Model {
protected $table = 'customer';
protected $casts = [
'wantsNewsletter' => 'boolean'
];
public function getNameAttribute() {
return "{$this->firstName} {$this->lastName}";
}
}
Now you can use a triple equal operator to perform your verification.
// app/Console/Commands/SendNewsletter.php
use Illuminate\Console\Command;
use App\Mail\NewsletterMail;
use App\Customer;
class SendNewsletter extends Command {
protected $signature = 'newsletter:send';
public function handle() {
$customers = Customer::all();
$customers = $customers->filter(function($customer) {
$customer->wantsNewseletter === true;
});
foreach($customers as $customer) {
Mail::to($customer->email)->send(new NewsletterMail($customer));
}
}
}
Conclusion
All of this can be overwhelming. Do not apply those in a row. Try to find a balance between what you feel is right and what you can do to improve your code base.
Adapting a quote found from Vuex website:
These tips are like glasses: You will know when you need it.
I hope you are as amazed as I was when I learn those techniques. Tell me in comments if you learned something new, if you have others tips I did not listed in, or if I can do better with those tips.
Happy optimizations!
Top comments (4)
Extremely helpful. Thanks!
Exraordinary work have been done here. Thank you a lot.
Thank's
Hats off!