Download the PHP package iak/result without Composer
On this page you can find all versions of the php package iak/result. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Package result
Short Description A fully typed Result object for PHP — Rust-inspired semantics with PHP-natural naming
License MIT
Homepage https://github.com/iaK/result
Informations about the package result
Result
A Result is a return type for operations that can fail. Instead of throwing an
exception for an outcome you fully expect — an expired card, an empty stock, a
closed kitchen — you return it. For example, check out the following code:
Two operations that can each fail, one place where failure is handled, and not a try/catch in sight. Every possible outcome is right there in the types — your IDE sees them, your static analyser sees them, and the next developer sees them.
Installation
That's it. Results require PHP 8.2+ and nothing else — no framework, no configuration, no service provider.
Your First Result
Let's build something real: placing an order. You've probably written this controller a hundred times:
It works — until someone adds a fourth failure mode to PlaceOrder and this
controller quietly starts responding with 500s. Nothing in handle()'s signature
says what it throws, and nothing checks that you caught it all.
Let's rebuild it with a Result. First, give every failure a name. An enum is perfect for this, and it gives the error messages a home too:
Next, instead of throwing, return the outcome — either way:
Notice the docblock: Result<Order, OrderError>. That one line now documents
every way this operation can end. No source-diving, no tribal knowledge.
Finally, the controller shrinks to a single expression:
[!NOTE] Where did the try/catch go? There's nothing to catch — failure is just a return value now. And unlike a catch block,
match()can't be forgotten: it's the only way to get at the order, and it requires both arms.
And when someone adds that fourth failure mode? They add an enum case, the
match inside message() immediately demands a message for it, and every
consumer of the error gets flagged by static analysis. The failure mode is born
handled.
That's the whole pattern. Everything else in this package is convenience on top of it.
Available Methods
expect · isFailure · match · tapError · valueOrElse
Method Listing
success()
The static success method wraps a value in a successful result:
You may call it without arguments when the operation has nothing meaningful to return — "it worked" is the whole message:
failure()
The static failure method wraps an error in a failed result. The error may be
anything: an enum, a value object carrying context, a string, or an exception if
you already have one:
all()
The static all method combines a collection of results into a single result.
If every result succeeded, you get one success holding all the values with their
keys preserved. If any failed, you get the first failure back, untouched:
You may pass any iterable. Iteration stops at the first failure, so a lazy generator won't do more work than necessary.
isSuccess()
The isSuccess method determines whether the operation succeeded:
isFailure()
The isFailure method is the mirror of isSuccess. It shines in
guard clauses, keeping the happy path flat:
[!NOTE] Prefer these methods over
instanceofchecks. Static analysers can't carry the value and error types through a bareinstanceof, butisSuccess()andisFailure()keep them intact.
value()
The value method returns the success value:
If the result is a failure, value throws a ResultException. An unguarded
call is therefore an assertion — "this can't fail here, and if I'm wrong I want
to hear about it." When you're not asserting, guard with
valueOr.
error()
The error method returns the error value, throwing a ResultException if the
result is actually a success. You'll use it after a guard, and all over your
tests:
expect()
The expect method works like value, but the exception carries your
message — so when the impossible happens, whoever's on call knows what you were
assuming:
expectError()
The expectError method is expect for the error side:
valueOr()
The valueOr method returns the success value, or your fallback if the
operation failed — for when you don't care why:
valueOrElse()
The valueOrElse method computes the fallback from the error, and only when
it's actually needed:
map()
The map method transforms the success value without unpacking the result. A
failure passes through untouched:
mapError()
The mapError method transforms the error instead. It's the tool for
translating low-level errors into your domain's language at a boundary:
tap()
The tap method runs a side effect on the success value — logging, metrics,
notifications — and hands the result back unchanged:
tapError()
The tapError method does the same for the failure side. Together they let you
observe a pipeline without interrupting it:
chain()
The chain method pipes the success value into the next operation that can
itself fail. The first failure short-circuits everything after it, so a whole
workflow needs exactly one failure handler:
Notice how the error types stack up: add a step, and every handler downstream is made aware of what it might have to deal with.
[!NOTE] Why isn't this called
then()? Promise libraries — Guzzle, and therefore Laravel'sHttp::async(), and ReactPHP — treat any object with a publicthen()method as a promise and try to resolve it. A Result named that way would break inside promise pipelines, so it'schain().
orElse()
The orElse method is recovery: on failure, try something else that can itself
succeed or fail. A success passes straight through:
match()
The match method handles both outcomes in one expression. Both arms are
required — the failure path can't be forgotten:
Handling Different Error Types
Once you start chaining, the error side becomes a union — OrderError|PaymentError
— and you may want to treat them differently. PHP's own match has you covered:
Static analysers check that inner match against the union — add a third error
type to the chain and this exact spot gets flagged until you handle it.
If every error is handled the same way, skip the dispatch entirely and give your errors a shared interface:
And when a union grows past comfort, normalize early: use
mapError at each boundary so downstream code only ever sees one
error type.
Good to Know
- Results are immutable. Transformations never modify a result in place — they hand you a new one (or the same one, untouched).
- Results compare structurally.
Result::success(1) == Result::success(1)istrue. - Results serialize. As long as the contained value serializes, a result survives caches and queues.
- Results are sealed.
SuccessandFailureare final, soisFailure() === falsealways means success. ResultExceptionis the only exception this package throws — fromexpectandexpectErroron the wrong variant. It carries the offending value on->value, and when the error is itself aThrowable, you'll find it chained on->getPrevious()with its stack trace intact.
Development
License
MIT. See LICENSE.md.