1

For studying purpose, I've tried to create a simple PHP implementation of Builder GoF (not the Joshua Bloch's one) inspired on this slide.

The main goal of this example is to encapsulate the construction of data files from diferents inputs (Directors) and outputs (Builders)

Lots of Builder example (including the wikipedia's one) define an explicit "getProduct/getResult" on the Builder Class.

In my case, I thought the creation of output file would be considered the literal product, so I have opted to not use a method to get the file.

So, I would like to know if the absence of this method would invalidate it as Builder GoF implementation.

<?php
namespace BuilderGoFEnglish;
abstract class Builder {
    protected \SplFileObject $output;
    public function __construct(string $outputFileName) {
        $this->output = new \SplFileObject($outputFileName, 'w');
    }
    abstract function addHeader(array $header);
    abstract function addLine(array $linha);
    abstract function finish();
}
abstract class Director {
    protected Builder $builder;
    public function __construct(Builder $builder) {
        $this->builder = $builder;
    }
    public abstract function build(string $inputFileName);
}
class XmlDirector extends Director {
    public function build(string $inputFileName) {
        $document = new \DOMDocument();
        $document->preserveWhiteSpace = false;
        $document->load($inputFileName);
        $root = $document->firstChild;
        $item1 = iterator_to_array($root->firstChild->childNodes);
        $this->builder->addHeader(array_column($item1, 'tagName'));
        foreach($root->childNodes as $child){
            $item = iterator_to_array($child->childNodes);
            $this->builder->addLine(array_column($item1, 'nodeValue'));
        }
        $this->builder->finish();
    }
}
class JsonDirector extends Director {
    public function build(string $inputFileName) {
        $jsonArray = json_decode(file_get_contents($inputFileName));
        $this->builder->addHeader(array_keys((array) $jsonArray[0]));
        foreach ($jsonArray as $jsonObject) {
            $this->builder->addLine((array) $jsonObject);
        }
        $this->builder->finish();
    }
}
class HtmlBuilder extends Builder {
    private \DOMDocument $document;
    private \DOMElement $table;
    public function __construct(string $output) {
        parent::__construct($output);
        $this->document = new \DOMDocument('1.0', 'utf-8');
        $this->document->appendChild($this->document->createElement('html'));
        $this->table = $this->document->createElement('table');
        $this->table->setAttribute('border', 1);
        $this->document->firstChild->appendChild($this->table);
    }
    private function createTableRow(array $linha, $tipo = 'td'){
        $tr = $this->document->createElement('tr');
        array_map(fn($v) =>
                $tr->appendChild($this->document->createElement($tipo, $v)),
                $linha);
        $this->table->appendChild($tr);
    }
    public function addHeader(array $header) {
        $this->createTableRow($header, 'th');
    }
    public function addLine(array $linha) {
        $this->createTableRow($linha);
    }
    public function finish() {
        $this->output->fwrite($this->document->saveHTML());
    }
}
class CsvBuilder extends Builder {
    private array $csvArray = [];
    public function addHeader(array $header) {
        $this->csvArray[] = $header;
    }
    public function addLine(array $linha) {
        $this->csvArray[] = $linha;
    }
    public function finish() {
        foreach ($this->csvArray as $linha) {
            $this->output->fputcsv($linha);
        }
    }
}

1 Answer 1

3

The builder pattern is meant to be a nicer way to construct an object that requires many or complex dependencies. It is only at the end of the builder that the object is created. If an exception gets thrown before calling the final "build" method, the object never gets constructed.

The first violation of the builder pattern I see in your code is that calls to the builder actually write to a file. If an exception gets thrown mid steam then you end up with a partially written file. Syntax aware file formats, like HTML, XML and JSON can cause errors in your application when being parsed with partial data. This essentially leaves your application in an unknown state, which should be avoided.

If you modified the code to write the file on the call to finish that would help. You'll get a correct file or no file at all. Either the program works completely, or not at all.

If you did this, it gets closer. To be honest the builder should return the data structure (the XML, HTML or JSON) as a tree of objects or a string, and delegates the writing of the file to the caller. Writing to a file might require more error handling. This is personal opinion, but I don't like using builders that interact with resources outside my current process. Writing to a file involves out-of-process calls, and I usually want some extra error handling around that, and a clear visual separation between prep work and doing the work.

1

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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