《Modern PHP》学习笔记

命名空间(namespace)

基本概念

  • PHP 命名空间与操作系统的物理文件系统不同,这是一个虚拟概念,没必要和文件系统中的目录结构完全对应。但是大多数 PHP 组件为了兼容 PSR-4 自动加载器标准,会把子命名空间放到文件系统的子目录中。
  • 从技术层面来看,命名空间只是 PHP 语言中的一种记号,PHP 解释器会将其作为前缀添加到类、接口、函数和常量的名称前面。
  • 不同代码可能使用相同的类名、接口名、函数名或常量名,如果不使用命名空间,名称会起冲突,导致 PHP 执行出错。而使用命名空间,把代码放在唯一的厂商命名空间中,这样不同命名空间下的代码就可以使用相同的名称命名类、接口、函数和常量。
  • 在同一个命名空间或子命名空间中的所有类没必要在同一个 PHP 文件中声明。你可以在 PHP 文件的顶部指定一个命名空间或子命名空间,此时,这个文件中的代码就是该命名空间或子命名空间的一部分。因此,我们可以在不同的文件中编写属于同一个命名空间的多个类。

导入和别名

  • 导入是指在每个 PHP 文件中告诉 PHP 想使用哪个命名空间、类、接口、函数和常量。导入后就不用输入全名了。
  • 创建别名是指告诉 PHP 我要使用简单的名称引用导入的类、接口、函数或常量。
  • 使用 use 关键字导入代码时无需在开头加上符号,因为 PHP 假定导入的是完全限定的命名空间。
  • use 关键字必须出现在全局作用域中(即不能在类或函数中),因为这个关键字在编译时使用。不过,use 关键字可以在命名空间声明语句之后使用,导入其他命名空间中的代码。
  • 导入函数和常量:
1
2
3
4
5
<?php
use func Namespace\functionName;
use constant Namespace\CONST_NAME;
functionName();
echo CONST_NAME;
  • 一个文件定义一个类,一个文件只使用一个命名空间。
  • 有些代码可能没有命名空间,这些代码在全局命名空间中。如果需要在命名空间中引用其他命名空间中的类、接口、函数或常量,必须使用完全限定的 PHP 类名(命名空间类名)。在命名空间中引用全局命名空间中的代码时,要在类、接口、函数或常量的名称前加上 \ 符号。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<?php
namespace My\App;

Class Foo
{
    public function dosomething() {
        $exception1 = new Exception();      // \My\App\Exception 类中搜索
        $exception2 = new \Exception();     // PHP 原生的 Exception 类
    }
}

接口(interface)

  • 接口是两个 PHP 对象之间的契约,其目的不是让一个对象依赖另一个对象的身份,而是依赖另一个对象的能力。
  • 接口将项目代码和依赖解耦,并允许项目代码依赖任何实现了预期接口的第三方代码而不用关心第三方代码是如何实现接口的,只关心第三方代码是否实现了指定的接口

例程(完整版):

 1
 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
74
75
76
77
78
79
80
81
82
83
84
<?php

$documentStore = new DocumentStore();

// Add HTML document
$htmlDoc = new HtmlDocument('http://php.net');
$documentStore->addDocument($htmlDoc);

// Add terminal command document
$cmdDoc = new CommandOutputDocument('cat /etc/hosts');
$documentStore->addDocument($cmdDoc);

print_r($documentStore->getDocuments());


class DocumentStore
{
    protected $data = [];
    public function addDocument(Documentable $document)
    {
        $key = $document->getId();
        $value = $document->getContent();
        $this->data[$key] = $value;
    }
    public function getDocuments()
    {
        return $this->data;
    }
}

interface Documentable
{
    public function getId();
    public function getContent();
}

class CommandOutputDocument implements Documentable
{
    protected $command;

    public function __construct($command)
    {
        $this->command = $command;
    }

    public function getId()
    {
        return $this->command;
    }

    public function getContent()
    {
        return shell_exec($this->command);
    }
}

class HtmlDocument implements Documentable
{
    protected $url;

    public function __construct($url)
    {
        $this->url = $url;
    }

    public function getId()
    {
        return $this->url;
    }

    public function getContent()
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $this->url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
        curl_setopt($ch, CURLOPT_MAXREDIRS, 3);
        $html = curl_exec($ch);
        curl_close($ch);

