PHP Design Patterns - Prototype

Fabio Hiroki - Oct 6 '21 - - Dev Community

Let's suppose you are a developer working on an eCommerce, and you're given the responsibility for this feature:

In the Cart screen, user should be able to add more of an existing product there, and change its color.

One intuitive solution in this context could be instantiating a new product object, and set all attributes to be equal the original product. But this seems too verbose, and you also have to know all the internals of the product code, which seems like breaking the encapsulation.

Prototype to the rescue!

Gif of someone being cloned

From refactoring.guru:

Prototype is a creational design pattern that lets you copy existing objects without making your code dependent on their classes.

That means you don't need to use the new operator and worry about how to setup a perfect copy instance. Just use the built-in clone operator and PHP will deal with everything.

Sample application

You check the final code on Github:

GitHub logo fabiothiroki / php-design-patterns

A collection of design patterns written in PHP

The Cart class doesn't make part of the design pattern but it's here just to demonstrate how it fits in a real world application:

final class Cart
{
    /**
     * @var ProductPrototype[]
     */
    private array $products;

    public function addProduct(ProductPrototype $product): void
    {
        $this->products[] = $product;
    }

    /**
     * @return ProductPrototype[]
     */
    public function getProducts(): array
    {
        return $this->products;
    }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, we are able to add a new product and see which products are already there.

abstract class ProductPrototype
{
    protected int $id;
    protected string $name;
    protected string $color;

    public function getId(): int
    {
        return $this->id;
    }

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

    public function getColor(): string
    {
        return $this->color;
    }

    public function setColor(string $color): void
    {
        $this->color = $color;
    }
}
Enter fullscreen mode Exit fullscreen mode

PHP already implements the Prototype design pattern natively because every object already has a built-in clone operator support. In this example, I created the ProductPrototype class just to illustrate a real use case: for Cart class is doesn't matter which specific kind of product it has, as long as it has the attributes id, name and color.

Just to simplify this example, I will add only two types of product: Smartphone and Laptop:

final class Smartphone extends ProductPrototype
{
    public function __construct()
    {
        $this->id = 1;
        $this->name = 'Smartphone';
        $this->color = 'Default color';
    }
}

final class Laptop extends ProductPrototype
{
    public function __construct()
    {
        $this->id = 2;
        $this->name = 'Smartphone';
        $this->color = 'Default color';
    }
}
Enter fullscreen mode Exit fullscreen mode

Explaining Prototype with Unit test

Gif of a house multiplying itself
What does the clone operator really do? It creates a copy of an instance with the same attributes. So in our example the cloned product will contain the same id, name and color as the original.

public function testSmartphoneClone(): void
{
    $smartphone = new Smartphone();

    $clonedSmartphone = clone $smartphone;

    self::assertEquals($clonedSmartphone->getId(), $smartphone->getId());
    self::assertEquals($clonedSmartphone->getName(), $smartphone->getName());
    self::assertEquals($clonedSmartphone->getColor(), $smartphone->getColor());
    self::assertEquals($clonedSmartphone, $smartphone);
    self::assertNotSame($clonedSmartphone, $smartphone);
}
Enter fullscreen mode Exit fullscreen mode

As you can see, if we were to manually clone an object, we would have to know all the properties to be copied, and how to set them.

Please notice this test shouldn't be written in a real application because we can assume the clone method already works since its provided by PHP itself.

Back to our use case

We finally can enable the Cart to increase the quantity of an existing product and change its color. Cart will be even happier because it doesn't matter if it's an Laptop or Smartphone.

Just make use of clone and add the new cloned product:

public function testCartCanAddClonedProducts(): void
{
    $laptop = new Laptop();
    $cart = new Cart();

    $cart->addProduct($laptop);

    $clonedLaptop = clone $laptop;
    $clonedLaptop->setColor('White');
    $cart->addProduct($clonedLaptop);

    self::assertCount(2, $cart->getProducts());
    self::assertEquals($cart->getProducts()[1]->getId(), $cart->getProducts()[0]->getId());
    self::assertEquals($cart->getProducts()[1]->getName(), $cart->getProducts()[0]->getName());
    self::assertEquals('White', $cart->getProducts()[1]->getColor());
    self::assertEquals('Default color', $cart->getProducts()[0]->getColor());
}
Enter fullscreen mode Exit fullscreen mode

Gif of spongebob multiplying

Thanks for reading and hope you could get a better picture of this design pattern and will remember to use it whenever the opportunity appears!

. . . . . . . . . . . . . . . . . . . . . . . . . . . .