Education Logo Images
Blog
Education Logo Images

Batchmate is an online coding platform offering free resources to help learners at all levels build and advance their coding skills.

  • batchmate.info@gmail.com
  • Blog
Find With Us
  • Home
  • Blogs
  • Details

SOLID Principles Using PHP

Master SOLID principles to build resilient, scalable software with maintainable, extensible, and decoupled code that is easier to test and evolve.

  • Last updated: 03 Jun, 2025

The SOLID principles are a set of five design principles that guide object-oriented programming and design. They help developers create systems that are easy to maintain, scale, and extend. These principles are particularly useful for writing clean and maintainable code in PHP. It was Conceptualized by Robert C. Martin, also known as Uncle Bob. Five Design principles of SOLID principles,


Single Responsibility Principle (SRP):

A class should have only one reason to change, meaning it should perform a single responsibility or task.

Example, Without SRP,

class UserManager {
    public function addUser($user) {
        // Add user logic
        $this->validateUser($user);
        $this->saveUserToDatabase($user);
        $this->sendWelcomeEmail($user);
    }

    public function validateUser($user) {
        // Validation logic
        echo 'Validating user: ' . print_r($user, true) . "\n";
    }

    public function saveUserToDatabase($user) {
        // Database logic
        echo 'Saving user to the database: ' . print_r($user, true) . "\n";
    }

    public function sendWelcomeEmail($user) {
        // Email logic
        echo 'Sending welcome email to: ' . $user['email'] . "\n";
    }
}

// Example usage
$userManager = new UserManager();
$user = ['name' => 'John Doe', 'email' => 'john.doe@example.com'];
$userManager->addUser($user);

With SRP,

class UserManager {
    private $userRepository;
    private $emailService;

    public function __construct(UserRepository $userRepository, EmailService $emailService) {
        $this->userRepository = $userRepository;
        $this->emailService = $emailService;
    }

    public function addUser($user) {
        $this->userRepository->save($user);
        $this->emailService->sendWelcomeEmail($user);
    }
}

class UserRepository {
    public function save($user) {
        // Save user to database logic
        echo 'Saving user to the database: ' . print_r($user, true) . "\n";
    }
}

class EmailService {
    public function sendWelcomeEmail($user) {
        // Email sending logic
        echo 'Sending welcome email to: ' . $user['email'] . "\n";
    }
}

// Example usage
$userRepository = new UserRepository();
$emailService = new EmailService();
$userManager = new UserManager($userRepository, $emailService);

$user = ['name' => 'John Doe', 'email' => 'john.doe@example.com'];
$userManager->addUser($user);

Here, the UserManager, UserValidator, and EmailService each have a single responsibility.

Open/Closed Principle (OCP):

Software entities (classes, modules, functions) should be open for extension but closed for modification.

Without OCP,

class PaymentService {
    public function processPayment($paymentType) {
        if ($paymentType === "CREDIT_CARD") {
            // Credit card processing logic
            echo "Processing credit card payment...\n";
        } elseif ($paymentType === "PAYPAL") {
            // PayPal processing logic
            echo "Processing PayPal payment...\n";
        }
    }
}

// Example usage
$paymentService = new PaymentService();
$paymentService->processPayment("CREDIT_CARD");
$paymentService->processPayment("PAYPAL");
With OCP,
// Define a PaymentProcessor interface
interface PaymentProcessor {
    public function processPayment();
}

// CreditCardProcessor class
class CreditCardProcessor implements PaymentProcessor {
    public function processPayment() {
        // Credit card processing logic
        echo "Processing credit card payment...\n";
    }
}

// PayPalProcessor class
class PayPalProcessor implements PaymentProcessor {
    public function processPayment() {
        // PayPal processing logic
        echo "Processing PayPal payment...\n";
    }
}

// PaymentService class
class PaymentService {
    private $processors;

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

    public function processPayments() {
        foreach ($this->processors as $processor) {
            $processor->processPayment();
        }
    }
}

// Example usage
$processors = [new CreditCardProcessor(), new PayPalProcessor()];
$paymentService = new PaymentService($processors);

$paymentService->processPayments();

New payment types can be added without modifying PaymentService.

Liskov Substitution Principle (LSP):

Subtypes must be substitutable for their base types without altering the correctness of the program.

Without LSP,

// Base Rectangle class
class Rectangle {
    private $width;
    private $height;

    public function setWidth(int $width) {
        $this->width = $width;
    }

    public function setHeight(int $height) {
        $this->height = $height;
    }

    public function getArea(): int {
        return $this->width * $this->height;
    }
}

// Square class that violates LSP
class Square extends Rectangle {
    public function setWidth(int $width) {
        parent::setWidth($width);
        parent::setHeight($width);
    }

    public function setHeight(int $height) {
        parent::setWidth($height);
        parent::setHeight($height);
    }
}

// Example usage
$rectangle = new Rectangle();
$rectangle->setWidth(4);
$rectangle->setHeight(5);
echo "Rectangle area: " . $rectangle->getArea() . "\n"; // 20

$square = new Square();
$square->setWidth(4);
echo "Square area: " . $square->getArea() . "\n"; // 16 (Expected behavior for a square)

$substitutedRectangle = new Square();
$substitutedRectangle->setWidth(4);
$substitutedRectangle->setHeight(5);
echo "Substituted rectangle area: " . $substitutedRectangle->getArea() . "\n"; // Incorrect behavior, 25

Problem:The Square class overrides the setWidth and setHeight methods to enforce its specific behavior (equal width and height). However, when substituted in place of the Rectangle class, it violates the principle of substitutability since the behavior is no longer consistent with what a Rectangle should do.


With LSP,

// Define the Shape interface
interface Shape {
    public function getArea(): int;
}

// Rectangle class implementing Shape
class Rectangle implements Shape {
    private $width;
    private $height;

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

    public function getArea(): int {
        return $this->width * $this->height;
    }
}

// Square class implementing Shape
class Square implements Shape {
    private $side;

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

    public function getArea(): int {
        return $this->side * $this->side;
    }
}

// Example usage
$rectangle = new Rectangle(4, 5);
echo "Rectangle area: " . $rectangle->getArea() . "\n"; // 20

$square = new Square(4);
echo "Square area: " . $square->getArea() . "\n"; // 16

// Both classes follow LSP and can be used interchangeably as Shape
function printArea(Shape $shape) {
    echo "Shape area: " . $shape->getArea() . "\n";
}

printArea($rectangle); // 20
printArea($square);    // 16
  • Both Rectangle and Square implement the Shape interface.

  • The getArea method is implemented independently in both classes without altering the expected behavior of either.

  • The Shape interface ensures that both types can be used interchangeably, adhering to the LSP.

Here, Square and Rectangle both implement Shape without breaking substitutability.

Interface Segregation Principle (ISP):

Clients should not be forced to implement interfaces they do not use.

Without ISP,

class Vehicle {
  startEngine() {
    throw new Error("startEngine method must be implemented");
  }

  fly() {
    throw new Error("fly method must be implemented");
  }
}

class Car extends Vehicle {
  startEngine() {
    // Start engine logic
    console.log("Car engine started.");
  }

  fly() {
    throw new Error("Cars can't fly"); // Violates ISP
  }
}

// Example usage
const car = new Car();
car.startEngine(); // Works fine
car.fly(); // Throws an error: "Cars can't fly"

Problem:The Car class is forced to implement the fly method from the Vehicle interface, even though it doesn't make sense for a car to fly. This violates the Interface Segregation Principle.


With ISP,

// Define the EngineVehicle interface (base class)
class EngineVehicle {
  startEngine() {
    throw new Error("startEngine method must be implemented");
  }
}

// Define the FlyingVehicle interface (base class)
class FlyingVehicle {
  fly() {
    throw new Error("fly method must be implemented");
  }
}

// Car class implements only EngineVehicle
class Car extends EngineVehicle {
  startEngine() {
    // Start engine logic
    console.log("Car engine started.");
  }
}

