PHP不权威总结

PHP不权威总结

欢迎阅读

本文目标用户是我自己,系统地持续集成PHP方方面面的知识,但不会事无巨细的一一列举,只会挑选我认为易忘、易错、重要的内容进行集成,而且大多是点到为止,需要更详细文档的话会额外链接。所以,如果你已经靠PHP谋生了几年的话这篇文档应该会对你有帮助,如果你是PHP新手,建议重点阅读学习资料一节。

安装配置 & 环境搭建

PHP的开发环境主要是LA/NMP,即Linux、Apache/Nginx、Mysql、PHP,关于这些工具在不同平台的安装有大量优秀详细的文档,这里不废话,重点还是PHP的安装,不过强烈推荐Laravel的Homestead解决方案,一套方便管理的、跨平台、统一的、虚拟化开发环境。

编译安装

推荐使用Like Unix操作系统作为开发、线上环境,强大且简单。我推荐的PHP安装方式是编译安装,对新手也是,不走弯路、不踩坑怎么成长。源码安装你的选择会更加自由,能第一时间尝试各种Alpha版,而且在日常工作中,安装扩展、调试、优化,都需要对PHP的目录、文件有一定了解。

  1. PHP官网下载你需要的版本源码
  2. 编译 & 安装
# --prefix指定目录,--with-config-file-path指定php-config目录,其他为要一同安装的扩展或开启的功能

./configure --prefix=/usr/local/php \
--with-config-file-path=/etc/php \
--enable-fpm \
--enable-pcntl \
--enable-mysqlnd \
--enable-opcache \
--enable-sockets \
--enable-sysvmsg \
--enable-sysvsem \
--enable-sysvshm \
--enable-shmop \
--enable-zip \
--enable-soap \
--enable-xml \
--enable-mbstring \
--disable-rpath \
--disable-debug \
--disable-fileinfo \
--with-mysql=mysqlnd \
--with-mysqli=mysqlnd \
--with-pdo-mysql=mysqlnd \
--with-pcre-regex \
--with-iconv \
--with-zlib \
--with-mcrypt \
--with-gd \
--with-openssl \
--with-mhash \
--with-xmlrpc \
--with-curl \
--with-imap-ssl

sudo make
sudo make install
  1. 添加配置。PHP的源码包中附带了一个开发环境使用的配置(开启了方便调试、减少性能消耗的配置),只需要放到默认位置就行
sudo mkdir /etc/php
sudo cp php.ini-development /etc/php/php.ini
  1. 扩展安装。如果需要在已安装的PHP中添加扩展的话,只需下载好扩展源码后进入扩展的源码目录
phpize
./configure --with-php-config=/usr/local/php-config
make
make install

Homestead

团队作战中,由于依赖众多(git、redis、memcache、nodejs、npm),每个人习惯不同,开发环境都会有所差别,会导致许多难以察觉的问题,并大大提高团队协调的难度,我曾遇到过PHP5.2、5.4两个版本的正则执行结果不同而导致上线折腾到凌晨2点的坑事。所以Laravel团队提供了一套基于虚拟机的跨平台开发环境搭建方案 —— Homestead(译为家园)。

我来简单梳理一下。Homestead的做法是,首先选择免费稳定的Virtual Box作为虚拟机,用Vagrant来管理虚拟机和开发环境,然后又配置了一套统一的git、redis等常用工具,直接安装即可。这里有详细的说明文档。

Mac安装

直接使用Homebrew安装,类似Linux的yum、apt-get方案。

Windows安装

推荐使用XAMPP、EasyPHP、WAMP等一类的套件,简单易用,包括了LN/AMP最基本的开发环境,提供了GUI管理界面。需要提醒的是千万不要用这些套件部署线上环境

基本特性

日期时间

日期处理非常讨厌的地方是每月天数不同和闰年、时区的差异,以及日期之间的运算。早期的phper主要使用date、strtotime等几个函数进行转换,费时费力易出错。5.2之后其实提供了以下几个类,来简化日期处理。

// 基本的日期处理对象
DateTime: __construct ([ string $time = "now" [, DateTimeZone $timezone = NULL ]] );

// 时区对象
DateTimeZone: __construct ($timezone);

// 时间段对象
DateInterval: __construct ($interval_spec);

// 时间迭代器
DatePeriod: __construct (DateTimeInterface $start, DateInterval $interval, DateTimeInterface $end, $options=0);

列举几个使用场景

$datetime = new DateTime();
echo $datetime->format('Y-m-d H:i:s'); // out当前时间

$datetime->setTimezone(new DateTimeZone('Asia/ShangHai'));
echo $datetime->format('Y-m-d H:i:s'); // out上海时间

$yesterday = new DateTime('-1 day');
echo $datetime->diff($yesterday)->format('%a'); // out日期天数差,1

// 生成一个时间段
$interval = new DateInterval('P2DT2H'); // P开头, 日期和时间T隔开 Y M D W H M S
$datetime->add($interval);
$datetime->sub($interval);

// 两个时间点之间进行迭代
$start = DateTime('2019-11-01');
$end = DateTime('2019-11-21');
$interval = new DateInterval('P2D');
$period = new DatePeriod($start, $interval, $end, DatePeriod::EXCLUDE_START_DATE);
foreach ($period as $datetime) {
    echo $datetime->format("Y-m-d H:i:s") , PHP_EOL; // out 2019-11-03 2019-11-05...
}

延伸

数据库

常用mysqli_* mysql_*函数簇由于比较底层且繁琐,加之经常使用框架封装好的方法,经常忘记原生的数据库操作,出于便捷性、通用性与安全性的考虑,数据库连接推荐使用PDO

// 建立数据库连接,pdo支持mysql oracle sqlite等多种常用数据库
$pdo = new PDO('mysql:host=...;dbname=...;prot=...;charset=...');
$stmt = $pdo->query('select * from user');
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); // fetchAll一次读取尚未读取的所有记录

// 逐条读取,减少内存消耗;注意查询结果以及缓存在本地,只是逐条加载近内存
$stmt = $pdo->query('select * from user');
while (false !== ($row = $stmt->fetch()) {
    print_r($row);
}

// 一般为了防止sql注入,和提高性能,常使用prepare
$sql = "insert into foo (name, age) values (:name, :age)";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':name', 'name');
$stmt->bindValue(':age', 18);
$stmt->execute();

define VS const

关于常量的处理有define和const两种实现方法,其中const是在编译阶段执行性能、可读性都要略好一些,但由于其不能使用函数、不能if条件控制,灵活性要比define差。

// define
define('NAME_1', 1);
define('NAME_2', [1, 2, 3]);
define('NAME_3', time());
if ($flag) {
    define('NAME_4', true);
}
class Foo {
    define('class_const', 1); // compile error
}

// const
const NAME_5 = 1;
const NAME_6 = time(); // compile error
if ($flag) {
    const NAME_7 = 1; // compile error
}
function foo () {
    const A = 1; // compile error
}

OOP特性

错误与异常

php程序运行时可能会出现各种错误,错误分为notice、warning、error、compile_error等,正确处理这些错误信息能够帮助我们快速解决问题。

// 错误报告控制,也可以在php.ini中修改error_reporting
error_reporting(E_ALL & ~E_NOTICE); // 关闭notice错误
error_reporting(E_WARNING | E_ERROR); // 只开启warning和error错误

// 错误显示
ini_set('display_errors', 1);
# php中错误相关配置
# 错误日志是否打开
log_errors = On (Off)

# 错误日志记录的位置
error_log = php_errors.log

# 是否打开错误显示
display_errors = Off

# 定义错误显示的级别
error_reporting = E_ALL

关于错误的控制,建议为:

  • 任何环境都要记录错误
  • 开发环境尽可能多显示错误
  • 生产环境要关闭错误显示

异常OOP的错误处理机制,强大且优雅。但php在这方面比较混乱,程序出错时既可能抛异常,也可能只是个warning或直接中断运行,甚至继续执行下去。php7中尝试对此进行一定的统一,部分运行时fatal error不再直接中断程序,而是改为抛出Error异常,并与Exception统一实现了Throwable接口

try {
    require_once 'b.php'; // 依然fatal error
    $b = 1 % 0; // Error, DivisionByZeroError
    foo(); // Error
    echo $a; // warning而已

    $b = null;
    $b->test(); // Error
 
} catch (Exception $e) {

} catch (Error $e) {

} catch (Throwable $e) {

}

如果想严格执行异常机制,可以注册一个error回调函数将所有错误转换为异常抛出。注意,E_ERROR E_PARSE E_CORE_ERROR E_CORE_WARNING E_COMPILE_ERROR E_COMPILE_WARNING会直接导致程序退出而无法转换为异常

set_error_handler(function($errno, $errstr, $errfile, $errline) {
    // 屏蔽不报告的错误
    if (!(error_reporting() & $errno)) {
        return;
    } else {
        throw new \ErrorException($errstr, $errno);
    }
});

延伸

延迟静态绑定

延迟静态绑定是指类静态方法调用时执行调用者的逻辑,而非方法定义时的逻辑,看个例子

class People {
    public static function out() {
        echo 'people' . PHP_EOL;
    }    
    public function callOut() {
        self::out();
        static::out();
    } 
}   
class User extends People {
    public static function out() {
        echo 'user' . PHP_EOL;
    } 
}   

$user = new User();
$user->callOut();

// static指向运行时的调用者也就是User类,而self为定义者也就是People类,所以输出为
// people
// user

反射

反射是一种面向对象的机制,提供了一套API允许程序在运行时对类进行检查,类定义、类方法、成员变量、可访问性、源码,甚至访问私有成员与方法。反射机制经常使用在测试、构建与框架底层代码中,能够产生很多“神奇”的效果。

