PHPackages                             denzyl/box - 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. [Debugging &amp; Profiling](/categories/debugging)
4. /
5. denzyl/box

ActiveLibrary[Debugging &amp; Profiling](/categories/debugging)

denzyl/box
==========

A type-safe Result type for explicit error handling in PHP.

v1.0.0(3w ago)41MITPHPPHP &gt;=8.2CI passing

Since May 18Pushed 2w ago1 watchersCompare

[ Source](https://github.com/denzyldick/Box)[ Packagist](https://packagist.org/packages/denzyl/box)[ Docs](https://github.com/denzyldick/box)[ RSS](/packages/denzyl-box/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (1)Dependencies (1)Versions (2)Used By (0)

Box
===

[](#box)

`Box` is a modern, type-safe error handling library for PHP 8.2+. It replaces implicit, optional exceptions with an explicit **Result type** that forces callers to acknowledge both success and failure.

Inspired by Rust's `Result` type, this library brings functional error handling and "railway oriented programming" to PHP with full support for **generics** (via PhpDoc), **immutability**, and **composition**.

---

Why a Result Type?
------------------

[](#why-a-result-type)

Traditional PHP error handling has two common patterns, both flawed:

PatternProblem**Exceptions**Uncaught exceptions crash the program. Nothing in the signature tells the caller an error can happen — you have to read the implementation or docs.**Returning null/false**Silent failures. The caller can forget to check. No error context — you lose *why* it failed.A `Result` solves both: the return type is always `Result`, so the caller **must** handle success and failure. The error variant carries the full `Throwable`, so nothing is lost. And since PHP has no native generics, PhpDoc annotations make the chain traceable for your IDE and static analysers.

---

The Generic Chain
-----------------

[](#the-generic-chain)

The library is designed around three components that work together:

```
Item  ──>  Box::put(Item)  ──>  Result

```

This split exists **by design**: separating the "what could go wrong" (the `Item` that may throw) from the "what do I do with the result" (the `Result`). You keep your domain logic clean (just throw when something's wrong) and get a functional, type-safe result on the other side.

### `Item` — The Input Contract

[](#itemt--the-input-contract)

The `Item` interface represents an operation that can produce a value of type `T` or throw. It's deliberately minimal: write your domain logic naturally, and let `Box::put()` convert it into a `Result`.

```
/** @template T */
interface Item
{
    /** @return T */
    public function grab(): mixed;
}
```

**Why `Item` exists:** Instead of wrapping every call in `try/catch` yourself, you declare "this is a fallible operation" by implementing `Item`. `Box::put()` handles the boilerplate of catching exceptions and wrapping them in an `Error` for you.

### `Box::put()` — The Gateway

[](#boxput--the-gateway)

`Box::put()` calls `grab()` and wraps the result in a `Result`, catching any exceptions:

```
/** @template TValue
 *  @param Item $bag
 *  @return Result
 */
public static function put(Item $bag): Result
```

**Why `Box::put()`:** It's the bridge between imperative domain code (which throws) and the functional result pipeline. Your `grab()` method throws naturally when something's wrong — `Box::put()` captures that throwable and turns it into an `Error` variant, keeping the API honest without forcing you to manually construct `Result::error()` everywhere.

### `Result` — The Output

[](#resultt-e--the-output)

A `Result` is either `Ok(T)` holding a value or `Error(E)` holding a `Throwable`. All methods are annotated with generics so your static analyser (PHPStan, Psalm, PhpStorm) can track types through chains of `map()`, `flatMap()`, etc.

**Why `Result` is immutable:** Every transformation (`map`, `filter`, `flatMap`, etc.) returns a *new* `Result` instance. This means you can safely share results across functions without worrying about mutation bugs. The original value is never modified.

### Wiring It Together

[](#wiring-it-together)

To preserve generic type information, your `Item` implementations **must** declare an `@implements` PhpDoc tag:

```
use Result\Item;
use Result\Box;
use Result\Result;

use App\Model\User;
use App\Exception\NotFoundException;

/**
 * @implements Item
 */
class UserFetcher implements Item
{
    public function __construct(
        private int $id,
        private Database $db,
    ) {}

    /** @return User */
    public function grab(): mixed
    {
        $user = $this->db->findUser($this->id);

        if ($user === null) {
            throw new NotFoundException("User {$this->id} not found");
        }

        return $user;
    }
}
```

Now calling `Box::put()` returns `Result`:

```
/**
 * @return Result
 */
function fetchUser(int $id): Result
{
    return Box::put(new UserFetcher($id, $this->db));
}
```

---

Creating Results
----------------

[](#creating-results)

```
// Direct — when you already have the value or error
$ok  = Result::ok("Happy value");            // Result
$err = Result::error(new Exception("Sad"));  // Result

// Try — wrap any callable that might throw
$res = Result::try(fn() => $this->riskyOp());

// When — conditional result without an if/else
$res = Result::when($age >= 18, "Access Granted", new Exception("Too young"));
```

**Why choose one over the other?**

MethodUse when...`Result::ok()`You already have a value and know it's valid`Result::error()`You already have a `Throwable` and want to represent failure`Result::try()`You have a callable that might throw and want to capture any exception automatically`Result::when()`You have a boolean condition and want to branch into Ok or Error in one expression — more concise than `if/else`---

Working with Results
--------------------

[](#working-with-results)

There are three styles, each with a different trade-off. Pick the one that fits your context.

### Pattern Matching (Explicit)

[](#pattern-matching-explicit)

```
$message = match ($result->state()) {
    ResultState::Ok    => $result->collect()->name,
    ResultState::Error => "Error: " . $result->exception()->getMessage(),
};
```

**Why use this:** When you need to handle both branches with completely different logic, or when your handler is complex enough that readability suffers from nested callbacks. The `match` expression forces you to enumerate all cases — the compiler won't let you forget the `Error` branch. Best for: **controller-level code** where you're translating domain results into HTTP responses or UI state.

### Railway (Implicit / Chain)

[](#railway-implicit--chain)

```
$nickname = fetchUser(123)
    ->map(fn(User $u) => $u->name)
    ->map(fn(string $name) => strtolower($name))
    ->unwrapOr("guest");
```

**Why use this:** When you only care about the success path and have a reasonable fallback for failure. Each `map()` only runs if the result is `Ok` — errors pass through silently. This is "railway oriented programming": the happy path runs along one track, the error track bypasses all operations. Best for: **data transformation pipelines** (parsing, validation, cleanup) where you're threading a value through a series of steps.

### Match (Functional)

[](#match-functional)

```
$result->match(
    onOk:  fn(User $u) => $u->name,
    onErr: fn($e)      => "fallback",
);
```

**Why use this:** More explicit than railway, more concise than `match ($result->state())`. Unlike the railway pattern, both branches are visible in one place. Unlike pattern matching, you don't need to import `ResultState`. Best for: **one-off handlers** where you want both branches visible but the logic is simple enough that a full `match` statement feels verbose.

### Compare the Three

[](#compare-the-three)

StyleVerbosityBoth branches visible?Best forPattern matchingHighYesComplex branching, API/UI handlersRailwayLowNo (chain assumes success)Data pipelines, transformationsFunctional matchMediumYesSimple handlers, callbacks---

Safe Unwrapping
---------------

[](#safe-unwrapping)

These methods extract the inner value. They differ in what happens when the result is an `Error`.

```
$val = $res->collect();                     // throws RuntimeException if Error
$val = $res->unwrapOr("default");           // returns "default" if Error
$val = $res->unwrapOrElse(fn($e) => ...);   // calls closure with the error if Error
$val = $res->expect("User must exist");     // throws with your message if Error
```

**Why so many?** Each serves a different failure-handling philosophy:

MethodWhen to use`collect()`When you *know* it must succeed (e.g. after `filter` + `recover`). Throws if you're wrong — it's a safety net for bugs in your logic, not a control flow mechanism.`unwrapOr($default)`When you have a simple static fallback. Use for: "default config value", "empty string", "0".`unwrapOrElse(callable)`When computing the fallback is expensive or requires the error context. The callable receives the `Throwable` so you can log it, format it, etc. Unlike `unwrapOr`, the fallback is **lazy** — it only runs if the result is an error.`expect($message)`Like `collect()` but with a custom error message that helps debugging. Use when the default `RuntimeException` message isn't descriptive enough.---

Side-Effects (Tap)
------------------

[](#side-effects-tap)

Sometimes you want to do something with the value without transforming it — logging, caching, metrics.

```
$result
    ->tapOk(fn($v) => Logger::info("Success: $v"))
    ->tapErr(fn($e) => Logger::error("Fail: " . $e->getMessage()));
```

Aliases: `inspect()` = `tapOk()`, `inspectErr()` = `tapErr()`.

**Why `tapOk` / `tapErr` exist:** `map()` implies transformation and should be pure. When you're just observing (logging, emitting events, incrementing counters), use `tapOk`/`tapErr`. They pass the result through unchanged — they're **side-effect windows** in an otherwise pure pipeline. The alias `inspect()` exists for familiarity if you come from Rust.

**The key difference from `map()`:**

- `map()` transforms the value and returns a new `Result`
- `tapOk()` runs a callback and returns the *same* `Result` instance (no transformation)

---

Transforming Collections
------------------------

[](#transforming-collections)

When your `Result` holds an iterable, these methods let you work with the elements.

```
$active = Result::ok($userList)
    ->filterEach(fn(User $u) => $u->isActive)
    ->mapEach(fn(User $u) => $u->email)
    ->collect();
```

**`mapEach(callable, $resetKeys = true)`** — transforms every element of the iterable. If `$resetKeys` is true, the returned array has sequential numeric keys (like `array_values()`). Set `$resetKeys = false` to preserve string or non-sequential keys.

**`filterEach(callable, $resetKeys = true)`** — keeps only elements where the predicate returns truthy.

**Why seperate methods?** Calling `map()` on a `Result` takes you from `Result` to `Result`. If `T` happens to be an array and you want to map its elements, you'd need to write `->map(fn(array $items) => array_map(...))` manually every time. `mapEach`/`filterEach` are convenience methods that skip the boilerplate. They also **only work on iterables** and throw `LogicException` otherwise — catching misuse early.

---

Transformation Reference
------------------------

[](#transformation-reference)

MethodInputOutputDescription`map(callable)``Result``Result`Transform the ok value, skip on error`mapOr(default, callable)``Result``TNew`Map ok value or return default`mapOrElse(errCb, okCb)``Result``TNew`Map either variant to a value`mapEach(callable)``Result``Result`Map each element of the iterable`flatMap(callable)``Result``Result`Chain another Result-returning operation`flatten()``Result``Result`Flatten nested Results`mapError(callable)``Result``Result`Transform the error`recover(callable)``Result``Result`Recover from error (turns Error→Ok)`orElse(callable)``Result``Result`Recover with another Result`filter(callable)``Result``Result`Error if predicate fails`filterEach(callable)``Result``Result`Filter elements of the iterable---

### Deep Dive: `flatMap`

[](#deep-dive-flatmap)

```
Result::ok(2)
    ->flatMap(fn(int $n) => match (true) {
        $n > 0 => Result::ok($n * 2),
        default => Result::error(new Exception("Must be positive")),
    });
```

**Why `flatMap` over `map`?** `map` wraps the callback's return in a new `Ok`. If your callback already returns a `Result`, you'd get `Result` — a nested result. `flatMap` avoids this nesting by expecting the callback itself to return a `Result`. Use `flatMap` when your callback *deliberately* decides success/failure (the inner logic can also produce an error). Use `map` when the inner logic is infallible and only transforms.

Callback returns `TNew`Callback returns `Result`Use `map`Yes — wraps in `Ok` automaticallyAvoid — creates nestingUse `flatMap`Errors (expects `Result`)Yes — no nesting---

### Deep Dive: `recover` vs `orElse` vs `mapError`

[](#deep-dive-recover-vs-orelse-vs-maperror)

All three handle errors, but with different goals:

```
// recover: turn error back into success with a computed value
$result->recover(fn(Exception $e) => "Fallback for: " . $e->getMessage());
// Result → Result

// orElse: turn error into a new Result (might still be an error)
$result->orElse(fn(Exception $e) => Result::error(new LoggedException($e)));
// Result → Result

// mapError: transform the error without changing Ok/Error state
$result->mapError(fn(Exception $e) => new DomainException("Wrapper", 0, $e));
// Result → Result
```

MethodOk passes through?Error becomes...`recover`Yes (unchanged)New `Ok` value`orElse`Yes (unchanged)Whatever `Result` the callback returns`mapError`Yes (unchanged)Same state, transformed error---

Batch Operations
----------------

[](#batch-operations)

When you have multiple independent results and need to combine them.

```
// combine: fail-fast, get all values or first error
$combined = Result::combine([$r1, $r2, $r3]);

// any: first success, or last error if all failed
$any = Result::any([$r1, $r2, $r3]);

// partition: split into successes and failures
$partitioned = Result::partition([$r1, $r2, $r3]);
```

**Why three different batch methods?**

MethodReturnsUse when...`combine()``Result`You need *all* results to proceed. Validation of a form with multiple fields — one field fails, the whole form fails.`any()``Result`You only need *one* success. Trying multiple API endpoints or cache replicas — the first one that works is enough.`partition()``{ok: array, error: array}`You want to process both successes and failures independently. Bulk operations where you need to report partial failures.---

Logical Composition
-------------------

[](#logical-composition)

```
$tuple = $r1->zip($r2);      // Result — pair two results
$res   = $r1->and($r2);      // $r2 if Ok, else $r1 — sequential dependency
$res   = $r1->or($r2);       // $r1 if Ok, else $r2 — fallback
```

**Why these matter:**

- **`zip($other)`** — when you need to combine two independent results into a tuple. Unlike `combine()`, `zip()` works on pairs and preserves the individual types `[A, B]` rather than flattening into a generic array.
- **`and($other)`** — "run this next only if I succeeded." Short-circuits on the first error. Use when operations are **sequential and dependent**: validate input → save to DB → send email. If validation fails, the email is never sent.
- **`or($other)`** — "try this if I failed." Short-circuits on the first success. Use when you have **alternatives**: load from cache → if missing, load from DB → if still missing, fetch from API.

---

Interoperability
----------------

[](#interoperability)

```
// JSON — useful for API responses
json_encode(Result::ok("hi"));            // {"state":"ok","value":"hi"}
json_encode(Result::error(new Exception("e")));  // {"state":"error","value":"e"}

// Iteration — treat Result as a collection of 0 or 1 items
foreach ($result as $value) { echo $value; }

// Debug — quick string representation
echo (string) $result;                     // Result::Ok('hi')
```

---

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

[](#installation)

```
composer require denzyl/box
```

Development
-----------

[](#development)

```
composer install
composer ci
```

The release gate validates Composer metadata, checks Mago formatting, runs Mago linting, runs Phanalist against `src/`, and executes the PHPUnit suite. For a local Phanalist checkout, run:

```
PHANALIST_BIN=../phanalist/phanalist composer phanalist
```

---

Philosophy
----------

[](#philosophy)

> "If an error can happen, the caller should have to look at it."

This library does not pretend PHP has checked exceptions. It simply provides a better failure model than `throw`-and-pray by making your API contracts honest and your code's intent explicit.

Because PHP lacks native generics, **PhpDoc annotations are the glue that makes type inference work**. Always add `@implements Item` to your Item classes so your IDE and static analysers can trace the generic chain from `Box::put()` through to the final `Result`.

###  Health Score

41

—

FairBetter than 87% of packages

Maintenance96

Actively maintained with recent releases

Popularity6

Limited adoption so far

Community7

Small or concentrated contributor base

Maturity46

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% 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

Unknown

Total

1

Last Release

22d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/1d0a8bd745f09678b85bde73cef8411e3f06cd38c0cb2de36da7bc153e67177a?d=identicon)[denzyl](/maintainers/denzyl)

---

Top Contributors

[![denzyldick](https://avatars.githubusercontent.com/u/2477646?v=4)](https://github.com/denzyldick "denzyldick (25 commits)")

---

Tags

error-handlingfunctional-programmingphpexceptionsresultfunctionalerror handling

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/denzyl-box/health.svg)

```
[![Health](https://phpackages.com/badges/denzyl-box/health.svg)](https://phpackages.com/packages/denzyl-box)
```

###  Alternatives

[vasek-purchart/tracy-blue-screen-bundle

This bundle lets you use the Tracy's debug screen in combination with the the default profiler in your Symfony application.

1178.1k](/packages/vasek-purchart-tracy-blue-screen-bundle)

PHPackages © 2026

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