若转载教程,请注明出自SW-X框架官方文档

一、数据类型的强制、严格模式

0、前言,PHP7的一些介绍

众所周知,PHP是一门弱类型语言,实际上PHP在5.*版本时,也是支持伪强类型的。
为什么说是伪强类型呢,那是因为那时候的强类型,不过是使用函数强制转换类型,并不依赖HHVM转码。
而PHP7后,PHP迎来了真正的强类型模式与严格类型模式。
PHP7有加入几十个功能,最显著的是下面提到 -
1、改进的性能 - PHPNG代码合并在PHP7中,这是比 PHP5快两倍;
2、降低内存消耗 - 优化后PHP7使用较少的资源;
3、标量类型声明 - 现在,参数和返回值类型可以被强制执行;
4、一致性的64位支持 - 64位架构机器持续支持;
5、改进异常层次结构 - 异常层次结构得到改善;
6、许多致命错误转换成异常 - 异常的范围增大覆盖为许多致命的错误转化异常;
7、安全随机数发生器 - 加入新的安全随机数生成器的API;
8、已过时的API和扩展删除 - 不同的旧的和不支持的应用程序和扩展,从最新的版本中删除;
9、null合并运算符(??)的新空合并运算符被加入;
10、返回和标量类型声明支持返回类型和参数类型也被加入;
11、增加了对匿名匿名类的支持;
12、零成本声明支持零成本加入断言。

1、什么是强制模式?

例如下面的代码:

  1. <?php
  2. function demo1 (int $num) {
  3. echo $num;
  4. }
  5. demo1(4.1);

你觉得会输出什么呢?
实际上,PHP7通过HHVM字节码,将小数强制转换成了整数,跟php5.*不同的是,前置用的是函数转换,后者用的是字节码转换

2、什么是严格模式?

严格模式早在类似Java这样的编程语言中早已使用了,总体来说,就是强制效验变量的传参类型。
我们在代码中,通过使用
declare(strict_types=1);
开启严格模式;
1:表示严格类型效验模式,作用于函数调用和返回语句;
0:表示弱列席效验模式。
PHP7支持的类型限制分别有:
1、字符串(string),
2、整数 (int),
3、浮点数 (float),
4、布尔值 (bool)
5、接口(interfaces)
6、类(类名)
7、数组(array)
8、回调(callable)
接下来,我们将上面的函数改写严格模式进行测试:

<?php
declare(strict_types=1);
function demo2(int $num) {
    echo $num;
}
demo2(4.1);

我们就会得到
Fatal error: Uncaught TypeError: Argument 1 passed to demo2() must be of the type integer, float given, called in
的错误提示,就跟Java一样,我们大PHP也正式支持严格数据类型啦。
同时注意,严格模式只支持 函数调用和返回语句,所以下面的模式,还是会沿用PHP5.*的函数强行转换。

<?php
declare(strict_types=1);
$num = 4.1;
$demo3 = (int)$num;
echo $demo3;

而且同学们注意,在PHP7中使用严格模式下的强类型限制,可以提供PHP的解析性能哦,不同的可以百度下HHVM字节码的解析原理。
同时,declare(strict_types=1);,必须放置在<?php的下一行,再它之前,不能有其他的代码或输出。

二、限制函数的返回类型

其实在PHP5.*之后,已经支持function声明返回的数据类型,不过很少有人使用而已。
放在PHP7的特性里也算是让不同的同学们学习一遍吧。

# 开启严格模式
declare(strict_types = 1);
# 返回数字类型
function demo() : int {
    return 10;
}
echo demo();
# 定义一个测试的class
class Eco{}
# 返回Eco的实例
function test() : Eco {
    return new Eco();
}
var_dump(test());

三、空并运算符

空合并运算符(??)是代替 ?: 与 isset()函数的结合。
如果它存在并且不为null,空合并运算符返回它的第一个操作数;否则返回第二个操作数。
例子如下:

<?php
# 注意,在PHP中,null 并不绝对等于 ''
$test = null;
echo isset($test) ? $test : '我爱小黄牛<br/>';
echo !empty($test) ? $test : '我爱小黄牛<br/>';

