👨‍💻SOLID

The Basics

Single Responsibility

Class should have one, and only one, reason to change.

<?php

class SalesReporter
{
    private $repository;

    public function __construct(SalesRepositoryInterface $repository)
    {
        $this->repository = $repository;
    }

    public function between(
        $startDate, $endDate, 
        SalesOutputInterface $formatter
    )
    {
        $sales = $this->repository->between($startDate, $endDate);
        
        return $formatter->output($sales);
    }
}

interface SalesRepositoryInterface {
    public function between(Carbon $startDate, Carbon $endDate);
}

class CollectionSalesRepository implements SalesRepositoryInterface
{
    public function between(Carbon $startDate, Carbon $endDate)
    {
        return collect([[
            'created_at' => new Carbon('2021-03-19 14:43:40'), 
            'charge' => '2111'
        ]])
            ->whereBetween('created_at', [$startDate, $endDate])
            ->sum('charge') / 100;
    }
}

interface SalesOutputInterface {
    public function output(float $sales);
}

class SalesHtmlOutput implements SalesOutputInterface
{

    public function output(float $sales)
    {
        return "<h1>Sales: $sales</h1>";
    }
}
Open-closed principals
  • Entitles should be open for extension but closed for modification.

  • Avoid code rot.

  • Separate extensibility behavior behind an interface, and flip the dependencies.

<?php

interface Shape
{
    public function area();
}

class Circle implements Shape
{
    protected $radius;

    public function __construct($radius)
    {
        $this->radius = $radius;
    }

    public function area()
    {
        return $this->radius * $this->radius * 3.141592;
    }
}

class Square implements Shape
{
    protected $width;
    protected $height;

    public function __construct($width, $height)
    {
        $this->width = $width;
        $this->height = $height;
    }

    public function area()
    {
        return $this->width * $this->height;
    }
}

class AreaCalculate
{
    public function calculate($shapes)
    {
        foreach ($shapes as $shape) {
            $area[] = $shape->area();
        }

        return array_sum($area);
    }
}
Liskov Substitution

Derivable classes must be substitutable for their base classes.

<?php

interface LessonRepositoryInterface
{
    public function getAll(): array;
}

class FileLessonRepository implements LessonRepositoryInterface
{
    public function getAll(): array
    {
        return [];
    }
}

class DbLessonRepository implements LessonRepositoryInterface
{
    public function getAll(): array
    {
        return DB::table('lessons')->get()->toArray();
    }
}
Interface Segregation

A client should not be force to implement an interface that doesn't use.

<?php

interface WorkableInterface
{
    public function work();
}

interface SleepableInterface
{
    public function sleep();
}

interface ManageableInterface
{
    public function beManaged();
}

class HumanWorker implements WorkableInterface, SleepableInterface, ManageableInterface
{
    public function work()
    {

    }

    public function sleep()
    {

    }

    public function beManaged()
    {
        $this->work();
        $this->sleep();
    }
}

class AndroidWorker implements WorkableInterface, ManageableInterface
{
    public function work()
    {

    }

    public function beManaged()
    {
        $this->work();
    }
}

class Captain
{
    public function manage(ManageableInterface $worker)
    {
        $worker->beManaged();
    }
}
Dependency Inversion
  • Depends on abstraction, not concretion.

  • Dependency inversion does not equal dependency injection.

That example of bad architecture, because high level module (PasswordReminder) depends on low level module (MysqlConnection)

<?php

// Bad example
class PasswordReminder
{
    protected MysqlConnection $dbConnection;
    
    public function __construct(MysqlConnection $dbConnection)
    {
        $this->dbConnection = $dbConnection;
    }
}

// Good example
interface ConnectionInterface
{
    public function connect();
}

class DbConnection implements ConnectionInterface
{
    public function connect()
    {
        
    }
}

class PasswordReminder
{
    protected ConnectionInterface $dbConnection;
    
    public function __construct(ConnectionInterface $dbConnection)
    {
        $this->dbConnection = $dbConnection;
    }
}

Digging Deeper

Implementing the SOLID in Laravel. A Comprehensive Example

Examples get from article

Single Responsibility Principle (SRP)

This principle states that a class should have only one reason to change. In other words, a class should have only one responsibility. Example: Consider a class called Order. Instead of having all the functionality related to an order in one class, we can break it down into smaller classes like OrderCalculator, OrderRepository, and OrderMailer.

class OrderCalculator
{
    public function calculateTotal(Order $order): float
    {
        // Calculate total price of order
    }
}

