SkillAgentSearch skills...

Collection

πŸ—ΊοΈ Type-safe, key-preserving, mutable/immutable List/Set/Map for PHP

Install / Use

/learn @noctud/Collection
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Noctud Collection

Docs codecov Latest Stable Version License: MIT Discord

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, bool or "1" retain their original types. No silent type casting.
  • Object keys: Use objects as map keys out of the box. Implement Hashable for 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, OrNull conventions, 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).
  • *By takes a selector, *With takes a comparator. Add Desc for 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 I
View on GitHub
GitHub Stars44
CategoryDevelopment
Updated2d ago
Forks0

Languages

PHP

Security Score

95/100

Audited on Mar 25, 2026

No findings