torann/promise
Torann/Promise provides a lightweight promise implementation for Laravel and PHP, letting you defer work and chain success or failure callbacks through a simple, fluent API. It helps organize asynchronous-style logic while keeping application code readable and easy to manage.
Architecture fit — unresolvable until one ambiguity is closed. The repository metadata describes "Simple Roles and Permissions for Laravel 5," while description.md documents a Promises/A+ library with then()/catch()/finally(), all(), race(), a deferred object, a service provider, and a facade. These are two different packages with two different integration surfaces. No architecture decision can be made against this input: the RBAC reading implies an Eloquent trait on User, roles/permissions pivot tables, a middleware alias in Kernel.php, and a Blade directive; the Promise reading implies a container binding plus a facade used inside request-scoped workflows. A PM should treat "what does this actually do?" as a blocking precondition and, separately, treat the drift between README, metadata, and code as the primary evidence of abandonment. The first task is a 30-minute spike — clone the repo, read composer.json and src/ — not a design session.
Integration feasibility is low on any current Laravel. The package targets a pre-Laravel-11 bootstrap (providers and aliases in config/app.php) and Laravel 5-era illuminate/* constraints. On Laravel 8+ composer resolution will probably fail outright. That failure is the honest signal; widening the constraint in an internal fork is the workable path, and --ignore-platform-reqs is not, because it defers an incompatibility from install time to production runtime. If the source turns out to be the RBAC package, the feasibility picture improves slightly — pivots and middleware are stable interfaces — but the Laravel 11 middleware registration and bootstrap changes remain a known break.
Technical risk is concentrated in three places. First, nothing here is asynchronous: PHP-FPM is synchronous, so chained callbacks buy readable control flow and a single error handler within a request, and nothing else. all() is sequential; race() in a cooperative single-threaded runtime is deterministic and generally just "the first element in the array." Second, silent failure is the default debugging tax — a chain with no catch, or a catch returning null instead of rethrowing, yields an empty page rather than an error. Third, if the provider registers a singleton, facade and app() resolution may share state across chains, leaking between workflows under test and in production.
Key questions to put to the team before any approval:
src/, and does either document match it?composer require resolve on our pinned Laravel version without flags?Promise bound as a singleton or a factory, and are then callbacks invoked eagerly at construction or on explicit settle?Http::pool for concurrency, a current RBAC package for permissions), and why is it not the default?The realistic verdict a TPM should be prepared to defend: this is a reference point for effort, not a dependency candidate. Its value is that it forces a standardization decision — one maintained library per capability — instead of re-litigating it feature by feature.
Stack fit. Under the Promise reading, the only legitimate role is a control-flow helper inside a synchronous request: three to five steps that may each throw, with one catch at the end replacing nested try/catch. Under the RBAC reading, it is a conventional authorization layer. Neither should be reached for when the workflow is short and lives in one class — plain try/catch is clearer, has no dependency, and will still compile in five years. This package is warranted only when the chain is long enough that the nesting actually hurts.
Migration path is a wrapper, and that is the highest-leverage decision available. Controllers and services depend on an internal contract, never on Torann\Promise:
interface DeferredWork {
public function run(mixed $seed): mixed;
public function then(callable $fn): static;
public function catch(callable $fn): static;
}
A thin adapter delegates to the package; a future swap to native concurrency, a Guzzle-style promise, or plain try/catch replaces one class instead of forty call sites. With zero dependents upstream, this is not over-engineering — it is the only thing that makes the dependency survivable. The same discipline applies if it is the RBAC package: wrap it in an AuthorizationService (assignRole, revokeRole, grant, can) so policies and views never touch the package trait directly.
Compatibility and extension. There is no upstream to file issues against and no release cadence to ride, so the fork is the extension mechanism. Guard-aware permission checks, team-scoped roles, permission caching, audit logging, a finally-style callback — all of it gets added in our namespace, tested, and documented next to the internal contract so the next engineer knows the behavior is local to us.
Sequencing:
docs/ stating which capability we evaluated and why, so this is not re-litigated from the same contradictory inputs.composer require resolving cleanly on our Laravel version.catch that translates package rejection semantics into our error model in one place. Scattering catch blocks through a chain reproduces the nesting we were avoiding.Add to the internal "vendored / at-risk" inventory at step 2, not after launch. Every PR that uses it should state why the chain warranted a package rather than try/catch, and — for the promise reading — state explicitly that this is not parallel execution, so nobody later "optimizes" by adding race().
Maintenance is effectively unbounded at the tail. Zero stars, zero dependents, no visible tests, no CI, no security disclosure path, Laravel 5-only, EOL since 2019. We are adopting source code, not consuming a dependency. Baseline estimate is a couple of engineer-days per year, plus an unbudgeted spike the first time Laravel changes a relevant contract — the Laravel 11 bootstrap and middleware registration changes are the obvious precedent. Require a named owner, a quarterly review entry, and a pinned commit; without those three, this becomes orphaned code that nobody will admit to owning.
Support has no external tier. There is no issue tracker that produces fixes, no maintainer to escalate to, no CVE feed. Every question — "does this callback run eagerly?", "why did this rejection vanish?" — is answered by reading src/ and by our own tests. That is acceptable only because the code is small and the fork is ours; budget the reading time explicitly rather than discovering it in the second sprint.
Scaling risk is a product risk, not a throughput risk. Promises give no latency improvement and no background execution: no retries, no durability, nothing that survives the response. The most expensive failure mode this package enables is a feature that demos beautifully on a small dataset — a PDF export, third-party sync, webhook fan-out, or report generation — times out in production, and gets "fixed" by adding another promise. Those belong on dispatch() and jobs, and that boundary needs to be written into the PR template or an ADR before the first feature ships. If the RBAC reading holds, scaling concerns are the familiar ones: multi-guard support, team/workspace scoping, wildcard inheritance, permission caching, delegated administration, and audit history are all ours to build, and should be scoped and priced rather than discovered.
Failure modes to instrument for:
catch, or the catch returns null — producing a blank page instead of an error. Log at each then when diagnosing.all() assumed to be parallel, leading to a redesign that delivers no latency gain.Ramp-up is cheap in code and expensive in ambiguity. A mid-level engineer will read the API in an hour. The cost sits in the disambiguation and the undocumented settlement semantics — roughly a day of team time if the naming mismatch is not pinned down in writing first. Onboarding material should lead with the three files that answer 90% of future questions (the service provider's binding, and the concrete class's then/catch implementations) and with the explicit statement that this package is vendored and locally owned, so no one wastes a sprint looking for upstream documentation that does not exist.
How can I help you explore Laravel packages today?