noctud/collection
Composer 安装命令:
composer require noctud/collection
包简介
🗺️ Type-safe, key-preserving, mutable/immutable List/Set/Map for PHP
README 文档
README
Type-safe, mutable/immutable, sortable and key-preserving List/Map/Set collections for PHP 8.4+.
composer require noctud/collection
✨ Features
- Type-safe: Full generics support. Static analyzers understand every element type through the chain.
- Key-preserving: Map keys like
float,boolor"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
Array access is strict by default — throws on missing keys/indices. Use ?? for safe fallback.
$list[0]; // throws if missing, get() $list[0] ?? null; // null if missing $list->getOrNull(0); // null if missing $list->firstOrNull(); // null if empty
$map['key']; // throws if missing, get() $map['key'] ?? null; // null if missing $map->getOrNull('key'); // null if missing $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
统计信息
- 总下载量: 650
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 48
- 点击次数: 1
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-02-15