class OrderMailer
{
    public function sendEmail(Order $order, User $user)
    {
        // Send email to customer with order details
    }
}
Open-Closed Principle (OCP)

This principle states that a class should be open for extension but closed for modification. In other words, we should be able to add new functionality without modifying existing code. Example: Consider a class called PaymentGateway. Instead of modifying this class every time we add a new payment method, we can create a new class for each payment method that extends the PaymentGateway class.

interface PaymentGateway
{
    public function pay(Order $order): bool;
}

class PayPalGateway implements PaymentGateway
{
    public function pay(Order $order): bool
    {
        // Process payment using PayPal API
    }
}

class StripeGateway implements PaymentGateway
{
    public function pay(Order $order): bool
    {
        // Process payment using Stripe API
    }
}

class OrderProcessor
{
    private PaymentGateway $gateway;

    public function __construct(PaymentGateway $gateway)
    {
        $this->gateway = $gateway;
    }

    public function process(Order $order): void
    {
        // Process order using PaymentGateway
    }
}

// In application service provider
$this->app->bind(PaymentGateway::class, StripeGateway::class);
Liskov Substitution Principle (LSP)

This principle states that objects of a superclass should be able to be replaced with objects of a subclass without affecting the correctness of the program. Example: Consider a class called Shape with subclasses Circle, Rectangle, and Square. If we have a function that takes an object of type Shape, we should be able to pass in objects of type Circle, Rectangle, or Square without affecting the behavior of the function.

interface OrderRepository
{
    public function save(Order $order): void;
}

class DatabaseOrderRepository implements OrderRepository
{
    public function save(Order $order): void
    {
        // Save order to database
    }
}

class InMemoryOrderRepository implements OrderRepository
{
    public function save(Order $order): void
    {
        // Save order to in-memory cache
    }
}

class OrderService
{
    private OrderRepository $repository;

    public function __construct(OrderRepository $repository)
    {
        $this->repository = $repository;
    }

    public function placeOrder(Order $order): void
    {
        // Place order and save to repository
    }
}

// In application service provider
$this->app->bind(OrderRepository::class, DatabaseOrderRepository::class);
Interface Segregation Principle (ISP)

This principle states that clients should not be forced to depend on methods they do not use. Example: Consider an interface called PaymentMethod with methods like pay, refund, and getTransactions. Instead of having all these methods in one interface, we can create separate interfaces for each method and have classes implement only the interfaces they need.

interface OrderTotalCalculator
{
    public function calculateTotal(Order $order): float;
}

interface OrderEmailSender
{
    public function sendEmail(Order $order, User $user);
}

class OrderProcessor
{
    private OrderTotalCalculator $calculator;
    private OrderEmailSender $mailer;

    public function __construct(
        OrderTotalCalculator $calculator, 
        OrderEmailSender $mailer
    )
    {
        $this->calculator = $calculator;
        $this->mailer = $mailer;
    }

    public function process(Order $order, User $user): void
    {
        $total = $this->calculator->calculateTotal($order);
        $this->mailer->sendEmail($order, $user);
        // Process order using total and mailer
    }
}
Dependency Inversion Principle (DIP)

This principle states that high-level modules should not depend on low-level modules. Both should depend on abstractions. Example: Consider a class called Order that depends on a class called OrderRepository. Instead of directly instantiating OrderRepository in Order, we can use dependency injection to inject an instance of OrderRepository into Order.

interface PaymentGateway
{
    public function pay(Order $order): bool;
}

interface OrderRepository
{
    public function save(Order $order): void;
}

interface OrderTotalCalculator
{
    public function calculateTotal(Order $order): float;
}

interface OrderEmailSender
{
    public function sendEmail(Order $order, User $user);
}

class OrderProcessor
{
    private PaymentGateway $gateway;
    private OrderRepository $repository;
    private OrderTotalCalculator $calculator;
    private OrderEmailSender $mailer;

    public function __construct(
        PaymentGateway $gateway,
        OrderRepository $repository,
        OrderTotalCalculator $calculator,
        OrderEmailSender $mailer
    ) {
        $this->gateway = $gateway;
        $this->repository = $repository;
        $this->calculator = $calculator;
        $this->mailer = $mailer;
    }

    public function process(Order $order, User $user): void
    {
        $total = $this->calculator->calculateTotal($order);
        $this->mailer->sendEmail($order, $user);
        $this->gateway->pay($order);
        $this->repository->save($order);
        // Process order using gateway, repository, calculator, and mailer
    }
}

Last updated