        return $html;
    }
}

性状(trait)

  • 性状既不是类也不是接口,而是类的部分实现(即常量,属性和方法),可以混入一个或多个现有的 PHP 类中。
  • 性状的作用:
    1. 表明类可以做什么(类似接口);
    2. 提供模块化实现(类似类)。
  • 性状能把模块化的实现方式注入多个无关的类中。而且性状还能促进代码重用。
  • 使用性状的场景:
    1. 创建一个基类,然后共同集成这个基类,将共用方法写在基类中。但无关的两个类并不应集成相同的父类。
    2. 创建接口,然后在两个类中分别实现该接口并使用。但如果实现方法相同,则违背了 DRY(Don't Repeat Yourself)原则。
  • 与定义类和接口一样,一个文件只定义一个性状。
  • 命名空间、类、接口函数和常量在类的定义体外导入,而性状在类的定义体内导入。
  • PHP 解释器在编译时会把性状复制粘贴到类的定义体中,但是不会处理这个操作引入的不兼容问題。如果性状假定类中有特定的属性或方法(在性状中没有定义),要确保相应的类中有对应的属性和方法。

例程:

 1
 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
<?php

$adapter = new \Ivory\HttpAdapter\CurlHttpAdapter();
$geocoder = new \Geocoder\Provider\GoogleMaps($adapter);

$store = new RetailStore();
$store->setAddress('420 9th Avenue, New York, NY 10001 USA');
$store->setGeocoder($geocoder);

$latitude = $store->getLatitude();
$longitude = $store->getLongitude();

echo $latitude, ':', $longitude;



class RetailStore
{
    use TraitExample;

    //......
}



trait Geocodable
{
    /** @var string */
    protected $address;

    /** @var \Geocoder\Geocoder */
    protected $geocoder;

    /** @var \Geocoder\Model\AddressCollection */
    protected $geocoderResult;

    public function setGeocoder(\Geocoder\Geocoder $geocoder)
    {
        $this->geocoder = $geocoder;
    }

    public function setAddress($address)
    {
        $this->address = $address;
    }

    public function getLatitude()
    {
        if (!isset($this->geocoderResult)) {
            $this->geocodeAddress();
        }

        return $this->geocoderResult->first()->getLatitude();
    }

    public function getLongitude()
    {
        if (!isset($this->geocoderResult)) {
            $this->geocodeAddress();
        }

        return $this->geocoderResult->first()->getLongitude();
    }

    protected function geocodeAddress()
    {
        $this->geocoderResult = $this->geocoder->geocode($this->address);

        return true;
    }
}

生成器(generator)

  • 生成器是简单的迭代器
  • PHP 生成器不要求类实现 Iterator 接口,并会根据需求即时计算并产出要迭代的值,不占用宝贵的内存资源。
  • 生成器是一次性的,无法多次迭代同个生成器,但可以重建或克隆生成器
  • 每次产出一个值之后,生成器的内部状态都会停顿;向生成器请求下一个值时,内部状态又会恢复。生成器的内部状态会一直在停顿和恢复之间切换,直到抵达函数定义体的末尾或遇到空的 return;语句为止。
  • 生成器是只能向前进的迭代器,不能使用生成器在数据集中执行后退、快进或査找操作,只能让生成器计算并产生下一个值。迭代大型数据集或数列时最适合使用生成器,因为这样占用的系统内存量极少。
  • 更多技巧与实践:@ircmaxell - What Generators Can Do For You

例程一:生成一个范围内的数值数组且善用内存

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<?php
function makeRange($length) {
    for ($i = 0; $i < $length; $i++) {
        yield $i;
    }
}

foreach (makeRange(1000000) as $i) {
    echo $i . PHP_EOL;
}

例程二:读取一个超过 PHP 可用内存上限的 CSV 文件。只为 CSV 文件中的一行分配内存,而不会把整个 CSV 文件都读取到内存中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<?php
function getRows($file) {
    $handle = fopen($file, 'rb');
    if (!$handle) {
        throw new Exception();
    }
    while (!feof($handle)) {
        yield fgetcsv($handle);
    }
    fclose($handle);
}

foreach (getRows('data.csv') as $row) {
    print_r($row);
}

闭包(closure)

  • 闭包是指在创建时封装周围状态的函数。即便闭包所在的环境不存在了,闭包中封装的状态依然存在。
  • 匿名函数就是没有名称的函数,可以调用,还可以传入参数。
  • 匿名函数可以赋值给变量,还能像对象那样传递。
  • 匿名函数适合作为函数或方法的回调。
  • 在 PHP 中,匿名函数 = 闭包。
  • 闭包和匿名函数是伪装成函数的对象
  • PHP 闭包不会自动封装应用的状态,必须手动调用闭包对象的 bindto() 方法或者使用 use 关键字,把状态附加到 PHP 闭包上,这样即便返回的闭包对象跳出了其函数的作用域,它也会记住参数的值,即将应用状态封装起来。
  • 与任何其他 PHP 对象类似,每个闭包实例都可以使用 $this 关键字获取闭包的内部状态。bindto() 方法可以把 Closure 对象的内部状态绑定到其他对象上。
  • 闭包可以访问绑定闭包的对象中受保护和私有的成员变量。

例程一:普通闭包

1
2
3
4
5
6
7
8
<?php
$numbersPlusOne = array_map(function ($number) {
    return $number + 1;
}, [1,2,3]);

print_r($numbersPlusOne);

// Outputs --> [2,3,4]

例程二:封装应用状态的闭包

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?php
function enclosePerson($name) {
    return function ($doCommand) use ($name) {
        return sprintf('%s, %s', $name, $doCommand);
    };
}

// Enclose "Clay" string in closure
$clay = enclosePerson('Clay');

// Invoke closure with command
echo $clay('get me sweet tea!');
// Outputs --> "Clay, get me sweet tea!"

Zend OPcache

  • PHP 是解释型语言,PHP 解释器执行 PHP 脚本时会解析 PHP 脚本代码,把 PHP 代码编译成一系列 Zend 操作码,然后执行字节码。Zend OPcache 可以让 PHP 解释器从内存中读取预先编译好的字节码,然后立即执行。这样能节省很多时间,极大地提升应用的性能。

  • 启用步骤:

    1. 编译:$ ./configure --enable-opcache
    2. 启用扩展:zend_extension=/path/to/opcache.so
    3. 配置:
    opcache.validate_timestamps = 1 //检测 PHP 脚本变动。建议生产环境设为 0
    opcache.revalidate_freq = 0 //检査 PHP 脚本内容变化的周期,单位秒。
    opcache.memory_consumption = 64 // 为操作码缓存分配的内存量(单位是 MB)。分配的内存量应该够保存应用中所有 PHP 脚本编译得到的操作码。
    opcache.interned_strings_buffer = 16 // 用来存储驻留字符串(interned string)的内存量(单位是 MB)。默认值为 4MB。
    opcache.max_accelerated_files = 4000 // 操作码缓存中最多能存储多少个 PHP 脚本。
    opcache.fast_shutdown = 1 // 设置操作码使用更快的停机步骤
    

内置的 HTTP 服务器

  • 从 PHP 5.4.0 起,PHP 内置了 Web 服务器。
  • 启动本地运行:$ php -S localhost:4000
  • 启动局域网访问:$ php -S 0.0.0.0:4000
  • 指定配置文件启动:$ php -S localhost:8000 -c app/config/php.ini
  • 指定路由器脚本启动:$ php -S localhost:8000 router.php
  • 内置的 HTTP 服务器仅用于本地开发调试,不应使用于生产环境。

标准

  • PHP Framework Interop Group(简称 PHP-FIG,http://www.php-fig.org )。PHP-FIG 由一些 PHP 框架代表组成,PHP-FIG 制定了推荐规范,PHP 框架可以自愿实现这些规范,改进与其他框架的通信和共享功能。
  • PHP-FIG 的使命是实现框架的互操作性:通过接口、自动加载机制和标准的风格,让框架相互合作。
  • 接口:框架通过 PHP 接口假定第三方依赖提供了什么方法,而不关心依赖是如何实现接口的。
    • 自动加载:PHP 解释器在运行时按需自动找到并加载 PHP 类的过程。只需使用一个自动加载器就能混合搭配多个 PHP 组件。
    • 风格:指定如何使用空格、大小写和括号的位置(等等)之类的代码格式规范。

PSR-1:基本的代码风格

  • PHP 标签:使用 <?php ?><?= ?>
  • 编码:使用 UTF-8 编码
  • 目的:一个 PHP 文件只处理一件事
  • 自动加载:PHP 命名空间和类必须遵守 PSR-4 自动加载器标准
  • 类的名称:PHP 类的名称必须一直使用大驼峰式(CamelCase)
  • 常量名称:PHP 常量的名称必须全部使用大写字母
  • 方法名称:PHP 方法的名称必须一直使用小驼峰式(camelCase)

PSR-2:严格的代码风格

  • 贯彻 PSR-1
  • 缩进:使用四个空格缩进。
  • 文件和代码行:使用 UNIX 换行符(LF),文末留一个空行不使用 PHP 关闭标签 ?>。每行代码不超过 80 个字符,至少不能超过 120 个字符。每行末尾不能有空格
  • 关键字:使用小写字母
  • 命名空间:命名空间声明语句和 use 声明语句后留一个空行
  • 类:类定义体的起始括号在类名之后新起一行写。扩展类或实现接口的 extendsimplements 关键字必须和类名写在同一行
  • 方法:方法定义体的起始括号要在方法名之后新起一行写。方法定义体的结束括号要在方法定义体之后新起一行写。起始圆括号之后没有空格,结束圆括号之前没有空格。方法的参数后面有一个逗号和空格
  • 可见性:类中的每个属性和方法都要声明可见性。abstractfinal 限定符放在可见性关键字之前static 限定符放在可见性关键字之后
  • 控制结构:所有控制结构关键字(ifelseifelseswitchcasewhiledo whileforforeachtrycatch)后面都要有一个空格。控制结构关键字后面的起始圆括号后面没有空格,结束圆括号之前没有空格,起始括号和控制结构关键字写在同一行,结束括号单独写在一行

PSR-3:日志记录器接口

  • PSR-3 是一个接口,规定 PHP 日志记录器组件可以实现的方法。
  • PSR-3 要求日志记录器必须实现 9 个方法:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<?php

namespace Psr\Log;

/**
 * See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md
 * for the full interface specification.
 */
interface LoggerInterface
{
    public function emergency($message, array $context = array());
    public function alert($message, array $context = array());
    public function critical($message, array $context = array());
    public function error($message, array $context = array());
    public function warning($message, array $context = array());
    public function notice($message, array $context = array());
    public function info($message, array $context = array());
    public function debug($message, array $context = array());
    public function log($level, $message, array $context = array());
}

PSR-4:自动加载器

  • PSR-4 是一个标准的自动加载器策略,规定运行时按需査找 PHP 类、接口或性状,并将其载入 PHP 解释器。
  • 自动加载器的引入解决了使用 require() 函数和 include() 函数手动引入文件的繁琐。
  • PSR-4 推荐规范不要求改变代码的实现方式,只建议如何使用文件系统目录结构和 PHP 命名空间组织代码。
  • PSR-4 自动加载器策略依赖 PHP 命名空间和文件系统目录结构査找并加载 PHP 类、接口和性状。
  • PSR-4 会把命名空间的前缀和文件系统中的目录对应起来。命名空间的前缀可以是顶层命名空间,也可以是顶层命名空间加上任意一个子命名空间。
  • 推荐使用依赖管理器 Composer 来自动生成符合 PSR-4 规范的自动加载器。

组件

  • PHP 组件是一系列相关的类、接口和性状,用于解决某个具体问题。组件中的类、接口和性状通常放在同一个命名空间中。
  • 好的 PHP 组件具有以下特征:
    • 作用单一:专注于解决一个问题,而且使用简单的接口封装功能。
    • 小型:只包含解决某个问题所需的最少代码。
    • 合作:PHP 组件之间能良好合作。把代码放在自己的命名空间中,防止与其他组件有名称冲突。
    • 测试良好:组件本身会提供测试,而且有充足的测试覆盖度。
    • 文档完善:有个 README 文件,说明组件的作用,如何安装,以及如何使用。抑或有网站介绍详细信息。
  • 如果是能通过一些 PHP 组件准确解决问题的小型项目,那就使用组件。如果是有多个团队成员开发的大型项目,而且能从框架提供的约定、准则和结构中受益,那就使用框架。
  • 查找组件目录:Packagist
  • 安装组件的工具:Composer
  • 自动加载是指在不使用 require()require_once()include()include_once() 函数的情况下按需自动加载 PHP 类。
  • 在较旧的 PHP 版本中可以使用 autoload() 函数自己编写自动加载器;实例化尚未加载的类时,PHP 解释器会自动调用这个函数。后来,PHP 在 SPL 库中引入了更灵活的 sql_autoload_register() 函数。如何自动加载 PHP 类完全由开发者决定。依赖管理器 Composer 为项目中的所有 PHP 组件自动生成符合 PSR 标准的自动加载器。Composers 有效抽象了依赖管理和自动加载。
  • Composer 和 Packagisti 都使用 vendor/package 这种命名约定,避免不同厂商的 PHP 组件有名称冲突。
  • PHP 组件版本号(如,1.13.2)语义:
    1. 第一个数字是主版本号,用于破坏了向后兼容性的版本更新。
    2. 第二个数字是次版本号,用于没破坏向后兼容性的小幅功能更新。
    3. 第三个数字是修订版本号,用于对向后兼容的缺陷的修正。
  • composer install 命令不会安装比 composer.lock 文件中列出的版本号新的版本。
  • composer update 命令会把组件更新到最新稳定版,还会更新 composer.lock 文件,写入 PHP 组件的新版本号。
  • Composer 创建的自动加载器就是 vendor/autoload.php 文件。Composer 下载各个 PHP 组件时会检査每个组件的 composer.json 文件,确定如何加载该组件。
  • Composer 仓库的凭据 auth.php 文件放在和 composer.json 文件同级目录,且不应加入版本控制:
1
2
3
4
5
6
7
8
{
    "http-basic": {
        "example.org" :{
            "username": "your-name",
            "password": "your-password"
        }
    }
}
  • composer.json 文件结构:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
    "name": "组件名称",
    "description": "描述",
    "keywords": [关键词],
    "homepage": "组件主页",
    "license": "软件许可协议",
    "authors": [作者信息],
    "support": {获取技术支持的方式},
    "require": {依赖的组件},
    "require-dev": {开发该组件时依赖的组件},
    "suggest": {建议安装的组件},
    "autoload": {自动加载方式}
}
  • Packagist 从组件的 composer.json 文件中读取了组件的名称、描述、关键字依赖和建议。还会发现显示了仓库的分支和标签。同时 Packagist 会把仓库的标签和语言版本号对应起来。

良好实践

过滤、验证和转义

  • 不要相信任何来自不受自己直接控制的数据源中的数据。
  • 过滤输入:转义或删除不安全的字符。在数据到达应用的存储层之前,一定要过滤输入数据。建议不使用正则表达式函数过滤 HTML,正则表达式很复杂,可能导致 HTML 无效,且出错的几率高。
  • 验证数据:确认输入的数据符合预期,在应用的存储层保存符合特定格式的正确数据。避免数据库出现潜在的错误。
  • 转义输出:避免渲染恶意代码,还能防止应用的用户无意中执行恶意代码。

密码

  • 绝对不能知道用户的密码
  • 绝对不要约束用户的密码
  • 绝对不能通过电子邮件发送用户的密码
  • 使用 bcrypt 计算用户密码的哈希值。bcrypt 算法会自动加盐,防止潜在的彩虹表攻击。bcrypt 算法永不过时,如果计算机的运算速度变快了,我们只需提高工作因子的值。bcrypt 算法得到了同行的大量审查,目前暂无漏洞。
  • 加密和哈希不是一回事。加密是双向算法,加密的数据以后可以解密。而哈希是单向算法,哈希后的数据不能再还原成原始值,而且相同的数据得到的哈希值始终相同。

日期、时间和时区

  • 推荐使用 nesbit/carbon 组件处理日期和时间值的有用方法。

数据库

  • PDO 扩展:PDO(PHP Data Objects,PHP 数据对象)是一系列 PHP 类,抽象了不同数据库的具体实现,只通过一个用户界面就能与多种不同的 SQL 数据库通信。不管使用哪种数据库系统,使用一个接口就能编写和执行数据库査询
  • 使用 PDO 时,建议编写符合 ANSI/ISO 标准的 SQL 语句,这样如果更换数据库系统 SQL 语句不会失效。
  • 数据库凭据应保存在一个位于文档根目录之外的配置文件中,然后在需要使用凭据的 PHP 文件中导入。另外,凭据也不能纳入版本控制,避免因仓库公开而泄漏凭据。
  • 事务:指把一系列数据库语句当成单个逻辑单元(具有原子性)执行。事务中的一系列 SQL 査询要么都成功执行,要么根本不执行。
  • 事务的一个副作用一一提升性能——事务把多个査询排成队列,一次全部执行。
  • PHP 中处理字符串的函数默认假设所有字符串都只使用 8 位字符,如果使用这些 PHP 原生的字符串函数处理包含多字节字符的 Unicode 字符串,可能会出错。为避免处理多字节字符串时出错,可以安装 mbstring 扩展。该扩展提供了能替代大多数 PHP 原生的且能处理多字节字符串的函数。
  • PHP 编码配置(pip.ini):default charset = "utf-8"

错误和异常

  • 错误会导致程序脚本停止执行,有些错误无法恢复,其他错误可使用全局错误处理程序处理。
  • 预测測、捕获并处理异常是我们自己的责任。未捕获的异常会导致 PHP 应用终止运行,显示致命错误信息。而更糟的是,可能会暴露敏感的调试详细信息,让应用的用户看到。因此,一定要使用 try/catch 块捕获异常,然后使用优雅的方式处理。
  • 捕获某种异常时只会运行其中一个 catch 块。如果 PHP 没找到适用的 catch 块,异常会向上冒泡,直到 PHP 脚本由于致命错误而终止运行。
  • PHP 异常可以在 PHP 应用的任何层级抛出和捕获。
  • PHP 异常和错误设置原则:
    • 一定要让 PHP 报告错误。
    • 在开发环境中要显示错误。
    • 在生产环境中不能显示错误。
    • 在开发环境和生产环境中都要记录错误。
  • 推荐配置:
; 开发环境
; 显示错误
display_startup_errors = On
display_errors = On
; 报告所有错误
error_reporting = -1
; 记录错误
log_errors = On


;生产环境
; 不显示错误
display_startup_errors = Off
display_errors = Off
; 除了注意事项之外,报告所有其他错误
error_reporting = E_ALL & ~E_NOTICE
; 记录错误
log_errors = On
  • 推荐使用 Whoops 组件在开发环境中处理 PHP 错误和异常。
  • 推荐使用 Monolog 组件在生产环境中记录 PHP 错误和异常。

主机

  • 存储 PHP 应用的四种方式:
    • 共享服务器(Shared hosting)
    • 虚拟私有服务器(VPS)
    • 专用服务器
    • 平台即服务(PasS)
  • 共享主机账户会与很多其他顾客的账户在同一个物理设备中。PHP 应用能使用多少设备在内存取决于这台设备中有多少账户。共享主机适合少预算,或简单需求。
  • 虚拟私有服务器(VPS)提供了足够的系统资源,需要我们根据 PHP 应用的需求,自己动手配置和保护操作系统。
  • PasS 只需账号登陆,使用提供商的控制面板,单击按钮即可完成操作。有些 PasS 提供商会提供命令行工具或 Http Api,让我们部署和管理存储的 PHP 应用。适合小型 PHP 应用省心省事。

配置

PHP-FPM

  • PHP-FPM 全局配置 php-fpm.conf
emergency_restart_threshold = 10
//在指定的一段时间内,如果失效的 PHP-FPM 子进程数超过这个值,PHP-FPM 主进程就优雅重启。

emergency_restart_interval = 1m
//设定 emergency_restart_threshold 设置采用的时间跨度。
  • PHP-FPM 进程池配置 fpm/pool.d/*.confphp-fpm.d/*.conf
user = deploy
//运行 PHP 应用的非根用户的用户名

group = deploy
//运行 PHP 应用的非根用户所属的用户组名

listen = 127.0.0.1:9000
//PHP-FPM 进程池监听的 IP 地址和端口号

listen.allowed_clients = 127.0.0.1
//可以向该 PHP-FPM 进程池发送请求的 IP 地址(一个或多个)。

pm.max_children = 51
//设定任何时间点 PHP-FPM 进程池中最多能有多少个进程。可以设置的值设为总内存 / 每个进程使用的内存大小

pm.start_servers = 3
//PHP-FPM 启动时 PHP-FPM 进程池中立即可用的进程数。建议设为 2 或 3。

pm.min_spare_servers = 2
//PHP 应用空闲时 PHP-FPM 进程池中可以存在的进程数量最小值。一般与 pm.start_servers 设置值一样

pm.max spare servers = 4
//PHP 应用空闲时 PHP-FPM 进程池中可以存在的进程数量最大值。一般比 pm.start_servers 设置的值大一点

pm.max requests = 1000
//回收进程之前,PHP-FPM 进程池中各个进程最多能处理的 HTP 请求数量。

slowlog = /path/to/slowlog log
//设置慢日志(处理时间超过 n 秒的 HTTP 请求信息)的绝对路径。注意 PHP-FPM 进程池所属的用户和用户组必须有这个文件的写权限。

request_slowlog_timeout = 5s
//慢日志记录的 HTTP 请求的时间下限,超过此值即记录慢日志

Nginx

server {
    # Nginx 监听端口
    listen 80;
    # 虚拟主机的域名
    server_name example.com;
    # HTTP 请求 URI 没指定文件时访问的默认文件
    index index.php;
    # Nginx 接受 HTTP 请求主体长度的最大值
    client_max_body_size 50M;
    # 错误日志文件路径
    error_log /home/deploy/apps/logs/example.error.log;
    # 访问日志文件路径
    access_log /home/deploy/apps/logs/example_access.log;
    # 文档根目录路径
    root /home/deploy/apps/example.com/current/public;

    location / {
        tryz_files $uri $uri/ /index.php$is_args$args;
    }
    location ~ \.php {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param SCRIPT_NAME $fastcgi_script_name;
        fastcgi_index index.php;
        fastcgi_pass 127.0.0.1:9000;
    }
}
  • 指定匹配指定 URL 模式的 HTTP 请求。location / {} 块使用 try_files 指令依次进行如下操作:
    1. 査找匹配所请求 URI 的文件
    2. 查找匹配所请求 URI 的目录
    3. 把 HTTP 请求的 URI 重写为 /index.php,把査询字符附加到 URI 的末尾。
  • 重写的 URL 和所有以 .php 结尾的 URI 都由 location ~ .php {} 块管理,上例中该块规定把 HTTP 请求转发给 PHP-FPM 进程池处理。

优化

  • php.ini 配置中,建议修改 upload_max_filesize = 10MB 设置允许上传文件的大小
  • php.ini 配置中,建议修改 max_execution_time = 5 设置单个 PHP 进程在终止之前最长可以运行多少时间。
  • 应该在较少的块中发送更多的数据,而不是在较多的块中发送较少的数据,这样能减少 HTTP 请求总数。为此需要在 php.ini 配置中启用 PHP 输出缓存功能:
output_buffering = 4096 // 输出缓存区大小
implicit_flush = false

部署

使用 Capistrano 来进行项目部署,可以方便地进行迭代和回滚操作。Capistrano 会在远程服务器中保存之前部署的应用,而且每次部署的版本放在各自的目录中。Capistrano 会维护五个或更多之前部署的应用,以防需要回滚到早前版本。Capistrano 还会创建一个 current/ 目录,通过符号链接指向当前部署的应用所在的目录。具体步骤如下:

  1. 在本地安装 ruby 和 gem
  2. 在本地安装 Capistrano:$ gem install capistrano
  3. 在项目根目录进行初始化:$ cap install,产生的初始化文件结构如下:
├── Capfile                 //配置文件
├── config                  //分环境配置文件
│   ├── deploy              //环境配置目录
│   │   ├── production.rb   //生产环境设置
│   │   └── staging.rb      //过渡环境设置
│   └── deploy.rb           //所有环境的通用设置
└── lib
    └── capistrano
            └── tasks
  1. 在远程服务器安装 Git:$ yum install git
  2. 部署时在本地执行部署命令:$ cap production deploy
  3. 回滚时在本地执行部署命令:$ cap production deploy:rollback

测试

不同的测试方式之间不是互斥:

  • 单元测试:单独证实应用中的各个类、方法和函数能正常运行。PHP 通常使用 PHPUnit 单元测试框架进行单元测试。PHPUnit 遵守 xUnit 测试架构。
  • 测试驱动开发(Test-Driven Development, TDD):在编写应用代码之前先写测试。故意让测试失败以描述应用应该具有怎样的表现。开发好应用的功能后,最终测试会成功通过。
  • 行为驱动开发(Behavior-Driven Development, BDD):编写故事,描述应用的表现。按导向不同氛围两类,二者一般可以共存互补,使项目获得更全面的测试。
    • SpecBDD 是一种单元测试,使用人类能读懂的流畅语言描述应用的实现方式。例如,可能会把某个 PHPUnit 测试命名为 testrendertemplate(),把等价的 SpecBDD 测试命名为 itrendersthetemplate(),和 Xunit 工具相比更易于阅读和理解。通常使用的 SpecBDD 测试工具是 PHPSPEC
    • StoryBDD 也使用人类能读懂的故事,不过 StoryBDD 关注更多的是整体行为,而不是低层实现。StoryBDD 测试关注项目整体的运行结果,而 SpecBDD 测试关注开发过程中的每个细节,如类、方法的正确性。通常使用的 StoryBDD 测试工具是 [Behat](http://behat.org/

分析

  • 开发环境使用 XDebug:使用时会消耗大量系统资源,借助 KCacheGrindWinCacheGrind 查看分析报告。
  • 生产环境使用 XHProf:使用时会消耗的系统资源少,借助 XHGUI 查看分析报告。

HHVM 和 Hack

  • PHP 是传统意义上的解释型语言,而不是编译型语言。因此,在命令行或 Web 服务器调用解释器解释 PHP 代码之前,PHP 代码就是 PHP 代码。PHP 解释器会解释 PHP 脚本,把代码转换成一系列 Zend 操作码(机器码指令),再把这些操作码交给 Zend Engine 执行。但解释型语言执行的速度比編译型语言慢很多,因为每次执行解释型语言编写的代码时都要将其转换成机器码,消耗额外的系统资源。

  • 2010 年 Facebook 开发一个 HPHPc 的编译器,把 PHP 代码编译为 C++ 代码,再把 C++ 代码编译成可执行文件。但 HPHPc 对性能的提升已经到顶。于是 Facebook 开发了下一代 HPHPc,即 HHVM。HHVM 先把 PHP 代码转换成一种字节码中间格式,而且会缓存转换得到的字节码,然后使用 JIT 编译器转换并优化缓存的字节码,将其变成 x86_64 机器码。之后 HPHPc 被废弃。

  • Hack 是一门服务器端语言,类似 PHP,且可以和 PHP 无缝集成。Hack 的开发者其实把 Hack 当做 PHP 的一种方言。从 PHP 转到 Hack 只需将 PHP 标签 <?php 改为 <?hh 即可。

  • Hack 既支持静态类型,也支持动态类型,且基本上能向后兼容普通的 PHP,所以其支持所有 PHP 动态类型特性。HHVM 读取 Hack 代码后会优化和缓存中间字节码,只在需要时才把 Hack 文件转换成 x86_64 机器码。Hack 充分利用了两种类型系统的特性,我们通过 Hack 的类型检査程序得到了静态类型的准确性和安全性,又通过 HVM 的 JIT 编译器得到了动态类型的灵活性和快速迭代。

  • Hack 自带一个单独的类型检査服务器,这个服务器在后台运行,会实时对代码做类型检査。

  • Hack 代码有三种編写模式:

    • 严格模式:<?hh // strict 要求所有变量、函数、方法等代码都有合适的类型注解。且代码中不能有 Hack 之外的代码。
    • 局部模式:<?hh // partial 允许在 Hack 代码中使用还没转换成 Hack 的 PHP 代码。不要求注解函数或方法的所有参数,如果只注解部分参数,Hack 的类型检査程序也不会报错。
    • 声明模式:<?php // decl 允许严格模式的 Hack 代码调用未指定类型的代码。
  • Hack 提供了 PHP 没有的新数据结构和接口数据结构:

updatedupdated2023-09-272023-09-27