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.
Architecture fit. technically/callable-reflection is a framework-agnostic utility that normalizes every PHP callable form — Closure, first-class callable syntax, invokable objects, [$obj, 'method'], 'Class::method', 'function' — into a single reflection surface with invoke() / invokeArgs() and parameter/return-type introspection. For a Laravel codebase the fit is narrow but real: it belongs at the seam where user-supplied callables enter the system as configuration rather than as code. Concretely, that means extensibility points such as pipeline stages, action/handler registries, custom validation or authorization callbacks in package config, attribute-driven metadata readers, and internal DSLs that accept "any callable" from package consumers. It is explicitly not a replacement for the container's app()->call(), which already resolves type-hinted dependencies; the two overlap on invocation but the package's value is reflection and parameter introspection first, invocation second.
Integration feasibility. High, and unchanged by 0.5.0. The release is a single change — PHP 8.5 compatibility — with no new public API and no changelog entry describing behavioral change. Still no service provider, no config publishing, no facade, no migrations, no runtime bootstrapping. Two things the 0.5.0 release does clarify, though, and both matter for how we write the dependency. First, the package is pre-1.0 (0.4.3 → 0.5.0), which corrects an assumption in our prior read of the version as 1.x-stable. Under semver, 0.x permits breaking changes in minor bumps, so the API surface we wrap is not contractually frozen. Second, the absence of dependents remains a double signal: no transitive-conflict exposure, but no third-party validation of the API against real production callable shapes.
Technical risk. The dominant risk remains supply chain and longevity, but the evidence base has improved. A new release landing with a working changelog, a linked PR, and a named contributor is a modest positive on maintainer activity — it is the first concrete datapoint that the project is not dormant, which directly addresses the "upstream abandonment" failure mode in the prior assessment. It does not change the bus factor: still a small maintainer set, still a single-maintainer namespace (technically-php), still nine stars and zero dependents. The version-number correction raises the contract risk relative to our prior assessment: 0.5.0 could have been 0.4.4 under a stricter policy, and a 0.6.0 could rename the reflection surface. Mitigate by pinning to the 0.x minor line rather than to the major, and by keeping the adapter boundary that makes a rename a contained edit. Functional risk is still dominated by classic PHP reflection edge cases — by-reference parameters via invokeArgs, variadics with named arguments, static vs. bound closures, private/protected visibility when reflecting string callables, union/intersection types, never returns — none of which 0.5.0 touches, so our test matrix obligation is unchanged. One case is genuinely new: the release advertises PHP 8.5 compatibility, so 8.5 belongs on the verification matrix before we adopt the claim, not after.
Key questions.
app()->call() and the Illuminate\Container resolution machinery — subset, superset, or orthogonal?ReflectionException / Error?E_STRICT) versus merely installs-and-runs, and whether 8.1+ features (enums, readonly, pure intersection types, never) are covered.Stack fit. Laravel 9–12 / PHP 8.1+ is a clean match; the package has no framework coupling, so it will not fight the container, Octane, or the test suite, and 0.5.0 does not add any. The natural placement is still a thin internal adapter — e.g. App\Support\CallableResolver — that exposes only the two or three methods our domain actually needs (describe(callable): CallableDescriptor, invoke(callable, array $args): mixed). All third-party types stay behind that adapter. With a pre-1.0 upstream this decision is no longer merely good hygiene; it is the thing that makes minor-version breakage survivable.
Migration path. Greenfield adoption, no migration, unchanged. Where app()->call() is already used and working, do not touch it. Where the codebase currently has hand-rolled is_callable() branching with ReflectionFunction / ReflectionMethod forks (a common pattern in plugin-style config), replace those branches with the adapter and delete the duplicated reflection logic. For any existing closure-invocation helpers, keep the old call sites and swap the internals — the adapter gives us a no-op rollback.
Compatibility. Update the matrix to include PHP 8.5, since that is precisely what 0.5.0 claims and the claim is ours to verify: PHP 8.1/8.2/8.3/8.4/8.5 against Laravel 10/11/12, plus one Octane-enabled run to confirm no long-lived-process state. Pin to ~0.5.0 rather than ^0.5 — for 0.x versions Composer's caret already resolves to >=0.5.0 <0.6.0, but writing the tilde makes the intent legible to the next engineer and prevents an accidental widening to ^0 in a future refactor. Commit the lockfile. Confirm the MIT license header is preserved in any vendored copy and that it does not conflict with our distribution model.
Sequencing.
Class::method, variadics, by-ref params, union/intersection types, named arguments. Run it on 8.5 as well as 8.4. Any failure here is a stop signal.Maintenance. Low effort, non-zero risk, now slightly better characterized. Budget roughly one half-day per quarter for version bumps and PHP-minor validation. The 0.5.0 release is evidence that PHP-minor tracking is happening upstream on its own schedule rather than being pushed by downstream pressure — that reduces the expected maintenance load, and it means we may receive 8.5 support without having to patch or fork. Mitigate remaining bus-factor risk by (a) pinning to the 0.x minor line, (b) keeping the adapter so a fork or vendored copy is a contained change, and (c) recording the MIT license and upstream URL in our dependency inventory. Add one standing item: re-read the changelog on every 0.x minor bump, because minor is where breaking changes will hide while the package is pre-1.0.
Support. Internal support burden is near zero because the library is invisible to application developers; only the adapter's owners interact with it. The support surface we do own is the exception contract: developers passing a bad callable (non-existent method string, wrong arity, visibility violation) should get a message that names the offending callable and the expected signature. Wrap upstream exceptions at the adapter boundary so messages are ours and so the contract survives both an upstream swap and an upstream 0.x rename.
Scaling. Negligible runtime footprint — reflection results are computed per invocation and are CPU-bound in microseconds. Under Octane, cache descriptors in a request-scoped or application-scoped map keyed by a stable callable identifier to avoid re-reflecting hot handlers on every request. No memory growth concerns provided descriptors are not retained per-request in a long-lived singleton; validate this during the Octane test, which now doubles as our 8.5 validation.
Failure modes.
E_ALL. Detection: 8.5 CI leg with deprecations escalated to failures. Severity: low, medium likelihood.Ramp-up. Under a day for a mid-level engineer to read the API and produce working code; the real ramp-up cost is the team's fluency in PHP's reflection edge cases, which the package does not remove — it only removes the boilerplate. Do not adopt this expecting the team to learn less about callables; adopt it expecting them to write less of it. One hour of that ramp-up is now specifically about version risk: brief the adapter owner that upstream is 0.x, that minor bumps can break, and that the adapter is the reason they don't care. Document the adapter, the supported callable matrix, the pinned version line, and the exception contract in the repo's developer docs, and treat that documentation as the deliverable that makes the dependency safe.
How can I help you explore Laravel packages today?