3.3. Composite

3.3.1. Intent

According to the Gang of Four, the Composite pattern is a way to “compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly” (Design Patterns: Elements of Reusable Object-Oriented Software, 2013, p. 163).

3.3.2. When to use it?

The composite pattern should be used when you don’t want to expose the difference between compositions of objects and individual objects.

3.3.3. Diagram

Created using PhpStorm and yFiles.

Class diagram of the Composite pattern

3.3.4. Implementation

ShipmentElementInterface.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php

namespace Phpatterns\Structural\Composite;

interface ShipmentElementInterface
{
    /**
     * Add an element to the collection of elements
     * @param ShipmentElementInterface $element
     */
    public function addElement(ShipmentElementInterface $element);

    /**
     * Remove an element from the collection of elements
     * @param ShipmentElementInterface $element
     */
    public function removeElement(ShipmentElementInterface $element);

    /**
     * Get the number of elements in the collection
     * @return int
     */
    public function getItemsCount();

    /**
     * Get the price of the element or the total price of the elements in the collection
     * @return float
     */
    public function getPrice();
}

Element.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<?php

namespace Phpatterns\Structural\Composite\ShipmentElement;

use Phpatterns\Structural\Composite;

class Element implements Composite\ShipmentElementInterface
{
    /** @var float */
    private $price;

    /**
     * @param float $price
     */
    public function __construct($price)
    {
        $this->price = $price;
    }

    /**
     * Add an element to the collection of elements
     * @param Composite\ShipmentElementInterface $element
     * @throws \Exception
     */
    public function addElement(Composite\ShipmentElementInterface $element)
    {
        throw new \Exception('Action not supported!');
    }

    /**
     * Remove an element from the collection of elements
     * @param Composite\ShipmentElementInterface $element
     * @throws \Exception
     */
    public function removeElement(Composite\ShipmentElementInterface $element)
    {
        throw new \Exception('Action not supported!');
    }

    /**
     * Get the number of elements in the collection (just one here)
     * @return int
     */
    public function getItemsCount()
    {
        return 1;
    }

    /**
     * Get the price of the element
     * @return float
     */
    public function getPrice()
    {
        return $this->price;
    }
}

Box.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<?php

namespace Phpatterns\Structural\Composite\ShipmentElement;

use Phpatterns\Structural\Composite;

class Box implements Composite\ShipmentElementInterface
{
    /** @var Composite\ShipmentElementInterface[] */
    private $elements;

    public function __construct()
    {
        $this->elements = [];
    }

    /**
     * @return Composite\ShipmentElementInterface[]
     */
    public function getElements()
    {
        return $this->elements;
    }

    /**
     * Add an element to the collection of elements
     * @param Composite\ShipmentElementInterface $element
     */
    public function addElement(Composite\ShipmentElementInterface $element)
    {
        if (! in_array($element, $this->elements, true)) {
            $this->elements[] = $element;
        }
    }

    /**
     * Remove an element from the collection of elements
     * @param Composite\ShipmentElementInterface $element
     */
    public function removeElement(Composite\ShipmentElementInterface $element)
    {
        $key = array_search($element, $this->elements, true);
        if ($key !== false) {
            unset($this->elements[$key]);
        }
    }

    /**
     * Get the number of elements in the collection
     * @return int
     */
    public function getItemsCount()
    {
        return count($this->elements);
    }

    /**
     * Get the total price of the elements in the collection
     * @return float
     */
    public function getPrice()
    {
        $totalPrice = 0.0;
        foreach ($this->elements as $element) {
            $totalPrice += $element->getPrice();
        }
        return $totalPrice;
    }
}

Pallet.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
<?php

namespace Phpatterns\Structural\Composite\ShipmentElement;

use Phpatterns\Structural\Composite;

class Pallet implements Composite\ShipmentElementInterface
{
    /** @var Composite\ShipmentElementInterface[] */
    private $elements;

    public function __construct()
    {
        $this->elements = [];
    }

    /**
     * @return Composite\ShipmentElementInterface[]
     */
    public function getElements()
    {
        return $this->elements;
    }

    /**
     * Add an element to the collection of elements
     * @param Composite\ShipmentElementInterface $element
     */
    public function addElement(Composite\ShipmentElementInterface $element)
    {
        if (! in_array($element, $this->elements, true)) {
            $this->elements[] = $element;
        }
    }

