A New Project

Imagine you’ve been tasked with building a file manager. How would you go about it? Your developer friend who still codes like its 1999 comes up with the following:

function calculateSize($item)
{
    if ($item instanceof File) {
        return $item->getSize(); // For File, return the file's size
    } elseif ($item instanceof Folder) {
        $totalSize = 0;
        foreach ($item->getItems() as $subItem) {
            $totalSize += calculateSize($subItem); // Recursively calculate the size of the folder's contents
        }
        return $totalSize; // Return total size of the folder
    }
}

He sends a message over Teams:

Here you go buddy, I’ve given you something to get you started, we also need list and find functions, okay?

I mean, that’ll work. If you passed in a File then you’d only fetch the size of that file, if you passed in a Folder, you’d get the total size of all the files within the Folder.

But how would we handle sub folders? It’s going to get messy, real quick! We’d have to rely on recursion or some similar method to handle nested folders, but doing so in the current structure would turn things into a tangled mess.

And I haven’t even started digging into the list and find functions yet. Can you imagine the state of the code once we’re done, good luck trying to maintain or expand it.

Fortunately, there’s a much cleaner way to do this, and that’s where the Composite Pattern comes in.

The Composite Pattern

Let’s backtrack a sec. What is the Composite Pattern?

The Composite Pattern is a structural design pattern that allows you to compose objects into tree-like structures to represent part-whole hierarchies.

It treats individual objects and compositions of objects uniformly, enabling clients to treat single objects and compositions of objects the same way.

Components are categorised into two types:

  • Leaf: A component that doesn't have children. It's an end node in the tree structure. In our example, a File is a leaf — it can't contain anything.

  • Composite: A component that can have children, which can be either other composites or leaves. In our example, a Folder is a composite — it can contain files and other folders.

The brilliance of the pattern is that both leaf and composite implement the same interface, so they can be treated the same.

Okay, great, so what does that mean?

Imagine you’re at a music festival. You’ve got solo artists (like Taylor Swift) and full bands (like Foo Fighters). You want to build a schedule that treats them both the same way—give them a time slot, a stage, a description, etc.

You don’t want your code to say:

“If Artist is a solo act, do X… but if Artist is a band, do Y…”

That’s chaos waiting to happen.

Instead, you make sure both solo artists and bands implement a common interface—let’s say, Performer. Now your schedule logic just works with Performer, and it doesn’t care whether it’s dealing with a one-woman powerhouse or an awesome rock band.

Same goes for our file manager.

A File is a single item. A Folder is a container that might hold files and other folders. But we want to treat them both like a single unit: an Item. That way, we can loop over them, calculate size, list contents, or search—without constantly branching our logic.

Let’s Figure This Out Together

First, let’s define an interface.

interface Item
{
    public function getSize(): float;
    public function find(string $name): ?Item;
    public function list(int $depth = 0): void;
}

Any class which implements this interface MUST have getSize, find and list methods, or we’ll get an error.

Next up, let’s create a File class. This class will be a Leaf.

A Leaf is a component that doesn't have children. It's an end node in the tree structure. In our example, a File is a leaf — it can't contain anything.

class File implements Item
{
    public function __construct(private readonly string $name, private readonly float $size)
    {
        //
    }

    #[\Override]
    public function getSize(): float
    {
        return $this->size;
    }

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

    public function find(string $name): ?Item
    {
        return $this->name === $name ? $this : null;
    }

    public function list(int $depth = 0): void
    {
        echo str_repeat("  ", $depth) . "- " . $this->name . " (" . $this->size . " KB)\n";
    }
}

Let’s break down this file:

__construct(private readonly string $name, private readonly float $size)

This is the constructor. It initialises a File object with a name (like "logo.png") and a size (like 50.0). The readonly means these values can't be changed after the object is created.

getSize(): float

Returns the size of the file in kilobytes.
Example: "logo.png" might return 50.0.

getName(): string

Returns the name of the file.
Example: "logo.png".

find(string $name): ?Item

Checks if this file's name matches the one you're looking for.

  • If yes, it returns itself.

  • If no, it returns null.

list(int $depth = 0): void

Prints the file name and size, indented by its nesting depth.

  • Useful when printing a folder structure.

  • depth = 0 means top-level, depth = 1 means inside a folder, etc.

Still with me? Right, lets create the Folder class. The Folder class is a Composite. It’s an object that can contain other objects, both leaves (File) and other composites (Folders).

class Folder implements Item
{
    /** @var Item[] */
    private array $items = [];

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

