Laravel Controller: 4 tips to refactor Laravel Controllers to be a senior developer

Root User

Apr 24, 2020

In this blog post I will share some tips to refactor your controller, Before to get started into the main content, I would like to share some background knowledge behind the controllers. Laravel is an MVC(Model View Controller) framework, so when a user hits some endpoint for ex. (http://example.com) ,Laravel searches routes that we defined int the route files i.e. routes/web.php in and further the request to matched controller. I have explained about the laravel routing in the previous article Here.

1. Use FormRequest for Form validation

Suppose we are Creating a small application for school, and adding a feature to allow admin to create a exam. We noticed that exam will have name, class_id, and **marks_distribution **(marks in a exam can distributed into as Practical, written, Attendance etc as well). The data types of these field will be string, integer, and array respectively. As raw user inputs are vulnerable in a security aspect, so it is good practice to validate user's form input data before to proceed into our application. So we did as:

At first we define a end points in rotues/web.php to store the exam as

// rotues/web.php
Route::post('exams','[email protected]');

and then validate the user's input in the controller as,

// app/Http/Controllers/ExamController.php
...
use Illuminate\Http\Request;

class ExamController extends Controller
{
    public function store(Request $request)
    {
        $this->validate(
            $request, [
                'name' => 'required|max:255',
                'class_id' => 'required|integer',
                'marks_distribution' => 'required|array',
            ]
        );
        // Remaining Logics goes here.
    }
}   

The above implementation is okay with small form, but when the form field input grows to large number may be 10 or 20, It covers large space in controllers. And as The single-responsibility principle (SRP) is a says that every class should be responsible for a single part of the functionality. So Let the Controller do it's work and separate the validation part outside of the controller. To do so we create a FormRequest class as:

php artisan make:request ExamStoreRequest

the above command generates the following class, In which we move all the validation logics to the rules function of this class. authorize function defines whether the user is authorized to perform this action. So by default we make it true for simplicity.

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class ExamStoreRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'name' => 'required|max:255',
            'class_id' => 'required|integer',
            'marks_distribution' => 'required|array',
        ];
    }
}

and to validate the form input, the above ExamStoreRequest Class is type-hinted into the controller's store method as follows.

use App\Http\Requests\ExamStoreRequest;

class ExamController extends Controller
{
	public function store(ExamStoreRequest $request)
    {
    	 // Remaining Logics goes here.
    }
}

Which is much cleaner than the previous one. Now we can add any numbers of validation rules into our dedicated form request class and even can add authorization logics to the requests as.

/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
    // Can make this request if the user is admin.
    return auth()->user()->isAdmin(); // Logic to check admin
}

2. Use Dedicated action Class to Perform Other Operation

To continue above example, after form validation, we need to create a exam into our database and then may be need to send emails to students, Or may be notification to admin. Let's implement this features.

public function store(ExamStoreRequest $request)
{
	Exam::create($request->all());
	// Logics to send Notification
	// Logics to send Email
	// Logics to Process Others Jobs
} 

Though, I have shown one line comment to the send Notification, Process Jobs, and send Emails, It may not be simple as we thought. Other problem here is that You can't test a individual action. For example, Consider above Job process task is a complex task and you want to perform a unittest for that single Job process unit only. Unfortunately you can't do it because your job processing code is tightly coupled with the controller. So to solve this let's create a action Class as.

// app/Http/Action/ExamStoreAction.php

use Illuminate\Http\Request;

class ExamStoreAction{

	public function execute($data=[]){
		$this->sendNotificationToAdmin();
		$this->sendEmailToAdmin();
		$this->processJobs();
	}
	
	public function sendNotificationToAdmin(){
		// Logic Goes here
	}
	
	public function sendEmailToAdmin(){
		// Logic Goes here.
	}
	public function processJobs(){
		// Logic Goes here
	}
}

Here, we define a action class for exam store, we will call the execute method from our controller class as follows.

use App\Http\Requests\ExamStoreRequest;
use App\Http\Action\ExamStoreAction;

