Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Result Type Laravel Package

graham-campbell/result-type

Result Type is a PHP implementation of the result type, maintained by Graham Campbell. It offers a clean way to represent success or failure values instead of exceptions. Install via Composer with graham-campbell/result-type ^1.1; requires PHP 7.2.5-8.6. MIT licensed.

View on GitHub
Deep Wiki
Context7

Getting Started

Install. It is a single Composer dependency with no Laravel package machinery:

composer require "graham-campbell/result-type:^1.1"

There is no service provider, no config file, no facade, and nothing to publish. Do not add it to config/app.php; Result is a value object, not a service. If you find yourself wanting to bind it in the container, you are holding it wrong — bind the service that returns it.

Where to look first. The README is a stub (install + license only), so skip it and read the source, which is roughly four small files:

  • src/Result.php — the abstract base: ok(), error(), try() factories, plus isOk(), isError(), map(), flatMap(), mapError(), flatMapError(), filter(), and the terminal unwrap/match helpers.
  • src/Success.php, src/Error.php — the two concrete variants.
  • composer.json — the real constraint worth knowing: it pulls in phpoption/phpoption (≥1.10 as of v1.2.0), so this is not a zero-dependency leaf.

Budget twenty minutes for that reading. It is the whole API surface.

First use case. Pick one service method where failure is an expected outcome — a payment authorization decline, an address-validation rejection, a third-party API returning a business-level "no" — and make that the return type instead of throwing. Not a controller, not a model, not a job:

namespace App\Services\Payments;

use GrahamCampbell\ResultType\Result;
use GrahamCampbell\ResultType\Success;
use GrahamCampbell\ResultType\Error;

final class AuthorizePayment
{
    /** @return Result<Charge, PaymentError> */
    public function handle(Money $amount, Card $card): Result
    {
        $response = $this->gateway->authorize($amount, $card);

        return $response->declined()
            ? Result::error(PaymentError::Declined)
            : Result::ok(Charge::fromResponse($response));
    }
}

Then translate it at the edge, on day one:

$result = app(AuthorizePayment::class)->handle($amount, $card);

if ($result->isError()) {
    return response()->json(['error' => $result->error()->value], 402);
}

return new ChargeResource($result->value());

That single vertical slice — service returns Result, controller converts — is the entire first milestone. Everything else is elaboration.

Implementation Patterns

Service-layer return type, framework-edge exception. The load-bearing convention is: Result lives inside the domain/service boundary; exceptions remain the language of HTTP, Eloquent, queues, and middleware. Every Result-returning service needs two adapters written on the same day as the service:

// Result -> HTTP (controller)
// Result -> exception (when a caller insists on throwing)
$result = $service->handle(...);

return $result->isOk()
    ? $result->value()
    : throw new PaymentDeclinedException($result->error());

Composition instead of nested try/catch. This is where the pattern earns its keep. Multi-step workflows chain with map/flatMap, and an error short-circuits the remainder of the chain for free:

return $this->validate($payload)              // Result<Validated, Error>
    ->flatMap(fn ($v) => $this->enrich($v))   // Result<Enriched, Error>
    ->flatMap(fn ($e) => $this->persist($e))  // Result<Saved, Error>
    ->map(fn ($s) => new OrderResource($s));

Steps after the failing one never execute, and you wrote no early returns. mapError()/flatMapError() are the mirror image for translating an upstream taxonomy into yours (e.g. SDK error codes → your PaymentError enum).

Wrap throwing SDKs with a small adapter, once. Third-party clients throw. Concentrate the impedance mismatch in exactly one place per integration rather than sprinkling try/catch through the domain:

private function attempt(callable $fn): Result
{
    try {
        return Result::ok($fn());
    } catch (GatewayException $e) {
        return Result::error(PaymentError::fromGateway($e->getCode()));
    }
}

Guard against catching too broadly here — catching Throwable will swallow TypeError and programming bugs, turning loud failures into silent Error values.

Error taxonomy as a backed enum. On PHP 8.1+, use a string-backed enum for the E side. It gives you ->name, ->value, ::from() at boundaries, and static analysis can actually reason about it. If the team can't articulate who owns the enum and how it evolves, stop the pilot — an ad-hoc zoo of string error codes is worse than exceptions.

Use filter() for validation-style predicates where the rule is a one-liner, and keep FormRequest for HTTP-shaped validation. Result is not a replacement for Laravel's validator; it is for rules that live in the domain and need to return a typed reason rather than a MessageBag.

Terminal handling via match-style callbacks or an explicit guard. The idiomatic exit is a single terminal call per Result. Where no helper fits, isOk()/isError() plus value()/error() is always available. Either way, both branches get written once, at the boundary, in one visible place.

Where not to use it. Do not put Result in controller signatures, FormRequest rules, middleware handle() returns, Eloquent casts/accessors, event listeners, or queued job payloads. Those layers are exception-driven and serialization-dependent; the translation cost exceeds the benefit every time.

Gotchas and Tips

  • Error collides with PHP's global \Error. In a namespace, use GrahamCampbell\ResultType\Error; silently shadows the engine's \Error class. Any catch (Error $e) in that file now catches the wrong thing. Alias it (use GrahamCampbell\ResultType\Error as ResultError;) or keep the import file-scoped and tiny.
  • No exhaustive matching. PHP has no sealed-type switch, so nothing stops a developer from calling ->value() on an Error and getting a runtime exception — the exact failure mode the pattern claims to prevent. Enforce it mechanically: PHPStan/Larastan or Psalm in CI with a rule that flags ignored Result returns. Without that gate, the convention erodes within a sprint or two.
  • Generics are PHPDoc only. Result<Charge, PaymentError> is erased at runtime. Static analysis is the only thing making the error type meaningful — write the @return Result<…> annotations, don't skip them.
  • Never serialize a Result. It has no __serialize contract you should rely on, cannot cross a queue boundary, and should never be returned as an API response body. Convert to a DTO/HTTP status/exception at the edge. This is the single most common design mistake with these types.
  • Don't instanceof Success. v1.2.0 made Result covariant in its value and error types and widened the factory/transformation signatures; the concrete class you get back is an implementation detail of the factory. Use isOk()/isError() as the guard.
  • try() is not a magic wand. Factory helpers that wrap callables still swallow the exception object into the error side — check what they actually carry before relying on the failure detail for logging.
  • Pin the transitive dependency. It requires phpoption/phpoption ≥1.10 (raised in v1.2.0). Commit composer.lock, and if you're mid-platform-upgrade, confirm the resolved phpoption version rather than assuming.
  • Bus factor is one. Small surface, MIT, disciplined release history, tracks PHP 8.3–8.6 promptly — so fork-and-vendor is a realistic contingency and you should say so in the risk register rather than discover it later. Commercial support exists via Tidelift if procurement needs it.
  • Test both branches, always. A test suite for a Result-returning service that only asserts the success path is worse than useless — it certifies coverage that doesn't exist. Make "one assertion per branch" a review checklist item; it's cheaper than static analysis and catches most of it.
  • Don't mix paradigms mid-stack. If a call chain throws in one frame and returns Result in the next and back again, stack traces become unreadable and error contracts diverge between teams. One convention per layer, written down, with a named owner.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource