brimmar / phpresult
PHP的类似Rust的结果类型
Requires
- php: >=8.1
Requires (Dev)
- pestphp/pest: ^2.34
README
本文档涵盖了PHP Rust-like Result Type的实现。结果类型用于返回和传递错误。它有两个变体:Ok
,表示成功并包含一个值,以及Err
,表示错误并包含一个错误值。
目录
结果接口
Result
接口定义了Ok
和Err
类之间的契约。
<?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
,否则返回self
的Err
值。
示例
$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
,否则返回 self
的 Err
值。
示例
$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
,否则返回 self
的 Ok
值。
示例
$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
将 Option
的 Result
逆序转换为 Result
的 Option
。
示例
$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 类型实现。
静态分析
我们建议使用 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。