如果上面的逻辑使用空并运算符,可以这样实现:

<?php
$test = null;
echo $test ?? '我爱小黄牛<br/>';
# 当$test为null时,上面三者的判断是等价的
# 当$test为''时,第一和第三条判断是等价的

解释:当$test不为null时,返回$test,否则返回 ?? 后面的参数;
同时,??空并运算符也是可以两个同时使用的,类型三元运算符,
假设我们要用空并运算符,替代下面的语句;
return isset($_GET[‘id’]) ? $_GET[‘id’] : ‘变量不存在’;
就可以这样写:
return $_GET[‘id’] ?? $_GET[‘id’] ?? ‘变量不存在’;

四、飞船操作符

飞船操作符( <=> ),这是PHP7的一个全新特征,
它是用于比较两个表达式。
当第一个表达式 比 第二个表达式分别
小于(return -1)
等于(return 0)
大于(return 1)
下面我们来编写一个demo进行测试学习:

echo 1  <=> 10;echo '<br/>'; // 左边小于右边,将返回-1
echo 2  <=> 2;echo '<br/>'; // 左边等于右边,将返回0
echo 10 <=> 2;echo '<br/>'; // 左边大于右边,将返回1
echo true <=> false; echo '<br/>'; // true大于false,将返回1
echo '我爱' <=> '小黄牛'; echo '<br/>'; // 字符串与字符串比较,只会得1

五、常量数组

在PHP5.6+时,定义一个数组常量并需要 const 关键字定义。
但从PHP7开始后,已经支持使用define()函数定义数组常量了。
具体用法如下:

<?php
<?php
# 我们来测试下
define('test', [
      '我',
      '爱',
      '小',
      '黄',
      '牛',
]);
echo '<pre>';
var_dump(test);
echo '</pre>';

六、匿名类

在PHP7中有一个全新好用的新特性,就是匿名类,它的定位在用完即销的场景下。
匿名类使用 new class 关键字定义。
注意:匿名函数也是可以继承对象 或者 继承接口的。
下面我们来编写一个匿名类继承父类,并且赋值给变量调用的DEMO:

<?php
class Test {
    public function __construct() {
        echo '我是父类粑粑<br/>';
    }
}
# 我们来测试下
$app = (new class extends Test{ 
    public function log(string $msg) { 
       print($msg); 
    } 
 })->log("我爱小黄牛<br/>"); 
 # 注意,向变量或是单独定义匿名类时,一定要使用()符号将实例囊括,否则将会是错误的语法。

七、安全随机数

PHP7新加了2个随机函数,用于替换rand()和mt_rand()的使用:
random_bytes() - 生成加密安全伪随机字节。适合于使用来生成密码,密码学随机的任意长度的字符串,如:生成 salt,密钥或初始向量。
random_int() - 生成加密安全伪随机整数。
下面我们来进行测试调用:

<?php
echo bin2hex(random_bytes(5));
echo '<br/>';
echo random_int(1, 100);

八、整数除法 与 Session新选项

PHP7新加了1个整除函数:
intdiv(), 整数除法并返回结果为 int 类型。

<?php
# 我们来测试下
echo intdiv(100, 3);
# 将输出33

同时,PHP7中对Session选项也有一些改进。
从PHP7+开始,session_start()函数接受数组参数覆盖在php.ini中设置的会话配置指令。这些选项支持 session.lazy,在默认情况下如果PHP会话数据改变,那么会覆盖任何会话数据信息。
所以PHP7添加了另一种选择:read_and_close选项,这表明会话数据应被读取,然后该会话应当立即被关闭不变。例如,session.cache_limiter选项设置为私有,并设置标志使用下面的代码片段之后立即关闭会话。

<?php
session_start([
   'cache_limiter' => 'private',
   'read_and_close' => true,
]);
?>

九、更方便的错误捕捉机制

0、全新的PHP错误异常机制

过去的 PHP,处理一些致命错误几乎是不可能的。致命错误不会调用由 set_error_handler() 设置的处理方式,而是简单的停止脚本的运行和执行。
在 PHP7 中,当致命错误和可捕获的错误( E_ERROR 和 E_RECOVERABLE_ERROR )发生时会抛出异常,而不是直接停止脚本的运行。
对于某些情况,比如内存溢出,致命错误则仍然像之前一样直接停止脚本执行。
在 PHP7 中, 一个未捕获的异常也会是一个致命错误 。
这意味着在 PHP5.x 中致命错误抛出的异常未捕获,在 PHP7 中也是致命错误。
注意:其他级别的错误如 warning 和 notice ,和之前一样不会抛出异常,只有 fatal 和 recoverable 级别的错误会抛出异常。
从 fatal 和 recoverable 级别错误抛出的异常并非继承自 Exception 类。
这种分离是为了防止现有 PHP5.x 的用于停止脚本运行的代码也捕获到错误抛出的异常。
fatal 和 recoverable 级别的错误抛出的异常是一个全新分离出来的类 Error 类的实例。
跟其他异常一样, Error 类异常也能被捕获和处理,同样允许在 finally 之类的块结构中运行。
为了统一两个异常分支, Exception 和 Error 都实现了一个全新的接口: Throwable类。

1、Throwable类详解。

如果在 PHP7 的代码中定义了 Throwable 类,它将会是如下这样:

<?php
interface Throwable {
    public function getMessage():string;
    public function getCode():int;
    public function getFile():string;
    public function getLine():int;
    public function getTrace():array;
    public function getTraceAsString():string;
    public function getPrevious():Throwable;
    public function __toString():string;
}

这个接口的内部结构看起来是还不是很熟悉。
Throwable 规定的方法跟 Exception 几乎是一样的。
唯一不同的是 Throwable::getPrevious() 返回的是 Throwable 的实例而不是 Exception 的。
Exception 和 Error 的构造函数跟之前 Exception 一样,可以接受任何 Throwable 的实例。
Throwable 可以用于 try/catch 块中捕获 Exception 和 Error 对象(或是任何未来可能的异常类型)。
记住捕获更多特定类型的异常并且对之做相应的处理是更好的实践。
然而在某种情况下我们想捕获任何类型的异常(比如日志或框架中错误处理)。
在 PHP7 中,要捕获所有的应该使用 Throwable 而不是 Exception,例如:

try {
    // 可能引发异常或错误的代码
} catch (Throwable $e) {
    // 监听并抛出错误异常
}

具体使用例子如下:

class MathOperations {
   protected $n = 10;
   // 尝试用零错误对象获得除法并显示为异常
   public function doOperation(): string {
      try {
         $value = $this->n % 0;
         return $value;
      } catch (Throwable $e) {
         return $e->getMessage();
      }
   }
}
$mathOperationsObj = new MathOperations();
var_dump($mathOperationsObj->doOperation());

注意:Throwable 也是可以被继承从而创建特定的包接口或者添加额外的方法。
一个继承自 Throwable 的接口只能被 Exception 或 Error 的子类来实现。
更具体的继承用法,可以参考官网文档。

十、断言

0、前言

断言实际上就是我们常说的断点,一般用于程序错误调试中。
在程序中,你是否会经常做这样的操作。

if ($info === false) {
    die('这里没有获取到数据');
}

然后调试完代码,上线后,我们就在代码里屏蔽掉,或删除掉这段代码。
编写代码时,我们总是会做出一些假设,断言就是用于在代码中捕捉这些假设,可以将断言看作是异常处理的一种高级形式。
程序员断言在程序中的某个特定点该的表达式值为真。如果该表达式为假,就中断操作。
可以在任何时候启用和禁用断言验证,因此可以在测试时启用断言,而在部署时禁用断言。
同样,程序投入运行后,最终用户在遇到问题时可以重新起用断言。
使用断言可以创建更稳定,品质更好且不易于出错的代码。
单元测试必须使用断言!

1、断言的一些使用规则和建议

规则一:使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是必然存在的并且是一定要作出处理的。
规则二:在函数的入口处,使用断言检查参数的有效性(合法性)。
建议一:在编写函数时,要进行反复的考查,并且自问:“我打算做哪些假定?”一旦确定了的假定,就要使用断言对假定进行检查。
建议二:一般教科书都鼓励程序员们进行防错设计,但要记住这种编程风格可能会隐瞒错误。当进行防错设计时,如果“不可能发生”的事情的确发生了,则要使用断言进行报警。

