PHPackages                             valb/eco - 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. [Validation &amp; Sanitization](/categories/validation)
4. /
5. valb/eco

ActiveLibrary[Validation &amp; Sanitization](/categories/validation)

valb/eco
========

A PHP library to handle results, errors and validations.

v4.0.0(1mo ago)225↓100%[1 issues](https://github.com/valb-mig/php.eco/issues)MITPHPPHP &gt;=8.1CI passing

Since Mar 9Pushed 1mo agoCompare

[ Source](https://github.com/valb-mig/php.eco)[ Packagist](https://packagist.org/packages/valb/eco)[ RSS](/packages/valb-eco/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (5)Dependencies (3)Versions (8)Used By (0)

eco
===

[](#eco)

A lightweight PHP library for handling results and errors without exceptions.

[![PHP](https://camo.githubusercontent.com/4cc99e4b10627fa5bb11782e3a51e7daace17a2107820826c485fd66d8ebb2c9/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d253345253344382e312d3737374242343f6c6f676f3d706870266c6f676f436f6c6f723d7768697465)](https://php.net)[![License](https://camo.githubusercontent.com/f8df3091bbe1149f398a5369b2c39e896766f9f6efba3477c63e9b4aa940ef14/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d677265656e)](LICENSE)![Tests](https://camo.githubusercontent.com/19360868d46d926238f7645b00e60edcab047e140f62b6afa7923bb6fb641a52/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f74657374732d383725323070617373696e672d627269676874677265656e)![Coverage](https://camo.githubusercontent.com/32855e94577df9d0a30995653b17d33a5fbfdf644518f96ea0374313397d19b7/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f636f7665726167652d3130302532352d627269676874677265656e)

---

Why
---

[](#why)

PHP handles failures in two ways: return values you have to remember to check, or exceptions you have to remember to catch. Neither scales well across a large codebase.

**eco** gives you a third way — a `Result` type that makes failure explicit, composable, and impossible to ignore.

```
// Before — easy to forget to check
$user = $repo->find($id); // null? false? throws?

// After — failure is part of the type
$result = $repo->find($id); // Result

$result
    ->transform(fn($user) => $user->toArray())
    ->unwrapOrHandle(fn($errors) => response()->json($errors, 422));
```

---

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

[](#installation)

```
composer require valb/eco
```

**Requirements:** PHP 8.1+

---

Core concepts
-------------

[](#core-concepts)

eco has three classes:

- **`Result`** — represents the outcome of an operation. Either `ok` (carries a value) or `fail` (carries errors).
- **`Error`** — an immutable error with a machine-readable code, a human-readable message, and an optional field.
- **`ErrorCode`** — a built-in enum with universal codes (`GENERIC`, `VALIDATION`). Bring your own enum for domain-specific codes.

---

Result
------

[](#result)

### Creating results

[](#creating-results)

```
use Eco\Result;
use Eco\Error;

// Success with a value
Result::ok($user);
Result::ok(['id' => 1, 'name' => 'Ana']);

// Success without a value (delete, update, send email...)
Result::void();

// Failure — plain string is auto-wrapped in Error::generic()
Result::fail('Something went wrong.');

// Failure — typed errors
Result::fail(Error::validation('email', 'Invalid format.'));

// Failure — multiple errors at once
Result::fail(
    Error::validation('name',  'Required.'),
    Error::validation('email', 'Invalid format.'),
);
```

### Checking state

[](#checking-state)

```
$result->isOk();   // true when successful
$result->isFail(); // true when failed
```

### Accessing errors

[](#accessing-errors)

```
$result->getErrors();        // Error[]
$result->getErrorMessages(); // string[]
```

---

Pipeline
--------

[](#pipeline)

The pipeline has two parallel tracks — the **ok path** and the **fail path**. Each method only runs on its track and leaves the other untouched.

```
ok path   ──→  then()  ──→  transform()  ──→  flatMap()  ──→
fail path ──→  orThen() ──→  otherwise()  ─────────────────────────────────→

```

MethodRuns whenAlters Result?Use for`then()`oknoSide-effect with the value`orThen()`failnoSide-effect with the errors`transform()`okyes — new valueTransform the carried value`flatMap()`okyes — new ResultChain a Result-returning operation`otherwise()`failyes — new ResultRecover from failure### `then` and `orThen` — side-effects

[](#then-and-orthen--side-effects)

```
getUserById($id)
    ->then(fn($user)     => $logger->info("Loaded: {$user->name}"))  // ok path
    ->orThen(fn($errors) => $logger->warning('Not found', $errors)); // fail path
```

### `transform` — change the value

[](#transform--change-the-value)

```
getUserById($id)
    ->transform(fn(UserDTO $user) => $user->name)
    ->transform(fn(string $name)  => mb_strtoupper($name))
    ->default('ANONYMOUS');
```

### `flatMap` — chain operations that can also fail

[](#flatmap--chain-operations-that-can-also-fail)

Unlike `transform`, the callback must return a `Result`. Short-circuits on the first failure — subsequent steps are skipped.

```
Result::ok($input)
    ->flatMap(fn($input) => validate($input))  // can fail
    ->flatMap(fn($input) => persist($input))   // can fail
    ->transform(fn($user) => new UserDTO($user));
```

### `otherwise` — recover from failure

[](#otherwise--recover-from-failure)

```
fetchFromCache($key)
    ->otherwise(fn($errors) => fetchFromDatabase($key))
    ->otherwise(fn($errors) => Result::ok($defaultValue));
```

### Full pipeline example

[](#full-pipeline-example)

```
getUserById(999)
    ->then(fn($user)     => dump("[LOG] found: {$user->name}"))
    ->orThen(fn($errors) => dump('[LOG] user not found'))
    ->otherwise(fn($errors) => Result::ok(UserDTO::guest()))
    ->then(fn($user)     => dump('[LOG] continuing with user'))
    ->transform(fn(UserDTO $user) => $user->name)
    ->default('Anonymous');
```

---

Ensure
------

[](#ensure)

Use `ensure()` to validate the current value against **multiple conditions at once**, collecting every error from every failing rule before returning. Unlike short-circuiting approaches, `ensure()` always evaluates all rules — ideal when you want to surface all violations in a single pass.

Each entry in `$rules` must be a two-element array: a callable condition and an `Error` or string for when it fails.

```
Result::ok($name)
    ->ensure([
        [fn($v) => !empty($v),        Error::validation('name', 'Required.')],
        [fn($v) => strlen($v)  ctype_alpha($v),   Error::validation('name', 'Letters only.')],
    ])
    ->transform(fn($v) => StrHandler::sanitize($v));
```

Plain strings are accepted as a shorthand for `Error::generic()`:

```
Result::ok($value)
    ->ensure([
        [fn($v) => $v > 0,    'Must be positive.'],
        [fn($v) => $v < 1000, 'Must be less than 1000.'],
    ]);
```

Works with objects too:

```
Result::ok($user)
    ->ensure([
        [fn($user) => $user->isActive(),       Error::generic('User is inactive.')],
        [fn($user) => $user->hasRole('admin'), Error::make(AppErrorCode::UNAUTHORIZED, 'Access denied.')],
    ])
    ->transform(fn($user) => $user->toArray());
```

Skipped entirely when the Result is already a failure.

---

Unwrap — exiting the pipeline
-----------------------------

[](#unwrap--exiting-the-pipeline)

MethodReturns on okReturns on fail`unwrap()`valuethrows `LogicException``default($default)`value`$default``unwrapOrHandle($fn)`valuecalls `$fn(errors)`, returns `null````
// Throws if failed — use only when certain it succeeded
$value = $result->unwrap();

// Returns value or a fallback
$name = $result->default('Anonymous');

// Delegates failure handling to the caller
$user = $result->unwrapOrHandle(function (array $errors): void {
    http_response_code(422);
    echo json_encode(['errors' => $errors]);
    exit;
});

// Converts failure into a RuntimeException
$user = $result->unwrapOrHandle(Result::throwOnFail());
```

---

Combine
-------

[](#combine)

Use `combine()` when you want to run **all validations at once** and collect every error — unlike `flatMap` which stops at the first failure.

Pass the value to carry forward as the first argument.

```
function registerUser(array $data): Result
{
    return Result::ok($data)
        ->flatMap(fn($data) => Result::combine($data,
            !empty($data['name'])                   ? Result::void() : Result::fail(Error::validation('name',  'Required.')),
            !empty($data['email'])                  ? Result::void() : Result::fail(Error::validation('email', 'Required.')),
            str_contains($data['email'] ?? '', '@') ? Result::void() : Result::fail(Error::validation('email', 'Invalid format.')),
            ($data['age'] ?? 0) >= 18               ? Result::void() : Result::fail(Error::validation('age',   'Must be 18+.')),
        ))
        ->transform(fn($data) => new User($data['name'], $data['email']));
}

// All errors collected at once
$result = registerUser(['name' => '', 'email' => 'invalid', 'age' => 15]);
$result->getErrorMessages();
// ['Required.', 'Required.', 'Invalid format.', 'Must be 18+.']
```

When no value needs to be carried, pass `null` explicitly:

```
Result::combine(null, $resultA, $resultB, $resultC);
```

---

Error
-----

[](#error)

### Built-in factories

[](#built-in-factories)

```
use Eco\Error;

// Generic — unclassified error
Error::generic('Unexpected error.');

// Validation — tied to an input field
Error::validation('email', 'Must be a valid e-mail address.');
Error::validation('age',   'Must be at least 18.');
```

### Custom domain errors with `make()`

[](#custom-domain-errors-with-make)

For errors beyond the built-in codes, implement `ErrorCodeContract` on your own enum:

```
use Eco\Contracts\ErrorCodeContract;

enum AppErrorCode: string implements ErrorCodeContract
{
    case UNAUTHORIZED         = 'UNAUTHORIZED';
    case INSUFFICIENT_BALANCE = 'INSUFFICIENT_BALANCE';
    case ORDER_CANCELLED      = 'ORDER_ALREADY_CANCELLED';

    public function value(): string
    {
        return $this->value;
    }
}

// Then use Error::make() with your codes
Error::make(AppErrorCode::UNAUTHORIZED,         'Access denied.');
Error::make(AppErrorCode::INSUFFICIENT_BALANCE, 'Not enough credits.', 'balance');
```

### Serialization

[](#serialization)

```
$error = Error::validation('email', 'Invalid format.');

$error->toArray();
// ['code' => 'VALIDATION_ERROR', 'message' => 'Invalid format.', 'field' => 'email']

(string) $error;
// '[email] Invalid format. (VALIDATION_ERROR)'
```

### Comparing codes

[](#comparing-codes)

```
use Eco\Enums\ErrorCode;

if ($result->getErrors()[0]->code === ErrorCode::VALIDATION) {
    // handle validation error
}

if ($result->getErrors()[0]->code === AppErrorCode::UNAUTHORIZED) {
    // redirect to login
}
```

---

Real-world example
------------------

[](#real-world-example)

```
class CreateOrderHandler
{
    public function handle(array $input): Result
    {
        return Result::ok($input)
            ->flatMap(fn($input) => Result::combine($input,
                !empty($input['product_id'])  ? Result::void() : Result::fail(Error::validation('product_id', 'Required.')),
                ($input['quantity'] ?? 0) > 0 ? Result::void() : Result::fail(Error::validation('quantity',   'Must be greater than 0.')),
            ))
            ->flatMap(fn($input)   => $this->products->find($input['product_id']))
            ->then(fn($product)    => $this->logger->info("Creating order for {$product->name}"))
            ->flatMap(fn($product) => $this->orders->create($product, $input['quantity']))
            ->orThen(fn($errors)   => $this->logger->warning('Order creation failed', [
                'errors' => array_map(fn($e) => $e->toArray(), $errors),
            ]));
    }
}

// In your controller
$order = $handler->handle($request->all())
    ->unwrapOrHandle(function (array $errors): void {
        http_response_code(422);
        echo json_encode(['errors' => array_map(fn($e) => $e->toArray(), $errors)]);
        exit;
    });
```

---

Testing
-------

[](#testing)

```
# Run tests
composer test

# Coverage report in terminal
composer test:coverage-text

# Coverage report in browser
composer test:coverage
open coverage/index.html
```

---

License
-------

[](#license)

MIT — see [LICENSE](LICENSE).

###  Health Score

39

—

LowBetter than 86% of packages

Maintenance80

Actively maintained with recent releases

Popularity13

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity47

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

Every ~1 days

Total

6

Last Release

52d ago

Major Versions

v1.0.0 → v2.0.02026-03-09

v2.1.0 → v3.0.02026-03-16

v3.0.0 → v4.0.02026-03-18

### Community

Maintainers

![](https://www.gravatar.com/avatar/d2687eca112c9e21f3ed671be65f8f074edac9d12419c150de9de812322fc82e?d=identicon)[valb-mig](/maintainers/valb-mig)

---

Top Contributors

[![valb-mig](https://avatars.githubusercontent.com/u/102031404?v=4)](https://github.com/valb-mig "valb-mig (21 commits)")

---

Tags

librarypatternphpresultvalidationerrorresultfunctionalrailway

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/valb-eco/health.svg)

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

###  Alternatives

[composer/semver

Version comparison library that offers utilities, version constraint parsing and validation.

3.3k489.6M671](/packages/composer-semver)[giggsey/libphonenumber-for-php

A library for parsing, formatting, storing and validating international phone numbers, a PHP Port of Google's libphonenumber.

5.0k148.7M413](/packages/giggsey-libphonenumber-for-php)[respect/validation

The most awesome validation engine ever created for PHP

5.9k37.4M381](/packages/respect-validation)[propaganistas/laravel-phone

Adds phone number functionality to Laravel based on Google's libphonenumber API.

3.0k35.7M106](/packages/propaganistas-laravel-phone)[opis/json-schema

Json Schema Validator for PHP

64236.9M185](/packages/opis-json-schema)[giggsey/libphonenumber-for-php-lite

A lite version of giggsey/libphonenumber-for-php, which is a PHP Port of Google's libphonenumber

8412.9M47](/packages/giggsey-libphonenumber-for-php-lite)

PHPackages © 2026

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