Leveraging SOLID Principles To Refactor A Laravel Controller

Reuben FrimpongReuben Frimpong
6 min read

Have you ever experienced a situation in your Laravel or any MVC application where it just doesn’t feel right to have a controller for certain models because of how they are interacted with?

Context
I have this Laravel application for managing my church and there are these models, Bacenta, Chapel and Minstry which are not really meant to change often and even if they are to change a user with administrative rights will be the one to effect the change. In the initial stages of the app it was assumed that the Chapel model will never change and from the developer perspective Bacenta and Minstry chapels did almost the same thing so I thought to myself it will be okay to have a SettingsController that handles the basic CRUD actions and probably one if statement to handle the minor differences that might occur.

For 3 years, this was all that I needed until the app reached a point where the Chapel model also required CRUD actions. I thought to myself, “That’s not a problem! I will just add another if statement“ and pass some parameters from the client which will tell me what model to use and how to respond.

Later, I realized that the table structures were not the same for all 3 models so I had to also pass what columns to query from the client. As if that was not enough, I realized some models could depend on more than one column when searching so I added extra params to dynamically determine which columns to use and it was at this point that I realized how much I have deviated from the original intention of the controller and how much the controller was begging to be refactored.
This is the initial SettingsController I ended up with before the refactor

 <?php

namespace App\Http\Controllers;

use App\Http\Requests\SettingsRequest;
use App\Models\Bacenta;
use App\Models\Ministry;
use Exception;
use Symfony\Component\HttpFoundation\Response;


class SettingsController extends Controller
{
    public function index () {
      $Model = 'App\\Models\\'.request()->model;
      $searchTerm = request()->searchTerm;
      $searchKey = request()->searchKey;
      $secondaryKey = request()->secondaryKey ?? 'uid';
      if(is_null($searchTerm)) {
        $list = $Model::orderBy($searchKey)->get();
        return response()->json(['list' => $list], Response::HTTP_OK);
      }
      $list = $Model::where($searchKey, 'like', $searchTerm.'%')->orWhere($secondaryKey, 'like', $searchTerm.'%')->limit(10)->get();
      return response()->json(['list' => $list], Response::HTTP_OK);;
    }

    public function store(SettingsRequest $request) {
      try {
        $isChapelModel = request()->model == 'Chapel';
        $Model = 'App\\Models\\'.request()->model;
        $payload = $request->validated();
        if ($isChapelModel) {
          unset($payload['chapel_id']);
        } else {
          $lastUid = $Model::latest()->limit(1)->value('uid');
          $uid = is_null($lastUid) ? 1001 : substr($lastUid, 1)+1; 
          $prefix = substr(request()->model, 0, 1);
          $payload = array_merge($request->validated(), ['uid' => $prefix.$uid]);
        }
        $Model::create($payload);
        return response()->json(['message' => 'Entity created successfully'], Response::HTTP_CREATED);
      } catch (Exception $e) {
        return response()->json([
          'message' => 'Unable to create entity',
        ], Response::HTTP_INTERNAL_SERVER_ERROR);
      }

    }

    public function update(SettingsRequest $request) {
      try {
        $Model = 'App\\Models\\'.request()->model;
        $validated = $request->validated();
        $payload = [
          'name' => $validated['name'],
          'chapel_id' => $validated['chapel_id']
        ];
        $isChapelModel = request()->model == 'Chapel';
        if ($isChapelModel) {
          unset($payload['chapel_id']);
        }
        $Model::whereId($validated['id'])->update($payload);
        return response()->json(['message' => 'Entity updated successfully'], Response::HTTP_CREATED); 
      } catch (Exception $e) {
        return response()->json([
          'message' => 'Unable to update entity',
        ], Response::HTTP_INTERNAL_SERVER_ERROR);
      }
    }

}

My Approach To Refactoring

When I looked at the code I had ended up with I thought to myself, why not allow each model to decide uniquely how to handle each of the user requests without cluttering it with unrelated logic as well as avoiding the need to create a new controller for each model because in this context, it just didn’t feel right.

Here are the 3 things that came to mind

  1. Use some kind of class to service the controller

  2. The service class should then delegate the implementation of the actual logic to whichever model that is responsible for it

  3. All models involved should have some strict agreement to implement certain required methods

