2

For teaching purposes, I am trying to create a PHP implementation of a conceptual example of Builder Pattern:

First of all, some products:

class Product1 {
    private string $attribute1;
    private string $attribute2;
    function getAttribute1(): string {
        return $this->attribute1;
    }
    function getAttribute2(): string {
        return $this->attribute2;
    }
    function setAttribute1(string $attribute1): void {
        $this->attribute1 = $attribute1;
    }
    function setAttribute2(string $attribute2): void {
        $this->attribute2 = $attribute2;
    }
}

class Product2 {
    private string $attribute1;
    private string $attribute2;
    function getAttribute1(): string {
        return $this->attribute1;
    }
    function getAttribute2(): string {
        return $this->attribute2;
    }
    function setAttribute1(string $attribute1): void {
        $this->attribute1 = $attribute1;
    }
    function setAttribute2(string $attribute2): void {
        $this->attribute2 = $attribute2;
    }
}

The Builder interface:

interface Builder {
    public function createNewProduct();
    public function makePart1($value);
    public function makePart2($value);
    public function getProduct();
}

The concrete Builders:

class ConcreteBuilder1 implements Builder {
    private Product1 $product;

    function __construct() {
        $this->product = $this->createNewProduct();
    }
    public function createNewProduct() {
        return new Product1();
    }
    public function makePart1($value) {
        $this->product->setAttribute1("variation $value a1"); 
    }
    public function makePart2($value) {
        $this->product->setAttribute2("variation $value a2");
    }
    public function getProduct() {
        return $this->product;
    }

}

class ConcreteBuilder2 implements Builder {
    private Product2 $product;

    function __construct() {
        $this->product = $this->createNewProduct();
    }
    public function createNewProduct() {
        return new Product2();
    }
    public function makePart1($value) {
        $this->product->setAttribute1("variation $value b1");
    }
    public function makePart2($value) {
        $this->product->setAttribute2("variation $value b2");
    }
    public function getProduct() {
        return $this->product;
    }

}

And the Director receiving builder instances by references:

class Director {
    public function createVariation1(Builder &$builder){
        $builder->makePart1(1);
        $builder->makePart2(2);
    }
    public function createVariation2(Builder &$builder){
        $builder->makePart1(3);
        $builder->makePart2(4);
    }
}

Using it:

$builder = new ConcreteBuilder2();
$director = new Director();
$director->createVariation1($builder);
var_dump($builder->getProduct());

So, Is it acceptable this approach? Still a valid GoF Builder?

If not, how would it be a correct conceptual Builder in PHP?

  • 3
    That's not a builder pattern. Builder pattern allows you to set all the values and only get the actual object created when you call the build method. or allow fluent building such as $logger = new LoggerBuilder()->withDateTime()->withLogLevel()->withHtml() to generate a logger which will log files, append dateTime when the log happend, then append LogLevel (Warn, Debug, Information, Tracing etc) and format it with Html (i.e. if its meant to be output on a website or email). What you have is more of a factory pattern, not build pattern – Tseng Feb 14 at 14:56
  • so @Tseng, how would it be a correct implementation? – celsowm Feb 14 at 15:01
  • @celsowm a correct implementation of a builder keeps all of the fields locally in the builder itself and then passes them to an instance's constructor at the end on a .Build() or similar. – Ant P Feb 14 at 15:02
  • @AntP which fields? – celsowm Feb 14 at 16:31
  • There are two different Builder patterns, what @Tseng is talking about is not the GoF Builder. What you have here is very much the GoF Builder pattern, except that the different concrete builders are meant to be used to construct completely disparate objects - with different types, different internal structure; and that's not obvious in this toy example. In line with that, getProduct() is not a part of the builder interface (at least in statically typed languages). What's your concern regarding passing the builders by reference? – Filip Milovanović Feb 15 at 16:06
4
+100

The Other Builder Pattern

First, let me just briefly mention that there's a different pattern under the same name: Joshua Bloch's Builder pattern described in Effective Java (see the excerpt here). This pattern was inspired by the GoF Builder pattern; however, it's purpose/intent is not the same - it solves a different problem, so, despite what that article says, it is a separate pattern. Basically, Bloch Builder was designed to help create an object with an unwieldy constructor; it's essentially a factory with a fluent interface. In the article, Joshua Bloch describes it as a way to simulate optional named parameters, but it can also be seen as way to provide a small domain-specific language.

GoF Builder

What is it for?

