New:Introducing Socket Scanning for VS Code Marketplace Extensions.Learn more →
Get Started

adachsoft/collection

Package Overview
Dependencies
Maintainers
1
Versions
12
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

adachsoft/collection

A collection library providing flexible and immutable collections.

Source
Packagist
Version
v3.0.2
Version published
Total downloads
205
Maintainers
1
Created
Source

AdachSoft Collection Library

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:

  • Run Rector directly with this config:
vendor/bin/rector process src \
    --config=vendor/adachsoft/collection/rector/collection-301-migration.php
  • 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

Keywords

collections

FAQs

Package last updated on 23 Feb 2026

Related posts