延伸

trait

trait,翻译为性状,主要用于在类之间抽象一些特性。比如超人和小鸟,他们都需要实现一个fly方法,一般的做法是抽象一个更高级的类实现fly方法他们共同继承,但是这样强行耦合了两个并不强相关的类;另一种做法是定义一个Fly的接口实现fly方法,这样虽然避免耦合,但同样的fly方法需要实现两遍,违背了DYR原则;比较好的办法是has-a,即定义一个包含fly方法的公共类,并在超人类和小鸟类中分别实例化。其实trai就类似第三种做法,而且使用起来更方便一些

trait Fly {
    public function fly() {
        echo 'I can fly';
    }
}
class Superman {
    use Fly;
}
class Bird {
    use Fly;
}
$clark = new Superman();
$clark->fly();

就是这么简单,但trait使用时有几个需要注意的地方

  • php的方法调用优先级为class自身方法 > trait > 继承
  • 多个trait用逗号分隔
  • trait也可以使用trait
  • trait也可以定义属性,但不能与class中属性冲突,fatal error

高级特性

命名空间

命名空间主要用来解决命名冲突。php的命名空间其实很简单,只是由于其只是个虚拟空间,并不要求与代码实际文件相对应,虽然会灵活许多,但导致使用起来不好理解,这也是我诟病php哲学的一点,过分追求灵活简单,反而导致方案太多而困惑。

先看几个命名空间的常见场景

namespace App\A\Route;
class Http {
}

namespace App\B\Route;
class Http {
}

// 直接new,默认当前命名空间
$routeB = new Http();

// 使用全限定命名空间,\开头
$routeA = new \App\A\Route();

// use导入,同时重命名
use App\A\Route\Http as HttpA;
$routeA = new HttpA();

// use导入半限定
use App\A;
$routeA = new A\Route\Http();

php的命名空间只是个虚拟概念,use并不能自动加载文件,还需要依靠其他办法加载文件,php标准psr要求每个php文件只能定义一个namespace,且与其文件路径保持一致,并利用spl_autoload自动加载,这样就大大优化了命名空间

  • namespace必须在文件开头
  • function const同样可以使用命名空间,导入时关键字为use function, use const

延伸

yield

yield,生成器,主要用于“逐步”的处理大文件、复杂计算,减少内存消耗,和一定程度的降低代码复杂度,类似游标。生成器是基于协程实现的,其基本特点是函数不再是调用-返回的模式,而是调用-运行-暂停……-返回,即函数可以让出,和重入。

生成器的基本使用,定义、生成、执行

function foo () {
    yield 'first record';
    yield 'second record';
}
$gen = foo();
$gen->current(); // out: first record
$gen->current(); // out: second record

// or
foreach($gen as $v) {
    echo $v;
}

// or
while ($gen->valid()) {
    $gen->current();
}

生成器,就是一个包含yield关键字的函数,比较特殊,调用这个函数时,会返回一个迭代器(即实现了Iterator,next valid current),这也是生成器名字的由来
生成器的返回值,和交互

function foo () {
    yield; // null
    yield 'string'; // string
    yield 'key' => 10; // key-val foreach($gen as $k => $v)

    return 'retval'; // php7以后支持,$foo->getReturn();
}

function foo () {
    while ('end' != ($msg = yield)) {
        echo $msg;
        sleep(1);
    }
}
$gen->send('go');
$gen->send('go ahead');
$gen->send('end');

需要注意的是直接send可能导致数据丢失

function foo() {
    yield 1;
    yield 2;
}
$gen = foo();
$gen->send('msg');
echo $gen->current(); // 2

// 正确做法
$gen->current(); // 1
$gen->send('msg');
$gen->current(); // 2

延伸

session

session意为会话,作用是解决无状态的Http协议实现用户登录的问题。用户第一次访问应用时生成一个seesion_id并将会话信息持久化,用户后续访问时均带上该session_id,应用程序籍此区分用户并共享会话信息,用户在退出时注销该session_id。目前直接使用原生的php session机制并不多,大多数是使用一个单点的统一用户会话管理服务,不过php本身的session机制在一些场景依然简单有效。

session_start(); // 开启session,无session_id时创建,并生成文件
$_SESSION; // 超全局数组,保存着session信息
session_unset(); // 清空session信息,并非unset($_SESSION)
session_commit(); // 保存session信息,并结束session
session_destroy(); // 清空session,删除session文件

