advanced php language

Predefined Interfaces and Classes

PHP ships with interfaces and classes that define common behaviour. You do not have to invent a custom interface every time an object needs to be counted, looped over, converted to JSON, represented as text, or handle dates.

Knowing these built-in contracts helps you write objects that work naturally with PHP functions and common framework code.

Countable

Implement Countable when an object represents a collection and count($object) should be meaningful.

PHP example
<?php

declare(strict_types=1);

final class Basket implements Countable
{
    /**
     * @param list<string> $items
     */
    public function __construct(
        private array $items,
    ) {
    }

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

$basket = new Basket(['Notebook', 'Pen']);

echo count($basket) . PHP_EOL;

// Prints:
// 2

Do not implement Countable just because a class has one array property. It should make sense to ask how many items the object contains.

IteratorAggregate

Implement IteratorAggregate when an object should be usable in foreach.

PHP example
<?php

declare(strict_types=1);

final class Basket implements IteratorAggregate
{
    /**
     * @param list<string> $items
     */
    public function __construct(
        private array $items,
    ) {
    }

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

$basket = new Basket(['Notebook', 'Pen']);

foreach ($basket as $item) {
    echo $item . PHP_EOL;
}

// Prints:
// Notebook
// Pen

IteratorAggregate is often easier than implementing Iterator directly because you return an iterator instead of managing cursor state yourself.

JsonSerializable

Implement JsonSerializable when an object needs a deliberate JSON shape.

PHP example
<?php

declare(strict_types=1);

final class ApiUser implements JsonSerializable
{
    public function __construct(
        private int $id,
        private string $email,
        private string $passwordHash,
    ) {
    }

    public function jsonSerialize(): array
    {
        return [
            'id' => $this->id,
            'email' => $this->email,
        ];
    }
}

echo json_encode(new ApiUser(7, 'ada@example.com', 'secret-hash'), JSON_THROW_ON_ERROR);
echo PHP_EOL;

// Prints:
// {"id":7,"email":"ada@example.com"}

The password hash stays out of the JSON because the class controls its exported representation.

Stringable

An object with __toString() automatically satisfies Stringable. Use it for objects that have one obvious text representation.

PHP example
<?php

declare(strict_types=1);

final class OrderNumber
{
    public function __construct(
        private int $number,
    ) {
    }

    public function __toString(): string
    {
        return 'ORD-' . str_pad((string) $this->number, 6, '0', STR_PAD_LEFT);
    }
}

function printLabel(Stringable $value): void
{
    echo (string) $value . PHP_EOL;
}

printLabel(new OrderNumber(42));

// Prints:
// ORD-000042

Avoid __toString() when the object has several possible formats. A named method such as toDisplayString() or toCsvValue() may be clearer.

DateTimeImmutable

DateTimeImmutable is a predefined class, not an interface, but it is one of the most important built-ins in application code. Prefer it over mutable DateTime for most domain work.

PHP example
<?php

declare(strict_types=1);

$createdAt = new DateTimeImmutable('2026-05-26 09:00:00', new DateTimeZone('UTC'));
$expiresAt = $createdAt->modify('+30 minutes');

echo $createdAt->format(DateTimeInterface::ATOM) . PHP_EOL;
echo $expiresAt->format(DateTimeInterface::ATOM) . PHP_EOL;

// Prints:
// 2026-05-26T09:00:00+00:00
// 2026-05-26T09:30:00+00:00

modify() returns a new object, so the original time is not accidentally changed elsewhere.

Combining Built-In Contracts

A collection object can implement more than one predefined interface when each contract is meaningful.

PHP example
<?php

declare(strict_types=1);

final class Basket implements Countable, IteratorAggregate, JsonSerializable
{
    /**
     * @param list<string> $items
     */
    public function __construct(
        private array $items,
    ) {
    }

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

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

    public function jsonSerialize(): array
    {
        return ['items' => $this->items];
    }
}

$basket = new Basket(['Notebook', 'Pen']);

echo count($basket) . PHP_EOL;
echo json_encode($basket, JSON_THROW_ON_ERROR) . PHP_EOL;

// Prints:
// 2
// {"items":["Notebook","Pen"]}

The class now works with count(), foreach, and json_encode() without custom helper methods.

What You Should Be Able To Do

After this lesson, you should be able to recognise common predefined interfaces, implement them when they make an object easier to use, and avoid adding them when the behaviour would be misleading.

For junior PHP work, this matters because frameworks, libraries, and code reviews often expect these built-in contracts. Using them well makes your objects feel like part of PHP rather than special cases.

Practice

Practice: Build A Basket Collection

Create a small basket collection that works with PHP's predefined interfaces.

Task

Build a Basket class that:

  • stores a list of item names
  • implements Countable
  • implements IteratorAggregate
  • implements JsonSerializable

Use strict types. Show that count($basket), foreach, and json_encode() all work. Keep the expected output inside the PHP code block as printed lines or comments.

Afterward, add a short note explaining why these built-in interfaces are better than custom methods for this example.

Show solution

This solution makes the basket work with PHP's built-in count(), foreach, and json_encode() behaviour.

PHP example
<?php

declare(strict_types=1);

final class Basket implements Countable, IteratorAggregate, JsonSerializable
{
    /**
     * @param list<string> $items
     */
    public function __construct(
        private array $items,
    ) {
    }

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

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

    public function jsonSerialize(): array
    {
        return ['items' => $this->items];
    }
}

$basket = new Basket(['Notebook', 'Pen']);

echo 'count: ' . count($basket) . PHP_EOL;

foreach ($basket as $item) {
    echo 'item: ' . $item . PHP_EOL;
}

echo json_encode($basket, JSON_THROW_ON_ERROR) . PHP_EOL;

// Prints:
// count: 2
// item: Notebook
// item: Pen
// {"items":["Notebook","Pen"]}

The built-in interfaces are better than custom methods here because PHP and other developers already know what count(), foreach, and json_encode() mean. The object integrates with the language instead of requiring special helper names.