欢迎

目前网络上充斥着大量的过时资讯,让 PHP 新手误入歧途,并且传播着错误的实践以及不安全的代码。PHP 之道 收集了现有的 PHP 最佳实践、编码规范和权威学习指南,方便 PHP 开发者阅读和查找。

使用 PHP 沒有规范化的方式。本网站主要是向 PHP 新手介绍一些他们没有发现或者是太晚发现的主题, 或是经验丰富的专业人士已经实践已久的做法提供一些新想法。本网站也不会告诉您应该使用什么样的工具,而是提供多种选择的建议,并尽可能地说明方法及用法上的差异。

当有更多有用的资讯以及范例时,此文件会随着相关技术的发展而持续更新。

其他翻译

PHP: The Right Way 被翻译为以下版本:

书本

最新版本的 PHP 之道 包含 PDF, EPUB 和 MOBI 版本,可以在 Leanpub 上购买。

如何参与

帮助我们让本网站作为 PHP 新手的最佳资源!在 GitHub 上贡献

回到顶部

入门指南

使用当前稳定版本 (8.3)

如果你刚开始学习 PHP,请使用最新的稳定版本 PHP 8.3。PHP 8.3 非常新,相较于 8.2 版本增加了强大的 新特性以及性能提升。

您应该尝试迅速升级到最新的稳定版本-PHP 8.0 已经停止支持升级很容易,因为没有很多向后兼容中断如果不确定函数或功能所在的版本,可以在php.net网站上查看PHP文档

内置的 web 服务器

PHP 5.4 之后, 你可以不用安装和配置功能齐全的 Web 服务器,就可以开始学习 PHP。 要启动内置的 Web 服务器,需要从你的命令行终端进入项目的 Web 根目录,执行下面的命令:

> php -S localhost:8000

Mac 安装

OS X 系统会预装 PHP, 只是一般情况下版本会比最新稳定版低一些。目前 Mavericks 是 5.4.17、Yosemite 则是 5.5.9,El Capitan 是 5.5.29、Sierra 是 5.6.24, 但在 PHP 8.2 出来之后, 这些往往是不够的。

以下介绍几种在 OS X 上安装 PHP 的方法。

通过 Homebrew 安装 PHP

Homebrew 是一个强大的 OS X 专用包管理器, 它可以帮助你轻松的安装 PHP 和各种扩展。 Homebrew PHP 是一个包含与 PHP 相关的 Formulae,能让你通过 homebrew 安装 PHP 的仓库。

也就是说, 你可以通过 brew install 命令安装 php53php54php55php56php70或者 php83,并且通过修改 PATH 变量来切换各个版本。或者你也可以使用 brew-php-switcher 来自动切换。

Install PHP via Macports

通过 Macports 安装 PHP

MacPorts 是一个开源的,社区发起的项目,它的目的在于设计一个易于使用的系统,方便编译,安装以及升级 OS X 系统上的 command-line, X11 或者基于 Aqua 的开源软件。

MacPorts 支持预编译的二进制文件,因此你不必每次都重新从源码压缩包编译,如果你的系统没有安装这些包,它会节省你很多时间。

此时,你可以通过 port install 命名来安装 php54, php55, php56, php70 或者 php83,比如:

sudo port install php56
sudo port install php83

你也可以执行 select 命令来切换当前的 php 版本:

sudo port select --set php php83

通过 phpbrew 安装 PHP

phpbrew 是一个安装与管理多个 PHP 版本的工具。它在应用程序或者项目需要不同版本的 PHP 时非常有用,让你不再需要使用虚拟机来处理这些情况。

源码编译

另一个让你控制安装 PHP 版本的选择就是 自行编译。 如果使用这种方法, 你必须先确认是否已经通过 「Apple’s Mac Developer Center」 下载、安装 Xcode 或者 “Command Line Tools for XCode”

集成包 (All-in-One Installers)

上面列出的解决方案主要是针对 PHP 本身, 并不包含:比如 ApacheNginx 或者 SQL 服务器。 集成包比如 MAMPXAMPP 会安装这些软件并且将他们绑在一起,不过易于安装的背后也牺牲了一定的弹性。

Windows 安装

你可以从 windows.php.net/download 下载二进制包。 解压后, 最好为你的 PHP 所在的根目录(php.exe 所在的文件夹)设置 PATH,这样就可以从命令行中直接执行 PHP。

如果只是学习或者本地开发,可以直接使用 PHP 5.4+ 内置的 Web 服务器, 还能省去配置服务器的麻烦。如果你想要包含有网页服务器以及 MySql 的集成包,那么像是Web Platform Installer, XAMPP, EasyPHPWAMP 这类工具将会帮助你快速建立 Windows 开发环境。不过这些工具将会与线上环境有些许差别,如果你是在 Windows 下开发,而生产环境则部署至 Linux ,请小心。

如果你需要将生产环境部署在 Windows 上,那 IIS7 将会提供最稳定和最佳的性能。你可以使用 phpmanager (IIS7 的图形化插件) 让你简单的设置并管理 PHP。IIS7 也有内置的 FastCGI ,你只需要将 PHP 配置为它的处理器即可。更多详情请见dedicated area on iis.net

一般情况下,使用不同环境开发,会导致你在上线代码时出现 Bug。如果你在 Window 下开发将会用于 Linux 下运行的代码,请应该考虑使用 虚拟机.

这里推荐 Chris Tankersley 的一篇关于 Window 下工具使用的文章 - Windows 下的 PHP 开发.

Linux 安装

请参考 安装PHP 安装。

回到顶部

代码风格指南

PHP 社区百花齐放,拥有大量的函数库、框架和组件。PHP 开发者通常会在自己的项目中使用若干个外部库,因此 PHP 代码遵循(尽可能接近)同一个代码风格就非常重要,这让开发者可以轻松地将多个代码库整合到自己的项目中。

PHP标准组 提出并发布了一系列的风格建议。其中有部分是关于代码风格的,即 PSR-0, PSR-1, PSR-12PSR-4。这些推荐只是一些被其他项目所遵循的规则,如 Drupal, Zend, Symfony, CakePHP, phpBB, AWS SDK, FuelPHP, Lithium 等。你可以把这些规则用在自己的项目中,或者继续使用自己的风格。

通常情况下,你应该遵循一个已知的标准来编写 PHP 代码。可能是 PSR 的组合或者是 PEAR 或 Zend 编码准则中的一个。这代表其他开发者能够方便的阅读和使用你的代码,并且使用这些组件的应用程序可以和其他第三方的组件保持一致。

你可以使用 PHP_CodeSniffer 来检查代码是否符合这些准则,文本编辑器 Sublime Text 的插件也可以提供实时检查。

你可以通过任意以下两个工具来自动修正你的程序语法,让它符合标准:

你也可以手动运行 phpcs 命令:

phpcs -sw --standard=PSR12 file.php

它会显示出相应的错误以及如何修正的方法。同时,这条命令你也可以用在 git hook 中,如果你的分支代码不符合选择的代码标准则无法提交。

如果你已经安装了 PHP_CodeSniffer,你将可以使用 PHP Code 美化修整器 来格式化代码:

phpcbf -w --standard=PSR12 file.php

另一个选项是使用 PHP 编码标准修复器,他可以让你预览编码不合格的部分:

php-cs-fixer fix -v --rules=@PSR2 file.php

所有的变量名称以及代码结构建议用英文编写。注释可以使用任何语言,只要让现在以及未来的小伙伴能够容易阅读理解即可。

最后,编写干净的PHP代码的一个很好的补充资源是Clean Code PHP

回到顶部

语言亮点

编程范式

PHP 是一个灵活的动态语言,支持多种编程技巧。这几年一直不断的发展,重要的里程碑包含 PHP 5.0 (2004) 增加了完善的面向对象模型,PHP 5.3 (2009) 增加了匿名函数与命名空间以及 PHP 5.4 (2012) 增加的 traits,PHP5.5新增Generators ,PHP7.0增加标量类型声明与返回值类型声明、匿名类,PHP7.4增加的属性类型声明、箭头函数、有限返回类型协变与参数类型逆变、FFI、Opcache预加载,PHP8.0增加的命名参数、注解、构造器属性提升、联合类型、Match 表达式、Nullsafe 运算符、JIT、可作为表达式使用 throw。PHP8.2添加枚举,只读属性,头等可调用语法,交集类型,初始化器中使用new,Fibers等。

面向对象编程

PHP 拥有完整的面向对象编程的特性,包括类,抽象类,接口,继承,构造函数,克隆和异常等。

函数式编程 Functional Programming

PHP 支持函数是「第一等公民」,即函数可以被赋值给一个变量,包括用户自定义的或者是内置函数,然后动态调用它。函数可以作为参数传递给其他函数(称为_高阶函数_),也可以作为函数返回值返回。

PHP 支持递归,也就是函数自己调用自己,但多数 PHP 代码使用迭代。

自从 PHP 5.3 (2009) 之后开始引入对闭包以及匿名函数的支持。

PHP 5.4 增加了将闭包绑定到对象作用域中的特性,并改善其可调用性,如此即可在大部分情况下使用匿名函数取代一般的函数。

元编程

PHP 通过反射 API 和魔术方法,可以实现多种方式的元编程。开发者通过魔术方法,如 __get(), __set(), __clone(), __toString(), __invoke(),等等,可以改变类的行为。Ruby 开发者常说 PHP 没有 method_missing 方法,实际上通过 __call()__callStatic() 就可以完成相同的功能。

命名空间

如前所述,PHP 社区已经有许多开发者开发了大量的代码。这意味着一个类库的 PHP 代码可能使用了另外一个类库中相同的类名。如果他们使用同一个命名空间,那将会产生冲突导致异常。

命名空间 解决了这个问题。如 PHP 手册里所描述,命名空间好比操作系统中的目录,两个同名的文件可以共存在不同的目录下。同理两个同名的 PHP 类可以在不同的 PHP 命名空间下共存,就这么简单。

因此把你的代码放在你的命名空间下就非常重要,避免其他开发者担心与第三方类库冲突。

PSR-4 提供了一种命名空间的推荐使用方式,它提供一个标准的文件、类和命名空间的使用惯例,进而让代码做到随插即用。

2014 年 10 月,PHP-FIG 废弃了上一个自动加载标准: PSR-0,而采用新的自动加载标准 PSR-4。但 PSR-4 要求 PHP 5.3 以上的版本,而许多项目都还是使用 PHP 5.2,所以目前两者都能使用。

如果你在新应用或扩展包中使用自动加载标准,应优先考虑使用 PSR-4。

类型声明

PHP类型声明 这个特性将允许php代码来标注参数和返回值的类型。使其具有强类型语言的特性。使用类型声明能减少程序错误,提高运行效率,在PHP8.0中启用了JIT,即时编译功能,使用的就是类型推断,如果你不用类型声明,将降低PHP程序的性能。为了使用JIT,建议一定要使用类型声明功能。

PHP 标准库

PHP 标准库 (Standard PHP Library 简写为 SPL) 随着 PHP 一起发布,提供了一组类和接口。包含了常用的数据结构类 (堆栈,队列,堆等等),以及遍历这些数据结构的迭代器,或者你可以自己实现 SPL 接口。

命令行接口

PHP 是为开发 Web 应用而创建,不过它的命令行脚本接口(CLI)也非常有用。PHP 命令行编程可以帮你完成自动化的任务,如测试,部署和应用管理。

CLI PHP 编程非常强大,可以直接调用你自己的程序代码而无需创建 Web 图形界面,需要注意的是 不要 把 CLI PHP 脚本放在公开的 web 目录下!

在命令行下运行 PHP :

> php -i

选项 -i 将会打印 PHP 配置,类似于 phpinfo() 函数。

选项 -a 提供交互式 shell,和 Ruby 的 IRB 或 python 的交互式 shell 相似,此外还有很多其他有用的命令行选项

接下来写一个简单的 “Hello, $name” CLI 程序,先创建名为 hello.php 的脚本:

<?php
if($argc != 2) {
    echo "Usage: php hello.php <name>.\n";
    exit(1);
}
$name = $argv[1];
echo "Hello, $name\n";

PHP 会在脚本运行时根据参数设置两个特殊的变量,$argc 是一个整数,表示参数个数$argv 是一个数组变量,包含每个参数的, 它的第一个元素一直是 PHP 脚本的名称,如本例中为 hello.php

命令运行失败时,可以通过 exit() 表达式返回一个非 0 整数来通知 shell,常用的 exit 返回码可以查看列表.

运行上面的脚本,在命令行输入:

> php hello.php
Usage: php hello.php <name>
> php hello.php world
Hello, world

Xdebug

合适的调试器是软件开发中最有用的工具之一,它使你可以跟踪程序执行结果并监视程序堆栈中的信息。Xdebug 是一个 php 的调试器,它可以被用来在很多 IDE(集成开发环境) 中做断点调试以及堆栈检查。它还可以像 PHPUnit 和 KCacheGrind 一样,做代码覆盖检查或者程序性能跟踪。

如果你仍在使用 var_dump()/print_r() 调错,经常会发现自己处于困境,并且仍然找不到解决办法。这时,你该使用调试器了。

安装 Xdebug 可能很费事,但其中一个最重要的「远程调试」特性 —— 如果你在本地开发,并在虚拟机或者其他服务器上测试,远程调试可能是你想要的一种方式。

通常,你需要修改你的 Apache VHost 或者 .htaccess 文件的这些值:

php_value xdebug.remote_host=192.168.?.?
php_value xdebug.remote_port=9000

「remote host」 和 「remote port」 这两项对应和你本地开发机监听的地址和端口。然后将你的 IDE 设置成「listen for connections」模式,并访问网址:

http://your-website.example.com/index.php?XDEBUG_SESSION_START=1