php的session在使用时有这么几个缺陷和注意点:

  • session_id默认是使用cookie传递,一旦cookie被禁用,需要其他办法兼容,比如session_id应用自己维护传输,或者自动加在url中
  • session信息默认是保存在单机文件中的,这就导致分布式集群无法正常使用,需要覆盖实现php的session回调,借助redis、mysql等集中式存储实现分布式的session
  • php的session过期有些费解
    • 由于判断session过期是要频繁的文件检查,性能原因考虑,配置了一个检查概率,默认1/100
    • 坑爹的以文件最后修改时间为过期依旧,而每次更新会话变量,最后修改时间都会刷新
    • 由于默认目录是/tmp,假如服务器部署多个应用而没有调整时,超时时间短的应用会导致长的被清理

延伸

取值范围

php中整型不区分int、short、long,统一全是long,且始终为有符号,位数与平台有关,32位系统取值范围为-2147483648到2147483647(正负21亿,共10位,-232~232-1),64位系统上为-9223372036854775808到9223372036854775807(正负19位)

浮点型不区分float和double,统一全是double,且始终有符号,位数与平台有关,具体表示数值的范围需要根据精度而定,php默认配置的精度是14位有效数字。32位系统中1位符号8位精度23位尾数,64位系统中1位符号11位精度52位尾数

调试

xdebug

xhprof

xdebug虽然强大,但由于采集的信息比较多,对性能影响较大无法在实际生产环境使用,导致一些问题难以发现。xhprof是Facebook发布的一款轻量级性能分析器,性能影响小,能够用于生产环境,同时收集的信息也能满足大部分分析需求,主要是function级别的,包括运行时间、运行次数、cpu占用、内存占用等。需要注意由于Facebook不再维护,官方版并不支持php7,github另外一个分支维护了php7版本

xhprof需要额外安装扩展,需要在应用程序中显示开启、终止分析,会生成性能分析文件,借助可视化工具,可清晰的看到性能瓶颈

延伸
github项目地址

单元测试

单测自己的使用经验有限,但也尝到了甜头。PHP的单元测试主要借助PHPunit,可以通过composer安装。简单记录几个使用经验

  • 最好在调试阶段就开始使用phpunit,既可以第一时间实际使用类/方法,发现错误与难用之处;也可以持续积累case,方便后续回归
  • TDD,所谓测试驱动,是先通过测试用例确认软件行为,然后再实现软件来通过测试用例,达到明确的需求确认和保证软件质量。但由于测试用例比较耗费精力,所以我一般用在小模块开发中
  • phpunit的--filter选项比较有用,可以方便的测试指定的用例
  • 将case放在一个回滚的事务中,可以在自动测试完后恢复现场
  • phpunit中的data准备回调非常好用,可以自动准备、恢复测试数据,保证每次测试的现场一致可复现

延伸
PHPUnit手册
如何有效的书写项目单元测试

gdb

延伸

安全

加密

加密的一个原则是绝对不要明文存储、传输密码,一般使用一种单向算法对密码加密,以前常用md5+salt的方式,但由于彩虹表的出现,md5不再安全,目前PHP中比较安全的是使用bcrypt算法加密。这个算法故意被设计的比较耗时(单次大约毫秒级),从而增加破解成本。

// 计算hash,可以指定算法、计算因子,会自动补充salt
$pwd = 'password';
$hash = password_hash($pwd, PASSWORD_DEFAULT, ['cost' => 10]);

// 调试,可以看到hash值中同时保存了salt,和配置信息(算法、因子等)
$pwdInfo = password_get_info($hash);

// 验证,就这么简单
$bolEqual = password_verify($pwd, $hash);

// 维护,可以自动判断hash是否满足当前配置,籍此可判断是否需要重新生成hash
$bolNeedRehash = password_needs_rehash($hash, PASSWORD_DEFAULT, ['cost' => 15]);

延伸
password_hash、password_verify、password_needs_rehash手册

组件 & 扩展 & 工具

自动加载

PHP中所谓的自动加载是class的命名空间与类名去解析class文件真实path并require,映射规则一般使用官方推荐的PSR-4,大概为一个文件只包含一个class、trait、interface,同时namespace要与文件路径一致。这样利用PHP内置的类加载器注册机制便可以自动require了。一个典型的autoloader.php如下:

<?php
    /**
     * 使用SPL组册这个自动加载函数后,遇到下述代码时这个函数会尝试   从/path/to/project/src/Baz/Qux.php文件中加载\Foo\Bar\Baz\Qux类:
     *  new \Foo\Bar\Baz\Qux;
     * @param string $class 完全限定的类名。
     * @return void
     **/
    spl_autoload_register(function ($class) {
        // 项目的命名空间前缀
        $prefix = 'Foo\\Bar\\';
    
        // 目录前缀对应的根目录
        $base_dir = __DIR__ . '/src/';
    
        // 判断传入的类是否使用了这个命名空间前缀
        $len = strlen($prefix);
        if (strncmp($prefix, $class, $len) !== 0) {
            // 没有使用,交给注册的下一个自动加载器处理
            return;
        }
    
        // 获取去掉前缀后的类名
        $relative_class = substr($class, $len);
    
        // 把命名空间前缀替换成根目录,
        // 在去掉前缀的类名中,把命名空间分隔符替换成目录分隔符,
        // 然后在后面加上.php
        $file = $base_dir . str_replace('\\', '/', $relative_class) . '.php';
    
        // 如果该文件存在,就将其导入
        if (file_exists($file)) {
            require $file;
        }
    });