2、断言的函数说明

PHP7使用断言主要依赖两个函数:
assert_options() : 断言配置信息设置函数
assert() : 断言函数
使用逻辑是,我们必须在代码顶部先使用assert_options()函数开启assert()的使用开关。
例如:

<?php
// 断言操作选项函数
// 默认是打开断言的
assert_options(ASSERT_ACTIVE, 1); 
// 将会抛出错误 Warning: assert(): Assertion "1==2" failed in
assert('1==2'); 
// 默认情况下继续执行,对于调试很好,尤其是可以使用callback,但是生产环境就不建议使用了。
echo 666;

assert() 会检查指定的 assertion 并在结果为 false 时采取适当的行动(视assert_options而定)。

3、assert_options()参数详解

assert_options的设置说明如下:
ASSERT_ACTIVE :assert()函数的开启,默认为1
ASSERT_WARNING:当表达式为false时,是否要输出警告性的错误提示,默认为1
ASSERT_BAIL :当表达式为false时,是否要中止运行,默认为0
ASSERT_QUIET_EVAL:当表达式为false时,是否关闭错误提示,默认为0
ASSERT_CALLBACK:当表达式为false时,是否启动回调函数,默认为null,填入函数名称即可
ASSERT_CALLBACK主要与error的handler函数一样,基本会传入三个参数:$file、$line,、$code,具体的用法如下:

<?php
// 断言操作选项函数
assert_options(ASSERT_ACTIVE, 1);
assert_options(ASSERT_WARNING, 0);
assert_options(ASSERT_QUIET_EVAL, 1);
// 断言回调函数
function my_assert_handler($file, $line, $code) {
    echo 1;
    echo "<hr>Assertion Failed:File '$file'<br />Line '$line'<br />Code '$code'<br /><hr />";
}
// 设置断言回调
assert_options(ASSERT_CALLBACK, 'my_assert_handler');
// 断言调试
assert('1==2');
echo 666;

4、安全性问题

由于assert()是将字符串转换成php代码执行,跟eval()类似, 所以它也一样存在一定的安全隐患,要谨慎使用,例如下面的例子:

<?php
function test(){
    echo 1;
    return true;
}
$func = $_GET["func"];
assert("$func()");

5、关于断言的使用

php的官方文档里是建议assert()用来进行debug,我们可以发现ASSERT_ACTIVE可以用来控制是否开启debug。
现在问题就产生了,如果程序员在开发的时候在代码中留下了很多assert(),然后在程序发布的时候关闭执行,设置assert_options(ASSERT_ACTIVE,0)这样做是否可行?有没有安全问题?
我的建议是,既然assert()主要作用是debug,就不要在程序发布的时候还留着它。
在程序中用assert()来对表达进行判断是不明智的,原因上文说了:
1、在生产环境中assert()可能被disabled,所以assert()不能被完全信任;
2、assert()可以被继续执行,而如果在生产环境让 ASSERT_ACTIVE=1,那这个表达式字符串可以被执行本身就存在安全隐患。
所以在程序发布后,assert()是能删除就删除,不能话,可以考虑将asser()搭建在单元测试中。

十一、unserialize()更安全的反序列化

PHP7引入了过滤 unserialize()函数以在反序列化不受信任的数据对象时提供更好的安全性。
它可以防止可能的代码注入,使开发人员能够使用序列化白名单类。

