/ Gists / 19. Composite
On gists

19. Composite

PHP Patterns

pic.md Raw #

  • Your leaf nodes and composites must implement the same component interface.
  • Your composite classes must have methods to both add and remove leaf nodes.
  • To treat a group of objects the same way as a single instance of the object.

composite1.php Raw #

<?php

abstract class Component
{
    /**
     * @var Component
     */
    protected $parent;

    public function setParent(Component $parent)
    {
        $this->parent = $parent;
    }

    public function getParent(): Component
    {
        return $this->parent;
    }

    public function add(Component $component): void { }

    public function remove(Component $component): void { }

    public function isComposite(): bool
    {
        return false;
    }

    abstract public function operation(): string;
}


class Leaf extends Component
{
    public function operation(): string
    {
        return "Leaf";
    }
}


class Composite extends Component
{
    /**
     * @var \SplObjectStorage
     */
    protected $children;
    
    public function __construct()
    {
        $this->children = new \SplObjectStorage;
    }

    public function add(Component $component): void
    {
        $this->children->attach($component);
        $component->setParent($this);
    }

    public function remove(Component $component): void
    {
        $this->children->detach($component);
        $component->setParent(null);
    }

    public function isComposite(): bool
    {
        return true;
    }
    
   public function operation(): string
    {
        $results = [];
        foreach ($this->children as $child) {
            $results[] = $child->operation();
        }

        return "Branch(" . implode("+", $results) . ")";
    }
}
    
    
// USAGE
function clientCode(Component $component)
{
    echo "RESULT: " . $component->operation();.
}


/**
 * This way the client code can support the simple leaf components...
 */
$simple = new Leaf;
echo "Client: I've got a simple component:\n";
clientCode($simple);
echo "\n\n";

/**
 * ...as well as the complex composites.
 */
$tree = new Composite;
$branch1 = new Composite;
$branch1->add(new Leaf);
$branch1->add(new Leaf);
$branch2 = new Composite;
$branch2->add(new Leaf);
$tree->add($branch1);
$tree->add($branch2);
echo "Client: Now I've got a composite tree:\n";
clientCode($tree);
echo "\n\n";

/**
 * Thanks to the fact that the child-management operations are declared in the
 * base Component class, the client code can work with any component, simple or
 * complex, without depending on their concrete classes.
 */
function clientCode2(Component $component1, Component $component2)
{
    // ...

    if ($component1->isComposite()) {
        $component1->add($component2);
    }
    echo "RESULT: " . $component1->operation();

    // ...
}

echo "Client: I don't need to check the components classes even when managing the tree:\n";
clientCode2($tree, $simple);

/*
Client: I get a simple component:
RESULT: Leaf

Client: Now I get a composite tree:
RESULT: Branch(Branch(Leaf+Leaf)+Branch(Leaf))

Client: I don't need to check the components classes even when managing the tree::
RESULT: Branch(Branch(Leaf+Leaf)+Branch(Leaf)+Leaf)
*/
    

composite-real-example.php Raw #

<?php
/**
 * The base Component class declares an interface for all concrete components,
 * both simple and complex.
 *
 * In our example, we'll be focusing on the rendering behavior of DOM elements.
 */
abstract class FormElement
{
    /**
     * We can anticipate that all DOM elements require these 3 fields.
     */
    protected $name;
    protected $title;
    protected $data;

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

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

    public function setData($data): void
    {
        $this->data = $data;
    }

    public function getData(): array
    {
        return $this->data;
    }

    /**
     * Each concrete DOM element must provide its rendering implementation, but
     * we can safely assume that all of them are returning strings.
     */
    abstract public function render(): string;
}

/**
 * This is a Leaf component. Like all the Leaves, it can't have any children.
 */
class Input extends FormElement
{
    private $type;

    public function __construct(string $name, string $title, string $type)
    {
        parent::__construct($name, $title);
        $this->type = $type;
    }

    /**
     * Since Leaf components don't have any children that may handle the bulk of
     * the work for them, usually it is the Leaves who do the most of the heavy-
     * lifting within the Composite pattern.
     */
    public function render(): string
    {
        return "<label for=\"{$this->name}\">{$this->title}</label>\n" .
            "<input name=\"{$this->name}\" type=\"{$this->type}\" value=\"{$this->data}\">\n";
    }
}

/**
 * The base Composite class implements the infrastructure for managing child
 * objects, reused by all Concrete Composites.
 */
abstract class FieldComposite extends FormElement
{
    /**
     * @var FormElement[]
     */
    protected $fields = [];

    /**
     * The methods for adding/removing sub-objects.
     */
    public function add(FormElement $field): void
    {
        $name = $field->getName();
        $this->fields[$name] = $field;
    }

    public function remove(FormElement $component): void
    {
        $this->fields = array_filter($this->fields, function ($child) use ($component) {
            return $child != $component;
        });
    }

