paint-brush
Real-World Examples of Using Design Patterns in Modern PHPby@zhukmax
11,843 reads
11,843 reads

Real-World Examples of Using Design Patterns in Modern PHP

by Max ZhukJanuary 2nd, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Design patterns are useful for creating scalable, maintainable, and reusable code. In this article, we will explore real-world examples of using design patterns in PHP 8 applications. By examining these examples, you will gain a deeper understanding of how design patterns can be applied in practical situations.

People Mentioned

Mention Thumbnail
featured image - Real-World Examples of Using Design Patterns in Modern PHP
Max Zhuk HackerNoon profile picture

Design patterns are an essential part of software development, providing a common language and best practices for designing and implementing solutions to recurring problems. In the world of PHP development, design patterns are particularly useful for creating scalable, maintainable, and reusable code.


In this article, we will explore real-world examples of using design patterns in PHP 8 applications. By examining these examples, you will gain a deeper understanding of how design patterns can be applied in practical situations and the benefits they bring to your code.


The purpose of this article is to provide a comprehensive guide to design patterns in PHP and how they can be used to improve the quality and efficiency of your code. Whether you are a beginner or an experienced developer, you will find valuable insights and practical tips in the following pages. So, without further ado, let's dive into the world of design patterns in PHP.

The Singleton Pattern

Photo by @felipepelaquim on Unsplash


The Singleton Pattern is a software design pattern that ensures a class has only one instance, while providing a global access point to that instance. This is useful in situations where it is necessary to limit the number of instances of a class, such as when managing resources or ensuring data integrity.


One real-world example of using the Singleton Pattern is in the management of user sessions in a web application. User sessions are used to track user activity and maintain state across HTTP requests. By using the Singleton Pattern to manage user sessions, we can ensure that there is only one instance of the session manager, and all requests for a user's session are directed to the same instance.


class SessionManager
{
    private static $instance;

    private function __construct()
    {
        // private constructor to prevent direct instantiation
    }

    public static function getInstance()
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }

        return self::$instance;
    }

    // other methods for managing sessions
}


To use the SessionManager class, we would call the getInstance method:


$sessionManager = SessionManager::getInstance();


There are several advantages to using the Singleton Pattern in this scenario. Firstly, it ensures that there is only one instance of the session manager, which simplifies the code and makes it easier to maintain. Secondly, it allows us to easily access the session manager from anywhere in the application, as it is a global access point. Finally, it allows us to centralize the management of user sessions, making it easier to maintain and debug the application.


Overall, the Singleton Pattern is a useful tool for ensuring that there is only one instance of a class, and for providing a global access point to that instance. It is particularly useful in situations where it is necessary to limit the number of instances of a class and to centralize the management of resources or data.

The Observer Pattern

Photo by Daniel Lerman on Unsplash


The Observer Pattern is a software design pattern in which an object, called the subject, maintains a list of its dependents, called observers, and notifies them automatically of any state changes. This allows the observers to be notified of changes to the subject's state and to synchronize their own state with the subject.


One real-world example of using the Observer Pattern is in a social media app to update followers' feeds when a user posts new content. In this scenario, the user's feed would be the subject, and the followers' feeds would be the observers. When the user posts new content, the subject would notify the followers' feeds, which would update to show the new content.


There are several advantages to using the Observer Pattern in this scenario. Firstly, it allows the followers' feeds to be updated automatically whenever new content is posted, without the need for the followers to manually refresh their feeds. This makes the app more user-friendly and reduces the load on the server. Secondly, it allows the followers' feeds to be updated in real-time, so they can see new content as soon as it is posted. Finally, it decouples the followers' feeds from the user's feed, so the followers' feeds can be updated independently of the user's feed.


Here is an example of how the Observer Pattern could be implemented in PHP:


interface Observer
{
    public function update(Subject $subject);
}

interface Subject
{
    public function attach(Observer $observer);
    public function detach(Observer $observer);
    public function notify();
}

class UserFeed implements Subject
{
    private $observers = [];
    private $content;

    public function attach(Observer $observer)
    {
        $this->observers[] = $observer;
    }

    public function detach(Observer $observer)
    {
        $key = array_search($observer, $this->observers, true);
        if ($key) {
            unset($this->observers[$key]);
        }
    }

    public function notify()
    {
        foreach ($this->observers as $observer) {
            $observer->update($this);
        }
    }

    public function addContent($content)
    {
        $this->content = $content;
        $this->notify();
    }

