brimmar/phpresult

PHP的类似Rust的结果类型

v1.0.0 2024-08-01 20:36 UTC

This package is auto-updated.

Last update: 2024-10-02 14:51:38 UTC


README

本文档涵盖了PHP Rust-like Result Type的实现。结果类型用于返回和传递错误。它有两个变体:Ok,表示成功并包含一个值,以及Err,表示错误并包含一个错误值。

目录

  1. 结果接口
  2. 用法
  3. 方法
  4. 互补包
  5. 静态分析
  6. 贡献
  7. 安全漏洞
  8. 许可

结果接口

Result接口定义了OkErr类之间的契约。

<?php

namespace Brimmar\PhpResult\Interfaces;

/**
 * @template T
 * @template E
 */
interface Result
{
    // ... (methods will be documented below)
}

用法

第一个例子

<?php
use Brimmar\PhpResult\Ok;
use Brimmar\PhpResult\Err;
use Brimmar\PhpResult\Interfaces\Result;

class UserRegistration
{
    private $db;
    private $emailService;

    public function __construct(Database $db, EmailService $emailService)
    {
        $this->db = $db;
        $this->emailService = $emailService;
    }

    public function registerUser(string $username, string $email, string $password): Result
    {
        return $this->validateInput($username, $email, $password)
            ->andThen(fn() => $this->checkUserExists($username, $email))
            ->andThen(fn() => $this->hashPassword($password))
            ->andThen(fn($hashedPassword) => $this->saveUser($username, $email, $hashedPassword))
            ->andThen(fn($userId) => $this->sendWelcomeEmail($userId, $email));
    }

    private function validateInput(string $username, string $email, string $password): Result
    {
        if (strlen($username) < 3) {
            return new Err("Username must be at least 3 characters long");
        }
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            return new Err("Invalid email address");
        }
        if (strlen($password) < 8) {
            return new Err("Password must be at least 8 characters long");
        }
        return new Ok(null);
    }

    private function checkUserExists(string $username, string $email): Result
    {
        $exists = $this->db->query("SELECT id FROM users WHERE username = ? OR email = ?", [$username, $email])->fetchColumn();
        return $exists ? new Err("Username or email already exists") : new Ok(null);
    }

    private function hashPassword(string $password): Result
    {
        $hashedPassword = password_hash($password, PASSWORD_DEFAULT);
        return $hashedPassword ? new Ok($hashedPassword) : new Err("Failed to hash password");
    }

    private function saveUser(string $username, string $email, string $hashedPassword): Result
    {
        $userId = $this->db->insert("INSERT INTO users (username, email, password) VALUES (?, ?, ?)", [$username, $email, $hashedPassword]);
        return $userId ? new Ok($userId) : new Err("Failed to save user to database");
    }

    private function sendWelcomeEmail(int $userId, string $email): Result
    {
        $sent = $this->emailService->send($email, "Welcome to our service!", "Thank you for registering...");
        return $sent ? new Ok($userId) : new Err("Failed to send welcome email");
    }
}

$registration = new UserRegistration($db, $emailService);
$result = $registration->registerUser("johndoe", "john@example.com", "password123")->match(
    Ok: fn($value) => echo "User registered successfully with ID: $value",
    Err: fn($error) => echo "Registration failed: $error",
);

第二个例子

<?php
use Brimmar\PhpResult\Ok;
use Brimmar\PhpResult\Err;
use Brimmar\PhpResult\Interfaces\Result;

class WeatherApiClient
{
    private $httpClient;
    private $cache;
    private $rateLimiter;
    private $apiKey;

    public function __construct(HttpClient $httpClient, CacheInterface $cache, RateLimiter $rateLimiter, string $apiKey)
    {
        $this->httpClient = $httpClient;
        $this->cache = $cache;
        $this->rateLimiter = $rateLimiter;
        $this->apiKey = $apiKey;
    }