    /**
     * Whereas a Leaf's method just does the job, the Composite's method almost
     * always has to take its sub-objects into account.
     *
     * In this case, the composite can accept structured data.
     *
     * @param array $data
     */
    public function setData($data): void
    {
        foreach ($this->fields as $name => $field) {
            if (isset($data[$name])) {
                $field->setData($data[$name]);
            }
        }
    }

    /**
     * The same logic applies to the getter. It returns the structured data of
     * the composite itself (if any) and all the children data.
     */
    public function getData(): array
    {
        $data = [];
        
        foreach ($this->fields as $name => $field) {
            $data[$name] = $field->getData();
        }
        
        return $data;
    }

    /**
     * The base implementation of the Composite's rendering simply combines
     * results of all children. Concrete Composites will be able to reuse this
     * implementation in their real rendering implementations.
     */
    public function render(): string
    {
        $output = "";
        
        foreach ($this->fields as $name => $field) {
            $output .= $field->render();
        }
        
        return $output;
    }
}

/**
 * The fieldset element is a Concrete Composite.
 */
class Fieldset extends FieldComposite
{
    public function render(): string
    {
        // Note how the combined rendering result of children is incorporated
        // into the fieldset tag.
        $output = parent::render();
        
        return "<fieldset><legend>{$this->title}</legend>\n$output</fieldset>\n";
    }
}

/**
 * And so is the form element.
 */
class Form extends FieldComposite
{
    protected $url;

    public function __construct(string $name, string $title, string $url)
    {
        parent::__construct($name, $title);
        $this->url = $url;
    }

    public function render(): string
    {
        $output = parent::render();
        return "<form action=\"{$this->url}\">\n<h3>{$this->title}</h3>\n$output</form>\n";
    }
}

/**
 * The client code gets a convenient interface for building complex tree
 * structures.
 */
function getProductForm(): FormElement
{
    $form = new Form('product', "Add product", "/product/add");
    $form->add(new Input('name', "Name", 'text'));
    $form->add(new Input('description', "Description", 'text'));

    $picture = new Fieldset('photo', "Product photo");
    $picture->add(new Input('caption', "Caption", 'text'));
    $picture->add(new Input('image', "Image", 'file'));
    $form->add($picture);

    return $form;
}

/**
 * The form structure can be filled with data from various sources. The Client
 * doesn't have to traverse through all form fields to assign data to various
 * fields since the form itself can handle that.
 */
function loadProductData(FormElement $form)
{
    $data = [
        'name' => 'Apple MacBook',
        'description' => 'A decent laptop.',
        'photo' => [
            'caption' => 'Front photo.',
            'image' => 'photo1.png',
        ],
    ];

    $form->setData($data);
}

/**
 * The client code can work with form elements using the abstract interface.
 * This way, it doesn't matter whether the client works with a simple component
 * or a complex composite tree.
 */
function renderProduct(FormElement $form)
{
    // ..

    echo $form->render();

    // ..
}

$form = getProductForm();
loadProductData($form);
renderProduct($form);


/*
<form action="/product/add">
<h3>Add product</h3>
<label for="name">Name</label>
<input name="name" type="text" value="Apple MacBook">
<label for="description">Description</label>
<input name="description" type="text" value="A decent laptop.">
<fieldset><legend>Product photo</legend>
<label for="caption">Caption</label>
<input name="caption" type="text" value="Front photo.">
<label for="image">Image</label>
<input name="image" type="file" value="photo1.png">
</fieldset>
</form>
*/

composite2.php Raw #

<?php

interface Renderable
{
    public function render(): string;
}


/**
 * The composite node MUST extend the component contract. This is mandatory for building
 * a tree of components.
 */
class Form implements Renderable
{
    /**
     * @var Renderable[]
     */
    private array $elements;

    /**
     * runs through all elements and calls render() on them, then returns the complete representation
     * of the form.
     *
     * from the outside, one will not see this and the form will act like a single object instance
     */
    public function render(): string
    {
        $formCode = '<form>';

        foreach ($this->elements as $element) {
            $formCode .= $element->render();
        }

        $formCode .= '</form>';

        return $formCode;
    }

    public function addElement(Renderable $element)
    {
        $this->elements[] = $element;
    }
}   


class InputElement implements Renderable
{
    public function render(): string
    {
        return '<input type="text" />';
    }
}


class TextElement implements Renderable
{
    private string $text;

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

    public function render(): string
    {
        return $this->text;
    }
}


// USAGE
  function testRender()
  {
        $form = new Form();
        $form->addElement(new TextElement('Email:'));
        $form->addElement(new InputElement());
        $embed = new Form();
        $embed->addElement(new TextElement('Password:'));
        $embed->addElement(new InputElement());
        $form->addElement($embed);

        // This is just an example, in a real world scenario it is important to remember that web browsers do not
        // currently support nested forms

        $this->assertSame(
            '<form>Email:<input type="text" /><form>Password:<input type="text" /></form></form>',
            $form->render()
        );
  }