composer

类似apt-get yum npm等,composer是PHP的类管理器,可以自动下载更新class、库、项目,管理项目依赖,结合目前最大的PHP包分享社区packagist可以非常方便的完成项目环境搭建

延伸
PHP最流行的包分享packagist
Composer使用手册

底层原理

生命周期

SPAI
Server Application Programming Interface,服务应用编程接口,php一切的开始,简单说作用是负责隔离php程序的执行与运行环境,使得无论是cli、fpm、webserver module,php代码都可以正常运行

php生命周期

php的生命周期如图所示,fpm中主要是启动请求周期中循环,cli中则仅仅是一次完整的流程。简要说明一下各个环节的主要作用:

  • MINIT,模块初始化阶段,只执行一次
    • 激活SAPI
    • 初始化垃圾回收器
    • 启动zend引擎
    • 注册PHP内置常量
    • 解析php.ini
    • 注册$_GET、$_POST等超全局变量及处理回调
    • 动态加载 .so
    • 回调扩展minit方法
  • RINIT,请求初始化阶段,每次请求到达时都会执行
    • 重置zend引擎,重置编译器、符号表等
    • 重置垃圾回收器
    • 回调扩展rinit
  • EXECUTE,脚本执行阶段
    • 加载php脚本
    • 预编译、词法解析、语法解析,生成抽象语法树(AST)
    • 编译生成中间码,op_code
    • ZendVM执行op_code
  • RSHUTDOWN,请求关闭阶段
    • flush输出内容
    • 发送http response header
    • 清理全局变量、关闭编译器
    • 关闭内存管理器
    • 回调扩展rshutdown
  • MSHUTDOWN,模块关闭阶段
    • 关闭zend引擎
    • 回调扩展mshutdown
    • 清理资源

延伸

zval zend_value zend_reference 和 CoW

PHP7中一个重要改动是变量数据结构的优化,将原来的变量的数据结构从一个zval拆为zval、zend_value,并将原zval中引用计数移入zend_value中,可以简单理解为zval是变量名,zend_value是实际的变量值,减少了内存占用,结构更清晰。另一个变化是 PHP7中整形,浮点型,布尔型,NULL是直接保存在zval中,无zend_value,所以无引用计数;另外字符串虽然有zend_value,但由于是程序结束后统一回收,所以也没引用计数

zend_reference则是原来的是否为引用变量标记位,在PHP7中扩展为一种数据类型。生成引用类型的唯一方法就是使用&$val,直接复制一个引用类型的变量是无法复制引用的,而对象和资源复制则默认为引用复制

struct _zval_struct {
    zend_value  value; // 变量实际值
    ……
};

struct _zend_value {
    zend_refcounted *counted; // gc头部
    zend_reference  *ref;   // 引用类型
    ……
}

实际测试一下

<?php
    /* 引用计数 */
    $a = 1; // a[zval]
    xdebug_debug_zval('a'); // a: (refcount=0, is_ref=0)=1

    $b = 'string'; // b[zval] -> 'string'[zend_value]
    xdebug_debug_zval('b'); // b: (refcount=1, is_ref=0)='string'

    $c = []; // c[zval] -> Array[zend_value]
    xdebug_debug_zval('c'); // c: (refcount=2, is_ref=0)=array ()
    
    $b_1 = $b; // b_1[zval] b[zval] -> 'string'[zend_value]
    xdebug_debug_zval('b'); // b: (refcount=1, is_ref=0)='string'
    xdebug_debug_zval('b_1'); // b_1: (refcount=1, is_ref=0)='string'

    $c_1 = $c; // c_1[zval] c[zval] -> Array[zend_value]
    xdebug_debug_zval('c'); // c: (refcount=3, is_ref=0)=array ()
    xdebug_debug_zval('c_1'); // c_1: (refcount=3, is_ref=0)='array ()

    /* 引用类型 */
    $r_c = &$c; // r_c[zval] c[zval] -> [zend_reference] -> Array[zend_value]
    xdebug_debug_zval('c'); // c: (refcount=2, is_ref=1)=array ()
    xdebug_debug_zval('r_c'); // r_c: (refcount=2, is_ref=1)=array ()

    $d = $r_c; // c[zval] -> Array[zend_value]
    xdebug_debug_zval('d'); // d: (refcount=4, is_ref=0)=array (),d并不是引用变量,但他们仍然指向同一个zend_value

    /* CoW */
    $d = [];
    xdebug_debug_zval('d'); // d: (refcount=2, is_ref=0)=array (),写时复制,d指向了新的zend_value

    $a = [];
    xdebug_debug_zval('a'); // a: (refcount=2, is_ref=0)=array ()

    $b = $a; // 此时公用zend_value
    xdebug_debug_zval('a'); // a: (refcount=3, is_ref=0)=array ()
    xdebug_debug_zval('b'); // b: (refcount=3, is_ref=0)=array ()

    $c = &$a; // a、c为引用变量,b单独,但他们依旧指向同一zend_value
    xdebug_debug_zval('a'); // a: (refcount=2, is_ref=1)=array ()
    xdebug_debug_zval('b'); // b: (refcount=3, is_ref=0)=array ()
    xdebug_debug_zval('c'); // c: (refcount=2, is_ref=1)=array ()

    $c = 'new'; // 发生变量分离
    xdebug_debug_zval('a'); // a: (refcount=2, is_ref=1)='new'
    xdebug_debug_zval('b'); // b: (refcount=2, is_ref=0)=array ()
    xdebug_debug_zval('c'); // c: (refcount=2, is_ref=1)='new'

