DEV Community

Cover image for 7 tips to stay productive with Laravel
Anwar
Anwar

Posted on • Updated on

7 tips to stay productive with Laravel

Laravel inner power resides in details. Let me give you these 7 tips to keep your code maintainable.

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);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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');
  }
}
Enter fullscreen mode Exit fullscreen mode

This is nice, but we could simplify more. First you will need to create a Form Request.

php artisan make:request CartRequest
Enter fullscreen mode Exit fullscreen mode

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'
    ];
  }
}
Enter fullscreen mode Exit fullscreen mode

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');
  }
}
Enter fullscreen mode Exit fullscreen mode

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) {
    // ...
  }
}
Enter fullscreen mode Exit fullscreen mode

HAPPY ROBERT REDFORD GIF

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');
Enter fullscreen mode Exit fullscreen mode
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);

    // ...
  }
}
Enter fullscreen mode Exit fullscreen mode

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;

    // ...
  }
}
Enter fullscreen mode Exit fullscreen mode

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';
}

Enter fullscreen mode Exit fullscreen mode

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');
  }
}
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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());
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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());
      }      
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

BEST GIF THUMBS UP GIF

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);
Enter fullscreen mode Exit fullscreen mode

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;
});
Enter fullscreen mode Exit fullscreen mode

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';
});
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

... And this is an excerpt. I also manage customer addresses, customer emails, customer companies, customer contracts, customer contracts invoices, ...

OH NO OOPS GIF BY HALLMARK CHANNEL<br>

Fortunately Laravel have our backs with Resources controllers.

// routes/web.php

Route::resource('customer', 'CustomerController');
Route::resource('customer.phone', 'CustomerPhoneController');
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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) {}
}

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
// 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) {}
}
Enter fullscreen mode Exit fullscreen mode

Productivity level: over 9000 meme

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
Enter fullscreen mode Exit fullscreen mode

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}";
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}";
  }
}
Enter fullscreen mode Exit fullscreen mode

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));
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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!

Photo by nappy from Pexels

Top comments (4)

Collapse
 
sixldaniel profile image
Daniel Sixl

Extremely helpful. Thanks!

Collapse
 
labibllaca profile image
labibllaca

Exraordinary work have been done here. Thank you a lot.

Collapse
 
rudestewing profile image
Rudi Setiawan

Thank's

Collapse
 
tjmapes profile image
TJ Mapes

Hats off!