    public function getContent()
    {
        return $this->content;
    }
}

class FollowerFeed implements Observer
{
    private $content;

    public function update(Subject $subject)
    {
        $this->content = $subject->getContent();
    }
}

$userFeed = new UserFeed();
$followerFeed1 = new FollowerFeed();
$followerFeed2 = new FollowerFeed();

$userFeed->attach($followerFeed1);
$userFeed->attach($followerFeed2);

$userFeed->addContent('Hello, world!');

echo $followerFeed1->content; // Outputs "Hello, world!"
echo $followerFeed2->content; // Outputs "Hello, world!"


The Observer interface defines a method for updating the observer when the subject's state changes. The Subject interface defines methods for attaching and detaching observers, and for notifying observers of state changes.


The UserFeed class represents the user's feed and implements the Subject interface. It maintains a list of attached observers and has a content property that stores the current content of the feed. When new content is added to the feed, it calls the notify method to notify the attached observers of the state change. The attach and detach methods are used to add and remove observers from the list of attached observers.


The FollowerFeed class represents a follower's feed and implements the Observer interface. It has a content property that stores the current content of the feed. When the update method is called, it updates the content property with the current content of the subject (i.e., the user's feed).


To use the UserFeed and FollowerFeed classes, we first create an instance of the UserFeed class and two instances of the FollowerFeed class. We then attach the two FollowerFeed instances to the UserFeed instance using the attach method.


When new content is added to the UserFeed using the addContent method, it calls the notify method to notify the attached observers. The update method of the FollowerFeed instances is called, and they update their content properties with the new content from the UserFeed.


Finally, we output the content properties of the FollowerFeed instances, which should show the new content that was added to the UserFeed.

The Builder Pattern

Photo by Ralph (Ravi) Kayden on Unsplash


The Builder Pattern is a software design pattern that allows the construction of complex objects to be separated from their representation. It allows for the creation of objects in a step-by-step fashion, using a builder object that provides a generic interface for creating parts of a complex object.


One real-world example of using the Builder Pattern is in a data import tool that constructs complex objects from simple input. For example, consider a tool that imports customer data from a CSV file into a customer management system. The tool might use the Builder Pattern to construct a customer object from the input data, separating the construction of the customer object from the parsing of the input data.


There are several advantages to using the Builder Pattern in this scenario. Firstly, it allows the construction of complex objects to be separated from the input data, making it easier to modify the input data or the construction process without affecting the other. Secondly, it allows for the creation of objects in a step-by-step fashion, which can make the code easier to understand and maintain. Finally, it allows for the reuse of builder objects, so the same construction process can be used to create multiple objects.


To implement the Builder Pattern in PHP, we can define an abstract Builder class that specifies the methods for constructing the complex object. We can then define a concrete implementation of the Builder class that actually constructs the object. The complex object itself can be represented by a separate class, which should have setter methods for each of its properties.


The data import tool can then use the Builder object to create the complex object by calling the appropriate methods to set its properties. Once all of the properties have been set, the Builder object can return the completed object.


Here is an example of how the Builder Pattern could be implemented in PHP:


abstract class CustomerBuilder
{
    abstract public function setName(string $name): void;
    abstract public function setEmail(string $email): void;
    abstract public function setPhone(string $phone): void;
    abstract public function setAddress(string $address): void;
    abstract public function getCustomer(): Customer;
}

class Customer
{
    public function __construct(
        public string $name = '',
        public string $email = '',
        public string $phone = '',
        public string $address = ''
    ) {}
}

class CustomerBuilderImpl extends CustomerBuilder
{
    private Customer $customer;

    public function __construct()
    {
        $this->customer = new Customer();
    }

    public function setName(string $name): void
    {
        $this->customer->->name = $name;
    }

    public function setEmail(string $email): void
    {
        $this->customer->email = $email;
    }

    public function setPhone(string $phone): void
    {
        $this->customer->phone = $phone;
    }

    public function setAddress(string $address): void
    {
        $this->customer->address = $address;
    }

    public function getCustomer(): Customer
    {
        return $this->customer;
    }
}

class ImportTool
{
    private CustomerBuilder $builder;

    public function setBuilder(CustomerBuilder $builder): void
    {
        $this->builder = $builder;
    }

    public function importCustomer(string $name, string $email, string $phone, string $address): void
    {
        $this->builder->setName($name);
        $this->builder->setEmail($email);
        $this->builder->setPhone($phone);
        $this->builder->setAddress($address);
    }

    public function getCustomer(): Customer
    {
        return $this->builder->getCustomer();
    }
}


This class has a setBuilder method that sets the CustomerBuilder object to be used for creating customer objects, and an importCustomer method that uses the CustomerBuilder to set the properties of a new customer object. The getCustomer method returns the completed customer object.


To use the Customer, CustomerBuilder, and ImportTool classes, you would first need to create an instance of the ImportTool class. Then, you would need to create an instance of a concrete CustomerBuilder implementation, such as CustomerBuilderImpl, and set it on the ImportTool instance using the setBuilder method.


You can then use the importCustomer method of the ImportTool instance to set the properties of a new customer object, and use the getCustomer method to retrieve the completed customer object.


Here is an example of how this might look in code:


$importTool = new ImportTool();

$builder = new CustomerBuilderImpl();
$importTool->setBuilder($builder);

$importTool->importCustomer('John Smith', '[email protected]', '123-456-7890', '123 Main St');
$customer = $importTool->getCustomer();

echo $customer->name; // Outputs "John Smith"
echo $customer->email; // Outputs "[email protected]"
echo $customer->phone; // Outputs "123-456-7890"
echo $customer->address; // Outputs "123 Main St"


In this example, we create an instance of the ImportTool class and an instance of the CustomerBuilderImpl class. We set the CustomerBuilderImpl instance on the ImportTool instance using the setBuilder method, and then use the importCustomer method to set the properties of a new customer object. Finally, we use the getCustomer method to retrieve the completed customer object and output its properties.

The Factory Pattern

Photo by Ant Rozetsky on Unsplash


The Factory Pattern is a creational design pattern that provides an interface for creating objects in a super class, but allows subclasses to alter the type of objects that will be created. In other words, it allows for the creation of objects without specifying the exact class of object that will be created.


A real-world example of using the Factory Pattern might be in an e-commerce platform. Let's say you have a Product class that represents an item that can be sold on your platform. This Product class might have various subclasses such as Book, Clothing, and Toy, which represent different types of products that can be sold.


To use the Factory Pattern in this scenario, you might create a ProductFactory class that has a method for creating new products. This method would take in a type parameter and return a new object of the appropriate subclass. Here's an example of how this might look in PHP:


class ProductFactory {
  public static function createProduct(string $type): Product
  {
    return match($type) {
      'book' => new Book(),
      'clothing' => new Clothing(),
      'toy' => new Toy(),
      default => throw new Exception("Invalid product type"),
    }
  }
}


To use this factory, you can simply call the createProduct method and pass in the type of product you want to create. For example:


$book = ProductFactory::createProduct('book');
$clothing = ProductFactory::createProduct('clothing');
$toy = ProductFactory::createProduct('toy');


One of the main advantages of using the Factory Pattern in this scenario is that it allows you to easily add new types of products to your platform without having to modify any existing code. All you have to do is create a new subclass for the new type of product and add a case to the createProduct method in the factory. This can make your code more flexible and easier to maintain over time.

The Prototype Pattern

Photo by Sigmund on Unsplash


The Prototype Pattern is a creational design pattern that allows you to create new objects by copying existing objects. This can be useful when creating new objects is expensive or time-consuming, as it allows you to avoid the overhead of creating a new object from scratch.


A real-world example of using the Prototype Pattern might be in a game engine, where you need to create a large number of game objects with similar characteristics. Using the Prototype Pattern, you could create a prototype object that serves as a template for creating new objects.


To use the Prototype Pattern in this scenario, you could create a GameObject class that represents a generic game object, and then create subclasses for specific types of game objects such as Player, Enemy, and PowerUp. You could then create a GameObjectPrototype class that has a clone method for creating new game objects based on the prototype. Here's an example of how this might look in PHP 8.1:


class GameObject
{
  public function __construct(protected string $name, protected int $health)
  {}

  public function getName(): string
  {
    return $this->name;
  }

  public function setName(string $name): void
  {
    $this->name = $name;
  }

  public function getHealth(): int
  {
    return $this->health;
  }

  public function setHealth(int $health): void
  {
    $this->health = $health;
  }

  public function __clone()
  {
    // Create a new object with the same name and health as the original
    return new self($this->name, $this->health);
  }
}


To use the GameObjectPrototype class, you can simply call the clone method on an existing game object to create a new object based on the prototype. For example:


$playerPrototype = new Player('Player', 100);
$enemyPrototype = new Enemy('Enemy', 50);

$player1 = clone $playerPrototype;
$player2 = clone $playerPrototype;
$enemy1 = clone $enemyPrototype;
$enemy2 = clone $enemyPrototype;


One of the main advantages of using the Prototype Pattern in this scenario is that it allows you to create new game objects quickly and efficiently. Rather than creating a new object from scratch, you can simply clone an existing prototype object and customize it as needed. This can save time and resources, and make your code more efficient.


Another example you can see in my article “Design Patterns in PHP 8: Prototype”:

The Adapter Pattern

The Adapter Pattern is a software design pattern that allows two incompatible interfaces to work together by wrapping one interface in an adapter class. It acts as a bridge between two incompatible interfaces, allowing one interface to be used by another.


A real-world example of using the Adapter Pattern is in the integration of a legacy system with a new system. Let's say that the legacy system has an old interface for retrieving data, but the new system requires a different interface to access the same data. The Adapter Pattern can be used to adapt the old interface to the new system, allowing the two to work together seamlessly.


Here is an example of how the Adapter Pattern could be implemented in PHP 8.1:


<?php

// Old interface for retrieving data from the legacy system
interface OldSystemInterface
{
  public function getData();
}

// New interface required by the new system for retrieving data
interface NewSystemInterface
{
  public function retrieveData();
}

// Adapter class that wraps the old interface and implements the new interface
class Adapter implements NewSystemInterface
{
  private $oldSystem;

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

  // Implementation of the new interface's method,
  // which simply calls the old interface's method
  public function retrieveData()
  {
    return $this->oldSystem->getData();
  }
}

// Example usage
$oldSystem = new OldSystem();
$adapter = new Adapter($oldSystem);
$data = $adapter->retrieveData();


Using the Adapter Pattern in this scenario has several advantages. It allows the legacy system to continue functioning as it did before, while also allowing the new system to access the data it needs through the adapter. It also decouples the two systems, making it easier to modify or replace either one without affecting the other. This can save time and resources, as it avoids the need to rewrite or modify the legacy system to fit the requirements of the new system.


On my Dev Community blog I have article “Design Patterns in PHP 8: Adapter“ with explaining the pattern.

The Bridge Pattern

Photo by Modestas Urbonas on Unsplash


The Bridge Pattern is a software design pattern that separates an abstraction from its implementation, allowing the two to vary independently. It allows a client to access the implementation through an interface, which can be useful in cases where the implementation may need to change over time.


A real-world example of using the Bridge Pattern is in the development of a mobile app. Let's say that the app has a feature that displays a list of items, and the implementation of this feature may need to change in the future. The Bridge Pattern can be used to separate the interface for displaying the list from the implementation of the feature.


Here is an example of how the Bridge Pattern could be implemented in PHP 8.1:


<?php

// Abstract interface for displaying the list of items
interface DisplayInterface
{
  public function display();
}

// Concrete implementation of the display interface
class ListDisplay implements DisplayInterface
{
  public function __construct(private array $items)
  {}

  public function display()
  {
    // Code to display the list of items
  }
}

// Example usage
$items = ['item 1', 'item 2', 'item 3'];
$display = new ListDisplay($items);
$display->display();


Using the Bridge Pattern in this scenario has several advantages. It allows the interface and implementation to vary independently, making it easier to modify either one without affecting the other. It also allows for flexibility in the implementation of the feature, as the interface remains unchanged even if the implementation changes. This can save time and resources, as it avoids the need to rewrite the interface whenever the implementation needs to be modified.

The Command Pattern

Photo by hannah joshua on Unsplash


The Command pattern is a behavioral design pattern that encapsulates a request as an object, allowing for the parameterization of clients with different requests. This can be useful in situations where it is necessary to decouple the sender of a request from the receiver, as the request can be issued without knowing which specific object will handle it.


One real-world example of using the Command pattern is in a video editing software to implement undo/redo functionality. Imagine that a user is editing a video and wants to be able to undo certain actions or redo actions that were previously undone.


With the Command pattern, we could create a "Command" interface that defines a "execute" method and concrete implementations of this interface for each type of edit action (e.g. "CutCommand", "PasteCommand", "AddTransitionCommand"). These command objects could then be added to a history list as they are executed.


To implement the undo functionality, we could simply retrieve the last command from the history list and call its "undo" method. To implement the redo functionality, we could retrieve the next command from the history list and call its "execute" method again.


