innmind/immutable
Innmind/Immutable provides persistent, immutable collections for PHP, including sequences, sets, maps, and streams. It offers a functional API with lazy operations, structural sharing, and type-safe wrappers for predictable, side-effect-free data manipulation.
Install with Composer (PHP 8.x is required on current majors):
composer require innmind/immutable
Look first at the README for the class map, then at src/ — each class mirrors a PHP primitive or an effect type:
Innmind\Immutable\Str — immutable, multibyte-aware stringInnmind\Immutable\Seq — ordered sequence (the old Stream)Innmind\Immutable\Set — unique valuesInnmind\Immutable\Map — key/value structure with Maybe-returning lookupsInnmind\Immutable\Maybe / Either — optional values and disjoint resultsA good first use case is replacing an array pipeline in one service with Seq, and replacing a ?User return with Maybe:
use Innmind\Immutable\Seq;
use Innmind\Immutable\Str;
$names = Seq::of('alice', 'bob', 'charlie')
->map(fn(string $name) => Str::of($name))
->filter(fn(Str $name) => $name->length() > 3)
->map(fn(Str $name) => $name->toUpper())
->toList(); // ['ALICE', 'CHARLIE']
That single refactor exercises the package's core value proposition: no defensive copying, no accidental mutation, and a small, composable API for the operations you already write by hand.
Convert at the boundaries, stay immutable inside. Controllers, jobs, and Eloquent are array/Collection-shaped. Wrap raw input into a Seq/Map in the entry point, do the domain work with the immutable types, then ->toList() (or ->toString()) before returning JSON, filling a model, or handing data back to Laravel. This keeps the mutable framework surface at the edges and gives you predictable value semantics in the middle.
Pipeline style instead of loops. map/filter/reduce/foreach cover almost everything you'd reach for a foreach to do. Use ->reduce() to compute aggregates and ->foreach() when the side effect is the point (logging, dispatching). Because every step returns a new collection, pipelines read top-to-bottom and are trivial to unit test one stage at a time.
Use Maybe for optionality, not null. ->get($key), ->find($callback), ->first(), and ->last() return Maybe. Compose with ->map()/->flatMap() and unwrap once with ->match(fn($v) => ..., fn() => ...) at the edge. This eliminates the if ($x !== null) ladder in lookups.
Use Either for typed failures. At a service boundary, return Either::left(DomainError) or Either::right($value). Consumers pattern-match instead of catching exceptions for expected outcomes; keep exceptions for truly exceptional states.
Choose the structure for the semantics. Set gives deduplication; Map gives keyed lookup via ->get() and is the right shape for "index this list by ID"; Seq preserves order and duplicates. Model domain value objects by holding a Str or Seq internally and exposing ->toString()/->toList() — this makes the immutability viral only inside your domain, not across the whole app.
Lean on the generics. The package is annotated for Psalm/PHPStan. Configure static analysis and you get typed map/filter callbacks and useful IDE hints; without it you lose most of the ergonomic benefit.
Laravel-specific integration. Alias imports to avoid confusion with framework helpers (use Innmind\Immutable\Str as IStr;), and be explicit about which layer owns string/collection logic. Populating a Seq of Eloquent models is fine for a read-only pipeline, but convert back before passing to Blade or query builders — those expect native types.
$seq->map(...) on its own is a no-op; you must reassign or chain. This is the most common source of "why is my data unchanged" bugs.Str is not a string. Laravel helpers, PDO, and json_encode won't accept it. Call ->toString() at the boundary; forgetting produces confusing type errors far from the cause.->equals(), not == or ===. Two Seqs with the same contents may be different objects.Map key handling is version-sensitive. Depending on the major, string keys are Str objects; mixing 'foo' and Str::of('foo') can cause silent lookup misses. Pick one convention and enforce it in a factory method.Maybe. $map->get($k) does not return null; don't if ($x === null) it — use ->match() or ->otherwise().Seq<Maybe> still iterates everything; convert to Maybe<Seq> early or collect then reduce.map/filter step still allocates an intermediate. In tight loops or large datasets, prefer a single ->reduce() or ->foreach() with a mutable accumulator over a long chain.var_dump. Internals are private wrappers; dump ->toList() / ->toString(), or use ->foreach(fn($v) => dump($v)) to inspect lazily without materializing twice.response()->json($seq) will not produce the array you expect; convert first.Stream → Seq rename). Read the upgrade notes before bumping, since this is a small-ecosystem package where the changelog is the documentation.Seq or implementing Collection unless you need interoperability with map/filter/reduce.null inside collections. Use an empty Seq::of() for "no items" and Maybe for "maybe one item"; mixing both representations in one codebase defeats the point of the package.How can I help you explore Laravel packages today?