PHPackages                             jsoizo/php-result - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. jsoizo/php-result

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

jsoizo/php-result
=================

A type-safe Result type for PHP 8.1+ with PHPStan support.

v0.3.0(1mo ago)1218[2 PRs](https://github.com/jsoizo/php-result/pulls)MITPHPPHP ^8.1CI passing

Since Jan 18Pushed 4w agoCompare

[ Source](https://github.com/jsoizo/php-result)[ Packagist](https://packagist.org/packages/jsoizo/php-result)[ Docs](https://github.com/jsoizo/php-result)[ RSS](/packages/jsoizo-php-result/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (4)Dependencies (12)Versions (22)Used By (0)

php-result
==========

[](#php-result)

[![Latest Version on Packagist](https://camo.githubusercontent.com/ac3192124bc9ae85b9ebb7eca7f63f1d7e43b0c8a05b4350338b13741fce2f18/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6a736f697a6f2f7068702d726573756c742e737667)](https://packagist.org/packages/jsoizo/php-result)[![CI](https://github.com/jsoizo/php-result/actions/workflows/ci.yml/badge.svg)](https://github.com/jsoizo/php-result/actions/workflows/ci.yml)[![License](https://camo.githubusercontent.com/1e433bb48f6ba4d2033cfa6731f7639e609d5b9747ba776a8c2750f1693bc0cc/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f6a736f697a6f2f7068702d726573756c742e737667)](https://packagist.org/packages/jsoizo/php-result)

A type-safe Result type for PHP 8.1+ with PHPStan support.

Features
--------

[](#features)

- Zero dependencies
- PHPStan level max support
- Rich composition functions (map, flatMap, mapError)
- Inspired by functional programming Result/Either types

Installation
------------

[](#installation)

```
composer require jsoizo/php-result
```

Requirements
------------

[](#requirements)

The library runtime supports PHP 8.1+. The development and test tooling in this repository requires PHP 8.2+ because the locked Pest/PHPUnit toolchain requires PHP 8.2.

Basic Usage
-----------

[](#basic-usage)

```
use Jsoizo\Result\Result;

// Create Success/Failure
$success = Result::success(42);
$failure = Result::failure('error message');

// Transform values
$doubled = $success->map(fn($x) => $x * 2); // Success(84)

// Chain operations
$result = $success
    ->flatMap(fn($x) => $x > 0
        ? Result::success($x * 2)
        : Result::failure('must be positive'));

// Get value with default
$value = $failure->getOrElse(0); // 0

// Get value with lazy fallback computed from the error
$value = $failure->getOr(fn($error) => strlen($error)); // 13
// The callback is only invoked on Failure, so expensive defaults are never built on success.

// Catch exceptions
$result = Result::catch(fn() => riskyOperation());

// Catch only an expected exception class; anything else is rethrown
$result = Result::catch(
    fn() => json_decode($json, flags: JSON_THROW_ON_ERROR),
    JsonException::class
);
// Result - a TypeError would propagate instead of becoming a Failure

// Convert a nullable value into a Result
$result = Result::fromNullable($userRepo->find($id), fn() => 'user not found');
// null → Failure('user not found'), non-null → Success(User)
// Only null counts as absence: falsy values like '', 0, false become Success.

// Handle both cases with fold
$message = $result->fold(
    onFailure: fn($error) => "Error: {$error->getMessage()}",
    onSuccess: fn($value) => "Got: {$value}"
);

// Compose validations with flatMap
$result = validateEmail($input['email'])
    ->flatMap(fn($email) => validatePassword($input['password'])
    ->flatMap(fn($password) => createUser($email, $password)));
// If each step can fail differently, the error type is the union of all possible errors.

// Recover from failure with default value
$recovered = $failure->recover(fn($e) => 'default'); // Success('default')
// recover() always returns a successful Result, so the error type becomes never.

// Chain fallback operations
$result = fetchFromPrimaryDb()
    ->recoverWith(fn($e) => fetchFromSecondaryDb())
    ->recoverWith(fn($e) => Result::success('cached fallback'));

// Side effects for debugging/logging
$result = validateInput($data)
    ->tap(fn($v) => logger()->info("Valid: $v"))
    ->tapError(fn($e) => logger()->error("Invalid: $e"))
    ->flatMap(fn($v) => processData($v));

// Get value as nullable
$value = $result->getOrNull(); // T|null

// Get error as nullable
if (($error = $result->getErrorOrNull()) !== null) {
    logger()->error("Failed: $error");
}

// Flatten nested Results
$nested = Result::success(Result::success(42));
$flat = $nested->flatten(); // Success(42)
// Result becomes Result.

// Monad comprehension with binding (avoids nested flatMap)
$result = Result::binding(function () use ($orderId) {
    /** @var Order $order */
    $order = yield Result::catch(fn() => $orderRepo->find($orderId));
    /** @var list $items */
    $items = yield Result::catch(fn() => $order->loadItems());
    return $items;
});
// Returns Result - short-circuits on first failure
// Every yielded value must be a Result; invalid yields throw ResultException.

// Accumulate a list of Results into one, collecting all errors
$result = Result::accumulate([
    validateName($input['name']),
    validateAge($input['age']),
    validateEmail($input['email']),
]);
// All Success → Success([name, age, email])
// Any Failure → Failure(['Name required', 'Invalid email']) (non-empty-list of errors)

// Sequence a list of Results into one, stopping at the first error
$result = Result::sequence([
    loadConfig($path),
    connectDb($dsn),
    fetchUser($id),
]);
// All Success → Success([config, connection, user])
// Any Failure → Failure('config not found') (first error, unwrapped)

// Accumulate errors from multiple independent validations
$result = Result::accumulate3(
    validateName($input['name']),
    validateAge($input['age']),
    validateEmail($input['email']),
    fn(string $name, int $age, string $email) => new User($name, $age, $email)
);
// All Success → Success(User(...))
// Any Failure → Failure(['Name required', 'Invalid email']) (non-empty-list of errors)
```

Use `accumulate($results)` for a homogeneous list of same-typed Results; use `accumulate2()`–`accumulate9()` to combine differently-typed Results into one value via a transform function. Use `sequence($results)` when you want fail-fast semantics instead: validation → `accumulate` (report all errors), sequential composition → `sequence` (stop at the first failure, error type stays as-is).

API
---

[](#api)

### Factory Methods

[](#factory-methods)

MethodDescription`Result::success($value)`Create a Success`Result::failure($error)`Create a Failure`Result::catch(callable $fn, string $exceptionClass = Throwable::class)`Wrap exception-throwing code, optionally capturing only a given exception class`Result::fromNullable($value, callable $onNull)`Convert a nullable value into a Result`Result::binding(callable $fn)`Monad comprehension using generators`Result::accumulate($results)`Convert a list of Results into one Result, collecting all errors`Result::sequence($results)`Convert a list of Results into one Result, stopping at the first error`Result::accumulate2($r1, ..., $transform)`Combine 2 Results, collecting all errors`Result::accumulate3($r1, ..., $transform)`Combine 3 Results, collecting all errors`Result::accumulate4($r1, ..., $transform)`Combine 4 Results, collecting all errors`Result::accumulate5($r1, ..., $transform)`Combine 5 Results, collecting all errors`Result::accumulate6($r1, ..., $transform)`Combine 6 Results, collecting all errors`Result::accumulate7($r1, ..., $transform)`Combine 7 Results, collecting all errors`Result::accumulate8($r1, ..., $transform)`Combine 8 Results, collecting all errors`Result::accumulate9($r1, ..., $transform)`Combine 9 Results, collecting all errors### Instance Methods

[](#instance-methods)

MethodDescription`isSuccess()`Returns true if Success`isFailure()`Returns true if Failure`getOrElse($default)`Get value or default`getOr($fn)`Get value or compute fallback lazily from error`get()`Get value or throw`getErrorOrElse($default)`Get error or default`getErrorOr($fn)`Get error or compute fallback lazily from value`getError()`Get error or throw ResultException`map($fn)`Transform success value`mapError($fn)`Transform error value`flatMap($fn)`Chain Result-returning operations`fold($onFailure, $onSuccess)`Handle both cases and return a value`recover($fn)`Recover from error with a value`recoverWith($fn)`Recover from error with a Result`tap($fn)`Execute side effect on success value, return same Result`tapError($fn)`Execute side effect on error value, return same Result`getOrNull()`Get success value or null`getErrorOrNull()`Get error value or null`flatten()`Flatten nested `Result` into `Result`### Error Types in flatMap

[](#error-types-in-flatmap)

`flatMap()` preserves both the original error type and the error type returned by the callback:

```
/** @var Result $result */
$saved = $result->flatMap(fn(string $value) => save($value));
// If save() returns Result, $saved is Result.
```

Long chains can naturally produce wide error unions. When that becomes awkward, normalize errors with `mapError()` to a domain-specific error type at a boundary.

### Recovering with Different Success Types

[](#recovering-with-different-success-types)

`recover()` and `recoverWith()` can return a different success type than the original `Result`:

```
/** @var Result $result */
$recovered = $result->recover(fn(string $error) => false);
// Result
```

After `recover()`, the Result can no longer be a Failure. `recoverWith()` keeps the callback's error type because the fallback operation may still fail.

### Flattening Nested Results

[](#flattening-nested-results)

`flatten()` preserves nested generic types. If the success value is another `Result`, the inner success type is used and the outer and inner error types are combined:

```
/** @var Result $result */
$flat = $result->flatten();
// Result
```

Calling `flatten()` on a non-nested Result keeps the original type.

PHPStan Integration
-------------------

[](#phpstan-integration)

### Sealed Class Support

[](#sealed-class-support)

Result is marked as a sealed class using `@phpstan-sealed`. This prevents creating custom subclasses of Result outside of Success and Failure.

```
// PHPStan will report an error for unauthorized subclasses:
// "Type CustomResult is not allowed to be a subtype of Result"
class CustomResult extends Result { ... }
```

Requirements:

- PHPStan 2.1.18 or later
- No additional packages needed

See: [PHPStan Sealed Classes](https://phpstan.org/writing-php-code/phpdocs-basics#sealed-classes)

### Type Narrowing with isSuccess/isFailure

[](#type-narrowing-with-issuccessisfailure)

The `isSuccess()` and `isFailure()` methods support [PHPStan type narrowing](https://phpstan.org/writing-php-code/narrowing-types):

```
/** @param Result $result */
function handleResult(Result $result): void
{
    if ($result->isSuccess()) {
        // PHPStan knows $result is Success
        $user = $result->get();
    } else {
        // PHPStan knows $result is Failure
        $error = $result->getError();
    }
}

// Early return pattern
/** @param Result $result */
function getValue(Result $result): int
{
    if ($result->isFailure()) {
        return -1;
    }
    // PHPStan knows $result is Success
    return $result->get();
}
```

### Match Exhaustiveness Check

[](#match-exhaustiveness-check)

This library includes a custom PHPStan rule that ensures match expressions on Result types are exhaustive.

**Setup:**

The rule is automatically enabled when you use PHPStan with this library (via `composer.json` extra config). Alternatively, add to your `phpstan.neon`:

```
includes:
    - vendor/jsoizo/php-result/extension.neon
```

**What it checks:**

```
// Error: Match expression on Result type is not exhaustive. Missing: Failure.
match (true) {
    $result instanceof Success => 'success',
};

// OK: All cases covered
match (true) {
    $result instanceof Success => 'success',
    $result instanceof Failure => 'failure',
};

// OK: default covers remaining cases
match (true) {
    $result instanceof Success => 'success',
    default => 'failure',
};
```

**Note: PHPStan's `match.unhandled` error**

Even when all cases are covered, PHPStan may report `Match expression does not handle remaining value: true`. This is because PHPStan doesn't use sealed class information for match exhaustiveness.

To suppress this error, use the `@phpstan-ignore` comment:

```
/** @phpstan-ignore match.unhandled (Result is sealed: Success|Failure) */
return match (true) {
    $result instanceof Success => 'success',
    $result instanceof Failure => 'failure',
};
```

The custom rule in this library ensures exhaustiveness, so it's safe to ignore `match.unhandled` for Result types.

**Limitations:**

The custom rule tracks simple variables in `instanceof` match arms, such as `$result instanceof Success`. It intentionally does not check property fetches or method calls such as `$this->result instanceof Success` or `$this->getResult() instanceof Success`.

###  Health Score

41

—

FairBetter than 87% of packages

Maintenance93

Actively maintained with recent releases

Popularity13

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity43

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 94.2% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~55 days

Total

4

Last Release

45d ago

PHP version history (2 changes)v0.1.0PHP ^8.2

v0.2.0PHP ^8.1

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/3462087?v=4)[jsoizo](/maintainers/jsoizo)[@jsoizo](https://github.com/jsoizo)

---

Top Contributors

[![jsoizo](https://avatars.githubusercontent.com/u/3462087?v=4)](https://github.com/jsoizo "jsoizo (97 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (6 commits)")

---

Tags

PHPStanresulterror handlingfunctional-programmingtype-safe

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/jsoizo-php-result/health.svg)

```
[![Health](https://phpackages.com/badges/jsoizo-php-result/health.svg)](https://phpackages.com/packages/jsoizo-php-result)
```

###  Alternatives

[graham-campbell/result-type

An Implementation Of The Result Type

554427.4M14](/packages/graham-campbell-result-type)[symfony/type-info

Extracts PHP types information.

20077.1M314](/packages/symfony-type-info)[ergebnis/phpstan-rules

Provides rules for phpstan/phpstan.

46010.2M324](/packages/ergebnis-phpstan-rules)[php-stubs/wordpress-stubs

WordPress function and class declaration stubs for static analysis.

20117.1M486](/packages/php-stubs-wordpress-stubs)[php-stubs/woocommerce-stubs

WooCommerce function and class declaration stubs for static analysis.

953.7M128](/packages/php-stubs-woocommerce-stubs)[ejsmont-artur/php-circuit-breaker

PHP Circuit Breaker component

1681.0M4](/packages/ejsmont-artur-php-circuit-breaker)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