你的 IDE 将会拦截当前执行的脚本状态,运行你设置的断点并查看内存中的值。

图形化的调试器可以让你非常容易的逐步的查看代码、变量,以及运行时的 evel 代码。许多 IDE 已经内置或提供了插件支持 XDebug 图形化调试器。比如 MacGDBp 是 Mac 上的一个免费,开源的单机调试器。

回到顶部

依赖管理

PHP 有很多可供使用的库、框架和组件。通常你的项目都会使用到其中的若干项 - 这些就是项目的依赖。直到最近,PHP 也没有一个很好的方式来管理这些项目依赖。即使你通过手动的方式去管理,你依然会为自动加载器而担心。但现在这已经不再是问题了。

目前 PHP 有两个使用较多的包管理系统 - ComposerPEAR。Composer 是 PHP 所使用的主要的包管理器,然而在很长的一段时间里,PEAR 曾经扮演着这个角色。你应该了解 PEAR 是什么,因为即使你从来没有使用过它,你依然有可能会碰到对它的引用。

Composer 与 Packagist

Composer 是一个 杰出 的依赖管理器。在 composer.json 文件中列出你项目所需的依赖包,加上一点简单的命令,Composer 将会自动帮你下载并设置你的项目依赖。Composer 有点类似于 Node.js 世界里的 NPM,或者 Ruby 世界里的 Bundler。

现在已经有许多 PHP 第三方包已兼容 Composer,随时可以在你的项目中使用。这些「packages(包)」都已列在 Packagist (Packagist中文网),这是一个官方的 Composer 兼容包仓库。

为了提高国内 Composer 的使用体验,阿里云维护了 Composer 中文镜像 /Packagist 中国全量镜像 ,将会极大加速 Composer 依赖的下载速度。

如何安装 Composer

最安全的下载方法就是使用 安装教程。 此方法会验证安装器是否安全,是否被修改。

安装器安装 Composer 的应用范围为 本地,也就是在你当前项目文件夹。

我们推荐你 全局 安装,即把可执行文件复制到 /usr/local/bin 路径中:

mv composer.phar /usr/local/bin/composer

注意: 以上命令如果失败,请尝试使用sudo 来增加权限。

本地 使用 Composer 的话,你可以运行 php composer.phar ,全局的话是:composer

Windows环境下安装

对于Windows 的用户而言最简单的获取及执行方法就是使用 ComposerSetup 安装程序,它会执行一个全局安装并设置你的 $PATH,所以你在任意目录下在命令行中使用 composer

如何手动安装 Composer

手动安装 Composer 是一个高端的技术。仅管如此还是有许多开发者有各种原因喜欢使用这种交互式的应用程序安装 Composer。在安装前请先确认你的 PHP 安装项目如下:

由于手动安装没有执行这些检查,你必须自已衡量决定是否值得做这些事,以下是如何手动安装 Composer :

curl -s https://getcomposer.org/composer.phar -o $HOME/local/bin/composer
chmod +x $HOME/local/bin/composer

路径 $HOME/local/bin (或是你选择的路径) 应该在你的 $PATH 环境变量中。这将会影响 composer 这个命令是否可用.

当你遇到文档指出执行 Composer 的命令是 php composer.phar install时,你可以使用下面命令替代:

composer install

本章节会假设你已经安装了全局的 Composer。

如何设置及安装依赖

Composer 会通过一个 composer.json 文件持续的追踪你的项目依赖。 如果你喜欢,你可以手动管理这个文件,或是使用 Composer 自己管理。composer require 这个指令会增加一个项目依赖,如果你还没有 composer.json 文件, 将会创建一个。这里有个例子为你的项目加入 Twig 依赖。

composer require twig/twig:~1.8

另外 composer init 命令将会引导你创建一个完整的 composer.json 文件到你的项目之中。无论你使用哪种方式,一旦你创建了 composer.json 文件,你可以告诉 Composer 去下载及安装你的依赖到 vendor/ 目录中。这命令也适用于你已经下载并已经提供了一个 composer.json 的项目:

composer install

接下来,添加这一行到你应用的主要 PHP 文件中,这将会告诉 PHP 为你的项目依赖使用 Composer 的自动加载器。

<?php
require 'vendor/autoload.php';

现在你可以使用你项目中的依赖,且它们会在需要时自动完成加载。

更新你的依赖

Composer 会建立一个 composer.lock 文件,在你第一次执行 php composer install 时,存放下载的每个依赖包精确的版本编号。假如你要分享你的项目给其他开发者,并且 composer.lock 文件也在你分享的文件之中的话。 当他们执行 php composer.phar install 这个命令时,他们将会得到与你一样的依赖版本。 当你要更新你的依赖时请执行 php composer update。请不要在部署代码的时候使用 composer update,只能使用 composer install 命令,否则你会发现你在生产环境中用的是不同的扩展包依赖版本。

当你需要灵活的定义你所需要的依赖版本时,这是最有用。 举例来说需要一个版本为 ~1.8 时,意味着 “任何大于 1.8.0 ,但小于 2.0.x-dev 的版本”。你也可以使用通配符 *1.8.* 之中。现在Composer在composer update 时将升级你的所有依赖到你限制的最新版本。

更新通知

要接收有关新版本发布的通知,您可以注册libraries.io,这是一种可以监视依赖关系并向您发送更新警报的Web服务。

检查你的依赖安全问题

Security Advisories Checker 是一个 web 服务和一个命令行工具,二者都会仔细检查你的 composer.lock 文件,并且告诉你任何你需要更新的依赖。

处理 Composer 全局依赖

Composer 也可以处理全局依赖和他们的二进制文件。用法很直接,你所要做的就是在命令前加上global前缀。如果你想安装 PHPUnit 并使它全局可用,你可以运行下面的命令:

composer global require phpunit/phpunit

这将会创建一个 ~/.composer 目录存放全局依赖,要让已安装依赖的二进制命令随处可用,你需要添加 ~/.composer/vendor/bin 目录到你的 $PATH 变量。

PEAR 介绍

PEAR 是另一个常用的依赖包管理器, 它跟 Composer 很类似,但是也有一些显著的区别。

PEAR 需要扩展包有专属的结构, 开发者在开发扩展包的时候要提前考虑为 PEAR 定制, 否则后面将无法使用 PEAR.

PEAR 安装扩展包的时候, 是全局安装的, 意味着一旦安装了某个扩展包, 同一台服务器上的所有项目都能用上, 当然, 好处是当多个项目共同使用同一个扩展包的同一个版本, 坏处是如果你需要使用不同版本的话, 就会产生冲突.

如何安装 PEAR

你可以通过下载 .phar 文件来安装 PEAR. 官方文档安装部分 里面有不同系统中安装 PEAR 的详细信息.

如果你是使用 Linux, 你可以尝试找下系统应用管理器, 举个栗子, Debian 和 Ubuntu 有个 php-pear 的 apt 安装包.

如何安装扩展包

如果扩展包是在 PEAR packages list 这个列表里面的, 你可以使用以下命令安装:

pear install foo

如果扩展包是托管到别的渠道上, 你需要 发现 (discover) 渠道先, 请见文档 使用渠道.

使用 Composer 来安装 PEAR 扩展包

如果你正在使用 Composer, 并且你想使用一些 PEAR 的代码, 你可以通过 Composer 来安装 PEAR 扩展包.

下面是从 pear2.php.net 安装代码依赖的示例:

{
    "repositories": [
        {
            "type": "pear",
            "url": "http://pear2.php.net"
        }
    ],
    "require": {
        "pear-pear2/PEAR2_Text_Markdown": "*",
        "pear-pear2/PEAR2_HTTP_Request": "*"
    }
}

第一部分 "repositories" 是让 Composer 从哪个渠道去获取扩展包, 然后, "require" 部分使用下面的命名规范:

pear-channel/Package

前缀 “pear” 是为了避免冲突写死的,因为 pear-channel 有可能是任意扩展包名称,所以 channel 的简称(或者是完整 URL)可以用来指引扩展包在哪个 channel 里。

成功安装扩展包以后, 代码会放到项目的 vendor 文件夹中, 并且可以通过加载 Composer 的自动加载器进行加载:

vendor/pear-pear2.php.net/PEAR2_HTTP_Request/pear2/HTTP/Request.php

在代码里面可以这样使用:

<?php
$request = new pear2\HTTP\Request();

回到顶部

开发实践

基础知识

PHP 是一门庞大的语言,各个水平层次的开发者都可以利用它进行迅捷高效的开发。然而在对语言逐渐深入的学习过程中,我们往往会因为走捷径和/或不良习惯而忘记(或忽视掉)基础的知识。为了帮助彻底解决这个问题,这一章的目的就是提醒开发人员注意有关 PHP 的基础编程实践。

日期和时间

PHP 中 DateTime 类的作用是在你读、写、比较或者计算日期和时间时提供帮助。除了 DateTime 类之外,PHP 还有很多与日期和时间相关的函数,但 DateTime 类为大多数常规使用提供了优秀的面向对象接口。它还可以处理时区,不过这并不在这篇简短的介绍之内。

在使用 DateTime 之前,通过 createFromFormat() 工厂方法将原始的日期与时间字符串转换为对象或使用 new DateTime 来取得当前的日期和时间。使用 format() 将 DateTime 转换回字符串用于输出。

<?php
$raw = '22. 11. 1968';
$start = DateTime::createFromFormat('d. m. Y', $raw);

echo 'Start date: ' . $start->format('Y-m-d') . "\n";

对 DateTime 进行计算时可以使用 DateInterval 类。DateTime 类具有例如 add()sub() 等将 DateInterval 当作参数的方法。编写代码时注意不要认为每一天都是由相同的秒数构成的,不论是夏令时(DST)还是时区转换,使用时间戳计算都会遇到问题,应当选择日期间隔。使用 diff() 方法来计算日期之间的间隔,它会返回新的 DateInterval,非常容易进行展示。

<?php
// create a copy of $start and add one month and 6 days
$end = clone $start;
$end->add(new DateInterval('P1M6D'));

$diff = $end->diff($start);
echo 'Difference: ' . $diff->format('%m month, %d days (total: %a days)') . "\n";
// Difference: 1 month, 6 days (total: 37 days)

DateTime 对象之间可以直接进行比较:

<?php
if ($start < $end) {
    echo "Start is before end!\n";
}

最后一个例子来演示 DatePeriod 类。它用来对循环的事件进行迭代。向它传入开始时间、结束时间和间隔区间,会得到这其中所有的事件。

<?php
// output all thursdays between $start and $end
$periodInterval = DateInterval::createFromDateString('first thursday');
$periodIterator = new DatePeriod($start, $periodInterval, $end, DatePeriod::EXCLUDE_START_DATE);
foreach ($periodIterator as $date) {
    // output each date in the period
    echo $date->format('Y-m-d') . ' ';
}

一个有名的 API 扩展是 Carbon。Carbon 不仅继承了所有 DateTime 类提供的功能,还提供了更多的人性化功能,例如自然语言时间处理、国际化支持、对象之间执行增减算术。

设计模式

当你在编写自己的应用程序时,最好在项目的代码和整体架构中使用通用的设计模式,这将帮助你更轻松地对程序进行维护,也能够让其他的开发者更快地理解你的代码。

当你使用框架进行开发时,绝大部分的上层代码以及项目结构都会基于所使用的框架,因此很多关于设计模式的决定已经由框架帮你做好了。当然,你还是可以挑选你最喜欢的模式并在你的代码中进行应用。但如果你并没有使用框架的话,你就需要自己去寻找适合你的应用的最佳模式了。

您可以了解有关PHP设计模式的更多信息,并查看以下工作示例:

https://designpatternsphp.readthedocs.io/

使用 UTF-8 编码

本章是由 Alex Cabal 最初撰写在 PHP Best Practices 中的,我们使用它作为进行建议的基础

这不是在开玩笑。请小心、仔细并且前后一致地处理它。

目前,PHP 仍未在底层实现对 Unicode 的支持。虽然有很多途径可以确保 UTF-8 字符串能够被正确地处理,但这并不是很简单的事情,通常需要对 Web 应用进行全方面的检查,从 HTML 到 SQL 再到 PHP。我们将争取进行一个简洁实用的总结。

PHP 层面的 UTF-8

最基本的字符串操作,像是连结两个字符串或将字符串赋值给变量,并不需要对 UTF-8 做特别的处理。然而大多数字符串的函数,像 strpos()strlen(),确实需要特别的处理。这些函数名中通常包含 mb_*:比如,mb_strpos()mb_strlen()。这些 mb_* 字符串是由 Multibyte String Extension 提供支持的,它专门为操作 Unicode 字符串而特别进行了设计。

在操作 Unicode 字符串时,请你务必使用 mb_* 函数。例如,如果你对一个 UTF-8 字符串使用 substr(),那返回的结果中有很大可能会包含一些乱码。正确的方式是使用 mb_substr()

最难的地方在于每次都要记得使用 mb_* 函数。如果你哪怕只有一次忘记了使用,你的 Unicode 字符串就有在接下来的过程中变成乱码的风险。

不是所有的字符串函数都有一个对应的 mb_* 函数。如果你想要的功能没有对应的 mb_* 函数的话,那只能说你运气不佳了。

你应该在你所有的 PHP 脚本(或全局包含的脚本)的开头使用 mb_internal_encoding() 函数,然后紧接着在会对浏览器进行输出的脚本中使用 mb_http_output()。在每一个脚本当中明确声明字符串的编码可以免去很多日后的烦恼。

另外,许多对字符串进行操作的函数都有一个可选的参数用来指定字符串编码。当可以设定这类参数时,你应该始终明确指定使用 UTF-8。例如,htmlentities() 有一个字符编码的选项,你应该始终将其设为 UTF-8。从 PHP 5.4.0 开始, htmlentities()htmlspecialchars() 的编码都已经被默认设为了 UTF-8。

