PHP 开发过程不可忽略的技巧

如何定义一个函数

函数的参数

1
2
3
4
5
6
function myReduce($first, $second)
{
return $first - $second;
}

echo myReduce(10,5);

乍看!这段代码没毛病啊!老铁!那我们继续看!

1
echo myReduce('laotie','meimaobing');

这明显是你传递的参数不正确!怎能怪我写的代码呢!调整好心态。写出好的代码是一个学无止境的过程!

但是我们怎么才能让一个函数严格接收能使其正确执行的参数呢? 现代的 PHP 解决了这个问题,并且有更多妙法能让你的代码质量更进一层,没有 bug。

你可以严格控制你的函数,使其只接收让它正确运行的参数。让我们改变上面的函数定义:

1
2
3
4
function myReduce(int $first, int $second)
{
return $first - $second;
}

如果传递一个非int类型参数给myReduce()则会报一个错误!大大杜绝了有时候无法复现的BUG

函数与返回值

php 逐渐增加了对返回类型的设置

1
2
3
4
function myPlus(int $first, int $second) : int
{
return $first + $second;
}

可选参数、可控参数

除了可选参数外,你还可以定义可空(nullable)参数,这意味着你可以定义一种可空参数类型。我们来看个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function nullableParameter(?string $name)
{
return $name;
}
echo nullableParameter(null); // 不会返回任何东西
echo nullableParameter('Nauman'); // Nauman
echo nullableParameter(); // 致命错误
function nullableParameterWithReturnType(?string $name) : string
{
return $name;
}
echo nullableParameter(null); // 致命错误,必须返回 string 类型
echo nullableParameter('Nauman'); // Nauman
function nullableReturnType(string $name) : ?string
{
return $name;
}
echo nullableParameter(null); // 致命错误,$name 应该是 string 类型
echo nullableParameter('Nauman'); // Nauman

在循环中执行查询与内存管理选择

循环中执行查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$connection = new mysqli('localhost', 'username', 'password', 'database');

$sql = <<<SQLCODE
SELECT
id,
name
FROM
users
WHERE
cell =
SQLCODE;

//多个手机号Array
$cells =[...];

foreach($cells as $cell){
$data[] = $connection->query($sql.$cell);
}

每个结果都会对数据库执行一次查询,如果数组中数据很大,那将对资源产生很多个单独的请求!如果这个脚本在多线程中被多次调用,则有系统崩溃的风险!因此,至关重要的是在对资源进行查询时候,尽可能的搜集查询条件,一次性的查询出所有结果。

以上代码优化方式

1
2
3
4
5
6
7
8
9
10
11
12
$sql = <<<SQLCODE
SELECT
id,
name
FROM
users
WHERE
cell
IN
SQLCODE;

$data = $connection->query($sql.'('.implode(',', $cells).')');

SQL语句的区别,IN的执行效率在新版本Mysql中性能有很大的提升。

内存优化

如果一次性查出数以百万计的数据,那则会对内存造成很大的压力,会造成内存方面的报错!

一次取多条记录肯定是比一条条的取高效,但是当我们使用 PHP 的 mysql 扩展的时候,这也可能成为一个导致 libmysqlclient 出现『内存不足』(out of memory)的条件。

我们在一个测试盒里演示一下,该测试盒的环境是:有限的内存(512MB RAM),MySQL,和 php-cli。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 连接 mysql
$connection = new mysqli('localhost', 'username', 'password', 'database');

// 创建 400 个字段
$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);

// 写入 2 百万行数据
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);
}

执行查询,查看内存使用情况。

1
2
3
4
5
6
7
8
9
// 连接 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";

输出结果

1
2
3
Before: 224704
Limit 1: 224704
Limit 10000: 224704

Cool。 看来就内存使用而言,内部安全地管理了这个查询的内存。

为了更加明确这一点,我们把限制提高一倍,使其达到 100,000。 额~如果真这么干了,我们将会得到如下结果:

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

WTF!!!

这就涉及到 PHP 的 mysql 模块的工作方式的问题了。它其实只是个 libmysqlclient 的代理,专门负责干脏活累活。每查出一部分数据后,它就立即把数据放入内存中。由于这块内存还没被 PHP 管理,所以,当我们在查询里增加限制的数量的时候, memory_get_peak_usage() 不会显示任何增加的资源使用情况 。我们被『内存管理没问题』这种自满的思想所欺骗了,所以才会导致上面的演示出现那种问题。 老实说,我们的内存管理确实是有缺陷的,并且我们也会遇到如上所示的问题。

如果使用 mysqlnd 模块的话,你至少可以避免上面那种欺骗(尽管它自身并不会提升你的内存利用率)。mysqlnd 被编译成原生的 PHP 扩展,并且确实 会 使用 PHP 的内存管理器。

因此,如果使用 mysqlnd 而不是 mysql,我们将会得到更真实的内存利用率的信息:

1
2
3
Before: 232048
Limit 1: 324952
Limit 10000: 32572912

顺便一提,这比刚才更糟糕。根据 PHP 的文档所说,mysql 使用 mysqlnd 两倍的内存来存储数据, 所以,原来使用 mysql 那个脚本真正使用的内存比这里显示的更多(大约是两倍)。

为了避免出现这种问题,考虑限制一下你查询的数量,使用一个较小的数字来循环,像这样:

1
2
3
4
5
6
7
8
$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 无法正常运行。

$_POST全局变量注意点

有时候使用$_POST获取前端传递过来的数据可能会获取不到!看下面这段代码例子🌰

1
2
3
4
5
6
7
// js
$.ajax({
url: 'https://blog.happyhacn.cn/举个/🌰',
method: 'post',
data: JSON.stringify({a: 'a', b: 'b'}),
contentType: 'application/json'
});

顺带一提,注意这里的 contentType: 'application/json' 。我们用 JSON 类型发送数据,这在接口中非常流行。这在 AngularJS $http service 里是默认的发送数据的类型。

在我们举例子的服务端,我们简单的打印一下 $_POST 数组:

1
var_dump($_POST);

WTF!

1
array(0) { }

为什么?我们的 JSON{a: 'a', b: 'b'} 究竟发生了什么?

原因在于 当内容类型为 application/x-www-form-urlencoded 或者 multipart/form-data 的时候 PHP 只会自动解析一个 POST 的有效内容。这里面有历史的原因 — 这两种内容类型是在 PHP 的 $_POST 实现前就已经在使用了的两个重要的类型。所以不管使用其他任何内容类型 (即使是那些现在很流行的,像 application/json), PHP 也不会自动加载到 POST 的有效内容。

既然 $_POST 是一个超级全局变量,如果我们重写 一次 (在我们的脚本里尽可能早的),被修改的值(包括 POST 的有效内容)将可以在我们的代码里被引用。这很重要因为 $_POST 已经被 PHP 框架和几乎所有的自定义的脚本普遍使用来获取和传递请求数据。

所以,举个例子,当处理一个内容类型为 application/json 的 POST 有效内容的时候 ,我们需要手动解析请求内容(decode 出 JSON 数据)并且覆盖 $_POST 变量,如下:

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

Bingo!

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

慎用empty()判断数组是否为空

我之前几乎所有的事情使用 empty() 做布尔值检验。不过,在一些情况下,这会导致混乱。

首先,让我们回到数组和 ArrayObject 实例(和数组类似)。考虑到他们的相似性,很容易假设它们的行为是相同的。然而,事实证明这是一个危险的假设。举例,在 PHP 5.0 中:

1
2
3
4
5
6
// PHP 5.0 或后续版本:
$array = [];
var_dump(empty($array)); // 输出 bool(true)
$array = new ArrayObject();
var_dump(empty($array)); // 输出 bool(false)
// 为什么这两种方法不产生相同的输出呢?

更糟糕的是,PHP 5.0之前的结果可能是不同的:

1
2
3
4
5
// PHP 5.0 之前:
$array = [];
var_dump(empty($array)); // 输出 bool(false)
$array = new ArrayObject();
var_dump(empty($array)); // 输出 bool(false)

这种方法上的不幸是十分普遍的。比如,在 Zend Framework 2 下的 Zend\Db\TableGatewayTableGateway::select() 结果中调用 current() 时返回数据的方式,正如文档所表明的那样。开发者很容易就会变成此类数据错误的受害者。

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

1
2
3
4
5
// 注意这会在 PHP 的所有版本中发挥作用 (5.0 前后都是):
$array = [];
var_dump(count($array)); // 输出 int(0)
$array = new ArrayObject();
var_dump(count($array)); // 输出 int(0)

顺便说一句, 由于 PHP 将 0 转换为 false , count() 能够被使用在 if() 条件内部去检验空数组。同样值得注意的是,在 PHP 中, count() 在数组中是常量复杂度 (O(1) 操作) ,这更清晰的表明它是正确的选择。

另一个使用 empty() 产生危险的例子是当它和魔术方法 _get() 一起使用。我们来定义两个类并使其都有一个 test 属性。

首先我们定义包含 test 公共属性的 Regular 类。

1
2
3
4
class Regular
{
public $test = 'value';
}

然后我们定义 Magic 类,这里使用魔术方法 __get() 来操作去访问它的 test 属性:

1
2
3
4
5
6
7
8
9
10
11
class Magic
{
private $values = ['test' => 'value'];

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

好了,现在我们尝试去访问每个类中的 test 属性看看会发生什么:

1
2
3
4
$regular = new Regular();
var_dump($regular->test); // 输出 string(4) "value"
$magic = new Magic();
var_dump($magic->test); // 输出 string(4) "value"

到目前为止还好。

但是现在当我们对其中的每一个都调用 empty() ,让我们看看会发生什么:

1
2
var_dump(empty($regular->test));    // 输出 bool(false)
var_dump(empty($magic->test)); // 输出 bool(true)

咳。所以如果我们依赖 empty() ,我们很可能误认为 $magic 的属性 test 是空的,而实际上它被设置为 'value'

不幸的是,如果类使用魔术方法 __get() 来获取属性值,那么就没有万无一失的方法来检查该属性值是否为空。
在类的作用域之外,你仅仅只能检查是否将返回一个 null 值,这并不意味着没有设置相应的键,因为它实际上还可能被设置为 null

相反,如果我们试图去引用 Regular类实例中不存在的属性,我们将得到一个类似于以下内容的通知:

1
2
3
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() 方法应该被谨慎地使用,因为如果不小心的话它可能导致混乱 – 甚至潜在的误导 – 结果。

鸣谢

转载自:

  1. laravel-china
  2. 知乎

没事多逛逛技术论坛会有很大的收获!