作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
伊利亚·萨诺西安的头像

By Ilya Sanosian

伊利亚是一名IT顾问, 网络架构师, 拥有12年以上团队建设和领导经验的经理.

专业知识

以前在

赫芬顿邮报
分享

PHP使得构建 基于web的系统这是它受欢迎的主要原因. 尽管它很容易使用, PHP已经发展成为一种相当复杂的语言 有很多框架, 细微差别, 而这些微妙之处可能会让开发人员头疼, 导致数小时的紧张调试. 这篇文章强调了10个更常见的错误 PHP开发人员 需要注意.

常见错误#1:在后面留下悬空数组引用 为each 循环

不知道如何在PHP中使用为each循环? 使用参考资料 为each 如果您想对正在迭代的数组中的每个元素进行操作,循环可能会很有用. 例如:

$arr = 数组(1,2,3,4);
Foreach ($arr)为 &美元值){
    美元的价值 = 美元的价值 * 2;
}
// $arr现在是数组(2,4,6,8)

问题是, 如果你不小心, 这也会产生一些不良的副作用和后果. 具体来说,在上面的例子中,代码执行后, 美元的价值 将保留在作用域中,并保存对数组中最后一个元素的引用. 后续操作涉及 美元的价值 因此可能会无意中修改数组中的最后一个元素.

最重要的是要记住 为each 不创建作用域. 因此, 美元的价值 在上面的例子中是a 参考 在脚本的顶部范围内. 在每次迭代中 为each 的下一个元素的引用 美元的数组. 因此,在循环完成后, 美元的价值 仍然指向的最后一个元素 美元的数组 仍然在范围内.

以下是这种做法可能导致的回避性和令人困惑的bug的例子:

美元的数组 = [1,2,3];
Echo implode(',', 美元的数组), "\n";

Foreach (美元的数组 &美元的价值){} //通过引用
Echo implode(',', 美元的数组), "\n";

Foreach (美元的数组 as 美元的价值){} //按值(i.e.,复制)
Echo implode(',', 美元的数组), "\n";

以上代码将输出如下内容:

1,2,3
1,2,3
1,2,2

不,这不是打字错误. 最后一行的最后一个值确实是2,而不是3.

为什么?

在经历了第一个之后 为each 循环, 美元的数组 保持不变,但如上文所述, 美元的价值 的最后一个元素的悬空引用 美元的数组 (因为这 为each 循环访问 美元的价值 by 参考).

因此,当我们进行第二步时 为each 循环,“奇怪的事情”似乎发生了. 具体地说,自 美元的价值 现在被访问的值(i.e., by 复制), 为each 副本 每一个顺序 美元的数组 元素进 美元的价值 在循环的每一步. 因此,这是第二步中每一步发生的事情 为each 循环:

  • 通过1: 副本 数组美元[0] (i.e.,“1”) 美元的价值 (这是一个参考 数组美元[2]), so 数组美元[2] 现在等于1. So 美元的数组 现在包含[1,2,1].
  • 通过2: 副本 数组美元[1] (i.e.,“2”) 美元的价值 (这是一个参考 数组美元[2]), so 数组美元[2] 现在等于2. So 美元的数组 现在包含[1,2,2].
  • 通过3: 副本 数组美元[2] (现在等于“2”)变成 美元的价值 (这是一个参考 数组美元[2]), so 数组美元[2] 仍然等于2. So 美元的数组 现在包含[1,2,2].

中使用引用的好处 为each 循环而不冒这类问题的风险,调用 设置() 在变量上,紧接在 为each 循环, to remove the 参考; e.g.:

$arr = 数组(1,2,3,4);
Foreach ($arr)为 &美元值){
    美元的价值 = 美元的价值 * 2;
}
un集(美元的价值);   // 美元的价值 no longer 参考s $arr[3]

常见错误2:误解 收取() 行为

尽管它的名字是, 收取() 如果项不存在,不仅返回假,而且 同样的回报 .

这种行为比一开始看起来更有问题,并且是问题的常见来源.

考虑以下几点:

元数据 = fetchRecordFromStorage($storage, $identifier);
if (!收取($ data [' keyShouldBeSet ']) {
    //如果' keyshould困扰'没有设置,在这里做一些事情
}

这段代码的作者大概想检查一下 keyShouldBeSet 是在 元数据. 但是,正如我们讨论过的, 收取($ data [' keyShouldBeSet ']) 如果返回假 $ data [' keyShouldBeSet ') 设定,但被设定了 . 所以上面的逻辑是有缺陷的.

下面是另一个例子:

if ($ _POST['主动']) {
    postData美元 = extractSomething($ _POST);
}

// ...

if (!收取(postData美元)){
    Echo 'post 不 active';
}

上面的代码假设 $ _POST['主动'] 返回 真正的,然后 postData 必然会被设定,因此呢 收取(postData美元) 将返回 真正的. 因此,相反地,上面的代码假设 只有 的方式 收取(postData美元) 将返回 是,如果 $ _POST['主动'] 返回 也。.

不.

解释说, 收取(postData美元) 也会返回 if postData美元 被设定为 . 因此 is 可能的 收取(postData美元) 返回 即使 $ _POST['主动'] 返回 真正的. 所以,上面的逻辑是有缺陷的.

顺便提一下,如果上面代码中的意图是再次检查 $ _POST['主动'] 回归了真,依靠了 收取() 因为无论如何,这都是一个糟糕的编码决定. 相反,最好是重新检查一下 $ _POST['主动']; i.e.:

if ($ _POST['主动']) {
    postData美元 = extractSomething($ _POST);
}

// ...

if ($ _POST['主动']) {
    Echo 'post 不 active';
}

不过,在某些情况下 is 检查变量是否真的设置了很重要.e.,以区分未设置的变量和设置为的变量 ), 数组_key_exists () 方法是一个更健壮的解决方案.

例如,我们可以将上面两个例子中的第一个重写如下:

元数据 = fetchRecordFromStorage($storage, $identifier);
if (! 数组_key_exists(' keyshould集 ', 元数据)) {
    //如果' keyshould困扰'未设置,执行此操作
}

此外,通过结合 数组_key_exists ()get_defined_vars (),我们可以可靠地检查当前作用域内的变量是否已设置:

if (数组_key_exists(' varshould困扰',get_defined_vars ())) {
    //变量$ varshould困扰在当前作用域中存在
}

常见错误#3:混淆引用返回与. 的价值

考虑下面的代码片段:

类配置
{
    Private 美元的价值 = [];

    getvalue () {
        return $这->值;
    }
}

$config = new config ();

$config->getvalue ()['测试'] = '测试';
echo $ config->getvalue ()['测试'];

如果你运行上面的代码,你会得到以下结果:

注意:未定义的索引:测试在/path/to/my/script.PHP第21行

什么是错的?

问题是上面的代码混淆了按引用返回数组和按值返回数组. 除非你显式地告诉PHP通过引用返回一个数组.e.,通过使用&), PHP将默认返回数组" 的价值 "。. 这意味着a 复制 将返回数组的实例,因此被调用的函数和调用者将不会访问数组的相同实例.

所以上面的调用 getvalue () 返回一个 复制美元的价值 数组而不是对它的引用. 考虑到这一点,让我们回顾一下上面例子中的两个关键行:

// getvalue ()返回美元的价值数组的副本,因此这添加了一个'测试'元素
//指向美元的价值数组的副本,但不指向美元的价值数组本身.
$config->getvalue ()['测试'] = '测试';

// getvalue ()再次返回美元的价值数组的另一个副本,而THIS副本不返回
//包含一个'测试'元素(这就是为什么我们得到"undefined index"消息).
echo $ config->getvalue ()['测试'];

的第一个副本是一个可能的修复 美元的价值 返回的数组 getvalue () and then operate on that 复制 subsequently; e.g.:

$vals = $config->getvalue ();
$vals['测试'] = '测试';
echo $ vals['测试'];

