helloimalemur/rustify-php

There is no license information available for the latest version (v0.0.1) of this package.

Rust-like Option and Result types for PHP

Installs: 1

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/helloimalemur/rustify-php

v0.0.1 2025-12-07 07:16 UTC

This package is auto-updated.

Last update: 2025-12-07 07:56:30 UTC


README

Rust-like Option and Result types for PHP 8+.

Fully compatible with typical PHP code

You can: • store Option or Result in arrays • return them from any function • use them as method return types • wrap exceptions with them • integrate with your Rust backend patterns (structural symmetry)

Option and Result behave like normal objects but with Rust semantics.

Install

composer require helloimalemur/rustify-php

Usage

use Rustify\Json;
use function Rustify\{ok, err, some, none};

$raw = '{"name":"James"}';

$res = Json::decodeObject($raw);

if ($res->isErr()) {
    $err = $res->unwrapErr();
    // handle error
} else {
    $data = $res->unwrap();
    // handle data
}

Construct Options

use Rustify\Option;
use function Rustify\{some, none};

$optA = some("value");
$optB = none();

if ($optA->isSome()) {
    $v = $optA->unwrap();
}

Functional transforms:

$nameOpt = some("James")
    ->map(fn($name) => strtoupper($name))
    ->andThen(fn($name) => strlen($name) > 0 ? some($name) : none());

$final = $nameOpt->unwrapOr("Unknown");

Construct Results

use Rustify\Result;
use function Rustify\{ok, err};

function open_file(string $path): Result
{
    return is_readable($path)
        ? ok(file_get_contents($path))
        : err("File not readable");
}

$res = open_file("config.json");

if ($res->isOk()) {
    $content = $res->unwrap();
} else {
    $error = $res->unwrapErr();
}

Chaining:

$parsed = open_file("config.json")
    ->andThen(fn($s) => ok(json_decode($s, true)))
    ->map(fn($arr) => $arr["name"] ?? "none")
    ->unwrapOr("missing");

Eager vs lazy fallbacks (orElseValue vs orElse)

There are two ways to provide a fallback for Option and Result:

  • Eager: provide another value right away using orElseValue(...).
  • Lazy: provide a closure that will be called only if needed using orElse(...).

Option example:

use function Rustify\{some, none};

$a = some('A');
$b = some('B');

// Eager: returns $a because it's Some
$x = $a->orElseValue($b);         // Some('A')

// Lazy: closure will NOT be called because $a is Some
$y = $a->orElse(fn() => some('C')); // Some('A')

// When None, eager uses provided value; lazy executes the closure
$n = none();
$x2 = $n->orElseValue($b);          // Some('B')
$y2 = $n->orElse(fn() => some('C')); // Some('C')

Result example (note: the lazy version receives the error):

use function Rustify\{ok, err};

$r1 = ok(10);
$r2 = err('network');

// Eager: value provided upfront
$a = $r1->orElseValue(ok(0)); // Ok(10)
$b = $r2->orElseValue(ok(0)); // Ok(0)

// Lazy: closure gets the error when called
$c = $r1->orElse(fn(string $e) => ok(0));     // Ok(10), closure not called
$d = $r2->orElse(fn(string $e) => ok(strlen($e))); // Ok(7)

// Likewise for unwrap defaults:
$n = $r2->unwrapOrElse(fn(string $e) => strlen($e)); // 7

Using Option and Result for API validation

function validateUser(array $body): Result
{
    if (!isset($body["email"]) || !is_string($body["email"])) {
    return err("Invalid email");
}
    if (!isset($body["name"]) || !is_string($body["name"])) {
    return err("Invalid name");
}

    return ok([
        "name" => $body["name"],
        "email" => $body["email"],
    ]);
}

Using helpers (if_some, if_ok)

use function Rustify\{if_some, if_ok};

$maybeToken = some("abc123");

if_some($maybeToken, function ($token) {
    error_log("Token = $token");
});

$result = ok(42);

if_ok($result, function ($v) {
    echo "Result: $v";
});

Match-like helpers (option_match, result_match)

use function Rustify\{option_match, result_match};

$opt = none();

$msg = option_match(
    $opt,
    fn($v) => "Some: $v",
    fn()   => "None"
);

$res = err("bad input");

$message = result_match(
    $res,
    fn($v) => "OK: $v",
    fn($e) => "ERR: $e"
);

Catching and logging errors with Result/Option

You can keep exceptions out of your core flow by returning Result/Option and logging at the boundary (controller/CLI/job). Here are a few idioms you can mix and match:

Log errors without changing the value using ifErr

use Rustify\Result;
use function Rustify\{ok, err};

function doWork(): Result
{
    // ... return ok($value) or err($reason)
}

$res = doWork();

$res->ifErr(function ($e) {
    // $e can be a string, array, or an Exception/Throwable you put there
    error_log('[doWork] failed: ' . (is_string($e) ? $e : (is_object($e) ? $e->getMessage() : json_encode($e))));
});

// Continue with a default if needed
$value = $res->unwrapOr('default');

Transform or annotate the error while logging using mapErr

$res = doWork()
    ->mapErr(function ($e) {
        error_log('[doWork] error: ' . (is_string($e) ? $e : (is_object($e) ? $e->getMessage() : json_encode($e))));
        // Optionally normalize to a domain error type/value
        return is_string($e) ? $e : 'internal_error';
    });

