PHP: Prototype

Prototype

Prototype is a creational design pattern that allows cloning objects, even complex ones, without coupling to their specific classes.

All prototype classes should have a common interface that makes it possible to copy objects even if their concrete classes are unknown. Prototype objects can produce full copies since objects of the same class can access each other's private fields.

More about Prototype

Application of the pattern in PHP

Complexity:

Popularity:

Usage examples: The Prototype pattern is available in PHP out of the box. You can use the clone keyword to create an exact copy of an object. To add cloning support to a class, you need to implement a __clone method.

Example: Structure of the Pattern

This example illustrates the structure of the Prototype design pattern and focuses on following questions:

  • What classes does it consists of?
  • What roles do these classes play?
  • In what way the elements of the pattern are related?

After learning about the pattern's structure it will be easier for you to grasp the following example, based on a real world PHP use case.

PrototypeStructural.php: Structural Example

<?php

namespace RefactoringGuru\Prototype\Structural;

/**
 * The example class that has cloning ability.
 */
class Prototype
{
    public $primitive;
    public $component;
    public $circularReference;

    /**
     * PHP has built-in cloning support. You can `clone` an object without
     * defining any special methods as long as it has fields of primitive types.
     * Fields containing objects retain their references in a cloned object.
     * Therefore, in some cases, you might want to clone those referenced
     * objects as well. You can do this in a special `__clone()` method.
     */
    public function __clone()
    {
        $this->component = clone $this->component;

        // Cloning an object that has a nested object with backreference
        // requires special treatment. After the cloning is completed, the
        // nested object should point to the cloned object, instead of the
        // original object.
        $this->circularReference = clone $this->circularReference;
        $this->circularReference->prototype = $this;
    }
}

class ComponentWithBackReference
{
    public $prototype;

    /**
     * Note that the constructor won't be executed during cloning. If you have
     * complex logic inside the constructor, you may need to execute it in the
     * `__clone` method as well.
     */
    public function __construct(Prototype $prototype)
    {
        $this->prototype = $prototype;
    }
}

/**
 * The client code.
 */
function clientCode()
{
    $p1 = new Prototype();
    $p1->primitive = 245;
    $p1->component = new \DateTime();
    $p1->circularReference = new ComponentWithBackReference($p1);

    $p2 = clone $p1;
    if ($p1->primitive === $p2->primitive) {
        print("Primitive field values have been carried over to a clone. Yay!\n");
    } else {
        print("Primitive field values have not been copied. Booo!\n");
    }
    if ($p1->component === $p2->component) {
        print("Simple component has not been cloned. Booo!\n");
    } else {
        print("Simple component has been cloned. Yay!\n");
    }

    if ($p1->circularReference === $p2->circularReference) {
        print("Component with back reference has not been cloned. Booo!\n");
    } else {
        print("Component with back reference has been cloned. Yay!\n");
    }

    if ($p1->circularReference->prototype === $p2->circularReference->prototype) {
        print("Component with back reference is linked to original object. Booo!\n");
    } else {
        print("Component with back reference is linked to the clone. Yay!\n");
    }
}

clientCode();

Output.txt: Output

Primitive field values have been carried over to a clone. Yay!
Simple component has been cloned. Yay!
Component with back reference has been cloned. Yay!
Component with back reference is linked to the clone. Yay!

Example: Real World Example

The Prototype pattern provides a convenient way to replicate existing objects instead of reconstructing them and copying over all of their fields directly. The direct approach not only couples you to the classes of the objects being cloned, but also doesn't allow you to copy the contents of the private fields. The Prototype pattern lets you perform the cloning within the context of the cloned class, where the access to the class' private fields is not restricted.

This example shows you how to clone a complex Page object using the Prototype pattern. The Page class has lots of private fields, which will be carried over to the cloned object thanks to the Prototype pattern.

PrototypeRealWorld.php: Real world example

<?php

namespace RefactoringGuru\Prototype\RealWorld;