最后,如果你所编写的是分布式的应用程序并且不能确定 mbstring 扩展一定开启的话,可以考虑使用 patchwork/utf8 Composer 包。它会在 mbstring 可用时自动使用,否则自动切换回非 UTF-8 函数。

数据库层面的 UTF-8

如果你使用 PHP 来操作到 MySQL,有些时候即使你做到了上面的每一点,你的字符串仍可能面临在数据库中以非 UTF-8 的格式进行存储的问题。

为了确保你的字符串从 PHP 到 MySQL都使用 UTF-8,请检查确认你的数据库和数据表都设定为 utf8mb4 字符集和整理,并且确保你的 PDO 连接请求也使用了 utf8mb4 字符集。请看下方的示例代码,这是 非常重要 的。

请注意为了完整的 UTF-8 支持,你必须使用 utf8mb4 而不是 utf8!你会在进一步阅读中找到原因。

浏览器层面的 UTF-8

使用 mb_http_output() 函数来确保 PHP 向浏览器输出 UTF-8 格式的字符串。

随后浏览器需要接收 HTTP 应答来指定页面是由 UTF-8 进行编码的。 然后,HTTP响应需要告知浏览器该页面应被视为UTF-8。今天,通常在HTTP响应头中设置字符集,如下所示:

<?php
            header('Content-Type: text/html; charset=UTF-8')

这样做的历史性方法是在页面标记包含charset<meta><head>标记。

<?php
                // Tell PHP that we're using UTF-8 strings until the end of the script
                mb_internal_encoding('UTF-8');
                $utf_set = ini_set('default_charset', 'utf-8');
                if (!$utf_set) {
                    throw new Exception('could not set default_charset to utf-8, please ensure it\'s set on your system!');
                }
                
                // Tell PHP that we'll be outputting UTF-8 to the browser
                mb_http_output('UTF-8');
                 
                // Our UTF-8 test string
                $string = 'Êl síla erin lû e-govaned vîn.';
                
                // Transform the string in some way with a multibyte function
                // Note how we cut the string at a non-Ascii character for demonstration purposes
                $string = mb_substr($string, 0, 15);
                
                // Connect to a database to store the transformed string
                // See the PDO example in this document for more information
                // Note the `charset=utf8mb4` in the Data Source Name (DSN)
                $link = new PDO(
                    'mysql:host=your-hostname;dbname=your-db;charset=utf8mb4',
                    'your-username',
                    'your-password',
                    array(
                        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                        PDO::ATTR_PERSISTENT => false
                    )
                );
                
                // Store our transformed string as UTF-8 in our database
                // Your DB and tables are in the utf8mb4 character set and collation, right?
                $handle = $link->prepare('insert into ElvishSentences (Id, Body, Priority) values (default, :body, :priority)');
                $handle->bindParam(':body', $string, PDO::PARAM_STR);
                $priority = 45;
                $handle->bindParam(':priority', $priority, PDO::PARAM_INT); // explicitly tell pdo to expect an int
                $handle->execute();
                
                // Retrieve the string we just stored to prove it was stored correctly
                $handle = $link->prepare('select * from ElvishSentences where Id = :id');
                $id = 7;
                $handle->bindParam(':id', $id, PDO::PARAM_INT);
                $handle->execute();
                
                // Store the result into an object that we'll output later in our HTML
                // This object won't kill your memory because it fetches the data Just-In-Time to
                $result = $handle->fetchAll(\PDO::FETCH_OBJ);
                
                // An example wrapper to allow you to escape data to html
                function escape_to_html($dirty){
                    echo htmlspecialchars($dirty, ENT_QUOTES, 'UTF-8');
                }
                
                header('Content-Type: text/html; charset=UTF-8'); // Unnecessary if your default_charset is set to utf-8 already
                ?><!doctype html>
                <html>
                    <head>
                        <meta charset="UTF-8">
                        <title>UTF-8 test page</title>
                    </head>
                    <body>
                        <?php
                        foreach($result as $row){
                            escape_to_html($row->Body);  // This should correctly output our transformed UTF-8 string to the browser
                        }
                        ?>
                    </body>
                </html>

Further reading

国际化(i18n)和本地化(l10n)

对于新手的免责声明:i18n 和 l10n 是使用数字简略拼写方式来实现缩写,在我们的例子里:internationalization 是 i18n,而 localization 简写为 l10n。

首先,我们需要定义这两个相似的概念,还有相关的概念:

一般实现的方法

使PHP软件国际化的最简单方法是使用数组文件并在模板中使用这些字符串,例如 <h1><?=$TRANS['title_about_page']?></h1>然而,这种方式几乎不建议用于严肃的项目,因为它会在路上造成一些维护问题 - 有些可能在最开始出现,例如多元化。所以,请不要尝试这个,如果你的项目将包含超过几页。

最经典的方式,通常作为i18n和l10n的参考,是一个名为gettextUnix工具它可以追溯到1995年,仍然是翻译软件的完整实现。它很容易运行,同时仍然运行强大的支持工具。这是关于我们将在这里谈论的Gettext。此外,为了帮助您避免在命令行上乱七八糟,我们将展示一个出色的GUI应用程序,可用于轻松更新您的l10n源代码

其他工具

使用的公共库支持Gettext和i18n的其他实现。其中一些可能看起来更容易安装或运行其他功能或i18n文件格式。在本文档中,我们将重点介绍PHP核心提供的工具,但在此我们列出了其他完成的工具:

其他框架也包括i18n模块,但这些模块在其代码库之外是不可用的:

如果您决定使用其中一个不提供提取器的库,您可能需要使用gettext格式,因此您可以使用原始的gettext工具链(包括Poedit),如本章其余部分所述。

Gettext的

安装

您可能需要使用包管理器安装Gettext和相关的PHP库,如apt-getyum安装后,通过添加extension=gettext.so(Linux / Unix)或extension=php_gettext.dll(Windows)来启用它php.ini

在这里,我们还将使用Poedit创建翻译文件。您可能会在系统的包管理器中找到它; 它适用于Unix,Mac和Windows,也可以在他们的网站上免费下载 。

结构体

文件类型

使用gettext时,通常会处理三个文件。主要是PO(可移植对象)和MO(机器对象)文件,第一个是可读“翻译对象”的列表,第二个是进行本地化时由gettext解释的相应二进制文件。还有一个POT(模板)文件,它只包含源文件中的所有现有密钥,可用作生成和更新所有PO文件的指南。这些模板文件不是强制性的:根据你用来做l10n的工具,你可以只使用PO / MO文件。每种语言和区域总是有一对PO / MO文件,但每个域只有一个POT。

在某些大型项目中,当相同的单词根据上下文传达不同的含义时,您可能需要将翻译分开。在这些情况下,您将它们分成不同的它们基本上是POT / PO / MO文件的命名组,其中文件名是所述翻译域为简单起见,中小型项目通常只使用一个域; 它的名称是任意的,但我们将使用“main”作为代码示例。例如,Symfony项目中,域用于分隔验证消息的转换。

区域代码

语言环境只是识别一种语言版本的代码。它遵循ISO 639-1和 ISO 3166-1 alpha-2规范定义:语言的两个小写字母,可选地后跟下划线和两个标识国家或地区代码的大写字母。对于稀有语言,使用三个字母。

对于一些发言者来说,国家部分可能看似多余。事实上,有些语言在不同国家有方言,例如奥地利德语(de_AT)或巴西葡萄牙语(pt_BR)。第二部分用于区分这些方言 - 当它不存在时,它被视为该语言的“通用”或“混合”版本。

目录结构

要使用Gettext,我们需要遵循特定的文件夹结构。首先,您需要为源存储库中的l10n文件选择任意根。在其中,您将拥有每个所需区域设置的文件夹,以及LC_MESSAGES包含所有PO / MO对的固定文件夹。例:

<project root>
     ├─ src/
     ├─ templates/
     └─ locales/
        ├─ forum.pot
        ├─ site.pot
        ├─ de/
        │  └─ LC_MESSAGES/
        │     ├─ forum.mo
        │     ├─ forum.po
        │     ├─ site.mo
        │     └─ site.po
        ├─ es_ES/
        │  └─ LC_MESSAGES/
        │     └─ ...
        ├─ fr/
        │  └─ ...
        ├─ pt_BR/
        │  └─ ...
        └─ pt_PT/
           └─ ...

多种形式

正如我们在引言中所说,不同的语言可能会有不同的复数规则。但是,gettext再一次让我们免于这个麻烦。在创建新.po文件时,您必须声明该语言复数规则,并且对于多个敏感的已翻译片段将针对每个规则具有不同的形式。在代码中调用Gettext时,您必须指定与该句子相关的数字,并且它将确定要使用的正确形式 - 如果需要,甚至使用字符串替换。

多个规则包括可用的复数的数量和一个布尔测试,n用于定义给定数字落在哪个规则中(以0开始计数)。例如:

现在您了解了复数规则的工作原理 - 如果您没有,请查看有关LingoHub教程的更深入的解释- 您可能希望从列表中复制所需的规则而不是手动编写它们。

当调用Gettext来对具有计数器的句子进行本地化时,您还必须提供相关的数字。Gettext将确定应该生效的规则并使用正确的本地化版本。.po对于定义的每个复数规则,您需要在文件中包含不同的句子。

示例实施

毕竟这个理论,让我们有点实用。这是一个.po文件的摘录- 不要介意它的格式,而是整体内容; 您将在以后学习如何轻松编辑它:

msgid ""
    msgstr ""
    "Language: pt_BR\n"
    "Content-Type: text/plain; charset=UTF-8\n"
    "Plural-Forms: nplurals=2; plural=(n > 1);\n"
    
    msgid "We are now translating some strings"
    msgstr "Nós estamos traduzindo algumas strings agora"
    
    msgid "Hello %1$s! Your last visit was on %2$s"
    msgstr "Olá %1$s! Sua última visita foi em %2$s"
    
    msgid "Only one unread message"
    msgid_plural "%d unread messages"
    msgstr[0] "Só uma mensagem não lida"
    msgstr[1] "%d mensagens não lidas"

第一部分像一个报头,将具有msgidmsgstr特别是空的。它描述了文件编码,复数形式和其他不太相关的东西。第二部分将一个简单的字符串从英语转换为巴西葡萄牙语,第三部分则相同,但利用字符串替换,sprintf因此翻译可能包含用户名和访问日期。最后一部分是复数形式的样本,msgid以英语显示单数和复数形式,并且它们对应的翻译为msgstr0和1(遵循复数规则给出的数字)。在那里,使用字符串替换,因此可以通过使用直接在句子中看到数字%d复数形式总是有两个msgid (单数和复数),因此建议不要使用复杂的语言作为翻译的来源。

关于l10n键的讨论

您可能已经注意到,我们使用英语中的实际句子作为源ID。msgid与所有.po文件中使用的相同,这意味着其他语言将具有相同的格式和相同的msgid字段,但是已翻译的msgstr行。

谈论翻译密钥,这里有两个主要的“学校”:

  1. msgid作为一个真正的句子主要优点是:

    • 如果在任何给定语言中有未翻译的软件,则显示的密钥仍将保留一些含义。示例:如果您碰巧将英语翻译成西班牙语但需要帮助翻译成法语,您可能会发布缺少法语句子的新页面,而网站的某些部分将以英语显示;

    • 翻译人员更容易理解正在发生的事情并根据其进行适当的翻译 msgid

    • 它为你提供一种语言的“免费”l10n - 源语言;

    • 唯一的缺点是:如果您需要更改实际文本,则需要msgid 在多个语言文件中替换相同的文本

  2. msgid作为一个独特的结构化密钥它将以结构化的方式描述应用程序中的句子角色,包括字符串所在的模板或部分而不是其内容。

    • 这是组织代码的好方法,将文本内容与模板逻辑分开。

    • 然而,这可能会给错过上下文的翻译带来问题。需要源语言文件作为其他翻译的基础。示例:开发人员理想情况下会有一个en.po文件,翻译人员会阅读文件以了解要写入的内容fr.po

    • 缺少翻译将在屏幕上显示无意义的键(top_menu.welcome而不是Hello there, User! 在所述未翻译的法语页面上)。这很好,因为在发布之前会强制完成翻译 - 但是,由于翻译问题在界面中会非常糟糕,因此很糟糕。但是,有些库包括将给定语言指定为“后备”的选项,具有与其他方法类似的行为。

Gettext的手册有利于为,在一般情况下,它是译者和用户在遇到麻烦的情况下,更容易的第一种方法。这也是我们在这里工作的方式。但是,Symfony文档支持基于关键字的翻译,允许在不影响模板的情况下对所有翻译进行独立更改。

日常使用

在典型的应用程序中,您可以在页面中编写静态文本时使用一些Gettext函数。然后,这些句子将出现在.po文件中,被翻译,编译成.mo文件,然后在呈现实际界面时由Gettext使用。鉴于此,让我们在一步一步的例子中将我们到目前为止所讨论的内容联系起来:

1.示例模板文件,包括一些不同的gettext调用

<?php include 'i18n_setup.php' ?>
    <div id="header">
        <h1><?=sprintf(gettext('Welcome, %s!'), $name)?></h1>
        <!-- code indented this way only for legibility -->
        <?php if ($unread): ?>
            <h2><?=sprintf(
                ngettext('Only one unread message',
                         '%d unread messages',
                         $unread),
                $unread)?>
            </h2>
        <?php endif ?>
    </div>
    
    <h1><?=gettext('Introduction')?></h1>
    <p><?=gettext('We\'re now translating some strings')?></p>

2.示例设置文件(i18n_setup.php如上所用),选择正确的区域设置并配置Gettext

<?php
    /**
     * Verifies if the given $locale is supported in the projecta
     * @param string $locale
     * @return bool
     */
    function valid($locale) {
       return in_array($locale, ['en_US', 'en', 'pt_BR', 'pt', 'es_ES', 'es']);
    }
    
    //setting the source/default locale, for informational purposes
    $lang = 'en_US';
    
    if (isset($_GET['lang']) && valid($_GET['lang'])) {
        // the locale can be changed through the query-string
        $lang = $_GET['lang'];    //you should sanitize this!
        setcookie('lang', $lang); //it's stored in a cookie so it can be reused
    } elseif (isset($_COOKIE['lang']) && valid($_COOKIE['lang'])) {
        // if the cookie is present instead, let's just keep it
        $lang = $_COOKIE['lang']; //you should sanitize this!
    } elseif (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
        // default: look for the languages the browser says the user accepts
        $langs = explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']);
        array_walk($langs, function (&$lang) { $lang = strtr(strtok($lang, ';'), ['-' => '_']); });
        foreach ($langs as $browser_lang) {
            if (valid($browser_lang)) {
                $lang = $browser_lang;
                break;
            }
        }
    }
    
    // here we define the global system locale given the found language
    putenv("LANG=$lang");
    
    // this might be useful for date functions (LC_TIME) or money formatting (LC_MONETARY), for instance
    setlocale(LC_ALL, $lang);
    
    // this will make Gettext look for ../locales/<lang>/LC_MESSAGES/main.mo
    bindtextdomain('main', '../locales');
    
    // indicates in what encoding the file should be read
    bind_textdomain_codeset('main', 'UTF-8');
    
    // if your application has additional domains, as cited before, you should bind them here as well
    bindtextdomain('forum', '../locales');
    bind_textdomain_codeset('forum', 'UTF-8');
    
    // here we indicate the default domain the gettext() calls will respond to
    textdomain('main');
    
    // this would look for the string in forum.mo instead of main.mo
    // echo dgettext('forum', 'Welcome back!');
    ?>

3.为第一次运行准备翻译

Gettext比定制框架i18n软件包具有的巨大优势之一是其广泛而强大的文件格式。“哦,伙计,这很难理解和手工编辑,一个简单的阵列会更容易!”毫无疑问,像Poedit这样的应用程序可以提供帮助 - 很多您可以从他们的网站获取该程序,它是免费的,适用于所有平台。这是一个非常容易习惯的工具,同时也是一个非常强大的工具 - 使用Gettext提供的所有功能。本指南基于PoEdit 1.8。

在第一次运行中,您应从菜单中选择“文件>新建...”。您将直接询问该语言:在这里您可以选择/过滤要翻译的语言,或使用我们之前提到的格式,例如 en_USpt_BR

现在,保存文件 - 使用我们提到的目录结构。然后,您应该单击“从源提取”,在此处您将为提取和转换任务配置各种设置。稍后您可以通过“目录>属性”找到所有这些:

设置这些点后,它将扫描源文件以查找所有本地化调用。每次扫描后,PoEdit将显示已找到的内容和从源文件中删除的内容的摘要。新条目将填充到转换表中,您将开始键入这些字符串的本地化版本。保存它,一个.mo文件将被(重新)编译到同一个文件夹和ta-dah:你的项目是国际化的。

4.翻译字符串

您可能已经注意到,有两种主要类型的本地化字符串:简单字符串和复数形式字符串。第一个只有两个框:源和本地化字符串。无法修改源字符串,因为Gettext / Poedit不包含更改源文件的权限 - 您应该更改源本身并重新扫描文件。提示:您可以右键单击翻译行,它会提示您使用源文件和正在使用该字符串的行。另一方面,复数形式字符串包括两个用于显示两个源字符串的框和制表符,因此您可以配置不同的最终形式。

每当您更改源并需要更新翻译时,只需点击刷新,Poedit将重新扫描代码,删除不存在的条目,合并已更改的条目并添加新条目。它也可能会根据您所做的其他翻译尝试猜测一些翻译。那些猜测和更改的条目将收到一个“模糊”标记,表明它需要审查,在列表中显示为黄金。如果你有一个翻译团队并且有人试图写一些他们不确定的东西也很有用:只需标记模糊,其他人稍后会复习。

最后,建议以“先查看>未翻译的条目”离开标记,因为它会帮助你很多不要忘记任何条目。从该菜单中,您还可以打开部分UI,以便在需要时为翻译人员提供上下文信息。

提示与技巧

可能的缓存问题

