定制 mhert/strict-returns 二次开发

按需修改功能、优化性能、对接业务系统,提供一站式技术支持

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

mhert/strict-returns

Composer 安装命令:

composer require mhert/strict-returns

包简介

Rust-style Result and Option types for PHP

README 文档

README

PHP License

Rust-style Result and Option types for PHP. Make failure and absence part of your return types — so callers can't quietly ignore them, and your static analyzer can prove they didn't.

"I call it my billion-dollar mistake. It was the invention of the null reference in 1965."

Tony Hoare, inventor of the null reference

use StrictReturns\Result\Err;
use StrictReturns\Result\Ok;
use StrictReturns\Result\Result;

/** @return Result<int, MathError> */
function divide(int $dividend, int $divisor): Result
{
    return $divisor === 0
        ? Result::err(MathError::DivisionByZero)
        : Result::ok(intdiv($dividend, $divisor));
}

$result = divide(10, 2);

echo match ($result::class) {
    Ok::class  => $result->value(),       // 5 — narrowed to Ok here
    Err::class => $result->error()->name, // the failure, as a value
};

Why

PHP has two conventional ways to say "this operation might not produce a value," and both are easy to get wrong:

Returning null — the absence is invisible in the type and trivial to forget:

$user = $repo->findByEmail($email); // User|null — but the signature often just says "User"
echo $user->name;                   // 💥 TypeError, far from where the null came from

Throwing for expected failures — control flow you can't see in the signature and can silently skip:

$price = $orders->priceFor($email, $qty); // may throw UnknownUserException... or may not. Who knows?

strict-returns moves the failure/absence into the return value:

  • A function that can fail returns Result<T, E> — either an Ok carrying a value or an Err carrying an error.
  • A function that might find nothing returns Option<T> — either Some with a value or None.

To read the value out you go through value()/error(), and the failure is part of the return type — a Result<int, MathError> is not an int, so it can't be mistaken for one. Match on the concrete variant — $result::class against Ok::class / Err::class — and a static analyzer that understands the library's types (this project uses Mago) narrows each arm to the exact type. Reach for the wrong side anyway and you get a loud UnwrapException at runtime instead of a silently wrong result.

Errors become ordinary values you pass around, branch on, and propagate — not exceptions thrown across half your call stack.

Requirements

  • PHP 8.5+
  • carthage-software/mago 1.43+

Quick start

Result<T, E> — success or failure

use StrictReturns\Result\Err;
use StrictReturns\Result\Ok;
use StrictReturns\Result\Result;

enum MathError
{
    case DivisionByZero;
}

/** @return Result<int, MathError> */
function divide(int $dividend, int $divisor): Result
{
    if ($divisor === 0) {
        return Result::err(MathError::DivisionByZero);
    }

    return Result::ok(intdiv($dividend, $divisor));
}

$result = divide(10, 0);

echo match ($result::class) {
    Ok::class  => "result: {$result->value()}",
    Err::class => "failed: {$result->error()->name}", // failed: DivisionByZero
};

Option<T> — value or nothing

use StrictReturns\Option\None;
use StrictReturns\Option\Option;
use StrictReturns\Option\Some;

final readonly class User
{
    public function __construct(
        public string $email,
        public string $name
    ) {
    }
}

/** @return Option<User> */
function findByEmail(array $usersByEmail, string $email): Option
{
    if (!array_key_exists($email, $usersByEmail)) {
        return Option::none();
    }

    return Option::some($usersByEmail[$email]);
}

$users = ['ada@example.com' => new User('ada@example.com', 'Ada')];

$found = findByEmail($users, 'ada@example.com');

echo match ($found::class) {
    Some::class => $found->value()->name,
    None::class => 'no such user',
};

How "strict" works

Two mechanisms keep you honest — one at analysis time, one at runtime. (Snippets from here on elide the use imports shown in the quick start.)

1. Static narrowing. Each variant is its own concrete class — Ok/Err, Some/None — so matching on $result::class narrows the object to the exact variant in each arm, and every accessor you reach for gets a precise type. Mago checks the match is exhaustive over the sealed Ok/Err pair, so no default is needed:

$result = divide(10, 2); // Result<int, MathError>

echo match ($result::class) {
    // $result is narrowed to Ok<int, MathError> here → value() returns int
    Ok::class  => "= {$result->value()}",
    // …and to Err<int, MathError> here → error() returns MathError
    Err::class => "! {$result->error()->name}",
};