该代码将正常工作(例如.e.,它将输出 测试 不生成任何" undefined index "消息), 但这取决于你想要完成什么, 这种方法可能足够,也可能不够. 特别说明,以上代码不会对原代码进行修改 美元的价值 数组. 所以如果你 do 希望您的修改(例如添加一个' 测试 '元素)能够影响原始数组, 您将需要修改 getvalue () 函数返回 参考美元的价值 数组本身. 这是通过加a来完成的 & be为e the function name, t在这里by indicating that it should return a 参考; i.e.:

类配置
{
    Private 美元的价值 = [];

    //返回一个引用到实际的美元的价值数组
    公共函数 &getvalue () {
        return $这->值;
    }
}

$config = new config ();

$config->getvalue ()['测试'] = '测试';
echo $ config->getvalue ()['测试'];

它的输出将是 测试不出所料.

但是为了让事情变得更混乱,考虑下面的代码片段:

类配置
{
    私人美元值;

    //使用ArrayObject而不是数组
    公共函数__construct() {
        $这->值 = new ArrayObject();
    }

    getvalue () {
        return $这->值;
    }
}

$config = new config ();

$config->getvalue ()['测试'] = '测试';
echo $ config->getvalue ()['测试'];

如果您猜到了,这将导致与前面相同的“未定义索引”错误 数组 例如,你错了. 事实上, 代码将正常工作. 原因是,与数组不同, PHP总是通过引用传递对象. (ArrayObject 是一个SPL对象,它完全模仿数组的使用,但作为一个对象工作.)

如这些例子所示, 在PHP中,处理的是副本还是引用并不总是很明显的. 因此,理解这些默认行为(例如.e., variables and 数组s are passed 的价值; objects are passed by 参考) and 也 to carefully check the API documentation 为 the function you are calling to see if it is returning a value, 数组的副本, 对数组的引用, 或者一个对象的引用.

尽管如此,重要的是要注意返回对数组或对象的引用的做法 ArrayObject 这通常是应该避免的吗, 因为它为调用者提供了修改实例私有数据的能力. 这是对封装的“公然蔑视”. 相反,最好使用旧式的“getter”和“集ter”.g.:

类配置
{
    Private 美元的价值 = [];
    
    集Value($key, 美元值){
        $这->值[$key] = 美元的价值;
    }
    
    getValue($key) {
        return $这->值[$key];
    }
}

$config = new config ();

$config->集Value('测试Key', '测试Value');
echo $ config->getValue('测试Key');    // echos '测试Value'

这种方法使调用者能够设置或获取数组中的任何值,而无需提供对其他私有值的公共访问 美元的价值 数组本身.

常见错误#4:在循环中执行查询

如果你的PHP不工作,遇到这样的事情并不罕见:

$models = [];

为each ($inputValues作为$inputValue) {
    $models[] = 美元的价值Repository->findByValue(美元inputValue);
}

虽然这可能完全没有错, 但是如果你遵循代码中的逻辑, 你可能会发现,看起来无辜的人在呼唤 美元的价值Repository->findByValue() 最终导致某种查询,例如:

$result = $connection->query("SELECT `x`,`y` FROM `值` WHERE `value`=" . 美元inputValue);

因此,上述循环的每次迭代都会导致对数据库的单独查询. 因此,如果, 例如, 你提供了一个1的数组,000个值到循环中, 它会生成1,000个对资源的单独查询! 如果在多个线程中调用这样的脚本, 这可能会让整个体系陷入停滞.

因此,识别代码和代码何时进行查询是至关重要的, 只要有可能, 收集这些值,然后运行一个查询来获取所有结果.

一个相当常见的地方会遇到查询效率低下的一个例子(i.e.(在循环中)是当表单与值列表(例如id)一起发布时. 然后, 检索每个id的完整记录数据, 代码将循环遍历数组,并为每个ID执行单独的SQL查询. 这通常看起来像这样:

元数据 = [];
Foreach ($id作为$id) {
    $result = $connection->query("SELECT `x`, `y` FROM `值` WHERE `id` = " . $id);
    元数据[] = $result->fetch_row();
}

但是同样的事情在a中可以更有效地完成 SQL查询如下:

元数据 = [];
If (count($ids)) {
    $result = $connection->query("SELECT `x`, `y` FROM `值` WHERE `id` IN (" . 内爆(',' $ id));
    while ($row = $result->fetch_row()) {
        元数据[] = $row;
    }
}

因此,识别何时进行查询是至关重要的, 直接或间接, 按你的代码. 只要有可能,收集这些值,然后运行一个查询来获取所有结果. 然而,这里也必须谨慎,这将导致我们下一个常见的PHP错误…

常见错误5:内存占用和效率低下

虽然一次获取许多记录肯定比为每一行获取运行单个查询更有效, 这种方法可能会导致“内存不足” libmysqlclient 当使用PHP的 mysql 扩展.

为了演示,让我们看一个有限资源(512MB RAM)、MySQL和 php cli.

我们将像这样引导一个数据库表:

//连接mysql
$connection = new mysqli('localhost', 'username', 'password', 'database');

//创建400列的表
美元的查询 = 'CREATE TABLE ' 测试 ' (' id ' INT NOT NULL PRIMARY KEY AUTO_INCREMENT');
为 ($col = 0; $col < 400; $col++) {
    美元的查询 .= ", ' col$col ' CHAR(10) NOT NULL";
}
美元的查询 .= ');';
$connection->query(美元的查询);

//写入200万行
为 ($row = 0; $row < 2000000; $row++) {
    美元的查询 = "INSERT INTO ' 测试 ' VALUES ($row";
    为 ($col = 0; $col < 400; $col++) {
        美元的查询 .= ', ' . mt_rand (1000000000, 9999999999);
    }
    美元的查询 .= ')';
    $connection->query(美元的查询);
}

好的,现在让我们检查资源使用情况:

//连接mysql
$connection = new mysqli('localhost', 'username', 'password', 'database');
echo "Be为e: " . memory_get_peak_usage () . "\n";

$res = $connection->query('SELECT `x`,`y` FROM `测试` LIMIT 1');
echo "Limit 1: " . memory_get_peak_usage () . "\n";

$res = $connection->query('SELECT `x`,`y` FROM `测试` LIMIT 10000');
echo "Limit 10000 " . memory_get_peak_usage () . "\n";

输出:

前:224704
限制1:224704
上限10000:224704

很酷的. 就资源而言,查询似乎在内部得到了安全管理.

不过,为了确保万无一失,让我们再一次提高限制,将其设置为100,000. 哦. 当我们这样做时,我们得到:

mysqli::query(): (HY000/2013):
              在/root/测试目录下查询时,与MySQL服务器失去连接.PHP在第11行

发生了什么事?

这里的问题是PHP的方式 mysql 模块工作. 它实际上只是 libmysqlclient,它会做脏活累活. 当选择一部分数据时,它直接进入内存. 由于这些内存不是由PHP的管理器管理的, memory_get_peak_usage () 当我们在查询中提高限制时,不会显示资源利用率的任何增加. 这就导致了像上面所展示的那样的问题,我们被欺骗而自满地认为我们的内存管理是好的. 但在现实中, 我们的内存管理存在严重缺陷,我们可能会遇到如上所示的问题.

您至少可以避免上面的headfake(尽管它本身不会提高内存利用率),而是使用 mysqlnd 模块. mysqlnd 编译为本地PHP扩展和它 使用PHP的内存管理器.

因此,如果我们使用 mysqlnd 而不是 mysql,我们就能更真实地了解我们的内存使用情况:

前:232048
限制:324952
上限10000:32572912

顺便说一句,情况比这更糟. 根据PHP文档, mysql 使用的资源是 mysqlnd 为了存储数据,所以原脚本使用 mysql 实际使用的内存比这里显示的还要多(大约是两倍).

为了避免这些问题, consider limiting the size of your queries and using a loop 与 small number of iterations; e.g.:

$totalNumberToFetch = 10000;
$portionSize = 100;

