lambdish/phunctional
lambdish/phunctional is a functional programming toolkit for PHP and Laravel. It offers immutable collections, Option and Either types, currying, function composition, memoization, and pipeline helpers, making it easier to write expressive, side-effect-aware code without leaving familiar PHP.
Install the package with Composer. There is no service provider or facade to register — Phunctional registers its function file through Composer's autoload, so it's usable immediately.
composer require lambdish/phunctional
The functions live under the Lambdish\Phunctional namespace. Import only what you use, once per file:
use function Lambdish\Phunctional\{map, filter, reduce, pipe};
Where to look first:
src/functions.php in the repository is the complete API surface — it's flat and short, so this is the fastest way to learn what's available.tests/ directory doubles as documentation because every function has example usage and edge cases.First realistic use case is replacing a hand-rolled loop over an array or generator with a small pure pipeline:
use function Lambdish\Phunctional\{map, filter, reduce, pipe};
$total = pipe(
fn (iterable $orders) => filter(fn ($o) => $o->isPaid(), $orders),
fn (iterable $orders) => map(fn ($o) => $o->total, $orders),
fn (iterable $totals) => reduce(fn ($acc, $t) => $acc + $t, $totals, 0),
)($orders);
// Or point-free, curried first:
$sumPaid = reduce(fn ($acc, $t) => $acc + $t, map(fn ($o) => $o->total, filter(fn ($o) => $o->isPaid(), $orders)), 0);
Note the convention: callbacks come first, data comes last (Ramda-style). That's what makes the functions convenient to curry and compose.
Pipelines over nested calls. pipe (left-to-right) and compose (right-to-left) are the core of the library. Reach for them whenever you'd otherwise produce three intermediate variables or stack three nested calls. In Laravel controllers and jobs, a pipeline of three or four pure stages usually reads better than a chain of mutating statements.
Curry for reusable transforms. Because data is the last argument, you can pre-bind the callback once and reuse it:
use function Lambdish\Phunctional\{curry, map};
$toDtos = curry(map)(fn (User $u) => UserDto::fromModel($u));
$dtos = $toDtos($users); // works over arrays, iterators, Collections
Lazy-friendly iterable handling. Phunctional functions accept any iterable, so they pair naturally with Eloquent's cursor() and lazy() (or a generator from ->getIterator()). This is where they genuinely beat collect() on memory:
$emails = map(
fn (User $u) => $u->email,
User::where('active', true)->cursor(),
);
Because map/filter on iterables yield generators, nothing is materialized until you foreach the result — ideal for exports and chunked jobs.
Grouping and indexing. groupBy, indexBy, and pluck are the functions most useful day-to-day because they replace the awkward "loop + push into array" pattern:
$byStatus = groupBy(fn (Order $o) => $o->status, $orders);
$byId = indexBy(fn (Order $o) => $o->id, $orders);
Memoizing expensive lookups. memoize wraps a callable and caches by serialized arguments. It's a lightweight fit for config lookups, per-request reference tables, or tax/fee calculators invoked repeatedly inside a loop — no need for a cache-store dependency.
Side effects at boundaries. Use tap/each to log or mutate at a single point in a pipeline rather than sprinkling logger() calls through callbacks. Keep the pure stages pure and put observability at the edges.
Testing. Because functions are referentially transparent, unit tests are trivial — pass a literal array, assert on the returned iterable (iterator_to_array(...) if you need a concrete array). No container bootstrapping, no mocking.
Namespace-only, no global helpers. You must add a use function line in every file, and IDE autocompletion can be weak unless you have the meta/stub file indexed. It's easy for reviewers to miss that a name like map in a file is Phunctional's and not a local helper.
Argument order flips against Laravel's fluent style. Phunctional is data-last (map($fn, $items)); Collection is data-first (method on the collection). Mixing the two in the same method is a common bug source — pick one style per file and stick to it.
You get iterables back, not Collections. Wrapping a Collection through Phunctional gives you a Traversable. Before handing results back to Laravel code that expects Collection methods (->toArray(), ->pluck(), ->paginate()), convert explicitly: collect(map(...)) or collect(iterator_to_array(...)). Similarly, don't call map on a Collection inside a Blade loop expecting a Collection API.
The 2020 release date matters. Verify the composer.json PHP constraint against your app's version before adding it to a modern Laravel project. If the constraint is stale, either pin with --ignore-platform-reqs (documented in your README), or, for a dependency this small, vendor/fork it — the source is a single function file.
Currying arity inference is reflection-based. Default arguments and variadic parameters can be inferred incorrectly. If a curried function "fires" too early or too late, switch from curry to curryN($n, $fn) and pass the arity explicitly.
memoize keys are serialized. Scalar arguments are fine; passing models or large object graphs as keys will serialize objects and can silently blow memory or miss the cache when object identity changes. Memoize on IDs, not entities.
Debugging pipelines is the weak spot. There's no step-through. Drop tap(fn ($x) => logger()->debug(compact('x'))) between stages while developing, then remove it. Stack traces through deeply curried closures are often unhelpful; break a long pipeline into named stages when you hit a bug.
Generators are single-use. If you foreach a mapped generator and then need it again, you'll get an empty iteration the second time. Materialize with collect() / iterator_to_array() when the value is consumed twice.
Extending it. There's no plugin or extension point — it's just namespaced functions. If you need project-specific helpers, create a small App\Support\Fn class (or your own functions.php under your own namespace) rather than adding functions to Lambdish\Phunctional, which Composer won't autoload and which will break if the package is ever updated or replaced.
Static analysis is loose. There are no rich stubs, so PHPStan/Psalm won't infer the iterable's element type through a pipeline. Add @param callable(int): string-style docblocks on the callables you pass in, or the analysis will degrade to mixed at every stage.
How can I help you explore Laravel packages today?