发现核心 OOP 编码差异
在 PHP 8 中,编写 OOP 代码的方式发生了许多重大变化。 在本节中,我们将重点讨论可能导致向后兼容性中断的三个关键领域。本节将讨论与静态方法调用、对象属性处理和 PHP 自动加载相关的常见错误做法。
阅读本节并通过示例学习后,您就能更好地发现 OOP 的不良实践,并了解 PHP 8 是如何对此类使用进行限制的。在本章中,你将学习到良好的编码实践,这最终将使你成为一名更好的程序员。您还将能够处理 PHP 自动加载中的变化,这些变化可能会导致迁移到 PHP 8 的应用程序出现故障。
首先让我们看看 PHP 8 是如何加强静态调用的。
在 PHP 8 中处理静态调用
令人惊讶的是,PHP 7 及以下版本允许开发人员对未声明为静态的类方法进行静态调用。乍一看,任何未来的开发人员在审查您的代码时,都会立即假定该方法已被定义为静态方法。这可能会导致意想不到的行为,因为未来的开发人员会在错误的假设下开始误用你的代码。
在这个简单的例子中,我们定义了一个带有 nonStatic()
方法的 Test
类。在类定义后的过程代码中,我们回调了该方法的返回值,但这样做时,我们进行了静态调用:
// /repo/ch05/php8_oop_diff_static.php
class Test {
public function notStatic() {
return __CLASS__ . PHP_EOL;
}
}
echo Test::notStatic();
在 PHP 7 中运行这段代码后,结果如下:
root@php8_tips_php7 [ /repo/ch05 ]# php php8_oop_diff_static.php
PHP Deprecated: Non-static method Test::notStatic() should not
be called statically in /repo/ch05/php8_oop_diff_static.php on
line 11
Test
从输出中可以看到,PHP 7 发出了弃用通知,但允许调用!而在 PHP 8 中,结果是致命错误,如图所示:
root@php8_tips_php8 [ /repo/ch05 ]#
php php8_oop_diff_static.php
PHP Fatal error: Uncaught Error: Non-static method
Test::notStatic() cannot be called statically in /repo/ch05/
php8_oop_diff_static.php:11
使用静态方法调用语法调用非静态方法是一种糟糕的做法,因为写得好的代码会让代码开发者的意图一目了然。如果您没有将一个方法定义为静态方法,但后来却在静态意义上调用了它,那么将来被指派维护您的代码的开发人员可能会感到困惑,并对代码的原始意图做出错误的假设。最终的结果将是更多糟糕的代码!
在 PHP 8 中,不能再使用静态方法调用来调用非静态方法了。现在让我们看看另一种把对象属性当作键的糟糕做法。
处理对象属性处理更改
早在 PHP 最早的版本中,数组就已经是一个核心功能了。另一方面,OOP 直到 PHP 4 才被引入。 在 OOP 的早期,数组函数经常被扩展以容纳对象属性。这导致对象和数组之间的区别变得模糊,进而产生了许多不好的做法。
为了保持数组处理和对象处理之间的明确区分,PHP 8 现在限制 array_key_exists()
函数只接受数组作为参数。为了说明这一点,请看下面的示例:
-
首先,我们定义一个简单的匿名类,它只有一个属性:
// /repo/ch05/php8_oop_diff_array_key_exists.php $obj = new class () { public $var = 'OK.'; };
-
然后,我们使用
isset()
、property_exists()
和array_key_exists()
,分别运行三个测试来检查$var
是否存在:// not all code is shown $default = 'DEFAULT'; echo (isset($obj->var)) ? $obj->var : $default; echo (property_exists($obj,'var')) ? $obj->var : $default; echo (array_key_exists('var',$obj)) ? $obj->var : $default;
在 PHP 7 中运行这段代码时,所有测试都成功了,如图所示:
root@php8_tips_php7 [ /repo/ch05 ]#
php php8_oop_diff_array_key_exists.php
OK.OK.OK.
但在 PHP 8 中,由于 array_key_exists()
现在只接受数组作为参数,因此出现了致命的 TypeError
。下面是 PHP 8 的输出结果:
root@php8_tips_php8 [ /repo/ch05 ]#
php php8_oop_diff_array_key_exists.php
OK.OK.PHP Fatal error: Uncaught TypeError: array_key_exists():
Argument #2 ($array) must be of type array, class@anonymous
given in /repo/ch05/php8_oop_diff_array_key_exists.php:10
最佳做法是使用 property_exists()
或 isset()
。现在我们来看看 PHP 自动加载的变化。
使用 PHP 8 自动加载
在 PHP 5.1 中首次引入的基本自动加载类机制在 PHP 8 中同样有效。主要区别在于,PHP 7.2 中废弃的全局函数 __autoload()
在 PHP 8 中被完全删除。从 PHP 7.2 开始,我们鼓励开发者使用 spl_autoload_register()
(从 PHP 5.1 开始就可以使用)注册他们的自动加载逻辑。另一个主要区别是,如果无法注册自动加载程序,spl_autoload_register()
会如何处理。
了解使用 spl_autoload_register()
时自动加载过程的工作原理对你的开发工作至关重要。如果不能掌握 PHP 如何自动定位和加载类,就会限制你作为开发人员的发展,并可能对你的职业道路产生不利影响。
在了解 spl_autoload_register()
之前,让我们先来看看 __autoload()
函数。
了解 __autoload() 函数
许多开发人员将 __autoload()
函数用作自动加载逻辑的主要来源。这个函数的行为就像魔法方法一样,所以它会根据上下文自动调用。自动调用 __autoload()
函数的情况包括创建一个新的类实例,但类的定义尚未加载。此外,如果类扩展了另一个类,自动加载逻辑也会被调用,以便在创建扩展它的子类之前加载超类。
使用 __autoload()
函数的优点是很容易定义,通常定义在网站的初始 index.php
文件中。缺点如下:
-
__autoload()
是一个 PHP 过程函数,没有使用 OOP 原则定义或控制。例如,在为应用程序定义单元测试时,这可能会成为一个问题。 -
如果应用程序使用命名空间,
__autoload()
函数必须定义在全局命名空间中;否则,定义了__autoload()
函数的命名空间之外的类将无法加载。 -
__autoload()
函数与spl_autoload_register()
不能很好地配合使用。如果同时使用__autoload()
函数和spl_autoload_register()
定义自动加载逻辑,则__autoload()
函数的逻辑将被完全忽略。
为了说明潜在的问题,我们将定义一个 OopBreakScan
类,更详细的讨论将在 第 11 章 将现有的 PHP 应用程序移植到 PHP 8 中进行:
-
首先,我们在
OopBreakScan
类中定义并添加了一个方法,用于扫描文件内容以查找__autoload()
函数。请注意,错误信息是 Base 类中定义的一个类常量,它只是警告存在__autoload()
函数:namespace Migration; class OopBreakScan extends Base { public static function scanMagicAutoloadFunction( string $contents, array &$message) : bool { $found = 0; $found += (stripos($contents, 'function __autoload(') !== FALSE); $message[] = ($found) ? Base::ERR_MAGIC_AUTOLOAD : sprintf(Base::OK_PASSED, __FUNCTION__); return (bool) $found; } // remaining methods not shown
该类扩展了一个
Migration\Base
类(未显示)。这一点非常重要,因为任何自动加载逻辑不仅需要找到子类,还需要找到它的超类。 -
接下来,我们定义一个调用程序,其中定义了一个神奇的
__autoload()
函数:// /repo/ch05/php7_autoload_function.php function __autoLoad($class) { $fn = __DIR__ . '/../src/' . str_replace('\\', '/', $class) . '.php'; require_once $fn; }
-
然后,我们通过调用程序扫描自身来使用该类:
use Migration\OopBreakScan; $contents = file_get_contents(__FILE__); $message = []; OopBreakScan::scanMagicAutoloadFunction($contents, $message); var_dump($message);
下面是在 PHP 7 中运行的输出结果:
root@php8_tips_php7 [ /repo/ch05 ]#
php php7_autoload_function.php
/repo/ch05/php7_autoload_function.php:23:
array(1) {
[0] => string(96) "WARNING: the "__autoload()" function is removed in PHP 8: replace with "spl_autoload_register()""
}
从输出结果中可以看到,Migration\OopBreakScan
类已被自动加载。我们之所以知道这一点,是因为我们调用了 scanMagicAutoloadFunction
方法,并得到了它的结果。此外,我们还知道 Migration\Base
类也被自动加载了。我们之所以知道这一点,是因为输出中出现的错误信息是超类的常量。
然而,在 PHP 8 中运行同样的代码却产生了这样的结果:
root@php8_tips_php8 [ /repo/ch05 ]#
php php7_autoload_function.php
PHP Fatal error: __autoload() is no longer supported, use
spl_autoload_register() instead in /repo/ch05/php7_autoload_
function.php on line 4
在 PHP 8 中,必须使用 spl_autoload_register()
。现在我们来看看 spl_autoload_register()
。
学习使用 spl_autoload_register()
spl_autoload_register()
函数的主要优点是可以注册多个自动加载器。虽然这看起来有点矫枉过正,但设想一下噩梦般的场景:您正在使用多个不同的开源 PHP 库……而且它们都定义了自己的自动加载器!只要所有此类库都使用 spl_autoload_register()
,那么拥有多个自动加载器回调就不成问题。
使用 spl_autoload_register()
注册的每个自动加载器都必须是可调用的。以下任何一种情况都被视为可调用:
-
一个 PHP 程序函数
-
匿名函数
-
可以静态方式调用的类方法
-
任何定义了
__invoke()
魔术方法的类实例 -
这种形式的数组
[$instance,'method']
数组
Composer 维护自己的自动加载器,而自动加载器又依赖于 |
一个非常典型的自动加载器类可能如下所示。请注意,我们在本书中展示的许多 OOP 示例都使用了这个类:
-
在
__construct()
方法中,我们指定了源代码目录。然后,我们使用上述数组可调用语法调用spl_autoload_register()
:// /repo/src/Server/Autoload/Loader.php namespace Server\Autoload; class Loader { const DEFAULT_SRC = __DIR__ . '/../..'; public $src_dir = ''; public function __construct($src_dir = NULL) { $this->src_dir = $src_dir ?? realpath(self::DEFAULT_SRC); spl_autoload_register([$this, 'autoload']); }
-
实际的自动加载代码与上文
__autoload()
函数示例中的代码类似。下面是实际执行自动加载的方法:public function autoload($class) { $fn = str_replace('\\', '/', $class); $fn = $this->src_dir . '/' . $fn . '.php'; $fn = str_replace('//', '/', $fn); require_once($fn); } }
现在您已经知道了如何使用 spl_auto_register()
函数,我们必须检查运行 PHP 8 时可能出现的代码错误。
PHP 8 中可能存在的 spl_auto_register() 代码断点
spl_auto_register()
函数的第二个参数是一个可选的布尔值,默认值为 FALSE
。在 PHP 7 及以下版本中,如果第二个参数设置为 TRUE
,自动加载器注册失败时,spl_auto_register()
函数会抛出异常。但在 PHP 8 中,如果第二个参数的数据类型不是可调用的,无论第二个参数的值是多少,都会抛出一个致命的 TypeError
!
下一个简单的程序示例就说明了这种危险。在这个示例中,我们使用 spl_auto_register()
函数注册一个不存在的 PHP 函数。我们将第二个参数设置为 TRUE
:
// /repo/ch05/php7_spl_spl_autoload_register.php
try {
spl_autoload_register('does_not_exist', TRUE);
$data = ['A' => [1,2,3],'B' => [4,5,6],'C' => [7,8,9]];
$response = new \Application\Strategy\JsonResponse($data);
echo $response->render();
} catch (Exception $e) {
echo "A program error has occurred\n";
}
如果我们在 PHP 7 中运行该代码块,结果如下:
root@php8_tips_php7 [ /repo/ch05 ]#
php php7_spl_spl_autoload_register.php
A program error has occurred
从输出结果可以看出,异常被抛出。catch
块被调用,并显示出 "A program error has occurred"(发生程序错误)的信息。然而,当我们在 PHP 8 中运行同样的程序时,却抛出了一个致命的错误:
root@php8_tips_php8 [ /repo/ch05 ]#
php php7_spl_spl_autoload_register.php
PHP Fatal error: Uncaught TypeError: spl_autoload_register():
Argument #1 ($callback) must be a valid callback, no array or
string given in /repo/ch05/php7_spl_spl_autoload_register.
php:12
显然,catch
块被绕过了,因为它是用来捕获异常而不是错误的。简单的解决办法是让 catch
块捕捉 Throwable
而不是 Exception
。这样,同样的代码就可以在 PHP 7 或 PHP 8 中运行。
下面是重写后的代码。由于输出结果与在 PHP 7 中运行的示例完全相同,因此没有显示输出结果:
// /repo/ch05/php8_spl_spl_autoload_register.php
try {
spl_autoload_register('does_not_exist', TRUE);
$data = ['A' => [1,2,3],'B' => [4,5,6],'C' => [7,8,9]];
$response = new \Application\Strategy\JsonResponse($data);
echo $response->render();
} catch (Throwable $e) {
echo "A program error has occurred\n";
}
现在你对 PHP 8 自动加载有了更好的了解,也知道了如何发现和纠正潜在的自动加载向后兼容性问题。现在让我们来看看 PHP 8 中与魔法方法有关的变化。