为 ($i = 0; $i <= ceil($totalNumberToFetch / $portionSize); $i++) {
    $limitFrom = $portionSize * $i;
    $res = $connection->query(
                         "SELECT ' x ', ' y ' FROM ' 测试 ' LIMIT $limitFrom, $portionSize");
}

当我们考虑这个PHP错误和 错误# 4 以上, 我们意识到,理想情况下,您的代码需要在两者之间实现健康的平衡, 一方面,一方面, 查询过于细粒度和重复, vs. 每个单独的查询都太大. 就像生活中的大多数事情一样, balance is needed; either extreme is 不 good and can cause problems 与 PHP 不 working 正确.

常见错误#6:忽略Unicode/UTF-8问题

在某种意义上, 这实际上是PHP本身的问题,而不是在调试PHP时遇到的问题, 但这个问题从未得到充分解决. PHP 6的核心是支持unicode的, 但当PHP 6的开发在2010年暂停时,这一计划被搁置了.

但这并不能免除开发者的责任 正确处理UTF-8 并避免错误地假设所有字符串都必须是“普通的旧ASCII”. 不能正确处理非ascii字符串的代码因引入粗糙而臭名昭著 heisenbugs 放入代码中. 即使是简单的 strlen ($ _POST['名字']) 如果姓“Schrödinger”的人试图登录您的系统,那么呼叫可能会导致问题.

下面是一个小清单,可以避免代码中出现这样的问题:

  • 如果您不太了解Unicode和UTF-8,那么至少应该学习基础知识. 这是一本很好的入门书 在这里.
  • 一定要经常使用 mb_ * 函数而不是旧的字符串函数(确保PHP构建中包含“multibyte”扩展).
  • 确保您的数据库和表设置为使用Unicode(许多版本的MySQL仍在使用) latin1 默认情况下).
  • 记住, json_encode () 转换非ascii符号(如.g., " Schrödinger "变成" Schr\u00f6dinger "),但是 serialize ().
  • 确保您的PHP代码文件也是UTF-8编码的,以避免在将字符串与硬编码或配置的字符串常量连接时发生冲突.

在这方面一个特别宝贵的资源是 用于PHP和MySQL的UTF-8 Primer 篇文章 Claria旧金山 在这个博客上.

常见错误7:假设 $ _POST 将始终包含您的POST数据

尽管它的名字是 $ _POST 数组并不总是包含POST数据,很容易发现它为空. 为了理解这一点,让我们看一个例子. 。发出服务器请求 jQuery.ajax () 呼叫如下:

// js
$.ajax ({
    url: http://my.网站/一些/路径”,
    方法:“文章”,
    JSON数据:.Stringify ({a: 'a', b: 'b'}),
    contentType:“application / json”
});

(顺便提一下,请注意 contentType:“application / json” 在这里. 我们以JSON的形式发送数据,这在api中非常流行. 它是默认的,例如,在 AngularJS http美元 服务.)

在我们示例的服务器端,我们只需转储 $ _POST 数组:

/ / php
var_dump ($ _POST);

令人惊讶的是,结果将是:

数组(0){}

为什么? 我们的JSON字符串发生了什么 {a: 'a', b: 'b'}?

答案是 PHP只在内容类型为的情况下自动解析POST有效负载 应用程序/ x-www-为m-urlencoded or 多部分/格式. 这样做的原因是有历史意义的——这两种内容类型是多年前PHP开发时唯一使用的 $ _POST 实施. 所以对于任何其他内容类型(即使是那些今天非常流行的内容类型,比如 application / json), PHP不会自动加载POST有效负载.