The GoF book says that the intent is to "separate the construction of a complex object from its representation so that the same construction process can create different representations." Looking at this in the context of an evolving codebase, the assumption is that there's some (probably nontrivial) construction logic that needs to produce several structurally quite different representations, and that these two (construction logic, and the kinds of products) need to evolve independently. Nevertheless, there is some way to describe the construction process in a generalized manner (again, this is a starting assumption that justifies the application of the pattern).

To decouple the two, an abstraction is introduced: the Builder interface (or an abstract class). It captures the essence of this general way of thinking about the construction process, across all the different kinds of products. Essentially, it's an application of the Dependency Inversion Principle:

Component A        Component B
[Director]-------->[Product]

becomes

/------- Component A ------\           /-------  Component B --------\
[Director]-------->[Builder]<|---------[ConcreteBuilder]---->[Product]

For this to work, the Builder interface needs to remain relatively stable as the code evolves.

Now, I'm not sure how often GoF Builder comes up in practice; Bloch Builder seems to be used more. In any case, GoF Builder is worth knowing, and in the context of teaching, it can be used to discuss abstraction.

We need to go a bit more concrete

I'm going to make your example slightly more concrete in order to be able to better discuss certain points. It's going remain quite contrived, though. I don't normally code in PHP, so if something looks off, feel free to fix it. My changes here are geared towards demonstrating the points I want to make, but you may need to simplify/alter the code for teaching purposes - I'll leave that decision to you.

In my version, the product is an "animal farm". Let's say that you need to construct different representations of it from a string of this form (I'll refer to this as the "farm string"):

"Dogs: Fido, Apollo, Molly, Hunter, Daisy; Cows: Daisy, Rosie, Thor; Cats: Leo, Nala, Roxy"

This is just so that there's some nontrivial construction logic involved, to better motivate having the Director class. The string itself may come from a database (or some other source). This is a custom format, but in a more realistic scenario, it may be in some standard format, like JSON.

Essentially, the Director needs to interpret this string in order to reconstitute an object into some representation of an "animal farm". But it needs to do it in a generalized way - the construction algorithm should be independent of the structure of the product.

(BTW, this farm only supports Cows, Dogs and Cats. This is on purpose; these represent different "parts" of the conceptual product (the "farm"). The ability to support arbitrary kinds of animals is not the focus or the point of this discussion. Rather, the focus is on how to separate the generalized construction process from the implementation details of each product).

Now, suppose that you're developing this system, and that you've come up with the builder interface shown below. Its design reflects your understanding of the construction problem, and of the kinds of changes that are most/least likely (to be requested by clients) - perhaps you've settled on this interface after several iterations. So, the assumption is that this is general enough to support the most likely kind of future changes. I.e., it's unlikely that the interface itself will have to be changed; instead the requirements can be met by changing the ConcreteBuilder-s and/or the Director.

interface FarmBuilder {
    public function makeCow(string $name);
    public function makeDog(string $name);
    public function makeCat(string $name);
}

The Director will interpret the input "farm string" (it just splits it using various delimiters), and construct a product through the FarmBuilder interface in the following way:

class Director {

    private string $farmString;

    function __construct(string $farmString) {
        $this->farmString = $farmString;
    }

    public function createAnimalFarm(FarmBuilder &$builder) {
        $groups = explode(";", $this->farmString);

        foreach ($groups as $groupString) {
            // Identify group (Cows, Dogs, Cats)
            $temp = explode(":", $groupString);
            $groupKey = strtolower(trim($temp[0]));
            $groupAnimalsString = $temp[1];

            // Get the individual animals in the group
            $groupAnimals = explode(",", $groupAnimalsString);
            $groupAnimals = array_map(function($str) { return trim($str); }, $groupAnimals);

            switch ($groupKey) {
                case "cows":
                    foreach ($groupAnimals as $animalName) $builder->makeCow($animalName);
                    break;
                case "dogs":
                    foreach ($groupAnimals as $animalName) $builder->makeDog($animalName);
                    break;
                case "cats":
                    foreach ($groupAnimals as $animalName) $builder->makeCat($animalName);
                    break;
            }
        }
    }
}

So, the Director encapsulates some construction algorithm. Note that there could be other Director classes that a programmer can choose from; e.g., maybe there's one that uses JSON input, or a stream of binary data. Some of the Director-s could, in principle, be hardcoded as in your original code, that is not forbidden. But, if the requirement to keep the construction process decoupled from the products isn't really there to justify the complexity introduced by the pattern, then it's probably better to just provide a couple of static factory methods on the products themselves, that provide a way to create predefined instances.

"I've found some Java examples where the Director receives builder by parameter without any kind of return. On the other hand, I've also seen examples using the Director's constructor to receive a builder object"

