PHP 返回类型的最佳实践

我已经使用 PHP 很多年了,并且一遍又一遍地看到使用函数的返回值做同样的事情。我一直认为这是非常标准的,但我想得越多,它就越没有意义。回顾我的职业生涯,我确信如果我没有混合返回类型,一些严重的错误是可以避免的。

由于 PHP 是一种松散类型的语言,这使开发人员能够更改从函数返回的值的类型。这在 PHP 代码库本身中经常发生,因为如果发生错误,许多内置函数将返回 false。

用户态代码中的一种常见做法是,如果出现问题,则从函数中返回 false。这可能是因为 PHP 本身鼓励这样做。

这里的一个例子是从数据库中加载东西。发生的情况是,如果无法访问数据库,或者出现某种错误,则该函数返回 false。如果一切正常,则返回数据。下面的代码代表了这一点。

function loadFromDatabase($db)
{
  $result = mysqli_query($db, 'SELECT * FROM thing;');
 
  if ($result) {
    return false;
  }
 
  $data = [];
 
  while ($row = $result->fetch_assoc())
  {
    $data[] = $row;
  }
 
  return $data;
}

从表面上看,如果出现问题,返回 false 是有道理的。然而,我们在这里所做的是向上游传递问题和复杂性。要使用此函数,我们需要注意可能会返回不同类型的数据。因此,在开始处理数据之前,我们需要检测是否返回了 false,或者是否返回了空结果集。

$db = mysqli_connect($host, $user, $pass, $database);
$result = loadFromDatabase($db);
 
if ($result === false) {
  // 遇到错误。
}
 
if (count($result) == 0) {
  // 未找到结果。
}
 
// 处理数据。

这可能看起来很简单,但我无法告诉你我遇到过多少次由于没有正确检测返回类型而导致的代码错误。这可以是简单的事情,例如尝试循环错误值,但可以以更微妙的方式表现出来。

例如,假设我们想要处理一个日期,如果该日期无效,那么我们返回 false。问题是,如果我们没有碰巧捕捉到无效日期,那么我们不知道该函数是否由于无效日期或strtotime()产生错误响应而失败 。

此外,如果我们没有正确检测到错误响应,那么我们可以轻松地尝试为时间戳创建一个带有 0(即“false”转换为整数)值的日期。我相信您已经在互联网上多次看到 1970 年 1 月 1 日的日期,它通常是由此类问题造成的。

这种情况在上面的数据库查询代码中也是一样的。手头有一个false,我们只能猜测数据库层发生了一些不好的事情,但不能准确地猜测发生了什么。

这个问题可以通过几种方式解决。

抛出异常

与返回 false 不同,更好的方法是抛出异常。这样,您可以以更精细(和可预测)的方式处理结果,而不是不确定函数返回 false 的原因。

loadFromDatabase()再次使用该函数,我们可以通过定义一个异常类然后在发生错误时抛出该异常来调整它。

class DatabaseErrorException extends Exception {}
 
function loadFromDatabase($db)
{
  $result = mysqli_query($db, 'SELECT * FROM thing;');
 
  if ($result === false) {
    throw new DatabaseErrorException();
  }
 
  $data = [];
 
  while ($row = $result->fetch_assoc())
  {
    $data[] = $row;
  }
 
  return $data;
}

然后可以重写使用它的代码 cat catch 这个异常。

$db = mysqli_connect($host, $user, $pass, $database);
 
try {
  $result = loadFromDatabase($db);
  // 处理数据。
} catch (DatabaseErrorException $e) {
  // 遇到错误。
}

这个单一的变化意味着如果数据库抛出错误,那么我们可以绝对确定这已经发生了。不仅如此,我们还可以防止代码在数据不存在时尝试处理数据。当loadFromDatabase()函数中抛出异常时,try 块中的其余代码都不会运行,因此我们可以确保我们不会处于半处理状态。显然,一个异常不会告诉我们发生了什么细节,但这只是一个简单的例子。

抛出异常是一个好主意,但像许多事情一样,它可能会被过度使用。您很容易发现自己处于应用程序崩溃的情况,因为您抛出了一个异常并且没有正确捕获它。出于这个原因,您应该始终确保您的代码捕获可能抛出的任何和所有异常。它确实为您的应用程序正在做什么提供了很好的反馈,并且可用于在不使用随机类型的情况下跳出函数。

设置返回类型

除了抛出异常,我们还可以设置返回类型。PHP 7 添加了一项语言功能,允许开发人员规定从函数返回的值的类型。使用此功能意味着我们可以通过在语言级别强制执行此功能来强制函数返回整数。

支持以下类型。

  • 整数

  • 漂浮

  • 布尔值

  • 细绳

  • 接口

  • 大批

  • 可调用的

  • 对象(自 PHP 7.2 起)

要实现此功能,您需要在代码中添加两件事。

在 PHP 文件的顶部,您需要包含一个声明,该声明告诉 PHP 使用强制返回类型来解释文件。如果没有这个,代码不会抛出语法错误,但不会强制返回类型。此功能允许您通过简单的更改打开和关闭返回类型检测。

declare(strict_types=1);

请务必注意,此声明仅影响声明文件中的代码。这是为了防止外部库意外受到此更改的影响。

有了它,您现在可以按以下方式定义函数。

function returnOne(): int {
  return 1;
}

这个函数定义声明这个函数必须返回一个整数。如果没有,那么我们会得到这样的错误。

PHP Fatal error:  Uncaught TypeError: Return value of returnOne() must be of the type integer, string returned.

PHP 在此处抛出的错误可以作为 TypeError 异常捕获。这允许我们潜在地适应返回错误类型的函数。

function returnWrongType(): int {
  return false;
}
 
try {
  returnWrongType();
} catch (TypeError $e) {
  echo 'Wrong type found.';
}

从 PHP 7.1 开始,您可以在返回类型前使用问号指定可为空的返回类型。这意味着您可以返回整数或空值。请注意,不返回任何内容在技术上也意味着返回空值。

function mightReturnInt() : ?int {
  return null;
}

使用返回类型和异常,我们可以生成具有非常可预测的实现的函数,这些实现更容易与之交互。不仅如此,测试可预测的功能也变得更加容易。