technically/callable-reflection
A PHP package for reflecting any callable—closures, invokable objects, function names, and array callables—through a single, consistent API. Inspect parameters, types, defaults, and return types to power dependency injection, routing, and event handling in Laravel or plain PHP.
Install it like any Composer library — there is no service provider, facade, or config file to publish:
composer require technically/callable-reflection
Verify the require block in composer.json — it targets PHP 8.0+, so it fits Laravel 9+ without ceremony.
Where to look first. The whole surface is the static constructor and the reflection/invocation methods it returns:
CallableReflection::from($callable) — normalizes any callable shape into one object.->getReflection() — returns the underlying ReflectionFunctionAbstract (ReflectionFunction or ReflectionMethod), so every native reflection API you already know is available.->getParameters() — ReflectionParameter[], shape-independent.->getName() — a stable string name regardless of shape ('strlen', 'App\Foo::bar', '{closure}').->invoke(...$args) — calls the underlying callable.First use case. The one that pays for itself immediately in a Laravel app is introspecting a controller action or single-action class before you run it. Something like:
use Technically\CallableReflection\CallableReflection;
$action = CallableReflection::from([SendInvoice::class, 'handle']);
$action->getName(); // "App\Actions\SendInvoice::handle"
$attributes = $action->getReflection()->getAttributes(Transactional::class);
$parameters = $action->getParameters();
$action->invoke($invoice); // no container resolution — see Gotchas
If you only need a name, skip this package — Laravel's Illuminate\Support\Reflector::getCallableName() already does that. Use this when you need parameters, attributes, return types, or invocation through the same object across heterogeneous callables.
Attribute-driven dispatch on listeners and actions. Read method-level attributes before invoking. This is the cleanest day-to-day win — you get the attributes off getReflection() and then decide whether to wrap, log, or short-circuit:
$ref = CallableReflection::from([$listener, 'handle']);
if ($ref->getReflection()->getAttributes(Transactional::class)) {
DB::transaction(fn () => $ref->invoke($event));
}
Signature-based conditional wiring. When a callable comes from config, a route macro, or a user-supplied option, inspect the parameters to decide what to pass. Parameter count and getType()?->getName() are your two tools:
$params = CallableReflection::from($callback)->getParameters();
$deps = array_map(fn ($p) => $p->getType()?->getName(), $params);
Normalizing heterogeneous callables. The core workflow: you accept string|array|Closure|object from somewhere (config, an attribute, a job payload) and funnel it through CallableReflection::from() so downstream code sees a single stable contract. This is the pattern that justifies the dependency — everywhere you'd otherwise write if (is_string(...)) { ... } elseif (is_array(...)) { ... }.
Reflect with this package, invoke with the container. Best integration tip: use getParameters() / getReflection() to describe the call, then delegate to app()->call($callable) for actual execution so Laravel resolves dependencies. Only use ->invoke(...$args) when you're passing everything explicitly.
Testing helpers. In architecture tests (Pest arch, PHPStan custom rules) you can assert shape — return types on controller methods, expected attributes on action classes, arity of closures — without writing per-shape reflection code.
Boundary validation at boot. If you're building a Laravel package that accepts a callable option (rate limiter, queue callback, macro), reflect the signature once at registration and throw a friendly error if the first parameter isn't the event type — instead of a runtime TypeError deep in a queue worker.
Small wrapper service. Because invocation is static (CallableReflection::from()), fakes are awkward in tests. Bind a thin ActionReflector interface in your container that delegates to the library, so tests can swap it out.
First-class callables lose metadata. $obj->method(...) is a Closure; you cannot reliably recover $obj. If you need the object (or the class/method name), pass [$obj, 'method'] or [$class, 'method'] instead. This is the number-one trap with PHP 8.1 syntax.
Invokable objects reflect __invoke. CallableReflection::from($invokable) reflects the __invoke method, not the class. Use getReflection()->getDeclaringClass() when you need the class identity.
Static string callables and autoloading. 'App\Foo::bar' is only callable once App\Foo is autoloadable. If your input comes from a config file or DB, call class_exists() first or you'll get a "not callable" exception instead of a useful message.
Array callables with a class string. [Foo::class, 'bar'] reflects against the class. If bar is non-static, invoke() will fail at runtime — resolve an instance ([app(Foo::class), 'bar']) and reflect that instead.
No container injection. invoke(...$args) passes exactly what you give it. Laravel devs reach for this and are surprised when dependencies aren't resolved. Prefer app()->call($callable) for execution and reserve invoke() for callables whose arguments you fully control.
Access levels. On PHP 8.1+ reflection no longer needs setAccessible(true) for non-public members; on 8.0 you may need it. Laravel 10 requires 8.1 and Laravel 11 requires 8.2, so this only bites legacy apps — but check your floor before assuming.
Overlap with Illuminate\Support\Reflector. Laravel's Reflector::getCallableName() and getParameterClassName() cover a slice of what this package does. Do not add a dependency for just a name string; add it when you want parameters + attributes + return type + invocation through one object.
Exception types leak. Invalid/non-callable input throws library-specific exceptions. Catch them at your HTTP/queue boundary and translate to a domain error — don't let a Technically\... exception escape to a 500.
Return types lie about self/static/parent. getReturnType()->getName() returns those literal strings. Resolve them yourself via the declaring class. Guard with instanceof ReflectionNamedType before assuming a single name — union/intersection types will break naive code.
Cache reflections. Reflecting on every route/listener is cheap but not free. Memoize by a stable key: the string name for string callables, Class::method for arrays, spl_object_hash() for objects. Avoid keying by closure identity unless you control lifetime.
Debugging is native. getReflection() returns a genuine ReflectionFunctionAbstract, so dd($ref->getReflection()), stack tracing, and IDE support all work as usual. This is a real advantage over hand-rolled callable string parsing.
Pin and wrap. Nine stars, zero dependents — treat it as a small utility, not an ecosystem. Pin a minor version, wrap it behind your own interface, and add tests for the exact callable shapes you rely on ('strlen', [$obj, 'm'], [$cls, 'm'], fn () => null, new Invokable, $obj(...)). If a maintained alternative appears, the swap should be a one-file change on your side.
How can I help you explore Laravel packages today?