Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for SOLID Principle with Laravel
Nad Lambino
Nad Lambino

Posted on • Edited on

     

SOLID Principle with Laravel

SOLID Principle

SOLID Principle with Laravel

What is SOLID principle?

The SOLID principles are a set of five design principles that aim to guide developers in creating software systems that are modular, maintainable, and extensible. These principles provide guidelines for writing clean, robust, and flexible code. Each principle focuses on a specific aspect of software design and encourages the separation of concerns, flexibility, and adherence to good coding practices. By following the SOLID principles, developers can build software that is easier to understand, test, and modify, leading to improved quality and long-term sustainability.

Benefits of SOLID principle

  • Code maintainability: SOLID principles promote clean and organized code, making it easier to understand, modify, and maintain over time.
  • Code reusability: By adhering to SOLID principles, code becomes modular and loosely coupled, allowing for easier reuse in different parts of the application or in future projects.
  • Testability: SOLID principles encourage code that is easy to test in isolation, leading to more reliable and effective unit tests.
  • Flexibility and adaptability: Following SOLID principles results in code that is flexible and can be easily extended or modified to accommodate changing requirements or new features.
  • Collaboration: SOLID principles make code easier to understand and work with, facilitating better collaboration among team members.
  • Scalability: SOLID principles help in building scalable systems by enabling the creation of loosely coupled, modular components that can be easily scaled up or down as needed.
  • Reduced time and cost: Following SOLID principles from the start of a project can save time and reduce costs by minimizing bugs, refactoring needs, and making changes later in the development cycle.

By addressing these areas, the SOLID principles contribute to overall software quality, maintainability, and developer productivity.


Single Responsibility Principle

TheSingle Responsibility Principle (SRP) states that a class should have only one reason to change. It means that a class should have only one responsibility or job.

In the context of a Laravel application, let's consider a scenario where we have aUserController class that handles user-related operations like creating a new user, updating user information, and sending welcome emails. However, this violates the SRP because the class has multiple responsibilities.

Here's an example that violates the SRP

// UserController.phpclass UserController{    public function create(Request $request)    {        // Validation and user creation logic        $user = User::create($request->all());        $this->sendWelcomeEmail($user); // Move this responsibility out of UserController    }    private function sendWelcomeEmail(User $user)    {        // Code to send the welcome email    }}
Enter fullscreen modeExit fullscreen mode

Here's an example that follows the SRP

// UserController.phpclass UserController{    public function create(Request $request, EmailService $emailService)    {        // Validation and user creation logic        $user = User::create($request->all());        // Delegate the responsibility to the EmailService class        $emailService->sendWelcomeEmail($user);    }}
Enter fullscreen modeExit fullscreen mode
// EmailService.phpclass EmailService{    public function sendWelcomeEmail(User $user)    {        // Code to send the welcome email    }    public function sendEmailWithAttachment()    {        // Code to send email with attachment    }}
Enter fullscreen modeExit fullscreen mode

In the refactored code, we extract the responsibility of sending a welcome email into a separateEmailService class. This separates the concerns, allowing theUserController to focus solely on user-related operations, while theEmailService class handles email-related tasks. This makes its functions to be reusable on other controllers or service that needs mail related tasks without repeating our code. This adheres to the SRP, as each class now has a single responsibility, making the code more modular, maintainable, and easier to extend or change in the future.


Open-Closed Principle

TheOpen-Closed Principle (OCP) states that software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. In simple terms, it means that you should be able to add new functionality to a system without modifying its existing code.

Let's consider an example in a Laravel application where we have aPaymentController that handles different payment methods: PayPal and Stripe. Initially, the controller has a switch statement to determine the payment method and perform the corresponding actions.

// PaymentController.phpclass PaymentController{    public function processPayment(Request $request)    {        $paymentMethod = $request->input('payment_method');        switch ($paymentMethod) {            case 'paypal':                $this->processPayPalPayment($request);                break;            case 'stripe':                $this->processStripePayment($request);                break;            default:                // Handle unsupported payment method                break;        }    }    private function processPayPalPayment(Request $request)    {        // Code for processing PayPal payment    }    private function processStripePayment(Request $request)    {        // Code for processing Stripe payment    }}
Enter fullscreen modeExit fullscreen mode

In the above code, adding a new payment method would require modifying thePaymentController by adding another case to the switch statement. This violates the OCP because we are modifying the existing code instead of extending it.

To adhere to the OCP, we can use a strategy pattern to decouple the payment processing logic from the controller and make it open for extension. Here's an updated version:

// PaymentController.phpclass PaymentController{    private $paymentProcessor;    public function __construct(PaymentProcessorInterface $paymentProcessor)    {        $this->paymentProcessor = $paymentProcessor;    }    public function processPayment(Request $request)    {        $this->paymentProcessor->processPayment($request);    }}
Enter fullscreen modeExit fullscreen mode
// PaymentProcessorInterface.phpinterface PaymentProcessorInterface{    public function processPayment(Request $request);}
Enter fullscreen modeExit fullscreen mode
// PayPalPaymentProcessor.phpclass PayPalPaymentProcessor implements PaymentProcessorInterface{    public function processPayment(Request $request)    {        // Code for processing PayPal payment    }}
Enter fullscreen modeExit fullscreen mode
// StripePaymentProcessor.phpclass StripePaymentProcessor implements PaymentProcessorInterface{    public function processPayment(Request $request)    {        // Code for processing Stripe payment    }}
Enter fullscreen modeExit fullscreen mode

Now, to dynamically select the payment processor based on user input, you can leverage Laravel's container and configuration capabilities. Here's an example:

// config/payments.phpreturn [    'default' => 'stripe',    'processors' => [        'paypal' => PayPalPaymentProcessor::class,        'stripe' => StripePaymentProcessor::class,    ],];
Enter fullscreen modeExit fullscreen mode
// PaymentServiceProvider.phpuse Illuminate\Support\Facades\App;use Illuminate\Support\ServiceProvider;class PaymentServiceProvider extends ServiceProvider{    public function register()    {        $this->app->bind(PaymentProcessorInterface::class, function ($app) {            $config = $app['config']->get('payments');            $defaultProcessor = $config['default'];            $processors = $config['processors'];            $selectedProcessor = $request->input('payment_method', $defaultProcessor);            $processorClass = $processors[$selectedProcessor];            return $app->make($processorClass);        });    }}
Enter fullscreen modeExit fullscreen mode

Liskov Substitution Principle

TheLiskov Substitution Principle (LSP) states that objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program. In simpler terms, it means that subclasses should be able to be used interchangeably with the base class without causing any unexpected behavior.

Let's consider a real-world example where we have a base class called Vehicle and two subclasses called Car and Bicycle. Each of them has a method calledstartEngine(), which represents starting the engine of the vehicle.

Here's an example that violates the Liskov Substitution Principle:

class Vehicle {    public function startEngine() {        // Default implementation for starting the engine        echo "Engine started!";    }}
Enter fullscreen modeExit fullscreen mode
class Car extends Vehicle {    public function startEngine() {        // Implementation specific to starting a car's engine        echo "Car engine started!";    }}
Enter fullscreen modeExit fullscreen mode
class Bicycle extends Vehicle {    public function startEngine() {        // Bicycles don't have engines, so this violates LSP        throw new Exception("Bicycles don't have engines!");    }}
Enter fullscreen modeExit fullscreen mode

In the above code, the Bicycle class violates the Liskov Substitution Principle because it throws an exception when trying to start the engine. This behavior is unexpected and breaks the principle.

Here's an example that follows the Liskov Substitution Principle:

class Vehicle {    // Common implementation for all vehicles    public function startEngine() {        // Default implementation for starting the engine        echo "Engine started!";    }}
Enter fullscreen modeExit fullscreen mode
class Car extends Vehicle {    public function startEngine() {        // Implementation specific to starting a car's engine        echo "Car engine started!";    }}
Enter fullscreen modeExit fullscreen mode
class Bicycle extends Vehicle {    // Bicycles don't have engines, so we don't override the startEngine() method}
Enter fullscreen modeExit fullscreen mode

In the above code, the Bicycle class follows the Liskov Substitution Principle by not overriding thestartEngine() method. Since bicycles don't have engines, the default implementation from the base class is used, which is acceptable and doesn't introduce unexpected behavior.

By following LSP, you ensure that your code is more maintainable, extensible, and less prone to bugs, as you can safely use objects of subclasses wherever objects of the base class are expected.


Interface Segregation Principle

TheInterface Segregation Principle (ISP) states that clients should not be forced to depend on interfaces they do not use. In simpler terms, it means that a class should not be forced to implement methods that it doesn't need.

Let's consider a real-world example of an online store application built with Laravel.

Violating the Interface Segregation Principle:

interface PaymentGatewayInterface {    public function processPayment($amount);    public function refundPayment($transactionId);    public function voidPayment($transactionId);}
Enter fullscreen modeExit fullscreen mode
class PaymentGateway implements PaymentGatewayInterface {    public function processPayment($amount) {        // Process payment logic    }    public function refundPayment($transactionId) {        // Refund payment logic    }    public function voidPayment($transactionId) {        // Void payment logic    }}
Enter fullscreen modeExit fullscreen mode
class EcommerceService {    private $paymentGateway;    public function __construct(PaymentGatewayInterface $paymentGateway) {        $this->paymentGateway = $paymentGateway;    }    public function processOrder($order) {        // Process order logic        $this->paymentGateway->processPayment($order->totalAmount);    }    public function refundOrder($order) {        // Refund order logic        $this->paymentGateway->refundPayment($order->transactionId);    }    public function voidOrder($order) {        // Void order logic        $this->paymentGateway->voidPayment($order->transactionId);    }}
Enter fullscreen modeExit fullscreen mode

In this example, thePaymentGatewayInterface defines three methods:processPayment(),refundPayment(), andvoidPayment(). However, in theEcommerceService class, we only need to use theprocessPayment() method to handle payment-related operations. TherefundPayment() andvoidPayment() methods are not relevant to theEcommerceService, but we're still forced to depend on them because the interface enforces their implementation.

Here's a modified version that follows the ISP:

interface PaymentProcessorInterface {    public function processPayment($amount);}
Enter fullscreen modeExit fullscreen mode
interface RefundableInterface {    public function refundPayment($transactionId);}
Enter fullscreen modeExit fullscreen mode
interface VoidableInterface {    public function voidPayment($transactionId);}
Enter fullscreen modeExit fullscreen mode
class PaymentGateway implements PaymentProcessorInterface, RefundableInterface, VoidableInterface {    public function processPayment($amount) {        // Process payment logic    }    public function refundPayment($transactionId) {        // Refund payment logic    }    public function voidPayment($transactionId) {        // Void payment logic    }}
Enter fullscreen modeExit fullscreen mode
class EcommerceService {    private $paymentProcessor;    public function __construct(PaymentProcessorInterface $paymentProcessor) {        $this->paymentProcessor = $paymentProcessor;    }    public function processOrder($order) {        // Process order logic        $this->paymentProcessor->processPayment($order->totalAmount);    }}
Enter fullscreen modeExit fullscreen mode

In this updated example, thePaymentProcessorInterface defines only theprocessPayment() method, which is the only method needed by theEcommerceService. TheRefundableInterface andVoidableInterface are created for other classes that require those specific functionalities. By separating the interfaces, we adhere to the ISP by allowing clients to depend only on the interfaces they actually need.

This adherence to the ISP improves the codebase's maintainability, reduces unnecessary dependencies, and makes it easier for entry-level developers to understand and work with the code.


Dependency Inversion Principle

TheDependency Inversion Principle (DIP) states that high-level modules should not depend on low-level modules, but both should depend on abstractions. In other words, instead of depending on specific implementations, modules should rely on interfaces or abstract classes.

Code Sample Violating DIP in Laravel using the Storage Facade:

class UserController extends Controller{    public function store(Request $request)    {        $avatar = $request->file('avatar');        // Violation: The UserController depends directly on the Storage facade.        // This makes it tightly coupled to the Laravel's file storage implementation.        $path = Storage::disk('local')->put('avatars', $avatar);        // ...    }}
Enter fullscreen modeExit fullscreen mode

TheUserController class relies on the Storage facade and calls its disk and put methods directly. By depending on the Storage facade directly, the UserController is tightly coupled to the specific file storage system implemented by Laravel, making it harder to switch to a different storage mechanism without modifying theUserController code.

Code Sample FollowingDIP in Laravel using the Storage Service:

interface FileStorage{    public function storeFile($directory, $file);}
Enter fullscreen modeExit fullscreen mode
class LocalFileStorage implements FileStorage{    public function storeFile($directory, $file)    {        return Storage::disk('local')->put($directory, $file);    }}
Enter fullscreen modeExit fullscreen mode
class S3FileStorage implements FileStorage{    public function storeFile($directory, $file)    {        return Storage::disk('s3')->put($directory, $file);    }}
Enter fullscreen modeExit fullscreen mode
class UserController extends Controller{    private $fileStorage;    public function __construct(FileStorage $fileStorage)    {        $this->fileStorage = $fileStorage;    }    public function store(Request $request)    {        $avatar = $request->file('avatar');        // The UserController depends on the FileStorage abstraction, which can be        // implemented using different storage systems.        $path = $this->fileStorage->storeFile('avatars', $avatar);        // ...    }}
Enter fullscreen modeExit fullscreen mode

The UserController class now depends on the FileStorage interface instead of directly relying on the Storage facade or a specific implementation. This interface serves as an abstraction that defines the contract for file storage operations.

TheS3FileStorage class implements theFileStorage interface and provides the concrete implementation for storing files in AWS S3. By injecting theFileStorage interface into theUserController constructor, the controller is decoupled from the specific storage mechanism and depends only on the abstraction.
This adherence to the Dependency Inversion Principle allows for easier interchangeability of storage implementations. You can easily introduce new implementations of the FileStorage interface (e.g., a LocalFileStorage class) without modifying the UserController code. The choice of storage mechanism can be determined at runtime or through configuration (.env file), providing flexibility and maintainability.


Conclusion:

The SOLID principles are guidelines for writing clean and maintainable code. While they offer numerous benefits, they should not be seen as rigid rules that must always be followed without exception. It's crucial to strike a balance between applying the SOLID principles and considering other factors such as project complexity.

Thank you for reading.

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

a passionate web developer
  • Location
    Philippines
  • Joined

More fromNad Lambino

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp