Skip to content

Instantly share code, notes, and snippets.

@heavensloop
Last active October 18, 2024 22:41
Show Gist options
  • Save heavensloop/fc33ffa3aa5ba4c9b9f011942250adeb to your computer and use it in GitHub Desktop.
Save heavensloop/fc33ffa3aa5ba4c9b9f011942250adeb to your computer and use it in GitHub Desktop.
PHP-Interface-Design-Patterns

PHP-Interface-Design-Patterns

Introduction

I recently started using a package called prophecy which is a mocking library for PHP. I was intrigued by the way the library was designed and how it was able to return the same object but with a different interface. This made me think about how I could use this in my own code. I decided to create a simple example of how this could be done.

Problem

I wanted to create an object that could be modified and then returned as a readonly object.

Solution

I created two interfaces, HumanModifierInterface and HumanBeingInterface.

HumanModifierInterface is used to modify the object and HumanBeingInterface is used to present the object as readonly.

I then created a class called HumanBuilder that implements both interfaces.

The HumanBuilder class has a static method called initialize that returns an instance of the class.

Enough talk.. Here is the code:

<?php

interface HumanModifierInterface
{
  public function identifyAs(string $type);

  /** Any [magic](https://www.php.net/manual/en/language.oop5.magic.php) methods will return the same interface */
  public function __call(): HumanModifierInterface;

  /** Returns the same object but presents an interface that makes it readonly */
  public function reveal(): HumanBeingInterface;
}

interface HumanBeingInterface
{
  public function getIdentity();

  public function getName();

  public function getAttribute();
}

class HumanBuilder implements HumanModifierInterface, HumanBeingInterface
{
  private string $identity;
  private array $attributes;

  public function __construct()
  {
    $this->attributes = [];
  }

  public function initialize(): HumanModifierInterface
  {
    return new self;
  }

  public function identifyAs(string $type): HumanModifierInterface
  {
    $this->identity = $type;
    return $this;
  }

  /** 
   * Any magic methods will return the same interface 
   * In this example, This method will be used to set attributes
   */
  public function __call($name, $arguments): HumanModifierInterface
  {
    // If it's a call method that starts with 'set', then set the attribute
    if (strpos($name, 'set') === 0) {
      $attribute = lcfirst(substr($name, 3));
      $this->attributes[$attribute] = $arguments[0];
    }

    return $this;
  }

  public function getAttribute(string $name)
  {
    if (strpos($name, 'get') === 0) {
      $attribute = lcfirst(substr($name, 3));
      return $this->attributes[$attribute] = $arguments[0];
    }

    return $this;
  }

  public function reveal(): HumanBeingInterface
  {
    return $this;
  }

  public function getIdentity()
  {
    return $this->identity;
  }
}

$humanCreator = (new HumanBuilder)->initialize()
  ->identifyAs('Human') // Returns a HumanModifierInterface
  ->setName('John Doe') // Returns a HumanModifierInterface
  ->setAge(25) // Returns a HumanModifierInterface
  ->reveal(); // Returns a HumanBeingInterface (readonly)

echo $humanCreator->getAttribute('name'); // John Doe
echo $humanCreator->getAttribute('age'); // 25
echo $humanCreator->getIdentity(); // Human

Finally

So, let's say we had a class (or service), Evolution that required an instance of HumanBeingInterface but we wanted to modify the object before passing it to the service. We could do something like this:

class Evolution
{
  private HumanBeingInterface $human;

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

  public function hasEvolved(): bool
  {
    return $this->human->getAttribute('age') > 25;
  }
}

$humanCreator = (new HumanBuilder)->initialize()
  ->identifyAs('Human') // Returns a HumanModifierInterface
  ->setName('John Doe') // Returns a HumanModifierInterface
  ->setAge(20); // Returns a HumanModifierInterface
  
$evolution = new Evolution($humanCreator->reveal());

echo $evolution->hasEvolved(); // false

$humanCreator->setAge(26);
echo $evolution->hasEvolved(); // true

And that's where the magic happens

Conclusion

This is a simple example of how you can use interfaces to present a readonly object. There are many other uses for this pattern. This example was inspired by a package called prophecy which is a mocking library for PHP.

I highly recommend it for mocking objects in your tests. I hope this helps you in your projects.

@heavensloop
Copy link
Author

If this made sense to you, lemme know in the comments

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment