objects namespaces and application architecture

Object Iteration

Object iteration is how PHP lets objects work with foreach. It is useful for collection classes, result sets, filtered lists, and any object that represents "many things" while still protecting its own rules.

Arrays are still the most common iterable structure in PHP, but objects can be iterable too. The important question is whether iteration exposes the right thing. A collection should let callers loop over the items it owns without giving them uncontrolled access to its internal storage.

Default Object Iteration

If an object does not implement an iterator interface, foreach loops over its visible public properties.

PHP example
<?php

declare(strict_types=1);

final class PublicProfile
{
    public string $name = 'Ada';

    public string $role = 'Engineer';

    private string $secret = 'hidden';
}

$profile = new PublicProfile();

foreach ($profile as $key => $value) {
    echo $key . ': ' . $value . PHP_EOL;
}

// Prints:
// name: Ada
// role: Engineer

This behaviour is rarely enough for domain objects. It depends on public properties and can expose implementation details rather than meaningful items.

IteratorAggregate

IteratorAggregate is the simplest way to make a collection object work with foreach. The class implements getIterator() and returns something iterable.

PHP example
<?php

declare(strict_types=1);

final readonly class OrderLine
{
    public function __construct(
        public string $sku,
        public int $quantity,
    ) {
    }
}

/** @implements IteratorAggregate<int, OrderLine> */
final class OrderLines implements IteratorAggregate
{
    /** @var list<OrderLine> */
    private array $lines = [];

    public function add(OrderLine $line): void
    {
        $this->lines[] = $line;
    }

    public function getIterator(): Traversable
    {
        return new ArrayIterator($this->lines);
    }
}

$lines = new OrderLines();
$lines->add(new OrderLine('BOOK', 2));
$lines->add(new OrderLine('PEN', 1));

foreach ($lines as $line) {
    echo $line->sku . ': ' . $line->quantity . PHP_EOL;
}

// Prints:
// BOOK: 2
// PEN: 1

The caller can loop over order lines, but the internal array remains private.

Generators With yield

getIterator() can also use a generator. A generator yields values one at a time.

PHP example
<?php

declare(strict_types=1);

/** @implements IteratorAggregate<int, string> */
final class ActiveUserEmails implements IteratorAggregate
{
    /** @param list<array{email: string, active: bool}> $users */
    public function __construct(
        private array $users,
    ) {
    }

    public function getIterator(): Traversable
    {
        foreach ($this->users as $user) {
            if ($user['active']) {
                yield $user['email'];
            }
        }
    }
}

$emails = new ActiveUserEmails([
    ['email' => 'ada@example.com', 'active' => true],
    ['email' => 'grace@example.com', 'active' => false],
    ['email' => 'linus@example.com', 'active' => true],
]);

foreach ($emails as $email) {
    echo $email . PHP_EOL;
}

// Prints:
// ada@example.com
// linus@example.com

Generators are useful when values are filtered, transformed, or expensive to build all at once.

Iterator Versus IteratorAggregate

Iterator gives you low-level control with methods such as current(), key(), next(), rewind(), and valid(). It is more work and easier to get wrong.

Most application collection classes should use IteratorAggregate unless they have a strong reason to manage the cursor themselves.

Countable

If a collection should work with count($object), implement Countable.

PHP example
<?php

declare(strict_types=1);

/** @implements IteratorAggregate<int, string> */
final class Tags implements IteratorAggregate, Countable
{
    /** @var list<string> */
    private array $tags = [];

    public function add(string $tag): void
    {
        $tag = trim($tag);

        if ($tag === '') {
            throw new InvalidArgumentException('Tag cannot be empty.');
        }

        $this->tags[] = $tag;
    }

    public function count(): int
    {
        return count($this->tags);
    }

    public function getIterator(): Traversable
    {
        yield from $this->tags;
    }
}

$tags = new Tags();
$tags->add('php');
$tags->add('architecture');

echo count($tags) . PHP_EOL;

foreach ($tags as $tag) {
    echo $tag . PHP_EOL;
}

// Prints:
// 2
// php
// architecture

Countable should return a cheap, unsurprising count. Avoid making count($object) run slow queries or perform hidden work.

Returning Arrays Still Has A Place

Sometimes returning an array is enough. If the caller just needs a small list and there are no rules to protect, an array is simpler.

Use a collection object when it adds meaning:

  • it validates items before they are added
  • it exposes helpful domain methods
  • it hides internal storage
  • it guarantees item types
  • it supports iteration without exposing mutation

Do not create collection classes just to wrap every array. Add them where they protect a real concept.

What You Should Be Able To Do

After this lesson, you should be able to explain how foreach behaves with ordinary objects, create an iterable collection using IteratorAggregate, use yield in getIterator(), and add Countable when a collection should support count().

For junior work, the practical skill is recognising when a collection object makes code safer than passing arrays around, while avoiding unnecessary wrappers for simple lists.

Practice

Practice: Build An Iterable Tag Collection

Create a small collection object for tags.

Task

Build a Tags class that:

  • implements IteratorAggregate
  • implements Countable
  • stores tags privately
  • rejects empty tags
  • trims tags before storing them
  • can be used in foreach

Use strict types. Keep the expected output in the PHP code block as printed lines or comments.

Check Your Work

Run cases for:

  • adding two valid tags
  • counting the collection
  • iterating over the collection
  • trying to add an empty tag

Afterward, explain why the internal array should stay private.

Show solution

This solution keeps storage private but exposes safe iteration and counting.

PHP example
<?php

declare(strict_types=1);

/** @implements IteratorAggregate<int, string> */
final class Tags implements IteratorAggregate, Countable
{
    /** @var list<string> */
    private array $tags = [];

    public function add(string $tag): void
    {
        $tag = trim($tag);

        if ($tag === '') {
            throw new InvalidArgumentException('Tag cannot be empty.');
        }

        $this->tags[] = $tag;
    }

    public function count(): int
    {
        return count($this->tags);
    }

    public function getIterator(): Traversable
    {
        yield from $this->tags;
    }
}

$tags = new Tags();
$tags->add(' php ');
$tags->add('architecture');

echo count($tags) . PHP_EOL;

foreach ($tags as $tag) {
    echo $tag . PHP_EOL;
}

try {
    $tags->add('   ');
} catch (InvalidArgumentException $exception) {
    echo $exception->getMessage() . PHP_EOL;
}

// Prints:
// 2
// php
// architecture
// Tag cannot be empty.

The internal array stays private so callers cannot bypass trimming and validation. foreach still works because getIterator() exposes the stored tags as an iterable sequence.