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
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
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
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
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
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.