adachsoft/collection

A collection library providing flexible and immutable collections.

Maintainers

👁 Arkadiusz Adach

Package info

gitlab.com/a.adach/collections

Issues

pkg:composer/adachsoft/collection

Statistics

Installs: 222

Dependents: 43

Suggesters: 0

Stars: 0

v3.0.2 2026-02-23 17:30 UTC

Requires

  • php: ^8.3

Suggests

None

Provides

None

Conflicts

None

Replaces

None

MIT 26b9ed573da540c1da02f6f4dbdb8dcb92b0f187

  • Arkadiusz Adach

collectionsfilterarraymapdata-structurecollectionimmutablegenericreadonlyphp83

This package is not auto-updated.

Last update: 2026-06-15 18:34:12 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 in 3.0.1:
    • Unified item-type validation in AbstractCollection and AbstractImmutableCollection via overridable isValidItem()/validateType() with ItemTypeValidationTrait; legacy $type constructor parameter and property are still supported for BC but considered deprecated.
    • AbstractImmutableMap no longer propagates the legacy $type when creating new instances internally, preparing for future removal of this parameter without breaking userland code.
  • NEW in 3.0.0:
    • API for first()/last() on collections and maps (throws UnderflowException on empty)
    • ReadableCollectionInterface::hasKey(int|string $key) for key type consistency
    • Dev tooling: adachsoft/changelog-linter and adachsoft/php-code-style with unified composer scripts
  • Previously added highlights:
    • Merge API for collections and maps with conflict strategies (OVERWRITE, SKIP, FAIL, APPEND_NEW_KEY)
    • toArray() for all collections (preserves keys)
    • get() / getOrDefault() for base collections and maps
    • Constructors accept iterable in all collections and maps
    • Advanced operations: filter, map, reduce, unique, reverse, chunk, take, skip
    • PHPStan integration with a custom rule enforcing @extends for collection and map subclasses
    • Improved PHPDoc generics for precise static analysis

Installation

composer require adachsoft/collection

PHPStan integration

adachsoft/collection ships with a PHPStan rule (EnforceCollectionExtendsTagRule) enforcing the use of @extends in collection subclasses.

Supported bases and required @extends:

  • AbstractCollection => @extends AbstractCollection
  • AbstractImmutableCollection => @extends AbstractImmutableCollection
  • AbstractMap => @extends AbstractMap<K, V>
  • AbstractImmutableMap => @extends AbstractImmutableMap<K, V>

Examples:

/**
 * @extends AbstractImmutableCollection<User>
 */
final class ImmutableUserCollection extends AbstractImmutableCollection
{
}

/**
 * @extends AbstractCollection<User>
 */
final class UserCollection extends AbstractCollection
{
}

/**
 * @extends AbstractMap<string, User>
 */
final class UserMap extends AbstractMap
{
}

/**
 * @extends AbstractImmutableMap<int, User>
 */
final class UserImmutableMap extends AbstractImmutableMap
{
}

No extra PHPStan configuration is required. The package ships an extension.neon and is auto-included via composer.json extra.phpstan.includes.

Rector migration helpers

To help projects upgrade from the legacy $type-based item validation to the new isValidItem() API introduced in 3.0.1, the library ships two optional Rector rules:

  • AdachSoft\Collection\Rector\CollectionConstructorTypeToIsValidItemRector
  • AdachSoft\Collection\Rector\ImmutableMapRemoveConstructorTypeArgumentRector

These rules are purely development-time helpers; they do not change how the library behaves at runtime.

What they do

CollectionConstructorTypeToIsValidItemRector

Targets subclasses of AbstractCollection and AbstractImmutableCollection that still rely on:

  • parent::__construct($items, Foo::class), or
  • protected ?string $type = Foo::class;

The rule:

  • generates protected function isValidItem(mixed $item): bool based on Foo::class (with a helpful @param Foo $item phpdoc),
  • removes the second $type argument from parent::__construct(...) calls,
  • keeps constructor signatures compatible with the base classes (to avoid "Too many arguments" at runtime),
  • removes the legacy $type property when it is safe to do so and it is not used elsewhere in the class.

ImmutableMapRemoveConstructorTypeArgumentRector

Targets subclasses of AbstractImmutableMap that still call:

parent::__construct($items, SomeClass::class);

The rule removes the unused second argument so that your map constructors match the internal behavior of AbstractImmutableMap in 3.0.1. Key/value validation stays defined solely by getKeyType() and getValueType().

How to use

The package ships a ready-made Rector config at:

  • vendor/adachsoft/collection/rector/collection-301-migration.php

You can either:

  1. Run Rector directly with this config:
vendor/bin/rector process src \
 --config=vendor/adachsoft/collection/rector/collection-301-migration.php
  1. Or import it from your main rector.php:
<?php

use Rector\Config\RectorConfig;

return static function (RectorConfig $rectorConfig): void {
 // your existing configuration...

 $rectorConfig->import(__DIR__ . '/vendor/adachsoft/collection/rector/collection-301-migration.php');
};

Then run Rector as usual in your project root:

