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