个性化阅读
专注于IT技术分析

Buggy PHP代码:PHP开发人员最常犯的10个错误

本文概述

PHP使构建基于Web的系统相对容易, 这是其流行的很多原因。但是, 尽管PHP具有易用性, 但它已经发展成为一种复杂的语言, 具有许多可能会咬住开发人员的框架, 细微差别和微妙之处, 从而导致数小时的繁琐调试工作。本文重点介绍了PHP开发人员需要注意的十个较常见的错误。

常见错误1:在foreach循环后留下悬空的数组引用

不确定如何在PHP中使用foreach循环?如果要对要遍历的数组中的每个元素进行操作, 则在foreach循环中使用引用很有用。例如:

$arr = array(1, 2, 3, 4);
foreach ($arr as &$value) {
    $value = $value * 2;
}
// $arr is now array(2, 4, 6, 8)

问题是, 如果你不小心, 还会带来一些不良后果和后果。具体来说, 在上面的示例中, 执行代码后, $ value将保留在范围内, 并将保留对数组中最后一个元素的引用。因此, 涉及$ value的后续操作可能会无意间结束修改数组中的最后一个元素。

要记住的主要事情是foreach不会创建作用域。因此, 以上示例中的$ value是脚本顶部范围内的引用。在每次迭代中, foreach都将引用设置为指向$ array的下一个元素。因此, 循环完成后, $ value仍指向$ array的最后一个元素, 并保持在作用域内。

这是一个可能导致错误的, 令人困惑的错误的示例:

$array = [1, 2, 3];
echo implode(', ', $array), "\n";

foreach ($array as &$value) {}    // by reference
echo implode(', ', $array), "\n";

foreach ($array as $value) {}     // by value (i.e., copy)
echo implode(', ', $array), "\n";

上面的代码将输出以下内容:

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

不, 那不是错字。最后一行的最后一个值的确是2, 而不是3。

为什么?

在经过第一个foreach循环之后, $ array保持不变, 但是, 如上所述, $ value保留为对$ array中最后一个元素的悬挂引用(因为foreach循环通过引用访问了$ value)。

结果, 当我们经历第二个foreach循环时, 似乎出现了”奇怪的事情”。具体来说, 由于$ value现在正在按值(即按副本)访问, 因此foreach在循环的每个步骤中将每个顺序的$ array元素复制到$ value中。结果, 这是第二个foreach循环的每个步骤中发生的事情:

  • 步骤1:将$ array [0](即” 1″)复制到$ value(是对$ array [2]的引用)中, 因此$ array [2]现在等于1。因此$ array现在包含[1, 2, 1]。
  • 步骤2:将$ array [1](即” 2″)复制到$ value(是对$ array [2]的引用), 因此$ array [2]现在等于2。因此$ array现在包含[1, [2, 2]。
  • 步骤3:将$ array [2](现在等于” 2″)复制到$ value(这是对$ array [2]的引用), 因此$ array [2]仍等于2。所以$ array现在包含[1] , 2, 2]。

为了仍然获得在foreach循环中使用引用的好处而又不冒此类问题的风险, 请在foreach循环之后立即在变量上调用unset()来删除引用;例如。:

$arr = array(1, 2, 3, 4);
foreach ($arr as &$value) {
    $value = $value * 2;
}
unset($value);   // $value no longer references $arr[3]

常见错误#2:误解isset()行为

尽管有它的名字, isset()不仅在项目不存在时返回false, 而且对于空值也返回false。

此行为比起初看起来要麻烦得多, 并且是问题的常见根源。

考虑以下:

$data = fetchRecordFromStorage($storage, $identifier);
if (!isset($data['keyShouldBeSet']) {
    // do something here if 'keyShouldBeSet' is not set
}

该代码的作者大概是想检查keyShouldBeSet是否在$ data中设置。但是, 如上所述, 如果设置了$ data [‘keyShouldBeSet’]但将其设置为null, 则isset($ data [‘keyShouldBeSet’])也将返回false。因此上述逻辑是有缺陷的。

这是另一个例子:

if ($_POST['active']) {
    $postData = extractSomething($_POST);
}

// ...

if (!isset($postData)) {
    echo 'post not active';
}

上面的代码假定如果$ _POST [‘active’]返回true, 则必须设置postData, 因此isset($ postData)将返回true。因此, 相反, 以上代码假定isset($ postData)返回false的唯一方法是$ _POST [‘active’]也返回false。

不。

如前所述, 如果$ postData设置为null, 则isset($ postData)也将返回false。因此, 即使$ _POST [‘active’]返回true, isset($ postData)也可能返回false。同样, 以上逻辑是有缺陷的。

顺便说一句, 作为补充, 如果上述代码中的意图确实是要再次检查$ _POST [‘active’]是否返回true, 则在任何情况下都依赖isset()进行编码决策。相反, 最好只检查$ _POST [‘active’];即:

if ($_POST['active']) {
    $postData = extractSomething($_POST);
}

// ...

if ($_POST['active']) {
    echo 'post not active';
}

但是, 在某些情况下, 重要的是要检查是否确实设置了变量(即, 区分未设置的变量和设置为null的变量), array_key_exists()方法要健壮得多。解。

例如, 我们可以重写以下两个示例中的第一个示例, 如下所示:

$data = fetchRecordFromStorage($storage, $identifier);
if (! array_key_exists('keyShouldBeSet', $data)) {
    // do this if 'keyShouldBeSet' isn't set
}

此外, 通过将array_key_exists()与get_defined_vars()结合使用, 我们可以可靠地检查当前范围内的变量是否已设置:

if (array_key_exists('varShouldBeSet', get_defined_vars())) {
    // variable $varShouldBeSet exists in current scope
}

常见错误3:关于按引用返回与按值返回的困惑

考虑以下代码片段:

class Config
{
    private $values = [];

    public function getValues() {
        return $this->values;
    }
}

$config = new Config();

$config->getValues()['test'] = 'test';
echo $config->getValues()['test'];

如果你运行上述代码, 则会得到以下信息:

PHP Notice:  Undefined index: test in /path/to/my/script.php on line 21

怎么了?

问题是上面的代码混淆了按引用返回数组和按值返回数组。除非你明确地告诉PHP通过引用(即使用&)返回数组, 否则PHP将默认以”按值”返回数组。这意味着将返回该数组的副本, 因此被调用函数和调用方将不会访问该数组的同一实例。

因此, 上述对getValues()的调用将返回$ values数组的副本, 而不是对其的引用。考虑到这一点, 让我们重新回顾示例上方的两条关键线:

// getValues() returns a COPY of the $values array, so this adds a 'test' element
// to a COPY of the $values array, but not to the $values array itself.
$config->getValues()['test'] = 'test';

// getValues() again returns ANOTHER COPY of the $values array, and THIS copy doesn't
// contain a 'test' element (which is why we get the "undefined index" message).
echo $config->getValues()['test'];

一种可能的解决方法是保存getValues()返回的$ values数组的第一个副本, 然后在该副本上进行操作。例如。:

$vals = $config->getValues();
$vals['test'] = 'test';
echo $vals['test'];

该代码可以正常工作(即, 它将在不生成任何”未定义索引”消息的情况下输出测试), 但是根据你要完成的工作, 此方法可能合适, 也可能不合适。特别是, 上面的代码将不会修改原始的$ values数组。因此, 如果你确实希望所做的修改(例如添加” test”元素)影响原始数组, 则需要修改getValues()函数以返回对$ values数组本身的引用。这是通过在函数名称前添加&来完成的, 从而表明它应该返回一个引用。即:

class Config
{
    private $values = [];

    // return a REFERENCE to the actual $values array
    public function &getValues() {
        return $this->values;
    }
}

$config = new Config();

$config->getValues()['test'] = 'test';
echo $config->getValues()['test'];

如预期的那样, 此输出将被测试。

但为了使事情更加混乱, 请考虑以下代码片段:

class Config
{
    private $values;

    // using ArrayObject rather than array
    public function __construct() {
        $this->values = new ArrayObject();
    }

    public function getValues() {
        return $this->values;
    }
}

$config = new Config();

$config->getValues()['test'] = 'test';
echo $config->getValues()['test'];

如果你猜想这会导致与我们前面的数组示例相同的”未定义索引”错误, 那么你错了。实际上, 此代码可以正常工作。原因是, 与数组不同, PHP始终通过引用传递对象。 (ArrayObject是一个SPL对象, 它完全模仿数组的用法, 但可以作为对象使用。)

如这些示例所示, 无论是处理副本还是引用, 在PHP中并不总是很明显。因此, 必须了解这些默认行为(即, 变量和数组按值传递;对象按引用传递), 并仔细检查API文档以了解所调用函数是否返回值, 即数组的副本, 对数组的引用或对对象的引用。

综上所述, 重要的是要注意, 通常应避免返回对数组或ArrayObject的引用的做法, 因为它为调用者提供了修改实例的私有数据的能力。这是封装的”面子”。相反, 最好使用旧式的” getters”和” setters”, 例如:

class Config
{
    private $values = [];
    
    public function setValue($key, $value) {
        $this->values[$key] = $value;
    }
    
    public function getValue($key) {
        return $this->values[$key];
    }
}

$config = new Config();

$config->setValue('testKey', 'testValue');
echo $config->getValue('testKey');    // echos 'testValue'

这种方法使调用者可以设置或获取数组中的任何值, 而无需提供对私有的$ values数组本身的公共访问。

错误四:循环执行查询

如果你的PHP无法正常工作, 遇到类似这样的情况并不罕见:

$models = [];

foreach ($inputValues as $inputValue) {
    $models[] = $valueRepository->findByValue($inputValue);
}

尽管这里可能绝对没有错, 但是如果你遵循代码中的逻辑, 则可能会发现对$ valueRepository-> findByValue()的无辜调用最终会导致某种查询, 例如:

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

结果, 上述循环的每次迭代都会导致对数据库的单独查询。因此, 例如, 如果你向循环提供了一个包含1, 000个值的数组, 它将对资源生成1, 000个单独的查询!如果在多个线程中调用了这样的脚本, 则可能会使系统陷入停顿。

因此, 关键是要识别代码何时进行查询, 并尽可能收集值, 然后运行一个查询以获取所有结果。

效率低下(即循环)遇到查询的一个相当常见的地方的例子是, 表单中带有值列表(例如, ID)。然后, 要检索每个ID的完整记录数据, 代码将遍历数组, 并对每个ID进行单独的SQL查询。这通常看起来像这样:

$data = [];
foreach ($ids as $id) {
    $result = $connection->query("SELECT `x`, `y` FROM `values` WHERE `id` = " . $id);
    $data[] = $result->fetch_row();
}

但是, 可以通过单个SQL查询更有效地完成同一件事, 如下所示:

$data = [];
if (count($ids)) {
    $result = $connection->query("SELECT `x`, `y` FROM `values` WHERE `id` IN (" . implode(', ', $ids));
    while ($row = $result->fetch_row()) {
        $data[] = $row;
    }
}

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

常见错误5:内存使用的假象和效率低下

虽然一次获取多个记录绝对比对每一行进行单个查询来获取效率更高, 但是当使用PHP的mysql扩展时, 这种方法可能会导致libmysqlclient中出现”内存不足”的情况。

为了演示, 让我们看一下一个资源有限(512MB RAM), MySQL和php-cli的测试箱。

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

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

// create table of 400 columns
$query = 'CREATE TABLE `test`(`id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT';
for ($col = 0; $col < 400; $col++) {
    $query .= ", `col$col` CHAR(10) NOT NULL";
}
$query .= ');';
$connection->query($query);

// write 2 million rows
for ($row = 0; $row < 2000000; $row++) {
    $query = "INSERT INTO `test` VALUES ($row";
    for ($col = 0; $col < 400; $col++) {
        $query .= ', ' . mt_rand(1000000000, 9999999999);
    }
    $query .= ')';
    $connection->query($query);
}

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

// connect to mysql
$connection = new mysqli('localhost', 'username', 'password', 'database');
echo "Before: " . memory_get_peak_usage() . "\n";

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

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

输出如下:

Before: 224704
Limit 1: 224704
Limit 10000: 224704

凉。看起来查询是在资源方面在内部安全地管理的。

不过, 请确保我们再提高一次限制, 并将其设置为100, 000。哦哦当我们这样做时, 我们得到:

PHP Warning:  mysqli::query(): (HY000/2013):
              Lost connection to MySQL server during query in /root/test.php on line 11

发生了什么?

这里的问题是PHP的mysql模块的工作方式。它实际上只是libmysqlclient的代理, 可以完成肮脏的工作。选择一部分数据后, 它将直接进入内存。由于此内存不是由PHP的经理管理的, 因此memory_get_peak_usage()不会显示资源利用率的任何增加, 因为我们超出了查询的上限。这会导致上述问题, 我们被欺骗时以为我们的内存管理很好就引起了自满。但实际上, 我们的内存管理存在严重缺陷, 我们可能会遇到上述问题。

通过使用mysqlnd模块, 你至少可以避免上述麻烦(尽管它本身不会提高内存利用率)。 mysqlnd被编译为本地PHP扩展, 并且确实使用了PHP的内存管理器。

因此, 如果我们使用mysqlnd而不是mysql运行上述测试, 我们将获得更加真实的内存利用情况:

Before: 232048
Limit 1: 324952
Limit 10000: 32572912

顺便说一下, 甚至比这更糟。根据PHP文档, mysql用于存储数据的资源是mysqlnd的两倍, 因此, 使用mysql的原始脚本实际使用的内存甚至比此处显示的还要多(大约两倍)。

为避免此类问题, 请考虑限制查询的大小, 并使用迭代次数少的循环。例如。:

$totalNumberToFetch = 10000;
$portionSize = 100;

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

当我们同时考虑上述PHP错误和错误#4时, 我们意识到, 理想情况下, 你的代码需要达到一个健康的平衡, 一方面, 让你的查询过于细化和重复, 另一方面让你的每个查询单个查询太大。就像生活中大多数事物一样, 需要平衡。任一种极端情况都不好, 并且可能导致PHP无法正常工作。

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

从某种意义上讲, 这实际上是PHP本身的问题, 而不是调试PHP时遇到的问题, 但从未得到充分解决。 PHP 6的核心是要使其具有Unicode感知能力, 但是当PHP 6的开发于2010年中止时, 它就被搁置了。

但这绝不能免除开发人员正确处理UTF-8的麻烦, 并避免了错误的假设, 即所有字符串都必须是”普通旧ASCII”。无法正确处理非ASCII字符串的代码因在代码中引入粗糙的heisenbug而臭名昭著。如果姓氏如”Schrödinger”的人试图登录你的系统, 则即使是简单的strlen($ _ POST [‘name’])调用也可能导致问题。

以下是一个小清单, 可避免代码中出现此类问题:

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

在这方面特别有价值的资源是Francisco Claria在此博客上发布的针对PHP和MySQL的UTF-8入门。

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

尽管$ _POST数组的名称是正确的, 但它并不总是包含你的POST数据, 并且很容易被发现为空。为了理解这一点, 让我们看一个例子。假设我们通过jQuery.ajax()发出服务器请求, 如下所示:

// js
$.ajax({
    url: 'http://my.site/some/path', method: 'post', data: JSON.stringify({a: 'a', b: 'b'}), contentType: 'application/json'
});

(顺便说一下, 请在此处注意contentType:’application / json’。我们以JSON格式发送数据, 这在API中非常流行。这是默认值, 例如, 用于在AngularJS $ http服务中发布。)

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

// php
var_dump($_POST);

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

array(0) { }

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

答案是, PHP仅在内容类型为application / x-www-form-urlencoded或multipart / form-data时才自动解析POST负载。原因是历史性的-这两种内容类型实质上是几年前实现PHP的$ _POST时使用的唯一内容类型。因此, 对于任何其他内容类型(甚至是当今非常流行的那些内容类型, 例如application / json), PHP都不会自动加载POST有效负载。

由于$ _POST是一个超全局变量, 因此如果我们重写一次(最好是在脚本的早期), 那么修改后的值(即包括POST有效负载)将在整个代码中都可引用。这很重要, 因为$ _POST通常被PHP框架和几乎所有自定义脚本用来提取和转换请求数据。

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

// php
$_POST = json_decode(file_get_contents('php://input'), true);

然后, 当我们转储$ _POST数组时, 我们看到它正确包含了POST负载;例如。:

array(2) { ["a"]=> string(1) "a" ["b"]=> string(1) "b" }

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

查看以下示例代码, 并尝试猜测它将输出什么:

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

如果你通过” z”回答” a”, 你可能会惊讶地发现自己错了。

是的, 它将打印” a”至” z”, 但随后还将打印” aa”至” yz”。让我们看看为什么。

在PHP中, 没有char数据类型;仅字符串可用。考虑到这一点, 在PHP中增加字符串z会产生aa:

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

更进一步混淆了事情, 在字典上, aa比z小:

php> var_export((boolean)('aa' < 'z')) . "\n";
true

因此, 上面显示的示例代码会打印字母a到z, 然后又打印aa到yz。当到达za时它停止, 这是它”大于” z时遇到的第一个值:

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

就是这种情况, 这是一种在PHP中正确循环通过值” a”至” z”的方法:

for ($i = ord('a'); $i <= ord('z'); $i++) {
    echo chr($i) . "\n";
}

或者:

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

for ($i = 0; $i < count($letters); $i++) {
    echo $letters[$i] . "\n";
}

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

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

忽略编码标准会在项目上引起很多问题。充其量, 它导致代码不一致(因为每个开发人员都在”做自己的事情”)。但是在最坏的情况下, 它会产生无法运行或难以导航(有时几乎是不可能)的PHP代码, 从而使其调试, 增强和维护变得极为困难。这意味着降低团队的生产力, 包括大量的浪费(或至少不必要的)努力。

幸运的是, 对于PHP开发人员来说, 有PHP标准建议书(PSR), 它由以下五个标准组成:

  • PSR-0:自动加载标准
  • PSR-1:基本编码标准
  • PSR-2:编码样式指南
  • PSR-3:记录器接口
  • PSR-4:自动加载器

PSR最初是基于市场上最受认可的平台的维护者的输入而创建的。 Zend, Drupal, Symfony, Joomla等人为这些标准做出了贡献, 现在正在遵循它们。甚至在多年之前一直试图成为标准的PEAR现在都参与PSR。

从某种意义上说, 只要你同意一个标准并坚持下去, 你的编码标准几乎就无关紧要, 但是遵循PSR通常是一个好主意, 除非你有令人信服的理由在项目上进行其他操作。越来越多的团队和项目符合PSR。目前, Tt已被大多数PHP开发人员公认是”标准”, 因此使用Tt将有助于确保新开发人员加入你的团队后对你的编码标准感到熟悉和满意。

常见错误10:滥用empty()

一些PHP开发人员喜欢使用empty()进行几乎所有内容的布尔检查。但是, 在某些情况下, 这可能会导致混乱。

首先, 让我们回到数组和ArrayObject实例(模拟数组)。鉴于它们的相似性, 很容易假设数组和ArrayObject实例的行为相同。但是, 这被证明是一个危险的假设。例如, 在PHP 5.0中:

// PHP 5.0 or later:
$array = [];
var_dump(empty($array));        // outputs bool(true) 
$array = new ArrayObject();
var_dump(empty($array));        // outputs bool(false)
// why don't these both produce the same output?

更糟糕的是, 在PHP 5.0之前的结果会有所不同:

// Prior to PHP 5.0:
$array = [];
var_dump(empty($array));        // outputs bool(false) 
$array = new ArrayObject();
var_dump(empty($array));        // outputs bool(false)

不幸的是, 这种方法非常流行。例如, 当文档建议在TableGateway :: select()结果上调用current()时, 这是Zend Framework 2的Zend \ Db \ TableGateway返回数据的方式。通过此类数据, 开发人员很容易成为该错误的受害者。

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

// Note that this work in ALL versions of PHP (both pre and post 5.0):
$array = [];
var_dump(count($array));        // outputs int(0)
$array = new ArrayObject();
var_dump(count($array));        // outputs int(0)

顺便说一句, 由于PHP将0强制转换为false, 因此count()也可以在if()条件下使用以检查空数组。还要注意的是, 在PHP中, count()是数组上的常数复杂度(O(1)操作), 这使我们更加清楚地知道这是正确的选择。

empty()可能很危险的另一个示例是将其与魔术类函数__get()组合使用。让我们定义两个类, 并在两个类中都有一个test属性。

首先, 我们定义一个Regular类, 其中将test作为常规属性:

class Regular
{
	public $test = 'value';
}

然后, 我们定义一个Magic类, 该类使用magic __get()运算符访问其测试属性:

class Magic
{
	private $values = ['test' => 'value'];

	public function __get($key)
	{
		if (isset($this->values[$key])) {
			return $this->values[$key];
		}
	}
}

好的, 现在让我们看看当尝试访问以下每个类的test属性时会发生什么:

$regular = new Regular();
var_dump($regular->test);    // outputs string(4) "value"
$magic = new Magic();
var_dump($magic->test);      // outputs string(4) "value"

到目前为止很好。

但是, 现在让我们看看当我们对以下每一个调用empty()时会发生什么:

var_dump(empty($regular->test));    // outputs bool(false)
var_dump(empty($magic->test));      // outputs bool(true)

啊。因此, 如果我们依赖empty(), 我们可能会误以为$ magic的测试属性为空, 而实际上它设置为’value’。

不幸的是, 如果类使用魔术__get()函数检索属性值, 则没有万无一失的方法来检查该属性值是否为空。在课程范围之外, 你实际上只能检查是否会返回null值, 这并不一定意味着未设置相应的键, 因为它实际上可以设置为null。

相反, 如果我们尝试引用Regular类实例的不存在属性, 则会收到类似于以下内容的通知:

Notice: Undefined property: Regular::$nonExistantTest in /path/to/test.php on line 10

Call Stack:
    0.0012     234704   1. {main}() /path/to/test.php:0

因此, 这里的要点是, 应谨慎使用empty()方法, 因为如果不小心, 它可能会使结果变得混乱, 甚至可能引起误解。

本文总结

PHP的易用性会使开发人员产生一种错误的舒适感, 由于该语言的某些细微差别和特质, 他们很容易受到冗长的PHP调试的影响。这可能会导致PHP无法正常工作, 并导致诸如此处所述的问题。

PHP语言在其20年的发展历程中已经有了长足的发展。熟悉其精妙之处是一项值得努力的工作, 因为这将有助于确保你生产的软件具有更高的可伸缩性, 健壮性和可维护性。

赞(0)
未经允许不得转载:srcmini » Buggy PHP代码:PHP开发人员最常犯的10个错误

评论 抢沙发

评论前必须登录!