如果您在Apache(mod_php上运行PHP作为模块,则可能会遇到.mo缓存文件的问题它会在第一次读取时发生,然后,为了更新它,您可能需要重新启动服务器。在Nginx和PHP5上,通常只需要几次页面刷新来刷新转换缓存,而在PHP7上则很少需要。

其他辅助功能

作为许多人的首选,它更容易使用_()而不是gettext()来自框架的许多自定义i18n库也使用类似的东西t(),以使翻译的代码更短。然而,这是体育捷径的唯一功能。您可能希望在项目中添加其他一些内容,例如__()或者_n()用于加入调用ngettext()的幻想其他库,例如 oscarotero的Gettext也提供这样的辅助函数。_r()gettext()sprintf()

在这些情况下,您需要指示Gettext实用程序如何从这些新函数中提取字符串。不要害怕; 这很容易。它只是.po文件中的一个字段,或Poedit上的“设置”屏幕。在编辑器中,该选项位于“目录>属性>源关键字”中。请记住:Gettext已经知道许多语言的默认函数,所以不要害怕该列表是否为空。您需要按照特定格式包含这些新功能的规格

在文件中包含这些新规则后.po,新扫描将像以前一样简单地引入新字符串。

参考


回到顶部

依赖注入

出自维基百科 Wikipedia:

依赖注入是一种允许我们从硬编码的依赖中解耦出来,从而在运行时或者编译时能够修改的软件设计模式。

这句解释让依赖注入的概念听起来比它实际要复杂很多。依赖注入通过构造注入,函数调用或者属性的设置来提供组件的依赖关系。就是这么简单。

基本概念

我们可以用一个简单的例子来说明依赖注入的概念

下面的代码中有一个 Database 的类,它需要一个适配器来与数据库交互。我们在构造函数里实例化了适配器,从而产生了耦合。这会使测试变得很困难,而且 Database 类和适配器耦合的很紧密。

<?php
namespace Database;

class Database
{
    protected $adapter;

    public function __construct()
    {
        $this->adapter = new MySqlAdapter;
    }
}

class MysqlAdapter {}

这段代码可以用依赖注入重构,从而解耦

<?php
namespace Database;

class Database
{
    protected $adapter;

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

class MysqlAdapter {}

现在我们通过外界给予 Database 类的依赖,而不是让它自己产生依赖的对象。我们甚至能用可以接受依赖对象参数的成员函数来设置,或者如果 $adapter 属性本身是 public的,我们可以直接给它赋值。

复杂的问题

如果你曾经了解过依赖注入,那么你可能见过 “控制反转”(Inversion of Control) 或者 “依赖反转准则”(Dependency Inversion Principle)这种说法。这些是依赖注入能解决的更复杂的问题。

控制反转

顾名思义,一个系统通过组织控制和对象的完全分离来实现”控制反转”。对于依赖注入,这就意味着通过在系统的其他地方控制和实例化依赖对象,从而实现了解耦。

一些 PHP 框架很早以前就已经实现控制反转了,但是问题是,应该反转哪部分以及到什么程度?比如, MVC 框架通常会提供超类或者基本的控制器类以便其他控制器可以通过继承来获得相应的依赖。这就是控制反转的例子,但是这种方法是直接移除了依赖而不是减轻了依赖。

依赖注入允许我们通过按需注入的方式更加优雅地解决这个问题,完全不需要任何耦合。

S.O.L.I.D

单一责任原则

单一责任原则是关于演员和高级架构。它指出“一个类应该只有一个改变的理由。”这意味着每个类应该对软件提供的单个功能部分负责。这种方法的最大好处是它可以提高代码的 可重用性通过设计我们的类只做一件事,我们可以在任何其他程序中使用(或重复使用)它而无需更改它。

开放/封闭原则

开放/封闭原则是关于类设计和功能扩展。它指出“软件实体(类,模块,函数等)应该是可以扩展的,但是关闭以进行修改。”这意味着我们应该在需要新功能时设计我们的模块,类和函数。 ,我们不应修改现有代码,而应编写将由现有代码使用的新代码。实际上,这意味着我们应该编写实现并遵守接口的类,然后对这些接口而不是特定类进行类型提示。

这种方法的最大好处是我们可以非常轻松地扩展我们的代码,支持新的代码,而无需修改现有代码,这意味着我们可以减少QA时间,并且大大降低了对应用程序产生负面影响的风险。我们可以更快,更自信地部署新代码。

利斯科夫替代原则

Liskov替代原则是关于子类型和继承。它声明“子类不应该破坏父类的类型定义。”或者,用Robert C. Martin的话来说,“子类型必须可以替代它们的基类型”。

例如,如果我们有一个FileInterface定义embed()方法接口,并且我们有AudioVideo 类都实现了FileInterface接口,那么我们可以预期embed()方法的使用总是会做我们想要的事情。如果我们稍后创建一个实现 接口PDF类或GistFileInterface,我们就会知道并理解该embed()方法的作用。这种方法的最大好处是我们能够构建灵活且易于配置的程序,因为当我们将一个类型的对象(例如FileInterface更改为另一个对象时,我们不需要更改程序中的任何其他对象。

接口隔离原理

接口隔离原则(ISP)是关于业务逻辑到客户端的通信。它声明“不应该强迫任何客户端依赖它不使用的方法。”这意味着我们应该提供一组较小的,特定于概念的接口,而不是只有一个单一的接口才能实现所有符合类的接口。符合类实现一个或多个。

例如,a CarBusclass会对方法感兴趣steeringWheel(),但是a MotorcycleTricycle class不会。相反,a MotorcycleTricycleclass会对方法感兴趣handlebars(),但是a CarBusclass不会。没有必要把所有这些类型的车辆的实施都支持 steeringWheel(),以及handlebars(),所以我们应该打破开的源接口。

依赖倒置原则

依赖性倒置原则是关于删除离散类之间的硬链接,以便通过传递不同的类来利用新功能。它指出应该“依赖于抽象。不要依赖于具体的东西。”简而言之,这意味着我们的依赖项应该是接口/契约或抽象类而不是具体实现。我们可以轻松地重构上面的例子来遵循这个原则。

<?php
namespace Database;

class Database
{
    protected $adapter;

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

interface AdapterInterface {}

class MysqlAdapter implements AdapterInterface {}

现在 Database 类依赖于接口,相比依赖于具体实现有更多的优势。

假设你工作的团队中,一位同事负责设计适配器。在第一个例子中,我们需要等待适配器设计完之后才能单元测试。现在由于依赖是一个接口/约定,我们能轻松地模拟接口测试,因为我们知道同事会基于约定实现那个适配器

这种方法的一个更大的好处是代码扩展性变得更高。如果一年之后我们决定要迁移到一种不同的数据库,我们只需要写一个实现相应接口的适配器并且注入进去,由于适配器遵循接口的约定,我们不需要额外的重构。

容器

你应该明白的第一件事是依赖注入容器和依赖注入不是相同的概念。容器是帮助我们更方便地实现依赖注入的工具,但是他们通常被误用来实现反模式设计:服务定位(Service Location)。把一个依赖注入容器作为服务定位器( Service Locator)注入进类中隐式地建立了对于容器的依赖,而不是真正需要替换的依赖,而且还会让你的代码更不透明,最终变得更难测试。

大多数现代的框架都有自己的依赖注入容器,允许你通过配置将依赖绑定在一起。这实际上意味着你能写出和框架层同样干净、解耦的应用层代码。

延伸阅读

回到顶部

数据库

绝大多数时候你的 PHP 程序都需要使用数据库来长久地保存数据。这时你有一些不同的选择可以来连接并与数据库进行交互。在 PHP 5.1.0 之前,我们推荐的方式是使用例如 mysqlipgsqlmssql 等原生驱动。

原生驱动是在只使用 一个 数据库的情况下的不错的方式,但如果,举个例子来说,你同时使用了 MySQL 和一点点 MSSQL,或者你需要使用 Oracle 的数据库,那你就不能够只使用一个数据库驱动了。你需要为每一个数据库去学习各自不同的 API — 这样做显然不科学。

MySQL 扩展

mysql 非常古老,并且已经被以下两个扩展取代:

PHP 中的 mysql 扩展已经不再进行新的开发了,在 PHP 5.5.0 版本中正式标记为废弃,并在 7.0 正式被移除

想要辨别 mysql 是否被使用,你不需要到 php.ini 去查看。只需要使用编辑器打开你的项目,然后全局搜索 mysql_* ,如果有类似 mysql_connect() 或者 mysql_query() 方法出现,那么你就使用了 mysql

即使你还没有使用PHP 7.x,但是当PHP 7.x升级出现时,如果不尽快考虑升级将导致更大的困难。最好的选择是在你自己的开发计划中用你的应用程序中的mysqliPDO替换mysql用法,这样你以后就不会匆忙。

如果你是从 mysql 升级到 mysqli,请尽量不要使用全局替换 mysql_*mysqli_*。你会错过 mysqli 提供的一些优秀特性,如数据参数绑定,此功能能有效的防止 SQL 注入攻击。PDO 也提供此功能。

PDO 扩展

PDO 是一个数据库连接抽象类库 — 自 5.1.0 版本起内置于 PHP 当中 — 它提供了一个通用的接口来与不同的数据库进行交互。比如你可以使用相同的简单代码来连接 MySQL 或是 SQLite:

<?php
// PDO + MySQL
$pdo = new PDO('mysql:host=example.com;dbname=database', 'user', 'password');
$statement = $pdo->query("SELECT some_field FROM some_table");
$row = $statement->fetch(PDO::FETCH_ASSOC);
echo htmlentities($row['some_field']);

// PDO + SQLite
$pdo = new PDO('sqlite:/path/db/foo.sqlite');
$statement = $pdo->query("SELECT some_field FROM some_table");
$row = $statement->fetch(PDO::FETCH_ASSOC);
echo htmlentities($row['some_field']);

PDO 并不会对 SQL 请求进行转换或者模拟实现并不存在的功能特性;它只是单纯地使用相同的 API 连接不同种类的数据库。

更重要的是,PDO 使你能够安全的插入外部输入(例如 ID)到你的 SQL 请求中而不必担心 SQL 注入的问题。这可以通过使用 PDO 语句和限定参数来实现。

我们来假设一个 PHP 脚本接收一个数字 ID 作为一个请求参数。这个 ID 应该被用来从数据库中取出一条用户记录。下面是一个错误的做法:

<?php
$pdo = new PDO('sqlite:/path/db/users.db');
$pdo->query("SELECT name FROM users WHERE id = " . $_GET['id']); // <-- NO!

这是一段糟糕的代码。你正在插入一个原始的请求参数到 SQL 请求中。这将让被黑客轻松地利用[SQL Injection]方式进行攻击。想一下如果黑客将一个构造的 id 参数通过像 http://domain.com/?id=1%3BDELETE+FROM+users 这样的 URL 传入。这将会使 $_GET['id'] 变量的值被设为 1;DELETE FROM users 然后被执行从而删除所有的 user 记录!因此,你应该使用 PDO 限制参数来过滤 ID 输入。

<?php
$pdo = new PDO('sqlite:/path/db/users.db');
$stmt = $pdo->prepare('SELECT name FROM users WHERE id = :id');
$id = filter_input(INPUT_GET, 'id', FILTER_SANITIZE_NUMBER_INT); // <-- filter your data first (see [Data Filtering](#data_filtering)), especially important for INSERT, UPDATE, etc.
$stmt->bindParam(':id', $id, PDO::PARAM_INT); // <-- Automatically sanitized for SQL by PDO
$stmt->execute();

这是正确的代码。它在一条 PDO 语句中使用了一个限制参数。这将对外部 ID 输入在发送给数据库之前进行转义来防止潜在的 SQL 注入攻击。

对于写入操作,例如 INSERT 或者 UPDATE,进行数据过滤并对其他内容进行清理(去除 HTML 标签,Javascript 等等)是尤其重要的。PDO 只会为 SQL 进行清理,并不会为你的应用做任何处理。

你也应该知道数据库连接有时会耗尽全部资源,如果连接没有被隐式地关闭的话,有可能会造成可用资源枯竭的情况。不过这通常在其他语言中更为常见一些。使用 PDO 你可以通过销毁(destroy)对象,也就是将值设为 NULL,来隐式地关闭这些连接,确保所有剩余的引用对象的连接都被删除。如果你没有亲自做这件事情,PHP 会在你的脚本结束的时候自动为你完成 —— 除非你使用的是持久链接。

数据库交互

当开发者第一次接触 PHP 时,通常会使用类似下面的代码来将数据库的交互与表示层逻辑混在一起:

<ul>
<?php
foreach ($db->query('SELECT * FROM table') as $row) {
    echo "<li>".$row['field1']." - ".$row['field1']."</li>";
}
?>
</ul>

这从很多方面来看都是错误的做法,主要是由于它不易阅读又难以测试和调试。而且如果你不加以限制的话,它会输出非常多的字段。

其实还有许多不同的解决方案来完成这项工作 — 取决于你倾向于 面向对象编程(OOP)还是函数式编程 — 但必须有一些分离的元素。

来看一下最基本的做法:

<?php
function getAllFoos($db) {
    return $db->query('SELECT * FROM table');
}

foreach (getAllFoos($db) as $row) {
    echo "<li>".$row['field1']." - ".$row['field1']."</li>"; // BAD!!
}

这是一个不错的开头。将这两个元素放入了两个不同的文件于是你得到了一些干净的分离。

创建一个类来放置上面的函数,你就得到了一个「Model」。创建一个简单的.php文件来存放表示逻辑,你就得到了一个「View」。这已经很接近 MVC — 一个大多数框架常用的面向对象的架构。

foo.php

<?php
$db = new PDO('mysql:host=localhost;dbname=testdb;charset=utf8mb4', 'username', 'password');

// Make your model available
include 'models/FooModel.php';

// Create an instance
$fooModel = new FooModel($db);
// Get the list of Foos
$fooList = $fooModel->getAllFoos();

// Show the view
include 'views/foo-list.php';

models/FooModel.php

<?php
class FooModel
{
    protected $db;

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

    public function getAllFoos() {
        return $this->db->query('SELECT * FROM table');
    }
}

views/foo-list.php

<?php foreach ($fooList as $row): ?>
    <?= $row['field1'] ?> - <?= $row['field1'] ?>
<?php endforeach ?>

向大多数现代框架的做法学习是很有必要的,尽管多了一些手动的工作。你可以并不需要每一次都完全这么做,但将太多的表示逻辑层代码和数据库交互掺杂在一些将会为你在想要对程序进行单元测试时带来真正的麻烦。

PHPBridge 具有一项非常棒的资源叫做创建一个数据类。它包含了非常相似的逻辑而且非常适合刚刚习惯数据库交互概念的开发者使用。

数据库抽象层

许多框架都提供了自己的数据库抽象层,其中一些是设计在 PDO 的上层的。这些抽象层通常将你的请求在 PHP 方法中包装起来,通过模拟的方式来使你的数据库拥有一些之前不支持的功能。这种抽象是真正的数据库抽象,而不单单只是 PDO 提供的数据库连接抽象。这类抽象的确会增加一定程度的性能开销,但如果你正在设计的应用程序需要同时使用 MySQL,PostgreSQL 和 SQLite 时,一点点的额外性能开销对于代码整洁度的提高来说还是很值得的。

有一些抽象层使用的是PSR-0PSR-4 命名空间标准,所以他们可以安装在任何你需要的应用程序中。

回到顶部

使用模板

模板提供了一种简便的方式,将展现逻辑从控制器和业务逻辑中分离出来。

一般来说,模板包含应用程序的 HTML 代码,但也可以使用其他的格式,例如 XML 。

模板通常也被称为「视图」, 而它是 模型-视图-控制器 (MVC) 软件架构模式第二个元素的 一部份

好处

使用模板的主要好处是可以将呈现逻辑与应用程序的其他部分进行分离。模板的单一职责就是呈现格式化后的内容。它不负责数据的查询,保存或是其他复杂的任务。进一步促成了更干净、更具可读性的代码,在团队协作开发中尤其有用,开发者可以专注服务端的代码(控制器、模型),而设计师负责客户端代码 (网页) 。

模板同时也改善了前端代码的组织架构。一般来说,模板放置在「视图」文件夹中,每一个模板都放在独立的一个文件中。这种方式鼓励代码重用,它将大块的代码拆成较小的、可重用的片段,通常称为局部模板。举例来说,网站的头、尾区块可以各自定义为一个模板,之后将它们放在每一个页面模板的上、下位置。

最后,根据你选择的类库,模板可以通过自动转义用户的内容,从而带来更多的安全性。有些类库甚至提供沙箱机制,模板设计者只能使用在白名单中的变量和函数。

原生 PHP 模板

原生 PHP 模板就是指直接用 PHP 来写模板,这是很自然的选择,因为 PHP 本身其实是个模板语言。这代表你可以在其他的语言中结合 PHP 使用,比如 HTML 。这对 PHP 开发者相当有利,因为不需要额外学习新的语法,他们熟知可以使用的函数,并且使用的编辑器也已经内置了语法高亮和自动补全。此外,原生的 PHP 模板没有了编译阶段,速度会更快。

现今的 PHP 框架都会使用一些模板系统,这当中多数是使用原生的 PHP 语法。在框架之外,一些类库比如 PlatesAura.View,提供了现代化模板的常见功能,比如继承、布局、扩展,让原生的 PHP 模板更容易使用。

原生 PHP 模板的简单示例

使用 Plates 类库。

<?php // user_profile.php ?>

<?php $this->insert('header', ['title' => 'User Profile']) ?>

<h1>User Profile</h1>
<p>Hello, <?=$this->escape($name)?></p>

<?php $this->insert('footer') ?>

原生 PHP 模板使用继承的示例

使用 Plates 类库。

<?php // template.php ?>

<html>
<head>
    <title><?=$title?></title>
</head>
<body>

<main>
    <?=$this->section('content')?>
</main>

</body>
</html>
<?php // user_profile.php ?>

<?php $this->layout('template', ['title' => 'User Profile']) ?>

<h1>User Profile</h1>
<p>Hello, <?=$this->escape($name)?></p>

编译模板

尽管 PHP 不断升级为成熟的、面向对象的语言,但它作为模板语言 没有改善多少。编译模板,比如 Twig , BrainySmarty* ,提供了模板专用的新语法,填补了这片空白。从自动转义到继承以及简化控制结构,编译模板设计地更容易编写,可读性更高,同时使用上也更加的安全。编译模板甚至可以在不同的语言中使用,Mustache 就是一个很好的例子。由于这些模板需要编译,在性能上会带来一些轻微的影响,不过如果适当的使用缓存,影响就变得非常小了。

*虽然 Smarty 提供了自动转义的功能, 不过这个功能默认是关闭的

编译模板简单示例

使用 Twig 类库。

{% include 'header.html' with {'title': 'User Profile'} %}

<h1>User Profile</h1>
<p>Hello, {{ name }}</p>

{% include 'footer.html' %}

编译模板使用继承示例

使用 Twig 类库。

// template.html

<html>
<head>
    <title>{% block title %}{% endblock %}</title>
</head>
<body>

<main>
    {% block content %}{% endblock %}
</main>

</body>
</html>
// user_profile.html

{% extends "template.html" %}

{% block title %}User Profile{% endblock %}
{% block content %}
    <h1>User Profile</h1>
    <p>Hello, {{ name }}</p>
{% endblock %}

延伸阅读

文章与教程

类库

回到顶部

错误与异常

错误

在许多「重异常」(exception-heavy) 的编程语言中,一旦发生错误,就会抛出异常。这确实是一个可行的方式。不过 PHP 却是一个 「轻异常」(exception-light) 的语言。当然它确实有异常机制,在处理对象时,核心也开始采用这个机制来处理,只是 PHP 会尽可能的执行而无视发生的事情,除非是一个严重错误。

举例来说:

$ php -a
php > echo $foo;
Notice: Undefined variable: foo in php shell code on line 1

这里只是一个 notice 级别的错误,PHP 仍然会愉快的继续执行。这对有「重异常」编程经验的人来说会带来困惑,例如在 Python 中,引用一个不存在的变量会抛出异常:

$ python
>>> print foo
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'foo' is not defined

本质上的差异在于 Python 会对任何小错误进行抛错,因此开发人员可以确信任何潜在的问题或者边缘的案例都可以被捕捉到,与此同时 PHP 仍然会保持执行,除非极端的问题发生此时它将抛出错误并报告。

错误严重性

PHP 有几个错误严重性等级。三个最常见的的信息类型是错误(error)、通知(notice)和警告(warning)。它们有不同的严重性: E_ERRORE_NOTICEE_WARNING。错误是运行期间的严重问题,通常是因为代码出错而造成,必须要修正它,否则会使 PHP 停止执行。通知是建议性质的信息,是因为程序代码在执行期有可能造成问题,但程序不会停止。 警告是非致命错误,程序执行也不会因此而中止。

另一个在编译期间会报错的信息类型是 E_STRICT。这个信息用來建议修改程序代码以维持最佳的互通性并能与今后的 PHP 版本兼容。

更改 PHP 错误报告行为

错误报告可以由 PHP 配置及函数调用改变。使用 PHP 内置的函数 error_reporting(),可以设定程序执行期间的错误等级,方法是传入预定义的错误等级常量,意味着如果你只想看到警告和错误(而非通知),你可以这样设定:

<?php
error_reporting(E_ERROR | E_WARNING);

你也可以控制错误是否在屏幕上显示 (开发时比较有用)或隐藏后记录日志 (适用于正式环境)。如果想知道更多细节,可以查看 错误报告 章节。

行内错误抑制

你可以让 PHP 利用错误控制操作符 @ 来抑制特定的错误。将这个操作符放置在表达式之前,其后的任何错误都不会出现。

<?php
echo @$foo['bar'];

如果 $foo['bar'] 存在,程序会将结果输出,如果变量 $foo 或是 'bar' 键值不存在,则会返回 null 并且不输出任何东西。如果不使用错误控制操作符,这个表达式会产生一个错误信息 PHP Notice: Undefined variable: fooPHP Notice: Undefined index: bar

这看起来像是个好主意,不过也有一些讨厌的代价。PHP 处理使用 @ 的表达式比起不用时效率会低一些。过早的性能优化在所有程序语言中也许都是争论点,不过如果性能在你的应用程序 / 类库中占有重要地位,那么了解错误控制操作符的性能影响就比较重要。

其次,错误控制操作符会 完全 吃掉错误。不但没有显示,而且也不会记录在错误日志中。此外,在正式环境中 PHP 也没有办法关闭错误控制操作符。也许你认为那些错误时无害的,不过那些较具伤害性的错误同时也会被隐藏。

如果有方法可以避免错误抑制符,你应该考虑使用,举例来说,上面的程序代码可以这样重写:

<?php
    //Null Coalescing Operator
    echo $foo['bar'] ?? '';

fopen() 载入文件失败时,也许是一个使用错误抑制符的合理例子。你可以在尝试载入文件前检查是否存在,但是如果这个文件在检查后才被删除,而此时 fopen() 还未执行 (听起来有点不太可能,但是确实会发生),这时 fopen() 会返回 false 并且 抛出操作。这也许应该由 PHP 本身来解决,但这时一个错误抑制符才能有效解决的例子。

前面我们提到在正式的 PHP 环境中没有办法关闭错误控制操作符。但是 Xdebug 有一个 xdebug.scream 的 ini 配置项,可以关闭错误控制操作符。你可以按照下面的方式修改 php.ini

xdebug.scream = On

你也可以在执行期间通过 ini_set 函数来设置这个值:

<?php
ini_set('xdebug.scream', '1')

Scream」这个 PHP 扩展提供了和 xDebug 类似的功能,只是 Scream 的 ini 设置项叫做 scream.enabled

当你在调试代码而错误信息被隐藏时,这是最有用的方法。请务必小心使用 scream ,而是把它当作暂时性的调试工具。有许多的 PHP 函数类库代码也许无法在错误抑制操作符停用时正常使用。

错误异常类

PHP 可以完美化身为「重异常」的程序语言,只需要几行代码就能切换过去。基本上你可以利用 ErrorException 类抛出「错误」来当做「异常」,这个类是继承自 Exception 类。

这在大量的现代框架中是一个常见的做法,比如 Symfony 和 Laravel。如果开启调试模式,或者进入开发环境的话,这两个框架都会将显示美观清晰的 调用栈

还有一些软件包可用于更好的错误和异常处理和报告。Whoops!,它与Laravel的默认安装一起提供,也可以在任何框架中使用。

在开发过程中将错误当作异常抛出可以更好的处理它,如果在开发时发生异常,你可以将它包在一个 catch 语句中具体说明这种情况如何处理。每捕捉一个异常,都会使你的应用程序越来越健壮。

更多关于如何使用 ErrorException 来处理错误的细节,可以参考 ErrorException Class

异常

异常是许多流行编程语言的标配,但它们往往被 PHP 开发人员所忽视。像 Ruby 就是一个极度重视异常的语言,无论有什么错误发生,像是 HTTP 请求失败,或者数据库查询有问题,甚至找不到一个图片资源,Ruby (或是所使用的 gems),将会抛出异常,你可以通过屏幕立刻知道所发生的问题。

PHP 处理这个问题则比较随意,调用 file_get_contents() 函数通常只会给出 FALSE 值和警告。许多较早的 PHP 框架比如 CodeIgniter 只是返回 false,将信息写入专有的日志,或者让你使用类似 $this->upload->get_error() 的方法来查看错误原因。这里的问题在于你必须找出错误所在,并且通过翻阅文档来查看这个类使用了什么样的错误的方法,而不是明确的暴露错误。

另一个问题发生在当类自动抛出错误到屏幕时会结束程序。这样做会阻挡其他开发者动态处理错误的机会。应该抛出异常让开发人员意识到错误的存在,让他们可以选择处理的方式,例如:

<?php
$email = new Fuel\Email;
$email->subject('My Subject');
$email->body('How the heck are you?');
$email->to('guy@example.com', 'Some Guy');

try
{
    $email->send();
}
catch(Fuel\Email\ValidationFailedException $e)
{
    // 验证失败
}
catch(Fuel\Email\SendingFailedException $e)
{
    // 这个驱动无法发送 email
}
finally
{
    // 无论抛出什么样的异常都会执行,并且在正常程序继续之前执行
}

SPL 异常

原生的 Exception 类并没有提供太多的调试情境给开发人员,不过可以通过建立一个特殊的 Exception 来弥补它,方式就是建立一个继承自原生 Exception 类的一个子类:

<?php
class ValidationException extends Exception {}

如此一来,可以加入多个 catch 区块,并且根据不同的异常分别处理。通过这样可以建立 许多自定义异常,其中有些已经在 SPL 扩展 提供的 SPL 异常中定义了。

举例来说,如果你使用了 __call() 魔术方法去调用一个无效的方法,而不是抛出一个模糊的标准 Exception 或是建立自定义的异常处理,你可以直接抛出 throw new BadMethodCallException;

回到顶部

安全

我对PHP的安全性找到的最好的资源是2018年指南构建安全PHP软件

Web应用程序安全

每个PHP开发人员都必须学习Web应用程序安全性的基础知识,这些知识可以分解为一些广泛的主题:

  1. 代码数据分离。
    • 当数据作为代码执行时,您将获得SQL注入,跨站点脚本,本地/远程文件包含等。
    • 当代码作为数据打印时,您会收到信息泄漏(源代码泄露,或者在C程序的情况下,有足够的信息来绕过ASLR)。
  2. 应用逻辑。
    • 缺少身份验证或授权控制。
    • 输入验证。
  3. 操作环境。
    • PHP版本。
    • 第三方库。
    • 操作系统。
  4. 密码学的弱点。

有坏人准备并愿意利用您的Web应用程序。务必采取必要的预防措施来加强Web应用程序的安全性。幸运的是,开放Web应用程序安全项目(OWASP)的优秀 人员编制了一份已知安全问题和方法的完整列表,以保护自己免受攻击。对于注重安全的开发人员来说,这是必读的。生存深层: Padraic的PHP安全性 Brady也是另一个很好的PHP应用程序安全指南。

密码哈希

每个人在建构 PHP 应用时终究都会加入用户登录的模块。用户的帐号及密码会被储存在数据库中,在登录时用来验证用户。

在存储密码之前正确散列密码非常重要散列和加密是两个非常不同的事情 ,经常会混淆。

散列是一种不可逆转的单向函数。这会产生一个固定长度的字符串,无法逆转。这意味着您可以将散列与另一个散列进行比较,以确定它们是否来自相同的源字符串,但您无法确定原始字符串。如果没有对密码进行哈希处理,并且未经授权的第三方访问您的数据库,则所有用户帐户现在都会受到损害。

与散列不同,加密是可逆的(只要你有密钥)。加密在其他方面很有用,但是安全存储密码的策略很差。

密码应该单独被 加盐处理 ,加盐值指的是在哈希之前先加入随机子串。以此来防范「字典破解」或者「彩虹碰撞」(一个可以保存了通用哈希后的密码数据库,可用来逆向推出密码)。

哈希和加盐非常重要,因为很多情况下,用户会在不同的服务中选择使用同一个密码,密码的安全性很低。

此外,您应该使用专门的密码散列算法而不是快速的通用加密散列函数(例如SHA256)。可接受的密码哈希算法(截至2018年6月)的简短列表是:

  • Argon2(适用于PHP 7.2及更高版本)
  • Scrypt
  • Bcrypt(PHP为您提供了这个;见下文)
  • PBKDF2与HMAC-SHA256或HMAC-SHA512

值得庆幸的是,在 PHP 中这些很容易做到。

使用 password_hash 来哈希密码

password_hash 函数在 PHP 5.5 时被引入。 此函数现在使用的是目前 PHP 所支持的最强大的加密算法 BCrypt 。 当然,此函数未来会支持更多的加密算法。 password_compat 库的出现是为了提供对 PHP >= 5.3.7 版本的支持。

在下面例子中,我们哈希一个字符串,然后和新的哈希值对比。因为我们使用的两个字符串是不同的(’secret-password’ 与 ‘bad-password’),所以登录失败。

<?php
require 'password.php';

$passwordHash = password_hash('secret-password', PASSWORD_DEFAULT);

if (password_verify('bad-password', $passwordHash)) {
    // Correct Password
} else {
    // Wrong password
}

password_hash() 已经帮你处理好了加盐。加进去的随机子串通过加密算法自动保存着,成为哈希的一部分。password_verify() 会把随机子串从中提取,所以你不必使用另一个数据库来记录这些随机子串。

数据过滤

永远不要信任外部输入。请在使用外部输入前进行过滤和验证。filter_var()filter_input() 函数可以过滤文本并对格式进行校验(例如 email 地址)。

外部输入可以是任何东西:$_GET$_POST 等表单输入数据,$_SERVER 超全局变量中的某些值,还有通过 fopen('php://input', 'r') 得到的 HTTP 请求体。记住,外部输入的定义并不局限于用户通过表单提交的数据。上传和下载的文档,session 值,cookie 数据,还有来自第三方 web 服务的数据,这些都是外部输入。

虽然外部输入可以被存储、组合并在以后继续使用,但它依旧是外部输入。每次你处理、输出、连结或在代码中包含时,请提醒自己检查数据是否已经安全地完成了过滤。

数据可以根据不同的目的进行不同的 过滤 。比如,当原始的外部输入被传入到了 HTML 页面的输出当中,它可以在你的站点上执行 HTML 和 JavaScript 脚本!这属于跨站脚本攻击(XSS),是一种很有杀伤力的攻击方式。一种避免 XSS 攻击的方法是在输出到页面前对所有用户生成的数据进行清理,使用 strip_tags() 函数来去除 HTML 标签或者使用 htmlentities() 或是 htmlspecialchars() 函数来对特殊字符分别进行转义从而得到各自的 HTML 实体。

另一个例子是传入能够在命令行中执行的选项。这是非常危险的(同时也是一个不好的做法),但是你可以使用自带的 escapeshellarg() 函数来过滤执行命令的参数。

最后的一个例子是接受外部输入来从文件系统中加载文件。这可以通过将文件名修改为文件路径来进行利用。你需要过滤掉"/", "../", null 字符或者其他文件路径的字符来确保不会去加载隐藏、私有或者敏感的文件。

数据清理

数据清理是指删除(或转义)外部输入中的非法和不安全的字符。

例如,你需要在将外部输入包含在 HTML 中或者插入到原始的 SQL 请求之前对它进行过滤。当你使用 PDO 中的限制参数功能时,它会自动为你完成过滤的工作。

有些时候你可能需要允许一些安全的 HTML 标签输入进来并被包含在输出的 HTML 页面中,但这实现起来并不容易。尽管有一些像 HTML Purifier 的白名单类库为了这个原因而出现,实际上更多的人通过使用其他更加严格的格式限制方式例如使用 Markdown 或 BBCode 来避免出现问题。

查看 Sanitization Filters

反序列化 Unserialization

使用 unserialize() 从用户或者其他不可信的渠道中提取数据是非常危险的事情。这样做会触发恶意实例化对象(包含用户定义的属性),即使对象没用被使用,也会触发运行对象的析构函数。所以你应该避免从不可信渠道反序列化数据。

如果你必须这样做,请你使用 PHP 7 的 allowed_classes 选项来限制反序列化的对象类型。

有效性验证

验证是来确保外部输入的是你所想要的内容。比如,你也许需要在处理注册申请时验证 email 地址、手机号码或者年龄等信息的有效性。

查看 Validation Filters

配置文件

当你在为你的应用程序创建配置文件时,最好的选择时参照以下的做法:

  • 推荐你将你的配置信息存储在无法被直接读取和上传的位置上。
  • 如果你一定要存储配置文件在根目录下,那么请使用 .php 的扩展名来进行命名。这将可以确保即使脚本被直接访问到,它也不会被以明文的形式输出出来。
  • 配置文件中的信息需要被针对性的保护起来,对其进行加密或者设置访问权限。
  • 建议不要把敏感信息如密码或者 API 令牌放到版本控制器中。

注册全局变量

注意: 自 PHP 5.4.0 开始,register_globals 选项已经被移除并不再使用。这是在提醒你如果你正在升级旧的应用程序的话,你需要注意这一点。

register_globals 选项被开启时,它会使许多类型的变量(包括 $_POST, $_GET$_REQUEST)被注册为全局变量。这将很容易使你的程序无法有效地判断数据的来源并导致安全问题。

例如:$_GET['foo'] 可以通过 $foo 被访问到,也就是可以对未声明的变量进行覆盖。如果你使用低于 5.4.0 版本的 PHP 的话,请 确保 register_globals 是被设为 off 的。

错误报告

错误日志对于发现程序中的错误是非常有帮助的,但是有些时候它也会将应用程序的结构暴露给外部。为了有效的保护你的应用程序不受到由此而引发的问题。你需要将在你的服务器上使用开发和生产(线上)两套不同的配置。

开发环境

为了在 开发 环境中显示所有可能的错误,将你的 php.ini 进行如下配置:

display_errors = On
display_startup_errors = On
error_reporting = -1
log_errors = On

将值设为 -1 将会显示出所有的错误,甚至包括在未来的 PHP 版本中新增加的类型和参数。 和 PHP 5.4 起开始使用的 E_ALL 是相同的。- php.net

E_STRICT 类型的错误是在 5.3.0 中被引入的,并没有被包含在 E_ALL 中。然而从 5.4.0 开始,它被包含在了 E_ALL 中。这意味着什么?这表示如果你想要在 5.3 中显示所有的错误信息,你需要使用 -1 或者 E_ALL | E_STRICT

不同 PHP 版本下开启全部错误显示

  • < 5.3 -1E_ALL
  •   5.3 -1E_ALL | E_STRICT
  • > 5.3 -1E_ALL

生产环境

为了在 生产 环境中隐藏错误显示,将你的 php.ini 进行如下配置:

display_errors = Off
display_startup_errors = Off
error_reporting = E_ALL
log_errors = On

当在生产环境中使用这个配置时,错误信息依旧会被照常存储在 web 服务器的错误日志中,唯一不同的是将不再显示给用户。更多关于设置的信息,请参考 PHP 手册:

测试

为你的 PHP 程序编写自动化测试被认为是最佳实践,可以帮助你建立良好的应用程序。 自动化测试是非常棒的工具,它能确保你的应用程序在改变或增加新的功能时不会影响现有的功能,不应该忽视。

PHP 有一些不同种类的测试工具 (或框架) 可以使用,它们使用不同的方法 - 但他们都试图避免手动测试和大型 QA 团队的需求,确保最近的变更不会破坏既有功能。

测试驱动开发

Wikipedia 上的定义:

测试驱动开发 (TDD) 是一种以非常短的开发周期不断迭代的软件开发过程:首先开发者对将要实现的功能或者新的方法写一个失败的自动化测试用例,然后就去写代码来通过这个测试用例,最终通过重构代码让一其达到可接受的水准。Kent Beck, 这个技术创造者或者说重新发现者,在2003年声明TDD 鼓励简单的设计和激励信心。

目前你可以应用的几种不同类型的测试:

单元测试

单元测试是一种编程方法来确认函数,类和方法以我们预期的方式来工作,单元测试会贯穿整个项目的开发周期。通过检查各个函数和方法的输入输出,你就可以保证内部的逻辑已经正确执行。通过使用依赖注入和编写”mock” 类以及 stubs 来确认依赖被正确的使用,提高测试覆盖率。

当你创建一个类或者一个函数,你应该为它们的每一个行为创建一个单元测试。至少你应该确认当你输入一个错误参数会触发一个错误,你输入一个有效的参数会得到正确的结果。这会帮助你在开发周期后段对类或者函数做出修改后,确认已有的功能任然可以正常的工作。可替代的方法是在源码中使用 var_dump() ,但这种方法却不能去构建一个或大或小的应用。

单元测试的其他用处是在给开源项目贡献代码时。如果你写了一个测试证明代码有bug,然后修复它,并且展示测试的过程,这样补丁将会更容易被接受。如果你在维护一个项目,在处理 pull request 的时候可以将单元测试作为一个要求。

PHPUnit 是业界PHP应用开发单元测试框架的标准,但也有其他可选的框架:

集成测试

Wikipedia 上的定义:

集成测试 (有时候称为集成和测试,缩写为 I&T)是把各个模块组合在一起进行整体测试的软件测试阶段。它处于单元测试之后,验收测试之前。集成测试将已经经过了单元测试的模块做为输入模块,组合成一个整体,然后运行集成测试用例,然后输出一个可以进行系统测试的系统。

许多可用于单元测试的相同工具可用于集成测试,因为使用了许多相同的原理。

功能性测试

有时候也被称之为验收测试,功能测试是通过使用工具来生成自动化的测试用例,然后在真实的系统上运行。而不是单元测试中简单的验证单个模块的正确性和集成测试中验证各个模块间交互的正确性。这些工具会使用代表性的真实数据来模拟真实用户的行为来验证系统的正确性。

功能测试的工具

  • Selenium
  • Mink
  • Codeception 是一个全栈的测试框架包括验收性测试工具。
  • Storyplayer 是一个全栈的测试框架并且支持随时创建和销毁测试环境。

行为驱动开发

有两种不同的行为驱动开发 (BDD): SpecBDD 和 StoryBDD。 SpecBDD 专注于代码的技术行为,而 StoryBDD 专注于业务逻辑或功能的行为和互动。这两种 BDD 都有对应的 PHP 框架。

采用 StoryBDD 时, 你编写可读的故事来描述应用程序的行为。接着这些故事可以作为应用程序的实际测试案例执行。Behat 是使用在 PHP 应用程序中的 StoryBDD框架,它受到 Ruby 的 Cucumber 项目的启发并且实现了 Gherkin DSL 來描述功能的行为。

采用 SpecBDD 时, 你编写规格来描述实际的代码应该有什么行为。你应该描述函数或者方法应该有什么行为,而不是测试函数或者方法。PHP 提供了 PHPSpec 框架来达到这个目的,这个框架受到了 Ruby 的 RSpec project 项目的启发。

BDD 链接

  • Behat, PHP 的 StoryBDD 框架, 受到了 Ruby’s Cucumber 项目的启发。
  • PHPSpec, PHP 的 SpecBDD 框架, 受到了 Ruby’s RSpec 项目的启发。
  • Codeception 是一个使用 BDD 准则的全栈测试框架。

其他测试工具

除了个别的测试驱动和行为驱动框架之外,还有一些通用的框架和辅助函数类库,对任何的测试方法都很有用。

工具地址

服务器与部署

可以通过多种方式在生产Web服务器上部署和运行PHP应用程序。

平台即服务Platform as a Service (PaaS)

PaaS 提供了运行 PHP 应用程序所必须的系统环境和网络架构。这就意味着只需做少量配置就可以运行 PHP 应用程序或者 PHP 框架。

现在,PaaS 已经成为一种部署、托管和扩展各种规模的 PHP 应用程序的流行方式。你可以在 资源部分 查看 PHP PaaS “Platform as a Service” 提供商 列表。

虚拟或专用服务器

如果你喜欢系统管理员的工作,或者对这方面感兴趣,虚拟或者专用服务器可以让你完全控制自己的生产环境。

nginx 和 PHP-FPM

PHP 通过内置的 FastCGI 进程管理器(FPM),可以很好的与轻量级的高性能 web 服务器 nginx 协作使用。nginx 比 Apache 占用更少内存而且可以更好的处理并发请求,这对于并没有太多内存的虚拟服务器尤其重要。

Apache 和 PHP

PHP 和 Apache 有很长的合作历史。Apache 有很强的可配置性和大量的 扩展模块 。是共享主机中常见的Web服务器,完美支持各种 PHP 框架和开源应用(如 WordPress )。可惜的是,默认情况下,Apache 会比 nginx 消耗更多的资源,而且并发处理能力不强。

Apache 有多种方式运行 PHP,最常见的方式就是使用 mode_php5 的 prefork MPM 方式。但是它对内存的利用效率并不高,如果你不想深入服务器管理方面学习,那么这种简单的方式可能是你最好的选择。需要注意的事如果你使用 mod_php5,就必须使用 prefork MPM。

如果你追求高性能和高稳定性,可以为 Apache 选择与 nginx 类似的的 FPM 系统 worker MPM 或者 event MPM,它们分别使用 mod_fastcgi 和 mod_fcgid。这种方式可以更高效的利用内存,运行速度也更快,但是配置也相对复杂一些。

共享主机

PHP 非常流行,很少有服务器没有安装 PHP 的,因而有很多共享主机,不过需要注意服务器上的 PHP 是否是最新稳定 版本。共享主机允许多个开发者把自己的网站部署在上面,这样的好处是费用非常便宜,坏处是你不知道将和哪些 网站共享主机,因此需要仔细考虑机器负载和安全问题。如果项目预算允许的话,避免使用共享主机是上策。

为确保你的共享主机使用了最新的 PHP 版本,请查看:共享主机 PHP 版本使用.

构建及部署应用

如果你在手动的进行数据库结构的修改或者在更新文件前手动运行测试,请三思而后行!因为随着每一个额外的手动任务的添加都需要去部署一个新的版本到应用程序,这些更改会增加程序潜在的致命错误。即使你是在处理一个简单的更新,全面的构建处理或者持续集成策略,构建自动化绝对是你的朋友。

你可能想要自动化的任务有:

  • 依赖管理
  • 静态资源编译、压缩
  • 执行测试
  • 文档生成
  • 打包
  • 部署

构建自动化工具

构建工具可以认为是一系列的脚本来完成应用部署的通用任务。构建工具并不属于应用的一部分,它独立于应用层 ‘之外’。

现在已有很多开源的工具来帮助你完成构建自动化,一些是用 PHP 编写,有一些不是。应该根据你的实际项目来选择最适合的工具,不要让语言阻碍了你使用这些工具,如下有一些例子:

Phing可以在XML构建文件中控制您的打包,部署或测试过程。Phing(基于Apache Ant)提供了安装或更新Web应用程序通常所需的丰富任务,并且可以使用PHP编写的其他自定义任务进行扩展。它是一个坚固而强大的工具并且已经存在了很长时间,但是由于它处理配置(XML文件)的方式,该工具可能被认为有点老式。

Capistrano 是一个为 中高级程序员 准备的系统,以一种结构化、可复用的方式在一台或多台远程机器上执行命令。对于部署 Ruby on Rails 的应用,它提供了预定义的配置,不过也可以用它来 部署 PHP 应用 。如果要成功的使用 Capistrano ,需要一定的 Ruby 和 Rake 的知识。

Ansistrano是一个Ansible角色,可以轻松管理脚本应用程序(如PHP,Python和Ruby)的部署过程(部署和回滚)。这是Capistrano的Ansible港口它已经被很多PHP公司使用过了。

Rocketeer 从 Laravel 框架中得到了很多灵感。 目标是默认智能化配置、高速、优雅的自动化部署工具。他支持多服务器,多阶段,并行部署等功能。工具的扩展性极强,并且是由 PHP 编写。

Deployer 是一个用 PHP 编写的部署工具,它很简单且实用。并行执行任务,原子化部署,在多台服务器之间保持一致性。为 Symfony、Laravel、Zend Framework 和 Yii 提供了通用的任务脚本。推荐阅读 Younes Rafie 的博文 快速使用 Deployer 部署 PHP 应用

Magallanes 是另一个由 PHP 编写的自动化部署工具。使用 YAML 作为配置信息,支持多服务器和多环境,自动化部署。并且自带了许多通用的任务。

延伸阅读:

服务器配置

面对许多服务器时,管理和配置服务器可能是一项艰巨的任务。有一些工具可以解决这个问题,因此您可以自动化您的基础架构,以确保您拥有正确的服务器并且它们已正确配置。它们通常与更大的云托管提供商(Amazon Web Services,Heroku,DigitalOcean等)集成以管理实例,这使得扩展应用程序变得更加容易。

Ansible是一个通过YAML文件管理您的基础架构的工具。它很容易上手,可以管理复杂的大规模应用程序。有一个用于管理云实例的API,它可以使用某些工具通过动态库存管理它们。

Puppet 拥有自定义语言和文件类型来管理服务和配置信息。支持 主从结构或者是 无主结构。在主从结构中,从属机器会在设定周期内更新主机上的配置信息。在无主架构中,你需要 push 推送修改到各个节点。

Chef 不仅仅只是一个部署框架, 它是一个基于 Ruby 的强大的系统集成框架,除了部署你的应用之外,还可以构建整个服务环境或者虚拟机。AWS 提供一个服务叫 OpsWorks,其集成了 Chef。

延伸阅读:

持续集成

持续集成是一种软件开发实践,团队成员经常整合他们的工作,通常每个人至少每天集成一次 - 每天导致多个集成。许多团队发现这种方法可以显着减少集成问题,并允许团队更快地开发内聚软件。

– Martin Fowler

对于 PHP 来说,有许多的方式来实现持续集成。近来 Travis CI 在持续集成上做的很棒,对于小项目来说也可以很好的使用。Travis CI 是一个托管的持续集成服务用于开源社区。它可以和 Github 很好的集成,并且提供了很多语言的支持包括 PHP 。

延伸阅读:

虚拟化

在开发和生产中的不同环境中运行应用程序可能会导致在您上线时出现奇怪的错误。使用与开发人员团队合作时使用的所有库的相同版本,使不同的开发环境保持最新也很棘手。

如果您在Windows上进行开发并部署到Linux(或任何非Windows)或正在团​​队中进行开发,则应考虑使用虚拟机。这听起来很棘手,但除了广为人知的虚拟化环境(如VMware或VirtualBox)之外,还有其他工具可以帮助您通过几个简单的步骤设置虚拟环境。

Vagrant 简介

Vagrant可帮助您在已知虚拟环境之上构建虚拟机,并将基于单个配置文件配置这些环境。这些box可以手动设置,也可以使用“配置”软件(如PuppetChef)为您执行此操作。配置基本box是确保以相同方式设置多个box的好方法,并且无需维护复杂的“设置set up”命令列表。您还可以“销毁”您的基本box并重新创建它而无需许多手动步骤,从而可以轻松创建“全新”安装。

Vagrant 还可以在虚拟机和主机上分享文件夹, 意味着你可以在主机里面编辑代码, 然后在虚拟机里面运行.

需要更多的帮助?

下面是一些其他的软件, 可以帮助你更好的使用 Vagrant:

  • Rove: 使用 Chef 自动化安装一些常用的软件, PHP 包含在内.
  • Puphpet:用于为PHP开发设置虚拟机的简单GUI。非常专注于PHP除本地VM外,它还可用于部署到云服务。使用Puppet进行配置。
  • Protobox: 是一个基于 vagrant 的一个层, 还有 Web 图形界面, 允许你使用一个 YAML 文件来安装和配置虚拟机里面的软件.
  • Phansible: 提供了一个简单的 Web 图形界面, 用来创建 Ansible 自动化部署脚本, 专门为 PHP 项目定制.

Docker 简介

Docker - 一个完整虚拟机的轻量级替代品 - 之所以这么称呼,因为它完全是关于“容器”的。容器是构建块,在最简单的情况下,它执行一个特定的工作,例如运行Web服务器。“image”是用于构建容器的包 - Docker有一个存储库,其中包含完整的容器。

典型的LAMP应用程序可能有三个容器:Web服务器,PHP-FPM进程和MySQL。与Vagrant中的共享文件夹一样,您可以将应用程序文件保留在原始位置,并告诉Docker在哪里找到它们。

你可以通过命令行来生成容器(请参阅下面的示例),或者,为了更好的维护,为你的项目构建一个 docker-compose.yml 文件来配置生成容器的规则和容器的通讯规则。

如果您正在开发多个网站,并希望在自己的虚拟机上安装每个网站时能够分离,但没有必要用磁盘空间或时间来使一切保持最新,Docker 可能会提供帮助。它效率高:安装和下载速度更快,只需存储每个映像的一个副本(无论它使用频率如何),容器所需的 RAM 更少,共享相同的操作系统内核,因此您可以同时运行更多服务器,并且只需几秒钟停止并启动它们,无需等待完整的服务器启动。

例子: 在 Docker 里面运行 PHP 应用

在你成功 安装 Docker 后, 可以使用一个命令启动Web服务器

下面的命令, 会下载一个功能齐全的 Apache 和 最新版本的 PHP, 并会设置 WEB 目录 /path/to/your/php/files 运行在 http://localhost:8080:

docker run -d --name my-php-webserver -p 8080:80 -v /path/to/your/php/files:/var/www/html/ php:apache

这将初始化并启动容器。-d使它在后台运行。要停止并启动它,只需运行docker stop my-php-webserverdocker start my-php-webserver(不再需要其他参数)。

了解更多关于 Docker 的信息

以上的命令教你快速的创建简单的服务器。Docker 还提供了很多功能等着你去发现。Docker Hub) 上更是提供了数以千计的已经构建好的镜像。花点时间去学习下 Docker 用户手册。并且不要运行您下载的随机代码而不检查它是否安全,非官方的镜像有时候并没有最新安全更新,如果你对镜像有疑问,优先选择 官方的仓库

PHPDocker.io 是另一个很棒的 PHP 相关的 Docker 资源站点。支持自动生成一个全栈的 LAMP/LEMP 服务器,包含你自定义的 PHP 版本和扩展。

缓存

PHP 本身来说是非常快的,但是但你当发起远程连接、加载文件等操作时也会遇到瓶颈。 幸运的是,有各种各样的工具可以用来加速你应用程序某些耗时的部分,或者说减少某些耗时任务所需要运行的次数。

Opcode 缓存

执行PHP文件时,必须先将其编译为操作码(CPU的机器语言指令)。如果源代码未更改,则操作码将相同,因此此编译步骤会浪费CPU资源。

操作码缓存通过将操作码存储在内存中并在连续调用中重用它们来防止冗余编译。如果有任何更改,它通常会首先检查文件的签名或修改时间。

操作码缓存可能会显着提高您的应用程序的速度。PHP 5.5 中自带了 opcode 缓存工具,叫做Zend OPcache,默认一般是开启的,请在 phpinfo() 输出中检查 opcache.enable 关键词是否出现来确定是否开启。早期的版本也能通过 PECL 扩展来安装。

更多关于 opcode 缓存的资料:

对象缓存

有时缓存代码中的单个对象会很有用,比如有些需要很大开销获取的数据或者一些结果集不怎么变化的数据库查询。你可以使用一些缓存软件将这些数据存放在内存中以便下次高速获取。如果你获得数据后把他们存起来,下次请求直接从缓存里面获取数据,在减少数据库负载的同时能极大提高性能。

许多流行的字节码缓存方案也能缓存定制化的数据,所以更有理由好好使用它们了。APCu、XCache 以及 WinCache 都提供了 API,以便你将数据缓存到内存中

最常用的内存对象缓存系统是 APCu 和 Memcached 。APCu 对于对象缓存来说是个很好的选择,它提供了简单的 API 让你能将数据缓存到内存,并且很容易设置和使用。APCu 的局限性表现在它依赖于所在的服务器。另一方面,Memcached 以独立的服务的形式安装,可以通过网络交互,这意味着你能将数据集中存在一个高速存取的地方,而且许多不同的系统能从中获取数据。

值得注意的是当你以 CGI(FastCGI) 的形式使用 PHP 时,每个进程将会有各自的缓存,比如说,APCu 缓存数据无法在多个工作进程中共享。在这种情况下,你可能得考虑 Memcached 了,由于它独立于 PHP 进程。

通常 APCu 在存取速度上比 Memcached 更快,但是 Memcached 在扩展上更有优势。如果你不希望应用程序涉及多个服务器,或者不需要 Memcached 提供的其他特性,那么 APCu 可能是最好的选择。

使用 APCu 的例子:

<?php
// check if there is data saved as 'expensive_data' in cache
$data = apc_fetch('expensive_data');
if ($data === false) {
    // data is not in cache; save result of expensive call for later use
    apc_add('expensive_data', $data = get_expensive_data());
}

print_r($data);

注意在 PHP 5.5 之前,APC 同时提供了对象缓存与字节码缓存。APCu 是为了将 APC 的对象缓存移植到 PHP 5.5+ 的一个项目,因为现在 PHP 有了内建的字节码缓存方案 (OPcache)。

更多关于缓存系统的项目:

代码注释

PHPDoc

PHPDoc 是注释 PHP 代码的非正式标准。它有许多不同的标记可以使用。完整的标记列表和范例可以查看 PHPDoc 指南

如下是撰写类方法时的一种写法:

<?php
/**
 * @author A Name <a.name@example.com>
 * @link http://www.phpdoc.org/docs/latest/index.html
 */
class DateTimeHelper
{
    /**
     * @param mixed $anything Anything that we can convert to a \DateTime object
     *
     * @throws \InvalidArgumentException
     *
     * @return \DateTime
     */
    public function dateTimeFromAnything($anything)
    {
        $type = gettype($anything);

        switch ($type) {
            // Some code that tries to return a \DateTime object
        }

        throw new \InvalidArgumentException(
            "Failed Converting param of type '{$type}' to DateTime object"
        );
    }

    /**
     * @param mixed $date Anything that we can convert to a \DateTime object
     *
     * @return void
     */
    public function printISO8601Date($date)
    {
        echo $this->dateTimeFromAnything($date)->format('c');
    }

    /**
     * @param mixed $date Anything that we can convert to a \DateTime object
     */
    public function printRFC2822Date($date)
    {
        echo $this->dateTimeFromAnything($date)->format('r');
    }
}

这个类的说明使用了 @author@link标记, @author 标记是用來说明代码的作者,在多位开发者的情况下,可以同时列出好几位。其次 @link 标记用来提供网站链接,进一步说明代码和网站之间的关系。

在这个类中,第一个方法的 @param 标记,说明类型、名字和传入方法的参数。此外,@return@throws 标记说明返回类型以及可能抛出的异常。

第二、第三个方法非常类似,和第一个方法一样使用一个 @param 标记。第二、和第三个方法之间关键差別在注释区块使用/排除 @return 标记。@return void 标记明确告诉我们没有返回值,而过去省略 @return void 声明也具有相同效果(沒有返回任何值)。

资源

PHP 官方

值得关注的大牛

刚进入社区时很难一下子找到很多有趣或者经验丰富的 PHP 社区成员,你可以在以下链接中找到 PHP 社区成员的 Twitter:

中国PHP大牛:

指导

PHP 的 Paas 提供商

要查看这些PaaS主机正在运行的版本,请转到PHP版本

框架

许多的 PHP 开发者都使用框架,而不是重新造轮子来构建 Web 应用。框架抽象了许多底层常用的逻辑,并提供了有益又简便的方法來完成常见的任务。

你并不一定要在每个项目中都使用框架。有时候原生的 PHP 才是正确的选择,但如果你需要一个框架,那么有如下三种主要类型:

  • 微型框架
  • 全栈框架
  • 组件框架

微型框架基本上是一个封装的路由,用来转发 HTTP 请求至一个闭包,控制器,或方法等等,尽可能地加快开发的速度,有时还会使用一些类库来帮助开发,例如一个基本的数据库封装等等。他們用来构建 HTTP 的服务卓有成效。

许多的框架会在微型框架上加入相当多的功能,我们则称之为全栈框架。这些框架通常会提供 ORMs ,身份认证扩展包等等。

基于组件的框架是专用和单用途库的集合。可以将不同的基于组件的框架一起用于构建微堆栈或完整堆栈框架。

组件

如上所述,“组件”是创建,分发和实现共享代码的共同目标的另一种方法。存在各种组件存储库,其中主要的两个是:

这两个存储库都具有与之关联的命令行工具,以帮助安装和升级过程,并在“ 依赖关系管理”部分中进行了更详细的说明

此外,还有基于组件的构成的框架和提供商提供不包含框架的组件。这些项目通常和其他的组件或者特定的框架没有依赖关系。

例如,您可以使用FuelPHP Validation包,而无需使用FuelPHP框架本身。

Laravel的Illuminate组件将更好地与Laravel框架分离。目前,上面仅列出了与Laravel框架最佳分离的组件。

Laravel 中文资料:

程序包目录结构

从为网络编写程序开始的人中常见的问题是,“我在哪里放置我的东西?”多年来,这个答案一直是“在哪里DocumentRoot。”虽然这个答案不完整,但它是个好地方开始。

出于安全原因,站点访问者不应访问配置文件; 因此,公共脚本保存在公共目录中,私有配置和数据保存在该目录之外。

对于每个团队,CMS或框架工作,每个实体使用标准目录结构。但是,如果一个人单独启动项目,那么知道要使用哪个文件系统结构可能会令人生畏。

Paul M. Jones对PHP领域数以万计的github项目的常见实践做了一些很棒的研究。基于这项研究编译了标准文件和目录结构,即标准PHP包骨架在这个目录结构中,DocumentRoot应该指向public/,单元测试应该在tests/目录中,并且由composer安装的第三方库属于该vendor/目录。对于其他文件和目录,遵守标准PHP包Skeleton将对项目的贡献者最有意义。

本标准描述了一个标准的文件系统架构适合所有PHP包。

一个PHP包通常包含以下目录结构

  • bin/
  • config/
  • docs/
  • public/
  • resources/
  • src/
  • tests/

一个PHP包通常包含以下文件

  • CHANGELOG(.*)
  • CONTRIBUTING(.*)
  • LICENSE(.*)
  • README(.*)

其它有用的资源

Cheatsheets

更多最佳实践

围绕PHP和Web开发社区的新闻

你可以通过订阅周刊资讯来获取关于扩展包推荐、最新消息、特殊事件或者是社区公告,还有不定时发布的资源:

您可能感兴趣的其他平台上也有Weeklies; 是一些清单

PHP 世界

视频教程

Youtube 视频

付费视频

书籍

有很多PHP书籍; 可悲的是,有些现在已经很老了,不再准确。特别是,避免使用“PHP 6”上的书籍,这个版本现在永远不会存在。5.6之后PHP的下一个主要版本是“PHP 7”,部分是因为这个原因

本节旨在成为有关PHP开发的推荐书籍的活文档。如果您希望添加您的图书,请发送PR,我们会检查相关性。

免费书籍

付费书籍

  • 构建你不会讨厌的API - 每个人和他们的狗都想要一个API,所以你应该学习如何构建它们。
  • 现代PHP - 涵盖现代PHP功能,最佳实践,测试,调优,部署和设置开发环境。
  • 构建安全的PHP应用程序 - 了解高级开发人员通常在多年经验中获得的安全基础知识,所有这些都集成在一个快速简便的手册中
  • 在PHP中实现旧版应用程序的现代化 - 通过一系列小的特定步骤来控制您的代码
  • 保护PHP:核心概念 - 一些最常见的安全术语的指南,并在每天PHP中提供它们的一些示例
  • 扩展PHP - 停止播放系统管理员并重新开始编码
  • 信令PHP - PCNLT信号在编写从命令行运行的PHP脚本时非常有用。
  • 最低可行性测试 - 长期PHP测试传播者克里斯·哈特斯(Chris Hartjes)重温了他认为开始时需要知道的最低限度。
  • PHP中的域驱动设计 - 查看用PHP编写的实例,展示领域驱动设计架构风格(六边形架构,CQRS或事件源),战术设计模式和有界上下文集成。

社区

PHP 社区多元化并且规模庞大,成员们也乐意并随时准备好帮助新人。你可以考虑加入当地的 PHP 使用者社区 (PUG) 或者参加教大型的 PHP 会议,从中学习更多最佳实践。你也可以使用 IRC 逛逛 irc.freenode.com 上的 #phpc 频道,也可以关注 @phpc 的Twitter 账号。试着去多结交一些新的开发者,学习新的东西,总之,交一些新朋友!其他的社区资源包含 StackOverflow

阅读 PHP 官方事件日历

PHP 用户组

如果你住在一个更大的城市,可能是附近有一个PHP用户组。您可以在PHP.ug轻松找到您当地的PUG 替代来源可能是Meetup.com或搜索php user group near me 使用您最喜欢的搜索引擎(即谷歌)。如果你住在一个较小的城镇,可能没有当地的PUG; 如果是这样的话,那就开始吧!

这里要特别提到两个全球的用户组:NomadPHPPHPWomenNomadPHP 提供每月两次的在线用户组会议,由 PHP 社区里顶尖的高手进行演讲。PHPWomen 原本是针对女性 PHP 开发者的非排他性的用户组。会员资格发放给那些支持多元化社区的人。PHPWomen 提供了一技术支持,指导和教育的个平台,并且促进了女性的创造力以及专业的氛围。

了解关于 PHP Wiki 上的用户群

PHP 会议

世界各地的 PHP 社区也会举办一些较大型的区域性或国际性的会议,一些知名的社区成员通常会在这些大型活动中现身演讲,这是一个直接和业内领袖学习的好机会。

查找 PHP 会议

ElePHPants

ElePHPant 是PHP项目设计中有大象的漂亮吉祥物. 它最初是为1998的PHP项目设计的,由 文森特·庞蒂埃(VincentPontier)设计, - 他是世界各地数千名大象的精神之父,10年后,可爱的毛绒大象玩具也诞生了。现在许多PHP会议上都有电子PHPants,还有许多PHP开发人员在他们的计算机上寻找乐趣和灵感.

访问文森特·庞蒂埃