// Airplane class implements both EngineVehicle and FlyingVehicle
class Airplane extends EngineVehicle {
  startEngine() {
    // Start engine logic
    console.log("Airplane engine started.");
  }

  fly() {
    // Fly logic
    console.log("Airplane is flying.");
  }
}

// Example usage
const car = new Car();
car.startEngine(); // "Car engine started."

const airplane = new Airplane();
airplane.startEngine(); // "Airplane engine started."
airplane.fly(); // "Airplane is flying."
  1. Separate Interfaces:
    • EngineVehicle defines the startEngine method.
    • FlyingVehicle defines the fly method.
  2. Class Implementation:
    • Car implements only EngineVehicle since it doesn't need flying functionality.
    • Airplane implements both EngineVehicle and FlyingVehicle since it needs both functionalities.
  3. Adheres to ISP: Classes implement only the methods they need, avoiding unnecessary implementation of unused methods.

Dependency Inversion Principle (DIP):

High-level modules should not depend on low-level modules; both should depend on abstractions.

Without DIP,

class UserRepository {
    public function save($user) {
        // Save logic
        echo "Saving user: " . $user['name'] . "\n";
    }
}

class UserService {
    private $userRepository;

    public function __construct() {
        $this->userRepository = new UserRepository(); // High-level module directly depends on a low-level module
    }

    public function saveUser($user) {
        $this->userRepository->save($user);
    }
}

// Example usage
$userService = new UserService();
$userService->saveUser(['name' => 'John Doe']);

Problem: In this example, the UserService class (high-level module) directly depends on the UserRepository class (low-level module). This tight coupling between the high-level and low-level modules violates the Dependency Inversion Principle (DIP).


With DIP,

// Define the UserRepository interface (abstraction)
interface UserRepository {
    public function save($user);
}

// Implement the JpaUserRepository class (low-level module)
class JpaUserRepository implements UserRepository {
    public function save($user) {
        // Save logic
        echo "Saving user: " . $user['name'] . "\n";
    }
}

// UserService depends on the abstraction (UserRepository), not the concrete implementation
class UserService {
    private $userRepository;

    public function __construct(UserRepository $userRepository) {
        $this->userRepository = $userRepository; // Dependency injected
    }

    public function saveUser($user) {
        $this->userRepository->save($user);
    }
}

// Example usage
$userRepository = new JpaUserRepository(); // Low-level module
$userService = new UserService($userRepository); // High-level module depends on abstraction
$userService->saveUser(['name' => 'John Doe']); // Output: Saving user: John Doe

By depending on UserRepository, the UserService can work with any repository implementation.

  1. Abstraction: The UserRepository interface defines the save method. This is the abstraction that allows UserService to interact with any repository without needing to know the specifics of how the repository works.

  2. Low-Level Module: The JpaUserRepository class implements the UserRepository interface and provides the concrete implementation of the save method.

  3. High-Level Module: The UserService class depends on the UserRepository interface rather than a concrete class, making it flexible and decoupled from the implementation details.

  4. Dependency Injection: The UserRepository dependency is passed into the UserService class through its constructor, allowing you to inject different implementations of UserRepository without modifying UserService.


By following these SOLID principles, you ensure your Java code is modular, reusable, and easier to maintain.

SOLID Principles
Related Post

Similar Post

Card image
SOLID Principles Using Kotlin
  • 03 Jun, 2025
  • 3 min read

Master the SOLID principles to design robust, maintainable, and scalable software. Learn to create clean code by ensuring single responsibility, extensibility, substitutability, focused interfaces, and dependency management.

Learn More
Card image
SOLID Principles Using PHP
  • 04 Jun, 2025
  • 3 min read

Master SOLID principles to build resilient, scalable software with maintainable, extensible, and decoupled code that is easier to test and evolve.

Learn More

Edu-cause

We’re here to support you in achieving your dream career!

Useful Links
  • Home
  • Terms and Conditions
  • Privacy Policy
Our Company
  • Faq
  • Blog
  • Contact Us
Get Contact
  • E-mail: batchmate.info@gmail.com

Copyright © 2024 Batchmate. All Rights Reserved