Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Curryable Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and confirm the surface.

composer require nunomaduro/curryable

That is the entire setup. No service provider to register in bootstrap/providers.php, no config/curryable.php to publish, no facade, no migration, no artisan command. It is a framework-agnostic micro-utility: one curry() function plus a Curryable trait for class methods. If you are wiring anything into the container just to make the package work, you have misread the install.

Where to look first, in order:

  1. The README. The entire public API fits on one screen. If you spend more than five minutes here, you are over-investing.
  2. The source. It is a few hundred lines: a trait, a function, and reflection-based arity detection. Read it specifically to understand how arity is counted — that is where every edge case in this package lives.
  3. The test suite. It is the de facto specification for how closures, invokable objects, static methods, and partial application actually behave. It tells you which argument shapes the author validated, which matters because you are the field testing (zero recorded dependents).

First use case — pick this one deliberately, not a harder one. Find an existing closure that is already being re-wrapped with the same leading argument at several call sites, and curry it. A formatter or report builder is ideal:

use function NunoMaduro\Curryable\curry;

// Before: a bespoke closure at every call site
$formatUsdForSpain = fn (float $amount) => $formatter->format('es_ES', 'USD', $amount);

// After: one curried callable, re-bound per locale/currency
$format = curry(
    fn (string $locale, string $currency, float $amount) => $formatter->format($locale, $currency, $amount)
);

$formatUsdForSpain = $format('es_ES')('USD');
$formatUsdForSpain(19.99);

Run it once in php artisan tinker or a scratch test. Confirm that a partial application returns a Closure and a full application returns a value, then stop. Do not start by currying a controller action or an Eloquent scope — those are precisely where the readability cost is highest and the measurable benefit is lowest.

Implementation Patterns

Fix the argument order once and treat it as a contract. Currying binds left to right, so the volatile argument (tenant ID, locale, connection, formatter) goes first and the payload goes last. This single convention is what makes curried code legible across a codebase; get it backwards and every call site reads in reverse.

// Volatile first, payload last — re-bind the tenant, reuse the rest
$notifyForTenant = curry(
    fn (int $tenantId, string $channel, string $message) => $notifier->send($tenantId, $channel, $message)
);

$notifyAcmeOnSlack = $notifyForTenant($acme->id)('slack');
$notifyAcmeOnSlack('Deploy finished');

Bind at wiring time, never per request. Reflection is the only non-trivial cost in the package. Curry once inside a service provider or a factory and hand the resulting closure to the container, so the reflection cost is amortized instead of paid on every call of a hot path.

// AppServiceProvider::register()
$this->app->singleton(
    'report.formatter.usd',
    fn () => curry(fn (string $currency, float $amount) => $this->formatter->format('en_US', $currency, $amount))('USD')
);

Use it as a pipeline-stage factory. This is the strongest day-to-day fit. Laravel's Pipeline and Collection::map want single-argument callables, and curry is a clean way to derive them from multi-argument service methods:

$result = app(Pipeline::class)
    ->send($payload)
    ->through([
        curry(fn (string $stage, array $data) => $this->stages->run($stage, $data))('normalize'),
        curry(fn (string $stage, array $data) => $this->stages->run($stage, $data))('enrich'),
        curry(fn (string $stage, array $data) => $this->stages->run($stage, $data))('validate'),
    ])
    ->thenReturn();

Use the trait for "half-configured" services. The Curryable trait is the right tool when a service method needs an ambient argument (tenant, connection, clock) baked in at construction time and the rest supplied per call:

use NunoMaduro\Curryable\Curryable;

final class PriceResolver
{
    use Curryable;

    public function resolve(int $tenantId, string $sku): Money { /* ... */ }
}

$acmeResolver = app(PriceResolver::class)->curry('resolve')($acme->id);
$acmeResolver('SKU-1');

Build curried factories in tests. Test builders re-configured with a different leading argument per test are where currying pays for itself fastest and where a bad diff is cheapest to revert:

$makeUser = curry(
    fn (string $role, array $overrides) => User::factory()->state(['role' => $role, ...$overrides])->create()
);

$makeAdmin = $makeUser('admin');
$makeAdmin(['email' => '[email protected]']);

Wire it behind one internal helper, and import that — not the package — from application code. Land App\Support\Functional\curry (about twenty lines) that re-exports the package function, pins the version, and crucially adds a terminal-application guard:

namespace App\Support\Functional;

use Closure;

function curry(callable $fn): Closure
{
    return \NunoMaduro\Curryable\curry($fn);
}

/** Force evaluation and fail loudly if the callable is still partially applied. */
function apply(Closure $curried, mixed ...$args): mixed
{
    $result = $curried(...$args);

    if ($result instanceof Closure) {
        throw new \LogicException('Curried callable is still partially applied — missing arguments.');
    }

    return $result;
}

This is the difference between an abandonment risk and a one-file swap. Keep the @phpstan-ignore / @psalm-suppress noise confined to this wrapper too.

Name the closures you keep around. Assign curried callables to descriptive variables rather than inlining them at the call site. Debuggers, error trackers, and Xdebug all render them as anonymous functions; a named variable is the only thing that keeps a stack trace legible.

Compose with native syntax rather than trying to replace it. Arrow functions (fn ($x) => ...) and first-class callable syntax (strlen(...)) already cover most partial-application needs. Reach for curry when the same callable is being re-bound at three or more call sites — not on the first occurrence.

Gotchas and Tips

The silent partial-application hazard is the dominant bug class. Under-applying does not throw. It returns a Closure, which then flows into something expecting a value, and the TypeError surfaces several frames downstream in unrelated code. Mitigations in order of effectiveness: the apply() guard above; an assertion in tests that curried callables are fully applied; a code-review checklist item; a habit of typing curried variables as Closure and their results as concrete types. This behaviour deserves a line in your team's ADR — it is the single most likely source of a production incident with this package.

Reflection is per-construction — keep construction out of loops. curry() inspects the callable to determine arity. Calling it inside a Collection::map, a queued job, or a request handler re-reflects constantly. Build once, reuse the closure. Under Octane or any long-lived worker, verify you are not accumulating memoized curried callables unboundedly across requests.

Static analysis degrades at curried call sites, and you should budget for it. The return type of a curried callable is arity-dependent, which PHPStan and Psalm model poorly — expect Closure/mixed inference and lost parameter hints after the first application. Baseline the noise before wide adoption. If the fix turns into broad @phpstan-ignore comments scattered across the codebase, that is your signal to shrink scope, not to suppress harder.

Validate the argument shapes you actually use — you are the field testing. Zero recorded dependents means nobody has exercised the edge cases against real code. Before relying on it, write characterization tests covering: variadic parameters, default values, named arguments, union and intersection types, by-reference parameters, and __invoke objects. These are exactly the surfaces reflection-based arity detection gets wrong, and a wrong arity count produces a partial application that never completes — the worst form of the bug above.

Add declare(strict_types=1) to any file that curries. Currying shifts type-coercion decisions into binding order; strict types turns silent coercion bugs into immediate, traceable ones.

Curried code is harder to grep and harder to trace. A curried call site reads $foo('a')('b'), so searching for where the underlying function is called no longer works. Constrain adoption to deliberately chosen modules (rules engine, transformation pipeline, report builder) and forbid it in controllers, models, and request hot paths. Forbid it across module boundaries in particular — that is where the reader loses all context for the argument order.

Debugging friction is real, not theoretical. Xdebug shows curried callables as anonymous functions with low-information frames, and when argument counts mismatch, ArgumentCountError/TypeError will point into package internals rather than at your call site. Named variables plus the apply() guard are the practical remedies; do not expect a better story from tooling.

Pin with a caret constraint and treat PHP version bumps as reviewable events. The package has to track PHP's evolving argument-handling semantics (named arguments, never, true, union types), and a single-maintainer project may lag a minor release. Renovate/Dependabot plus a CI job running your characterization tests is enough. If upstream goes quiet, the wrapper turns the emergency into a one-file change — MIT licensing makes a fork or a vendored twenty-line helper entirely realistic.

Extension points are yours, not theirs. The package gives you exactly two: the curry() function and the Curryable trait. Everything else — the terminal-application guard, container bindings, a partial() alias, analyzer stubs — belongs in your wrapper. Keeping your extensions out of application code is what preserves the exit path.

Do not put it near auth, payments, or PII handling. Not because the package is unsafe — it has no I/O, no serialization, no network surface — but because a single-maintainer utility with no field-tested dependents cannot justify third-party risk in those paths when a hand-written closure costs thirty lines and carries zero supply-chain question marks.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core