Here, I've chosen to pass the builder as parameter to the createAnimalFarm method, because I wanted to be able to reuse the director with different concrete builders. Passing it via the constructor is another option. It would still be the same pattern - there's quite a bit of wiggle room when it comes to the exact implementation; what matters is the roles of the different elements and the overall relationships between them, and how it all works together to support the intent of the pattern. (This is why we have the notion of a concrete realization of a pattern.) The fact that I've chosen to pass the "farm string" via the constructor is also not relevant in terms of how the pattern is defined.

That is to say, design patterns don't prescribe every detail; there is no "one true way" to implement them.

The Products

The products usually have a complex structure (but they don't have to), in the sense that they may be somehow composed of other objects. The products can be very different, and serve different purposes, and so are likely to have dissimilar interfaces.
Let's say that these are the products that need to be built:

Product 1: An Instance of the Farm Class

class Animal { 
    public string $species; 
    public string $name; 
    function __construct($species, $name) {
        $this->species = $species;
        $this->name = $name;
    }
}

class Farm {
    private $animals = [];   // elements are Animal instances

    function __construct(array $animals) {
        foreach ($animals as $animal) {
            $this->animals[] = $animal;
        }
    }

    public function getRandomAnimal() : Animal {
            $count = count($this->animals);
            return $this->animals[rand(0, $count - 1)];
    }

    public function petRandomAnimal() : string {
        // Produces a string of the form:
        // "We're on the farm, and we're petting Hunter the Dog! Leo the Cat is jealous."
        // (omitted...)
    }
}

This is rather contrived, but the idea is to demonstrate that construction and/or modification interface of the product doesn't have to offer the same level of granularity as that of the Builder interface. Here, the Farm class is immutable, with a one-off constructor; however, the builder abstraction provides an incremental construction interface. The builder for this product is shown below:

class DefaultFarmBuilder implements FarmBuilder {
    private $animals = [];

    public function makeCow(string $name) {
        $this->animals[] = new Animal("Cow", $name);
    }

    public function makeDog(string $name) {
        $this->animals[] = new Animal("Dog", $name);
    }

    public function makeCat(string $name) {
        $this->animals[] = new Animal("Cat", $name);
    }

    public function getFarm(): Farm {
        return new Farm($this->animals);
    }
}

This particular builder essentially incrementally builds the parameter for the Farm constructor, and only constructs the farm object when the getFarm method is called (the "getProduct" method for this builder). This builder, as implemented, always returns a new instance, but the implementation can be changed to return the same instance every time, if that makes more sense.

Now, if client code needs to create and somehow manipulate a Farm object, it can do it like this:

// Construct a Farm using the algorithm represented by the Director class
$farmBuilder = new DefaultFarmBuilder();
$director->createAnimalFarm($farmBuilder);
$farm = $farmBuilder->getFarm();

// Use the farm object
echo $farm->petRandomAnimal();

// Output: 
// We're on the farm, and we're petting Hunter the Dog! Leo the Cat is jealous.

Product 2: An array of AnimalOwner instances

class AnimalOwner {
    private string $name;
    private $animals = [];

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

    public function addAnimal(string $species, string $name) {
        $this->animals[] = $name . " (" . $species . ")";
    }

    public function toString() : string {
        // Produces a string of the form:
        // "[Owner] owns [animals]."
        // (omitted...)
    }
}

Unlike the previous example, this class provides a way to add individual animals, and its builder can make use of that. However, to construct an array of these, processing of some special-purpose data is required (owner names, and ownership relationships); the idea is that this is specific to this usage (the other types of product don't need ownership data), so the abstract builder interface doesn't provide any support. This will be handled by the concrete builder:

class OwnerArrayBuilder implements FarmBuilder {
    private $ownershipMap = [];
    private $owners = [];
    private int $animalIndex = -1;

    function __construct(array $ownershipData) {
        // $ownershipData example: 
        // Given: "Dogs: Fido, Molly; Cats: Nala"  (F, M, N)
        // $ownershipData value of                           F  M  N
        // ["Owners" => ["Alice", "Bob"], "OwnershipMap" => [0, 1, 0]]
        // means that Fido and Nala belong to Alice (the owner at index 0), 
        // and that Molly belongs to Bob (the owner at index 1)

        $owners = $ownershipData["Owners"];
        foreach($owners as $owner) {
            $this->owners[] = new AnimalOwner($owner);
        }

        $this->ownershipMap = $ownershipData["OwnershipMap"];
    }

    public function makeCow(string $name) {
        $this->animalIndex++;
        $ownerIndex = $this->ownershipMap[$this->animalIndex];
        $this->owners[$ownerIndex]->addAnimal("Cow", $name);
    }

    public function makeDog(string $name) {
        $this->animalIndex++;
        $ownerIndex = $this->ownershipMap[$this->animalIndex];
        $this->owners[$ownerIndex]->addAnimal("Dog", $name);
    }

    public function makeCat(string $name) {
        $this->animalIndex++;
        $ownerIndex = $this->ownershipMap[$this->animalIndex];
        $this->owners[$ownerIndex]->addAnimal("Cat", $name);
    }

    public function getOwners(): array {
        return $this->owners;
    }
}

So, here, the builder is provided with additional data (perhaps it came from the same database as the "farm string"). In this case, the resulting array and the AnimalOwner objects are created immediately, and each of the builder functions (makeCow, makeDog, makeCat) calls the addAnimal() function on those instances. The "getProduct" function, here called getOwners, returns a completely different type; this is why it isn't a part of the abstract builder interface - the products aren't unified. Client code creates the concrete builder anyway, so it can use it to retrieve the product:

// Ownership data imported from somewhere:
$ownershipData = ["Owners" => ["Alice", "Bob"], "OwnershipMap" => [0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1]];

// Construct the AnimalOwner array using the algorithm represented by the Director class
$ownerArrayBuilder = new OwnerArrayBuilder($ownershipData);
$director->createAnimalFarm($ownerArrayBuilder);
$owners = $ownerArrayBuilder->getOwners();

// use the owners array:
$ownerStrings = array_map(function($owner) {return $owner->toString();}, $owners);
echo join("\n", $ownerStrings);

// Output: 
// Alice owns Fido (Dog), Apollo (Dog), Molly (Dog), Hunter (Dog), Daisy (Cow) and Leo (Cat).
// Bob owns Daisy (Dog), Rosie (Cow), Thor (Cow), Nala (Cat) and Roxy (Cat).

Product 3: An HTML Table that Summarizes the Animals on the Farm

This product is not a custom, user-defined class. Instead, what needs to be created is a DOMDocument that represents an HTML table of the following form:

+------+----------+
| Cows | #numCows |
+------+----------+
| Dogs | #numDogs |
+------+----------+
| Cats | #numCats |
+------+----------+

In this case, the builder essentially provides a special-purpose construction mechanism over the general DOMDocument/DOMElement APIs:

class HtmlSummaryTableBuilder implements FarmBuilder {
    private DOMDocument $dom;
    private $countCells = [];
    private $groups = ['Cows', 'Dogs', 'Cats'];

    function __construct() {

        $this->dom = new DOMDocument();
        $table = $this->dom->createElement('table');

        foreach ($this->groups as $group) {
            $tableRow = $this->dom->createElement('tr');
            $tableCell_Group = $this->dom->createElement('td', $group);
            $tableCell_Count = $this->dom->createElement('td');

            $this->countCells[$group] = ["Cell" => $tableCell_Count, "Count" => 0];

            $tableRow->appendChild($tableCell_Group);
            $tableRow->appendChild($tableCell_Count);
            $table->appendChild($tableRow);
        }

        $this->dom->appendChild($table);
    }

    public function makeCow(string $name) {
        $this->countCells["Cows"]["Count"]++;
    }

    public function makeDog(string $name) {
        $this->countCells["Dogs"]["Count"]++;
    }

    public function makeCat(string $name) {
        $this->countCells["Cats"]["Count"]++;
    }

    public function getTable(): DOMDocument {
        foreach ($this->groups as $group) {
            $this->countCells[$group]["Cell"]->appendChild(
                $this->dom->createTextNode(strval($this->countCells[$group]["Count"])));
        }
        return $this->dom;
    }

}

Aside: Given that the table only contains the count, the builder ignores animal names, even though it receives them. The idea is that the designer(s) had reason to include the support for the name in the builder interface. It seems that the names are used by most product types, and perhaps our hypothetical developers anticipated, based on their experience with the project so far, that future changes will generally make use of the name as well.

Client code can then create the DOMDocument like this:

$tableBuilder = new HtmlSummaryTableBuilder();
$director->createAnimalFarm($tableBuilder);

// obtain and use the table
echo $tableBuilder->getTable()->saveHTML();
// Output:
<table>
  <tr><td>Cows</td><td>3</td></tr>
  <tr><td>Dogs</td><td>5</td></tr>
  <tr><td>Cats</td><td>3</td></tr>
</table>

Summary

Hopefully, this served to show that concrete builders don't have to all follow the same form; they can do very different things. The key abstraction here is the generalized construction mechanism. The Director and the concrete Builders depend on it, which makes them decoupled - as long as this interface is expressive enough to support the construction of various products, while itself being stable in the face of change. In general, the different concrete builders can come from different modules, and can be developed independently (by different individuals/teams). Both the construction algorithm and the product builder can vary independently. Different Director-s can be defined, as long as they too can make use of the abstract builder interface. E.g., new Director-s could be added in order to support a better construction algorithm, a different input format, or to provide predefined "preset" objects.

P.S. Here's the entire example in a PHP Sandbox.

0

Your is not a proper way for builder pattern in PHP. Here is the example for Builder pattern in PHP,

<?php

namespace RefactoringGuru\Builder\Conceptual;

/**
 * The Builder interface specifies methods for creating the different parts of
  * the Product objects.
 */

interface Builder
{
     public function producePartA(): void;

     public function producePartB(): void;

     public function producePartC(): void;
}

/**
 * The Concrete Builder classes follow the Builder interface and provide
 * specific implementations of the building steps. Your program may have several
 * variations of Builders, implemented differently.
 */

 class ConcreteBuilder1 implements Builder
{
    private $product;

    /**
     * A fresh builder instance should contain a blank product object, which is
     * used in further assembly.
     */
     public function __construct()
    {
        $this->reset();
    }

     public function reset(): void
    {
        $this->product = new Product1;
    }

    /**
     * All production steps work with the same product instance.
     */
    public function producePartA(): void
    {
        $this->product->parts[] = "PartA1";
    }

    public function producePartB(): void
    {
        $this->product->parts[] = "PartB1";
    }

    public function producePartC(): void
    {
        $this->product->parts[] = "PartC1";
    }

    /**
     * Concrete Builders are supposed to provide their own methods for
     * retrieving results. That's because various types of builders may create
     * entirely different products that don't follow the same interface.
     * Therefore, such methods cannot be declared in the base Builder interface
     * (at least in a statically typed programming language). Note that PHP is a
     * dynamically typed language and this method CAN be in the base interface.
     * However, we won't declare it there for the sake of clarity.
     *
     * Usually, after returning the end result to the client, a builder instance
     * is expected to be ready to start producing another product. That's why
     * it's a usual practice to call the reset method at the end of the
     * `getProduct` method body. However, this behavior is not mandatory, and
     * you can make your builders wait for an explicit reset call from the
     * client code before disposing of the previous result.
     */
     public function getProduct(): Product1
    {
        $result = $this->product;
        $this->reset();

        return $result;
    }
}

/**
 * It makes sense to use the Builder pattern only when your products are quite
 * complex and require extensive configuration.
 *
 * Unlike in other creational patterns, different concrete builders can produce
 * unrelated products. In other words, results of various builders may not
 * always follow the same interface.
 */
 class Product1
{
     public $parts = [];

    public function listParts(): void
    {
        echo "Product parts: " . implode(', ', $this->parts) . "\n\n";
    }
}

/**
 * The Director is only responsible for executing the building steps in a
 * particular sequence. It is helpful when producing products according to a
 * specific order or configuration. Strictly speaking, the Director class is
 * optional, since the client can control builders directly.
 */
 class Director
{
    /**
     * @var Builder
     */
     private $builder;

    /**
     * The Director works with any builder instance that the client code passes
     * to it. This way, the client code may alter the final type of the newly
     * assembled product.
     */
    public function setBuilder(Builder $builder): void
    {
        $this->builder = $builder;
    }

    /**
     * The Director can construct several product variations using the same
     * building steps.
     */
    public function buildMinimalViableProduct(): void
    {
        $this->builder->producePartA();
    }

    public function buildFullFeaturedProduct(): void
    {
        $this->builder->producePartA();
        $this->builder->producePartB();
        $this->builder->producePartC();
    }
}

/**
 * The client code creates a builder object, passes it to the director and then
 * initiates the construction process. The end result is retrieved from the
 * builder object.
 */
 function clientCode(Director $director)
{
     $builder = new ConcreteBuilder1;
    $director->setBuilder($builder);

     echo "Standard basic product:\n";
    $director->buildMinimalViableProduct();
     $builder->getProduct()->listParts();

    echo "Standard full featured product:\n";
    $director->buildFullFeaturedProduct();
    $builder->getProduct()->listParts();

    // Remember, the Builder pattern can be used without a Director class.
    echo "Custom product:\n";
    $builder->producePartA();
    $builder->producePartC();
    $builder->getProduct()->listParts();
}

 $director = new Director;
 clientCode($director);

Reference: https://refactoring.guru/design-patterns/builder/php/example

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.