helloimalemur / rustify-php
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
Requires
- php: >=8.0
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
mapErrwhen you need to add context as the error bubbles up,orElsewhen you can safely recover with a fallback, andunwrapOrElsefor 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