在前面的代码中一并演示了Cow,写时复制,变量复制时,PHP解释器并不会真的复制内存,而是当变量的值实际发生变化时才真正复制内存,从而减少内存消耗,提高性能

但引用类型的加入导致CoW有一点疑惑,在PHP5中由于只有zval,所以即便变量没有修改值,但由于&操作变为引用类型,则会导致一次隐性的变量分离,潜在内存溢出风险,不易排查。不过PHP7中由于zval分离,zend_value没有变化时,便不会发生CoW,算是修复了这个缺陷

最后看一个引用类型导致的诡异现象

<?php
    $foo['love'] = 1;
    $bar = &$foo['love'];
    $tipi = $foo;
    $tipi['love'] = '2';
    echo $foo['love']; // 1 or 2?

延伸

Hash Table

HashTable,散列表,PHP的杀手锏array便使用的该数据结构,既可以O(1)读,也可以按序遍历数据,其大致结构如下:

hashtable.jpg

插入一个新key,先经过散列函数映射到中间映射表里,其中记录着元素数组的位置,中间映射表与元素数组为内存中连续空间。当直接访问key值时与插入步骤一样,当遍历数组时则直接依次读元素数组

gc回收

PHP提供了自动内存管理功能,zend_value中记录了变量值的引用次数,当我们使用unset时便会减少引用次数,当引用次数减少为0时便会触发内存回收

循环引用 无法使用该方法回收,比如某个数组中元素又引用了数组本身。针对这种情况,PHP会在引用计数减少时收集可能的垃圾变量,然后定期对收集的变量进行深度遍历,在遍历中引用次数依次减一,若存在循环引用则最终引用计数将变为0,变量回收

延伸
PHP垃圾回收机制

JIT

JIT,just in time,即时编译,是解释型语言的一种性能优化手段。虽然编译型语言与解释型语言都有编译阶段,但不同的是编译型语言是直接将源码编译为机器码,而解释型语言则是编译为字节码供虚拟机执行,导致运行性能折损。而JIT技术,则是实时在程序运行期间,将热点代码直接编译为机器码,从而提高性能。PHP已经确定将在PHP8中引入JIT技术,将PHP的运行性能提高到一个新的台阶

那为什么不直接编译为机器码运行呢? 直接编译运行称为事前编译AOT(ahead of time),php也是支持这种做法的,但这样做有几个缺点,首先背离php坚持的一贯简单原则,项目的发布上线需要提前编译,对研发者也不够友好,而且JIT由于可以收集大量运行时信息,编译的优化效果更好,综合来说,JIT更适合一些

JIT也并非没有缺点 首先是JIT是需要额外消耗性能的,如果程序中无明显热点代码,或者运行周期比较短,则JIT反而会导致性能下降

php引入JIT后的性能变化


jit.png

社区

FIG & PSR

PHP令人诟病的问题之一是框架太多,ThinkPHP、CI、Yii、Zend、Laravel……,虽然有各自的适合场景,但由于风格设计不统一,一些公共组件如日志、缓存、http请求无法公用,反复造轮子,浪费精力。PHP-FIG PHP Framework Interop Group 目标便是解决这个问题,它从代码规范、接口标准、自动加载几个角度持续推荐了一批规范 这些推荐标准便是 PSR PHP Standards Recommendation,并不强制,各框架开发者自行决定是否支持,任何人都可以向FIG反馈意见

目前已经初见成效,目前最流行的Laravel框架便大量使用了公共组件,提高开发效率

延伸

架构

实践

PHP7比较常用的变化

PHP7发布后除了性能上的巨大变化外,在语言的规范、语法糖和许多细节地方都做了许多改进,这里只记录一些我认为经常涉及的改动

// 新增??三元操作符,类似?:
$foo = $a ?? 'nothing';
$foo = isset($a) ? $a : 'nothing';

