Mastering SOLID Principles in PHP: Real-World Examples

Mastering SOLID Principles in PHP: Real-World Examples

·

10 min read

The SOLID principles are a set of best practices in object-oriented programming that can help developers to create more maintainable and scalable code. In PHP, these principles can be applied in the following ways:

Single Responsibility Principle

The Single Responsibility Principle states that a class should have only one reason to change. In PHP, this can be achieved by ensuring that each class has a clear and well-defined purpose, and that it only contains code that is directly related to that purpose. For example, a User class might contain methods for storing and retrieving user information, but it should not include code for sending email notifications or handling payment transactions.

| Here is an example of how to apply the Single Responsibility Principle in PHP:

<?php

// A class that represents a user
class User
{
  private $name;
  private $email;

  public function __construct($name, $email)
  {
    $this->name = $name;
    $this->email = $email;
  }

  // A method that returns the user's name
  public function getName()
  {
    return $this->name;
  }

  // A method that returns the user's email
  public function getEmail()
  {
    return $this->email;
  }
}

// A class that represents a user repository
class UserRepository
{
  // A method that saves a user to the database
  public function save(User $user)
  {
    // Save the user to the database
  }
}

// A class that sends email notifications
class EmailNotifier
{
  // A method that sends an email to a user
  public function sendEmail(User $user, $message)
  {
    // Send the email to the user
  }
}

// Create a new user and save it to the database
$user = new User("John Doe", "john.doe@example.com");
$repository = new UserRepository();
$repository->save($user);

// Send an email notification to the user
$notifier = new EmailNotifier();
$notifier->sendEmail($user, "Welcome to our website!");

In this example, the User class has a single responsibility, which is to represent a user and provide methods for getting their name and email. The UserRepository class has a single responsibility, which is to save users to the database. And the EmailNotifier class has a single responsibility, which is to send email notifications to users. By following the Single Responsibility Principle, these classes are easier to maintain and extend, and their code is more readable and organized.

Open-Closed Principle

The Open-Closed Principle states that a class should be open for extension but closed for modification. In PHP, this can be achieved by designing classes that have clear, well-defined interfaces and that use inheritance and abstraction to allow for flexibility and extensibility. For example, a Shape class might define an area() method that calculates the area of a shape, and subclasses such as Circle and Rectangle could override this method to provide their own implementation.

| Here is an example of how to apply the Open-Closed Principle in PHP:

<?php

// An abstract shape class that defines a common interface for all shapes
abstract class Shape
{
  // An abstract method that calculates the area of a shape
  abstract public function area();
}

// A class that represents a circle
class Circle extends Shape
{
  private $radius;

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

  // An implementation of the area() method that calculates the area of a circle
  public function area()
  {
    return 3.14 * $this->radius * $this->radius;
  }
}

// A class that represents a rectangle
class Rectangle extends Shape
{
  private $width;
  private $height;

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

  // An implementation of the area() method that calculates the area of a rectangle
  public function area()
  {
    return $this->width * $this->height;
  }
}

// A class that calculates the total area of a collection of shapes
class AreaCalculator
{
  private $shapes;

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

  // A method that calculates the total area of all shapes
  public function getTotalArea()
  {
    $totalArea = 0;
    foreach ($this->shapes as $shape) {
      $totalArea += $shape->area();
    }
    return $totalArea;
  }
}

// Create some shapes and calculate their total area
$circle = new Circle(5);
$rectangle = new Rectangle(10, 5);
$calculator = new AreaCalculator([$circle, $rectangle]);
$totalArea = $calculator->getTotalArea();

// Output the total area
echo "Total area: " . $totalArea . "\n";

In this example, the Shape class is an abstract class that defines a common interface for all shapes. The Circle and Rectangle classes extend Shape and provide their own implementation of the area() method. The AreaCalculator class calculates the total area of a collection of shapes, without knowing or caring about the specific types of shapes that are passed to it. This allows the AreaCalculator to be easily extended to support additional shape types, without modifying its existing code. By following the Open-Closed Principle, the code is more flexible and maintainable.