    public function getWeatherForecast(string $city): Result
    {
        return $this->checkRateLimit()
            ->andThen(fn() => $this->getCachedForecast($city))
            ->orElse(fn() => $this->fetchForecastFromApi($city))
            ->andThen(fn($forecast) => $this->cacheForecast($city, $forecast));
    }

    private function checkRateLimit(): Result
    {
        return $this->rateLimiter->isAllowed('weather_api')
            ? new Ok(null)
            : new Err("Rate limit exceeded. Please try again later.");
    }

    private function getCachedForecast(string $city): Result
    {
        $cachedForecast = $this->cache->get("weather_forecast:$city");
        return $cachedForecast ? new Ok($cachedForecast) : new Err("Cache miss");
    }

    private function fetchForecastFromApi(string $city): Result
    {
        try {
            $response = $this->httpClient->get("https://api.weather.com/forecast", [
                'query' => ['city' => $city, 'apikey' => $this->apiKey]
            ]);

            if ($response->getStatusCode() !== 200) {
                return new Err("API request failed with status code: " . $response->getStatusCode());
            }

            $forecast = json_decode($response->getBody(), true);
            return new Ok($forecast);
        } catch (\Exception $e) {
            return new Err("Failed to fetch forecast: " . $e->getMessage());
        }
    }

    private function cacheForecast(string $city, array $forecast): Result
    {
        $cached = $this->cache->set("weather_forecast:$city", $forecast, 3600); // Cache for 1 hour
        return $cached ? new Ok($forecast) : new Err("Failed to cache forecast");
    }
}

$weatherClient = new WeatherApiClient($httpClient, $cache, $rateLimiter, 'your-api-key');
$result = $weatherClient->getWeatherForecast("New York")->match(
    Ok: fn($value) => echo "Weather forecast for New York: $value['summary']",
    Err: fn($error) => echo "Failed to get weather forecast: $error",
);;

第三个例子

<?php
use Brimmar\PhpResult\Ok;
use Brimmar\PhpResult\Err;
use Brimmar\PhpResult\Interfaces\Result;

class ConfigManager
{
    private $configs = [];

    public function getConfig(string $key): Result
    {
        return $this->getFromEnvironment($key)
            ->or($this->getFromFile($key))
            ->orElse(fn() => $this->getDefaultConfig($key));
    }

    private function getFromEnvironment(string $key): Result
    {
        $value = getenv($key);
        return $value !== false ? new Ok($value) : new Err("Not found in environment");
    }

    private function getFromFile(string $key): Result
    {
        return isset($this->configs[$key])
            ? new Ok($this->configs[$key])
            : new Err("Not found in config file");
    }

    private function getDefaultConfig(string $key): Result
    {
        $defaults = ['timeout' => 30, 'retries' => 3];
        return isset($defaults[$key])
            ? new Ok($defaults[$key])
            : new Err("No default value for $key");
    }

    public function setConfig(string $key, $value): void
    {
        $this->configs[$key] = $value;
    }
}

$manager = new ConfigManager();
$manager->setConfig('database_url', 'mysql:///mydb');

$dbConfig = $manager->getConfig('database_url')
    ->map(fn($url) => parse_url($url))
    ->isOkAnd(fn($parsed) => isset($parsed['scheme'], $parsed['host'], $parsed['path']));

if ($dbConfig) {
    echo "Valid database configuration found";
} else {
    echo "Invalid or missing database configuration";
}

$timeout = $manager->getConfig('timeout')
    ->expect("Timeout configuration is required");

echo "Timeout set to: $timeout";

第四个例子

<?php
use Brimmar\PhpResult\Ok;
use Brimmar\PhpResult\Err;
use Brimmar\PhpResult\Interfaces\Result;
use Brimmar\PhpOption\Some;
use Brimmar\PhpOption\None;
use Brimmar\PhpOption\Interfaces\Option;

class UserService
{
    private $users = [];

    public function findUser(int $id): Option
    {
        return isset($this->users[$id]) ? new Some($this->users[$id]) : new None();
    }

