adachsoft/collection

A collection library providing flexible and immutable collections.

2.2.0 2025-09-18 06:11 UTC

This package is not auto-updated.

Last update: 2025-09-19 04:03:32 UTC


README

A simple, focused PHP library for building strongly-typed collections with both mutable and immutable variants. It promotes clear, predictable APIs and removes the pitfalls of loosely-typed arrays.

  • PHP 8.3+
  • No external runtime dependencies
  • PSR-4 autoloading

Key Features

  • Mutable and immutable base classes to extend
  • Strong runtime type validation for items (and for maps: for keys and values)
  • Array-like access []:
    • Mutable collections support read/write via []
    • Immutable collections support read-only via [] (write operations throw)
  • Clean iteration with IteratorAggregate
  • Utility type system for maps (scalar and class types)
  • NEW: Advanced collection operations - filter, map, reduce, unique, reverse, chunk, take, skip

Installation

composer require adachsoft/collection

Quick Start

1) Your domain object

<?php

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

2) Mutable collection

Extend AdachSoft\Collection\AbstractCollection and set the expected item type. The simplest way is to predefine the $type property in the subclass.

<?php

use AdachSoft\Collection\AbstractCollection;

final class UserCollection extends AbstractCollection
{
    /** @var class-string<User>|null */
    protected ?string $type = User::class;
}

Usage:

$user1 = new User(1, 'Alice');
$user2 = new User(2, 'Bob');

$users = new UserCollection([$user1]);
$users->add($user2);       // OK
$users[] = new User(3, 'C'); // also OK via ArrayAccess (write)

foreach ($users as $user) {
    echo $user->name . "\n"; // Alice, Bob, C
}

// Type safety: adding wrong type will throw InvalidArgumentException
$users->add('not a user'); // throws InvalidArgumentException

3) Immutable collection (read-only, array-like access for reads)

Extend AdachSoft\Collection\AbstractImmutableCollection. No mutator methods exist; read access via [] is allowed, write attempts via [] throw.

<?php

use AdachSoft\Collection\AbstractImmutableCollection;

final class ImmutableUserCollection extends AbstractImmutableCollection
{
    /** @var class-string<User>|null */
    protected ?string $type = User::class;
}

Usage:

$users = new ImmutableUserCollection([
    new User(1, 'Alice'),
    new User(2, 'Bob'),
]);

// Read operations
echo $users[0]->name;   // Alice
var_dump(isset($users[1])); // true

// Write operations are not allowed (immutable)
$users[0] = new User(3, 'Charlie'); // throws BadMethodCallException
unset($users[0]);                   // throws BadMethodCallException

// Functional helpers return the same collection class (static)
$names = $users->map(fn (User $u) => $u->name); // ImmutableUserCollection

4) Advanced Collection Operations (NEW)

All collections (both mutable and immutable) now support advanced functional operations:

$numbers = new ImmutableUserCollection([1, 2, 3, 4, 5, 5, 6]);

// Reduce - aggregate values
$sum = $numbers->reduce(fn ($carry, $item) => $carry + $item, 0); // 21

// Unique - remove duplicates
$unique = $numbers->unique(); // [1, 2, 3, 4, 5, 6]

// Custom unique comparator for objects
$users = new UserCollection([
    new User(1, 'Alice'),
    new User(2, 'Bob'),
    new User(1, 'Alice2'), // Same ID, different name
]);
$uniqueUsers = $users->unique(fn ($a, $b) => $a->id === $b->id);

// Reverse - reverse order
$reversed = $numbers->reverse(); // [6, 5, 5, 4, 3, 2, 1]

// Chunk - split into smaller collections
$chunks = $numbers->chunk(3); // [[1,2,3], [4,5,5], [6]]

// Take - get first N elements
$firstThree = $numbers->take(3); // [1, 2, 3]

// Skip - skip first N elements
$withoutFirst = $numbers->skip(2); // [3, 4, 5, 5, 6]

All operations return new collection instances (immutable operations), preserving the original collection type and maintaining key associations where applicable.

5) Immutable, typed map (keys and values)

Use AdachSoft\Collection\AbstractImmutableMap for key/value collections with strict, explicit types for keys and values.

<?php

use AdachSoft\Collection\AbstractImmutableMap;
use AdachSoft\Collection\Type\TypeDefinitionInterface;
use AdachSoft\Collection\Type\ScalarTypeEnum;
use AdachSoft\Collection\Type\ClassType;

final class UserMap extends AbstractImmutableMap
{
    protected function getKeyType(): TypeDefinitionInterface
    {
        return ScalarTypeEnum::STRING; // keys must be strings
    }

    protected function getValueType(): TypeDefinitionInterface
    {
        return new ClassType(User::class); // values must be User instances
    }
}

Usage:

$map = new UserMap([
    'owner' => new User(1, 'Alice'),
    'guest' => new User(2, 'Bob'),
]);

$owner = $map->get('owner');    // returns User
var_dump($map->hasKey('guest')); // true

foreach ($map as $key => $user) {
    echo $key . ': ' . $user->name . "\n";
}

$allKeys = iterator_to_array($map->keys());   // ['owner', 'guest']
$allVals = iterator_to_array($map->values()); // [User(...), User(...)]

// Invalid types at construction time will throw:
// - AdachSoft\Collection\Exception\InvalidKeyTypeException for wrong key type
// - AdachSoft\Collection\Exception\InvalidItemTypeException for wrong value type

6) Mutable, typed map

Use AdachSoft\Collection\AbstractMap to allow in-place mutation of key/value pairs while keeping strict types.

<?php

use AdachSoft\Collection\AbstractMap;
use AdachSoft\Collection\Type\TypeDefinitionInterface;
use AdachSoft\Collection\Type\ScalarTypeEnum;
use AdachSoft\Collection\Type\ClassType;

final class MutableUserMap extends AbstractMap
{
    protected function getKeyType(): TypeDefinitionInterface
    {
        return ScalarTypeEnum::STRING; // keys must be strings
    }

    protected function getValueType(): TypeDefinitionInterface
    {
        return new ClassType(User::class); // values must be User instances
    }
}

Usage:

$map = new MutableUserMap();
$map->set('x', new User(1, 'Alice'));
$map->set('x', new User(2, 'Bob')); // updates existing key
$map->removeKey('x');
$map->clear();

Contracts

Prefer the contracts from AdachSoft\Collection\Contract:

  • ReadableCollectionInterface for read-only collections
  • MutableCollectionInterface for mutable collections
  • KeyedCollectionInterface for immutable maps
  • MutableKeyedCollectionInterface for mutable maps

Tooling

  • PHPUnit tests: composer test
  • Coding style: composer csfix
  • Static analysis: composer phpstan

Changelog

See CHANGELOG.md for release notes.

License

MIT