loading...
Cover image for 7 tips to stay productive with Laravel

7 tips to stay productive with Laravel

khalyomede profile image Khalyomede Updated on ・9 min read

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

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

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

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);

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, ...

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

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

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

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!

Photo by nappy from Pexels

Posted on by:

khalyomede profile

Khalyomede

@khalyomede

Fullstack developer @ Carlili

Discussion

markdown guide