    public function updateUser(int $id, array $data): Result
    {
        return $this->findUser($id)
            ->ok()
            ->mapErr(fn() => "User not found")
            ->andThen(fn($user) => $this->validateUserData($data))
            ->map(fn($validData) => array_merge($this->users[$id], $validData))
            ->inspect(fn($updatedUser) => $this->users[$id] = $updatedUser);
    }

    private function validateUserData(array $data): Result
    {
        $errors = array_filter([
            'name' => strlen($data['name'] ?? '') < 2 ? 'Name too short' : null,
            'email' => filter_var($data['email'] ?? '', FILTER_VALIDATE_EMAIL) ? null : 'Invalid email',
        ]);

        return empty($errors) ? new Ok($data) : new Err($errors);
    }

    public function getUserStats(): array
    {
        return array_map(
            fn($user) => $this->calculateUserScore($user)->unwrapOr(0),
            $this->users
        );
    }

    private function calculateUserScore(array $user): Option
    {
        return isset($user['activities'])
            ? new Some(array_sum($user['activities']))
            : new None();
    }
}

$service = new UserService();

// Simulate adding a user
$service->users[1] = ['name' => 'Alice', 'email' => 'alice@example.com'];

$updateResult = $service->updateUser(1, ['name' => 'Alicia'])
    ->transpose();

$name = $updateResult
    ->iter()
    ->current()['name'] ?? 'Unknown';

echo "Updated name: $name";

$stats = $service->getUserStats();
echo "User stats: " . implode(', ', $stats);

方法

isOk(): bool

如果结果是Ok,则返回true

示例

$result = new Ok(42);
echo $result->isOk(); // Output: true

$result = new Err("error");
echo $result->isOk(); // Output: false

isOkAnd(callable $fn): bool

如果结果是Ok并且其内部的值匹配谓词,则返回true

示例

$result = new Ok(42);
echo $result->isOkAnd(fn($value) => $value > 40); // Output: true
echo $result->isOkAnd(fn($value) => $value < 40); // Output: false

$result = new Err("error");
echo $result->isOkAnd(fn($value) => true); // Output: false

isErr(): bool

如果结果是Err,则返回true

示例

$result = new Ok(42);
echo $result->isErr(); // Output: false

$result = new Err("error");
echo $result->isErr(); // Output: true

isErrAnd(callable $fn): bool

如果结果是Err并且其内部的值匹配谓词,则返回true

示例

$result = new Err("error");
echo $result->isErrAnd(fn($error) => $error === "error"); // Output: true
echo $result->isErrAnd(fn($error) => $error === "other"); // Output: false

$result = new Ok(42);
echo $result->isErrAnd(fn($error) => true); // Output: false

ok(?string $className): mixed

Result<T, E>转换为Option<T>

示例

$result = new Ok(42);
$option = $result->ok('\Brimmar\PhpOption\Some');
echo $option->unwrap(); // Output: 42

$result = new Err("error");
$option = $result->ok('\Brimmar\PhpOption\None');
echo $option->isNone(); // Output: true

err(?string $className): mixed

Result<T, E>转换为Option<E>

示例

$result = new Ok(42);
$option = $result->err('\Brimmar\PhpOption\None');
echo $option->isNone(); // Output: true

$result = new Err("error");
$option = $result->err('\Brimmar\PhpOption\Some');
echo $option->unwrap(); // Output: "error"

unwrap(): mixed

返回包含的Ok值。如果值是Err,则抛出异常。

示例

$result = new Ok(42);
echo $result->unwrap(); // Output: 42

$result = new Err("error");
$result->unwrap(); // Throws RuntimeException

expect(string $msg): mixed

返回包含的Ok值。如果值是Err,则抛出包含提供信息的异常。

示例

$result = new Ok(42);
echo $result->expect("Failed to get value"); // Output: 42

$result = new Err("error");
$result->expect("Failed to get value"); // Throws RuntimeException with message "Failed to get value: error"

expectErr(string $msg): mixed

返回包含的Err值。如果值是Ok,则抛出包含提供信息的异常。

示例

