Skip to content

Composite

The Composite Pattern is a structural design pattern that composes objects into tree structures to represent part-whole hierarchies. It allows clients to treat individual objects (leaves) and groups of objects (composites) uniformly.


Structure

Role Example Responsibility
Component GUIComponent Declares the common interface for leaves and composites
Leaf Button, Label Represents end objects with no children
Composite Panel Stores child components and delegates operations to them
Client Any class Works with components through the common interface

Steps

  1. Create a Component interface
  2. Implement Leaf classes representing individual objects
  3. Implement a Composite class that stores child components
  4. Allow clients to interact with all components through the same interface
classDiagram
    class GUIComponent {
        <<interface>>
        +render(indent: int)
        +getWidth(): int
        +getHeight(): int
    }

    class Button {
        -label: string
        +render(indent: int)
        +getWidth(): int
        +getHeight(): int
    }

    class Label {
        -text: string
        +render(indent: int)
        +getWidth(): int
        +getHeight(): int
    }

    class Panel {
        -children: GUIComponent[]
        +add(GUIComponent)
        +remove(GUIComponent)
        +render(indent: int)
        +getWidth(): int
        +getHeight(): int
    }

    Button <|.. GUIComponent
    Label <|.. GUIComponent
    Panel <|.. GUIComponent
    Panel o--> GUIComponent : contains

The key idea is that both individual objects (like Button, Label) and containers (like Panel) implement the same interface (like GUIComponent), allowing the client to treat them uniformly.


Example: GUI Component Tree

A Panel can contain:

  • Buttons
  • Labels
  • other Panels

Component Interface

GUIComponent.php
<?php

declare(strict_types=1);

namespace DesignPattern\Structural\Composite\GUI;

interface GUIComponent
{
    public function render(): string;
    public function move(int $x, int $y): string;
}

Leaves

Button.php
<?php

declare(strict_types=1);

namespace DesignPattern\Structural\Composite\GUI;

class Button implements GUIComponent
{
    public function __construct(
        private readonly string $name,
        private int $x = 0,
        private int $y = 0
    ) {
    }

    public function render(): string
    {
        return "Rendering Button: {$this->name} at ({$this->x}, {$this->y})\n";
    }

    public function move(int $x, int $y): string
    {
        $this->x = $x;
        $this->y = $y;
        return "Moved Button: {$this->name} to ({$this->x}, {$this->y})\n";
    }
}
Label.php
<?php

declare(strict_types=1);

namespace DesignPattern\Structural\Composite\GUI;

class Label implements GUIComponent
{
    public function __construct(
        private readonly string $text,
        private int $x = 0,
        private int $y = 0
    ) {
    }

    public function render(): string
    {
        return "Rendering Label: '{$this->text}' at ({$this->x}, {$this->y})\n";
    }

    public function move(int $x, int $y): string
    {
        $this->x = $x;
        $this->y = $y;
        return "Moved Label: '{$this->text}' to ({$this->x}, {$this->y})\n";
    }
}

Composite

Panel.php
<?php

declare(strict_types=1);

namespace DesignPattern\Structural\Composite\GUI;

class Panel implements GUIComponent
{
    /**
     * @var array<int, GUIComponent> $children
     */
    private array $children = [];
    private int $x = 0;
    private int $y = 0;

    /**
     * @param GUIComponent $child
     * @return void
     */
    public function addChild(GUIComponent $child): void
    {
        $this->children[] = $child;
    }

    /**
     * @return string
     */
    public function render(): string
    {
        $result =  "Rendering Panel at ({$this->x}, {$this->y})\n";
        foreach ($this->children as $child) {
            $result .= $child->render();
        }
        return $result;
    }

    /**
     * @param int $x
     * @param int $y
     * @return string
     */
    public function move(int $x, int $y): string
    {
        $dx = $x - $this->x;
        $dy = $y - $this->y;
        $this->x = $x;
        $this->y = $y;
        $result = "Moved Panel to ({$this->x}, {$this->y})\n";

        foreach ($this->children as $child) {
            $child->move($dx, $dy);
        }
        return $result;
    }
}

Tests

GUIComponentTest.php
<?php

declare(strict_types=1);

namespace Tests\Structural\Composite\GUI;

use DesignPattern\Structural\Composite\GUI\GUIComponent;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
use DesignPattern\Structural\Composite\GUI\Panel;
use DesignPattern\Structural\Composite\GUI\Label;
use DesignPattern\Structural\Composite\GUI\Button;

#[CoversClass(GUIComponent::class)]
final class GUIComponentTest extends TestCase
{
    public function testRender(): void
    {
        $button1 = new Button('OK');
        $button2 = new Button('Cancel');
        $label = new Label('Username:');

        $panel = new Panel();
        $panel->addChild($label);
        $panel->addChild($button1);
        $panel->addChild($button2);

        $expected = "Rendering Panel at (0, 0)\n"
            . "Rendering Label: 'Username:' at (0, 0)\n"
            . "Rendering Button: OK at (0, 0)\n"
            . "Rendering Button: Cancel at (0, 0)\n";

        self::assertEquals($expected, $panel->render());
    }

    public function testMove(): void
    {
        $button = new Button('Submit');
        $label = new Label('Email:');

        $panel = new Panel();
        $panel->addChild($label);
        $panel->addChild($button);

        $expectedMove = "Moved Panel to (100, 50)\n";
        $result = $panel->move(100, 50);

        self::assertEquals($expectedMove, $result);

        $expectedRender = "Rendering Panel at (100, 50)\n"
            . "Rendering Label: 'Email:' at (100, 50)\n"
            . "Rendering Button: Submit at (100, 50)\n";
        self::assertEquals(
            $expectedRender,
            $panel->render()
        );
    }
}