// 常量数组
define('OPTION', [1, 2, 3,]);

// json_encode支持unicode不转码
json_encode(['一', '二',], JSON_UNESCAPED_UNICODE);

// 数组解包
$a = [1, 2, 3];
$b = [1, 2, ...$a, 3,]; // $b为[1, 2, 1, 2, 3, 3,]

// 箭头函数,一个语法糖
$factor = 10;
$nums = array_map(fn($n)=>$n * $factor,[1,2,3]); // [10,20,30]
// 之前的写法
$nums = array_map(function($num)use($factor){
  return $num * $factor;
},[1,2,3]);

// group use
use app\model\service\{User, Task, Log};

正则

正则能够替代大部分字符串相关工作,而且非常高效方便,只是性能问题不适合密集计算场景。PHP7中废弃了ereg_,保留preg_。此处记录使用正则过程中的经验

// 常用的三个为
preg_replace($pattern, $replace, $subject);
preg_match($pattern, $subject, &$match); // 只匹配第一个
preg_match_all($pattern, $subject, &$match); // 可匹配多个

常见的正则网上一堆,这里只积累一些易错的地方:

  • [家|(春秋)] 与 [家|春秋]有区别吗?有的,中括号中'|'就是匹配竖线,并非‘或’的意思,所以第一个意思为:匹配包含家或竖线或春秋;第二个意思为:匹配包含家或竖线或春或秋
  • 处理Unicode时别忘了加u,/pattern/u

延伸

输入/输出过滤、转义

一般场景使用urlencode(url中特殊字符转义,如汉字、空格),http_build_query(urlencode的封装),htmlspecialchars和html_entity_decode是对html实体转义(如< >)

如果应对复杂安全性要求更高的场景,建议学习并使用htmlpurifier

文件操作

常用的文件读写为feof fread fwrite fgets fget file_get_contents file_put_contents fgetcsv fputcsv

延伸

执行外部命令

PHP执行外部命令并不友好,提供了多种方式,令人迷惑,自己目前并不清楚底层差别,只是从函数行为角度加以区分

// exec最常用
$last_line = exec(string $cmd, array &$out, int &$retval);

// system会在方法中输出命令内容
$last_line = system(string $cmd, int &$retval);

// 返回值为cmd执行结果的字符串
$out = shell_exec(string $cmd, int &$retval);
$out = `$cmd`;

// 命令与system的最大区别是,直接将执行结果输出到浏览器,支持二进制
passthru($cmd, int &$retval);

延伸

字符串操作

PHP提供了很多方便的字符串函数,常用的有:

  • strstr ( string $haystack , mixed $needle [, bool $before_needle = false ] )。返回 haystack 字符串从 needle 第一次出现的位置开始到 haystack 结尾的字符串。若为before_needle为 TRUE,strstr() 将返回 needle 在 haystack 中的位置之前的部分。
  • substr( string $string , int $start [, int $length ] )。返回字符串 string 由 start 和 length 参数指定的子字符串。
  • substr_replace ( mixed $string , mixed $replacement , mixed $start [, mixed $length ] )。substr_replace() 在字符串 string 的副本中将由 start 和可选的 length 参数限定的子字符串使用 replacement 进行替换。
  • strrev ( string $string )。返回 string 反转后的字符串。
  • str_replace ( mixed $search , mixed $replace , mixed $subject [, int &$count ] )。该函数返回一个字符串或者数组。该字符串或数组是将 subject 中全部的 search 都被 replace 替换之后的结果。subject为执行替换的数组或者字符串。也就是 haystack。如果 subject 是一个数组,替换操作将遍历整个 subject,返回值也将是一个数组。如果count被指定,它的值将被设置为替换发生的次数。
  • strpos ( string $haystack , mixed $needle [, int $offset = 0 ] )。返回 needle 在 haystack 中首次出现的数字位置;如果提供了offset参数,搜索会从字符串该字符数的起始位置开始统计。 如果是负数,搜索会从字符串结尾指定字符数开始。
  • ltrim()rtrim()trim()。这仨都是删除字符串中的空白符。ltrim()删除字符串开头的空白字符;rtrim()删除字符串末端的空白字符;trim()去除字符串首尾处的空白字符。

数组操作

这篇文章总结的非常好,不做赘述。PHP数组使用之道

非阻塞与并行操作

在遇到慢速操作导致fpm进程积压时,可以使用fastcgi_finish_request()方法触发请求结束,但fpm进程继续执行慢速操作

另一个有用的场景是批量网络请求或io,若不使用非阻塞方法,则需要依次进行,效率极其低下,可以使用curl_muti_*和非阻塞socket,详见PHP的非阻塞或并行请求实现方式

除此之外其他常用的办法可以直接借助系统命令nohup,或者使用多进程编程

多进程编程

PHP多进程编程中几点心得