    /**
     * Remove an element from the collection of elements
     * @param Composite\ShipmentElementInterface $element
     */
    public function removeElement(Composite\ShipmentElementInterface $element)
    {
        $key = array_search($element, $this->elements, true);
        if ($key !== false) {
            unset($this->elements[$key]);
        }
    }

    /**
     * Get the number of elements in the collection
     * @return int
     */
    public function getItemsCount()
    {
        $totalElements = 0;
        foreach ($this->elements as $element) {
            $totalElements += $element->getItemsCount();
        }
        return $totalElements;
    }

    /**
     * Get the total price of the elements in the collection
     * @return float
     */
    public function getPrice()
    {
        $totalPrice = 0.0;
        foreach ($this->elements as $element) {
            $totalPrice += $element->getPrice();
        }
        return $totalPrice;
    }
}

Shipment.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?php

namespace Phpatterns\Structural\Composite;

class Shipment
{
    /** @var ShipmentElementInterface[] */
    private $elements;

    /**
     * @param ShipmentElementInterface[] $elements
     */
    public function __construct(array $elements)
    {
        $this->elements = $elements;
    }

    /**
     * Get the number of elements in the Shipment
     * @return int
     */
    public function getItemsCount()
    {
        $total = 0;
        foreach ($this->elements as $element) {
            $total += $element->getItemsCount();
        }
        return $total;
    }

    /**
     * Get the total price of the elements in the Shipment
     * @return float
     */
    public function getValue()
    {
        $totalValue = 0.0;
        foreach ($this->elements as $element) {
            $totalValue += $element->getPrice();
        }
        return $totalValue;
    }
}

3.3.5. Tests

CompositeTest.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
<?php

namespace Test\Phpatterns\Structural\Composite;

use Phpatterns\Structural\Composite;
use Phpatterns\Structural\Composite\ShipmentElement;

class CompositeTest extends \PHPUnit_Framework_TestCase
{
    public function testAddAndRemoveElementsToCollection()
    {
        $pallet = new ShipmentElement\Pallet();
        $element = new ShipmentElement\Element(1.50);

        $pallet->addElement($element);
        $this->assertSame(
            $element,
            $pallet->getElements()[0]
        );

        $pallet->removeElement($element);
        $this->assertEmpty($pallet->getElements());
    }

    public function testTotalElementsCount()
    {
        $box = new ShipmentElement\Box();
        $box->addElement(new ShipmentElement\Element(13.22));

        $box2 = new ShipmentElement\Box();
        $box2->addElement(new ShipmentElement\Element(14.50));
        $box2->addElement(new ShipmentElement\Element(54.67));

        $pallet = new ShipmentElement\Pallet();
        $pallet->addElement($box2);

        $shipment = new Composite\Shipment(
            [
                new ShipmentElement\Element(25.90),
                $box,
                $pallet
            ]
        );

        $this->assertSame(
            4,
            $shipment->getItemsCount()
        );
    }

    public function testTotalElementsValue()
    {
        $box = new ShipmentElement\Box();
        $box->addElement(new ShipmentElement\Element(42.33));

        $box2 = new ShipmentElement\Box();
        $box2->addElement(new ShipmentElement\Element(22));
        $box2->addElement(new ShipmentElement\Element(123.0));

        $pallet = new ShipmentElement\Pallet();
        $pallet->addElement($box2);

        $shipment = new Composite\Shipment(
            [
                new ShipmentElement\Element(15.90),
                new ShipmentElement\Element(9),
                $box,
                $pallet
            ]
        );

        $this->assertSame(
            212.23,
            $shipment->getValue()
        );
    }

    public function testInterfaceImplementation()
    {
        //individual element (Element)
        $this->assertInstanceOf(
            Composite\ShipmentElementInterface::class,
            new ShipmentElement\Element(3.30)
        );

        //composition of elements (Box)
        $this->assertInstanceOf(
            Composite\ShipmentElementInterface::class,
            new ShipmentElement\Box()
        );

        //composition of elements (Pallet)
        $this->assertInstanceOf(
            Composite\ShipmentElementInterface::class,
            new ShipmentElement\Pallet()
        );
    }
}