catpaw/unsafe

使用 Unsafe 管理错误

1.0.4 2024-02-18 21:19 UTC

This package is auto-updated.

Last update: 2024-09-18 22:46:15 UTC


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();
}

然后

  1. 打开一个文件
  2. 读取其内容
  3. 关闭文件
<?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 或包含 ErrorUnsafe 时,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.",

};

或者应用您想要的任何形式的内联表达式。