Here is some example PHP code for a "Command" interface and a concrete "CutCommand" implementation:


interface Command
{
  public function execute(): void;
  public function undo(): void;
}

class CutCommand implements Command
{
  private $originalContent;

  public function __construct(
    private Video $video,
    private $start,
    private $end
  ) {}

  public function execute(): void
  {
    $this->originalContent = $this->video->getContent();
    $this->video->cut($this->start, $this->end);
  }

  public function undo(): void
  {
    $this->video->setContent($this->originalContent);
  }
}


Using the Command pattern in this scenario has several advantages. It allows for a clear separation of concerns, as the command objects handle the details of executing and undoing specific actions, while the client code simply issues the request and maintains the history list. This makes it easy to add new edit actions without having to modify the client code. It also makes it easy to implement the undo/redo functionality, as the command objects handle the details of undoing their respective actions.

The Decorator Pattern

Photo by laura adai on Unsplash


The Decorator pattern is a structural design pattern that allows for the dynamic addition of behavior to an existing object without modifying its code. It does this by wrapping the original object in a decorator object, which provides the additional behavior.


One real-world example of using the Decorator pattern is in a content management system to add functionality to core objects, such as posts or pages. For example, imagine that we want to allow users to add custom styles to their posts or pages, such as setting the font size or color.


With the Decorator pattern, we could create a "PostDecorator" class that implements the same interface as the "Post" class and contains an instance of the "Post" class. The "PostDecorator" class could then provide additional methods for setting the font size and color, which could be implemented by modifying the original content of the "Post" object.


Here is some example PHP 8 code for a "Post" interface and a "BoldPostDecorator" implementation:


interface Post
{
  public function getContent(): string;
}

class BoldPostDecorator implements Post
{
  private Post $post;

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

  public function getContent(): string
  {
    return "<strong>" . $this->post->getContent() . "</strong>";
  }
}


Using the Decorator pattern in this scenario has several advantages. It allows for the extension of functionality without modifying the core "Post" class, which means that we can add new decorators without affecting existing code. It also allows for a flexible and extensible design, as we can easily add new decorators as needed without having to make changes to the core objects. This can make it easier to maintain and update the content management system over time.

The Facade Pattern

Photo by Hardik Pandya on Unsplash


The Facade pattern is a structural design pattern that provides a simplified interface to a complex system. It does this by creating a facade object that hides the complexity of the system and provides a more straightforward interface for interacting with it.


One real-world example of using the Facade pattern is in a service-oriented architecture to provide a simplified interface to complex systems. Imagine that we have a system that consists of several different services, such as a database, a messaging queue, and a cache. Each of these services has its own set of APIs and interfaces, which can be difficult for clients to use and integrate with.


With the Facade pattern, we could create a "SystemFacade" class that provides a single, unified interface for interacting with the system. This class could contain methods for performing common tasks, such as retrieving data from the database, sending messages to the queue, and storing data in the cache.


Here is some example PHP 8 code for a "SystemFacade" class:


class SystemFacade
{
  public function __construct(
    private Database $database,
    private MessageQueue $queue,
    private Cache $cache
  ) {}

  public function getData(string $key): ?string
  {
    $data = $this->cache->get($key);

    if ($data === null) {
      $data = $this->database->get($key);
      $this->cache->set($key, $data);
    }

    return $data;
  }

  public function sendMessage(string $message): void
  {
    $this->queue->send($message);
  }
}


Using the Facade pattern in this scenario has several advantages. It provides a simpler and more unified interface for interacting with the system, which makes it easier for clients to use and integrate with. It also helps to decouple the clients from the underlying implementation of the system, which makes it easier to make changes to the system without affecting the clients. This can make it easier to maintain and update the service-oriented architecture over time.

Conclusion

We have discussed several real-world examples of using design patterns in PHP development, including the use of the factory pattern for creating objects, the observer pattern for communication between objects, and the singleton pattern for ensuring a single instance of an object. These examples demonstrate the practicality and usefulness of design patterns in solving common design problems in software development.


Using design patterns in PHP development can bring numerous benefits, such as improved code reuse, flexibility, and maintainability. It helps developers to design and structure their code in a more organized and efficient way, leading to better overall software quality.


Overall, the use of design patterns is an important aspect of software development that should not be overlooked. By familiarizing ourselves with different design patterns and understanding when and how to use them, we can create more robust and scalable applications that are easier to maintain and evolve over time.


Main photo by Joshua Aragon on Unsplash