nunomaduro/curryable
Laravel package by Nuno Maduro that lets you curry class methods, so multi-argument calls become chainable partial applications. Add the Curryable trait and call methods step by step, e.g. Calculator::add(1)(2), for cleaner functional-style PHP.
Architecture fit. Curryable is a framework-agnostic micro-utility — one curry() function and one Curryable trait — that converts any callable into a chain of single-argument functions. It registers no service provider, publishes no config, ships no facade or migration, and has no I/O, serialization, or network surface. Architecturally it is a leaf dependency: it composes with native closures and Laravel's container, Pipeline, and Collection::map, but nothing depends on it in turn. That makes it safe to adopt in isolation and trivial to remove. Its natural home is configuration-heavy internal code — formatter chains, pricing resolvers, validation pipelines, report builders, test factory builders — not controllers, models, or request hot paths.
Integration feasibility. The entire evaluation surface is a README that fits on one screen plus a few-hundred-line source tree. Install is composer require nunomaduro/curryable; there is nothing to wire. A meaningful spike — curry one existing re-wrapped closure, confirm partial application returns a Closure and full application returns a value, add declare(strict_types=1) — fits in an afternoon. The real integration work is not technical but organizational: locking an argument-order convention (volatile argument first, payload last), enforcing wiring-time binding, and standing up an internal wrapper.
Technical risk. The dominant risk is the silent-partial-application hazard: under-application does not throw, it returns a Closure that fails several frames downstream as a TypeError in unrelated code. Second is static-analysis degradation — PHPStan and Psalm infer Closure/mixed across curried call sites, and broad @phpstan-ignore suppression is a signal to shrink scope, not suppress harder. Third is arity detection via reflection: variadics, default values, named arguments, union/intersection types, by-reference parameters, and __invoke objects are exactly where reflection-based counting breaks, and a bad count produces a partial application that never completes. Fourth is the maturity profile — MIT, ~175 stars, single maintainer, zero recorded dependents, meaning no one has field-tested the edge cases against real code. The package is a few hundred lines, so replacement cost is roughly a 20-line internal wrapper; that caps the downside but does not eliminate the bug class.
Key questions.
App\Support\Functional\Curry, with an apply() guard) land before any application code imports the package function?Stack fit. Targets Laravel 10/11 on PHP 8.2+. Nothing about the runtime contract conflicts with the framework; the package is dependency-free and test-friendly. The important fit criterion is usage pattern, not stack version: curry at wiring time (service provider, factory, singleton binding) so reflection cost is amortized, and never inside Collection::map, queue jobs, or request handlers where construction re-reflects on every invocation.
Migration path. Brownfield and incremental by design — no coexistence shim, no dual-write, no data migration. The path is: land the wrapper → pick one candidate closure already re-wrapped at multiple call sites → curry it → measure diff size and readability → expand module by module. Existing native fn ($x) => ... and first-class callable syntax stay in place; currying is introduced only where the same callable is re-bound three or more times. There is no cutover date and no flag day, which is the migration's strongest property: any module can be reverted by inlining the closure back.
Compatibility. Two compatibility fronts. Runtime: the package must track PHP's argument-handling semantics (named arguments, never, true, union types), and a single-maintainer project may lag a PHP minor release — pin with a caret constraint and let Dependabot/Renovate plus a characterization-test CI job catch drift. Tooling: static analysis loses parameter hints after the first application, so baseline PHPStan/Psalm noise before wide adoption and confine any suppressions to the wrapper file. Debugging shows curried callables as anonymous functions with low-information frames; named variables mitigate this and should be a review convention, not a suggestion.
Sequencing.
php artisan tinker.App\Support\Functional\Curry) with the terminal-application apply() guard. This is the abandonment insurance and the one-file swap point.__invoke. We are the field testing; zero dependents means the burden is ours.Maintenance. Low, and bounded. The dependency itself is a few hundred lines with no transitive graph; keeping it current is a caret constraint plus automated PRs. Our own maintenance burden is the wrapper, the ADR, and the characterization test suite — all deliberately in our code, not application code, which is what preserves the exit path. If upstream goes quiet, MIT licensing makes a fork or a vendored ~20-line helper entirely realistic. Treat PHP version bumps as reviewable events with the characterization suite as the gate.
Support. No vendor, no SLA, no commercial support — a single maintainer and zero recorded dependents. Internally that means support is self-service: the README, the test suite (the de facto specification), and our ADR. Onboarding docs matter more than usual here because curried call sites are harder to grep and harder to trace; a reader who does not know the argument-order convention cannot follow the code. That is a documentation obligation we take on, not one we inherit.
Scaling. Technical scaling is a non-issue at application scale — it is a pure function-composition helper with no runtime resource footprint. The scaling constraint is cognitive and spatial: readability debt grows with the number of engineers who have to read curried code without having written it, and greppability degrades proportional to how far currying spreads across module boundaries. The realistic ceiling is a handful of deliberately chosen modules owned by teams fluent in higher-order functions. Under Octane and long-lived workers, verify curried callables are not accumulating unboundedly — that is an audit item, not a blocker.
Failure modes. Ranked by likelihood:
Closure; the TypeError surfaces frames away. Mitigations in order: the apply() guard, a test assertion that curried callables are fully applied, a code-review checklist item, typed variables. This deserves an ADR line.@phpstan-ignore comments, shrink scope.Explicitly out of scope: auth, payments, and PII handling. Not because the package is unsafe — it has no I/O, serialization, or network surface — but because a single-maintainer utility with no field-tested dependents cannot justify third-party risk where a hand-written closure costs thirty lines.
Ramp-up. Shallow to install, moderate to use well. Install and first curried closure: under an hour for anyone comfortable with fn and higher-order functions. Productive, convention-following use: a week, gated on the ADR and the wrapper landing first. Team-wide literacy: the real cost. The failure mode to watch for is a team that finds the syntax easy and the semantics hard — curried code that reads fine to its author and is opaque to its next reader. That is a hiring and team-profile input to the decision, not a detail to defer. Budget one engineer-day for the spike plus wrapper plus characterization tests, and frame the expected return as narrower diffs, less duplicate closure code, and faster iteration in test builders — developer-productivity value attached to a refactor already on the roadmap. If no such initiative exists this quarter, defer; there is no forcing function and no roadmap slot to allocate.
How can I help you explore Laravel packages today?