composite-vrana.php Raw #

<?php
abstract class Database {
    protected $connection;
    function __construct(Connection $connection) {
        $this->connection = $connection;
    }
    abstract function getTables();
    function getVals($query) {
        $this->connection->query($query);
        // ...
    }
}

class DatabaseMysql extends Database {
    function getTables() {
        return $this->getVals("SHOW TABLES");
    }
}

interface Connection {
    function query($query);
}

class ConnectionMysql implements Connection {
    function query($query) {
        return mysql_query($query);
    }
}

$connection = new ConnectionMysql;
$database = new DatabaseMysql($connection);
$connection->query("");
$database->getTables();
$database->getVals("");

composite3.php Raw #

<?php

abstract class ComponentInterface
{
    public $id;
    public $name;
 
    abstract public function getFile($offset = 0);
    abstract public function addFile(ComponentInterface $file);
    abstract public function removeFile(ComponentInterface $file);
 
    public function __construct($id, $name)
    {
        $this->id = $id;
        $this->name = $name;
    }
}
 
class DirectoryComponent extends ComponentInterface
{
    private $files = [];
 
    public function getFile($offset = 0)
    {
        $content = str_repeat("-", $offset) . ' ' . $this->name . "/\n";
 
        foreach ($this->files as $file) {
            $content .= $file->getFile(++$offset);
        }
 
        return $content;
    }
 
    public function addFile(ComponentInterface $file)
    {
        $this->files[$file->id] = $file;
 
        return $this;
    }
 
    public function removeFile(ComponentInterface $file)
    {
        unset($this->files[$file->id]);
    }
}
 
class FileComponent extends ComponentInterface
{
    public function getFile($offset = 0)
    {
        return str_repeat("-", $offset-1) . '> ' . $this->name . "\n";
    }
 
    public function addFile(ComponentInterface $file)
    {
        return false;
    }
 
    public function removeFile(ComponentInterface $file)
    {
        return false;
    }
}
 
$root = new DirectoryComponent(1, "/");
 
$etc = new DirectoryComponent(2, "etc");
$var = new DirectoryComponent(3, "var");
 
$root->addFile($etc)->addFile($var);
 
$etc->addFile(
    new FileComponent(2, "php.ini", "simon")
);
$var->addFile(
    new FileComponent(3, "nginx.log", "simon")
);
$var->addFile(
    new FileComponent(3, "hadoop.log", "simon")
);
 
echo $root->getFile();


/*
/
– etc
-> php.ini
– var
-> nginx.log
-> hadoop.log
*/

composite-nette.php Raw #

<?php

// dedicnost vs kompozice

// dedicnost
class PersonForm extends Nette\Application\UI\Form
{
	public function __construct()
	{
		parent::__construct();

		$this->addText('name', 'Jméno');
		$this->addSubmit('submit', 'Odeslat');
	}
}

class PersonWithAddressForm extends PersonForm
{
	public function __construct()
	{
		parent::__construct();

		$this->addText('city', 'Město');
		$this->addText('street', 'Ulice');
	}
}


// kompozice
class PersonContainer extends Nette\Forms\Container
{
	public function __construct()
	{
		parent::__construct();

		$this->addText('name', 'Jméno');
		$this->addText('surname', 'Příjmení');
	}
}


class AddressContainer extends Nette\Forms\Container
{
	public function __construct()
	{
		parent::__construct();

		$this->addText('city', 'Město');
		$this->addText('street', 'Ulice');
	}
}


// USAGE
class MyPresenter extends BasePresenter
{
	protected function createComponentPersonWithAddressForm()
	{
		$form = new Nette\Application\UI\Form;
		$form['user'] = new PersonContainer();
		$form['user']['address'] = new AddressContainer();
		$form['user']['correspondence'] = new AddressContainer();
		$form['user']['billing'] = new AddressContainer();

		$form->addSubmit('submit', 'Odeslat');
		$form->onSuccess[] = function () {};

		return $form;
	}
}

// nebo tovarna
class MyPresenter extends BasePresenter
{	
	public function __construct()		protected function createComponentPersonWithAddressForm()
	{		{
		parent::__construct();			$form = new Nette\Application\UI\Form;
		$form['user'] = new PersonContainer();
		$form['user']['address'] = new AddressContainer();
		$form['user']['correspondence'] = new AddressContainer();
		$form['user']['billing'] = new AddressContainer();


		$this['user'] = new PersonContainer();			$form->addSubmit('submit', 'Odeslat');
		$this['user']['address'] = new AddressContainer();			$form->onSuccess[] = function () {};
		$this['user']['correspondence'] = new AddressContainer();	
		$this['user']['billing'] = new AddressContainer();	


		$this->addSubmit('submit', 'Odeslat');			return $form;
	}		}
}