php-standard-library/result
Result type for PHP that captures success or failure as a value, enabling controlled error handling without exceptions. Part of the PHP Standard Library.
Architecture fit. This is a framework-agnostic value object, not a Laravel package: no service provider, no config, no facade, no published assets. That is a feature. Result slots in underneath the framework as a return-type convention for actions, domain services, jobs, and CLI commands, while Laravel's exception handler, validation pipeline, and HTTP kernel continue to behave as they do. The mismatch to plan for is at the boundaries — Laravel's core primitives (ValidationException, ModelNotFoundException, DB/HTTP client exceptions) all signal failure by throwing, so every seam between "our" code and "their" code needs a translation adapter we write and own. This package gives us the vocabulary, not the integration.
Integration feasibility. Installation is a one-line Composer require with no transitive dependency surface and no runtime bootstrapping; it cannot break container resolution or middleware ordering. The honest cost is not installation, it is convention: adoption only pays off if the team agrees where Result is mandatory and where exceptions remain idiomatic. Budget for an internal adapter layer (Result → JsonResponse, Result → ValidationException, Result → Job::fail()) and for PHPStan/Psalm configuration, because the "type-safe" claim is static-analysis-only — PHP has no reified generics, so Result<User, ValidationError> is documentation, not enforcement.
Technical risk. Ranked:
unwrap() throws on an Err (the source docs truncate before naming the exception class), so an unhandled Result degrades to the exact hidden control flow the library is sold as eliminating. We must confirm the exception type and either lint against bare unwrap() or ban it outside tests.Result objects cross a queue boundary (dispatch(new Job($result))), we need confirmation they serialize/deserialize cleanly; otherwise the convention must stop at the job's handle() signature.php-standard-library, and issues/PRs route elsewhere. Version alignment with sibling packages is a real constraint, not a footnote.Key questions for the maintainers or a spike.
unwrap() throw, and is it part of the public contract?@template annotations shipped, and are there stubs for the generic signatures?Result implement JsonSerializable / support __serialize for queue payloads?Stack fit. PHP 8.1+ per the source assessment, which is compatible with any Laravel version we would plausibly run. No conflict with Octane (immutable, request-scoped values), no global state, no I/O. It composes with Actions/lorisleiva/laravel-actions, form requests, and job middleware. It does not replace Laravel's Validator, HttpException rendering, or retry() semantics — it wraps them.
Migration path. Strangler, leaf-first:
Result; infrastructure and programming errors → exceptions. Write the negative list explicitly (DB connectivity, OOM, auth failures surfaced by the kernel).app/Support/Result/: HTTP mapping, validation mapping, queue mapping, logging mapping. This is the reusable asset; the package itself is trivial by comparison.Compatibility. No dependency conflict risk — the package is effectively standalone. The compatibility concern is internal: if other php-standard-library components are (or become) in use, pin all of them to the same monorepo minor to avoid a split-package skew where one component expects a newer core. Also flag that Result in return types changes the contract of every function that adopts it; downstream callers must be updated in the same PR or they will silently ignore a failure value — a compiler-invisible break.
Sequencing and decision gate.
| Phase | Deliverable | Exit criterion |
|---|---|---|
| 0 | Spike: confirm exception contract, static analysis support, serialization | All key questions answered; no blockers |
| 1 | Convention doc + adapters + PHPStan config | Reviewed and merged, zero production impact |
| 2 | Pilot in one bounded domain | Error-path test coverage ≥ existing; no increase in 5xx rate |
| 3 | Measured expansion | Defect/regression trend flat or better over two sprints |
| 4 | Adopt or retire | Explicit go/no-go; do not let the pilot linger as a half-convention |
Recommendation. Adopt as a convention with a thin library, not as a platform decision. The technical risk is low and reversible; the organizational risk — two competing error idioms coexisting indefinitely — is the one that actually needs a decision gate.
Maintenance. Near-zero dependency maintenance: no config to drift, no migrations, no framework upgrade coupling. The recurring cost is entirely ours — adapters, convention enforcement, static analysis baselines, and code review discipline. Because this is a split package, upgrades arrive bundled with unrelated monorepo changes, so upgrade work must be scheduled rather than absorbed opportunistically. Pin to an exact minor and review bumps.
Support. Community-only, routed to the monorepo's issue tracker, and effectively no Stack Overflow or blog corpus to draw on. That means the internal convention doc and the adapter layer are not optional niceties — they are the actual support surface. Name an internal owner. Onboarding a new engineer to map/flatMap semantics will be their first exposure to functional error handling in PHP, so pair on the first three PRs.
Scaling. Runtime overhead is negligible: small immutable objects, no reflection, no I/O. The one performance caveat is allocation in tight loops (bulk/batch paths) — prefer early returns there rather than wrapping per row. The operational scaling risk is inconsistency at team scale: with three squads applying Result differently and one still throwing, error observability fragments and log-based alerting loses fidelity. Standardize the mapping once, centrally.
Failure modes to instrument.
unwrap() on an Err in a production path → uncaught exception, and depending on the exception class, a 500 instead of a mapped 4xx.Result — a caller that ignores the return value swallows the failure with no log line. Mitigate with static analysis rules and mandatory match()/isSuccess() checks at call sites.Result crossing a queue payload or an Eloquent model attribute and failing serialization at dispatch time, not test time.Err values that are plain strings strip stack traces the previous exception path gave us for free. Define an error taxonomy with typed error objects plus a captured correlation ID before, not after, the pilot.Ramp-up. Realistically one to two sprints for a mid-sized team to reach fluency: half a sprint for the concepts, half for working through the first real refactor and the adapter edge cases. Cheap to learn, moderately expensive to learn consistently. Because the library is a single concept with a small API (ok, err, isSuccess, isFailure, match, map, flatMap, mapErr, fromCallable), ramp-up risk is manageable — provided the team's first encounter is a reviewed example PR from our own codebase, not the upstream docs site.
How can I help you explore Laravel packages today?