Liskov Substitution Principle

The Liskov Substitution Principle states that derived classes should be substitutable for their base classes. In PHP, this can be achieved by ensuring that subclasses conform to the same contracts and behave in the same way as their base classes. For example, if a Car class defines a start() method that starts the car's engine, a Truck class that extends Car should also have a start() method that starts the truck's engine in the same way.

| Here is an example of how to apply the Liskov Substitution Principle in PHP:

<?php

// An abstract vehicle class that defines a common interface for all vehicles
abstract class Vehicle
{
  // An abstract method that starts the vehicle
  abstract public function start();
}

// A class that represents a car
class Car extends Vehicle
{
  // An implementation of the start() method that starts the car's engine
  public function start()
  {
    // Start the car's engine
  }
}

// A class that represents a truck
class Truck extends Vehicle
{
  // An implementation of the start() method that starts the truck's engine
  public function start()
  {
    // Start the truck's engine
  }
}

// A class that controls a vehicle
class VehicleController
{
  private $vehicle;

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

  // A method that starts the vehicle
  public function startVehicle()
  {
    $this->vehicle->start();
  }
}

// Create a car and a truck, and control them
$car = new Car();
$truck = new Truck();
$carController = new VehicleController($car);
$truckController = new VehicleController($truck);
$carController->startVehicle();
$truckController→startVehicle();

In this example, the Vehicle class is an abstract class that defines a common interface for all vehicles. The Car and Truck classes extend Vehicle and provide their own implementation of the start() method. The VehicleController class controls a vehicle without knowing or caring about the specific type of vehicle that it is controlling. This allows the VehicleController to be used with any type of vehicle that extends Vehicle, and ensures that all subclasses of Vehicle are substitutable for their base class. By following the Liskov Substitution Principle, the code is more flexible and maintainable.

Interface Segregation Principle

The Interface Segregation Principle states that clients should not be forced to depend on methods that they do not use. In PHP, this can be achieved by defining separate interfaces for different groups of related methods, and having classes implement only the interfaces that are relevant to their functionality. For example, a PaymentProcessor class might implement an AuthorizePayment interface that defines methods for authorizing payment transactions, and a RefundProcessor class might implement a ProcessRefund interface that defines methods for processing refunds.

| Here is an example of how to apply the Interface Segregation Principle in PHP:

<?php

// An interface that defines methods for managing user accounts
interface UserAccountManager
{
  // A method that creates a new user account
  public function createAccount(string $username, string $password);

  // A method that updates a user's password
  public function updatePassword(string $username, string $password);

  // A method that deletes a user account
  public function deleteAccount(string $username);
}

// An interface that defines methods for managing user profiles
interface UserProfileManager
{
  // A method that gets a user's profile
  public function getProfile(string $username);

  // A method that updates a user's profile
  public function updateProfile(string $username, array $profile);
}

// An interface that defines methods for managing user payments
interface UserPaymentManager
{
  // A method that gets a user's payment information
  public function getPaymentInfo(string $username);

  // A method that updates a user's payment information
  public function updatePaymentInfo(string $username, array $paymentInfo);
}

// A class that manages user accounts, profiles, and payments
class UserManager implements UserAccountManager, UserProfileManager, UserPaymentManager
{
  // An implementation of the createAccount() method
  public function createAccount(string $username, string $password)
  {
    // Create the user account
  }

  // An implementation of the updatePassword() method
  public function updatePassword(string $username, string $password)
  {
    // Update the user's password
  }

  // An implementation of the deleteAccount() method
  public function deleteAccount(string $username)
  {
    // Delete the user account
  }

  // An implementation of the getProfile() method
  public function getProfile(string $username)
  {
    // Get the user's profile
  }

  // An implementation of the updateProfile() method
  public function updateProfile(string $username, array $profile)
  {
    // Update the user's profile
  }

  // An implementation of the getPaymentInfo() method
  public function getPaymentInfo(string $username)
  {
    // Get the user's payment information
  }

  // An implementation of the updatePaymentInfo() method
  public function updatePaymentInfo(string $username, array $paymentInfo)
  {
    // Update the user's payment information
  }
}

// Create a new user manager and use its methods
$userManager = new UserManager();
$userManager->createAccount("john.doe", "password123");
$userManager->updateProfile("john.doe", ["name" => "John Doe"]);
$userManager->updatePaymentInfo("john.doe", ["card_number" => "1234567890"]);

In this example, the UserManager class implements three separate interfaces for managing user accounts, profiles, and payments. Each interface defines a group of related methods, and the UserManager class only implements the methods that are relevant to its functionality. This allows the UserManager to be easily unit tested and extended, and it also makes the code more readable and organized. By following the Interface Segregation Principle, the code is more flexible and maintainable.

Dependency Inversion Principle

The Dependency Inversion Principle states that high-level modules should not depend on low-level modules, but rather both should depend on abstractions. In PHP, this can be achieved by using dependency injection to provide an object with its dependencies, rather than creating the dependencies directly within the object. For example, a UserService class might accept a DatabaseConnection object in its constructor, rather than creating the connection itself. This allows the UserService to be easily unit tested, and also allows for greater flexibility if the underlying implementation of the DatabaseConnection class changes.

| Here is an example of how to apply the Dependency Inversion Principle in PHP:

<?php

// An interface that defines methods for connecting to a database
interface DatabaseConnection
{
  // A method that opens a connection to the database
  public function connect();

  // A method that closes the connection to the database
  public function disconnect();
}

// A class that provides a MySQL database connection
class MySQLDatabaseConnection implements DatabaseConnection
{
  // An implementation of the connect() method
  public function connect()
  {
    // Connect to the MySQL database
  }

  // An implementation of the disconnect() method
  public function disconnect()
  {
    // Close the MySQL database connection
  }
}

// A class that provides a PostgreSQL database connection
class PostgreSQLDatabaseConnection implements DatabaseConnection
{
  // An implementation of the connect() method
  public function connect()
  {
    // Connect to the PostgreSQL database
  }

  // An implementation of the disconnect() method
  public function disconnect()
  {
    // Close the PostgreSQL database connection
  }
}

// A class that uses a database connection to query the database
class QueryExecutor
{
  private $databaseConnection;

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

  // A method that queries the database and returns the result
  public function query(string $query)
  {
    // Connect to the database
    $this->databaseConnection->connect();

    // Execute the query and get the result
    $result = // Query the database

    // Close the database connection
    $this->databaseConnection->disconnect();

    return $result;
  }
}

// Create a query executor with a MySQL database connection
$mysqlConnection = new MySQLDatabaseConnection();
$queryExecutor = new QueryExecutor($mysqlConnection);

// Query the database using the MySQL connection
$result = $queryExecutor->query("SELECT * FROM users");

// Create a query executor with a PostgreSQL database connection
$postgresConnection = new PostgreSQLDatabaseConnection();
$queryExecutor = new QueryExecutor($postgresConnection);

// Query the database using the PostgreSQL connection
$result = $queryExecutor->query("SELECT * FROM users");

In this example, the QueryExecutor class depends on an abstract DatabaseConnection interface, rather than a concrete MySQLDatabaseConnection or PostgreSQLDatabaseConnection class. This allows the QueryExecutor to be easily unit tested and extended, and it also allows for greater flexibility if the underlying implementation of the DatabaseConnection class changes. By following the Dependency Inversion Principle, the code is more flexible and maintainable.

Did you find this article valuable?

Support Olgun by becoming a sponsor. Any amount is appreciated!