Skip to content

maybePHP without null.
Errors without try/catch.

Option, Result, Schema and DTO for PHP 7.4+. Adopt one boundary at a time, no rewrite.

composer require gabrielalmir/maybe
customer.php
$result = Schema::shape([
    'email' => Schema::string()->trimmed(),
])->safeParse($payload)
    ->andThen(fn (array $data) => saveCustomer($data));

$result->match(
    fn (Customer $customer) => "Ok: saved",
    fn (mixed $error) => "Err: fix input"
);
A PHP Result pipeline that makes success and failure explicit
1
runtime dependency (opis/closure)
7.4 → 8.5
PHP versions tested in CI
Windows
Async job runs in CI, no pcntl
MIT
permissive, no strings attached

Same code path. Only one of them tells you what happened.

An order-confirmation email fails to send. Either checkout crashes over a non-critical side effect, or the failure is swallowed and nobody finds out the customer was never notified.

Without Maybe
// Silently invisible:
@mail($to, $subject, $body);

// "Handled", but the outcome is thrown away:
try {
    $mailer->send($to, $subject, $body);
} catch (\Exception $e) {
    error_log($e->getMessage());
}

The caller has no way to know whether the customer was notified, and no way to tell a malformed address from a timed-out relay.

With Maybe
$emailResult = $emailSchema->safeParse($message)
    ->andThen(static fn (array $valid): Result
        => sendWithFallback($valid));

$emailResult->match(
    static fn (string $ref): string => "sent ({$ref})",
    static fn (array $error): string => $error['retryable']
        ? "queued for retry"
        : "rejected: fix the input"
);

The error payload keeps `retryable` explicit. A malformed address and a flaky SMTP relay are different problems: one needs a data fix, the other a retry queue. The type stops them being handled identically by accident.

Five blocks, one philosophy.

The blocks are designed to compose with each other, in a single dependency that installs on PHP 7.4.

  • SchemaResult

    safeParse() never throws. It returns a Result carrying a ValidationErrorBag.

  • DTOSchema

    One schema, one immutable typed object, built from raw input.

  • OptionResult

    okOr() turns an absent value into a named error at the boundary.

The niche

It runs where legacy PHP actually lives.

Not a rewrite, not a framework. One small dependency that installs on the PHP you already have.

  • PHP 7.4 and up

    No enums, no promotion, no readonly. Tested in CI against 7.4, 8.2, 8.3, 8.4 and 8.5.

  • Windows, without extensions

    Async uses proc_open, not pcntl. A dedicated CI job proves it on Windows.

  • CodeIgniter 3

    Global helpers and a loadable library. Adopt one controller at a time.

  • Honest about async

    Process-based concurrency, deliberately not an event loop. Need one? Use amphp or reactphp.

In production

Three boundaries worth naming.

  • Transactional email that can't break checkout

    A confirmation email fails. Either the whole checkout crashes over a non-critical side effect, or the failure is silently swallowed.

  • Pushing orders into SAP without losing data silently

    SAP fails for structured reasons: duplicate document, missing cost center, expired session, timeout. Legacy code collapses them all into the same non-answer.

  • Contract validation with cross-field rules

    Validation scattered across a controller lets a contract get half-saved in an invalid state, with errors too unstructured for a review screen to point at the offending field.

Point your agent at the exact API.

A published llms.txt with exact method names and signatures, including the getters that deliberately do not exist, so an assistant stops inventing them.

Paired with AGENTS.md in the repository for anyone contributing with an assistant.

https://gabrielalmir.github.io/maybe/llms.txt

When not to use Maybe

Honesty first. Reach for something else when:

  • You're on modern PHP (8.1+) and already standardized on a mature specialist library you're happy with.
  • You need a full async event loop with non-blocking I/O. Use amphp or reactphp, not Async.
  • You need a validation engine with a large built-in rule catalog and i18n messages out of the box.
  • Your team strongly prefers exceptions and won't adopt Result at boundaries. The value comes from consistent use.
See the full comparison →

Start at the boundary you already have.

No framework coupling, no rewrite. Validate input, model a fallible service, and decide what happens at the edge.

composer require gabrielalmir/maybe

Start in 5 minutesRead the tutorial

v0.4.0 · MIT · PHP ≥ 7.4

New here? Read Why Maybe? for the trade-offs, follow the Tutorial to build a validated flow end to end, or keep the API Reference open while you work.