PHP的简化运算符

前言

最近在尝试做代码review,期间遇到了很多有趣的操作符。问题来了,写了这么多年的PHP语言,你对常见的简化运算符熟悉吗?我们一起来看看常见运算符吧。

三元运算符 ?:

在很多地方,我们会尝试使用三元运算符来简化代码,减少if的编写。对程序员来说,少敲一行代码,也是节省时间的。个人认为三元运算符最直接的作用是可以直接把多行代码简化为一行。

// 通常的写法
if (empty($_POST['action'])) {
    $action = 'default';
} else {
    $action = $_POST['action'];
}
// 使用三元运算符,只需要一行
$action = (empty($_POST['action'])) ? 'default' : $_POST['action'];

甚至三元运算符也可以使用简写的方式,但仅用在判断条件为表达式或者变量,如果是为isset这类bool时,只能应用在返回bool值。

// 简写
return true?:false;
// 表达式
$test1="test1";
$test3="test3";
// 返回test1
return $test1?:$test3;
// 返回test3
$test1=false;
return $test1?:$test3;
// 返回bool
$a = true;
return isset($a)?:'isset';

空值合并运算符 ??

在PHP的7.0版本里,为了解决isset在三元运算符的语法冗余问题,PHP又添加一个新操作符??。它是这样使用的。

$username = $_GET['user'] ?? 'nobody';
// 等同于下面的代码
$username = isset($_GET['user']) ? $_GET['user'] : 'nobody';
// 另外的例子
$model = Model::get($id) ?? $default_model;
// 等同于下面的代码
if (($model = Model::get($id)) === NULL) { $model = $default_model; }
// 再举一个例子
$imageData = json_decode(file_get_contents('php://input'));
$width = $imageData['width'] ?? 100;
// 等同于
$width = isset($imageData['width']) ? $imageData['width'] : 100;

空值合并赋值运算符 ??=

在解决上面的isset的判断问题后,发现有一些代码写起来还是很冗余。比如下面的代码。

// 例子
$this->request->data['comments']['user_id'] = $this->request->data['comments']['user_id'] ?? 'value';

没办法,为了写更少的代码,php开发组在PHP7.4添加新的赋值运算符??=。

// 简写
$this->request->data['comments']['user_id'] ??= 'value';

你看又一个语法糖诞生了,它并没有改变什么本质的东西,只是你要写的代码更少了。

组合比较运算符 <=>

像上面这类运算符,在7.0的版本也有,比如<=>。有时候,我们只是简单的想要知道两个变量的比较结果而已。

新运算符 (expr) <=> (expr),如果两边相等,就返回0,左边大于右边就返回1,右边大于左边就返回-1。

以下是该运算符其行为的示例:

// Integers
echo 1 <=> 1; // 0
echo 1 <=> 2; // -1
echo 2 <=> 1; // 1

// Floats
echo 1.5 <=> 1.5; // 0
echo 1.5 <=> 2.5; // -1
echo 2.5 <=> 1.5; // 1

// Strings
echo "a" <=> "a"; // 0
echo "a" <=> "b"; // -1
echo "b" <=> "a"; // 1

echo "a" <=> "aa"; // -1
echo "zz" <=> "aa"; // 1

// Arrays
echo [] <=> []; // 0
echo [1, 2, 3] <=> [1, 2, 3]; // 0
echo [1, 2, 3] <=> []; // 1
echo [1, 2, 3] <=> [1, 2, 1]; // 1
echo [1, 2, 3] <=> [1, 2, 4]; // -1

// Objects
$a = (object) ["a" => "b"]; 
$b = (object) ["a" => "b"]; 
echo $a <=> $b; // 0

$a = (object) ["a" => "b"]; 
$b = (object) ["a" => "c"]; 
echo $a <=> $b; // -1

$a = (object) ["a" => "c"]; 
$b = (object) ["a" => "b"]; 
echo $a <=> $b; // 1

$a = (object) ["a" => "b"]; 
$b = (object) ["b" => "b"]; 
echo $a <=> $b; // 0

该运算符与常用比较运算法 <, <=, ==, >= and >的比较如下:

operator <=> equivalent
$a < $b ($a <=> $b) === -1
$a <= $b ($a <=> $b) === -1 || ($a <=> $b) === 0
$a == $b ($a <=> $b) === 0
$a != $b ($a <=> $b) !== 0
$a >= $b ($a <=> $b) === 1 || ($a <=> $b) === 0
$a > $b ($a <=> $b) === 1

箭头函数第二版 =>

PHP在5.3的版本添加了闭包函数,你可以像下面这样来写闭包函数:

function array_values_from_keys($arr, $keys) {
    return array_map(function ($x) use ($arr) { return $arr[$x]; }, $keys);
}

看起来,写的代码有点多,有没有优化的空间呢?当然有,PHP在7.4版本优化了箭头函数,你可以像ES6一样编写箭头函数:

// 感觉少写很多代码
function array_values_from_keys($arr, $keys) {
    return array_map(fn($x) => $arr[$x], $keys);
}
// 举例
$y = 1;
$fn2 = function ($x) use ($y) {
    return $x + $y;
};
// 箭头函数
$y = 1;
$fn1 = fn($x) => $x + $y;
// 嵌套也是可以的
$z = 1;
$fn = fn($x) => fn($y) => $x * $y + $z;

解参数数组表达式 …

PHP为了”进一步”解放你的生产力,在7.4的版本还添加了和ES6类似的解参数运算符,但目前只能应用在数组里。

$parts = ['apple', 'pear'];
$fruits = ['banana', 'orange', ...$parts, 'watermelon'];
// 你得到一个新的数组 ['banana', 'orange', 'apple', 'pear', 'watermelon'];
$arr1 = [1, 2, 3];
$arr2 = [...$arr1]; //[1, 2, 3]
$arr3 = [0, ...$arr1]; //[0, 1, 2, 3]
$arr4 = array(...$arr1, ...$arr2, 111); //[1, 2, 3, 1, 2, 3, 111]
$arr5 = [...$arr1, ...$arr1]; //[1, 2, 3, 1, 2, 3]

平方运算符

最后,我甚至还在rfc发现平方运算符(从5.6起就可以使用),这下又少调用一个函数了。

echo 2 ** 3; // 8
$x = 2;
$x **= 3;
echo $x; // 8
// 支持GMP
$base = gmp_init(2);
$exponent = 3;
var_dump($base ** $exponent);

// output
object(GMP)#3 (1) {
  ["num"]=>
  string(1) "8"
}

总结

文章总结目前PHP最新版本支持的7个运算符,其本质就是语法糖,让你可以少写很多冗余的代码,让你的代码读起来简单。

但语法糖毕竟是糖,在吃的过程中很甜,甜完过后,并没有什么本质上改变。

其实我更希望能看到更多的”干货”。

参考链接

  1. 三元运算符 Ternary Operator
  2. 空值合并运算符 Null Coalesce Operator
  3. 空值合并赋值运算符 Null Coalescing Assignment Operator
  4. 组合比较运算符 Combined Comparison (Spaceship) Operator
  5. 箭头函数 v2Arrow Functions 2.0
  6. 解参数数组表达式 Spread Operator in Array Expression
  7. 平方运算符 Power Operator

研发环境那些事

前言

作为工程师,研发环境是日常研发工作必不可少的一个环节。什么是研发环境?我理解的研发环境,是除了生产环境(也有人叫线上环境,现网环境等)外的作为研发日常工作的环境。最常用的环境有3类:开发环境、测试环境、预发布环境。为了保证线上环境的稳定性,你可不想一不小心,就搞挂了生产环境,我们会隔离线上环境,独立出来对应的开发、测试、预发布等研发环境。

开发环境

开发环境是开发的日常开发工作都在这个环境进行。大部分的基础开发工作都会在该环境进行,你可以在本地搭建开发环境,也可以在本地开发完后,同步代码到服务器远程开发。

测试环境

所谓的测试环境,主要用于测试验证当前的需求开发是否符合预期。开发工程师完成需求开发后,将更新同步该环境,测试工程师进行需求验证。

预发布环境

所谓的预发布环境,其实就是真实的线上环境,几乎全部的环境配置都是一模一样的,包括但不限于,操作系统版本以及软件配置,开发语言的运行环境,数据库配置等。
最后上线前的验证环境,除了不对用户开放外,这个环境的数据和线上是一致的。产品、运营、测试、开发都可以尝试做最后的线上验证,提前发现问题。

环境对比

分类 使用场景 使用者 使用时机 备注
开发环境 日常开发 开发工程师 需求开发
测试环境 测试验证 开发、测试工程师 开发完成
预发布环境 线上验证 开发、测试、产品、运营等 上线之前

环境切换

将研发环境分为3类后,就会有切换的需求。一个需求从开发到上线,会涉及到开发->测试->预发布等步骤,对应的环境也需要进行变更。

最简单的方法是直接配置Host。我们都知道,计算机能识别的只有IP地址,IP地址由数字组成,而人类对数字的记忆,确实不在行。所以有了DNS系统,其核心是将对应的域名转换为IP地址。这样人类不用记忆复杂的数字,计算机也能正确得到IP地址。通常情况下,Windows和Linux 都可以直接修改hosts配置文件,前面是IP地址加一个空格,后面是域名地址。

常见的工具有SwitchHost,chrome插件Awesome Host Manager等。

也可以手动修改Host文件:

// Windows 需要管路员权限
C:\WINDOWS\system32\drivers\etc\hosts
// linux
/etc/hosts
// 配置示例
127.0.0.1       localhost

在我的大部分工作期间,也是这样过来的。配置Hosts文件的问题在于:

  1. 每次提测需求后,都需要邮件通知到测试相关人员,配置对应的Host。
  2. 有时候每次的地址还都不一样,更不用说无法解决移动端测试问题,手机测试移动网页是无法配置Host的。除非手机越狱或者有root的权限,进行一番复杂的操作才能配置Host。
  3. 随着开发人员的增多,可能会出现测试域名冲突,甚至还需要有api1.xbc.me、api2.xbc.me这样的域名存在。

在更复杂的网络环境,比如上网需要代理的情况,根本无法预知配置的Host是否生效的问题。所以,有没有简单一点办法?这类环境问题经常遇到,解决的办法还是有的。

多环境解析

我们是这样解决这个问题的。

首先,采用三级域名来区分不同环境的域名,比如测试环境和预发布环境的域名为:www.dev.xbc.me、www.test.xbc.me

其次,采用四级域名来区分不同的业务和访问客户端。

最后,我们预留了5级域名区分多个版本的情况。

以域名www.dev.xbc.me为例:

www对应的终端业务标识,这里代表了Web App,也就是我们常使用的台式机,使用Web浏览器访问网页。

dev对应是环境标识,一般有dev、test、pre等。

xbc.me对应是业务域名。

这里,我用一张表格来说明。

分类 终端/业务 对应域名 备注
开发环境 www www.dev.xbc.me PC端
app app.dev.xbc.me App接口
m m.dev.xbc.me 手机端
测试环境 www www.test.xbc.me PC端
app app.test.xbc.me App接口
m m.test.xbc.me 手机端

对应的Nginx配置如下:

server{
  listen      80;
  server_name ~^(?<version>.+)\.www\.(dev|test|pre)\.xbc\.me$;
  root        /var/www/vhosts/$version/public;
  location ~ \.php$ {
        ...
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;
        fastcgi_param  PATH_INFO          $fastcgi_path_info;
        fastcgi_param  PATH_TRANSLATED    $document_root$fastcgi_path_info;
        fastcgi_param  APP_ENV            'test'; # prd|pre|test|dev
        fastcgi_param  APP_VERSION        $version;
        include        fastcgi_params;
}
}
  1. server_name 采用正则去解析对应的域名,如:my.www.dev.xbc.me等。解析为my版本号,www为PC端,dev为开发环境。
  2. 添加环境和版本的常量 fastcgi_param APP_ENV APP_VERSION。
  3. 代码里定义相关环境常量。进行判断isset($_SERVER['APP_ENV']) isset($_SERVER['APP_VERSION'])

相关代码如下:

<?php
    // 常量为PRD/PRE/TEST/DEV
    if (isset($_SERVER['APP_ENV'])) {
      defined($env = strtoupper($_SERVER['APP_ENV'])) or define($env, true);
      unset($env, $_SERVER['APP_ENV']);
    }
    // 检查版本常量APP_VERSION是否定义,没有定义的话去默认值 "default"
    defined('APP_VERSION') or define('APP_VERSION', isset($_SERVER['APP_VERSION']) ? $_SERVER['APP_VERSION'] : 'default');

总结

文章总结日常开发中遇到的3类环境以及多环境的使用场景。

其中分析了配置Host的办法切换环境的优缺点,进而提出采用多级域名解决多人协作的环境问题。

但在云时代的现在,我们还需要这么多的环境吗?有没有办法解决多个环境搭建的难题呢?我觉得Docker可能是保证多个基础环境保证一致的有效技术手段,但如何将Docker技术应用起来又是另外一个话题了。

PHP自动加载

前言

在PHP里引用类,通常使用include,include_once,require,require_once这个几个函数引入PHP文件。但如果你有一堆classes需要引入呢?

自动加载

在PHP5.3以后,为了解决批量加载文件的问题,__autoload函数就出现了。__autoload是标准的SPL扩展函数,当调用一个不存在的类,会尝试从该函数进行加载。这也就是所谓的延迟加载。

简单的代码实现如下:

<?php
// php/src/Autoload/AutoCase.php
class AutoCase
{
    public static function run()
    {
        echo "run by __autoload." . PHP_EOL;
    }
}

测试用例:

<?php
// php/tests/autoload/case1.php
function __autoload($class)
{
    $dir = dirname(dirname(__DIR__));
    include_once $dir . '/src/Autoload/AutoCase.php';
}
AutoCase::run();

执行测试用例:

php php/tests/autoload/case1.php
# 输出
run by __autoload.

内部实现

实际上__autoload函数的内部实现是spl_autoload_register。
代码实现如下:

<?php
// php/src/Autoload/SplCase.php
class AutoCase
{
    public static function run()
    {
        echo "run by spl_autoload_register." . PHP_EOL;
    }
}

测试用例:

<?php
// php/tests/autoload/case2.php
spl_autoload_register(function($class)
{
    $dir = dirname(dirname(__DIR__));
    include_once $dir . '/src/Autoload/SplCase.php';
});
AutoCase::run();

执行测试用例:

php php/tests/autoload/case2.php
# 输出
run by spl_autoload_register.

那如果两个函数同时存在呢?会出现什么情况呢?

测试用例如下:

<?php
// php/tests/autoload/case3.php
function __autoload($class)
{
    $dir = dirname(dirname(__DIR__));
    include_once $dir . '/src/Autoload/AutoCase.php';
}
spl_autoload_register(function($class)
{
    $dir = dirname(dirname(__DIR__));
    include_once $dir . '/src/Autoload/SplCase.php';
});
AutoCase::run();

执行测试用例:

php php/tests/autoload/case3.php
# 输出
run by spl_autoload_register.

事实上,spl_autoload_register函数默认会覆盖__autoload加载规则。

为什么要使用spl_autoload_register

首先,PHP官方在7.2已经把__autoload 变为DEPRECATED,也就是废弃的状态,后续该函数肯定会被删除的。
其次,spl_autoload_register函数可以有多个自动加载函数,更加灵活地加载自定义类。而__autoload 只能定义一次,只能有一个加载函数。
最后,spl_autoload_register与__autoload 同时存在的话,spl_autoload_register函数默认会覆盖__autoload加载规则。

spl_autoload_register应用

spl_autoload_register常用于多个类的加载,在composer里,composer install 后会生成一个vendor目录,有一个autoload.php。源代码如下:

<?php
// vendor/autoload.php
// autoload.php @generated by Composer

require_once __DIR__ . '/composer/autoload_real.php';

return ComposerAutoloaderInitc8bbd05443dfc4a7281619cb2b14f876::getLoader();

实际上的加载规则在autoload_real.php和ClassLoader.php

<?php
// vendor/composer/autoload_real.php
<?php

// autoload_real.php @generated by Composer

class ComposerAutoloaderInitc8bbd05443dfc4a7281619cb2b14f876
{
    public static function loadClassLoader($class)
    {
        if ('Composer\Autoload\ClassLoader' === $class) {
            require __DIR__ . '/ClassLoader.php';
        }
    }
    public static function getLoader()
    {
        if (null !== self::$loader) {
            return self::$loader;
        }
                  spl_autoload_register(array('ComposerAutoloaderInitc8bbd05443dfc4a7281619cb2b14f876', 'loadClassLoader'), true, true);
        self::$loader = $loader = new \Composer\Autoload\ClassLoader();
        // bala bala
        $loader->register(true);
    }
}

class ClassLoader
{
   /**
     * Registers this instance as an autoloader.
     *
     * @param bool $prepend Whether to prepend the autoloader or not
     */
    public function register($prepend = false)
    {
        spl_autoload_register(array($this, 'loadClass'), true, $prepend);
    }
}

总结

我们对比了PHP的spl_autoload_register与__autoload 加载的实现。在后续的版本中,__autoload 实现是应该要废弃的。spl_autoload_register与__autoload 同时存在的话,spl_autoload_register函数默认会覆盖__autoload加载规则。我把上面的用例相关的代码在GitHub版本库,你可以随时查看。