adachsoft / collection
A collection library providing flexible and immutable collections.
Installs: 16
Dependents: 6
Suggesters: 0
Security: 0
Stars: 0
Forks: 0
pkg:composer/adachsoft/collection
Requires
- php: ^8.3
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^12.3
This package is not auto-updated.
Last update: 2025-10-11 12:34:55 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 this version:
- 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
- getOrDefault() for maps
- Constructors accept iterable in all collections and maps
- Advanced 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
All collections (both mutable and immutable) support functional operations:
<?php
use AdachSoft\Collection\AbstractImmutableCollection;
// A simple immutable collection without item type constraints
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() and getOrDefault() for base collections
$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'
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
$ownerOrDefault = $map->getOrDefault('admin', null); // returns default when missing
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(...)]
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([
'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();
7) Constructors accept iterable
All collections and maps accept iterable
as constructor argument. This means you can pass:
- plain arrays
- generators (with or without keys)
- other collections (any
IteratorAggregate
orTraversable
)
Examples:
// from an array
$users = new UserCollection([$user1, $user2]);
// from a generator
$gen = (function () {
yield 'a' => new User(1, 'Alice');
yield 'b' => new User(2, 'Bob');
})();
$map = new MutableUserMap($gen);
// from another collection (preserves keys)
$users2 = new UserCollection($users);
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;
General rules:
- Immutable classes return a new instance with changes; mutable classes modify the current instance and return
$this
(fluent). - For collections:
- When
$preserveKeys === false
, items are appended (no conflicts occur,$mode
is ignored). - When
$preserveKeys === true
, keys in$source
must beint|string
; conflicts are resolved per$mode
. - APPEND_NEW_KEY generates a new unique string key:
{key}{suffix}
,{key}{suffix}_2
, ...
- When
- For maps:
$preserveKeys
is ignored (keys are always meaningful and preserved).- Key and value types are validated using the map’s
getKeyType()
andgetValueType()
. - APPEND_NEW_KEY: if string keys are allowed, a string key with suffix is generated; otherwise, the next free integer key is used (validated against key type).
Conflict strategies (MergeConflictModeEnum):
- OVERWRITE – replace existing value with source value
- SKIP – keep existing value, ignore source value
- FAIL – throw MergeConflictException on conflict
- APPEND_NEW_KEY – add the source value under a new unique key
Exceptions:
- InvalidArgumentException – for collections when
$preserveKeys === true
and a source key is notint|string
- MergeConflictException – when
$mode === FAIL
and a conflict is detected - InvalidKeyTypeException – for maps when a key does not match the key type
- InvalidItemTypeException – for maps when a value does not match the value type
Examples:
use AdachSoft\Collection\Type\MergeConflictModeEnum;
// Collections (mutable)
$col = new UserCollection(['a' => new User(1, 'A')]);
$col->merge(['a' => new User(2, 'B')], MergeConflictModeEnum::OVERWRITE); // overwrites
$col->merge(['a' => new User(3, 'C')], MergeConflictModeEnum::APPEND_NEW_KEY, true, '_cpy'); // adds 'a_cpy'
// Collections (immutable)
$col = new ImmutableUserCollection(['a' => new User(1, 'A')]);
$new = $col->merge(['a' => new User(2, 'B')], MergeConflictModeEnum::SKIP); // original unchanged
// Maps (mutable)
$map = new MutableUserMap(['owner' => new User(1, 'A')]);
$map->merge(['owner' => new User(2, 'B')], MergeConflictModeEnum::OVERWRITE); // overwrites
// Maps (immutable)
$imap = new UserMap(['owner' => new User(1, 'A')]);
$imap2 = $imap->merge(['owner' => new User(2, 'B')], MergeConflictModeEnum::SKIP);
Examples
The repository contains runnable examples in the examples/
directory that demonstrate typical usage patterns of the library:
examples/01-mutable-collection.php
– Mutable object collection (PostCollection)examples/02-immutable-collection.php
– Immutable object collection (TagCollection)examples/03-mutable-map.php
– Mutable typed map: int => User (IdToUserMap)examples/04-immutable-map.php
– Immutable typed map: string => float (PriceBySymbolMap)
Run any example with PHP (Composer autoload required):
php examples/01-mutable-collection.php
php examples/02-immutable-collection.php
php examples/03-mutable-map.php
php examples/04-immutable-map.php
Contracts
Prefer the contracts from AdachSoft\Collection\Contract
:
ReadableCollectionInterface
for read-only collectionsMutableCollectionInterface
for mutable collectionsKeyedCollectionInterface
for immutable maps (now with getOrDefault)MutableKeyedCollectionInterface
for mutable mapsMergeableCollectionInterface
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.
License
MIT