PHP中的动态属性是怎么回事?

537 阅读7分钟

类上的动态属性在PHP中已经存在了相当长的一段时间。在同样长的时间里,它们一直是一种不明智的做法。

当你有一个类时,你可以给不存在的属性赋值,系统会自动地创建属性,这样你的赋值就不会失败。

比如说:

<?php

class SampleClass {
    public function doTheThing(): void
    {
        $this->theThing = 'done';
    }

    public function isTheThingDone(): bool
    {
        return (bool)$this->theThing
            ?? false;
    }
}

这段代码在PHP 8、PHP 7、PHP 5中都能正常工作......但我甚至读它都觉得很痛苦。(对不起,戏剧性的提醒。)该类缺少一个声明的属性,叫做theThing 。PHP在后台创建了它。

你可能会说这很有帮助,或者说它允许你做很多整洁的事情。而且你也不完全是错的。

然而,它也是一个障碍。它导致了维护问题。如果你以后为了一个不同的目的而添加一个叫做theThing的属性,会发生什么?突然间,你有两个完全不同的代码路径,为不同的目的使用同一个属性。

如果你想直接引用theThing ,但你忘记了它的确切名称,而使用类似swampThing 的东西,会发生什么?代码似乎可以正常工作,但你的打字错误可能会被忽视,直到提交一份错误报告。

所以这些原因以及更多的原因导致了RFC 废弃了 PHP 中的动态属性,并最终在语言层面上将其删除。

不幸的是,有些项目--应用程序或库--可能会依赖这种功能来实现其价值。这些项目可能需要大量的努力来找到缓解该功能被废弃和删除的方法。而在这个晚期资本主义的时代,再加上令人疲惫的大流行病和拐角处的气候灾难的威胁,人们没有时间做这种事情。我们已经集体耗尽了勺子。

有一个全球性的勺子短缺

消除这种能力,对语言来说是好事。这种隐含的魔力使得预测使用这种模式编写的代码的事情具有挑战性。

另一方面,人类的花费,勺子的负担,可能是重新评估废止和移除计划的一个有效论据。也许还有其他的策略,可以将语言推向更纯粹、更优化的实现,同时仍然允许遗留项目访问这个功能?

不管怎么说,项目的维护者需要一些方法来快速发现他们是否可能受到这个问题的影响。

这就是静态分析器的作用。他们有能力扫描整个代码库,找出各种令人窒息的罪过,包括动态属性。

让我们启动一个包含上述类的例子项目,看看哪些静态分析工具可以帮助指出问题。

我使用composer init 来创建一个名为whateverthing/sample-dynamic-properties 的项目。我把上面的SampleClass代码放在了src/ 文件夹中,更新了它的命名空间,使之与Composer帮助定义的自动加载器相匹配。

然后我创建了一个脚本来练习上述代码,叫做go.php 。它很短,也很朴实。

<?php

require __DIR__ . '/vendor/autoload.php';

$obj = new Whateverthing\SampleDynamicProperties\SampleClass;

echo "Has the thing been done? "
    . ($obj->isTheThingDone() ? 'yes' : 'no')
    . "\n";
echo "Doing the thing ..." . "\n";
$obj->doTheThing() . "\n";
echo "Has the thing been done? "
    . ($obj->isTheThingDone() ? 'yes' : 'no')
    . "\n";

现在,当我在 PHP 8.0 下运行它时,我看到了以下输出。


# php go.php
PHP Warning:  Undefined property: Whateverthing\SampleDynamicProperties\SampleClass::$theThing in /home/sample/projects/whateverthing-sample-dynamic-properties/src/SampleClass.php on line 11
PHP Stack trace:
PHP   1. {main}() /home/sample/projects/whateverthing-sample-dynamic-properties/go.php:0
PHP   2. Whateverthing\SampleDynamicProperties\SampleClass->isTheThingDone() /home/sample/projects/whateverthing-sample-dynamic-properties/go.php:7

Warning: Undefined property: Whateverthing\SampleDynamicProperties\SampleClass::$theThing in /home/sample/projects/whateverthing-sample-dynamic-properties/src/SampleClass.php on line 11

Call Stack:
    0.0002     395400   1. {main}() /home/sample/projects/whateverthing-sample-dynamic-properties/go.php:0
    0.0012     477400   2. Whateverthing\SampleDynamicProperties\SampleClass->isTheThingDone() /home/sample/projects/whateverthing-sample-dynamic-properties/go.php:7

Has the thing been done? no
Doing the thing ...
Has the thing been done? yes

哇,这都是什么?好多的警告文字啊。看起来直到现在还在使用动态属性的人可能忽略了一些相当重要的信息。要么就是我误解了动态属性的常见使用模式。谁知道呢?但是我能够确认这个警告至少可以追溯到PHP7.2。你正在审计你的代码以监控和删除警告,对吗?(我敢肯定,大多数公司和项目都尽量减少警告,但他们很可能没有强迫症)。

总之。回到手头的任务上。我打算从检查PHPStan检测这个问题的能力开始,所以我用composer require --dev phpstan/phpstan 来安装它。

果然,运行vendor/bin/phpstan analyze src/SampleClass.php 返回一些有用的输出。