$ _POST 如果我们重写它,它是超全局的吗 一次 (最好是在脚本的早期),修改后的值(i.e.(包括POST有效负载)将在整个代码中被引用. 这很重要,因为 $ _POST 是常用的PHP框架和几乎所有的自定义脚本提取和转换请求数据.

因此,例如,在处理内容类型为的POST有效负载时 application / json,我们需要手动解析请求内容(例如.e.(解码JSON数据)并覆盖 $ _POST 变量,如下:

/ / php
$ _POST = json_decode(file_get_contents('php://input'), 真正的);

然后当我们倾倒 $ _POST 数组, we see that it correctly includes the POST payload; e.g.:

数组(2) { ["a"]=> 字符串(1) "a" ["b"]=> 字符串(1) "b" }

常见错误#8:认为PHP支持字符数据类型

看看这段示例代码,并试着猜测它将打印什么:

为 ($c = 'a'; $c <= 'z'; $c++) {
    echo $ c . "\n";
}

如果你的答案是从a到z,你可能会惊讶地发现你错了.

是的,它会打印' a '到' z ',但是它会 打印' aa '到' yz '. 让我们看看为什么.

在PHP中没有 字符 datatype; 只有 字符串 是可用的. 记住这一点,增加 字符串 z PHP中 aa:

php> $c = 'z'; echo ++$c . "\n";
aa

然而,让事情更加混乱的是, aa 是按 z:

php> var_export((boolean)('aa' < 'z')) . "\n";
真正的

这就是上面给出的示例代码打印字母的原因 a 通过 z,但是后来 打印 aa 通过 yz. 到达时停止 za,这是它遇到的第一个“大于”的值 z:

php> var_export((boolean)('za' < 'z')) . "\n";
假

既然如此,有一种方法 正确 在PHP中遍历值' a '到' z ':

为 ($i = ord('a'); $i <= ord('z'); $i++) {
    回声空空(i)美元 . "\n";
}

或者:

$letters = range('a', 'z');

为 ($i = 0; $i < count($letters); $i++) {
    回波信美元($ i) . "\n";
}

常见错误#9:忽略编码标准

尽管忽略编码标准并不会直接导致需要调试PHP代码, 这可能仍然是这里要讨论的最重要的事情之一.

忽略编码标准可能会给项目带来一系列问题. 充其量,它会导致代码不一致(因为每个开发人员都在“做自己的事情”)。. 但在最坏的情况下, 它产生的PHP代码不能工作,或者很难(有时几乎不可能)导航, 使得调试极其困难, 增强, 维护. 这意味着降低了团队的工作效率, 包括大量浪费的(或者至少是不必要的)努力.

对于PHP开发人员来说幸运的是, PHP标准推荐(PSR), 由以下五个标准组成:

PSR最初是根据市场上最受认可的平台的维护者的输入创建的. Zend, Drupal, Symfony, Joomla和 其他人 对这些标准做出了贡献,现在正在遵循这些标准. 甚至在此之前多年试图成为标准的PEAR现在也参与了PSR.

在某种意义上, 你的编码标准是什么几乎无关紧要, 只要你同意一个标准并坚持下去, 但遵循PSR通常是个好主意,除非你的项目有令人信服的理由不这么做. 越来越多的团队和项目符合PSR. 在这一点上,它绝对被大多数PHP开发人员认可为“标准”, 因此,使用它将有助于确保新开发人员在加入您的团队时熟悉并适应您的编码标准.

常见错误10:误用 空()

一些PHP开发人员喜欢使用 空() 对于布尔值检查几乎所有的东西. 不过,在某些情况下,这可能会导致混淆.

首先,我们回到数组和 ArrayObject 实例(模拟数组). 考虑到它们的相似性,很容易假设数组和 ArrayObject 实例的行为将相同. 然而,事实证明这是一个危险的假设. 例如,在PHP 5中.0:

// PHP 5.0或更高:
美元的数组 = [];
var_dump(empty(美元的数组));        // outputs bool(真正的) 
美元的数组 = new ArrayObject();
var_dump(empty(美元的数组));        // outputs bool(假)
//为什么它们不能产生相同的输出?

更糟糕的是,在PHP 5之前,结果可能会有所不同.0:

// PHP 5之前版本.0:
美元的数组 = [];
var_dump(empty(美元的数组));        // outputs bool(假) 
美元的数组 = new ArrayObject();
var_dump(empty(美元的数组));        // outputs bool(假)

不幸的是,这种方法非常流行. 举个例子,就是这样 Zend \ Db \ TableGateway Framework 2调用时返回数据 当前的() on TableGateway: select () 按照医生建议的结果. 开发人员很容易成为这类数据错误的受害者.

为了避免这些问题,检查空数组结构的更好方法是使用 count ():

//注意,这在所有版本的PHP中都有效(包括5之前和5之后).0):
美元的数组 = [];
var_dump(count(美元的数组));        // outputs int(0)
美元的数组 = new ArrayObject();
var_dump(count(美元的数组));        // outputs int(0)

顺便说一下,由于PHP强制转换 0 to , count () 也可以用在里面吗 if () 检查空数组的条件. 值得注意的是,在PHP中, count () 恒定的复杂度(O(1) 操作),这使得它更清楚地表明这是正确的选择.

另一个例子是 空() 当将它与魔法类函数结合在一起时会有危险吗 __get (). 我们定义两个类 测试 双方的财产.

首先定义a 常规的 类,包含 测试 作为正常属性:

类常规
{
	公共$测试 = “价值”;
}

然后我们定义a 魔法 类,它使用魔法 __get () 操作员访问其 测试 属性:

类魔法
{
	private 美元的价值 = ['测试' => “价值”];

	公共函数__get($key)
	{
		if (is集($这->值[$key])) {
			return $这->值[$key];
		}
	}
}

好,现在让我们看看当我们试图访问 测试 每一类的属性:

$regular = new regular ();
var_dump($regular->测试);    // outputs 字符串(4) "value"
美元的魔法 = new magic ();
var_dump(美元的魔法->测试);      // outputs 字符串(4) "value"

目前还好.

现在让我们看看调用时会发生什么 空() 在每一个问题上:

var_dump(empty($regular->测试));    // outputs bool(假)
var_dump(empty(美元的魔法->测试));      // outputs bool(真正的)

啊. 所以如果我们依靠 空(),我们可能会误以为 测试 的属性 美元的魔法 是空的,而实际上它被设置为 “价值”.

不幸的是,如果一个类使用了魔法 __get () 函数检索属性的值, 没有万无一失的方法来检查该属性值是否为空. 在类的作用域之外,您实际上只能检查是否 值将返回, 这并不一定意味着没有设置相应的键, 因为实际上 可以 一直在 to .

相反,如果我们试图引用a的一个不存在的属性 常规的 类实例,我们将得到类似以下的通知:

注意:未定义属性:常规的::$nonExistantTest in /path/to/测试.PHP在第10行

调用堆栈:
    0.0012     234704   1. {主要}()/ / /测试.php: 0

所以这里的重点是 空() 方法应该小心使用,因为它可能会导致混淆——甚至潜在地误导——结果, 如果一个人不小心.

总结

PHP的易用性可能会让开发人员产生一种错误的舒适感, 由于语言的一些细微差别和特性,使他们容易受到冗长的PHP调试的影响. 这可能会导致PHP无法工作,并出现以下描述的问题.

PHP语言在其20年的历史中有了显著的发展. 熟悉它的微妙之处是值得努力的,因为它将有助于确保 你生产的软件 是否更具可伸缩性、健壮性和可维护性.

标签

聘请Toptal这方面的专家.
现在雇佣
伊利亚·萨诺西安的头像
Ilya Sanosian

位于 牛津,英国

成员自 2013年9月19日

作者简介

伊利亚是一名IT顾问, 网络架构师, 拥有12年以上团队建设和领导经验的经理.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

专业知识

以前在

赫芬顿邮报

世界级的文章,每周发一次.

<为m aria-label="Sticky subscribe 为m" class="-Ulx1zbi P7bQLARO _2ABsLCza">

订阅意味着同意我们的 隐私政策

世界级的文章,每周发一次.

<为m aria-label="Bottom subscribe 为m" class="-Ulx1zbi P7bQLARO _2ABsLCza">

订阅意味着同意我们的 隐私政策

Toptal开发者

加入总冠军® 社区.