中高级PHP实践总结
# 1. PSR规范
PSR 是 PHP Standard Recommendations 的简写,由 PHP FIG 组织制定的 PHP 规范,是 PHP 开发的实践标准。
目前已表决通过了 6 套标准,已经得到大部分 PHP 框架的支持和认可。
- PSR-1:基础编码规范
- PSR-2:编码风格规范
- PSR-3:日志接口规范
- PSR-4:自动加载规范
- PSR-5:缓存接口规范
- PSR-7:HTTP 消息接口规范
# 1.1 PSR-1 基础编码规范
- PHP 代码文件 必须 以 <?php 或 <?= 标签开始;
- PHP 代码文件 必须 以 不带 BOM 的 UTF-8 编码;
- PHP 代码中 应该 只定义类、函数、常量等声明,或其他会产生 副作用 的操作(如:生成文件输出以及修改 .ini 配置文件等),二者只能选其一;
- 命名空间以及类 必须 符合 PSR 的自动加载规范:PSR-4 中的一个;
- 类的命名 必须 遵循 StudlyCaps 大写开头的驼峰命名规范;
- 类中的常量所有字母都 必须 大写,单词间用下划线分隔;
- 方法名称 必须 符合 camelCase 式的小写开头驼峰命名规范。
# 1.2 PSR-2 编码风格规范
- 代码 必须 遵循 PSR-1 中的编码规范 。
- 代码 必须 使用 4 个空格符而不是「Tab 键」进行缩进。
- 每行的字符数 应该 软性保持在 80 个之内,理论上 一定不可 多于 120 个,但 一定不可 有硬性限制。
- 每个 namespace 命名空间声明语句和 use 声明语句块后面,必须 插入一个空白行。
- 类的开始花括号({) 必须 写在类声明后自成一行,结束花括号(})也 必须 写在类主体后自成一行。
- 方法的开始花括号({) 必须 写在函数声明后自成一行,结束花括号(})也 必须 写在函数主体后自成一行。
- 类的属性和方法 必须 添加访问修饰符(private、protected 以及 public),abstract 以及 final 必须 声明在访问修饰符之前,而 static 必须 声明在访问修饰符之后。
- 控制结构的关键字后 必须 要有一个空格符,而调用方法或函数时则 一定不可 有。
- 控制结构的开始花括号({) 必须 写在声明的同一行,而结束花括号(}) 必须 写在主体后自成一行。
- 控制结构的开始左括号后和结束右括号前,都 一定不可 有空格符。
# 1.3 PSR-3 日志接口规范
- 日志接口提供了8个级别的日志方法包含(debug, info,notice,warning,error,critical,alert,emergency),log方法接受日志级别作为第一个参数
- 每一个方法都接受一个字符串作为一个描述,或者是实现了__toString()方法的类
- 每一个方法都接受一个数组类型的上下文数据
- Psr\Log\AbstractLogger是实现日志接口的好的方式,
Psr\Log\LoggerTrait
仅仅需要实现log方法,Psr\Log\NullLogger
空日志接口,Psr\Log\LoggerAwareInterface
可以wrapper一个logger,Psr\Log\LogLevel
包含8个日志级别
# 1.4 PSR-4 自动加载规范
- 适用于类、接口、traits和其他相似的结构
- 一个完全限定的类名具备如下结构
\NamespaceName\SubNamespaceName*\ClassName
, 即包含顶级的命名空间as a vendor namespace
,或许包含一个或者多个子命名空间,必须包含一个类名,下划线不再具有任何意义,类名大小写敏感。自动加载不能抛出异常,不报错,不返回任何值
# 1.5 PSR-7 HTTP 消息接口规范
- 包含
Psr\Http\Message\RequestInterface
和Psr\Http\Message\ResponseInterface
,这两个接口都是扩展Psr\Http\Message\MessageInterface
而来 - http头大小写不敏感
# 2. Composer的自动加载机制
Composer是 用PHP开发的用来管理项目依赖的工具,当你在项目中声明了依赖关系后,composer可以自动帮你下载和安装这些依赖库,并实现自动加载代码。
定义一个composer.json:
{
"name": "gitlib/composer",
"require":{
"predis/predis":"1.1.1"
}
}
2
3
4
5
6
输入命令composer install
,composer会帮我们自动下载predis库,依赖库会默认放在项目的vendor目录下。
├── composer.json
├── composer.lock
├── index.php
└── vendor
├── autoload.php
├── composer
│ ├── ClassLoader.php
│ ├── LICENSE
│ ├── autoload_classmap.php
│ ├── autoload_namespaces.php
│ ├── autoload_psr4.php
│ ├── autoload_real.php
│ ├── autoload_static.php
│ └── installed.json
└── predis
└── predis
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
composer不仅仅帮我们处理依赖,还帮我们实现了自动加载。在vendor目录下有一个autoload.php, 只要在我们的项目中引入这个文件就可以自动加载依赖库。
require 'vendor/autoload.php';
$client = new Predis\Client();
$client->set('foo', 'bar');
$value = $client->get('foo');
echo $value;
2
3
4
5
6
可以看到Predis库完全不需要我们手动去加载,只需要require 'vendor/autoload.php'
,composer的自动加载机制会帮我们找到对应的文件并加载。
对于依赖库,composer帮我们处理好了自动加载, 那对于其他的类库,如何实现自动加载呢?
composer主要用到了3种自动加载的方式:Files
、Classmap
、PSR-4
, 其中PSR-4
是当前推荐的加载方式。
# 2.1 Files
Files是最简单的加载方式,这种方式不管加载的文件是否用到始终都会加载,而不是按需加载, 修改项目根目下的composer.json, 加入 “autoload” 项:
{
"name": "gitlib/composer",
"require":{
"predis/predis":"1.1.1"
},
"autoload":{
"files":["Controller/User.php"]
}
}
2
3
4
5
6
7
8
9
files键对应的值是一个数组,数组元素是文件的路径,路径是相对于应用的根目录。加上上述内容后,运行命令:
composer dump-autoload
让composer重建自动加载的信息,composer会把配置值写入与 Files加载方式对应的verndor\composer\autoload_files.php
配置文件中:
// autoload_files.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'7efd69bb86214589340b40039fd363f7' => $baseDir . '/Controller/User.php',
);
2
3
4
5
6
7
8
现在就可以在代码中里调用User类了:
require 'vendor/autoload.php';
$client = new Predis\Client();
$user = new \Controller\User();
$user->login();
2
3
4
5
6
7
# 2.2 Classmap
classmap
引用的所有组合,都会在install/update
过程中生成,并存储到vendor/composer/autoload_classmap.php
文件中。这个map
是经过扫描指定目录(同样支持直接精确到文件)中所有的.php
和.inc
文件里内置的类而得到的。
{
"name": "gitlib/composer",
"require":{
"predis/predis":"1.1.1"
},
"autoload":{
"classmap":["Controller"]
}
}
2
3
4
5
6
7
8
9
Composer会扫描Controller目录下的所有**.php
和.inc**
文件,存储到vendor/composer/autoload_classmap.php
文件中:
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'Controller\\User' => $baseDir . '/Controller/User.php',
);
2
3
4
5
6
7
8
9
# 2.3 PSR-4
PSR-4
是Composer推荐使用的一种方式(关于PSR规范可参考:PHP标准规范PSR),因为它更易使用并能带来更简洁的目录结构。对于上面的Controller目录我们先改名src:
├── composer.json
├── composer.json.bk
├── composer.lock
├── index.php
├── src
│ └── User.php
└── vendor
├── autoload.php
├── composer
└── predis
2
3
4
5
6
7
8
9
10
在composer.json中我们将Controller命名空间和src关联起来:
{
"name": "gitlib/composer",
"require":{
"predis/predis":"1.1.1"
},
"autoload":{
"psr-4": {
"Controller\\":"src/"
}
}
}
2
3
4
5
6
7
8
9
10
11
PSR-4的命名空间前缀也必须以\ 结尾,以避免类似前缀间的冲突。
psr-4中的key和value定义了namespace以及其对应的目录映射。按照PSR-4的规则,当试图自动加载”Controller\User”类的使用,会去寻找”src/User.php”这个文件,此时Controller并不会出现在文件路径中。
# 2.4 自动加载原理
下面我们通过源码分析composer是如何实现自动加载功能。
// 入口
require 'vendor/autoload.php';
2
我们通过require ‘vendor/autoload.php
实现自动加载,vendor/autoloaad.php
文件引用composer/autoload_real.php
。
<?php
// autoload.php @generated by Composer
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInitb84761f57e62a6a534584b91ca213591::getLoader();
2
3
4
5
6
7
# 2.4.1 autoload_real
autoload_real.php
是自动加载引导类,程序主要调用了引导类的静态方法 getLoader()
。
// autoload_real.php @generated by Composer
class ComposerAutoloaderInitb84761f57e62a6a534584b91ca213591
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
public static function getLoader()
{
// 返回Composer\Autoload\ClassLoader单例
if (null !== self::$loader) {
return self::$loader;
}
// 调用spl_autoload_register加载\Composer\Autoload\ClassLoader
spl_autoload_register(array('ComposerAutoloaderInitb84761f57e62a6a534584b91ca213591', 'loadClassLoader'), true, true);
// 实例化\Composer\Autoload\ClassLoader类
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInitb84761f57e62a6a534584b91ca213591', 'loadClassLoader'));
// 静态初始化只支持 PHP5.6 以上版本并且不支持 HHVM 虚拟机
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
if ($useStaticLoader) {
// 使用 autoload_static 进行静态初始化
require_once __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInitb84761f57e62a6a534584b91ca213591::getInitializer($loader));
} else {
// 如果PHP版本低于 5.6 或者使用 HHVM 虚拟机环境,那么就要使用核心类的接口进行初始化
// PSR0 标准
$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) {
$loader->set($namespace, $path);
}
// PSR4 标准
$map = require __DIR__ . '/autoload_psr4.php';
foreach ($map as $namespace => $path) {
$loader->setPsr4($namespace, $path);
}
// classmap
$classMap = require __DIR__ . '/autoload_classmap.php';
if ($classMap) {
$loader->addClassMap($classMap);
}
}
$loader->register(true);
// files
if ($useStaticLoader) {
$includeFiles = Composer\Autoload\ComposerStaticInitb84761f57e62a6a534584b91ca213591::$files;
} else {
$includeFiles = require __DIR__ . '/autoload_files.php';
}
// files定义的文件,直接require就行了
foreach ($includeFiles as $fileIdentifier => $file) {
composerRequireb84761f57e62a6a534584b91ca213591($fileIdentifier, $file);
}
return $loader;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# 2.4.2 autoload_static
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInitb84761f57e62a6a534584b91ca213591
{
public static $files = array (
'7efd69bb86214589340b40039fd363f7' => __DIR__ . '/../..' . '/Controller/User.php',
);
public static $prefixLengthsPsr4 = array (
'P' =>
array (
'Predis\\' => 7,
),
'C' =>
array (
'Controller\\' => 11,
),
);
public static $prefixDirsPsr4 = array (
'Predis\\' =>
array (
0 => __DIR__ . '/..' . '/predis/predis/src',
),
'Controller\\' =>
array (
0 => __DIR__ . '/../..' . '/src',
),
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInitb84761f57e62a6a534584b91ca213591::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInitb84761f57e62a6a534584b91ca213591::$prefixDirsPsr4;
}, null, ClassLoader::class);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
静态初始化类的核心就是getInitializer()
函数,它将自己类中的顶级命名空间映射给了ClassLoader
类。
PSR4标准顶级命名空间映射用了两个数组,第一个是用命名空间第一个字母作为前缀索引,然后是 顶级命名空间,但是最终并不是文件路径,而是顶级命名空间的长度
,为什么呢?
因为PSR4标准是用顶级命名空间目录替换顶级命名空间,所以获得顶级命名空间的长度很重要。
# 2.4.3 ClassLoader
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
includeFile($file);
return true;
}
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
function includeFile($file)
{
include $file;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
ClassLoader的register()
函数将loadClass()
函数注册到PHP的SPL函数堆栈中,每当PHP遇到不认识的命名空间时就会调用函数堆栈的每个函数,直到加载命名空间成功。所以loadClass()
函数就是自动加载的关键了。
# 3. 进阶知识
# 3.1 PHP网络编程
PHP实现系统编程(一) --- 网络Socket及IO多路复用 (opens new window) PHP实现系统编程(二) --- 多进程编程介绍及孤儿进程、僵尸进程 (opens new window) PHP实现系统编程(三) --- 信号 (opens new window) PHP实现系统编程(四)--- 本地套接字(Unix Domain Socket) (opens new window)
# 3.2 SESSION实现机制
会话的工作流程很简单,当开始一个会话时,PHP会尝试从请求中查找会话ID(通常通过会话cookie传递), 如果请求中不包含会话ID信息,PHP就会创建一个新的会话。 会话开始之后,PHP就会将会话中的数据设置到$_SESSION
变量中。 当PHP停止的时候,它会自动读取$_SESSION
中的内容,并将其进行序列化, 然后发送给会话保存管理器来进行保存。
# 3.2.1 会话保持
默认情况下,PHP使用内置的文件会话保存管理器(files)来完成会话的保存。 也可以通过配置项session.save_handler
来修改所要采用的会话保存管理器。 对于文件会话保存管理器,会将会话数据保存到配置项 session.save_path
所指定的位置。
[Session]
; Handler used to store/retrieve data.
; http://php.net/session.save-handler
session.save_handler = files
; The file storage module creates files using mode 600 by default.
; You can change that by using
;
; session.save_path = "N;MODE;/path"
;
; where MODE is the octal representation of the mode. Note that this
; does not overwrite the process's umask.
; http://php.net/session.save-path
;session.save_path = "/tmp"
; Name of the session (used as cookie name).
; http://php.net/session.name
session.name = PHPSESSID
; Initialize session on request startup.
; http://php.net/session.auto-start
session.auto_start = 0
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 3.2.2 Session操作
# 3.2.2.1 创建session
可以通过调用函数session_start()
来手动开始一个会话,系统会分配一个会话ID。 如果配置项session.auto_start
设置为1, 那么请求开始的时候,会话会自动开始。
session_start();
$_SESSION["username"] = "gitlib";
2
session_start()
是session机制的开始,它具有一定概率开启垃圾回收。这个概率是根据php.ini的session.gc_probability
配置项决定的,因为在有的系统中session.gc_probability = 0
,即概率是0,这时就不具备垃圾回收。
; Defines the probability that the 'garbage collection' process is started
; on every session initialization. The probability is calculated by using
; gc_probability/gc_divisor. Where session.gc_probability is the numerator
; and gc_divisor is the denominator in the equation. Setting this value to 1
; when the session.gc_divisor value is 100 will give you approximately a 1% chance
; the gc will run on any given request.
; Default Value: 1
; Development Value: 1
; Production Value: 1
; http://php.net/session.gc-probability
session.gc_probability = 1
session.gc_divisor = 1000
; After this number of seconds, stored data will be seen as 'garbage' and
; cleaned up by the garbage collection process.
; http://php.net/session.gc-maxlifetime
session.gc_maxlifetime = 1440
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
概率是:session.gc_probability/session.gc_divisor
, 结果 1/1000。
# 3.2.2.2 读取session
PHP内置的$_SESSION
变量可以很方便的访问设置的session变量。
例子:
session_start();
echo "用户名:".$_SESSION["username"];
2
# 3.2.2.3 销毁session
可以通过session_unregister()
函数来注销单个session 变量或使用session_unset()
来注销整个 session 会话。
例子:
session_unset(); //注销 session 会话
也可以使用unset()
函数来注销单个session 变量:
<?php
session_start();
unset($_SESSION["username"]);
?>
2
3
4
提示:不可使用unset()
来销毁 session 会话。
# 3.2.3 自定义Session处理机制
默认配置下,Session的持久化是通过文件来存储,我们也可以将Session信息保存到Key-Value数据或Mysql数据库中,可以通过session_set_save_handler()
函数自定义session会话保存管理器。
语法:
# PHP5.4以前版本
session_set_save_handler ( callable $open , callable $close , callable $read , callable $write , callable $destroy , callable $gc [, callable $create_sid [, callable $validate_sid [, callable $update_timestamp ]]] ) : bool
# PHP5.4以后版本
session_set_save_handler ( object $sessionhandler [, bool $register_shutdown = TRUE ] ) : bool
2
3
4
5
session_set_save_handler()
设置用户自定义会话存储函数,如果想使用 PHP 内置的会话存储机制之外的方式,可以使用本函数,例如,可以自定义会话存储函数来将会话数据存储到数据库。下面,以将会话保存的文件为例,说明一下这个函数的用法:
<?php
class FileSessionHandler
{
private $savePath;
// open 回调函数类似于类的构造函数, 在会话打开的时候会被调用。 这是自动开始会话或者通过调用 session_start() 手动开始会话 之后第一个被调用的回调函数。 此回调函数操作成功返回 TRUE,反之返回 FALSE。
function open($savePath, $sessionName)
{
$this->savePath = $savePath;
if (!is_dir($this->savePath)) {
mkdir($this->savePath, 0777);
}
return true;
}
// close 回调函数类似于类的析构函数。 在 write 回调函数调用之后调用。 当调用 session_write_close() 函数之后,也会调用 close 回调函数。
function close()
{
return true;
}
// 如果会话中有数据,read 回调函数必须返回将会话数据编码(序列化)后的字符串。 如果会话中没有数据,read 回调函数返回空字符串。
function read($id)
{
return (string)@file_get_contents("$this->savePath/sess_$id");
}
// 在会话保存数据时会调用 write 回调函数。 此回调函数接收当前会话 ID 以及 $_SESSION 中数据序列化之后的字符串作为参数。
function write($id, $data)
{
return file_put_contents("$this->savePath/sess_$id", $data) === false ? false : true;
}
// 当调用 session_destroy() 函数, 或者调用 session_regenerate_id() 函数并且设置 destroy 参数为 TRUE 时, 会调用此回调函数
function destroy($id)
{
$file = "$this->savePath/sess_$id";
if (file_exists($file)) {
unlink($file);
}
return true;
}
// 为了清理会话中的旧数据,PHP 会不时的调用垃圾收集回调函数。 调用周期由 session.gc_probability 和 session.gc_divisor 参数控制。 传入到此回调函数的 lifetime 参数由 session.gc_maxlifetime 设置。
function gc($maxlifetime)
{
foreach (glob("$this->savePath/sess_*") as $file) {
if (filemtime($file) + $maxlifetime < time() && file_exists($file)) {
unlink($file);
}
}
return true;
}
}
$handler = new FileSessionHandler();
session_set_save_handler(
array($handler, 'open'),
array($handler, 'close'),
array($handler, 'read'),
array($handler, 'write'),
array($handler, 'destroy'),
array($handler, 'gc')
);
// 下面这行代码可以防止使用对象作为会话保存管理器时可能引发的非预期行为
register_shutdown_function('session_write_close');
session_start();
// 现在可以使用 $_SESSION 保存以及获取数据了
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
在脚本执行完毕之后,PHP内部会清除对象, 所以有可能不调用write
和close
回调函数。 这样可能会引发非预期的行为,所以当使用对象作为会话保存管理器时, 需要通过注册shutdown
回调函数来规避风险。 通常,你可以通过调用register_shutdown_function()
函数 来注册session_write_close'
回调函数。
# 3.3 高级语法
# 3.3.1 太空船运算符
<?php
// 整数
echo 1 <=> 1; // 0
echo 1 <=> 2; // -1
echo 2 <=> 1; // 1
// 浮点数
echo 1.5 <=> 1.5; // 0
echo 1.5 <=> 2.5; // -1
echo 2.5 <=> 1.5; // 1
// 字符串
echo "a" <=> "a"; // 0
echo "a" <=> "b"; // -1
echo "b" <=> "a"; // 1
?>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 3.3.2 箭头函数
$posts = [/* … */];
// 之前的写法
$ids = array_map(function ($post) {
return $post->id;
}, $posts);
// 箭头函数的写法
$ids = array_map(fn($post) => $post->id, $posts);
2
3
4
5
6
7
8
9
# 3.3.3 过滤器
如需过滤变量,请使用下面的过滤器函数之一:
filter_var()
- 通过一个指定的过滤器来过滤单一的变量filter_var_array()
- 通过相同的或不同的过滤器来过滤多个变量filter_input()
- 获取一个输入变量,并对它进行过滤filter_input_array()
- 获取多个输入变量,并通过相同的或不同的过滤器对它们进行过滤
var_dump(filter_var('true', FILTER_VALIDATE_BOOLEAN));
var_dump(filter_var('false', FILTER_VALIDATE_BOOLEAN));
//bool(true)
//bool(false)
2
3
4
5
# 3.3.4 number_format
通过千位分组来格式化数字,用法:number_format(number,decimals,decimalpoint,separator)
<?php
echo number_format("1000000")."<br>"; // 1,000,000
echo number_format("1000000",2)."<br>"; // 1,000,000.00
echo number_format("1000000",2,",","."); // 1.000.000,00
?>
2
3
4
5
# 3.3.5 bin2hex()/pack/unpack
把ASCII字符的字符串转换为十六进制值。字符串可通过使用 pack() 函数再转换回去
<?php
$str = bin2hex("Hello World!");
echo($str); // 48656c6c6f20576f726c6421
?>
2
3
4
# 3.3.6 Yield
yield生成器是php5.5之后出现的,官方文档这样解释:yield提供了一种更容易的方法来实现简单的迭代对象,相比较定义类实现 Iterator 接口的方式,性能开销和复杂性大大降低。 生成器的核心是一个yield关键字,一个生成器函数看起来像一个普通的函数,不同的是:普通函数返回一个值,而一个生成器可以yield生成许多它所需要的值。生成器函数被调用时,返回的是一个可以被遍历的对象。 yield和return有点类似,不过不同的是,return会返回值并且终止代码的执行,而yield会返回一个值给循环调用此生成器的代码并且只是暂停执行生成器函数。
// 对某一数组进行加权处理
$numbers = array(‘nike‘ => 200, ‘jordan‘ => 500, ‘adiads‘ => 800);
//通常方法,如果是百万级别的访问量,这种方法会占用极大内存
function rand_weight($numbers)
{
$total = 0;
foreach ($numbers as $number => $weight) {
$total += $weight;
$distribution[$number] = $total;
}
$rand = mt_rand(0, $total-1);
foreach ($distribution as $num => $weight) {
if ($rand < $weight) return $num;
}
}
//改用yield生成器
function mt_rand_weight($numbers) {
$total = 0;
foreach ($numbers as $number => $weight) {
$total += $weight;
yield $number => $total;
}
}
function mt_rand_generator($numbers)
{
$total = array_sum($numbers);
$rand = mt_rand(0, $total -1);
foreach (mt_rand_weight($numbers) as $num => $weight) {
if ($rand < $weight) return $num;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 3.3.7 反射
PHP的反射机制提供了一套反射API,用来访问和使用类、方法、属性、参数和注释等,比如可以通过一个对象知道这个对象所属的类,这个类包含哪些方法,这些方法需要传入什么参数,每个参数是什么类型等等,不用创建类的实例也可以访问类的成员和方法,就算类成员定义为private
也可以在外部访问。 官方文档提供了诸如ReflectionClass
、ReflectionMethod
、ReflectionObject
、ReflectionExtension
等反射类及相应的API,用得最多的是ReflectionClass
。
为了演示反射效果,首先创建一个类(假设定义了一个类 User),并实例化。基于这个实例,反射类可以访问User中的属性和方法。
<?php
/**
* 用户相关类
*/
class User {
public $username;
private $password;
public function __construct($username, $password)
{
$this->username = $username;
$this->password = $password;
}
/**
* 获取用户名
* @return string
*/
public function getUsername()
{
return $this->username;
}
/**
* 设置用户名
* @param string $username
*/
public function setUsername($username)
{
$this->username = $username;
}
/**
* 获取密码
* @return string
*/
private function getPassword()
{
return $this->password;
}
/**
* 设置密码
* @param string $password
*/
private function setPassowrd($password)
{
$this->password = $password;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
创建反射类实例:
$refClass = new ReflectionClass(new User('liulu', '123456'));
// 也可以写成
$refClass = new ReflectionClass('User'); // 将类名User作为参数,建立User类的反射类
反射属性
$properties = $refClass->getProperties(); // 获取User类的所有属性,返回ReflectionProperty的数组
$property = $refClass->getProperty('password'); // 获取User类的password属性
//$properties 结果如下:
Array (
[0] => ReflectionProperty Object ( [name] => username [class] => User )
[1] => ReflectionProperty Object ( [name] => password [class] => User )
)
//$property 结果如下:
ReflectionProperty Object ( [name] => password [class] => User )
2
3
4
5
6
7
8
9
10
11
12
13
反射方法:
$methods = $refClass->getMethods(); // 获取User类的所有方法,返回ReflectionMethod数组
$method = $refClass->getMethod('getUsername'); // 获取User类的getUsername方法
//$methods 结果如下:
Array (
[0] => ReflectionMethod Object ( [name] => __construct [class] => User )
[1] => ReflectionMethod Object ( [name] => getUsername [class] => User )
[2] => ReflectionMethod Object ( [name] => setUsername [class] => User )
[3] => ReflectionMethod Object ( [name] => getPassword [class] => User )
[4] => ReflectionMethod Object ( [name] => setPassowrd [class] => User )
)
//$method 结果如下:
ReflectionMethod Object ( [name] => getUsername [class] => User )
2
3
4
5
6
7
8
9
10
11
12
反射注释:
$classComment = $refClass->getDocComment(); // 获取User类的注释文档,即定义在类之前的注释
$methodComment = $refClass->getMethod('setPassowrd')->getDocComment(); // 获取User类中setPassowrd方法的注释
//$classComment 结果如下:
/** * 用户相关类 */
//$methodComment 结果如下:
/** * 设置密码 * @param string $password */
2
3
4
5
6
反射实例化:
$instance = $refClass->newInstance('admin', 123, '***'); // 从指定的参数创建一个新的类实例
//$instance 结果如下:
User Object ( [username] => admin [password:User:private] => 123 )
注:虽然构造函数中是两个参数,但是newInstance方法接受可变数目的参数,用于传递到类的构造函数。
$params = ['xiaoming', 'asdfg'];
$instance = $refClass->newInstanceArgs($params); // 从给出的参数创建一个新的类实例
//$instance 结果如下:
User Object ( [username] => xiaoming [password:User:private] => asdfg )
2
3
4
5
6
7
8
访问、执行类的公有方法——public:
$instance = $refClass->newInstance('admin', 123, '***'); // 从指定的参数创建一个新的类实例
//$instance 结果如下:
User Object ( [username] => admin [password:User:private] => 123 )
注:虽然构造函数中是两个参数,但是newInstance方法接受可变数目的参数,用于传递到类的构造函数。
$params = ['xiaoming', 'asdfg'];
$instance = $refClass->newInstanceArgs($params); // 从给出的参数创建一个新的类实例
//$instance 结果如下:
User Object ( [username] => xiaoming [password:User:private] => asdfg )
2
3
4
5
6
7
8
访问、执行类的非公有方法——private、protected:
try {
// 正确写法
$property = $refClass->getProperty('password'); // ReflectionProperty Object ( [name] => password [class] => User )
$property->setAccessible(true); // 修改 $property 对象的可访问性
$property->setValue($instance, '987654321'); // 可以执行
$value = $property->getValue($instance); // 可以执行
echo $value . "\n"; // 输出 987654321
// 错误写法
$refClass->getProperty('password')->setAccessible(true); // 临时修改ReflectionProperty对象的可访问性
$refClass->getProperty('password')->setValue($instance, 'password'); // 不能执行,抛出不能访问异常
$refClass = $refClass->getProperty('password')->getValue($instance); // 不能执行,抛出不能访问异常
$refClass = $instance->password; // 不能执行,类本身的属性没有被修改,仍然是private
} catch (Exception $e){
echo $e;
}
// 错误写法 结果如下:
ReflectionException: Cannot access non-public member User::password in xxx.php
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
小结:
- 不管反射类中定义的属性、方法是否为
public
,都可以获取到。 - 直接访问
protected
或则private
的属性、方法,会抛出异常。 - 访问非公有成员需要调用指定的
ReflectionProperty
或ReflectionMethod
对象setAccessible(true)
方法。
# 3.3.8 参数传递
- php中,数组是当一个普通变量,值传递是要一个实参的一个拷贝副本,跟实参无关,引用传递后可以改变实参的值
- 类的对象是无论值传递和引用传递都是引用传递,是对对象的引用,都会改变实参的值
# 3.3.9 单引号/双引号的区别
- 双引号可以解析变量,单引号不能解析变量
- 双引号和单引号可以互相嵌套
- 双引号当中的变量可以使用特殊字符分隔开,但是特殊字符会原样输出,使用{}不会输出
- 双引号当中包含单引号,单引号当中包含变量,变量会被解析,单引号会被原样输出
- 双引号可以解析转义字符,单引号不会解析转义字符,单引号只会解析*本身和’*单引号本身的转义
- 单引号当中嵌套单引号,双引号当中嵌套双引号,当中的单引号和双引号需要使用****转义符合
- 单引号效率要高于双引号
# 3.3.10 ??与?:区别
$a=$c??$b; // 等同于 $a=isset($c)?$c:$b;
$a=$c?:$b; // 等同于 $a=$c?$c:$b;
2
# 3.3.11 Clone
(clone $tableQuery)
->select('date')
->addSelect(Db::raw('sum(coin_amount) as coin_amount'))
->addSelect(Db::raw('sum(amount) as amount'))
->where('status', 8)
->groupBy('date')
->orderByDesc('date')
->get()
->keyBy('date')
->map(function ($item) use (&$info) {
$info[$item->date]['coin_amount'] = $item->coin_amount;
$info[$item->date]['amount'] = $item->amount;
});
2
3
4
5
6
7
8
9
10
11
12
13
# 3.3.12 魔法方法
__call() 对象调用某个方法,若方法存在,则直接调用;若不存在,则会去调用__call函数。(此方法工作中很常见,很常用)
__get() 读取一个对象的属性时,若属性存在,则直接返回属性值;若不存在,则会调用__get函数。
__set() 设置一个对象的属性时,若属性存在,则直接赋值;若不存在,则会调用__set函数。
__toString() 打印一个对象的时被调用。如echo $obj;或print $ob
__clone() 克隆对象时被调用。如:$t=new Test();$t1=clone $t;
__sleep() serialize(序列化)之前被调用。若对象比较大,想删减一点东东再序列化,可考虑一下此函数。
__wakeup() unserialize(反序列化)时被调用,做些对象的初始化工作。
__isset() 检测对象一个不存在的属性时被调用。如:isset($c->name)。
__unset() 删除对象一个不存在的属性时被触发。如:unset($c->name)。
__set_state() 调用var_export时,被调用。用__set_state的返回值做为var_export的返回值。
__autoload() 实例化一个对象时,如果对应的类不存在,则该方法被调用。(此方法,工作中很常用,尤其自己封一个框架用的时候,避免需要引入很多类文件,可以使用此方法)
2
3
4
5
6
7
8
9
10
11
# 3.3.13 其他
set_error_handler(error_function,error_types); // 设置用户自定义的错误处理函数。
set_exception_handler(exception_function); // 用户自定义的异常处理函数,只存在一个回调函数
register_shutdown_function('_shutdown_handler'); // 注册一个会在php中止时执行的函数,注册一个 callback ,它会在脚本执行完成或者 exit() 后被调用,可以多次调用 register_shutdown_function() ,这些被注册的回调会按照他们注册时的顺序被依次调用。 如果你在注册的方法内部调用 exit(), 那么所有处理会被中止,并且其他注册的中止回调也不会再被调用。
debug_backtrace(); // 生成 backtrace
ucfirst(string $str): string // 将字符串的首字母转换为大写
extension_loaded(string $name): bool // 检查一个扩展是否已经加载
ini_set(string $varname , string $newvalue):string //设置指定配置选项的值。这个选项会在脚本运行时保持新的值,并在脚本结束时恢复.不是所有有效的选项都能够用 ini_set() 来改变的
number_format(float $number , int $decimals = 0): string // 以千位分隔符方式格式化一个数字
version_compare() // 对比两个「PHP 规范化」的版本数字字符串
str_pad(string $input, int $pad_length, string $pad_string = " ", int $pad_type = STR_PAD_RIGHT):string // 使用另一个字符串填充字符串为指定长度
memory_get_usage() // 返回分配给 PHP 的内存量
getrusage(int $who = 0): array // 取当前资源使用状况
is_callable(); // 检测参数是否为合法的可调用结构
instanceof(); // 用于确定一个 PHP 变量是否属于某一类 class 的实例
get_object_vars():array // 返回由对象属性组成的关联数组
get_class_vars():array // 返回由类的默认属性组成的数组
glob(string $pattern, int $flags = 0):array // 寻找与模式匹配的文件路径
ord(); // 转换字符串第一个字节为 0-255 之间的值,ASCI码
pack(format,args+); // 把数据装入一个二进制字符串
unpack(format,data); // 从二进制字符串对数据进行解包
random_bytes() // 加密生存被保护的伪随机字符串。
random_int() // 加密生存被保护的伪随机整数。
# 常量
PHP_VERSION
DIRECTORY_SEPARATOR
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 4. Swoole
# 4.1 协程是什么
协程可以理解为纯用户态的线程,其通过协作而不是抢占来进行切换,相对于进程或者线程,协程所有的操作都可以在用户态完成,创建和切换的消耗更低,Swoole可以为每一个请求创建对应的协程,根据IO的状态来合理的调度协程。
在Swoole 4.x中,协程(Coroutine)取代了异步回调,成为Swoole推荐的编程方式。Swoole协程解决了异步回调编程困难的问题,使用协程可以以传统同步编程的方法编写代码,底层自动切换为异步IO,既保证了编程的简单性,又可借助异步 IO,提升系统的并发能力。
注:Swoole 4.x 之前的版本也支持协程,不过 4.x 版本对协程内核进行了重构,功能更加强大,提供了完整的协程+通道特性,带来全新的CSP
编程模型。
# 4.2 基本使用示例
- PHP 版本要求:>= 7.0;
- 基于
Server
、Http\Server
、WebSocket\Server
进行开发的时候,Swoole底层会在onRequest
、onReceive
、onConnect
等事件回调之前自动创建一个协程,在回调函数中即可使用协程API; - 你也可以使用
Coroutine::create
或go
方法创建协程,在创建的协程中使用协程API进行编程。
以Swoole自带的TCP服务器Swoole\Server
实现为例,我们可以定义服务器端实现如下:
$server = new \Swoole\Server("127.0.0.1", 9501);
// 调用 onReceive 事件回调函数时底层会自动创建一个协程
$server->on('receive', function ($serv, $fd, $from_id, $data) {
// 向客户端发送数据后关闭连接(在这里面可以调用 Swoole 协程 API)
$serv->send($fd, 'Swoole: ' . $data);
$serv->close($fd);
});
$server->start();
2
3
4
5
6
7
8
9
然后我们以协程方式实现TCP客户端如下:
// 通过 go 函数创建一个协程
go(function () {
$client = new Swoole\Coroutine\Client(SWOOLE_SOCK_TCP);
// 尝试与指定 TCP 服务端建立连接,这里会触发 IO 事件切换协程,交出控制权让 CPU 去处理其他事情
if ($client->connect("127.0.0.1", 9501, 0.5)) {
// 建立连接后发送内容
$client->send("hello world\n");
// 打印接收到的消息(调用 recv 函数会恢复协程继续处理后续代码,比如打印消息、关闭连接)
echo $client->recv();
// 关闭连接
$client->close();
} else {
echo "connect failed.";
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 4.3 底层实现原理
我们以 MySQL 连接查询为例,对Swoole协程底层实现做一个简单的介绍:
$server = new Swoole\Http\Server('127.0.0.1', 9501, SWOOLE_BASE);
$server->on('Request', function($request, $response) {
$mysql = new Swoole\Coroutine\MySQL();
$res = $mysql->connect([
'host' => '127.0.0.1',
'user' => 'root',
'password' => 'root',
'database' => 'test',
]);
if ($res == false) {
$response->end("MySQL connect fail!");
return;
}
$ret = $mysql->query('show tables', 2);
$response->end("swoole response is ok, result=".var_export($ret, true));
});
$server->start();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
在这段代码中,我们启动一个基于Swoole实现的HTTP服务器监听客户端请求,如果有onRequest
事件发生,则通过基于Swoole协程实现的异步MySQL客户端组件对MySQL服务器发起连接请求,并执行查询操作,然后将结果以响应方式返回给 HTTP 客户端,下面我们来看一下协程在这段代码中的应用:
- 调用
Swoole\Http\Server
的onRequest
事件回调函数时,底层会调用C函数coro_create
创建一个协程(#1位置),同时保存这个时间点的CPU寄存器状态
和ZendVM堆栈信息
; - 调用
mysql->connect
时会发生IO
操作,底层会调用C函数coro_save
保存当前协程的状态,包括ZendVM
上下文以及协程描述信息,并调用coro_yield
让出程序控制权,当前的请求会挂起; - 协程让出程序控制权后,会继续进入HTTP服务器的事件循环处理其他事件,这时Swoole可以继续去处理其他客户端发来的请求;
- 当数据库IO事件完成后,MySQL连接成功或失败,底层调用C函数
coro_resume
恢复对应的协程,恢复ZendVM上下文,继续向下执行PHP代码; mysql->query
的执行过程与mysql->connect
一样,也会触发IO事件并进行一次协程切换调度;- 所有操作完成后,调用
end
方法返回结果,并销毁此协程。
注:更深层次的协程底层实现可以参考 Swoole 官方文档的介绍。
上面这段代码我们借助了Swoole实现的协程MySQL客户端(Swoole 还提供了很多其他协程客户端,如 Redis、HTTP等,后面我们会详细介绍),所有的编码和之前编写同步代码时并没有任何不同,但是 Swoole 底层会在 IO 事件发生时,保存当前状态,将程序控制权交出,以便 CPU 处理其它事件,当IO事件完成时恢复并继续执行后续逻辑,从而实现异步IO
的功能,这正是协程的强大之处,它可以让服务器同时可以处理更多请求,而不会阻塞在这里等待IO事件处理完成,从而极大提高系统的并发性。
# 4.4 协程的适用场景
通过上面这个简单的示例,我们得出协程非常适合并发编程,常见的并发编程场景如下:
- 高并发服务,如秒杀系统、高性能 API 接口、RPC 服务器,使用协程模式,服务的容错率会大大增加,某些接口出现故障时,不会导致整个服务崩溃;
- 爬虫,可实现非常强大的并发能力,即使是非常慢速的网络环境,也可以高效地利用带宽;
- 即时通信服务,如 IM 聊天、游戏服务器、物联网、消息服务器等等,可以确保消息通信完全无阻塞,每个消息包均可即时地被处理。
# 4.5 协程引入的问题
协程再为我们带来便利的同时,也引入了一些新的问题:
- 协程需要为每个并发保存栈内存并维护对应的虚拟机状态,如果程序并发很大可能会占用大量内存;
- 协程调度会增加额外的一些 CPU 开销。 尽管如此,在处理高并发应用时,使用协程带来的优势还是远远高于 PHP 默认的同步阻塞机制。
# 4.6 协程 vs 线程
Swoole 的协程在底层实现上是单线程的,因此同一时间只有一个协程在工作,协程的执行是串行的,这与线程不同,多个线程会被操作系统调度到多个 CPU 并行执行。 一个协程正在运行时,其他协程会停止工作。当前协程执行阻塞 IO 操作时会挂起,底层调度器会进入事件循环。当有 IO 完成事件时,底层调度器恢复事件对应的协程的执行。 在 Swoole 中对 CPU 多核的利用,仍然依赖于 Swoole 引擎的多进程机制。
# 4.7 协程 vs 生成器
一些框架中会使用 PHP 的生成器来实现半自动化的协程,但在实际使用中,开发者需要在涉及协程逻辑的函数调用前增加yield
关键字,这带来了额外的学习和维护成本,非常容易犯错,此外 Yield/Generator 代码风格与传统的同步风格代码存在冲突,无法复用已有代码。
Swoole 协程是全自动化的协程,开发者无需添加任何关键字,底层自动实现协程的切换和调度,此外,Swoole 协程风格与传统的同步风格代码是一致的,因此可以复用已有代码。
# 5. PHP运行模式(SAPI)
SAPI(Server Application Programming Interface)服务器应用程序编程接口,它提供了PHP与其他应用交互的接口。PHP脚本执行有很多方式:Web服务器/命令行下/嵌入在其他程序中等等。
PHP提供了一个常量PHP_SAPI
和一个方法php_sapi_name()
来判断当前PHP运行环境,以下是常见运行环境:
# 5.1 CLI
cli命令行接口, 在命令行下执行PHP脚本, 内置web服务器,如果请求未指定执行哪个PHP脚本,则默认执行目录下的index.php或index.html,如果这两个文件都不存在则返回404
# 5.2 CGI
CGI即通用网关接口(Commom Gateway Interface),它把网页和WEB服务器中的执行程序连接起来,把从HTML接收的指令传递给服务器的执行程序,再把服务器执行程序的结果返还给HTML页。CGI的跨平台性能极佳,几乎可以在任何操作系统上实现。
CGI方式在遇到连接请求先要创建CGI的子进程,激活一个CGI进程,然后处理请求,处理完后结束这个子进程。这就是fork-and-execute模式。所以用cgi方式的服务器有多少连接请求就会有多少cgi子进程,子进程反复加载是CGI性能低下的主要原因。都会当用户请求数量非常多时,会大量挤占系统的资源如内存,CPU时间等,造成效能低下。
# 5.3 FastCGI
fast-cgi
是CGI的升级版本,FastCGI可以看成是一个常驻型的CGI,它可以一直执行着,只要激活后,不会每次都要花费时间去fork一次。PHP使用PHP-FPM(FastCGI Process Manager),全称PHP FastCGI 进程管理器进行管理。
FastCGI的工作原理:
- Web Server启动时载入FastCGI进程管理器
- FastCGI进程管理器自身初始化,启动多个CGI解释器(可见多个php-cgi)并等待来自Web Server的连接
- 当客户端请求到达Web Server是,FastCGI进程管理器选择并连接到一个CGI解释器。Web Server将CGI环境变量和标准输入发送到FastCGI子进程php-cgi
- FastCGI子进程完成处理后将标准输出和错误信息从同一连接返回Web Server。当FastCGI子进程关闭连接时,请求便告知处理完成。FastCGI子进程接着等待并处理来自FastCGI继承管理器的下一个连接。在CGI模式中,php-cgi在此便退出了。
CGI每个Web请求PHP都必须重新解析php.ini、重新载入全部扩展并重新始化全部数据结构,而使用FastCGI,所有这些都只在进程启动时发生一次。
# 5.4 Apache模块的DLL
该运行模式是我们以前在windows环境下使用apache服务器经常使用的,而在模块化(DLL)中,PHP是与Web服务器一起启动并运行的。
# 6. PHP-FPM原理
Nginx服务器通过FastCGI协议,发送环境变量和HTTP数据给PHP-FPM,Nginx和PHP-FPM之间可以通过Unix domain socket
和TCP connection
通信。PHP-FPM 处理请求后,通过相同的连接返回数据给 Nginx。
PHP-FPM是PHP实现的FastCGI Process Manager(FastCGI进程管理器), 负责创建和管理PHP FastCGI进程,支持的功能如下:
- 平滑停止/启动的高级进程管理功能
- 慢日志记录脚本
- 动态/静态子进程产生
- 基于php.ini的配置文件
PHP-FPM在5.4之后已经整合进入PHP源代码中,提供更好的PHP进程管理方式,可以有效控制内存和进程,平滑重载PHP配置。如果需要使用,在./configure
的时候带上-enable-fpm
参数即可,使用PHP-FPM来控制FastCGI进程。
./configure –prefix=/usr/local/php7 –with-config-file-path=/usr/local/php7/etc –enable-fpm …
# 6.1 PHP-FPM工作原理
- FastCGI进程管理器自身初始化,包括master和worker进程两部分,master进程监听端口,接收来自Web Server请求,worker进程一般具有多个,每个worker进程都有一个cgi进程解释器,用来执行php代码, 等待来自Web Server的连接。
- 当客户端请求到达Web Server时,FastCGI进程管理器选择并连接到一个CGI解释器。Web Server将CGI环境变量和标准输入发送到FastCGI子进程php-cgi
- FastCGI子进程完成处理后将标准输出和错误信息从同一连接返回Web Server。当FastCGI子进程关闭连接时,请求便告知处理完成。FastCGI子进程接着等待并处理来自FastCGI继承管理器的下一个连接。
以下是某服务器上运行的PHP-FPM进程情况:
root 16728 0.0 0.3 223832 12320 ? Ss Oct10 0:25 php-fpm: master process (/usr/local/php7/etc/php-fpm.conf)
www-data 16729 0.0 0.5 447776 19868 ? Sl Oct10 0:08 php-fpm: pool www
www-data 16730 0.0 0.5 523508 23248 ? Sl Oct10 0:07 php-fpm: pool www
www-data 16731 0.0 0.4 447592 18624 ? Sl Oct10 0:08 php-fpm: pool www
www-data 16732 0.0 0.4 447572 18340 ? Sl Oct10 0:08 php-fpm: pool www
www-data 16733 0.0 0.4 447592 18424 ? Sl Oct10 0:09 php-fpm: pool www
www-data 16746 0.0 0.4 447572 19524 ? Sl Oct10 0:09 php-fpm: pool www
2
3
4
5
6
7
# 6.2 PHP-FPM配置
FPM配置文件为php-fpm.conf和进程池配置文件,其语法类似 php.ini 。其php手册上也有详细的讲解:http://php.net/manual/zh/install.fpm.configuration.php 。
配置文件示例:
[global]
pid = run/php-fpm.pid
log_level = error
[www]
user = www-data
group = www-data
listen = 127.0.0.1:9000
slowlog = /usr/local/php7/var/log/$pool.log.slow
request_slowlog_timeout = 2
request_terminate_timeout = 60
pm = dynamic
pm.max_children = 50
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 35
pm.max_requests = 500
pm.status_path = /fpm_status
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 6.2.1 全局配置
pid
:master进程pid存放路径,默认:pid = #INSTALL_PREFIX#/run/php-fpm.pid
error_log
: 错误日志文件路径,默认:error_log = #INSTALL_PREFIX#/log/php-fpm.log
, 设置为 “syslog”,日志将不会写入本地文件,而是发送到 syslogdlog_level
:日志级别, alert, error, warning, notice, debug, 默认值: noticesyslog.facility
:设置何种程序记录消息,默认值:daemon。syslog.ident
:为每条信息添加前缀。 如果在同一台服务器上运行了多个 FPM 实例,可以修改此默认值来满足需求。默认值:php-fpmdaemonize
:设置FPM是否在后台运行,默认值:yesprocess.max
:Fork 的最大 FPM 进程数。使用动态管理进程数时,此设计可以控制在一个进程池内的全局进程数量。 使用需谨慎。默认值:0。rlimit_files
:设置 master 进程的打开文件描述符 rlimit 数,默认取系统值
# 6.2.2 进程池配置
php-fpm可以配置多个pool(进程池),每个pool都是以一个独立的配置文件来运作,默认都会定义在主配置文件的include包含文件目录中。php默认会提供一个www的pool,大概配置如下:
user
:FPM 进程运行的Unix用户,默认为nobody,一般设置为www-datagroup
: FPM 进程运行的 Unix 用户组, 默认为nobody, 一般设置为www-datalisten
: 设置接受 FastCGI 请求的地址,默认为127.0.0.1:9000pm
:设置进程管理器如何管理子进程,可用值:static
,ondemand
,dynamic
static
: 子进程的数量是固定的(由 pm.max_children 参数控制)ondemand
:进程在有需求时才产生(当请求时才启动,与 dynamic 相反,在服务启动时 pm.start_servers 就启动了)dynamic
:子进程的数量在下面配置的基础上动态设置:pm.max_children,pm.start_servers,pm.min_spare_servers,pm.max_spare_serverspm.max_children
:pm设置为static时表示创建的子进程的数量,pm设置为dynamic时表示最大可创建的子进程的数量pm.start_server
s:设置启动时创建的子进程数目,仅在pm设置为dynamic时使用pm.min_spare_servers
:设置空闲服务进程的最低数目,仅在pm设置为dynamic时使用pm.max_spare_servers
:设置空闲服务进程的最大数目,仅在pm设置为dynamic时使用
pm.process_idle_timeout
:秒数,多久之后结束空闲进程,仅当设置pm为ondemand时使用。 可用单位:s(秒),m(分),h(小时)或者 d(天)。默认单位:10spm.max_requests
:设置每个子进程重生之前服务的请求数pm.status_path
:FPM 状态页面的网址, 可以借助于这个数据调整PHP-FPM配置ping.path
:FPM 监控页面的 ping 网址。如果没有设置,则无法访问 ping 页面。该页面用于外部检测 FPM 是否存活并且可以响应请求,请注意必须以斜线开头(/)ping.response
:用于定义 ping 请求的返回响应。返回为 HTTP 200 的 text/plain 格式文本,默认值:pongaccess.log
:Access log 文件路径,默认未设置access.format
:Access log 格式,默认值:“%R - %u %t "%m %r" %s”
slowlog
:慢请求的记录日志,默认值:*#INSTALL_PREFIX#/log/php-fpm.log.slow*
request_slowlog_timeout
:当一个请求该设置的超时时间后,就会将对应的 PHP 调用堆栈信息完整写入到慢日志中,默认值:0(关闭)request_terminate_timeout
:设置单个请求的超时中止时间,该选项可能会对 php.ini 设置中的max_execution_time
因为某些特殊原因没有中止运行的脚本有用,设置为 ‘0’ 表示 ‘Off’rlimit_files
:设置文件打开描述符的 rlimit 限制,默认值:系统定义值
# 6.3 配置优化
# 6.3.1 进程配置
PHP-FPM是多进程模式,master进程管理worker进程,进程的数量,都可以通过php-fpm.conf做具体配置,而PHP-FPM的进程,亦可以分为动态模式及静态模式。
静态(static)
:直接开启指定数量的php-fpm进程,不再增加或者减少;启动固定数量的进程,占用内存高。但在用户请求波动大的时候,对Linux操作系统进程的处理上耗费的系统资源低。动态(dynamic)
:开始的时候开启一定数量的php-fpm进程,当请求量变大的时候,动态的增加php-fpm进程数到上限,当空闲的时候自动释放空闲的进程数到一个下限。动态模式,会根据max、min、idle children 配置,动态的调整进程数量。在用户请求较为波动,或者瞬间请求增高的时候,进行大量进程的创建、销毁等操作,而造成Linux负载波动升高,简单来说,请求量少,PHP-FPM进程数少,请求量大,进程数多。优势就是,当请求量小的时候,进程数少,内存占用也小。按需模式(ondemand)
:这种模式下,PHP-FPM的master不会fork任何的子进程,纯粹就是按需启动子进程,这种模式很少使用,因为这种模式,基本上是无法适应有一定量级的线上业务的。由于php-fpm是短连接的,所以每次请求都会先建立连接,建立连接的过程必然会触发上图的执行步骤,所以,在大流量的系统上master进程会变得繁忙,占用系统cpu资源,不适合大流量环境的部署
大概逻辑总结如下:
- 在pm=dynamic时,如果
idle worker
数量 <pm.min_spare_servers
,创建新的子进程 - 在pm=dynamic时,如果
idle worker
数量 >pm.max_spare_servers
,杀死多余的空闲子进程 - 在pm=ondemand时,如果
idle worker
空闲时间 >pm.process_idle_timeout
,杀死该空闲进程 - 当连接到达时,检测如果worker数量 >
pm.max_children
,打印warning日志,退出;如果无异常,使用idle worker服务,或者新建worker服务
# 6.3.2 进程配置总结
进程配置关系到PHP-FPM并发处理量,与服务器QPS指数息息相关,以下是几点经验总结:
- 内存比较少,并发量不是很大的应用,可以考虑采用
dynamic
的方式,这样可以控制php-fpm
所消耗的总内存数。 - 在并发高或者流量波动大的情况下,使用
static
可以在高并发下获得比dynamic
更快的响应速度。 - 可配置进程数量 = php-fpm 可配置内存 / (php-fpm 子进程的内存占用 * 1.2)
pm = dynamic
#pm参数指定了进程管理方式,有两种可供选择:static或dynamic,从字面意思不难理解,为静态或动态方式。如果是静态方式,那么在php-fpm启动的时候就创建了指定数目的进程,在运行过程中不会再有变化(并不是真的就永远不变);而动态的则在运行过程中动态调整,当然并不是无限制的创建新进程,受pm.max_spare_servers参数影响;动态适合小内存机器,灵活分配进程,省内存。静态适用于大内存机器,动态创建回收进程对服务器资源也是一种消耗
pm.max_children = 24
#static模式下创建的子进程数或dynamic模式下同一时刻允许最大的php-fpm子进程数量
pm.start_servers = 16
#动态方式下的起始php-fpm进程数量
pm.min_spare_servers = 12
#动态方式下服务器空闲时最小php-fpm进程数量
pm.max_spare_servers = 24
#动态方式下服务器空闲时最大php-fpm进程数量
2
3
4
5
6
7
8
9
10
一般php-fpm进程占用20~30m左右的内存,就按30m算,如果单独跑php-fpm,动态方式起始值可设置:物理内存Mem/30M
。
# 6.3.3 最大处理请求数
最大处理请求数是指一个php-fpm的worker进程在处理多少个请求后就终止掉,master进程会重新respawn一个新的,这个配置的主要目的是避免php解释器或程序引用的第三方库造成的内存泄露。
pm.max_requests = 10240
# 6.3.4 最长执行时间
最大执行时间在php.ini和php-fpm.conf里都可以配置,配置项分别为max_execution_time
和request_terminate_timeout
max_execution_time = 20
request_terminate_timeout = 20
2
这个是用来处理因为PHP执行时间超长而报502错误的解决。这个时长配置可以在php.ini或php-fpm.conf中配置均可,为了不影响全局配置,可在php-fpm.conf中实现。
# 6.4 常见异常
# 6.4.1 502错误
502 Bad Gateway 在php.ini和php-fpm.conf中分别有这样两个配置项:max_execution_time
和request_terminate_timeout
。 这两项都是用来配置一个PHP脚本的最大执行时间的。
当超过这个时间时,PHP-FPM不只会终止脚本的执行,还会终止执行脚本的Worker进程。所以Nginx会发现与自己通信的连接断掉了,就会返回给客户端502错误。
# 6.4.2 504错误
504 Gateway Time-out由于程序执行时间过长导致响应超时,例如程序需要执行90秒,而nginx最大响应等待时间为30秒,这样就会出现超时。
PHP-FPM设置的脚本最大执行时间已经够长了,但执行耗时PHP脚本时,发现Nginx报错从502变为504了。这是为什么呢?
因为我们修改的只是PHP的配置,Nginx中也有关于与上游服务器通信超时时间的配置factcgi_connect/read/send_timeout
。
# fastcgi连接超时时间,默认60秒
fastcgi_connect_timeout
# nginx 进程向 fastcgi 进程发送请求过程的超时时间,默认值60秒
fastcgi_send_timeout
# fastcgi 进程向 nginx 进程发送输出过程的超时时间,默认值60秒
fastcgi_read_timeout
2
3
4
5
6
7
8
总结:
- 502网关错误,主要是PHP执行超时,但是没有超过Web设置的响应时长,这个时候PHP-FPM的woker进程挂起重启,所以出现网关错误,即下游服务器中断了,这种情况一般都是调整php执行时间。
- 504网关超时,主要是PHP执行时间超过Web设置的响应时长,这种情况一般都是修改Web服务器响应时间,
# 7. PHP优化
# 7.1 Opcache
当解析器执行PHP脚本时会解析脚本代码,将它们生成可以直接运行的中间代码,称为Zend OpCode
, 类似于Java的ByteCode。
Zend引擎是PHP的编译引擎和执行引擎,当它执行一段PHP脚本时,会依次按照如下步骤执行:
Scan
:扫描,将PHP代码转换成语言片段。Parse
:解析,将语言片段组合成有意义的表达式。Complie
:编译,将表达式编译程OpCode。Excute
:执行,顺次执行OpCode。
Nginx或其他Web服务器把HTTP请求转发给PHP-FPM, PHP-FPM再把请求交给某个PHP子进程处理,PHP进程找到PHP脚本后执行,把脚本编译为OpCode后生成响应。
如果每次请求一个PHP脚本都要编译一次Zend OpCode,然后执行字节码,就会消耗很多资源。如果每次HTTP请求PHP都必须不断解析、编译和执行PHP脚本,消耗的资源更多。如果有一个工具能缓存预告编译好的字节码,减少应用的响应时间,降低系统资源压力,这当然就是我们想要的方式—字节码缓存。
字节码缓存的共通特性就是能够存储预先编译的Zend OpCode,使用OpCode缓存后,当请求一个PHP脚本时,不用再读取、解析和编译PHP代码。PHP解释器会从内存中读取预先编译好的字节码,立即执行。这样就能节省很多时间,极大提升应用的性能。
在PHP5.5以后,Zend OpCache虽然被内置,但默认没有启用,需要显式指定启用Zend OpCache。如果是自己编译的PHP运行环境,需要在configure命令时包含如下:
–enable -opcache
编译好,须在php.ini文件中配置Zend OpCache。
zend_extension=opcache.so
[opcache]
;开启opcache
opcache.enable=1
;CLI环境下,PHP启用OPcache
opcache.enable_cli=1
;OPcache共享内存存储大小,单位MB
opcache.memory_consumption=128
;PHP使用了一种叫做字符串驻留(string interning)的技术来改善性能。例如,如果你在代码中使用了1000次字符串“foobar”,在PHP内部只会在第一使用这个字符串的时候分配一个不可变的内存区域来存储这个字符串,其他的999次使用都会直接指向这个内存区域。这个选项则会把这个特性提升一个层次——默认情况下这个不可变的内存区域只会存在于单个php-fpm的进程中,如果设置了这个选项,那么它将会在所有的php-fpm进程中共享。在比较大的应用中,这可以非常有效地节约内存,提高应用的性能。
这个选项的值是以兆字节(megabytes)作为单位,如果把它设置为16,则表示16MB,默认是4MB
opcache.interned_strings_buffer=8
;这个选项用于控制内存中最多可以缓存多少个PHP文件。这个选项必须得设置得足够大,大于你的项目中的所有PHP文件的总和。
设置值取值范围最小值是 200,最大值在 PHP 5.5.6 之前是 100000,PHP 5.5.6 及之后是 1000000。也就是说在200到1000000之间。
opcache.max_accelerated_files=4000
;设置缓存的过期时间(单位是秒),为0的话每次都要检查
opcache.revalidate_freq=60
;从字面上理解就是“允许更快速关闭”。它的作用是在单个请求结束时提供一种更快速的机制来调用代码中的析构器,从而加快PHP的响应速度和PHP进程资源的回收速度,这样应用程序可以更快速地响应下一个请求。把它设置为1就可以使用这个机制了。
opcache.fast_shutdown=1
;如果启用(设置为1),OPcache会在opcache.revalidate_freq设置的秒数去检测文件的时间戳(timestamp)检查脚本是否更新。
如果这个选项被禁用(设置为0),opcache.revalidate_freq会被忽略,PHP文件永远不会被检查。这意味着如果你修改了你的代码,然后你把它更新到服务器上,再在浏览器上请求更新的代码对应的功能,你会看不到更新的效果
强烈建议你在生产环境中设置为0,更新代码后,再平滑重启PHP和web服务器。
opcache.validate_timestamps=0
;开启Opcache File Cache(实验性), 通过开启这个, 我们可以让Opcache把opcode缓存缓存到外部文件中, 对于一些脚本, 会有很明显的性能提升.
这样PHP就会在/tmp目录下Cache一些Opcode的二进制导出文件, 可以跨PHP生命周期存在.
opcache.file_cache=/tmp
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
重启PHP FPM,使用phpinfo函数查看,确认Zend OpCache是否正常工作,如图所示:
# 8. 其他
# 8.1 Declare
declare
代码段可以通过一些指令来影响程序运行,目前只支持三个指令:ticks
、 encoding
、 strict_types
指令 。
declare(strict_types=1)
:表示开启严格模式,此时在调用函数的时候,会验证参数和返回值的类型和函数定义的类型是否完全一致,如果不一致将报错,该指令在项目实践中用的较多;<?php // 开启严格模式 declare(strict_types=1);
1
2
3declare(encoding='XXXX')
:可以用encoding
指令来对每段脚本指定其编码方式;<?php declare(encoding='ISO-8859-1'); // 在这里写代码 ?>
1
2
3
4declare(ticks=1)
Tick
(时钟周期)是一个在declare
代码段中解释器每执行N条可计时的低级语句就会发生的事件。N的值是在declare
中的directive
部分用ticks=N
来指定的。不是所有语句都可计时,通常条件表达式和参数表达式都不可计时。
在每个
tick
中出现的事件是由register_tick_function()
来指定的。<?php declare(ticks=1); // 每次 tick 事件都会调用该函数 function tick_handler() { echo "tick_handler() called\n"; } register_tick_function('tick_handler'); // 引起 tick 事件 $a = 1; // 引起 tick 事件 if ($a > 0) { $a += 2; // 引起 tick 事件 print($a); // 引起 tick 事件 } ?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 8.2 emtpy、isset、is_null的区别
isset()
用来检测一个变量是否已声明且值不为NULL。empty()
用来检测一个变量是否为空,也就是说有如下情况时返回真值:变量是一个空字符串
,false
,空数组[array()]
,NULL
,0
,' '
,以及被unset
删除后的变量。is_null()
函数用来判断变量内容是否是NULL值,即返回真值的条件仅为变量是NULL时。值得一提的是,is_null()
是isset()
函数的反函数,区别是isset()
函数可以应用到未知变量,但is_null()
只能针对已声明变量。