2. Runtime guard. If you reach for the wrong variant anyway, the accessor throws instead of returning a bogus value — the equivalent of Rust's .unwrap() panicking:

Result::ok(5)->error();                      // throws UnwrapException: "Called error() on an Ok value."
Result::err(MathError::DivisionByZero)->value(); // throws UnwrapException: "Called value() on an Err value."
Option::none()->value();                     // throws UnwrapException: "Called value() on a None value."

Each type ships its own exception — Result accessors throw StrictReturns\Result\UnwrapException, Option accessors throw StrictReturns\Option\UnwrapException — so catch the one matching the type you're unwrapping.

Coming from Rust: isOk/isSome map to is_ok/is_some, and value()/error() are the checked unwraps.

Composition without combinators

The API is intentionally tiny — there is no map, andThen, or unwrapOr. You branch with ordinary PHP and let the analyzer track the types.

Propagate the first failure — a narrowed Err<T, E> is a valid Result<T, E>, so you can return it straight through:

/** @return Result<int, MathError> */
function divideChain(int $a, int $b, int $c): Result
{
    $first = divide($a, $b);

    return match ($first::class) {
        Err::class => $first,                      // narrowed to Err → a valid Result, propagated as-is
        Ok::class  => divide($first->value(), $c), // narrowed to Ok  → value() is safe
    };
}

Provide a fallback for an absent value:

$name = match ($found::class) {
    Some::class => $found->value()->name,
    None::class => 'guest',
};

Combine both types — consume an Option inside a Result-returning method:

/** @return Result<int, OrderError> */
public function priceFor(string $email, int $quantity): Result
{
    if ($quantity <= 0) {
        return Result::err(OrderError::InvalidQuantity);
    }

    $user = $this->users->findByEmail($email);

    return match ($user::class) {
        Some::class => Result::ok($quantity * self::UNIT_PRICE),
        None::class => Result::err(OrderError::UnknownUser),
    };
}

Unwrap or throw at a boundary — when the caller can't proceed without the value (a controller, say), a throw arm collapses the match to the success type:

/** @param Option<User> $found */
function requireUser(Option $found): User
{
    return match ($found::class) {
        Some::class => $found->value(),
        None::class => throw new RuntimeException('user not found'),
    };
}

API reference

StrictReturns\Result\Result<T, E>

Abstract, readonly. Concrete variants: Ok<T, E> and Err<T, E>.

Member Returns Description
Result::ok(mixed $value = null) Ok<T, never> Success carrying $value. The default null models a payload-free success (Rust's Result<(), E>).
Result::err(mixed $error) Err<never, E> Failure carrying $error.
isOk(): bool bool true for Ok. Narrows $this to Ok<T, E> / Err<T, E>.
isErr(): bool bool true for Err. Narrows $this to Err<T, E> / Ok<T, E>.
value(): T T The success value. Throws UnwrapException when called on an Err.
error(): E E The error value. Throws UnwrapException when called on an Ok.

StrictReturns\Option\Option<T>

Abstract, readonly. Concrete variants: Some<T> and None<T>.

Member Returns Description
Option::some(mixed $value) Some<T> A present value.
Option::none() None<never> Absence.
isSome(): bool bool true for Some. Narrows $this to Some<T> / None<T>.
isNone(): bool bool true for None. Narrows $this to None<T> / Some<T>.
value(): T T The contained value. Throws UnwrapException when called on a None.

Exceptions

  • StrictReturns\Result\UnwrapException — thrown by Result::value() on an Err and Result::error() on an Ok.
  • StrictReturns\Option\UnwrapException — thrown by Option::value() on a None.

Both extend \Exception.

Design notes

  • Immutable. The Result and Option types are readonly; a Result/Option never changes after construction. (The UnwrapException classes extend Exception, which cannot be readonly.)
  • Covariant generics. The type parameters are @template-covariant, so Result::ok(5) (an Ok<int, never>) satisfies Result<int, MathError>, and a narrowed Err can be returned where the wider Result is expected. This is what makes error propagation type-check cleanly.
  • No shared instances. Every factory call — Result::ok(), Result::err(), Option::some(), Option::none() — returns a fresh instance, so all variants behave the same regardless of how they were created. Branch on ::class, not on identity.
  • Minimal on purpose. No monadic combinator zoo. The surface is construct → inspect → access, and everything else is plain PHP you can read at a glance.

License

MIT © Mathias Hertlein. See LICENSE.

统计信息

  • 总下载量: 0
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 0
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-07-12

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固