PHP7.4 新特性

Typed properties

  • 现在类的属性支持类型声明了:

    class User {
        public int $id;
    public string $name;
    }
  • 注:设置属性 $id$name 的值时必须要指定类型,否则会报错:
    PHP Fatal error: Uncaught TypeError: Typed property User::$id must be int, string used

  • 注:访问属性之前必须要先初始化,否则会报错:

    • PHP Fatal error: Uncaught Error: Typed property User::$id must not be accessed before initialization

Arrow functions

  • 箭头函数是简化版语法的匿名函数,它们功能相同,只有一个区别:箭头函数可以直接使用父作用域内的变量。

  • 基本语法:fn (argument_list) => expr

  • 匿名函数 vs 箭头函数

    $y = 1;
    $fn1 = fn($x) => $x + $y;
    // 相当于 use $y:
    $fn2 = function ($x) use ($y) {
    return $x + $y;
    };
  • 箭头函数支持任意形式的函数签名(包括参数类型、参数默认值、返回值类型、变长参数)以及按引用传递和返回。

    <?php
    fn(array $x) => $x;
    static fn(): int => $x;
    fn($x = 42) => $x;
    fn(&$x) => $x;
    fn&($x) => $x;
    fn($x, ...$rest) => $rest;
  • 因为箭头函数「按值绑定」使用变量,相当于 use ($x),所以函数内不能修改外部的变量。

    <?php
    $x = 1;
    $fn = fn() => $x++;
    $fn();
    echo $x; // 1

return type covariance and argument type contravariance(协变返回和逆变参数)

  • Covariant (协变):类型从通用到具体
  • Contravariant (逆变): 类型从具体到通用

    // 协变返回
    <?php
    class ParentType {}
    class ChildType extends ParentType {}
    class A
    {
    public function covariantReturnTypes(): ParentType
    { return new ParentType(); }
    }
    class B extends A
    {
    public function covariantReturnTypes(): ChildType
    { return new ChildType(); }
    }
    // 逆变参数
    class C
    {
    public function contraVariantArguments(ChildType $type)
    { return $type; }
    }
    class D extends C
    {
    public function contraVariantArguments(ParentType $type)
    { return $type; }
    }

Null coalescing assignment operator(NULL 合并运算符)

<?php
$array['key'] ??= 1;
// 等于下面
$array['key'] = $array['key'] ?? 1;
// 或
if (!isset($array['key'])) {
    $array['key'] = 1;
}

var_dump($array);

Unpacking inside arrays(数组内支持展开运算符)

  • 语法:$arr = [...$args];
<?php
$parts = ['apple', 'pear'];
$fruits = ['banana', 'orange', ...$parts, 'watermelon'];
  • 注:仅支持数字索引的数组

  • 展开运算符的一个优点是它支持任何可遍历的对象,如生成器:

    function generator() {
    for ($i = 3; $i <= 5; $i++) {
    yield $i;
    }
    }
    $arr1 = [0, 1, 2, ...generator()];
  • 注:不支持引用传递的方式:

    <?php
    $arr2 = ['red', 'green', 'blue'];
    $arr3 = [...&$arr2]; // 报错:PHP Parse error: syntax error, unexpected '&'
  • 但是支持包含引用值的数组:

    $red = 'red';
    $arr4 = [&red, 'green', 'blue'];
    $arr5 = ['white', ...$arr1, 'black'];

Numeric literal separator(数字字面量分隔符)

  • 支持数字中的下划线来可视化地分隔数字(可以提高代码的可读性)
6.674_083e-11; // float

299_792_458;   // decimal

0xCAFE_F00D;   // hexadecimal

0b0101_1111;   // binary

0137_041;      // octal
  • 注:下划线必须直接位于两个数字之间,以下是不合法的示例:
// PHP Parse error:  syntax error, unexpected '_' (T_STRING)

100_;       // trailing

1__1;       // next to underscore

1_.0; 1._0; // next to decimal point

0x_123;     // next to x

0b_101;     // next to b

1_e2; 1e_2; // next to e

Weak references(弱引用)

<?php
$object = new stdClass;
$weakRef = WeakReference::create($object);

var_dump($weakRef->get());
unset($object);
var_dump($weakRef->get());

// 第一次 var_dump
object(stdClass)#1 (0) {}

// 第二次 var_dump,当 object 被销毁的时候,并不会抛出致命错误
NULL

Allow exceptions from __toString()

现在允许 __toString() 里面抛出异常了,而之前会抛出一个 fatal error。

OPcache 支持预加载

服务器启动时 – 在运行任何应用程序代码之前 – 我们可以将一组 PHP 文件加载到内存中 – 并使得这些预加载的内容,在后续的所有请求中 “永久可用”。这些文件中定义的所有函数和类在请求时,就可以开箱即用,与内置函数相同。

