How to Make a Typed PHP Array or Collection

July 11, 2025 full-stack

So, one thing a lot of people want that PHP doesn't natively support is typed arrays. In other languages you can say "I want an array of integers" or "I want an array of strings" or "I want this to be an array of <someClass>", etc.

But, with a little creatively, it's still possible.

<?php

declare(strict_types=1);

namespace App\DataObjects\CollectionOf;

use Illuminate\Support\Collection;

class CollectionOf extends Collection
{
    /**
     * Convention is that the class that extends
     * this will be `CollectionOf<something>` and
     * `$elementsMustBe...` will be
     * `<something>`. Hence:
     * If the class name is `CollectionOfFoobarDataObject`
     * then `$elementsMustBe(InstanceOf) = FoobarDataObject::class;`
     *
     * You can define ONE or the OTHER. If the extending
     * class defines `elementsMustBe`, then it will be checked
     * against that precise class. If the extending class
     * defined `elementsMustBeInstanceOf`, then it will be
     * checked against the object's class, or parents or
     * interface.
     */
    protected string $elementsMustBe = '';
    protected string $elementsMustBeInstanceOf = '';

    public function __construct($items = [])
    {
        if ($this->elementsMustBe && $this->elementsMustBeInstanceOf) {
            throw new \InvalidArgumentException(
                'Only define _either_ `elementsMustBe` _or_ `elementsMustBeInstanceOf` but not both.'
            );
        } elseif (!$this->elementsMustBe && !$this->elementsMustBeInstanceOf) {
            throw new \InvalidArgumentException(
                'Need to define either `elementsMustBe` or `elementsMustBeInstanceOf`.'
            );
        }

        parent::__construct($items);
    }

    public function offsetSet($key, $val): void
    {
        if ($this->valIsValidType($val)) {
            parent::offsetSet($key, $val);
        } else {
            $this->throwInvalidItemException($val);
        }
    }

    public function push(...$values)
    {
        foreach ($values as $val) {
            if ($this->valIsValidType($val)) {
                $this->items[] = $val;
            } else {
                $this->throwInvalidItemException($val);
            }
        }

        return $this;
    }

    protected function throwInvalidItemException(object $item)
    {
        $itemInstance = get_class($item);

        $mustBe = !empty($this->elementsMustBe) ?
            $this->elementsMustBe :
            $this->elementsMustBeInstanceOf;

        throw new \UnexpectedValueException(
            "Attempted to set an item with an instance of `{$itemInstance}` when instance of `{$mustBe}` is required."
        );
    }

    /**
     * Check whether to test against an exact class, if
     * `elementsMustBe` is set, or a class it's parent(s)
     * or an interface.
     */
    protected function valIsValidType($val): bool
    {
        if (! empty($this->elementsMustBe)) {
            return get_class($val) === $this->elementsMustBe;
        } else {
            return $val instanceof $this->elementsMustBeInstanceOf;
        }
    }
}

To implement it, you just need to define the CollectionOf element type:

<?php

namespace App\DataObjects\CollectionOf;

use App\DataObjects\CollectionOf\CollectionOf as CollectionOf;
use App\DataObjects\EventDataCollectionDataObject;

class CollectionOfEventDataCollectionDataObjects extends CollectionOf
{
    protected string $elementsMustBe = EventDataCollectionDataObject::class;
}

Now, anything you try to add, must be that type. In the above example, it would work like this:

$collectionOfEventDataObjects = new CollectionOfEventDataCollectionDataObjects();

$collectionOfEventDataObjects->push(new EventDataCollectionDataObject()); // yup
$collectionOfEventDataObjects[] = new EventDataCollectionDataObject(); // yup

You can't push or [] anything but that type of data, as defined in $elementsMustBe so you can be sure your collection being passed around has the proper type of data in it.

$collectionOfEventDataObjects[] = 'Hello'; // nope
$collectionOfEventDataObjects[] = 11111; // nope
$collectionOfEventDataObjects[] = 115.115; // nope
$collectionOfEventDataObjects[] = []; // nope
$collectionOfEventDataObjects[] = (object)[]; // nope

To me, this is useful for native types like integers, strings, etc, but really shines on custom data objects, which then permit you to pass around a collection/array of a specifically structured data class, which can have strongly typed properties in it as well, and you know without-a-doubt what you are operating on.