PHP S.O.L.I.D Principles


Solid are PHP principles to help expand complicated code to be more readable and easier to handle.

  1. S - Single-responsibility principle
  2. O - Open-closed principle
  3. L - Liskov substitution principle
  4. I - Interface segregation principle
  5. D - Dependency Inversion Principle
  6. Summary

Single responsibility principle

Every module or class should have one responsibility, in a practical example can a complicated method be separated into multiple?

In the example below, currently, there are validation rules and the worker database structure that can be moved into separate classes. For Laravel, we could create a Form Request to validate the form. Furthermore, a custom repository to could be created to handle the CRUD methods that should not be known to a Controller.

/**
 * Original code
 */
class WorkerController extends Controller
{
    public function store(Request $request, Worker $worker)
    {
        $validated_data = $request->validate([
          'name' => 'required',
          'email' => 'required|email',
        ])

        $worker->name = $request->name
        $worker->email = $request->email;
        $worker->save();

        return response()->json(['worker' => $worker], 201);
    }
}

/**
 * Refactored code
 */
class WorkerController extends Controller
{
    public function store(StoreWorkerRequest $request, WorkerRepository $worker_repository)
    {
        $worker = $worker_repository->create($request->validated());

        return response()->json(['worker' => $worker], 201);
    }
}

Open closed principle

When code is extendable (open) but closed for modification, this means that the original existing code should never be altered, however the basic functionality can be extended for further development.

Take the example below, perhaps we only currently have one method in the WorkableInterface, this of course can be extended upon. The original method is never altered but instead we are extending the functionality of the Interface.

/**
 * Original code
 */
interface WorkableInterface
{
    public function typeOfWork() : string;
}

class Tester implements WorkableInterface
{
    public function typeOfWork() : string
    {
        return 'tester';
    }
}

class Programmer implements WorkableInterface
{
    public function typeOfWork() : string
    {
        return 'coder';
    }
}

class ProjectManagement
{
    public function process(WorkableInterface $member)
    {
        return $member->typeOfWork();
    }
}

/**
 * Extended code
 * Now all other classes must implement this method too.
 */
 interface WorkableInterface
 {
     public function typeOfWork() : string;

     public function communicate();
 }

Liskov substitution principle

Any class that is the child of a parent should can be used in replacement of it's parent without any odd behaviour.

Take the example below, currently two classes are extending the Worker class which implements the WorkableInterface. However, we wish to extend the Programmer class further, this would normally mean we would have to add more methods to the Interface which possibly would not be used. Instead the Programmer can extend direct Interfaces so that there is no interference.

/**
 * Original code
 */
interface WorkableInterface
{
    public function typeOfWork() : string;
}

class Worker implements WorkableInterface
{
    public function typeOfWork() : string
    {
        return 'working';
    }
}

class ProjectManager extends Worker
{
    public function typeOfWork() : string
    {
        return 'management';
    }
}

class Programmer extends Worker
{
    public function typeOfWork() : string
    {
        return 'coder';
    }
}

/**
 * Refactored code
 */
 interface WorkableInterface
 {
     public function typeOfWork() : string;
 }


 interface ProgrammableInterface
 {
     public function canCode() : bool;
 }

 class Programmer implements WorkerInterface, ProgrammableInterface
 {
     public function typeOfWork() : string
     {
         return 'coder';
     }

     public function canCode() : bool
     {
         return true;
     }
 }

Interface segregation principle

Interfaces are a way in which methods a class must implement. However, such methods should not be forced upon a class and instead be more tailored to possible inheriting methods.

Generic interfaces that have multiple methods may not also be relatable to classes, instead separating interfaces are much more flexible for coding and will make refactoring easier for the future.

/**
 * Original code
 */
interface WorkableInterface
{
    public function typeOfWork() : string;

    public function canCommunicate() : bool;
}

class Programmer implements WorkableInterface
{
    public function typeOfWork() : string
    {
        return 'coder';
    }

    public function canCommunicate() : bool
    {
        return true;
    }
}

class Tester implements WorkableInterface
{
    public function typeOfWork() : string
    {
        return 'tester';
    }

    public function canCommunicate() : bool
    {
        return true;
    }
}

class ProjectManagement
{
    public function processCode(Tester $worker)
    {
      return $worker->canCommunicate();
    }
}

/**
 * Refactored code
 */
 interface CommunicateInterface
 {
     public function canCommunicate() : bool;
 }

 class Programmer implements WorkableInterface
 {
     public function typeOfWork() : string
     {
         return 'coder';
     }
 }

 class Tester implements WorkableInterface, CommunicateInterface
 {
     public function typeOfWork() : string
     {
         return 'tester';
     }

     public function canCommunicate() : bool
     {
         return true;
     }
 }

class ProjectManagement
{
    public function processCode(CommunicateInterface $worker)
    {
      return $worker->canCommunicate();
    }
}

Dependency Inversion Principle

Instead of relying on a class that requires a specific object, interfaces can be used to define how to get a method by using that as a dependency.

In the example below, we are only relying specifically on the WorkerRepositoryInterface, instead of class dependency injection.

/**
 * Original code
 */
 class WorkerController extends Controller
 {
     public function show(Worker $worker)
     {
         $workers = $worker->where('created_at', Carbon::yesterday())->get();

         return response()->json(['workers' => $workers], 200);
     }
 }

/**
 * Refactored code
 */
class WorkerController extends Controller
{
    public function show(WorkerRepositoryInterface $worker_repository)
    {
        $workers = $worker_repository->getByDate(Carbon::yesterday());

        return response()->json(['workers' => $workers], 200);
    }
}

Summary

These basic design patterns in object-oriented programming help other programmers to understand the structure behind the code and intern make code more readable, easier to extend and in turn increase workflow.