<?php
class MyClass1 { 
   public $obj1prop;   
}
class MyClass2 {
   public $obj2prop;
}
$obj1 = new MyClass1();
$obj1->obj1prop = 1;
$obj2 = new MyClass2();
$obj2->obj2prop = 2;
$serializedObj1 = serialize($obj1);
$serializedObj2 = serialize($obj2);
# 将所有对象分为__PHP_Incomplete_Class对象
$data = unserialize($serializedObj1, ["allowed_classes" => false]);
# 将所有对象分为__PHP_Incomplete_Class 对象 除了ClassName1和ClassName2
$data = unserialize($serializedObj2, ["allowed_classes" => ["ClassName1", "ClassName2"]);
# 默认行为,和 unserialize($foo)相同
$data = unserialize($serializedObj1, ["allowed_classes" => true]);

十二、Closure::call()临时绑定对象

Closure::call() 方法被添加作为临时绑定的对象范围,以封闭并简便调用它的方法。
它的性能相比PHP5.6 匿名类::bindTo()要快得多。

<?php
class Test{
    public $name = "小黄牛";
}
# PHP5.6+使用匿名函数的bindTo方法
$getNameFunc = function() { 
    return $this->name;
};
$name = $getNameFunc->bindTo(new Test, 'Test');
echo $name();
# PHP7可只需要使用call,而且语法更加简洁
$getX = function() {return $this->name;};
echo $getX->call(new Test);

十三、use 命名空间导入的改变

从PHP7起,单次使用 use 语句可以用来从同一个命名空间导入类,函数和常量(而不用多次使用 use 语句)。
这个改进非常棒,在PHP7之前,我们只能一条条的导入,现在只要在同一个命名空间下,就允许多个同时导入了。

<?php
# PHP5.4+,只能这样
use com\yiibai\ClassA;
use com\yiibai\ClassB;
use com\yiibai\ClassC as C;
use function com\yiibai\fn_a;
use function com\yiibai\fn_b;
use function com\yiibai\fn_c;
use const com\yiibai\ConstA;
use const com\yiibai\ConstB;
use const com\yiibai\ConstC;
# PHP7+,可以这样优化
use com\yiibai\{ClassA, ClassB, ClassC as C};
use function com\yiibai\{fn_a, fn_b, fn_c};
use const com\yiibai\{ConstA, ConstB, ConstC};

十四、foreach语句的指针改变

1、foreach不再改变内部数组指针

在PHP7之前,当数组通过 foreach 迭代时,数组指针会移动。
从现在开始,不再如此,foreach 语句不再改变内部数组指针,例如下面代码:

<?php
$array = [0, 1, 2];
foreach ($array as &$val) {
    var_dump(current($array));
}

原来PHP5的指针会一直往下,所以使用current()函数可以实现获取下一个数组元素的效果,得到结果:

int(1)
int(2)
bool(false)

而在PHP7中,只能得到:

int(0)
int(0)
int(0)

2、foreach通过引用遍历时,有更好的迭代特性

当使用引用遍历数组时,现在 foreach 在迭代中能更好的跟踪变化。
例如,在迭代中添加一个迭代值到数组中,参考下面的代码:

<?php
$array = [0];
foreach ($array as &$val) {
    var_dump($val);
    $array[1] = 1;
}

PHP5输出:

int(0)

PHP7输出:

int(0)int(1)

这两点不兼容性,在项目从PHP5升级到PHP7时,需要格外注意的。
同时还有一点非常重要的,从PHP7开始,$HTTP_RAW_POST_DATA 变量被彻底移除了,请使用 php://input 作为替代。

十五、Generator生成器及yield

0、什么是yield

yield和return有点类似,不过不同的是,return会返回值并且终止代码的执行,而yield会返回一个值给循环调用此生成器的代码并且只是暂停执行生成器函数。
实例分析:

<?php
function gen_one_to_three() {
    # 注意yield有点类似指针,调用一次就会往下走,可以用循环理解
    yield 1;
    yield 2;
    yield 3;
    return 4;
    yield 5;
}
$generator = gen_one_to_three();
var_dump($generator);
echo "<br/>";
var_dump($generator instanceof Iterator); // bool(true)
echo "<br/>";
foreach ($generator as $value) {
    var_dump($value);
}

上面的代码运行会获得结果:

object(Generator)#1 (0) { } 
bool(true) 
int(1) int(2) int(3)

同时,我们也可以清晰的看到,在yield后再使用return是无效的,同时会中断程序往下执行。
逻辑解析:
调用gen_one_to_three()的时候,里面的代码并没有真正的执行,而是返回了一个生成器对象$generator = Generator对象;
$generator instanceof Iterator说明Generator实现了Iterator接口,可以用foreach进行遍历,每次遍历都会隐式调用current()、next()、key()、valid()等方法。
(Generator系统类中已经写好的方法)

1、指定键名来生成值

<?php
function input_parser($input) {
    foreach ($input as $key=>$val) {
        yield $key => $val;
    }
}
$input = [
    'name' => '小黄牛',
    'des'  => '真帅!',
];
foreach (input_parser($input) as $field =>$title) {
    echo "$field:\n";
    echo "$title\n";
}

2、生成NULL

<?php
function gen_three_nulls() {
    foreach (range(1, 3) as $i) {
        yield;
    }
}
var_dump(iterator_to_array(gen_three_nulls()));

3、PHP7中的生成器 - yield from

PHP7开始允许从其他的generator,Traversable对象, 或者数组通过yield from 生成数函数来yield值。
通俗来说yield from就是允许生成器中引入其他的生成器。

<?php
function count_to_ten() {
    yield 1;
    yield 2;
    yield from [3, 4];
    yield from new ArrayIterator([5, 6]);
    yield from seven_eight();
    yield 9;
    yield 10;
}
function seven_eight() {
    yield 7;
    yield from eight();
}
function eight() {
    yield 8;
}
foreach (count_to_ten() as $num) {
    var_dump($num);
}

4、PHP7中的生成器 - yield from and return values

<?php
function count_to_ten() {
    yield 1;
    yield 2;
    yield from [3, 4];
    yield from new ArrayIterator([5, 6]);
    yield from seven_eight();
    return yield from nine_ten();
}
function seven_eight() {
    yield 7;
    yield from eight();
}
function eight() {
    yield 8;
    return 9;
}
function nine_ten() {
    yield 10;
    return 11;
}
$gen = count_to_ten();
foreach ($gen as $num) {
    echo "$num ";
}
echo $gen->getReturn();

在PHP5.5引入生成器的概念。生成器函数每执行一次就得到一个yield标识的值。
在PHP7中,当生成器迭代完成后,可以获取该生成器函数的返回值。
通过Generator::getReturn()也可以获得这个返回值。
不过值得注意的是,就像上面的代码一样,Generator::getReturn()也是只能获取到最后一次的返回值。

5、Generator系统类的内部结构

<?php
Generator implements Iterator {
    public mixed current ( void )//返回当前产生的值
    public mixed key ( void ) //返回当前产生的键
    public void next ( void )//生成器继续执行
    public void rewind ( void ) //重置迭代器,如果迭代已经开始了,这里会抛出一个异常。
    public mixed send ( mixed $value ) //向生成器中传入一个值,当前yield接收值,然后继续执行下一个yield
    public void throw ( Exception $exception )  //向生成器中抛入一个异常
    public bool valid ( void )  //检查迭代器是否被关闭,已被关闭返回 FALSE,否则返回 TRUE
    public void __wakeup ( void ) //序列化回调
    public mixed getReturn ( void )//返回generator函数的返回值,PHP version 7+
}

最后再来看一个实例:

function test() {
    $a = (yield 111);
    var_dump('test()->$a:'.$a);
    $b = (yield 222);
    var_dump('test()->$b:'.$b);
}
$gen = test();
echo "first:";
var_dump($gen->current());echo "<br/>";
echo "second:";
var_dump($gen->send(333));echo "<br/>";
echo "third:";
var_dump($gen->next());

输出如下:

first:int(111) 
second:string(14) "test()->$a:333" int(222) 
third:string(11) "test()->$b:" NULL

分析:
1、第一次test()函数,执行到yield,就中断了执行,这次yield停留在了yield 111
2、通过$gen->current()把111进行返回,故输出了111
3、$gen->send(333),把333传递给了test()中的yield,被yield接收到,故 $a = 333 ,输出了 $test->$a:333,这时又重新唤醒了yield的执行,
4、$b = yield 222,执行到这时,把结果又进行了一次返回,并且进行了中断执行,这时 $gen-send(333) 等于 222,故输出了222
5、$gen->next() 又重新唤醒了 $gen 的迭代执行,但不是使用send()进行传递,故这时 $b 没接收到任何值,所以输出了NULL