CodeNewbie Community 🌱

Albert Colom
Albert Colom

Posted on • Originally published at Medium on

Use custom PHP Collection instead Array

In this article we will see how to define a custom PHP collection


Array vs Collection

An Array is a basic data structure that stores values key/value without any constrictions or any OOP method and it is very difficult to control and maintain the data they contain.

To solve this problem some modern frameworks like Laravel (Illuminate Collection) or Symfony (Doctrine ArrayCollection) use their own OOP wrapper with a lot of functions.

But if we want to decouple from the framework or we use another framework and do not want to install the dependency we can create our own collection.

In this article, we will see how to build our own collection with some methods to work with objects. In the example, it is focused on a CQRS architecture and we place the collection in the reading layer.

NOTE: In the code examples we have used PHP 8.1 but the code is fully adaptable to any other version.

Custom collection

Firstly create a custom collection without type validation and add some callable functions fromMap, reduce, map, each, some, filter and other OOP functions like first, last, count, isEmpty, add, values, items, getIterator . This is the base of OOP wrapper for working with arrays.

src/Shared/Domain/Read/Collection.php

<?php

declare(strict_types=1);

namespace App\Shared\Domain\Read;

use ArrayIterator;
use IteratorAggregate;
use Traversable;

abstract class Collection implements IteratorAggregate
{
    public function __construct(private array $elements)
    {
    }

    public static function createEmpty(): static
    {
        return new static([]);
    }

    public static function fromMap(array $items, callable $fn): static
    {
        return new static(array_map($fn, $items));
    }

    public function reduce(callable $fn, mixed $initial): mixed
    {
        return array_reduce($this->elements, $fn, $initial);
    }

    public function map(callable $fn): array
    {
        return array_map($fn, $this->elements);
    }

    public function each(callable $fn): void
    {
        array_walk($this->elements, $fn);
    }

    public function some(callable $fn): bool
    {
        foreach ($this->elements as $index => $element) {
            if ($fn($element, $index, $this->elements)) {
                return true;
            }
        }

        return false;
    }

    public function filter(callable $fn): static
    {
        return new static(array_filter($this->elements, $fn, ARRAY_FILTER_USE_BOTH));
    }

    public function first(): mixed
    {
        return reset($this->elements);
    }

    public function last(): mixed
    {
        return end($this->elements);
    }

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

    public function isEmpty(): bool
    {
        return empty($this->elements);
    }

    public function add(mixed $element): void
    {
        $this->elements[] = $element;
    }

    public function values(): array
    {
        return array_values($this->elements);
    }

    public function items(): array
    {
        return $this->elements;
    }

    public function getIterator(): Traversable
    {
        return new ArrayIterator($this->elements);
    }
}
Enter fullscreen mode Exit fullscreen mode

Custom typed Collection

The typed collection extends the above collection and implements the constraints.

NOTE : To avoid creating more code than necessary I have used the webmozzart/assert (pakagist) library but feel free to implement your validations.

src/Shared/Domain/Read/TypedCollection.php

<?php

declare(strict_types=1);

namespace App\Shared\Domain\Read;

use Webmozart\Assert\Assert;

abstract class TypedCollection extends Collection
{
    public function __construct(array $elements = [])
    {
        Assert::allIsInstanceOf($elements, $this->type());

        parent::__construct($elements);
    }

    abstract protected function type(): string;

    public function add(mixed $element): void
    {
        Assert::isInstanceOf($element, $this->type());

        parent::add($element);
    }
}
Enter fullscreen mode Exit fullscreen mode

Example implement Typed Collection

First of all, we need a create a basic Entity Class, in this case create a simple class called Foo .

<?php

declare(strict_types=1);

namespace App\Context\Foo\Domain\Read\View\Foo;

final readonly class Foo
{
    public function __construct(
        public string $id,
        public string $name
    ) {
    }

    public function toArray(): return
    {
        return [
          'id' => $this->id,
          'name' => $this->name,
        ];
    }

    public function equals(self $other): bool
    {
        return $this->id === $other->id && $this->name === $other->name;
    }
}
Enter fullscreen mode Exit fullscreen mode

Then can create a typed collection that contain a Foo inside

src/Context/Foo/Domain/Read/View/FooCollection.php

<?php

declare(strict_types=1);

namespace App\Context\Foo\Domain\Read\View;

use App\Shared\Domain\Read\TypedCollection;

final class FooCollection extends TypedCollection
{
    protected function type(): string
    {
        return Foo::class;
    }
}
Enter fullscreen mode Exit fullscreen mode

Some examples of how to use it

Create an empty collection, then add two Foo elements and filter elements with other name:

$collection = FooCollection::createEmpty();
$collection->add(new Foo('4dae0971-ac81-43f1-b7e1-952df598af5a', 'name'));
$collection->add(new Foo('42deac29-9661-47e8-8746-062fc784ae1b', 'other'));

# Filter by name "other"

$filteredCollection = $collection->filter(function(Foo $foo) {
    return $foo->mame === 'other';
});

# Or if you prefer you can use Arrow function version

$filteredCollection = $collection->filter(fn (Foo $foo) => $foo->mame === 'other');
Enter fullscreen mode Exit fullscreen mode

Create collection from map function using array :

$array = [
  [
    'id' => '4dae0971-ac81-43f1-b7e1-952df598af5a',
    'name' => 'name',
    'surname' => 'surname',
  ],
  [
    'id' => '42deac29-9661-47e8-8746-062fc784ae1b',
    'name' => 'other',
  ],
];

# Create fromMap

$collection = FooCollection::fromMap($array, function(array $data){
    return new Foo($data['id'], $data['name'])
});

# Or if you prefer you can use Arrow function version

$collection = FooCollection::fromMap(
    $array,
    fn(array $data): Foo => new Foo($data['id'], $data['name'])
);
Enter fullscreen mode Exit fullscreen mode

Concat different function with arrow functions:

$array = [
  [
    'id' => '4dae0971-ac81-43f1-b7e1-952df598af5a',
    'name' => 'name',
    'surname' => 'surname',
  ],
  [
    'id' => '42deac29-9661-47e8-8746-062fc784ae1b',
    'name' => 'other',
  ],
];

# Create a collection from map, then filter by name and finally return array

$collection = FooCollection::fromMap(
    $array,
    fn(array $data): Foo => new Foo($data['id'], $data['name'])
)
->filter(fn (Foo $foo) => $foo->mame === 'other')
->map(fn (Foo $foo) => $foo->toArray());
Enter fullscreen mode Exit fullscreen mode

Original published at:albertcolom.com

Top comments (0)