    public function addItem(Item $item): void
    {
        $this->items[] = $item;
    }

    #[\Override]
    public function getSize(): float
    {
        $totalSize = 0;
        foreach ($this->items as $item) {
            $totalSize += $item->getSize();
        }

        return $totalSize;
    }

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

    public function find(string $name): ?Item
    {
        if ($this->name === $name) {
            return $this;
        }

        foreach ($this->items as $item) {
            $found = $item->find($name);
            if ($found !== null) {
                return $found;
            }
        }

        return null;
    }

    public function list(int $depth = 0): void
    {
        echo str_repeat("  ", $depth) . "+ " . $this->name . " (" . $this->getSize() . " KB)\n";
        foreach ($this->items as $item) {
            $item->list($depth + 1);
        }
    }

There’s actually some pretty cool stuff going on here, the Composite Pattern is AWESOME at recursive stuff. Here’s why:

addItem(Item $item): void

Adds a file or subfolder to this folder.

getSize(): float

Calculates the total size of the folder by summing the sizes of everything it contains, recursively.

This is where the Composite Pattern shines. Each child knows how to calculate its size, so the folder just delegates the job downward. Whether the folder contains 10 files or 10 nested folders full of files, it all just works.

foreach ($this->items as $item) {
    $totalSize += $item->getSize();
}

As we loop through the items, we know that an Item is either a File or a Folder. If it’s a file, it’ll simply return that specific file size and add it to the total size, however, if its a sub folder, we’ll loop through the items of that sub folder and so on and so on. Pretty clever!

find(string $name): ?Item

Searches the folder and its contents recursively for an item with the given name. Without needing a giant if or switch statement, this leverages polymorphism. Each child Item — whether a File or Folder — is asked to find(), and returns itself if it matches.

Simple, clean, and infinitely nestable.

Lets look at this code:

foreach ($this->items as $item) {
    $found = $item->find($name);
    if ($found !== null) {
        return $found;
    }
}

We loop through the items of a folder, again, the items are either a File or a Folder. If it’s a file, we check if the name matches, otherwise we loop through the sub folder and do the same again until we find a match.

list(int $depth = 0): void

Prints out a structured, indented list of the folder and its contents, nicely showing the hierarchy.

The $depth parameter controls indentation so you can visually follow nested folders. Again, recursion means each subfolder calls its own list() method, with an incremented depth — no manual nesting logic needed.

echo str_repeat("  ", $depth) . "+ " . $this->name . " (" . $this->getSize() . " KB)\n";

foreach ($this->items as $item) {
    $item->list($depth + 1);
}

It’s simple but effective. We loop through items, if we come across a folder, we loop through that until we eventually print out all the files with the appropriate indentation.

The Implementation

Here’s a quick example.

// Create files
$file1 = new File("index.php", 15);
$file2 = new File("style.css", 10);
$file3 = new File("script.js", 25);
$file4 = new File("readme.txt", 5);
$file5 = new File("logo.png", 50);

// Create subfolders
$imgFolder = new Folder("images");
$imgFolder->addItem($file5);

// Create higher-level folders
$folder1 = new Folder("assets");
$folder1->addItem($file1);
$folder1->addItem($file2);
$folder1->addItem($imgFolder); // nested folder

$folder2 = new Folder("src");
$folder2->addItem($file3);
$folder2->addItem($file4);

// Create root folder
$root = new Folder("project");
$root->addItem($folder1);
$root->addItem($folder2);

// Print total size
echo "Total Project Size: " . $root->getSize() . " KB\n";

// Search for a file
$foundItem = $root->find("logo.png");

if ($foundItem) {
    echo "Found: " . $foundItem->getName() . " (" . $foundItem->getSize() . " KB)\n";
} else {
    echo "File not found.\n";
}

// Print directory structure
echo "\nProject Structure:\n";
$root->list();

This would generate:

Total Project Size: 105 KB
Found: logo.png (50 KB)

Project Structure:
+ project (105 KB)
  + assets (75 KB)
    - index.php (15 KB)
    - style.css (10 KB)
    + images (50 KB)
      - logo.png (50 KB)
  + src (30 KB)
    - script.js (25 KB)
    - readme.txt (5 KB)

Pretty cool, right? Not bad from creating 2 classes and an Interface.

Code smarter. Not harder.

Want to see a quick example? Check out my coding tips repository which explains the Composite pattern, gives examples and backs everything up with tests.

Thanks for reading.