1/1 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%

 ------ -------
  Line   SampleClass.php
 ------ -------
  7      Access to an undefined property Whateverthing\SampleDynamicProperties\SampleClass::$theThing.
  11     Access to an undefined property Whateverthing\SampleDynamicProperties\SampleClass::$theThing.

 [ERROR] Found 2 errors

很好,它找到了我们想要找到的东西。

现在让我们试试Phan,另一个静态分析的工具。这个工具需要更多的配置,并且需要一个额外的标志来使用AST polyfill来扫描抽象语法树,因为我没有安装php-ast扩展。

# vendor/bin/phan --init
# vendor/bin/phan --allow-polyfill-parser

[info] Disabling Xdebug: Phan is around five times as slow when Xdebug is enabled (Xdebug only makes sense when debugging Phan itself)
[info] To run Phan with Xdebug, set the environment variable PHAN_ALLOW_XDEBUG to 1.
[info] To disable this warning, set the environment variable PHAN_DISABLE_XDEBUG_WARN to 1.
[info] To include function signatures of Xdebug, see .phan/internal_stubs/xdebug.phan_php
[debug] Checking PHAN_ALLOW_XDEBUG
[debug] Because xdebug was installed, Phan will restart.
[debug] The Xdebug extension is loaded (3.0.4) mode=develop
[debug] Process restarting (PHAN_ALLOW_XDEBUG=internal|3.0.4|1|*|*)
[debug] Running /opt/local/bin/php80 -n -c /private/var/folders/jv/j8t0hwbd7_qbl89wjkymf7380000gn/T/Kpot39 vendor/bin/phan --allow-polyfill-parser
   analyze ███████████████████ 100.0% 359MB/363MB
src/SampleClass.php:7 PhanUndeclaredProperty Reference to undeclared property \Whateverthing\SampleDynamicProperties\SampleClass->theThing
src/SampleClass.php:11 PhanUndeclaredProperty Reference to undeclared property \Whateverthing\SampleDynamicProperties\SampleClass->theThing
[debug] Restarted process exited 1

这个也找到了我们想要的东西。而且这两个都是用一个开箱即用的实现来做的。

看起来,维护者应该很容易发现他们的项目是否有这个问题。但要解决这个问题有多容易呢?

这取决于为什么要使用动态属性--以及使用的广泛程度。动态属性RFC有几个建议--最简单的是声明属性,第二简单的是声明一个映射属性并覆盖__get/__set魔法方法来与之交互。还有一种技巧性的方法,就是声明属性,在构造函数中取消设置,然后在请求时在__get中设置它。

我对这个RFC的个人看法是:我喜欢这个想法,但对PHP开发周期速度的抱怨越来越多,这令人担忧。一个语言的成长和成熟是好的,不要误会我的意思,但是变化太快容易让人对自己的知识和对语言的投资产生怀疑。

如果目标是使这门语言更加平易近人,那么这似乎在新人可能绊倒他们脚趾的地方清单上是很低的。

我建议要么推迟,要么完全寻找一种不同的方法。也许一个INI设置与一个扩展或一个恢复原始功能的用户界面包相结合。RFC谈到了一个可以启用旧功能的属性;也许INI设置可以使其应用于所有对象(尽管有性能上的代价)?

如果不是这样,按原样推进这个计划也是可以的。它只是可能会带来一些反弹和意料之外的后果......有点像使用动态属性本身。

更新(2021-11-15)。

Benjamin Eberlei指出了静态分析方法的一个缺陷,那就是当变量-变量被用来更新动态属性时,例如,从数据库结果中填充数据对象。

静态分析器无法检测到一个重要的设置动态属性的情况,比如代码从数据库中设置对象的列。object>object->rowName = $value。

静态分析法的论点作为反对废止动态属性的论据没有任何价值。

- Benjamin Eberlei (@beberlei)November 15, 2021

我能够100%确认这一点。我创建了一个DbSampleClass.php。

<?php

namespace Whateverthing\SampleDynamicProperties;

class DbSampleClass {
    public function __construct(array $data)
    {
        foreach ($data as $key => $value) {
            $this->$key = $value;
        }
    }
}

它使用key作为变量-variable来设置动态属性。从技术上讲,由于它没有使用$$variable的定义,它可能不是一个真正的变量-变量-但基本操作非常相似。也许它可以被认为是一个属性变量?

不管怎样--接下来,我创建了db-go.php来加载这个类。

<?php

require __DIR__ . '/vendor/autoload.php';

$obj = new Whateverthing\SampleDynamicProperties\DbSampleClass(
    [
        'Row1' => 'value1',
        'Row2' => 'value2',
    ]
);

echo "What is Row1? " . $obj->Row1 . "\n";
echo "What is Row2? " . $obj->Row2 . "\n";

我运行了php db-go.php ,得到了这个输出。

# php db-go.php
What is Row1? value1
What is Row2? value2

没有警告。

然后我针对该文件运行了phpstanphan

两者都给出了100%的合格成绩。没有发现任何问题。

所以,这很令人担忧,并表明这个问题甚至没有一些人所建议的那么简单明了。