预加载由 php.ini 的 opcache.preload 进行控制。这个参数指定在服务器启动时编译和执行的 PHP 脚本。

opcache.preload=preload.php

在这个文件里面用 includerequire 等方法包含的文件名都会被预加载。

Custom object serialization(自定义对象序列化函数)

  • 新增了两个魔术方法,用于序列化和反序列化对象,并且替代了 Serializable 接口,后者在未来会被弃用。
public function __serialize(): array;

public function __unserialize(array $data): void;

参考

2020/09/02 posted in  编程技术

几千万数据,更改 MySQL 表结构

数据量大、并发量高场景,如何在流量低峰期,平滑实施表结构变更?

  • 如果是减column,升级程序不使用即可
  • 如果是修改column,程序兼容性容易出问题

方案一:在线修改表结构

alter table add column
数据量大的情况下,锁表时间会较长,造成拒绝服务,一般不可行。

方案二:通过增加表的方式扩展属性,通过外键join来查询

e.g 
t_user(uid, c1, c2, c3)

想要扩展属性,可以通过增加一个表实现:
t_user_ex(uid, c4, c5, c6)

数据量大的情况下,join性能较差,一般不可行。

方案三:通过增加表的方式扩展,通过视图来屏蔽底层复杂性

同上,视图效率较低,一般不使用视图。

方案四:揍产品经理,阻止她修改需求

...

方案五:提前预留一些reserved字段,加列可复用这些字段

这个方案可行,但如果预留过多,会造成空间浪费。

方案六:pt-online-schema-change

对于MySQL而言,这是目前比较成熟的方案,被广大公司所使用。

下面仍以用户表扩展为例,说下这个工具内部的原理与步骤。

假设:
user(uid, name, passwd)
要扩展到:
user(uid, name, passwd, age, sex)

1. 先创建一个扩充字段后的新表:

user_new(uid, name, passwd, age, sex)

2. 在原表user上创建三个触发器,对原表user进行的所有insert/delete/update操作,都会对新表user_new进行相同的操作

3. 分批将原表user中的数据insert到新表user_new,直至数据迁移完成;

4. 删掉触发器,把原表移走(默认是drop掉);

5. 把新表user_new重命名(rename)成原表user;

扩充字段完成,整个过程不需要锁表,可以持续对外提供服务。

操作过程中需要注意

  • 变更过程中,最重要的是冲突的处理,一条原则,以触发器的新数据为准,这就要求被迁移的表必须有主键(这个要求基本都满足);

  • 变更过程中,写操作需要建立触发器,所以如果原表已经有很多触发器,方案就不行(互联网大数据高并发的在线业务,一般都禁止使用触发器);

  • 触发器的建立,会影响原表的性能,所以这个操作必须在流量低峰期进行;

使用方法

前提先在任何一台可以连接到 MySQL 的实例的 linux 机器上安装,安装之后使用帮助:

pt-online-schema-change --help
pt-online-schema-change \
--host={host} \
-u{user} \
-p'{password}' \
--alter "add column ..." \
D={schema},t='{tableName}' \
--execute --print --statistics --no-check-alter --charset=utf8

pt-online-schema-change是DBA必备的利器,比较成熟,在互联网公司使用广泛,要了解更详细的细节,亦可以Google一下。

2020/05/11 posted in  编程技术

APP集成ApplePay调研

目前国内 接入 Apple Pay 有两种模式:API模式和银联SDK模式。

Apple Pay API 模式

下图为目前国内 Apple Pay 支付接入的一个通用的流程(银联API模式),仅供参考:

整个流程中如下:

1、客户端通过苹果API,在 APP 应用内展示 Apple Pay 支付控件。

2、用户在 Apple Pay 的支付控件上进行生物验证(指纹或者人脸识别)或者手机密码验证。

3、苹果在用户验证通过之后,会生成一个用户选中的银行卡相关的 PaymentToken 加密数据,Apple Pay 必须在有网情况下才能进行,苹果需要从开发者网站上使用证书的公钥进行加密,完成后通过 API 回调返回给客户端前端。

4、客户端获取到 PaymentToken 后,给服务端发送扣款请求,等待支付结果。

5、服务端收到客户端上送的 PaymentToken,解密 PaymentToken 取出一些关键字段信息,附带其他订单信息,再与支付供应商(如国内银联)进行通信发起扣款。

6、服务端收到扣款结果后,再返支付结果给手机客户端,最终通知用户支付结果。

优点

这种方式,对于接入商户来说,证书和密钥都由接入商户自己管理,不再依赖支付供应商,客户端和服务端开发更加灵活。

缺点

这种方式,iOS 开发者需要自己控制和处理 Apple Pay 的 UI 展示和交互,并应对以下的一些异常:

1、部分场景中,用户验证通过后,正在发送扣款请求时,用户又点击了取消按钮,取消 Apple Pay 操作,在这种场景下,支付需要采用一定的方案和策略避免多扣用户的钱。

2、同其他支付方式一样,要考虑如何处理异常情况下的订单重复提交问题。

3、移动端需要对支持性进行验证,协商银联接口数据对Payment Sheet的展示关闭进行控制,对各种异常进行捕抓和处理。商家后台需要对 PaymentToken 做解密处理,同时需要自己实现对银联接口的交互,加密和解密接口数据。

客户端可能的问题

1、显示 ApplePay 弹窗到支付验证成功过程中何时调用后端发起扣款?

2、如何集成 ApplePay 到现有的支付选择界面中?

3、苹果设备生物验证成功后,发起扣款到显示结果,同步返回仅表示银联已受理此交易,并不代表交易成功,所以前端如何显示支付结果,是否需要发起主动查询。

服务端可能的问题

1、直接使用银联API,因为没有对应的SDK,需要自己封装扣款、查询、回调、退款等接口;这些接口需要对银行卡等业务有较深的理解,对于不同的接口返回情况需要作正确的处理。

2、需要多处解密加密,解密苹果返回的 PaymentToken,加解密与银联交互的所有接口数据,至少需要引入两种加解密的方式。

银联SDK模式

银联 SDK 模式是中国银联以对外提供 SDK 的方式给到 APP 使用 Apple Pay 支付。

银联支付控件 Apple Pay 支付的实现方式:

1-2、商户生成订单,通过商户SERVER端将订单信息发送给银联支付网关;

3-4、银联支付网关记录订单信息,返回用来标识订单的TN号(参见产品接口规范),经由商户SERVER返回至商户APP;

5、商户APP调用银联SDK,将TN号传递给银联SDK;

6、银联SDK向Apple公司的PASSKIT FRAMEWORK发起支付请求;

7、接口返回加密的支付Token信息;

8-9、银联SDK将支付Token传递给银联支付网关,完成交易认证;

10-12、银联将支付结果返回给商户APP,商户SERVER,商户APP负责提示用户交易结果。

13、最终交易结果以银联回调/主动查询为准,SDK返回只作为页面结果显示。

优点

使用 SDK 的好处就是客户端接入简单,只管调用 SDK 的接口,处理支付结果回调即可,客户端不需要处理各种异常。

缺点

使用 SDK 模式的缺陷是:

1、增加了 APP 的体积。

2、SDK 模式下,证书和密钥都是由银联生成,APP 开发用银联提供的 CSR 文件生成 Apple Pay 证书并绑定,证书和密钥更新麻烦。

3、Apple Pay 的页面展示完全由银联 SDK 控制,当需要增加展示项时,需要向外部寻求银联 SDK 的支持。

客户端可能的问题

1、银联SDK的 ApplePay 界面如何集成到原有的支付流程中。

服务端可能的问题

1、发起扣款成功后,会发起回调,一分钟内未收到交易结果(回调),可通过发起交易查询状态API获取结果


苹果官方强烈建议使用供应商SDK的方式进行支付:https://developer.apple.com/cn/apple-pay/planning/

我们强烈建议您使用由支持 Apple Pay 的支付服务商提供的 SDK 或 JS API。您也可以提供自己的服务器端解决方案,以便从您的 app 或网站接收付款,解密付款令牌,以及对接支付服务商来处理授权。信用卡和借记卡付款的处理可能非常复杂;如果您尚不具备相应的专业知识和系统,但希望您的 app 或网站支持 Apple Pay,最为便捷可靠的方式就是使用支付服务商提供的 Apple Pay SDK 或 JS API。请联系您的平台或支付服务商以了解更多信息。查看电子商务平台和支付服务商。
2020/04/23 posted in  编程技术

Laravel 使用中间件过滤参数

问题

使用 Laravel Validator 验证参数以后,对于某些参数需要手动转换为对应的类型(比如 int,float),那么能不能在验证之前先自动过滤一下参数类型呢?

解决

使用中间件可以实现在 Controller 处理逻辑之前先过滤一下参数。

创建一个中间件类

$ php artisan make:middleware FilterParams
Read more   2020/04/04 posted in  编程技术

国家哀悼日 - 网站全局变灰

国务院公告:4月4日举行全国性哀悼活动,哀悼抗疫牺牲烈士和逝世同胞。因此,在自己网站上加上下面一段CSS代码,就能把网站全局变灰了。

<style type="text/css">
    html {
        filter: grayscale(100%);
        -webkit-filter: grayscale(100%);
        -moz-filter: grayscale(100%);
        -ms-filter: grayscale(100%);
        -o-filter: grayscale(100%);
        filter: url("data:image/svg+xml;utf8,#grayscale");
        filter: progid:DXImageTransform.Microsoft.BasicImage(grayscale=1);
        -webkit-filter: grayscale(1);
    }
