PHP Design Patterns - Prototype
Tags: php, webdev, tutorial, oop
Published: 2021-10-06 18:37:25

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](https://media.giphy.com/media/3oKHWnC0GztmsjVLCE/giphy.gif)

From [refactoring.guru](https://refactoring.guru/design-patterns/prototype):
> 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](https://github.com/fabiothiroki/php-design-patterns):
 {% github fabiothiroki/php-design-patterns no-readme %} 

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:

```php
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;
    }
}
```

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

```php
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;
    }
}
```

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`:
```php
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';
    }
}
```

## Explaining Prototype with Unit test
![Gif of a house multiplying itself](https://media.giphy.com/media/l4FB2YVpNjtedlFWE/giphy.gif)
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.
```php
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);
}
```
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:
```php
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());
}
```

![Gif of spongebob multiplying](https://media.giphy.com/media/xT3i1faW3SLPwdoVs4/giphy.gif)

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!