whsv26/functional
Functional programming primitives for PHP: Option, Either, Pair, and immutable collections with map, filter, fold, and pattern matching. Handle nulls and errors without exceptions, chain transformations safely, and write expressive, side-effect-free code that works with Laravel and static analysis.
composer require whsv26/functional
There is no service provider, facade, or config to publish — it's a framework-agnostic PHP library that works fine inside a Laravel app, a CLI tool, or a queue worker. Add it and start importing.
Where to look first:
README.md — skim it for the canonical Option/Either examples and the Prelude helper list.src/Monad/ — this is where Option, Either, TryCatch, and friends live. This is the 80% of what you'll use day to day.src/Collection/ — Seq, Map, Set and their transformation APIs.tests/ directory — in practice this is the most reliable documentation for exact method names and constructor signatures.compose, pipe, curry, identity) — usually a Prelude or functions.php.First use case — kill a nullable branch. The lowest-risk, highest-value entry point is replacing ?-> chains and if ($x === null) guards with an Option. You get a uniform pipeline and an explicit "absence" value that reads better than scattered null checks:
use Whsv26\Functional\Monad\Option\Option;
$displayName = Option::of($user?->profile?->display_name)
->map(fn (string $n) => trim($n))
->filter(fn (string $n) => $n !== '')
->getOrElse('Anonymous');
(Method/constructor names are illustrative — confirm against the source before committing to a pattern.)
Start with one bounded context: a single service class or a single report builder. Don't convert the whole codebase on day one.
Option at repository boundaries. Have repository methods return Option<Model> for "find by id" style lookups instead of leaking null. Your service layer then map/flatMap over the result and never touches a null. Convert back at the controller/job boundary with getOrElse or fold.
$invoice = $invoiceRepo->find($id) // Option<Invoice>
->map(fn (Invoice $i) => $i->withTax($rate))
->getOrElseThrow(fn () => new InvoiceNotFound($id));
Either / TryCatch for validation and error flow. This is the pattern that pays off most in domain services. Instead of throwing exceptions for expected failures (invalid state transitions, business rule violations), return Either<Error, Value> and short-circuit with flatMap. Reserve exceptions for genuinely exceptional conditions. TryCatch is the wrapper for third-party calls that do throw.
$result = $this->validate($dto) // Either<array, Dto>
->flatMap(fn ($d) => $this->reserveStock($d)) // Either<array, Reservation>
->map(fn ($r) => $this->toResponse($r));
Collection pipelines for report/aggregation code. When you find yourself writing nested foreach + array_filter + array_map + accumulator variables, a Seq pipeline is usually clearer:
Seq::of($orders)
->filter(fn ($o) => $o->isPaid())
->flatMap(fn ($o) => $o->lines())
->groupBy(fn ($l) => $l->sku())
->map(fn ($lines) => $lines->sum(fn ($l) => $l->total()))
->toArray();
Keep Eloquent queries doing the filtering in SQL — use these collections after you've fetched, not as a replacement for where().
Composition for reusable transforms. compose/pipe/curry shine when you have small, reusable mappers that get combined differently per endpoint. Curry a "money formatter" or a "normalize SKU" function once, then reuse it in map calls without inline closures.
Keep monads at the edges. A good rule: domain internals may speak Option/Either; your controllers, API resources, jobs, and events should return native arrays, DTOs, and exceptions. Laravel's ecosystem (validation, exception handler, queue serialization) does not understand monads. Convert deliberately with fold/getOrElse / match at the boundary.
Fold to render, not to branch. Use fold/pattern-match at the very end of a chain to produce a value or side effect; avoid calling isSome()/isLeft() and branching midway unless you genuinely need two different control paths.
Laravel integration tips. Wrap FormRequest → DTO mapping in an Either to accumulate validation errors coherently. Use TryCatch around HTTP clients and payment gateways. In queued jobs, unwrap monads before dispatching so the serialized payload is plain data.
Generics are PHPDoc-only. The Option<T>, Seq<T> types are not enforced at runtime. Add PHPStan or Psalm at a decent level, or you'll get false confidence — a map returning the wrong type will blow up far from where it was written.
Immutability means "use the return value." Collection and monad methods return new instances; nothing mutates in place. The single most common beginner bug is calling ->map(...) and discarding the result. If a chain "does nothing," check you assigned the return.
Object overhead is real. Every wrapped value allocates. Don't put Option around things in hot loops or tight numeric code, and don't wrap entire large arrays in Seq only to unwrap them. Use collections where the pipeline clarity is worth the allocation; use plain arrays where it isn't.
Don't put monads or closures in the cache/queue. A Seq holding a closure from map is not serializable. If you're caching results or dispatching jobs, call ->toArray() / getOrElse() first.
Debugging is awkward — stringify explicitly. Dumping an Option or Either gives you an object graph, not a value. Break the chain and inspect the intermediate with fold/->toArray()/var_export rather than dd($monad). With Xdebug, set breakpoints inside the closures: the stack trace will be full of map/flatMap frames, so name your closures or add logging at stage boundaries.
Never mix null semantics and Option in the same layer. Pick one convention per module. A method that "returns Option but might also return null if you call the wrong overload" will destroy the benefit immediately. Same with Either vs exceptions — document which layer converts.
Know what throws. Confirm in the source whether getOrElseThrow, head, or get on a None throws and what. Wrap at the boundary and translate into your domain exceptions so callers get consistent errors.
Startup/learning curve. Functional vocabulary (functor, applicative, monad, Kind) is not obvious to a team that hasn't used it. Pilot it in one service, write two or three copy-pasteable examples in your team wiki, and code-review aggressively for the first few PRs. Over-applied in framework glue code, it makes Laravel feel alien; applied in domain services and aggregation code, it makes them much cleaner.
Extension points. If you need your own container type (a Result, a Validation accumulator), look at how the library emulates higher-kinded types (typically a Kind/Box wrapper plus typeclass interfaces). You can implement the same interfaces so your type composes with the library's map/flatMap/fold. This is the intended escape hatch — but only reach for it once you're fluent with the built-ins.
Pin the version. composer.json should constrain to a caret range and you should check the changelog before upgrading — pre-1.0 functional libraries occasionally rename static constructors (some vs Some, none vs None), which ripples across every call site.
How can I help you explore Laravel packages today?