class ExamController extends Controller
{
	public function store(ExamStoreRequest $request, ExamStoreAction $examStoreAction)
    {
    	 $examStoreAction->execute($request);
    }
}

As in the example, the controller class is much cleaner that all of our business logic is moved to the action class. As I mentioned earlier that It will help to test individual component, So to illustrate the testing part, you can test as:

use App\Http\Action\ExamStoreAction;

class ExamStoreJobstest extends TestCase{
    
    /** @test **/
    public function action_will_happen_when_jobs_process(){
        // Execute Jobs 
        app(ExamStoreAction::class)->processJobs();
        
        // Your assertion goes here
        // assert Job is processed.
    }
}

3. Use Laravel Observer Pattern

Instead of above action approach, we can also use observer pattern instead to clean up controller. The Logic behind observer is that the some methods will execute when some events happen to the model. For example: Let's create a new observer class as

php artisan make:observer EventObserver --model=Event
<?php

namespace App\Observers;

use App\Exam;

class ExamObserver
{
    /**
     * Handle the exam "created" event.
     *
     * @param  \App\Exam  $exam
     * @return void
     */
    public function created(Exam $exam)
    {
        //
    }

    /**
     * Handle the exam "updated" event.
     *
     * @param  \App\Exam  $exam
     * @return void
     */
    public function updated(Exam $exam)
    {
        //
    }

    /**
     * Handle the exam "deleted" event.
     *
     * @param  \App\Exam  $exam
     * @return void
     */
    public function deleted(Exam $exam)
    {
        //
    }

    /**
     * Handle the exam "restored" event.
     *
     * @param  \App\Exam  $exam
     * @return void
     */
    public function restored(Exam $exam)
    {
        //
    }

    /**
     * Handle the exam "force deleted" event.
     *
     * @param  \App\Exam  $exam
     * @return void
     */
    public function forceDeleted(Exam $exam)
    {
        //
    }
}

And at last register the observer in Service provider. as

<?php

namespace App\Providers;

use App\Observers\ExamObserver;
use App\Event;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        Event::observe(ExamObserver::class);
    }
}

Here, when you create a exam model, created method of ExamObserver class will be trigger and respective methods will be triggered for the other event as well.

4. Use Data Object Class

in another example, Suppose we are creating a resource may be a resume for a Jobseeker. And we are passing a lists of models as

class ResumeController extends Controller
{

    public function create()
    {
        $currencies = Currency::where('status', 'Active')->get();

        $positionLevel = PositionLevel::where('status', 'Active')->get();

        $availableFor = AvailableFor::where('status', 'Active')->get();

        $categories = Category::where('status', 'Active')
            ->where('flag', '!=', 'industry')->limit(200)->get();

        $cities = City::where('status', 'Active')->limit(23)->get();

        return view('backend.jobseeker.resume.create', compact('currencies', 'positionLevel', 'availableFor', 'categories', 'cities'));
    }
}

You can clean up these controller by creating a dedicated a data object class as.

class ExamCreateFormObject{
    public function getFormData(){
        $currencies = Currency::where('status', 'Active')->get();

        $positionLevel = PositionLevel::where('status', 'Active')->get();

        $availableFor = AvailableFor::where('status', 'Active')->get();

        $categories = Category::where('status', 'Active')
            ->where('flag', '!=', 'industry')->limit(200)->get();

        $cities = City::where('status', 'Active')->limit(23)->get();
        return  compact('currencies', 'positionLevel', 'availableFor', 'categories', 'cities');
    }
}

and Call the method getFormData From the controller.

class ResumeController extends Controller
{

    public function create(ExamCreateFormObject $examCreateFormObject)
    {
        $data = $examCreateFormObject->getFormData();
        return view('backend.jobseeker.resume.create', $data);
    }
}

Wrapping Up

In this post, I share some tips to clean up your controllers. First tip was to extract validation Logics to dedicated form request class. Second was to use Action class, Third was to use Laravel Model Observer, last one is to use a dedicated object class. Hope You like this post. Thanks.

Related Articles