$result = new Err("error");
echo $result->expectErr("Failed to get error"); // Output: "error"

$result = new Ok(42);
$result->expectErr("Failed to get error"); // Throws RuntimeException with message "Failed to get error: 42"

flatten(): Result

Result<Result<T, E>, E>转换为Result<T, E>

示例

$result = new Ok(new Ok(42));
$flattened = $result->flatten();
echo $flattened->unwrap(); // Output: 42

$result = new Ok(new Err("inner error"));
$flattened = $result->flatten();
echo $flattened->unwrapErr(); // Output: "inner error"

$result = new Err("outer error");
$flattened = $result->flatten();
echo $flattened->unwrapErr(); // Output: "outer error"

intoErr(): mixed

返回包含的Err值。如果值是Ok,则抛出异常。

示例

$result = new Err("error");
echo $result->intoErr(); // Output: "error"

$result = new Ok(42);
$result->intoErr(); // Throws RuntimeException

intoOk(): mixed

返回包含的Ok值。如果值是Err,则抛出异常。

示例

$result = new Ok(42);
echo $result->intoOk(); // Output: 42

$result = new Err("error");
$result->intoOk(); // Throws RuntimeException

iter(): Iterator

返回一个可能包含值的迭代器。

示例

$result = new Ok(42);
foreach ($result->iter() as $value) {
    echo $value; // Output: 42
}

$result = new Err("error");
foreach ($result->iter() as $value) {
    echo "This won't be executed";
}

unwrapOr(mixed $default): mixed

返回包含的Ok值或提供的默认值。

示例

$result = new Ok(42);
echo $result->unwrapOr(0); // Output: 42

$result = new Err("error");
echo $result->unwrapOr(0); // Output: 0

unwrapOrElse(callable $fn): mixed

返回包含的Ok值或从闭包中计算它。

示例

$result = new Ok(42);
echo $result->unwrapOrElse(fn() => 0); // Output: 42

$result = new Err("error");
echo $result->unwrapOrElse(fn($error) => strlen($error)); // Output: 5

map(callable $fn): Result

通过将函数应用于包含的Ok值,将Result<T, E>映射到Result<U, E>,保留Err值不变。

示例

$result = new Ok(42);
$mapped = $result->map(fn($value) => $value * 2);
echo $mapped->unwrap(); // Output: 84

$result = new Err("error");
$mapped = $result->map(fn($value) => $value * 2);
echo $mapped->unwrapErr(); // Output: "error"

mapErr(callable $fn): Result

通过将函数应用于包含的Err值,将Result<T, E>映射到Result<T, F>,保留Ok值不变。

示例

$result = new Err("error");
$mapped = $result->mapErr(fn($error) => strtoupper($error));
echo $mapped->unwrapErr(); // Output: "ERROR"

$result = new Ok(42);
$mapped = $result->mapErr(fn($error) => strtoupper($error));
echo $mapped->unwrap(); // Output: 42

mapOr(mixed $default, callable $fn): mixed

如果结果是Err,则返回提供的默认值,如果结果是Ok,则应用函数到包含的值。

示例

$result = new Ok(42);
echo $result->mapOr(0, fn($value) => $value * 2); // Output: 84

$result = new Err("error");
echo $result->mapOr(0, fn($value) => $value * 2); // Output: 0

mapOrElse(callable $default, callable $fn): mixed

通过将回退函数default应用于包含的Err值,或函数fn应用于包含的Ok值,将Result<T, E>映射到U

示例

$result = new Ok(42);
echo $result->mapOrElse(
    fn($error) => strlen($error),
    fn($value) => $value * 2
); // Output: 84

$result = new Err("error");
echo $result->mapOrElse(
    fn($error) => strlen($error),
    fn($value) => $value * 2
); // Output: 5

inspect(callable $fn): self

使用包含的值(如果Ok)的引用调用提供的闭包。

示例

$result = new Ok(42);
$result->inspect(function($value) {
    echo "Got value: $value";
}); // Output: Got value: 42

