Skip to content
← Back to projects

Result Type

A Result Type implementation in PHP

ResultType

GitHub Workflow Status (master) Total Downloads Latest Version License


#Result Type

Result Type provides small, typed objects for representing operations that may succeed, fail, or return no value.

Instead of throwing exceptions for expected outcomes, you may return a Result or Option and handle each case where it occurs.

use NunoMaduro\ResultType\ResultType;

$result = ResultType::try(fn () => json_decode($json, associative: true, flags: JSON_THROW_ON_ERROR));

$name = $result->match(
    ok: fn (array $payload): string => $payload['name'],
    err: fn (): string => 'Guest',
);

Result Type requires PHP 8.5+.

#Installation

You may install the package via Composer:

composer require nunomaduro/result-type

#Introduction

Result Type offers two main types:

Type States Use it when
Result Ok or Err An operation may succeed with a value or fail with an error.
Option Some or None A value may or may not exist, and absence is not an error.

Result and Option values are immutable. Methods such as map, mapErr, andThen, orElse, and filter return a new value instead of changing the original one.

#Creating Results

You may create a successful result using the ResultType::ok method, or a failed result using the ResultType::err method:

use NunoMaduro\ResultType\ResultType;

$result = ResultType::ok(42);

$error = ResultType::err('The user could not be created.');

The ResultType::try method may be used to convert exception-throwing code into a Result. If the callback completes successfully, an Ok result will be returned. If the callback throws, an Err result containing the thrown exception will be returned:

use NunoMaduro\ResultType\ResultType;

$result = ResultType::try(fn () => json_decode($json, associative: true, flags: JSON_THROW_ON_ERROR));

if ($result->isErr()) {
    report($result->unwrapErr());
}

#Creating Options

You may create an option containing a value using the OptionType::some method. To represent an absent value, use the OptionType::none method:

use NunoMaduro\ResultType\OptionType;

$name = OptionType::some('Taylor');

$missing = OptionType::none();

The OptionType::fromNullable method is useful when working with values that may be null:

$name = OptionType::fromNullable($request->input('name'));

#Matching Values

The most common way to consume a Result or Option is with the match method. The method receives one callback for each possible state and returns the value from the matching callback:

$message = ResultType::try(fn (): string => 'Profile updated.')->match(
    ok: fn (string $message): string => $message,
    err: fn (\Throwable $throwable): string => $throwable->getMessage(),
);

Options may be matched in the same way:

$name = OptionType::fromNullable($user->name)->match(
    some: fn (string $name): string => $name,
    none: fn (): string => 'Anonymous',
);

When you only need to branch by predicates, PHP's native match expression also works well with isOk, isErr, isSome, isNone, and the predicate helpers:

$label = match (true) {
    $result->isOkAnd(fn (array $user): bool => $user['role'] === 'admin') => 'Administrator',
    $result->isOk() => 'User',
    $result->isErr() => 'Guest',
};

For most value extraction, prefer the package's match method since it unwraps the correct branch for you and keeps both states visible at the call site.

#Unwrapping Values

You may use unwrap to retrieve the value inside an Ok or Some instance:

$value = ResultType::ok(42)->unwrap();

$name = OptionType::some('Taylor')->unwrap();

If unwrap is called on an Err or None, an exception will be thrown. If that is not the behavior you want, use unwrapOr to provide a default value:

$value = ResultType::err('Invalid amount.')->unwrapOr(0);

$name = OptionType::none()->unwrapOr('Anonymous');

You may also lazily compute the fallback value using unwrapOrElse:

$message = ResultType::err('Invalid amount.')->unwrapOrElse(
    fn (string $error): string => "Please try again: {$error}",
);

$name = OptionType::none()->unwrapOrElse(fn (): string => config('app.name'));

The expect and expectErr methods behave like unwrap and unwrapErr, but allow you to provide the exception message:

$user = $result->expect('The user should have been loaded.');

$error = $result->expectErr('The operation was expected to fail.');

#Transforming Results

The map method transforms the value inside an Ok result. If the result is an Err, the callback will not be executed:

$result = ResultType::ok(21)
    ->map(fn (int $value): int => $value * 2);

$result->unwrap(); // 42

The mapErr method transforms the error inside an Err result. If the result is Ok, the callback will not be executed:

$result = ResultType::err('not_found')
    ->mapErr(fn (string $error): string => strtoupper($error));

$result->unwrapErr(); // NOT_FOUND

The mapOr and mapOrElse methods are convenient when you want to transform an Ok value into a plain value while providing a fallback for Err:

$score = ResultType::ok(21)->mapOr(0, fn (int $value): int => $value * 2);

$message = ResultType::err('missing')->mapOrElse(
    default: fn (string $error): string => "Could not load: {$error}",
    callback: fn (string $value): string => $value,
);