Recover from an error while logging using orElse

use function Rustify\ok;

$safe = doWork()
    ->orElse(function ($e) {
        error_log('[doWork] recovered: ' . (is_string($e) ? $e : (is_object($e) ? $e->getMessage() : json_encode($e))));
        return ok('fallback'); // provide a fallback Ok value
    })
    ->unwrap(); // safe because we've recovered

One‑liner logging with defaults using unwrapOrElse

$value = doWork()->unwrapOrElse(function ($e) {
    error_log('[doWork] defaulted: ' . (is_string($e) ? $e : (is_object($e) ? $e->getMessage() : json_encode($e))));
    return 'default';
});

Catch exceptions once, convert to Result, then log via the patterns above

use Rustify\Result;
use function Rustify\{ok, err};

function parseJsonSafe(string $raw): Result
{
    try {
        $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
        return ok($data);
    } catch (\Throwable $e) {
        return err($e); // store the Throwable in Err
    }
}

$res = parseJsonSafe($input);
$data = $res->unwrapOrElse(function ($e) {
    // $e is the Throwable we stored
    error_log('[parseJsonSafe] ' . $e->getMessage());
    return [];
});

Optional values: log when a value is missing using the ifNone pattern

use function Rustify\{some, none};

/** @return Rustify\Option */
function maybeEnv(string $key)
{
    $v = getenv($key);
    return $v === false ? none() : some($v);
}

$opt = maybeEnv('API_TOKEN');
$opt->ifNone(fn() => error_log('[env] API_TOKEN is not set'));
$token = $opt->unwrapOr('');
  • Prefer logging at the application boundary (controllers, handlers, CLI commands) rather than deep inside pure functions. This keeps core code testable and composable.
  • Use mapErr when you need to add context as the error bubbles up, orElse when you can safely recover with a fallback, and unwrapOrElse for concise defaulting with logging.

WHY!?

Null is not a value. It is an absence of information. It carries zero context about what and why something is, where it came from or what state the system is in.

Option and Result turn absence into structured flow, explicit alternate paths, recoverable and predictable outcomes. They convey intent, preserve context, and shorten debugging time.

Option represents an intentional “maybe”:

Some(value) → a value is present
None → a value is intentionally absent

Result represents the outcome of an operation:

Ok(value) → the operation succeeded
Err(error) → the operation failed, with a meaningful reason

By using Option and Result, the purpose of a function becomes explicit.

They eliminate ambiguity, prevent silent failures, and preserve information that null would throw away.

it’s practical
it reduces bugs
it simplifies reasoning
it improves interfaces
it prevents hours of debugging
it’s how you write reliable APIs

This is not “Functional Programming nonsense”, this is industry standard.

Swift has Optional<T>
Kotlin has Nullable<T> with smart handling
Rust has Option<T> and Result<T,E>
Haskell, Elm, OCaml all use Maybe/Result
TypeScript uses | undefined but companies implement Option for safety

Null introduces silent ambiguity

Null can mean any number of different things, but the language gives you no way to distinguish them. It could signal an error, missing data, invalid input, or an uninitialized value—yet all of these collapse into the same opaque null.

Example:

function findUser(int $id) {
    if ($id === 1) return ['id'=>1, 'name'=>'James'];
    return null;
}

What does null mean??? user not found?.. DB error?.. invalid input?.. developer forgot a return statement?.. exception swallowed somewhere?..

In production debugging, you cannot tell which one happened.

null destroys signal clarity.

Null forces defensive code everywhere

code you’ve written a thousand times:

$user = findUser($id);
    if ($user === null) {
    // handle maybe-error maybe-not-error?
}

Your brain has to constantly do “Is null okay here, or is null an error?” This cognitive cost multiplies across the codebase.

Null often fails far away from where the real problem happened

Consider: Fatal error: Call to a member function foo() on null. The real problem happened in a function 3 layers earlier, but the crash happened way later when dereferencing was attempted.

Option and Result prevent this entirely because they force handling.

Null causes production bugs that are extremely hard to trace

$total = $invoice['amount'] + $invoice['tax'];

If $invoice was null, you get a warning or TypeError. But logs don’t tell why it was null. No context survives and you lose the root cause.

Result fixes this by carrying failure information forward:

return err("Invoice not found");

You cannot lose the root cause.

Option makes “maybe” explicit

When something is optional, the type should say so, not the comments, not the mental model, not tribal knowledge.

Example:

function maybeGetEmail(User $u): ?string

// vs

function maybeGetEmail(User $u): Option<string>

Which one communicates meaning?

?string → maybe string, maybe null, maybe error, figure it out yourself
Option<string> → explicitly either Some(string) or None, handle both

Result replaces exceptions and null-return error codes with structured information

Exceptions in hot paths suck. null for errors is painful. Result gives a middle ground:

$result = doThing();

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

This is the same reason Go developers use error returns:

explicit
linear
predictable
no hidden control flow

Result makes success and failure equally obvious

return err("Invalid request");
return ok($user);

Both outcomes are visible. No ambiguity. No surprises.

Developers get better autocomplete and static analysis

PHPStan/Psalm can reason about:

Option<User>
Result<User, Error>

But they cannot reason about null except “maybe null, maybe not.”

So with Option/Result:

fewer runtime errors
more pre-runtime catches
more confident refactoring