</style>
2020/04/03 posted in  编程技术

RabbitMQ实现延时任务

场景:下单之后的15分钟内如果没有下单,则自动取消订单。

第一个想到的方法是开启一个定时任务,每隔1分钟就去扫描订单状态,如果有符合条件的,则取消之。这种方式缺点很明显,很浪费资源,因为大部分查询的结果都是无效的。

下面进入正题:使用 RabbitMQ 的死信队列(Dead Letter Exchange)来实现延时任务。

什么是死信?

首先,什么是死信?一个消息在一个队列中处于下列三种状态:(1)消息被拒绝并且不再重新投递.(2)消息超期。(3)队列超载。就变成了死信。如果队列中出现了死信,就会被重新投递到另一个交换机,那么该队列就是死信队列。该交换机根据绑定规则转发到对应的队列上,监听该队列就可以重新消费。简单地说,就是(因为某些原因)没有被消费的消息换个地方重新被消费。

Read more   2020/03/06 posted in  编程技术

组合与组合模式

继承的问题

继承是一种强大的设计方式,但是它也会限制灵活性,特别是类承担多职责时。

下面是一个继承的简单示例。

抽象类 Food 表示食物,它定义了抽象方法 make() 方法。两个实现类 SweetFoodSaltyFood 分别代表了食物的两种口味。

通过使用这种继承机制,客户端代码只知道它在使用一个 Food 对象,食物制作的细节被封装了。

Read more   2019/09/24 posted in  编程技术

CodeIgnitor 3.0.x 之 Load 加载机制

CI 框架中的 library 和很多内置的类都是通过 CI_Loader 这个类加载的,也就是 Controller 中常常出现的 $this->load 属性。那么它本身是如何被加载的。

CI_Loader 初始化

这一切又要从最初的入口点 index.php 说起了。首先,外界的请求定位到了 index.php,所有的请求的处理都是从这里开始的。大略看一下 index.php 的内容,其实也很简单,就是利用当前文件的位置,定义了一些环境和路径常量,比如 ENVIRONMENT, BASEPATH, APPPATH, VIEWPATH,基于这些常量,又定义了一些全局变量:$system_path, $application_folder, $view_folder,最后加载了CI框架的核心类:core/CodeIgnitor.php,完成了它的使命。

Read more   2019/08/20 posted in  编程技术

CodeIgnitor 配置类的使用

CI 的配置文件统一放在 application/config/ 目录下面,框架有一个默认的主配置文件 application/config/config.php。其部分内容如下:

<?php
$config['uri_protocol'] = 'REQUEST_URI';

// ...

$config['charset'] = 'UTF-8';

// ...

$config['subclass_prefix'] = 'My_';
Read more   2019/06/04 posted in  编程技术

变量存储区:堆和栈

最近在看PHP源码解析,涉及到堆栈存储区的知识,而我对于这个却不太清楚,因此,看了一下相关资料,总结一下。

栈,存储函数中的局部变量(临时变量),存储函数地址,栈是后进先出的结构,由CPU管理和优化。

使用栈存储变量的优势在于:你不用再管理内存了,不必手动分配内存或释放它,此外,由于CPU相关的优化,读取写入的效率也很高。

关于栈需要注意的一点是:存储在栈上的变量的大小是有限制的,而堆却不是。

Read more   2019/05/19 posted in  编程技术

《Modern PHP》 - 笔记1 - 最佳实践

Read more   2019/04/03 posted in  编程技术

Distinct 与 Group by 的比较

看了很多文章,这两个SQL语句在不同的数据库上面的实现上可能有相同或有不同,但是应当要明确它们在功能概念上的区别,最终得出结论:

GROUP BY 用来使用聚集函数获得值,比如 AVG, MAX, MIN, SUM 和 COUNT,而 DISTINCT 用于去除重复值。

要根据实际的应用场景来使用(即使它们有时候返回的结果是一样的)

2019/04/01 posted in  编程技术

CI框架入门笔记

Read more   2019/03/22 posted in  编程技术

Laravel 查询包括软删除的记录

查询结果包括已被软删除的记录:

Model::withTrashed()->get();

只查询软删除记录:

Model::onlyTrashed()->get();

这两个方法找了好久,特地记了下来。

2019/01/21 posted in  编程技术

Laravel 中的异常处理

这篇文章里,我们将研究 Laravel 框架中最重要也最少被讨论的功能 —— 异常处理。

Laravel 自带了一个异常处理类,它能够让你以简单、优雅的方式 report 和 render 异常。

文章的前半部分,我们将研究异常处理类(exception handler)提供的默认配置,然后研究异常处理类,并借此理解 Laravel 框架处理异常的过程。

文章的后半部分,我们将创建一个自定义的异常处理类,用于捕获自定义的异常。

Read more   2019/01/09 posted in  编程技术