Already I wrote a blog on SRP and OCP in solid principles. I am continuing further with the same example… and in this blog, we will be discussing the Liskov Substitution Principle (LSP).
Definition: Objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program.
LSP ensures that derived classes can be used interchangeably with base class or interface without breaking the system.
This is done using an interface that will be implemented across all repositories. So as a first step, we define a common interface.. and all its methods will have to be defined in all repositories.
Step 1: Define the Interface
<?php
namespace App\Http\Repositories\Contracts;
interface UserRepositoryInterface{
public function getAllUsers();
public function getUserById($id);
public function createUser(array $data);
public function updateUser($id, array $data);
public function softDeleteUser($id);
}
This interface declares common methods that all repositories must implement.
By adhering to this contract, we ensure the Liskov Substitution Principle because any repository implementing this interface can be substituted for another.
Step 2: Update UserRepository
<?php
namespace App\Http\Repositories;
use App\Http\Repositories\Contracts\UserRepositoryInterface;
use DB;
class UserRepository implements UserRepositoryInterface{
protected $table = 'users';
public function getAllUsers(){
return DB::table($this->table)->get();
}
public function getUserById($id){
return DB::table($this->table)->where('id', $id)->first();
}
public function createUser(array $data){
return DB::table($this->table)->insert($data);
}
public function updateUser($id, array $data){
return DB::table($this->table)->where('id', $id)->update($data);
}
public function softDeleteUser($id){
return DB::table($this->table)->where('id', $id)->update(['deleted_at' => now()]);
}
}
UserRepository is the direct implementation of the interface.
Step 3: Updating specialized repository
<?php
namespace App\Http\Repositories;
use DB;
class RoleBasedUserRepository extends UserRepository{
public function getUsersByRole($role){
return DB::table($this->table)->where('role', $role)->first();
}
}
We need not to import UserRepository as RoleBasedUserRepository.php and UserRepository.php file has same namespace while in case of interface we need to import it as it has different namespace.
RoleBasedUserRepository is extending UserRepository that implements the interface therefore, we need not to implement it here again. That way, UserRepository and RoleBasedUserRepository, both implement the same interface.
Step 4: Update the Service Layer
Use the interface in the service. Service should remain independent of any specific repository to maintain flexibility… so it can have methods in the interface. That means it will not have specialized methods or extended functionality methods that are in any repository
<?php
namespace App\Http\Services;
use App\Http\Repositories\RoleBasedUserRepository;
use App\Http\Repositories\Contracts\UserRepositoryInterface;
class UserService {
protected $userRepository;
public function __construct(UserRepositoryInterface $objUserRepoInterface ){
$this->userRepository = $objUserRepoInterface;
}
public function getAllUsers(){
return $this->userRepository->getAllUsers();
}
public function getUserById($id){
return $this->userRepository->find($id);
}
public function createUser(array $data){
return $this->userRepository->create($data);
}
public function updateUser($id, array $data){
$user = $this->userRepository->find($id);
return $this->userRepository->update($user, $data);
}
public function deleteUser($id){
$user = $this->userRepository->find($id);
return $this->userRepository->delete($user);
}
}
Step 5: Bind in Service provider
Bind the appropriate repository at runtime in service provider to instantiate
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Http\Repositories\Contracts\UserRepositoryInterface;
use App\Http\Repositories\UserRepository;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void{
$this->app->bind(UserRepositoryInterface::class, UserRepository::class);
}
}
Step 6: Update the Controller
Inject the service into the controller to keep it clean. For specialized methods like getRoleBasedUsers(), directly interact with specialized repositories when such behavior is needed.
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Requests\UserRequest;
use App\Http\Services\UserService;
use App\Http\Repositories\RoleBasedUserRepository;
class UserController extends Controller{
protected $service, $roleBasedUserService;
public function __construct(UserService $objUS){
$this->service = $objUS;
}
public function index(){
return $this->service->getAllUsers();
}
public function store(UserRequest $request){
return $this->service->createUser($request->validated());
}
public function show($id){
return $this->service->getUserById($id);
}
public function update(UserRequest $request, $id){
return $this->service->updateUser($id, $request->validated());
}
public function destroy($id){
return $this->service->deleteUser($id);
}
public function getUsersByRole($role){
// Directly use the RoleBasedUserRepository for specialized behavior
$roleBasedRepository = app()->make(\App\Http\Repositories\RoleBasedUserRepository::class);
return $roleBasedRepository->getUsersByRole($role);
}
}
Overall working:
As per LSP, derived classes can be used interchangeably with base classes or interfaces without breaking the system. So we started with an interface that will be implemented in classes wherever needed.
In step 2, we implement this interface in Class UserRepository
And in Step 3,
Class RoleBasedUserRepository extends UserRepository that way it has access to all properties and methods of UserRepository as well as Interface
That means the instance of RoleBasedUserRepository can be used interchangeably with UserRepository instance
Step 4 is UserService which has interface injection. This interface injection helps to call all repository methods.
Step 5 is the binding of Interface to the repository. On run time Laravel resolves the binding. Binding means whenever there is interface, Laravel will provide the instance of UserRepository. This binding ensures that the UserRepository implementation is injected whenever the UserRepositoryInterface is needed.
The Interface keeps UserService decoupled and thus flexible and binding helps to get required repository at run time.
The Liskov Substitution Principle states that objects of a superclass or interface should be replaceable with objects of a subclass or another implementation without affecting the program’s correctness.
By injecting UserRepositoryInterface, we ensure the service works with any implementation of the interface.
Step 6, is the controller where we directly call specialized repository methods. Special method getUsersByRole is not from interface but from another repository.