Just by reading the above you should be able to deduce the solution but I will share what I arrived at anyways

  1. A SettingsService class whose responsibility it is to delegate to various model implementations
  •     <?php
    
        namespace App\Services;
    
        use Illuminate\Http\Request;
    
        class SettingsService {
          public function index (Request $request)
          {
            $model = 'App\\Models\\'.$request->model;
            return (new $model)->index($request);
          }
    
          public function store (Request $request)
          {
            $model = 'App\\Models\\'.$request->model;
            return (new $model)->store($request->validated());
          }
    
          public function update (Request $request)
          {
            $model = 'App\\Models\\'.$request->model;
            return (new $model)->modify($request->validated());
          }
        }
    

    2. A SettingsInterface which enforces the strict implementation of the methods invoked in the service class

  •     <?php
    
        namespace App\Interfaces;
    
        use Illuminate\Database\Eloquent\Collection;
        use Illuminate\Http\Request;
    
        interface SettingsInterface {
    
          public function index(Request $request) : Collection;
    
          public function store(array $paload);
    
          public function modify(array $paload);
    
          public function remove(int $id);
        }
    

    3. Lastly, the individual models implementing the various methods in their unique ways

  • Bacenta Model

  •     <?php
    
        namespace App\Models;
    
        use App\Interfaces\SettingsInterface;
        use Illuminate\Database\Eloquent\Collection;
        use Illuminate\Database\Eloquent\Model;
        use Illuminate\Http\Request;
    
        class Bacenta extends Model implements SettingsInterface
        {
            public function index(Request $request) : Collection
            {
              $searchTerm = $request->searchTerm;
              if(is_null($searchTerm)) {
                return $this::orderBy('name')->get();
              }
              return $this::where('name', 'like', $searchTerm.'%')->orWhere('uid', 'like', $searchTerm.'%')->limit(10)->get();
            }
    
            public function store(array $payload) 
            {
              $lastUid = $this::latest()->limit(1)->value('uid');
              $uid = is_null($lastUid) ? 1001 : substr($lastUid, 1)+1; 
              $payload = array_merge($payload, ['uid' => 'B'.$uid]);
              $this->create($payload);
            }
    
            public function modify(array $payload) {
              $this::whereId($payload['id'])->update($payload);
            }
    
            public function remove(int $id) {
                // TO BE IMPLEMENTED
            }
        }
    

    Ministry model

  •     <?php
    
        namespace App\Models;
    
        use App\Interfaces\SettingsInterface;
        use Illuminate\Database\Eloquent\Collection;
        use Illuminate\Database\Eloquent\Model;
        use Illuminate\Http\Request;
    
        class Ministry extends Model implements SettingsInterface
        {
            public function index(Request $request) : Collection
            {
              $searchTerm = $request->searchTerm;
              if(is_null($searchTerm)) {
                return $this::orderBy('name')->get();
              }
              return $this::where('name', 'like', $searchTerm.'%')->orWhere('uid', 'like', $searchTerm.'%')->limit(10)->get();
            }
    
            public function store(array $payload) 
            {
              $lastUid = $this::latest()->limit(1)->value('uid');
              $uid = is_null($lastUid) ? 1001 : substr($lastUid, 1)+1; 
              $payload = array_merge($payload, ['uid' => 'M'.$uid]);
              $this->create($payload);
            }
    
            public function modify(array $payload) {
              $this::whereId($payload['id'])->update($payload);
            }
    
            public function remove(int $id) {
                // TO BE IMPLEMENTED
            }
    
        }
    

    Chapel model

  •     <?php
    
        namespace App\Models;
    
        use App\Interfaces\SettingsInterface;
        use Illuminate\Database\Eloquent\Model;
        use Illuminate\Database\Eloquent\Collection;
        use Illuminate\Http\Request;
    
        class Chapel extends Model implements SettingsInterface
        {
    
            public function index(Request $request) : Collection
            {
              $searchTerm = $request->searchTerm;
              if(is_null($searchTerm)) {
                return $this::orderBy('name')->get();
              }
              return $this::where('name', 'like', $searchTerm.'%')->limit(10)->get();
            }
    
            public function store(array $payload) 
            {
              unset($payload['chapel_id']);
              $this::create($payload);
            }
    
            public function modify(array $payload) {
              unset($payload['chapel_id']);
              $this::whereId($payload['id'])->update($payload);
            }
    
            public function remove(int $id) {
                // TO BE IMPLEMENTED
            }
        }
    

Finally, lets look at the new SettingsController

<?php

namespace App\Http\Controllers;

use App\Http\Requests\SettingsRequest;
use App\Services\SettingsService;
use Exception;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;


class SettingsController extends Controller
{
    public $settingsService;

    public function __construct(SettingsService $service)
    {
      $this->settingsService = $service;
    }

    public function index (Request $request) {
      $list = $this->settingsService->index($request);
      return response()->json(['list' => $list], Response::HTTP_OK);;
    }

    public function store(SettingsRequest $request) {
      try {
        $this->settingsService->store($request);
        return response()->json(['message' => 'Entity created successfully'], Response::HTTP_CREATED);
      } catch (Exception $e) {
        return response()->json([
          'message' => $e->getMessage(),
        ], Response::HTTP_INTERNAL_SERVER_ERROR);
      }

    }

    public function update(SettingsRequest $request) {
      try {
        $this->settingsService->update($request);
        return response()->json(['message' => 'Entity updated successfully'], Response::HTTP_CREATED); 
      } catch (Exception $e) {
        return response()->json([
          'message' => 'Unable to update entity',
        ], Response::HTTP_INTERNAL_SERVER_ERROR);
      }
    }
}

The immediate benefit I derived was that I no more depended on the client to tell me which columns to select. All I needed to know was which model the user is interacting with on the client and that’s how I refactored my code.

Final Note

Remember, the point of this article is not to flex my design pattern or SOLID skills but to show how I used it to solve my unique problem in my code.

Connect with me on Twitter and LinkedIn.

0
Subscribe to my newsletter

Read articles from Reuben Frimpong directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Reuben Frimpong
Reuben Frimpong

A Software engineer with passion for learning and experimenting. I love to build products and try out my business and marketing skills as well.