projek-xyz / callable
Auto-wire and invoke any callable through PSR-11
Fund package maintenance!
Requires
- php: >=8.4
- psr/container: ^2.0
Requires (Dev)
- kahlan/kahlan: ^6.1
- laravel/pint: ^1.32
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Auto-wire and invoke any callable through PSR-11
Resolve any callable through a PSR-11 container and invoke it with dependency injection — same semantics as native call_user_func_array(), except missing parameters auto-wire from the container instead of raising ArgumentCountError.
Installation
composer require projek-xyz/callable
Resolving and invoking callables
Works with any PSR-11 container; the examples below use projek-xyz/container.
Invoking with Handler
Handler::handle() accepts any callable shape — a function name, Class::method string, [Class::class, 'method'], [$object, 'method'], a closure, or an __invoke() object — and binds arguments with native call_user_func_array() semantics: integer keys are positional (in order), string keys are named, and a trailing variadic absorbs the rest. Whatever you do not pass is auto-wired from the container (then its default value) instead of raising ArgumentCountError:
use Projek\Callable\Handler; use Projek\Container; class Clock { public function now(): int { return time(); } } class Greeter { public function __construct(private Clock $clock) {} public function greet(string $who): string { return sprintf('Hello %s (at %d)', $who, $this->clock->now()); } } $container = new Container([Clock::class => Clock::class]); $handler = new Handler($container); // $who is bound by name; the Greeter receiver is built automatically and // its Clock dependency comes from the container. echo $handler->handle([Greeter::class, 'greet'], ['who' => 'world']); // e.g. "Hello world (at 1791177991)" // Positional arguments behave like any native call... $handler->handle('strtoupper', ['hello']); // "HELLO" // ...and so do Class::method strings, closures, and __invoke() objects. $handler->handle(Greeter::class.'::greet', ['who' => 'PHP']);
Resolving without invoking
Resolver is the layer Handler is built on; reach for it directly when you only need the callable:
use Projek\Callable\Resolver; $callable = (new Resolver($container))->resolveCallable(Greeter::class.'::greet'); // [new Greeter(new Clock), 'greet']
Error handling
Failures are named the way PHP itself would name them:
-
DependencyException— a parameter could not be provided or auto-wired. The message mirrors nativeTypeErrorphrasing with the 1-based argument position:$handler->handle(fn (string $dsn) => $dsn); // Projek\Callable\DependencyException: // {closure}(): Argument #1 ($dsn) is not resolvable
-
UnresolvableException— the callable itself cannot be resolved:Instance of missing_function is not resolvable Method Greeter::nope() does not exist -
TypeError— argument type mismatches surface PHP's ownTypeErroruntouched: invocation runs natively understrict_types.
License
This library is open-sourced software licensed under MIT license.