分类 技术 下的文章

在这篇文章中,我将从头梳理PHP开发中 Exception 这个关键字的周边知识,争取梳理清楚如下的几个问题
1.什么是异常
2.异常从哪里来
3.异常应该到哪里去
4.异常之分门别类
5.异常和错误的区别和联系
6.异常之于业务场景

什么是异常

在我们开始所有解释之前,我么首先举个栗子.
假设你要按照给定id来查找humans表的用户邮箱,这个功能可以写成这样:

function getHumanEmailById($id)
{
    return \Humans::whereId($id)->email;
}

但是这绝对不是一个健壮的代码,一旦用户传递一个不存在的id(数据库不存在),或者传递是压根就不是int型的正整数,程序就会进入: PHP Notice 状态,导致整个程序进入非预期流程,这里的非预期流程可能是个php层的警告,也可能是个fatal error(假设方法的实现还有更加致命的逻辑假设),导致程序异常终止,所以这时候需要这个方法告诉调用方你的参数有误这种信息,这个时候通常我们的做法有两种.
1.

if (##如果id非法) {return '';}

2.

if (##如果id非法) {throw new \InvalidArgumentException("参数非法");}

而第二种做法就是我们讨论的Exception,好了,到这里我们只是讨论清楚了抛出异常的必要性,但是就如上面两个例子看到的,调用方调用完毕这个方法怎么按照你给的信号(throw 出来的exception)做出合理的处理呢?

异常从哪里来?

Q:上面说清楚了异常的必要性以及可能性,那么异常从哪里来呢?
A:异常来自开发者对整体代码逻辑的非预期结果给出的提示.
所以简单来说,异常是PHP代码层抛出的.

异常应该到哪里去

上面已经定义了function getHumanEmailById,同时对参数的非法性做了异常的抛出,但是我们不知道异常到哪里去了,我们尝试调用它getHumanEmailById(1),这里假设id是1的human信息在数据库中已经被删除,看看php执行器返回结果:

Fatal error: Uncaught InvalidArgumentException: 参数非法

这里发现,我们抛出的异常居然变成了Fatal error,所以异常抛出后交给了PHP解释器,解释器没有找到 catch 这个异常的逻辑代码,所以直接fatal error,说明这一点,我们继续修改代码

    try{
        getHumanEmailById(1);
    }catch (\Exception $e)
    {
        echo $e->getMessage();
    }

执行结果正常输出: 参数非法
到此,我们解释清楚了异常应该到哪里去的问题

异常之分门别类

也许你留意到上面的代码没有catch InvalidArgumentException,是因为ExceptionInvalidArgumentException的父类,因为异常的类型很多,我们有一些需求确实需要根据不同的异常做不同的事情,例如下面的伪代码:

try {
    $variable = 'string';
} catch (MyException $e) {
    ##todo1
} catch (YourException $e) {
    ##todo2
} catch (OurException $e) {
    ##todo3
} catch (Exception $e) {
    echo "异常信息:" . $e->getMessage();
}

如果你想了解更多的异常分类,可以查看php手册,也可以通过执行 这个脚本 来打印异常分类.

异常和错误的区别和联系

这个主题,我想只有PHP官方php5时代的开发才能解释清楚这一点,通俗的来说,错误可能是不可修复的,当然现在的php7版本已经将error和exception统一了,他们都集成自throwable这个interface.具体的关系如图:
继承关系

php7一下的话,我们可以尝试将error转为exception,具体实现代码:

protected function registerErrorHandler()
{
    set_error_handler(array($this, 'handleError'));
}

public function handleError($level, $message, $file, $line, $context)
{
    if (error_reporting() & $level)
    {
        throw new ErrorException($message, $level, 0, $file, $line);
    }
}

更多是实现方案参照 laravel的exceptionHandler

异常之于业务场景

特定一类throwable统一输出json

最后,我们回归到上面最初的代码,如果human的id是非法的,就抛出了异常,假设这个id恰好是业务前端传递的,我们就需要告诉用户这个id是非法的,明确告诉他非法请求.
实现的逻辑代码大致为:

        try{
            getHumanEmailById(1);
        }catch (\InvalidArgumentException $e)
        {
            echo json_encode([
                'code' => 444, 
                'msg' => "参数id错误: " . $e->getMessage(), 
                'data' => []
            ]);
        }

如果这中参数对应的msg想统一起来,且前端提交的参数非常多,都需要这样判断呢? 这里我们可以抽象出有一个类,继承自InvalidArgumentException的类ApiInvalidArgumentException,然后统一在上层捕获这个异常,然后统一输出json格式
这里的最佳实践可以在laravel中看到影子,大致思路如下:
1.继承IlluminateFoundationExceptionsHandler::render($request, Exception $e)
2.ApiInvalidArgumentException类定义toJson方法
3.拿到$e,特判ApiInvalidArgumentException,return出tojson

如此对业务代码无侵入,看起来干净且明了

将没有catch的异常介入第三方统计

线上在所难免的会有一些异常是未捕获的,这时php会将信息直接fatal error,并输出堆栈信息,所以我们可以将这些信息介入第三方,实时发消息给开发者,解决和发现线上问题.
推荐大家使用sentry作为php异常处理和发现的工具,很强大,目前我们团队线上就在大量使用.

总结

因为我们的业务需要判断和处理太多太多不符合预期的结果了,有时候一个鲁棒性强的代码可能是核心业务代码的2-3倍,这个时候如果我们能够很好的利用exception,既能让代码健壮,又能让健壮性判断可读性高,例如我们可以二次封装if判断,转变为assert断言,然后在断言中抛出异常.现在很多第三方类库,都很好的定义了自己的异常,为的就是健壮和可读性,希望通过这篇文章,大家能够收获一些新的心得体会,也希望你能斧正文章的错误,下篇博客见!

短信

上面是典型的电商运营短信,为了最大化营销效果,那些丑陋的二级甚至更多级的域名,加上一堆来源参数的的url,如果加在短信内容中,显得格格不入,这个时候短链接(例如这里的t.cn/EfbKGhL)的需求诞生了.

短链接应用

丑陋的url地址: h5.stock.xueshop.cn/callback/resouce/index.php?from=sms&device=iphonex&phone=12345342345&goto=www.newname.com

好看的url地址: t.cn/xeXZR4

原理

如果落地页需要一致,就需要上面两个url有1N1的映射关系,所以只要做到这个映射关系,就解决了转换地址的问题,剩余的就是服务器端做302跳转.

实现方案

1.进制算法
我们知道计算机的二进制可以表达字符,字符串,那么我们可以把一个长url计算成二级制,这样就解决了二进制数字是不可碰撞的,随后我们需要解决二进制长度太长的问题,进制转换有个特性:越往大进制转,表达式越短,例如:10001转为10进制就是:17, 转成:16进制就是:11,转成64进制就是:R,所以我们解决转换成大进制即可完成简短的功能,但是这个进制怎么转换呢? 我们可以这样考虑 [a-zA-Z0-9] 这个区间的字符组成的字符串,我们把他定义为:62进制.
举个栗子:

二进制62进制
0a
26z
27A
629
639a

这样一个6位数的62进制可以表达 62的6次方(0-56800235583) 个url

您可能已经知道PHP中的一些比较运算符。比如三元?:,空合并??以及宇宙飞船的操作符 <=>。但你真的知道它们是如何工作的吗?了解这些运算符会使您更多地使用它们,从而产生更清晰的代码。

三元运算符

三元运算符是为了简化 if {} else {} 结构, 例如我们如下的代码:

if ($condition) {
    $result = 'foo' 
} else {
    $result = 'bar'
}

你可以这样写:

$result = $condition ? "foo" : "bar";

如果上面代码中的$condition的值是true, 左边的值就赋值给$result,如果值是false, 右边的值将会用于$condition.

一个有趣的事实: 三元运算符的名称实际上是指"作用于三个操作数的运算符".运算符是可以通过给出的一或多个值(用编程行话来说,表达式)来产生另一个值(因而整个结构成为一个表达式)的东西。

运算符可按照其能接受几个值来分组。一元运算符只能接受一个值,例如 !(逻辑取反运算符)或 ++(递增运算符)。 二元运算符可接受两个值,例如熟悉的算术运算符 +(加)和 -(减),大多数 PHP 运算符都是这种。最后是唯一的三元运算符 ? :,可接受三个值;通常就简单称之为“三元运算符”(尽管称之为条件运算符可能更合适)。

关于这些知识,官方手册是系统的学习这些知识的渠道, 这是地址

回头我们继续三目运算符: 你知道哪些表达式是true, 哪些表达式是false吗?你可以从这里得到答案 这是地址

当条件的计算结果为真时,三元运算符将使用其左边操作数。这可以是字符串、整数、布尔值等。右侧操作数将用于所谓的“错误值”。例如0或“0”、空数组或字符串、空值、未定义或未分配的变量,当然还有false本身。所有这些值都将使三元运算符使用其右手操作数。

更加简短的三目运算表达式

php5.3,你可以省略左侧的操作数,例如:

$result = $initial ?: 'default';

在这种情况下,$result的值将是$initial的值,除非$initial的计算结果为false,在这种情况下,将使用字符串default

当然你可以将这种场景的代码按照正常的三目运算表达式:

$result = $condition ? $condition : 'default';

但是这里特定意义上来说,简化的三目运算表达式,变成了二元运算符.

链式三目运算符

如下的代码,看起来很符合逻辑,但是他在PHP中不能按照常理输出内容.

$firstCondition = $elseCondition = true;
$result = $firstCondition
    ? 'truth'
    : $elseCondition
        ? 'elseTrue'
        : 'elseFalse';

原因是PHP中的三元运算符是左相关的,因此以一种非常奇怪的方式进行解析:上面的示例总是首先判断$elsecondition部分,因此即使$firstCondition为true,也不会看到它的输出.

所以上面的代码输出: elseTrue

关于这一点你可以, 点击进入stackoverflow学习

值得注意的是,在php7.4版本中,官方已经deprecated了不带括号的三目运算,这里是更详实的资料

空合并运算符

你以前看过类型比较表吗?从php 7.0开始,可以使用空合并运算符。它类似于三元运算符,但其行为类似于左侧操作数上的ISSET,而不仅仅是使用其布尔值。这使得该运算符对于数组和未设置变量时指定默认值特别有用。

$undefined ?? 'fallback'; // 'fallback'

$unassigned;
$unassigned ?? 'fallback'; // 'fallback'

$assigned = 'foo';
$assigned ?? 'fallback'; // 'foo'

'' ?? 'fallback'; // ''
'foo' ?? 'fallback'; // 'foo'
'0' ?? 'fallback'; // '0'
0 ?? 'fallback'; // 0
false ?? 'fallback'; // false

空合并操作符在数组上的使用

此运算符与数组结合使用时特别有用,因为它的行为类似于isset。这意味着您可以快速检查键的存在,甚至是嵌套键,而无需编写详细的表达式。

$input = [
    'key' => 'key',
    'nested' => [
        'key' => true
    ]
];

$input['key'] ?? 'fallback'; // 'key'
$input['nested']['key'] ?? 'fallback'; // true
$input['undefined'] ?? 'fallback'; // 'fallback'
$input['nested']['undefined'] ?? 'fallback'; // 'fallback'

null ?? 'fallback'; // 'fallback'

这里举个栗子,上面第一行的表达式$input['key'] ?? 'fallback'; 我们比较传统(low)一些的做法是:

$output = isset($input['key']) ? $input['key'] : 'fallback';

请注意,在检查数组键是否存在时,不可能使用简化三元运算符。它将触发错误或返回布尔值,而不是实际的左操作数的值。
举个栗子:

//特别注意,这里返回的是true而不是key键对应的值
$output = isset($input['key']) ?: 'fallback' 
//这里如果没有key值,将会触发一个 `undefined index` 错误
$output = $input['key'] ?: 'fallback';

链式操作空合并操作符

与三元运算符一样,也可以链接空合并运算符。它的语法比三元的要简单得多。

$input = [
    'key' => 'key',
];

$input['undefined'] ?? $input['key'] ?? 'fallback'; // 'key'

空合并分配运算符

在php 7,4中,我们可以期待一种更简短的语法,称为“空合并赋值操作符”。

// 这个操作符在7.4中才生效

function (array $parameters = []) {
    $parameters['property'] ??= 'default';
}

在此示例中,$parameters['property']将被设置为“default”,除非它在传递给函数的数组中设置。这相当于使用当前的空合并运算符执行以下操作:

function (array $parameters = []) {
    $parameters['property'] = $parameters['property'] ?? 'default';
}

太空操作符

太空船操作符,虽然有一个很特别的名字,但可能非常有用。它是用于比较的运算符。它将始终返回三个值之一:0、-1或1。

当两个值完全相等时,返回0, 左边的大返回1, 右边的大返回-1.
举个栗子:

1 <=> 2; // 返回 -1

它还不止这么简单的比较数字,还有其他的,比如:

// 比较字符串
'a' <=> 'z'; // -1

// 数组
[2, 1] <=> [2, 1]; // 0

// 多维数组
[[1, 2], [2, 2]] <=> [[1, 2], [1, 2]]; // 1

// 甚至大小写 
'Z' <=> 'z'; // -1

奇怪的是,在比较字母大小写时,小写字母被认为是最高的。不过,有一个简单的解释:字符串比较是通过每个字符比较字符来完成的。一旦一个字符不同,就会比较它们的ASCII值。因为在ASCII表中,小写字母排在大写字母之后,所以它们的值更高。

比较对象

其实我感觉对象比较就有些过分了,毕竟对象作为复杂的数据结构,比较的基础是什么?但是这里可以提到一点,下面的做法有时又很有用,如下:

$datea=DateTime::createFromFormat('y-m-d', '2000-02-01');
$dateb=DateTime::createFromFormat('y-m-d', '2000-01-01');
$datea<=>$dateb;//返回1

当然,比较日期只是一个例子,但仍然是一个非常有用的例子。

排序函数

这个运算符的一个重要用途是对数组进行排序。在PHP中对数组进行排序有很多种方法,其中一些方法允许使用用户定义的排序函数。此函数必须比较两个元素,并根据它们的位置返回1、0或-1。

$array = [5, 1, 6, 3];

usort($array, function ($a, $b) {
    return $a <=> $b;
});

// $array = [1, 3, 5, 6];

逆序排列:

usort($array, function ($a, $b) {
    return -($a <=> $b);
});

好了,至此,php的这些新特性介绍完毕,相信它对你的代码的优雅型有一些提高,谢谢阅读!

为啥要回滚

之前mac上一直是php最新版本,过年前为了测试php7.3新特性,升级到7.3, 可是公司的项目构建在php7.0上,每次composer update都会改写composer.lock对php版本的限制.
为此决定回滚php版本到php7.0,但是这个过程比我想象的难

如果不回滚版本,有办法解决composer update的问题吗?

- 阅读剩余部分 -

trait

1.用于在类中使用$this->{trait的method},方便代码插入,按照我的理解使用trait解决单继承问题,同事将一个类中的方法统一放入trait,即便于管理和书写逻辑,又方便调用,当然完全可以定义类,定义public static function的方式
2.trait的属性private,public其他类均可调用