fork子进程和父进程的分道扬镳
以前总是好奇那种if pid>0写分支的方式是否足矣支持复杂编程,“没有什么计算机问题是一层抽象解决不了,如果不能解决,就再抽象一层”,确实通过简单抽象,再结合exit()便能很清晰的控制逻辑边界

子进程会继承父进程打开的资源(文件描述符)
以前对这句话理解不深,这次数据库连接的子进程关闭算是结实的上了一课,也学到了php运行模式在多进程方面的一个缺陷(进程结束后释放所有资源)。为了避免混乱,资源的申请若不需要共享,一定要控制好申请的时机。

另外一个关于该点的坑是,标准输入、输出、错误,也是父进程打开的资源,同样会被继承(导致exec不能退出)

进程守护化
实现进程的守护化需要进行以下几个步骤:

  1. fork一次,父进程退出,子进程认1作父,自成一个进程组组长
  2. setsid,重新创建一个会话,成为一个会话组(包含多个进程组)的组长
  3. 改变工作目录为/,与当前启动目录解耦
  4. 关闭标准输入、输出,重新定向到文件或者/dev/null,避免资源泄露
  5. umask(0),避免父进程继承权限掩码

实用代码片段

php管理

# 查看配置文件位置
php --ini
# 指定加载php.ini的绝对路径
php -c another.ini

# 查看phpinfo
php -i

# 查看扩展目录
php-config --extension-dir
# 查看扩展模块,注意扩展模块是可以通过修改ini不启用的,内置的不行
php -m
# 查看摸个扩展的信息
php --ri swoole
# 查看某个扩展提供了哪些类和函数
php --re swoole

# 启动一个内置的Web服务器,用于开发环境内进行程序的调试
php -S 0.0.0.0:9000 [-t /data/webroot/]

# 检测一个php代码文件是否有语法错误
php -l file

# 执行一段php代码
php -r "echo 'hello world';"

php-fpm管理

# php在5.3.3之前fpm是需要自己打补丁的
# 然后在管理时
php-fpm [start|stop|reload]

# 5.3.3之后则已加入源码中,只需要编译中开启即可
# 关于php-fpm的编译参数有
–enable-fpm –with-fpm-user=www –with-fpm-group=www –with-libevent-dir=libevent_path

# 还有个变化是,必须通过信号管理fpm
# SIGINT, SIGTERM 立刻终止  
# SIGQUIT 平滑终止  
# SIGUSR1 重新打开日志文件  
# SIGUSR2 平滑重载所有worker进程并重新载入配置和二进制模块
kill -SIGINT `cat fpm.pid`

配置修改

ini_set('memory_limit', '200M');

数组去重

# 正常方法
$array = array_unique($array);

# 快速方法,key与val连续翻转
$array = array_flip($array);
$array = array_flip($array);

# 但这样还有个问题就是,若是索引数组,则索引乱序,可以直接使用array_keys
$array = array_flip($array);
$array = array_keys($array);

输出所有已定义的常量

print_r(get_defined_constants());

curl

# 以前我们通过 PHP 的 cURL 上传文件是,是使用“@+文件全路径”的来实现的:
curl_setopt(ch, CURLOPT_POSTFIELDS, array(
    'file' => '@'.realpath('image.png'),
));

# PHP 从 5.5 开始引入了新的 CURLFile 类用来指向文件,CURLFile 类也可以详细定义 MIME 类型、文件名等可能出现在multipart/form-data 数据中的附加信息,PHP 推荐使用 CURLFile 替代旧的@语法
# 而PHP 5.6 直接只支持 CURLFile 方法
curl_setopt(ch, CURLOPT_POSTFIELDS, [
    'file' => new CURLFile(realpath('image.png')),
]);

时间处理

// 获取上个月第一天及最后一天,下个月同理
date('Y-m-01', strtotime('-1 month'));
date('Y-m-t', strtotime('-1 month'));

// 获取当月第一天及最后一天.
date('Y-m-01', time());
date('Y-m-t', time());

// 当前年份
date('Y');
// 当前月份
date('m');
// 当前几号
date('d');
// 本月天数,因为t为最后一天的号
date("t");

文件操作

// 遍历文件夹,加载文件
foreach ($arrRequireDir as $requireDir) {
  $objDir = dir($requireDir);

  while ($file = $objDir->read()) {
      $filePath = $requireDir . $file;

      if (is_file($filePath) && ($filePath != __FILE__)) {
          var_dump($filePath);
          include_once($filePath);
      }
   }
}

编码转换

// utf8转big5,ignore跳过无法编码
$T = iconv("utf8","big5//ignore", $T);
$T = mb_convert_encoding($T, "big5", "utf8");

令人迷惑

PHP由于初期的野蛮生长,即便进入PHP7时代,语言中依然保留了大量令人迷惑的地方:

面试常见

有些知识在实际开发中很少遇到,或者遇到就要把始作俑者叉出去,但在面试中却喜闻乐见:

学习资料

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269