0

Composite — tree structures of objects

Intermediate5 min read·eng-03-005
compare

Concept

Composite pattern organizes objects into tree structures to represent part-whole hierarchies. Individual objects (Leaves) and compositions of objects (Composites) are treated through the same interface.

The problem: You have files and folders. A folder can contain files AND other folders. You want to calculate the total size of a folder, which means summing file sizes AND recursively summing sizes of nested folders. Without Composite, you'd need different handling for files vs. folders. Composite makes them interchangeable.

Structure:

  • Component Interface: The common interface for both leaves and composites. size(): int, render(): string.
  • Leaf: A concrete component with no children. A file, a UI widget, a menu item.
  • Composite: A component that holds children (both Leaves and other Composites). Delegates operations to children and aggregates results.

Real-world examples:

  • File system: File (leaf) and Directory (composite).
  • UI rendering: Widget trees — buttons, panels, windows.
  • Menu structures: Menu items and nested menus.
  • HTML DOM: Elements can contain other elements or text nodes.
  • Laravel Route groups: A route group IS a composite — it holds routes and sub-groups.

PHP array equivalent: PHP arrays ARE tree structures. Composite formalizes this with typed objects and a polymorphic interface — useful when the tree components have behavior, not just data.

Code Example

php
<?php
// Component Interface — same interface for files and directories
interface FileSystemNode
{
    public function getName(): string;
    public function getSize(): int;
    public function render(int $depth = 0): string;
    public function count(): int;
}

// Leaf — no children
class File implements FileSystemNode
{
    public function __construct(
        private readonly string $name,
        private readonly int    $size, // bytes
    ) {}

    public function getName(): string         { return $this->name; }
    public function getSize(): int            { return $this->size; }
    public function count(): int              { return 1; }
    public function render(int $depth = 0): string
    {
        return str_repeat('  ', $depth) . "📄 {$this->name} (" . number_format($this->size) . " bytes)\n";
    }
}

// Composite — can contain files and other directories
class Directory implements FileSystemNode
{
    /** @var FileSystemNode[] */
    private array $children = [];

    public function __construct(private readonly string $name) {}

    public function add(FileSystemNode $node): static
    {
        $this->children[] = $node;
        return $this;
    }

    public function remove(FileSystemNode $node): void
    {
        $this->children = array_filter($this->children, fn($n) => $n !== $node);
    }

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

    // Aggregate: sum all children's sizes (recursively)
    public function getSize(): int
    {
        return array_sum(array_map(fn($child) => $child->getSize(), $this->children));
    }

    public function count(): int
    {
        return array_sum(array_map(fn($child) => $child->count(), $this->children));
    }

    public function render(int $depth = 0): string
    {
        $indent = str_repeat('  ', $depth);
        $output = "{$indent}📁 {$this->name}/ (" . number_format($this->getSize()) . " bytes)\n";
        foreach ($this->children as $child) {
            $output .= $child->render($depth + 1);
        }
        return $output;
    }
}

// Building the tree
$root = new Directory('project');
$root->add(new File('README.md', 2048))
     ->add((new Directory('src'))
         ->add(new File('app.php', 8192))
         ->add((new Directory('controllers'))
             ->add(new File('UserController.php', 4096))
             ->add(new File('PostController.php', 3072))
         )
     )
     ->add((new Directory('tests'))
         ->add(new File('UserTest.php', 5120))
     );

echo $root->render();
// 📁 project/ (22,528 bytes)
//   📄 README.md (2,048 bytes)
//   📁 src/ (15,360 bytes)
//     📄 app.php (8,192 bytes)
//     📁 controllers/ (7,168 bytes)
//       📄 UserController.php (4,096 bytes)
//       📄 PostController.php (3,072 bytes)
//   📁 tests/ (5,120 bytes)
//     📄 UserTest.php (5,120 bytes)

echo "Total files: " . $root->count() . "\n"; // 5
echo "Total size:  " . number_format($root->getSize()) . " bytes\n"; // 22,528