vendor/bin/rector process

The rules will only touch your collection/map subclasses that still use the legacy $type API.

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 define the expected item type.

Since 3.0.1 the recommended way is to override isValidItem() for custom validation logic. The legacy $type property and constructor parameter are still supported for backward compatibility but are considered deprecated and may be removed in a future major version.

<?php

use AdachSoft\Collection\AbstractCollection;

/**
 * @extends AbstractCollection<User>
 */
final class UserCollection extends AbstractCollection
{
 protected function isValidItem(mixed $item): bool
 {
 return $item instanceof User;
 }
}

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

Legacy style (still supported for BC): you can also set a protected $type property with a FQCN of the expected item type. This pattern is deprecated and will be removed after the $type constructor parameter is dropped.

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

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.

As with mutable collections, since 3.0.1 the recommended approach for item type validation is to override isValidItem(); the legacy $type property/constructor parameter remain supported but are considered deprecated.

<?php

use AdachSoft\Collection\AbstractImmutableCollection;

/**
 * @extends AbstractImmutableCollection<User>
 */
final class ImmutableUserCollection extends AbstractImmutableCollection
{
 protected function isValidItem(mixed $item): bool
 {
 return $item instanceof User;
 }
}

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

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

<?php

use AdachSoft\Collection\AbstractImmutableCollection;

// A simple immutable collection without item type constraints
/**
 * @extends AbstractImmutableCollection<int>
 */
final class IntCollection extends AbstractImmutableCollection {}

$numbers = new IntCollection([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, preserving the original collection type and maintaining key associations where applicable.

4.1) get(), getOrDefault(), first(), last()

$users = new UserCollection([
 'owner' => new User(1, 'Alice'),
]);

$owner = $users->get('owner'); // returns User
$guest = $users->getOrDefault('guest'); // returns null by default
$guest2 = $users->getOrDefault('guest', new User(0, 'Guest')); // returns default

// Distinguishes missing key from existing null value
$values = new class (['a' => null]) extends AdachSoft\Collection\AbstractCollection {};
var_dump($values->getOrDefault('a', 'x')); // null
var_dump($values->getOrDefault('b', 'x')); // 'x'

// New in 3.0.0
$first = $users->first(); // first value by insertion order
$last = $users->last(); // last value by insertion order

5) Immutable, typed map (keys and values)

Use AdachSoft\Collection\AbstractImmutableMap for key/value collections with strict, explicit types for keys and values. Maps now also expose first()/last() returning values.

<?php

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

/**
 * @extends AbstractImmutableMap<string, User>
 */
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
$ownerOrDefault = $map->getOrDefault('admin', null); // default when missing
$first = $map->first(); // first value
$last = $map->last(); // last value

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

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

Note: starting from 3.0.1, AbstractImmutableMap does not internally propagate the legacy $type parameter when creating new instances (e.g. in map/filter). Key/value validation is fully driven by getKeyType()/getValueType().

6) Mutable, typed map

Use AdachSoft\Collection\AbstractMap to allow in-place mutation of key/value pairs while keeping strict types. Maps now also expose first()/last().

<?php

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

/**
 * @extends AbstractMap<string, User>
 */
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([
 'x' => new User(1, 'Alice'),
]);
$map->set('x', new User(2, 'Bob')); // updates existing key
$map->removeKey('x');
$map->clear();

$values = $map->values();
$keys = $map->keys();
$all = $map->toArray();
$first = $map->first();
$last = $map->last();

7) Constructors accept iterable

All collections and maps accept iterable as constructor argument. This means you can pass arrays, generators, or other collections.

Note: Keys must be of type int|string. In maps, invalid key/value types will throw domain exceptions at construction time.

8) Merge API for collections and maps

The library provides a unified merge method for both collections and maps via MergeableCollectionInterface.

Signature:

public function merge(
 iterable $source,
 MergeConflictModeEnum $mode = MergeConflictModeEnum::OVERWRITE,
 bool $preserveKeys = true,
 ?string $appendSuffix = '_copy'
): static;

Rules summary:

  • Immutable classes return a new instance with changes; mutable classes modify $this.
  • Collections: $preserveKeys === false appends; $preserveKeys === true respects keys with conflict strategies.
  • Maps: $preserveKeys is ignored; APPEND_NEW_KEY: string suffix for string-key maps, next free int for int-key maps.

Exceptions:

  • InvalidArgumentException, MergeConflictException, InvalidKeyTypeException, InvalidItemTypeException as appropriate.

Contracts

Prefer the contracts from AdachSoft\Collection\Contract:

  • ReadableCollectionInterface for read-only collections (now with first/last/hasKey(int|string))
  • MutableCollectionInterface for mutable collections
  • KeyedCollectionInterface for maps (now with first/last)
  • MutableKeyedCollectionInterface for mutable maps
  • MergeableCollectionInterface for a unified merge operation across collections and maps

Tooling

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

Changelog

See CHANGELOG.md for release notes, including BC breaks in 3.0.0 and the incremental changes in 3.0.1.

License

MIT