catpaw / unsafe
使用 Unsafe
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.8
- phpunit/phpunit: ^9
README
安装方式
composer require catpaw/unsafe
控制流为王
我认为,控制流是作为程序员处理的最重要的事情之一,它影响我的思考,有时它甚至指导我的问题解决过程。
管理错误不应该打断我控制程序流程的方式,我不应该需要在我的文件中跳上跳下,以捕捉到20行代码以上新调用的函数引入的新异常。
Try/Catch
我发现自己过度依赖以下代码
try { // some code } catch(SomeException1 $e){ // manage error 1 } catch(SomeException2 $e) { // manage error 2 } catch(SomeException3 $e) { // manage error 3 }
或者甚至
try { // some code } catch(SomeException1|SomeException2|SomeException3 $e){ // manage all errors in one place }
理论上最后一个可能是有意义的,但在实践中,这些异常可能意味着不同的事情,不同的错误原因。
现实是,我经常把这些异常混为一谈,因为我忘记管理它们,或者因为在凌晨4点,我突然决定“是的,我应该让我的IDE决定我的错误管理”。
Try/catch 错误处理一直是(可能仍然是)PHP中管理错误最流行的方式,我认为它仍然是在全局范围内处理错误的有效方式。
我无法否认,有一个集中管理所有错误的地方是件好事,但我不想总是被迫以这种方式处理错误管理。
如果你像我一样,你可能会更喜欢在源代码中管理错误,直接在错误出现的地方处理,这样你就不用再想它了。
Unsafe
我有一个解决方案。
不要在你的代码中抛出异常,而是将错误作为 Unsafe 返回。
namespace CatPaw\Unsafe; /** * @template T */ readonly class Unsafe { /** @var T $value */ public $value; public false|Error $error; }
使用 ok() 和 error() 函数来创建 Unsafe 对象。
ok()
namespace CatPaw\Unsafe; /** * @template T * @param T $value * @return Unsafe<T> */ function ok($value);
当你的程序中没有错误时,返回 ok($value)。
这个函数将创建一个新的 Unsafe,包含有效的 $value 和没有错误。
error()
namespace CatPaw\Core; /** * @param string|Error $error * @return Unsafe<void> */ function error($error);
当你在程序中遇到错误并希望将其传播到上游时,返回 error($error)。
这个函数将创建一个新的 Unsafe,包含 null $value 和给定的 error。
示例
以下示例尝试在管理错误的情况下读取文件内容。
首先,我声明所有涉及的实体,类和函数。
<?php use CatPaw\Unsafe\Unsafe; use function CatPaw\Unsafe\anyError; use function CatPaw\Unsafe\error; use function CatPaw\Unsafe\ok; // This is not required, but you can return custom errors class FileNotFoundError extends Error { public function __construct(private string $fileName) { parent::__construct('', 0, null); } public function __toString() { return "I'm looking for $this->fileName, where's the file Lebowski????"; } } /** * Attempt to open a file. * @param string $fileName * @return Unsafe<resource> */ function openFile(string $fileName){ if(!file_exists($fileName)){ return error(new FileNotFoundError($fileName)); } if(!$file = fopen('file.txt', 'r+')){ return error("Something went wrong while trying to open file $fileName."); } return ok($file); } /** * Attempt to read 5 bytes from the file. * @param resource $stream * @return Unsafe<string> */ function readFile($stream){ $content = fread($stream, 5); if(false === $content){ return error("Couldn't read from stream."); } return ok($content); } /** * Attempt to close the file. * @param resource $stream * @return Unsafe<void> */ function closeFile($stream){ if(!fclose($stream)){ return error("Couldn't close file."); } return ok(); }
然后
- 打开一个文件
- 读取其内容
- 关闭文件
<?php // open file $file = openFile('file.txt')->try($error); if ($error) { echo $error.PHP_EOL; die(); } // read contents $contents = readFile($file)->try($error); if ($error) { echo $error.PHP_EOL; die(); } // close file closeFile($file)->try($error); if ($error) { echo $error.PHP_EOL; die(); } echo $contents.PHP_EOL;
如果所有操作都成功,此代码将打印 file.txt
的内容。
每次调用 ->try($error)
时,Unsafe 对象都会尝试展开其值。
如果 Unsafe 对象包含错误,->try($error)
返回的值解析为 null
,变量 $error
通过引用分配包含的错误。
anyError()
你可以使用 anyError() 来处理重复的代码片段
if($error){ echo $error.PHP_EOL; // manage error here... }
以下是使用 anyError() 编写的相同示例
<?php $contents = anyError(function() { // open file $file = openFile('file.txt')->try($error) or yield $error; // read contents $contents = readFile($file)->try($error) or yield $error; // close file closeFile($file)->try($error) or yield $error; return $contents; })->try($error); if($error){ echo $error.PHP_EOL; die(); } echo $contents.PHP_EOL;
anyError() 函数接收一个生成器函数,并逐个步骤消耗它。
当生成器函数 yield
一个 Error 或包含 Error 的 Unsafe 时,anyError 函数将立即停止执行生成器,并返回一个包含给定错误的新的 Unsafe。
实际上,or yield $error
的作用就像
if($error){ return error($error); }
另一方面,如果 ->try()
的结果有效,则不会执行 or <expression>
,生成器将继续运行,直到遇到下一个 yield error
语句、下一个 return
语句或生成器被消耗。
匹配
由于错误是结果,你实际上可以 match()
它们
$result = anyError(/* ... */)->try($error) or match($error:class){ FileNotFoundError::class => $error->getMessage(), default => "Let me explain something to you. Um, I am not Mr. Lebowski. You're Mr. Lebowski.", };
或者应用您想要的任何形式的内联表达式。