PHPackages                             noctud/collection - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. noctud/collection

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

noctud/collection
=================

v0.1.1(2mo ago)4374MITPHPPHP &gt;=8.4CI passing

Since Feb 15Pushed 2mo ago1 watchersCompare

[ Source](https://github.com/noctud/collection)[ Packagist](https://packagist.org/packages/noctud/collection)[ RSS](/packages/noctud-collection/feed)WikiDiscussions 0.1.x Synced 1mo ago

READMEChangelog (2)Dependencies (8)Versions (5)Used By (0)

Noctud Collection
=================

[](#noctud-collection)

Type-safe, mutable/immutable, sortable and key-preserving List/Map/Set collections for **PHP 8.4+**.

[![Docs](https://camo.githubusercontent.com/7198d57ebd3a922695cfc0b14867222cf586a07e8054e0671ad15ea911d77275/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f646f63732d6e6f637475642e6465762d384132424532)](https://noctud.dev/collection/getting-started)[![codecov](https://camo.githubusercontent.com/0c67b2ca97c763d98a713cffcee2775dc3dbc2b7d49f10aa392577cf5dfb00d1/68747470733a2f2f636f6465636f762e696f2f67682f6e6f637475642f636f6c6c656374696f6e2f6272616e63682f302e312e782f67726170682f62616467652e737667)](https://codecov.io/gh/noctud/collection)[![Latest Stable Version](https://camo.githubusercontent.com/0f654225fb0f7e14d555dfd2ad3798d4fe84443926fcde082cec77ae33889ab1/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6e6f637475642f636f6c6c656374696f6e2e737667)](https://packagist.org/packages/noctud/collection)[![License: MIT](https://camo.githubusercontent.com/08cef40a9105b6526ca22088bc514fbfdbc9aac1ddbf8d4e6c750e3a88a44dca/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d626c75652e737667)](https://camo.githubusercontent.com/08cef40a9105b6526ca22088bc514fbfdbc9aac1ddbf8d4e6c750e3a88a44dca/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d626c75652e737667)[![Discord](https://camo.githubusercontent.com/248061ff84b21b0f1f74dc4c740e9792447d50cffc545a245001680100321ae4/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f646973636f72642d6a6f696e2d3538363546323f6c6f676f3d646973636f7264266c6f676f436f6c6f723d7768697465)](https://discord.gg/jS3fKe6vW9)

```
composer require noctud/collection:0.1.0-beta2
```

✨ Features
----------

[](#-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 `Hashable` for custom identity semantics.
- **Mutable &amp; 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
---------------

[](#️-architecture)

```
Collection               → Ordered elements, read-only
├── List                 → Indexed, array access
│   ├── MutableList      → Mutating methods (write & sort)
│   └── ImmutableList    → Mutation returns new with #[NoDiscard]
└── Set                  → Unique values, no array access
    ├── MutableSet       → Mutating methods (write & sort)
    └── ImmutableSet     → Mutation returns new with #[NoDiscard]

Map                    → Ordered key-value pairs, array access
├── MutableMap         → Mutating methods (write & sort)
└── ImmutableMap       → Mutation returns new with #[NoDiscard]
```

Full architecture is [shown in docs](https://noctud.dev/collection/getting-started#architecture), there are also Writable interfaces for easy third party implementations.

### 🏗️ Constructing

[](#️-constructing)

Use [factory functions](https://noctud.dev/collection/api/functions) from namespace `Noctud\Collection`.

```
setOf(['a', 'b']); // ImmutableSet
mutableSetOf(['a', 'b']); // MutableSet

listOf(['a', 'b']); // ImmutableList
mutableListOf(['a', 'b']); // MutableList

mapOf(['a' => 1, 'b' => 2]); // ImmutableMap
mutableMapOf(['a' => 1, 'b' => 2]); // MutableMap
```

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

[](#-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 &amp; transformations

[](#️-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
$map->filter(fn($v, $k) => strlen($k->property) > 3); // new Map
$map->filterValuesNotNull(); // new Map where V is not null
$map->values->filter(fn($v) => $v > 10); // new Collection
```

### 📊 Sorting

[](#-sorting)

Every Collection and Map is sequentially ordered, so [sorting](https://noctud.dev/collection/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

[](#️-map-views)

Every Map exposes live read-only [`$keys`, `$values`, and `$entries`](https://noctud.dev/collection/map#views) 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

[](#️-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

[](#-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

[](#️-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()`](https://noctud.dev/collection/mutability#change-tracking) 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](https://noctud.dev/collection/mutability)

[](#️-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
$map->put('b', 'wrong'); // ❌ PHPStan error: string is not int

// Immutable — widening allowed, returns new instance
$map = mapOf(['a' => 1]); // ImmutableMap
$new = $map->put('b', 'text'); // ✅ ImmutableMap
```

### 🔑 [Preserving key types](https://noctud.dev/collection/map#preserving-key-types)

[](#-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](https://noctud.dev/collection/lazy-collections)

[](#-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](https://www.php.net/manual/en/language.oop5.lazy-objects.php) — 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](https://noctud.dev/collection/map#objects-as-keys)

[](#️-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

[](#-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](https://noctud.dev/collection/extending).

### 🚀 Performance

[](#-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

[](#-static-analysis)

Generics are fully supported by PHPStan and Psalm. PhpStorm has known limitations with generics inference.

📚 Documentation
---------------

[](#-documentation)

- [Getting started](https://noctud.dev/collection/getting-started) — Installation, architecture, basic usage
- [List](https://noctud.dev/collection/list) / [Set](https://noctud.dev/collection/set) / [Map](https://noctud.dev/collection/map) — Type guides with examples
- [Mutability](https://noctud.dev/collection/mutability) — Mutable vs immutable, change tracking, copy-on-write
- [Sorting](https://noctud.dev/collection/sorting) — Full sorting reference with quick-reference table
- [Lazy collections](https://noctud.dev/collection/lazy-collections) — Deferred initialization
- [Extending](https://noctud.dev/collection/extending) — Custom implementations, stores, traits
- [API reference](https://noctud.dev/collection/api/collection) — All method signatures

###  Health Score

43

—

FairBetter than 91% of packages

Maintenance85

Actively maintained with recent releases

Popularity23

Limited adoption so far

Community9

Small or concentrated contributor base

Maturity44

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 70.8% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~7 days

Total

5

Last Release

63d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/4bf4ee88447a7ca740f8af98fb7d3616e62db0401458102425b2322d0cc4019d?d=identicon)[Delacry](/maintainers/Delacry)

---

Top Contributors

[![delacry](https://avatars.githubusercontent.com/u/45132928?v=4)](https://github.com/delacry "delacry (17 commits)")[![rixafy](https://avatars.githubusercontent.com/u/45132928?v=4)](https://github.com/rixafy "rixafy (7 commits)")

---

Tags

collectioncollectionsphpphp8php84static-analysis

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP\_CodeSniffer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/noctud-collection/health.svg)

```
[![Health](https://phpackages.com/badges/noctud-collection/health.svg)](https://phpackages.com/packages/noctud-collection)
```

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
