noctud / collection
Installs: 4
Dependents: 0
Suggesters: 0
Security: 0
Stars: 27
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/noctud/collection
Requires
- php: >=8.4
Requires (Dev)
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^13.0
- slevomat/coding-standard: ^8.16
- squizlabs/php_codesniffer: ^3.11
This package is not auto-updated.
Last update: 2026-02-16 18:13:49 UTC
README
Type-safe, mutable/immutable, sortable and key-preserving List/Map/Set collections for PHP 8.4+.
composer require noctud/collection:0.1.0-beta1
β¨ Features
- Type-safe: Full generics support. Static analyzers understand every element type through the chain.
- Key-preserving: Map keys like null, float, bool or "1" retain their original types. No silent type casting.
- Object keys: Use objects as map keys out of the box. Implement
Hashablefor custom identity semantics. - Mutable & Immutable: Choose the right variant. Immutable methods are marked with
#[NoDiscard]. - Lazy Init: Construct collections from closures. Uses PHP 8.4 lazy objects β materialized only on first access.
- Interface-driven: Every type is an interface. Factory functions return contracts, not concrete classes.
- Expressive: Rich set of higher-order functions β map, filter, sorted, flatMap, groupBy, partition, and more.
- Chainable: Mutating methods return a collection β read result like
$set->tracked()->add('a')->changed. - Strict: Choose between throwing and nullable methods (
get/getOrNull,first/firstOrNull, etc.) - Inspired by Kotlin: Factory functions, mutable/immutable split,
OrNullconventions, and namings.
πΊοΈ Architecture
Collection<E> β Ordered elements, read-only βββ List<E> β Indexed, array access β βββ MutableList<E> β Mutating methods (write & sort) β βββ ImmutableList<E> β Mutation returns new with #[NoDiscard] βββ Set<E> β Unique values, no array access βββ MutableSet<E> β Mutating methods (write & sort) βββ ImmutableSet<E> β Mutation returns new with #[NoDiscard] Map<K,V> β Ordered key-value pairs, array access βββ MutableMap<K,V> β Mutating methods (write & sort) βββ ImmutableMap<K,V> β Mutation returns new with #[NoDiscard]
Full architecture is shown in docs, there are also Writable interfaces for easy third party implementations.
ποΈ Constructing
Use factory functions from namespace Noctud\Collection.
setOf(['a', 'b']); // ImmutableSet<string> mutableSetOf(['a', 'b']); // MutableSet<string> listOf(['a', 'b']); // ImmutableList<string> mutableListOf(['a', 'b']); // MutableList<string> mapOf(['a' => 1, 'b' => 2]); // ImmutableMap<string, int> mutableMapOf(['a' => 1, 'b' => 2]); // MutableMap<string, int>
Use stringMapOf/mutableStringMapOf and intMapOf/mutableIntMapOf for better
performance and ~50% less memory β they use single-array storage and enforce key types at runtime.
π Accessing
Always two options β nullable or throwing.
The need for array_key_exists() checks or ?? null is eliminated.
$list[0]; // null if missing, getOrNull() $list(0); // throws if missing, get() $list->firstOrNull(); // null if empty
$map['key']; // null if missing, getOrNull() $map('key'); // throws if missing, get() $map->values->first(); // throws if empty
Sets support only the contains method, they have no array access by design.
πͺοΈ Filtering & transformations
All transformation methods (filter, map, flatMap, zip, partition, ...) always return a new immutable collection, regardless of whether the source is mutable or immutable. Unlike array_filter, Lists are always reindexed β no gaps, no need for array_values().
$set->filter(fn($el) => strlen($el->property) > 3); // new Set<E> $map->filter(fn($v, $k) => strlen($k->property) > 3); // new Map<K,V> $map->filterValuesNotNull(); // new Map<K,V> where V is not null $map->values->filter(fn($v) => $v > 10); // new Collection<V>
π Sorting
Every Collection and Map is sequentially ordered, so sorting is supported everywhere.
sorted*returns a new collection,sort*sorts in place (Mutable only).*Bytakes a selector,*Withtakes a comparator. AddDescfor descending.
// Basic $list->sort(); // also sortDesc() $map->sortByKey(); // also sortByValue() // Selector examples $list->sortBy(fn ($v) => $v->score); $map->sortByKeyDesc(fn ($k) => strlen($k)); // Comparator examples (advanced use cases) $list->sortWith(fn ($a, $b) => $b->score <=> $a->score); $map->sortWithKey(fn ($a, $b) => $a <=> $b); // also sortWithValue() $map->sortWith(fn (MapEntry $a, MapEntry $b) => $a->value <=> $b->value);
ποΈ Map views
Every Map exposes live read-only $keys, $values, and $entries views. These are real Set and Collection objects backed by the same underlying store β mutations to the map are immediately visible through views and vice versa.
$map = mapOf(['alice' => 28, 'bob' => 35, 'carol' => 22]); $map->values->min(); // 22 $map->keys->filter(fn($k) => strlen($k) > 3); // Set {'alice', 'carol'} $map->entries->first(); // MapEntry { key: 'alice', value: 28 }
βοΈ Quantifiers
Check if all/any or none of the elements match the predicate.
$set->all(fn($v) => strlen($v->property) > 3); // true|false $map->any(fn($v, $k) => strlen($k->property) > 3); // true|false $map->values->none(fn($v) => $v->isActive); // true|false
β° Iterating
All collections are traversable.
$set->forEach(fn($v) => print("$v->property\n")); $map->forEach(fn($v, $k) => print("$k = $v\n")); // Keys for Sets are generated on the fly (0, 1, 2, ...) foreach ($collection as $k => $v) { print("$k = $v\n"); }
βοΈ Chainable
Mutating methods return $this (Mutable) or a new instance (Immutable). Both share the same API, but immutable methods are marked with #[NoDiscard] to prevent accidental misuse.
$new = $map->put('b', 2) ->remove('a') ->filter(fn($v, $k) => $v > 1) ->mapValues(fn($v, $k) => $v * 2) ->sortedByKey(); $mutableSet->clear() ->addAll(['a', 'b', 'c', null]) ->removeIf(fn($v) => $v === null);
Method tracked() wraps a mutable collection in a proxy that tracks changes. The $changed flag is available on the return value of each mutation method, not on the wrapper itself.
$map = mutableMapOf(['a' => 'b']); if ($map->tracked()->remove('a')->changed) { // do something only if 'a' was actually removed }
π‘οΈ Type safety
Mutable collections enforce strict typing β PHPStan warns if you try to add elements of incompatible types. Immutable collections allow type widening since they return a new instance with potentially different types.
// Mutable β strict, PHPStan warns on type mismatch $map = mutableMapOf(['a' => 1]); // MutableMap<string, int> $map->put('b', 'wrong'); // β PHPStan error: string is not int // Immutable β widening allowed, returns new instance $map = mapOf(['a' => 1]); // ImmutableMap<string, int> $new = $map->put('b', 'text'); // β ImmutableMap<string, int|string>
π Preserving key types
$map = mutableMapOf(['1' => 'a']); // β Key '1' will be cast to int(1) before the map is created $map = mutableMapOfPairs([['1', 'a']]); // β Key '1' will stay as a string $map['2'] = 'b'; // β Key '2' will stay as string // Enforce string keys (int are only allowed at construction time) $map = stringMapOf(['1' => 'a', 2 => 'b']); // β Keys '1' and '2' will be strings // Constructing from a generator $map = mapOf((function() { yield '1' => 'a'; // β Key '1' will stay as a string })());
Map will always preserve original keys, you have to only worry about constructing the map.
π€ Lazy Initialization
Construct from a closure β the callback executes only on first access. Under the hood, lazy collections use PHP 8.4's Lazy Objects β the internal store is a ghost proxy materialized only when first accessed.
// The query runs only if $users is actually read $template->users = listOf(fn() => $repository->getAllUsers()); $lazyMap = mapOf(fn () => ['a' => 1]); // β Good, callback returning an array $lazyMap = mapOf(fn () => $generator); // β Good, callback returning Generator $lazyMap->values; // still lazy, no code executed yet $lazyMap->count(); // first read - executes the callback, materializes the map
Lazy collections behave identically to eager ones β there is no way to tell from outside. Always construct lazy collections using closures, not Generator objects directly.
γοΈ Objects as keys
Use objects as map keys out of the box. By default, objects are hashed using spl_object_id.
$map = mapOfPairs([[$user, 'data']]); isset($map[$user]); // β True, same object instance isset($map[clone $user]); // β False, different instance
Implement Hashable for custom identity semantics:
class User implements \Noctud\Collection\Hashable { public function identity(): string|int { return "user_$this->id"; } } $map = mutableMapOf(); $map[$user] = 'cacheData'; isset($map[clone $user]); // β True, same user ID
π§© Extending
Every type you interact with is an interface β ImmutableList, MutableMap, Set, even MapEntry. Logic is encapsulated in traits, so you can turn any class into a collection.
For custom stores, database-backed collections, and more, see the Extending guide.
π Performance
This library prioritizes type safety and correctness. Lists and Sets have minimal overhead compared to native arrays. The generic mapOf() uses dual-array storage to preserve any key type, which adds memory and performance overhead.
When keys are exclusively strings or integers, use the optimized variants for maximum performance:
$users = stringMapOf(['alice' => 28, 'bob' => 35]); // or mutableStringMapOf() $scores = intMapOf([1 => 100, 2 => 85, 3 => 92]); // or mutableIntMapOf()
These use single-array storage, skip key hashing entirely, and use ~50% less memory than mapOf().
Converting between mutable and immutable via toMutable()/toImmutable() uses copy-on-write β the data is shared until either side is modified, making variant switching virtually free.
π Static analysis
Generics are fully supported by PHPStan and Psalm. PhpStorm has known limitations with generics inference.
π Documentation
- Getting started β Installation, architecture, basic usage
- List / Set / Map β Type guides with examples
- Mutability β Mutable vs immutable, change tracking, copy-on-write
- Sorting β Full sorting reference with quick-reference table
- Lazy collections β Deferred initialization
- Extending β Custom implementations, stores, traits
- API reference β All method signatures