$result = new Err("error");
$result->inspect(function($value) {
    echo "This won't be executed";
});

inspectErr(callable $fn): self

使用包含的错误(如果Err)的引用调用提供的闭包。

示例

$result = new Err("error");
$result->inspectErr(function($error) {
    echo "Got error: $error";
}); // Output: Got error: error

$result = new Ok(42);
$result->inspectErr(function($error) {
    echo "This won't be executed";
});

and(Result $res): Result

如果结果是Ok,则返回res,否则返回selfErr值。

示例

$result1 = new Ok(42);
$result2 = new Ok(10);
$combined = $result1->and($result2);
echo $combined->unwrap(); // Output: 10

$result1 = new Err("error");
$result2 = new Ok(10);
$combined = $result1->and($result2);
echo $combined->unwrapErr(); // Output: "error"

andThen(callable $fn): Result

如果结果是 Ok,则调用 fn,否则返回 selfErr 值。

示例

$result = new Ok(42);
$chained = $result->andThen(fn($value) => new Ok($value * 2));
echo $chained->unwrap(); // Output: 84

$result = new Err("error");
$chained = $result->andThen(fn($value) => new Ok($value * 2));
echo $chained->unwrapErr(); // Output: "error"

or(Result $res): Result

如果是 Ok,则返回 self,否则返回 res

示例

$result1 = new Ok(42);
$result2 = new Ok(10);
$combined = $result1->or($result2);
echo $combined->unwrap(); // Output: 42

$result1 = new Err("error1");
$result2 = new Ok(10);
$combined = $result1->or($result2);
echo $combined->unwrap(); // Output: 10

orElse(callable $fn): Result

如果结果是 Err,则调用 fn,否则返回 selfOk 值。

示例

$result = new Ok(42);
$chained = $result->orElse(fn($error) => new Ok($error . " handled"));
echo $chained->unwrap(); // Output: 42

$result = new Err("error");
$chained = $result->orElse(fn($error) => new Ok($error . " handled"));
echo $chained->unwrap(); // Output: "error handled"

transpose(?string $noneClassName, ?string $someClassName): mixed

OptionResult 逆序转换为 ResultOption

示例

$result = new Ok(new Some(42));
$transposed = $result->transpose('\Brimmar\PhpOption\None', '\Brimmar\PhpOption\Some');
echo $transposed->unwrap()->unwrap(); // Output: 42

$result = new Ok(new None());
$transposed = $result->transpose('\Brimmar\PhpOption\None', '\Brimmar\PhpOption\Some');
echo $transposed->isNone(); // Output: true

$result = new Err("error");
$transposed = $result->transpose('\Brimmar\PhpOption\None', '\Brimmar\PhpOption\Some');
echo $transposed->unwrap()->unwrapErr(); // Output: "error"

match(callable $Ok, callable $Err): mixed

根据提供的模式匹配结果并返回值。

示例

$result = new Ok(42);
$value = $result->match(
    Ok: fn($value) => "Success: $value",
    Err: fn($error) => "Error: $error"
);
echo $value; // Output: "Success: 42"

$result = new Err("error");
$value = $result->match(
    Ok: fn($value) => "Success: $value",
    Err: fn($error) => "Error: $error"
);
echo $value; // Output: "Error: error"

互补包

此包与实现 Option 类型的 PHP Option 类型包配合良好。此包中的一些方法,如 transpose,依赖于 Option 类型实现。

PhpOption

静态分析

我们建议使用 PHPStan 进行静态代码分析。此包包含自定义的 PHPStan 规则,以增强 Result 类型检查。要启用这些规则,请将以下内容添加到您的 PHPStan 配置中

composer require brimmar/phpstan-rustlike-result-extension --dev
// phpstan.neon
includes:
    - vendor/brimmar/phpstan-rustlike-result-extension/extension.neon

贡献

有关详细信息,请参阅 CONTRIBUTING.md。

安全漏洞

请查看我们的安全策略了解如何报告安全漏洞。

许可

有关更多信息,请参阅 LICENSE.md。