/**
 * Prototype Design Pattern
 *
 * Intent: Produce new objects by copying existing ones without compromising
 * their internal structure.
 *
 * Example: The Prototype pattern provides a convenient way to replicate
 * existing objects instead of reconstructing them and copying over all of their
 * fields directly. The direct approach not only couples you to the classes of
 * the objects being cloned, but also doesn't allow you to copy the contents of
 * the private fields. The Prototype pattern lets you perform the cloning within
 * the context of the cloned class, where the access to the class' private
 * fields is not restricted.
 *
 * This example shows you how to clone a complex Page object using the Prototype
 * pattern. The Page class has lots of private fields, which will be carried
 * over to the cloned object thanks to the Prototype pattern.
 */

/**
 * Prototype.
 */
class Page
{
    private $title;

    private $body;

    /**
     * @var Author
     */
    private $author;

    private $comments = [];

    /**
     * @var \DateTime
     */
    private $date;

    // +100 private fields.

    public function __construct($title, $body, $author)
    {
        $this->title = $title;
        $this->body = $body;
        $this->author = $author;
        $this->author->addToPage($this);
        $this->date = new \DateTime();
    }

    public function addComment($comment)
    {
        $this->comments[] = $comment;
    }

    /**
     * You can control what data you want to carry over to the cloned object.
     *
     * For instance, when a page is cloned:
     * - It gets a new "Copy of ..." title.
     * - The author of the page remains the same. Therefore we leave the
     * reference to the existing object while adding the cloned page to the list
     * of the author's pages.
     * - We don't carry over the comments from the old page.
     * - We also attach a new date object to the page.
     */
    public function __clone()
    {
        $this->title = "Copy of " . $this->title;
        $this->author->addToPage($this);
        $this->comments = [];
        $this->date = new \DateTime();
    }
}

class Author
{
    private $name;

    /**
     * @var Page[]
     */
    private $pages = [];

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

    public function addToPage($page)
    {
        $this->pages[] = $page;
    }
}

/**
 * The client code.
 */
function clientCode()
{
    $author = new Author("John Smith");
    $page = new Page("Tip of the day", "Keep calm and carry on.", $author);

    // ...

    $page->addComment("Nice tip, thanks!");

    // ...

    $draft = clone $page;
    print("Dump of the clone. Note that the author is now referencing two objects.\n\n");
    print_r($draft);
}

clientCode();

Output.txt: Output

Dump of the clone. Note that the author is now referencing two objects.

RefactoringGuru\Prototype\RealWorld\Page Object
(
    [title:RefactoringGuru\Prototype\RealWorld\Page:private] => Copy of Tip of the day
    [body:RefactoringGuru\Prototype\RealWorld\Page:private] => Keep calm and carry on.
    [author:RefactoringGuru\Prototype\RealWorld\Page:private] => RefactoringGuru\Prototype\RealWorld\Author Object
        (
            [name:RefactoringGuru\Prototype\RealWorld\Author:private] => John Smith
            [pages:RefactoringGuru\Prototype\RealWorld\Author:private] => Array
                (
                    [0] => RefactoringGuru\Prototype\RealWorld\Page Object
                        (
                            [title:RefactoringGuru\Prototype\RealWorld\Page:private] => Tip of the day
                            [body:RefactoringGuru\Prototype\RealWorld\Page:private] => Keep calm and carry on.
                            [author:RefactoringGuru\Prototype\RealWorld\Page:private] => RefactoringGuru\Prototype\RealWorld\Author Object
 *RECURSION*
                            [comments:RefactoringGuru\Prototype\RealWorld\Page:private] => Array
                                (
                                    [0] => Nice tip, thanks!
                                )

                            [date:RefactoringGuru\Prototype\RealWorld\Page:private] => DateTime Object
                                (
                                    [date] => 2018-06-04 14:50:39.306237
                                    [timezone_type] => 3
                                    [timezone] => UTC
                                )

                        )

                    [1] => RefactoringGuru\Prototype\RealWorld\Page Object
 *RECURSION*
                )

        )

    [comments:RefactoringGuru\Prototype\RealWorld\Page:private] => Array
        (
        )

    [date:RefactoringGuru\Prototype\RealWorld\Page:private] => DateTime Object
        (
            [date] => 2018-06-04 14:50:39.306272
            [timezone_type] => 3
            [timezone] => UTC
        )

)