#Chaining Results

The andThen method may be used to chain operations that each return a Result. If any step returns an Err, the remaining steps will not be executed:

use NunoMaduro\ResultType\Contracts\Result;
use NunoMaduro\ResultType\ResultType;

function findUser(int $id): Result
{
    // ...
}

function sendReceipt(User $user): Result
{
    // ...
}

$result = ResultType::ok($userId)
    ->andThen(fn (int $id): Result => findUser($id))
    ->andThen(fn (User $user): Result => sendReceipt($user));

The orElse method allows you to recover from an Err by returning another Result:

$result = ResultType::err('primary_failed')
    ->orElse(fn (string $error): Result => ResultType::ok('Recovered value'));

You may also combine results directly using and and or:

ResultType::ok('first')->and(ResultType::ok('second'))->unwrap(); // second

ResultType::err('failed')->or(ResultType::ok('fallback'))->unwrap(); // fallback

#Transforming Options

The map method transforms the value inside a Some option. If the option is None, the callback will not be executed:

$option = OptionType::some('taylor')
    ->map(fn (string $name): string => ucfirst($name));

$option->unwrap(); // Taylor

The filter method keeps the Some value only when the callback returns true:

$option = OptionType::some(42)
    ->filter(fn (int $value): bool => $value > 10);

The andThen method may be used to chain operations that each return an Option:

$timezone = OptionType::fromNullable($user->profile)
    ->andThen(fn (Profile $profile) => OptionType::fromNullable($profile->timezone));

The orElse method allows you to provide another option when the current option is None:

$name = OptionType::fromNullable($user->name)
    ->orElse(fn () => OptionType::some('Anonymous'));

#Converting Between Result and Option

Results may be converted to options using the ok and err methods:

$value = ResultType::ok(42)->ok();       // Some(42)
$error = ResultType::ok(42)->err();      // None

$value = ResultType::err('oops')->ok();  // None
$error = ResultType::err('oops')->err(); // Some('oops')

Options may be converted to results using okOr and okOrElse:

$result = OptionType::fromNullable($user)
    ->okOr('User not found.');

$result = OptionType::fromNullable($user)
    ->okOrElse(fn (): string => "User [{$id}] not found.");

#Inspecting Values

The inspect and inspectErr methods are useful for logging or debugging without changing the result:

$result = ResultType::try(fn () => $client->send($request))
    ->inspect(function (Response $response): void {
        logger()->info('Request sent.', [
            'status' => $response->status(),
        ]);
    })
    ->inspectErr(fn (\Throwable $throwable): void => report($throwable));

Options also provide inspect for running a side effect when the option is Some:

$option = OptionType::fromNullable($user->email)
    ->inspect(fn (string $email): void => logger()->info("Email found: {$email}"));

#Nested Values

The flatten method removes one level of nesting from nested results or options:

$result = ResultType::ok(ResultType::ok(42))->flatten();

$option = OptionType::some(OptionType::some('Taylor'))->flatten();

The transpose method converts between nested Result and Option shapes:

$option = ResultType::ok(OptionType::some(42))->transpose();

$result = OptionType::some(ResultType::ok(42))->transpose();

#Iterating Values

The iter method returns an iterable containing the successful or present value. Empty states return an empty iterable:

foreach (ResultType::ok(42)->iter() as $value) {
    // 42
}

foreach (OptionType::none()->iter() as $value) {
    // This block will not run.
}

#Converting to Arrays

You may convert results and options to arrays using the toArray method:

ResultType::ok(42)->toArray();       // ['ok' => true, 'value' => 42]
ResultType::err('oops')->toArray();  // ['ok' => false, 'error' => 'oops']

OptionType::some(42)->toArray();     // ['some' => true, 'value' => 42]
OptionType::none()->toArray();       // ['some' => false]

#Available Methods

Results provide the following methods: isOk, isErr, isOkAnd, isErrAnd, unwrap, unwrapErr, unwrapOr, unwrapOrElse, expect, expectErr, map, mapErr, mapOr, mapOrElse, and, or, andThen, orElse, match, inspect, inspectErr, ok, err, flatten, transpose, iter, and toArray.

Options provide the following methods: isSome, isNone, isSomeAnd, unwrap, unwrapOr, unwrapOrElse, expect, map, mapOr, mapOrElse, okOr, okOrElse, and, or, andThen, orElse, filter, match, inspect, flatten, transpose, iter, and toArray.

#Development

To keep the codebase formatted, run Pint:

composer lint

To run Rector:

composer refactor

To run static analysis:

composer test:types

To run the unit tests:

composer test:unit

To run the entire test suite:

composer test

Result Type was created by Nuno Maduro and is open-sourced software licensed under the MIT license.

New version available.