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

Callable Invoker Laravel Package

open-solid/callable-invoker

Open Solid Callable Invoker is a lightweight PHP utility for invoking any callable with automatic dependency resolution. It supports closures, class methods, and invokable objects, making it easy to wire actions and handlers in Laravel applications. Clean, flexible, and framework-friendly.

View on GitHub
Deep Wiki
Context7

Getting Started

Install (pin the version — this is 0.x):

composer require open-solid/callable-invoker:^0.2.1

Bind it once, resolve it everywhere. The package only needs a PSR-11 container. In Laravel, Illuminate\Container\Container already implements Psr\Container\ContainerInterface, so wire it in a service provider:

// app/Providers/AppServiceProvider.php
use OpenSolid\CallableInvoker\CallableInvoker;
use Psr\Container\ContainerInterface;

$this->app->singleton(CallableInvoker::class, function ($app) {
    return new CallableInvoker($app->make(ContainerInterface::class));
});

First use case — dispatch a handler by name from config or DB:

$handler = config('actions.send_welcome_email');
// e.g. SendWelcomeEmail::class or ['NotificationHandler', 'send'] or a closure

$result = app(CallableInvoker::class)->invoke($handler, ['userId' => 42]);

The invoker reflects the callable, matches $userId from your array (and Laravel container resolution for typed deps like Mailer, LoggerInterface, etc.), fills defaults/nulls, and calls it. No app()->call() fallback needed if you standardize on this seam.

Where to look first in the repo:

  • Constructor signature (verify the exact parameter name — invoke() vs call()).
  • How it treats an array of mixed named args vs positional args.
  • The NotFoundException/ContainerExceptionInterface re-throws — these propagate Laravel's binding exceptions when a dep can't be resolved.

Validate the gap versus core before committing. app()->call($callable, $params) already does container-aware resolution. If your only need is dispatching a service-bound callable with typed deps, you don't need this package. It earns its keep when you need one reusable, framework-agnostic primitive and predictable semantics across PHP callables (closure, [$obj, 'method'], [ClassName::class, 'method'], __invoke), and when you want an easily mocked interface.

Implementation Patterns

1. Contextual dispatch from config/DB — the primary use case.

Store the callable reference, not the code path:

// config/webhooks.php
return [
    'user.created' => [
        'SendWelcomeEmail::class',   // invokable
        ['App\Hooks\NotifySlack', 'post'], // array callable
    ],
];

Then in the dispatcher:

class WebhookDispatcher
{
    public function __construct(private CallableInvoker $invoker) {}

    public function dispatch(string $event, array $context): void
    {
        foreach (config("webhooks.$event", []) as $handler) {
            $callable = is_string($handler) ? app($handler) : $handler;
            $this->invoker->invoke($callable, $context);
        }
    }
}

Note the app($handler) — the invoker resolves parameters, not the callable's class. You still need to materialize the object (or pass the class name and let the invoker handle it if it supports that form — verify in the source).

2. Action/command pattern with a thin wrapper for testability.

Don't spread CallableInvoker across your codebase. Wrap it behind a domain interface so you can swap in app()->call() or a stub later, and so upgrades from v0.x are contained to one file:

interface ActionRunner
{
    public function run(callable|string $action, array $input = []): mixed;
}

final class InvokerActionRunner implements ActionRunner
{
    public function __construct(private CallableInvoker $invoker) {}

    public function run(callable|string $action, array $input = []): mixed
    {
        if (is_string($action)) {
            $action = app($action);
        }
        return $this->invoker->invoke($action, $input);
    }
}

Every other class depends on ActionRunner, which is trivial to mock:

$this->mock(ActionRunner::class)
     ->shouldReceive('run')
     ->with(SendWelcomeEmail::class, ['userId' => 42])
     ->once();

3. Strategy registries and plugin hooks.

For multi-tenant or feature-flagged workflows, resolve the callable at runtime and pass tenant/context data as parameters:

$strategy = $tenant->resolveOnboardingStrategy(); // returns a callable
$strategyResult = app(CallableInvoker::class)->invoke($strategy, [
    'tenant'  => $tenant,
    'user'    => $user,
    'plan'    => $plan,
]);

Any parameter the invoker can't match to your array is delegated to the container — so a handler that declares User $user, NotificationService $notifier will get $user from your array and $notifier from the container in one call.

4. Queue-job-friendly: pass closures only when safe.

Closures cannot be serialized onto a queue. Prefer class-name/array callables for anything that crosses a queue boundary:

// Good — serializable
ProcessOrder::dispatch($order, ['handler' => [ShipOrder::class, 'handle']]);

// In the job:
$this->invoker->invoke($this->handler, ['order' => $this->order]);

5. Integration with Laravel events / form handlers.

The invoker is a clean seam behind your own event-dispatcher glue when you need per-tenant or per-config handlers:

Event::listen(OrderPlaced::class, function (OrderPlaced $event) {
    foreach (config('order.handlers') as $callable) {
        app(CallableInvoker::class)->invoke($callable, ['event' => $event]);
    }
});

Gotchas and Tips

Verify the method name before you commit. The repo docs are sparse at v0.2.x. Check the source to confirm whether the entry point is invoke(), call(), or __invoke(). The pattern is the same but the naming matters for your wrapper interface.

Match semantics of app()->call() before you migrate anything. Laravel's app()->call() supports named-argument overrides keyed by the parameter name, plus positional args. Test:

  • Do array values map by parameter name or by position?
  • If a param is ?Bar $b = null and you didn't pass it, does it return null or throw?
  • Are union types (Foo|Bar) resolved by the container or rejected?

Write a few characterization tests against both your wrapper and app()->call() so behavior is pinned. This is the main risk of swapping.

Do not assume route-model binding or contextual binding. app()->call() honors contextual bindings and route parameters; this package resolves from the PSR-11 container directly. If your handler depends on a route-bound User, pass it explicitly via the parameter array. Don't rely on the container to know the current request.

Singleton in the provider is important. Register CallableInvoker as a singleton, not a bind. Reconstructing on every call wastes reflection overhead. If you ever need to clear state (you shouldn't), a scoped binding also works.

Exception handling lives at the boundary. The invoker will re-throw the underlying container exception (usually BindingResolutionException from Laravel) when a dependency can't be resolved — it does not swallow it. Treat this as an explicit failure. Catch it in the dispatcher and log the offending callable, otherwise a bad config value will look like a random 500:

try {
    $this->invoker->invoke($callable, $input);
} catch (\Psr\Container\ContainerExceptionInterface $e) {
    Log::error('Callable dispatch failed', [
        'callable' => is_string($callable) ? $callable : get_debug_type($callable),
        'error'    => $e->getMessage(),
    ]);
    throw $e;
}

Array callables with static class + method ([ClassName::class, 'method']) are supported — this is the pattern you actually want from config files, since no instance is required. But be careful: if the class has a constructor requiring deps and the method is non-static, the invoker must instantiate it via the container. Test this path explicitly — it's the one where a v0.x behavior change would hurt most.

No sandboxing — do not feed user-controlled callables. This package executes whatever you pass it. If the callable reference comes from user input (plugin marketplace, admin panel), you need an allow-list in your wrapper:

private const ALLOWED = [SendWelcomeEmail::class, ShipOrder::class];

if (is_string($action) && !in_array($action, self::ALLOWED, true)) {
    throw new \InvalidArgumentException("Unknown action: {$action}");
}

Vendor/pin aggressively. At v0.2.1 (a patch with no changelog detail), API changes are realistic at 0.3. Lock in composer.json to a caret on the minor (^0.2.1) and keep a thin wrapper so an upgrade — or a swap to app()->call() — is a one-file change. Reassess at 1.0 for revenue-critical code paths.

Debugging tips.

  • If a parameter defaults weirdly, dump the reflected signature: (new ReflectionFunction(Closure::fromCallable($callable))) — the invoker's decisions follow that signature.
  • If the container can't build the callable but your tests pass with new, check that the service provider binding for the callable is not binded behind a feature flag that isn't active in that environment.
  • In tests, use app(CallableInvoker::class) from the container rather than new CallableInvoker(app()) — you get the same instance Laravel would give your production code, avoiding a subtle mismatch.

Extension point. Because the constructor takes a ContainerInterface, you can pass an adapted container (a decorator that applies cross-cutting concerns like authorization or metrics) without touching the invoker. That's the cleanest place to add "can this user run this action?" checks.

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.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums