欢迎
目前网络上充斥着大量的过时信息,让 PHP 新手误入歧途,并且传播着错误的实践以及不安全的代码。PHP 之道 收集了现有的 PHP 最佳实践、编码规范和权威学习指南,方便 PHP 开发者阅读和查找。
使用 PHP 沒有规范化的方式。本文档主要是向 PHP 新手介绍一些他们没有发现或者是太晚发现的主题, 或是经验丰富的专业人士已经实践已久的做法提供一些新想法。本文档也不会告诉您应该使用什么样的工具,而是提供多种选择的建议,并尽可能地说明方法及用法上的差异。
这是一个动态文档,将继续更新,提供更多有用的信息和示例。
入门
使用当前稳定版本 (8.1)
如果您开始使用 PHP,请从当前稳定的 PHP 8.1 版本开始。PHP 8.x 在旧的 7.x 和 5.x 版本上增加了许多新特性。该引擎已在很大程度上重新编写,PHP 现在比旧版本更快。PHP 8 是该语言的一次重大更新,包含许多新特性和优化。
您应该尝试快速升级到最新的稳定版本 - PHP 5.6 已经结束生命周期。升级很容易,因为没有太多的向下不兼容。如果您不确定某个函数或特性在哪个版本中,您可以查看 php.net 网站上的 PHP 文档。
内置 Web 服务器
使用 PHP 5.4 或更高版本,您无需安装和配置成熟的 Web 服务器即可开始学习 PHP。要启动服务器,请从项目 Web 根目录中的终端运行以下命令:
> php -S localhost:8000
- 了解内置的命令行 Web 服务器
Mac 安装
macOS 预装了 PHP,但它通常比最新的稳定版本低一些。有多种方法可以在 macOS 上安装最新的 PHP 版本。
通过 Homebrew 安装 PHP
Homebrew 是 macOS 的包管理器,可帮助您轻松安装 PHP 和各种扩展。Homebrew 核心存储库为 PHP 5.6、7.0、7.1、7.2、7.3、7.4、8.0、8.1 。使用以下命令安装最新版本:
您可以通过修改brew install php@8.1
PATH变量在 Homebrew PHP 版本之间切换。或者,您可以使用 brew-php-switcher 自动切换 PHP 版本。
您还可以通过取消链接和链接想要的版本来手动切换 PHP 版本: ```shell8.0
brew unlink php brew link —overwrite php@8.0
8.1
brew unlink php brew link —overwrite php@8.1
**通过 Macports 安装 PHP**<br />[MacPorts](https://www.macports.org/install.php) 项目是一个开源社区计划,旨在设计一个易于使用的系统,用于在 OS X 操作系统上编译、安装和升级基于命令行、X11 或 Aqua 的开源软件。<br />MacPorts 支持预编译的二进制文件,因此您无需每次都从源代码重新编译每个依赖项,如果您的系统上没有安装任何软件包,它可以节省您很多时间。<br />此时,您可以安装 php54, php55, php56, php70, php71, php72, php73, php74, php80, php81,使用`port install` 命令,例如:```shellsudo port install php74sudo port install php80
您可以运行 select 命令来切换您的 PHP 版本:
sudo port select --set php php81
通过 phpbrew 安装 PHP
phpbrew 是一个安装和管理多个 PHP 版本的工具。如果两个不同的应用程序/项目需要不同版本的 PHP,并且您没有使用虚拟机,这将非常有用。
通过 Liip 的二进制安装程序安装 PHP
另一个流行的选项是 php-osx.liip.ch,它为 5.3 到 7.3 版本提供了一个单独安装方法。它不会覆盖 Apple 安装的 PHP 二进制文件,而是将所有内容安装在单独的位置 (/usr/local/php5)。
从源代码编译
另一个让您可以控制所安装 PHP 版本的选项是自行编译。在这种情况下,请务必安装 Xcode 或 Apple 的替代 “XCode 命令行工具”,可从 Apple 的 Mac 开发人员中心下载。
集成包安装程序
上面列出的解决方案主要处理 PHP 本身,不提供 Apache、Nginx 或 SQL 服务器之类的东西。MAMP 和 XAMPP 等“集成包”解决方案将为您安装这些其他软件,并将它们捆绑在一起,但易于设置会带来灵活性的折衷。
Windows 安装
您可以从 windows.php.net/download 下载二进制文件。解压 PHP 后,建议将 PATH 设置为 PHP 文件夹的根目录(php.exe 所在的位置),以便您可以从任何地方执行 PHP。
对于学习和本地开发,您可以使用 PHP 5.4+ 的内置 Web 服务器,因此您无需担心配置它。如果您想要一个“集成包”,其中还包括一个成熟的 Web 服务器和 MySQL,那么 Web Platform Installer、XAMPP、EasyPHP、OpenServer、WAMP 等工具将有助于快速启动和运行 Windows 开发环境。也就是说,这些工具与生产环境略有不同,因此如果您在 Windows 上工作并部署到 Linux,请注意环境差异。
如果您需要在 Windows 上运行您的生产系统,那么 IIS7 将为您提供最稳定和最佳的性能。您可以使用phpmanager(IIS7 的 GUI 插件)来简化 PHP 的配置和管理。IIS7 内置了 FastCGI 并准备就绪,您只需将 PHP 配置为处理程序。如需支持和其他资源,iis.net 上有专门的 PHP 区域。
通常在开发和生产的不同环境中运行您的应用程序可能会导致在您上线时弹出奇怪的错误。如果您在 Windows 上开发并部署到 Linux(或任何非 Windows),那么您应该考虑使用虚拟机。
Chris Tankersley 有一篇非常有用的博客文章,介绍了他使用哪些工具来使用 Windows 进行 PHP 开发。
通用目录结构
那些开始为网络编写程序的人的一个常见问题是,“我把我的东西放在哪里?” 多年来,这个答案一直是“DocumentRoot 在哪里”。虽然这个答案并不完整,但它是一个很好的起点。
出于安全原因,站点的访问者不应访问配置文件;因此,公共脚本保存在公共目录中,私有配置和数据保存在该目录之外。
对于每个工作的团队、CMS 或框架,每个实体都使用标准目录结构。但是,如果一个人单独开始一个项目,那么知道使用哪种文件系统结构可能会令人生畏。
Paul M. Jones 对 PHP 领域数以万计的 github 项目的常见实践进行了一些出色的研究。他根据这项研究编译了标准文件和目录结构,即标准 PHP 包骨架。在这个目录结构中,DocumentRoot 应该指向 public/,单元测试应该在 tests/ 目录中,而由 composer 安装的第三方库应该在 vendor/ 目录中。对于其他文件和目录,遵守标准 PHP 包骨架将对项目的贡献者最有意义。
代码风格指南
PHP 社区庞大而多样,由无数的库、框架和组件组成。PHP 开发人员通常会选择其中的几个并将它们组合到一个项目中。重要的是,PHP 代码必须(尽可能接近)一种通用的代码风格,以使开发人员可以轻松地为他们的项目混合和匹配各种库。
Framework Interop Group 提出并批准了一系列风格建议。并非所有这些都与代码风格有关,但与代码风格相关的是 PSR-1、PSR-12、PSR-4 。这些建议只是 Drupal、Zend、Symfony、Laravel、CakePHP、phpBB、AWS SDK、FuelPHP、Lithium 等许多项目采用的一组规则。您可以将它们用于您自己的项目,或继续使用您自己的个人风格。
理想情况下,您应该编写符合已知标准的 PHP 代码。这可以是 PSR 的任意组合,也可以是 PEAR 或 Zend 制定的编码标准之一。这意味着其他开发人员可以轻松阅读和使用您的代码,实现组件的应用程序即使在使用大量第三方代码时也可以保持一致性。
您可以使用 PHP_CodeSniffer 根据这些建议中的任何一项检查代码,并使用 Sublime Text 等文本编辑器的插件来获得实时反馈。
您可以使用以下工具之一自动修复代码布局:
- 一个是 PHP Coding Standards Fixer,它有一个经过很好测试的代码库。
- 此外,PHP_CodeSniffer 附带的 PHP Code Beautifier and Fixer 工具可用于相应地调整您的代码。
您可以从 shell 手动运行 phpcs:
phpcs -sw --standard=PSR1 file.php
它将显示错误并描述如何修复它们。将此命令包含在 git hook 中也很有帮助。这样,包含违反所选标准的分支无法进入存储库,直到这些违规被修复。
如果您有 PHP_CodeSniffer,那么您可以使用 PHP Code Beautifier 和 Fixer 自动修复它报告的代码布局问题。
phpcbf -w --standard=PSR1 file.php
另一种选择是使用 PHP Coding Standards Fixer 。它将显示代码结构在修复它们之前存在哪些类型的错误。
php-cs-fixer fix -v --rules=@PSR1 file.php
所有符号名称和代码基础结构都首选英语。评论可以用任何语言编写,所有当前和未来可能在代码库上工作的各方都可以轻松阅读。
最后,编写干净的 PHP 代码的一个很好的补充资源是 Clean Code PHP。
语言亮点
编程范式
PHP 是一种灵活的动态语言,支持多种编程技术。多年来,它发生了巨大的变化,特别是在 PHP 5.0 (2004) 中添加了可靠的面向对象模型,在 PHP 5.3 (2009) 中添加了匿名函数和命名空间,在 PHP 5.4 (2012) 中添加了 traits。
面向对象编程
PHP 有一套非常完整的面向对象的编程特性,包括对类、抽象类、接口、继承、构造函数、克隆、异常等的支持。
函数式编程
PHP 支持一等函数,这意味着可以将函数分配给变量。用户定义和内置函数都可以被变量引用并动态调用。函数可以作为参数传递给其他函数(称为高阶函数的功能),函数可以返回其他函数。
递归是一种允许函数调用自身的功能,该语言支持,但大多数 PHP 代码都专注于迭代。
自 PHP 5.3 (2009) 起出现了新的匿名函数(支持闭包)。
PHP 5.4 增加了将闭包绑定到对象作用域的能力,还改进了对可调用对象的支持,这样它们几乎可以在所有情况下与匿名函数互换使用。
- 继续阅读 PHP 函数式编程
- 阅读匿名函数
- 阅读有关 Closure 类的信息
- 闭包 RFC 中的更多详细信息
- 阅读 Callables
- 阅读有关动态调用函数的信息 call_user_func_array()
元编程
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-0 和 PSR-4 仍然完全可用。后者需要 PHP 5.3,因此许多仅 PHP 5.2 的项目都实现了 PSR-0。
如果您要为新的应用程序或包使用自动加载标准,请查看 PSR-4。 - 了解 PSR-0
-
PHP 标准库
PHP 标准库 (SPL) 与 PHP 一起打包,并提供了类和接口的集合。它主要由常用的数据结构类(堆栈、队列等)和可以遍历这些数据结构或实现 SPL 接口的您自己的类的迭代器组成。
-
命令行脚本
PHP 是为编写 Web 应用程序而创建的,但也可用于编写命令行 (CLI) 程序的脚本。 PHP 命令行程序可以帮助自动执行常见任务,例如测试、部署和应用程序管理。
CLI PHP 程序非常强大,因为您可以直接使用应用程序的代码,而无需为其创建和保护 Web GUI。请确保不要将 CLI PHP 脚本放在公共 Web 根目录中!
尝试从命令行运行 PHP:> php -i选项
-i将像 phpinfo() 函数一样打印您的 PHP 配置。
选项-a提供了一个交互式 shell,类似于 ruby 的 IRB 或 python 的交互式 shell。还有许多其他有用的命令行选项。
让我们编写一个简单的“Hello, $name” CLI 程序。要试用它,请创建一个名为hello.php的文件,如下所示。<?php if ($argc !== 2) { echo "Usage: php hello.php <name>" . PHP_EOL; exit(1); } $name = $argv[1]; echo "Hello, $name" . PHP_EOL;PHP 根据运行脚本的参数设置两个特殊变量。$argc 是一个包含参数计数的整数变量,$argv 是一个包含每个参数值的数组变量。第一个参数始终是 PHP 脚本文件的名称,在本例中为 hello.php 。
exit()表达式与非零数字一起使用,以让 shell 知道命令失败。可以在这里找到常用的退出代码。
要从命令行运行上面的脚本:> 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 置于“监听连接”模式,然后加载 URL:
http://your-website.example.com/index.php?XDEBUG_SESSION_START=1您的 IDE 现在将在脚本执行时截取当前状态,允许您设置断点并探测内存中的值。
图形调试器使单步调试代码、检查变量和针对实时运行时评估代码变得非常容易。许多 IDE 都为使用 Xdebug 进行图形调试提供了内置或基于插件的支持。MacGDBp 是适用于 Mac 的免费、开源、独立的 Xdebug GUI。 -
依赖管理
PHP 有大量的库、框架和组件可供选择。您的项目可能会使用其中的几个——这些是项目依赖项。直到最近,PHP 还没有很好的方法来管理这些项目依赖项。即使您手动管理它们,您仍然需要担心自动加载器。但现在这已经不再是问题了。
目前有两个主要的包管理系统 PHP - Composer 和 PEAR。Composer 目前是 PHP 最流行的包管理器,但很长一段时间 PEAR 是使用的主要包管理器。了解 PEAR 的历史是个好主意,因为即使您从不使用它,您仍然可以找到对它的引用。Composer 与 Packagist
Composer 是 PHP 推荐的依赖管理器。在一个
composer.json文件中列出您项目的依赖项,通过一些简单的命令,Composer 将自动下载您项目的依赖项并为您设置自动加载。Composer 类似于 node.js 世界中的 NPM,或 Ruby 世界中的 Bundler。
有大量的 PHP 库与 Composer 兼容并可以在您的项目中使用。这些“包”列在 Packagist 上,这是 Composer 兼容的 PHP 库的官方存储库。
如何安装 Composer
下载 composer 最安全的方法是按照官方说明进行操作。这将验证安装程序没有损坏或被篡改。composer.phar 安装程序会在您当前的工作目录中安装一个二进制文件。
我们建议全局安装 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.json 的文件。如果您愿意,可以手动管理它,或者使用 Composer 本身。composer require命令将添加一个项目依赖项,如果您没有 composer.json 文件,将创建一个文件。这是一个将 Twig 添加为项目依赖项的示例。composer require twig/twig:^2.0或者,
composer init命令将指导您为项目创建完整的 composer.json 文件。无论哪种方式,一旦您创建了 composer.json 文件,您就可以告诉 Composer 下载并安装您的依赖项到vendor/目录中。这也适用于您下载的已经提供 composer.json 文件的项目:composer install接下来,将此行添加到应用程序的主 PHP 文件中;这将告诉 PHP 为您的项目依赖项使用 Composer 的自动加载器。
<?php require 'vendor/autoload.php';现在您可以使用您的项目依赖项,它们会按需自动加载。
更新你的依赖
Composer 创建一个名为 composer.lock 的文件,该文件存储您第一次运行 composer install 时下载的每个包的确切版本。如果您与其他人共享您的项目,请把 composer.lock 文件也共享出去,以便他们在运行 composer install 时获得与您相同的版本。要更新您的依赖项,请运行 composer update 。composer update 部署时不要使用 ,只能使用 composer install 命令,否则您最终可能会在生产环境中使用不同的包版本。
这在您灵活定义版本要求时最有用。例如,版本要求的 ~1.8 意思是“任何大于 1.8.0,但小于 2.0.x-dev”的版本。您也可以使用通配符 ,如 1.8.。 现在 Composer 的 composer update 命令会将所有依赖项升级到符合您定义的限制的最新版本。
更新通知
要接收有关新版本发布的通知,您可以注册 library.io,这是一个可以监控依赖关系并向您发送更新通知的 Web 服务。
检查您的依赖项是否存在安全问题
Local PHP Security Checker 是一个命令行工具,它将检查您的 composer.lock 文件并告诉您是否需要更新任何依赖项。
使用 Composer 处理全局依赖项
Composer 还可以处理全局依赖项及其二进制文件。用法很简单,您只需在命令前加上global。例如,如果您想安装 PHPUnit 并使其在全局范围内可用,您将运行以下命令:composer global require phpunit/phpunit这将创建一个 ~/.composer 文件夹,您的全局依赖项所在的位置。要让已安装包的二进制文件随处可用,您需要将 ~/.composer/vendor/bin 文件夹添加到您的 $PATH 变量中。
-
PEAR
一些 PHP 开发人员喜欢的资深包管理器是PEAR。它的行为类似于 Composer,但有一些显著差异。
PEAR 要求每个包都具有特定的结构,这意味着包的作者必须准备好与 PEAR 一起使用。使用未准备好与 PEAR 一起工作的项目是不可能的。
PEAR 全局安装包,这意味着在安装它们之后,服务器上的所有项目都可以使用。如果许多项目依赖于具有相同版本的相同包,这可能会很好,但如果两个项目之间出现版本冲突,则可能会导致问题。
如何安装 PEAR
您可以通过下载.phar安装程序并执行它来安装 PEAR 。PEAR 文档为每个操作系统提供了详细的安装说明。
如果您使用的是 Linux,您还可以查看您的分发包管理器。例如,Debian 和 Ubuntu 有一个 php-pear 的 apt 安装包。
如何安装扩展包
如果扩展包在 PEAR Packages 列表中列出,您可以通过指定正式名称来安装它:pear install foo如果包托管在另一个渠道上,您需要先 discover 渠道,并在安装时指定它。有关此主题的更多信息,请参阅使用渠道文档。
使用 Composer 处理 PEAR 依赖项
如果您已经在使用 Composer 并且还想安装一些 PEAR 代码,您可以使用 Composer 来处理您的 PEAR 依赖项。此示例将从 pear2.php.net 安装代码:
{
"repositories": [
{
"type": "pear",
"url": "https://pear2.php.net"
}
],
"require": {
"pear-pear2/PEAR2_Text_Markdown": "*",
"pear-pear2/PEAR2_HTTP_Request": "*"
}
}
第一部分”repositories”将用于让 Composer 知道它应该“初始化”(或用 PEAR 术语“发现”)pear repo。然后 require 部分将为包名称添加前缀,如下所示:
pear-channel/Package
“pear”前缀是硬编码的,以避免任何冲突,例如,pear 渠道可能与另一个包供应商名称相同,然后可以使用渠道短名称(或完整 URL)来引用包所在的渠道。
安装此代码后,它将在您的 vendor 目录中可用,并通过 Composer 自动加载器自动加载:
vendor/pear-pear2.php.net/PEAR2_HTTP_Request/pear2/HTTP/Request.php
要使用这个 PEAR 包,只需像这样引用它:
<?php
$request = new pear2\HTTP\Request();
了解有关将 PEAR 与 Composer 一起使用的更多信息
编码实践
基础知识
PHP 是一种庞大的语言,它使所有级别的编码人员都能够不仅快速而且高效地生成代码。然而,在学习语言的过程中,我们经常忘记我们最初学习(或忽视)的基础知识,而倾向于捷径和/或坏习惯。为了帮助解决这个常见问题,本节旨在提醒编码人员 PHP 中的基本编码实践。
-
日期和时间
PHP 有一个名为 DateTime 的类来帮助您读取、写入、比较或计算日期和时间。除了 DateTime 之外,PHP 中还有许多与日期和时间相关的函数,但它为大多数常见用途提供了很好的面向对象的接口。DateTime 可以处理时区,但这超出了本简短介绍的范围。
要开始使用 DateTime,请将原始日期和时间字符串转换为使用 createFromFormat() 工厂方法的对象,或者 new DateTime 获取当前日期和时间。使用 format() 方法将 DateTime 转换回字符串以进行输出。 ```php <?php $raw = ‘22. 11. 1968’; $start = DateTime::createFromFormat(‘d. m. Y’, $raw);
echo ‘Start date: ‘ . $start->format(‘Y-m-d’) . PHP_EOL;
使用 DateInterval 类可以使用 DateTime 进行计算。DateTime 具有类似 add() 和 sub() 的方法,它们将 DateInterval 作为参数。不要编写期望每天有相同秒数的代码。夏令时和时区更改都将打破这一假设。请改用日期间隔。要计算日期差异,请使用该 diff() 方法。它将返回新的 DateInterval,它非常容易显示。
```php
<?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)') . PHP_EOL;
// Difference: 1 month, 6 days (total: 37 days)
您可以对 DateTime 对象直接比较:
<?php
if ($start < $end) {
echo "Start is before the end!" . PHP_EOL;
}
最后一个示例来演示 DatePeriod 类。它用于迭代重复发生的事件。它可以采用两个 DateTime 对象,start 和 end,以及返回其间所有事件的时间间隔。
<?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') . ' ';
}
一个流行的 PHP API 扩展是 Carbon。它继承了 DateTime 类中的所有内容,因此涉及的代码更改最少,但额外的功能包括本地化支持、添加、减去和格式化 DateTime 对象的更多方法,以及通过模拟您选择的日期和时间来测试代码的方法。
- 阅读有关日期时间的信息
- 阅读日期格式
设计模式
在构建应用程序时,在代码中使用通用模式以及对项目的整体结构使用通用模式会很有帮助。使用通用模式很有帮助,因为它可以更轻松地管理您的代码,并让其他开发人员快速了解所有内容如何组合在一起。
如果您使用框架,那么大多数上层代码和项目结构都将基于该框架,因为很多模式决策都是由框架做出的。但是,您仍然可以在框架之上构建的代码中选择要遵循的最佳模式。另一方面,如果您没有使用框架来构建应用程序,那么您必须找到最适合您正在构建的应用程序类型和大小的模式。
您可以在以下位置了解有关 PHP 设计模式的更多信息并查看工作示例:
https://designpatternsphp.readthedocs.io/使用 UTF-8 编码
本节最初由 Alex Cabal 在 PHP Best Practices 上编写,并已用作我们自己的 UTF-8 建议的基础。
这不是在开玩笑。请小心、仔细并且前后一致地处理它。
目前,PHP 仍未在底层实现对 Unicode 的支持。有一些方法可以确保 UTF-8 字符串被正确处理,但这并不容易,它需要深入到几乎所有层面的 Web 应用程序,从 HTML 到 SQL 到 PHP。我们的目标是简短、实用的总结。
PHP 层面的 UTF-8
基本的字符串操作,比如连接两个字符串和将字符串赋值给变量,对于 UTF-8 不需要任何特殊的处理。但是,大多数字符串函数,如 strpos() 和 strlen(),确实需要特别处理。这些函数通常有 mb* 对应的:例如 mb_strpos() 和 mb_strlen() 。这些 mb 字符串是通过多字节字符串扩展提供给您的,并且专门设计用于对 Unicode 字符串进行操作。
mb_ 每当您对 Unicode 字符串进行操作时,都必须使用这些函数。例如,如果您 substr() 在 UTF-8 字符串上使用,则结果很可能会包含一些乱码的半字符。正确使用的函数是对应的多字节函数mbsubstr()。
最难的部分是记住始终使用这些 mb 函数。如果您忘记一次,您的 Unicode 字符串就有可能在进一步处理过程中出现乱码。
并非所有字符串函数都有 mb_ 对应的。如果没有你想要的 mb_ 函数,那么你可能不走运。
您应该在您编写的每个 PHP 脚本的顶部(或在您的全局包含脚本的顶部)使用 mb_internal_encoding() 函数,如果您的脚本正在输出到浏览器,则 mb_http_output() 函数紧随其后。在每个脚本中明确定义字符串的编码将为您省去很多麻烦。
此外,许多对字符串进行操作的 PHP 函数都有一个可选参数,可让您指定字符编码。给定选项时,您应该始终明确指出 UTF-8。例如,htmlentities() 有一个字符编码选项,如果处理此类字符串,您应该始终指定 UTF-8。请注意,从 PHP 5.4.0 开始,UTF-8 是 htmlentities() 和 htmlspecialchars() 的默认编码。
最后,如果您正在构建一个分布式应用程序并且不能确定 mbstring 扩展是否会启用,那么请考虑使用symfony/polyfill-mbstring Composer 包。如果可用,这将使用 mbstring,如果不可用,则回退到非 UTF-8 函数。
数据库 层面的 UTF-8
如果您使用 PHP 脚本访问 MySQL,即使您遵循上述所有预防措施,您的字符串也有可能在数据库中存储为非 UTF-8 字符串。
为确保您的字符串以 UTF-8 从 PHP 传输到 MySQL,请确保您的数据库和表都设置为 utf8mb4 字符集和排序规则,并且在 PDO 连接字符串中使用 utf8mb4 字符集。
请注意,您必须使用 utf8mb4 字符集来获得完整的 UTF-8 支持,而不是 utf8 字符集!
*浏览器 层面的 UTF-8
使用 mb_http_output() 函数确保您的 PHP 脚本将 UTF-8 字符串输出到您的浏览器。
然后浏览器需要被 HTTP 响应告知该页面应被视为 UTF-8。如今,在 HTTP 响应标头中设置字符集很常见,如下所示:
这样做的历史方法是将字符集标记包含在页面标记中。 ```php <?php // 告诉PHP使用UTF-8编码直到脚本执行结束 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!’); }<?php header('Content-Type: text/html; charset=UTF-8')
// 告诉PHP使用UTF-8编码输出到浏览器 mb_http_output(‘UTF-8’);
// 我们UTF-8测试的字符串 $string = ‘Êl síla erin lû e-govaned vîn.’;
// 使用字符串函数以某种方式转换字符串 // 现在我们使用非ASCII码字符来剪切字符串 $string = mb_substr($string, 0, 15);
// 连接数据库来存储转换后的字符串
// 有关更多信息,请参阅本文档中的PDO示例
// 注意在数据源中的字符集是 charset=utf8mb4
$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
)
);
// 以UTF-8存储我们转换后字符串到数据库 // 校验下你的DB和tables是不是以utf8mb4编码的 $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); // 明确告诉PDO期待一个int类型 $handle->execute();
// 检索我们刚刚存储的字符串是否存储正确 $handle = $link->prepare(‘select * from ElvishSentences where Id = :id’); $id = 7; $handle->bindParam(‘:id’, $id, PDO::PARAM_INT); $handle->execute();
// 存储的结果对象我们稍后会输出在HTML中 // 此对象不会占据你的内存,因为数据可以及时从DB中获取 $result = $handle->fetchAll(\PDO::FETCH_OBJ);
// 一个包装器样例来允许存储的转义数据到HTML中 function escape_to_html($dirty){ echo htmlspecialchars($dirty, ENT_QUOTES, ‘UTF-8’); }
header(‘Content-Type: text/html; charset=UTF-8’); // 如果您的默认字符集已设置为utf-8,则不需要 ?><!doctype html>
<?php foreach($result as $row){ escape_to_html($row->Body); // 这将正确地将转换后的UTF-8字符串输出到浏览器 } ?>
**进一步阅读**
- [PHP 手册:字符串操作](https://secure.php.net/language.operators.string)
- [PHP 手册:字符串函数](https://secure.php.net/ref.strings)
- [strpos()](https://secure.php.net/function.strpos)
- [strlen()](https://secure.php.net/function.strlen)
- [substr()](https://secure.php.net/function.substr)
- [PHP 手册:多字节字符串函数](https://secure.php.net/ref.mbstring)
- [mb_strpos()](https://secure.php.net/function.mb-strpos)
- [mb_strlen()](https://secure.php.net/function.mb-strlen)
- [mb_substr()](https://secure.php.net/function.mb-substr)
- [mb_internal_encoding()](https://secure.php.net/function.mb-internal-encoding)
- [mb_http_output()](https://secure.php.net/function.mb-http-output)
- [htmlentities()](https://secure.php.net/function.htmlentities)
- [htmlspecialchars()](https://secure.php.net/function.htmlspecialchars)
- [Stack Overflow:什么原因使 PHP Unicode 不兼容?](https://stackoverflow.com/questions/571694/what-factors-make-php-unicode-incompatible)
- [Stack Overflow:PHP 和 MySQL 国际化最佳实践](https://stackoverflow.com/questions/140728/best-practices-in-php-and-mysql-with-international-strings)
- [如何在 MySQL 数据库中完美支持 Unicode](https://mathiasbynens.be/notes/mysql-utf8mb4)
- [使用 Portable UTF-8 将 Unicode 引入 PHP](https://www.sitepoint.com/bringing-unicode-to-php-with-portable-utf8/)
- [堆栈溢出:DOMDocument loadHTML 未正确编码 UTF-8](https://stackoverflow.com/questions/8218230/php-domdocument-loadhtml-not-encoding-utf-8-correctly)
<a name="kg3Le"></a>
### 国际化和本地化
对新手声明:i18n 和 l10n 是数字名词,一种使用数字来缩短单词的缩写 - 在我们的例子中,国际化变为 i18n,本地化变为 l10n。<br />首先,我们需要定义这两个相似的概念和其他相关的东西:
- 国际化是指您组织代码以便它可以适应不同的语言或地区而无需重构。此操作通常执行一次 - 最好在项目开始时执行,否则您可能需要对源代码进行一些巨大的更改!
- 当您(主要)通过翻译内容来调整界面时,本地化就会发生,基于之前完成的 i18n 工作。它通常在每次新语言或地区需要支持时完成,并在添加新界面时更新,因为它们需要在所有支持的语言中可用。
- 多元化定义了不同语言之间互操作包含数字和计数器的字符串所需的规则。例如,在英语中,当您只有一项时,它是单数,与此不同的任何东西都称为复数;这种语言中的复数是通过在某些单词后添加 S 来表示的,有时会更改部分单词。在其他语言中,例如俄语或塞尔维亚语,除了单数之外还有两种复数形式——您甚至可以找到总共有四种、五种或六种形式的语言,例如斯洛文尼亚语、爱尔兰语或阿拉伯语。
**常见的实现方式**<br />国际化 PHP 软件的最简单方法是使用数组文件并在模板中使用这些字符串,例如 <h1><?=$TRANS['title_about_page']?></h1>。然而,这种方式几乎不推荐用于严肃的项目,因为它会带来一些维护问题 - 有些可能会在一开始就出现,例如复数形式。所以,如果您的项目包含多个页面,请不要尝试此操作。<br />最经典的方式并且经常作为 i18n 和 l10n 的参考是一个[Unix 工具,称为gettext](https://en.wikipedia.org/wiki/Gettext)。它可以追溯到 1995 年,仍然是翻译软件的完整实现。运行起来很容易,同时仍然具有强大的支持工具。我们将在这里讨论 Gettext。此外,为了帮助您不要在命令行上弄得一团糟,我们将展示一个出色的 GUI 应用程序,可用于轻松更新您的 l10n 源代码。<br />**其他工具**<br />有一些常用的库支持 Gettext 和 i18n 的其他实现。其中一些似乎更易于安装或具有附加功能或 i18n 文件格式。在本文档中,我们专注于 PHP 核心提供的工具,但在这里我们列出其他工具以供参考:
- [aura/intl](https://github.com/auraphp/Aura.Intl):提供国际化 (I18N) 工具,特别是面向包的每个语言环境的消息翻译。它对消息使用数组格式。不提供消息提取器,但通过 intl 扩展提供高级消息格式(包括复数消息)。
- [oscarotero/Gettext](https://github.com/oscarotero/Gettext):带有 OO 接口的 Gettext 支持;包括改进的辅助功能,多种文件格式的强大提取器(其中一些不被命令本机支持 gettext),并且还可以导出到文件以外的其他格式 .mo/.po。如果您需要将翻译文件集成到系统的其他部分(如 JavaScript 界面)中,这可能会很有用。
- [symfony/translation](https://symfony.com/doc/current/components/translation.html):支持很多不同的格式,但建议使用详细的 XLIFF。不包括辅助函数或内置提取器,但支持在 strtr() 内部使用占位符。
- [laminas/laminas-i18n](https://docs.laminas.dev/laminas-i18n/):支持数组和 INI 文件,或 Gettext 格式。实现一个缓存层来避免每次读取文件系统。它还包括视图助手、区域感知输入过滤器和验证器。但是,它没有消息提取器。
其他框架也包括 i18n 模块,但这些模块在其代码库之外不可用:
- [Laravel](https://laravel.com/docs/master/localization):支持基本的数组文件,没有自动提取器,但包含 @lang 模板文件的帮助程序。
- [Yii](https://www.yiiframework.com/doc/guide/2.0/en/tutorial-i18n):支持数组、Gettext 和基于数据库的翻译,并包含一个消息提取器。它由 [Intl](https://secure.php.net/manual/intro.intl.php) 扩展支持,自 PHP 5.3 起可用,并基于 [ICU 项目](http://www.icu-project.org/);这使 Yii 能够运行强大的替换功能,例如拼写数字、格式化日期、时间、间隔、货币和序数。
**参考**
- [维基百科:i18n 和 l10n](https://en.wikipedia.org/wiki/Internationalization_and_localization)
- [维基百科:Gettext](https://en.wikipedia.org/wiki/Gettext)
- [LingoHub:使用 Gettext 教程实现 PHP 国际化](https://lingohub.com/blog/2013/07/php-internationalization-with-gettext-tutorial/)
- [PHP 手册:Gettext](https://secure.php.net/manual/book.gettext.php)
- [Gettext 手册](https://www.gnu.org/software/gettext/manual/gettext.html)
<a name="BI4gR"></a>
## 依赖注入
来自[维基百科](https://wikipedia.org/wiki/Dependency_injection):
> 依赖注入是一种软件设计模式,它允许删除硬编码的依赖关系,并可以在运行时或编译时更改它们。
这句话使这个概念听起来比实际上要复杂得多。依赖注入是通过构造函数注入、方法调用或属性设置为组件提供其依赖项。就是这么简单。
<a name="ze5Hq"></a>
### 基本概念
我们可以用一个简单但幼稚的例子来演示这个概念。<br />这里我们有一个 Database 类,它需要一个适配器来与数据库交互。我们在构造函数中实例化适配器并创建硬依赖。这使测试变得困难,并且意味着 Database 类与适配器非常紧密地耦合。
```php
<?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 我们可以直接设置它。
复杂问题
如果您曾经阅读过依赖注入,那么您可能已经看过“控制反转”或 “依赖反转原则”这些术语。这些是依赖注入解决的复杂问题。
控制反转
控制反转正如它所说,通过将组织控制与我们的对象完全分开来“反转控制”系统。就依赖注入而言,这意味着通过在系统的其他地方控制和实例化它们来解耦我们的依赖。
多年来,PHP 框架一直在实现控制反转,然而,问题变成了,我们要反转哪一部分控制,以及在哪里反转?例如,MVC 框架通常会提供一个超级对象或基本控制器,其他控制器必须继承它才能访问其依赖项。这是控制反转,但是,这种方法并没有解耦依赖关系,而是简单地移动了它们。
依赖注入允许我们通过仅在需要时注入我们需要的依赖项来更优雅地解决这个问题,而根本不需要任何硬编码的依赖项。
SOLID 原则
单一职责原则
单一职责原则是关于参与者和高级架构的。它指出“一个类应该只有一个改变的理由”。这意味着每个类应该只对软件提供的功能的一个部分负责。这种方法的最大好处是它可以提高代码的可复用性。通过设计我们的类只做一件事,我们可以在任何其他程序中使用(或复用)它而无需更改它。
开闭原则
开放/关闭原则是关于类设计和功能扩展的。它指出“软件实体(类、模块、函数等)应该对扩展开放,但对修改关闭。” 这意味着我们应该以这样一种方式设计我们的模块、类和函数,即当需要新功能时,我们不应该修改现有代码,而是编写将由现有代码使用的新代码。实际上,这意味着我们应该编写实现并遵守接口的类,然后针对这些接口而不是特定类进行类型提示。
这种方法的最大好处是我们可以很容易地扩展我们的代码,支持新的东西,而无需修改现有代码,这意味着我们可以减少 QA 时间,并且大大降低了对应用程序产生负面影响的风险。我们可以更快、更有信心地部署新代码。
里氏替换原则
里氏替换原则是关于子类和继承的。它指出“子类永远不应该破坏父类的定义。” 或者,用 Robert C. Martin 的话来说,“子类必须可以被父类替代。”
例如,如果我们有一个 FileInterface 接口定义了一个 embed() 方法,并且我们有 Audio 类和 Video 类都实现了 FileInterface 接口,那么我们可以期望 embed() 方法的使用总是会做我们想要做的事情。如果我们稍后创建一个 PDF 类或 Gist 实现 FileInterface 接口的类,我们将已经知道并理解 embed() 方法将做什么。这种方法的最大好处是我们能够构建灵活且易于配置的程序,因为当我们将一个类型的对象(例如,FileInterface)更改为另一个对象时,我们不需要更改程序中的任何其他内容。
接口隔离原则
接口隔离原则是关于业务逻辑到客户端的通信。它指出“不应强迫任何客户依赖它不使用的方法。” 这意味着我们应该提供一组较小的、特定于概念的接口,而不是所有符合标准的类都需要实现一个单一的整体接口,这些接口由符合标准的类实现一个或多个。
例如,一个 CarorBus 类会对一个 steeringWheel() 方法感兴趣,但一个 MotorcycleorTricycle 类不会。相反,一个 MotorcycleorTricycle 类会对一个 handlebars() 方法感兴趣,但一个 CarorBus 类不会。没有必要让不同类型的车都实现对 steeringWheel() 和 handlebars() 的支持,因此我们应该拆分源接口。
依赖倒置原则
依赖倒置原则是关于删除离散类之间的硬链接,以便可以通过传递不同的类来利用新功能。它指出应该“依赖于抽象。不要依赖具体实现。” . 简而言之,这意味着我们的依赖项应该是接口/契约或抽象类,而不是具体的实现。我们可以很容易地重构上面的例子来遵循这个原则。
<?php
namespace Database;
class Database
{
protected $adapter;
public function __construct(AdapterInterface $adapter)
{
$this->adapter = $adapter;
}
}
interface AdapterInterface {}
class MysqlAdapter implements AdapterInterface {}
Database 现在依赖于接口而不是具体实现,该类有几个好处。
假设我们在一个团队中工作,而适配器正在由一位同事开发。在我们的第一个示例中,我们必须等待所述同事完成适配器,然后才能正确模拟它以进行单元测试。现在依赖项是一个接口/契约,我们可以愉快地模拟该接口,因为我们的同事将基于该契约构建适配器。
这种方法的一个更大的好处是我们的代码现在更具可扩展性。如果一年后我们决定要迁移到不同类型的数据库,我们可以编写一个适配器来实现原始接口并注入它,而不需要更多的重构,因为我们可以确保适配器遵循接口设置的契约。
容器
关于依赖注入容器,您应该了解的第一件事是它们与依赖注入不同。容器是一种便利实用程序,可以帮助我们实现依赖注入,但是,它们可能并且经常被滥用来实现反模式服务定位。将 DI 容器作为服务定位器注入到您的类中,可以说会创建对容器的依赖,而不是您要替换的依赖。它还使您的代码变得不那么透明,最终更难测试。
大多数现代框架都有自己的依赖注入容器,允许您通过配置将依赖项连接在一起。这在实践中意味着您可以编写与构建它的框架一样干净和解耦的应用程序代码。
延伸阅读
- 什么是依赖注入?
- 依赖注入:类比
- 依赖注入:嗯?
-
数据库
很多时候,您的 PHP 代码将使用数据库来保存信息。您有几个选项可以连接数据库并与之交互。PHP 5.1.0 之前的推荐选项是使用原生驱动程序,例如 mysqli、pgsql、 mssql 等。
如果您在应用程序中只使用一个数据库,则原生驱动程序非常好,但如果您使用 MySQL 和一点点 MSSQL,或者您需要连接到 Oracle 数据库,那么您将无法使用相同的驱动程序。您需要为每个数据库学习一个全新的 API——这可能会变得很愚蠢。MySQL 扩展
PHP 的 mysql 扩展非常古老,已被其他两个扩展取代:
- pdo
不仅在 mysql 上的开发很久以前就停止了,而且在 PHP 5.5.0 中已被弃用,并且在 PHP 7.0 中已被正式删除。
为了深入了解您的设置以查看您正在使用的模块,您不需要到 php.ini 去查看。只需要使用编辑器打开您的项目,然后全局搜索 mysql* ,如果有类似 mysql_connect() 或者 mysql_query() 方法出现,则表示 mysql 正在使用中。
即使您还没有使用 PHP 7.x,如果不尽快考虑这次升级,当 PHP 7.x 升级确实到来时,将会导致更大的困难。最好的选择是在您自己的开发计划中将应用程序中的 mysqli 或 PDO 替换为 mysqli 使用,这样您以后就不会匆忙。
如果您要从 mysql 升级到 mysqli ,请注意懒惰的升级指南,这些指南建议您不要简单地查找并替换mysql 为 mysqli_ 。这不仅过于简单化,而且忽略了 mysqli 提供的优势,例如 PDO 中也提供的参数绑定。
- MySQLi 预处理语句
- PHP:为 MySQL 选择 API
PDO 扩展
PDO 是一个数据库连接抽象库——自 5.1.0 以来内置于 PHP 中——它提供了一个通用接口来与许多不同的数据库通信。例如,您可以使用基本相同的代码与 MySQL 或 SQLite 交互: ```php <?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 连接到多种类型的数据库。<br />更重要的是,PDO 允许您安全地将外部输入(例如 ID)注入到您的 SQL 查询,而不必担心数据库 SQL 注入攻击。这可以使用 PDO 语句和绑定参数来实现。<br />假设一个 PHP 脚本接收一个数字 ID 作为查询参数。此 ID 应用于从数据库中获取用户记录。这是错误执行此操作的方法:
```php
<?php
$pdo = new PDO('sqlite:/path/db/users.db');
$pdo->query("SELECT name FROM users WHERE id = " . $_GET['id']); // <-- NO!
这是可怕的代码。将原始参数直接插入 SQL 语句中, 这会造成 SQL 注入的风险。 如果黑客通过调用 URL(如 domain.com/?id=1%3BDELETE+FROM+users )传入一个修改过的的 id 参数。使用 $_GET[‘id’] 获取到的参数为 1;DELETE FROM users 将会删除所有用户! 相反,应该使用 PDO 绑定参数。
<?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,您可以通过确保删除对它的所有剩余引用(即设置为 NULL)来销毁对象来隐式关闭连接。如果你没有明确地这样做,PHP 将在你的脚本结束时自动关闭连接——当然除非你使用持久连接。
- 了解 PDO 连接
数据库交互
当开发人员第一次开始学习 PHP 时,他们通常会将数据库交互与表示逻辑混合在一起,使用的代码可能如下所示:
由于各种原因,这是一种不好的做法,主要是它难以调试、难以测试、难以阅读,而且如果你不在那里设置限制,它会输出很多字段。<ul> <?php foreach ($db->query('SELECT * FROM table') as $row) { echo "<li>".$row['field1']." - ".$row['field1']."</li>"; } ?> </ul>
虽然有许多其他解决方案可以做到这一点 - 取决于您喜欢 OOP 还是 函数式编程 - 必须有一些分离元素。
考虑最基本的步骤: ```php <?php function getAllFoos($db) { return $db->query(‘SELECT * FROM table’); }
$results = getAllFoos($db); foreach ($results as $row) { echo “
这是一个好的开始。把这两个项目放在两个不同的文件中,你就有了一些干净的分离。<br />创建一个类来放置该方法,您就有一个“模型”。创建一个简单的.php文件来放入表示逻辑,你就有一个“视图”,它非常接近于[MVC](https://code.tutsplus.com/tutorials/mvc-for-noobs--net-10488)——大多数[框架](https://phptherightway.com/#frameworks)的常见 OOP 架构 。<br />**foo.php**
```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): ?>
<li><?= $row['field1'] ?> - <?= $row['field1'] ?></li>
<?php endforeach ?>
这与大多数现代框架所做的基本相同,尽管需要更多手动操作。您可能不需要每次都这样做,但是如果您想对应用程序进行单元测试,那么将过多的表示逻辑和数据库交互混合在一起可能是一个真正的问题。
数据库抽象层
许多框架提供了自己的抽象层,其中一些是设计在 PDO 上层的。这些通常会通过将查询包装在 PHP 方法中来模拟另一个数据库系统缺少的功能,从而为您提供实际的数据库抽象,而不仅仅是 PDO 提供的连接抽象。这当然会增加一点开销,但是如果你正在构建一个需要使用 MySQL、PostgreSQL 和 SQLite 的可移植应用程序,那么为了代码的简洁性,一点开销是值得的。
一些抽象层是使用 PSR-0 或 PSR-4 命名空间标准构建的,因此可以安装在您喜欢的任何应用程序中:
- Atlas
- Aura SQL
- Doctrine2 DBAL
- Propel
- Zend-db
使用模板
模板提供了一种将控制器和域逻辑与表示逻辑分离的便捷方式。模板通常包含应用程序的 HTML,但也可用于其他格式,例如 XML。模板通常被称为“视图”,它构成了模型-视图-控制器(MVC) 软件架构模式的第二个组件的 一部分。好处
使用模板的主要好处是它们在表示逻辑和应用程序的其余部分之间创建了清晰的分离。模板全权负责显示格式化的内容。他们不负责数据查找、持久性或其他更复杂的任务。这导致代码更清晰、更易读,这在开发人员处理服务端代码(控制器、模型)而设计人员处理客户端代码(视图)的团队环境中特别有用。
模板还改进了演示代码的组织。模板通常放置在“views”文件夹中,每个都在一个文件中定义。这种方法鼓励代码重用,其中较大的代码块被分解成较小的、可重用的部分,通常称为部分。例如,您的网站页眉和页脚都可以定义为模板,然后包含在每个页面模板之前和之后。
最后,根据您使用的库,模板可以通过自动转义用户生成的内容来提供更高的安全性。一些库甚至提供沙盒,模板设计者只能访问白名单中的变量和函数。普通 PHP 模板
普通 PHP 模板只是使用原生 PHP 代码的模板。它们是自然的选择,因为 PHP 实际上是一种模板语言。这仅仅意味着您可以将 PHP 代码与其他代码(如 HTML)结合起来。这对 PHP 开发人员是有益的,因为不需要学习新的语法,他们知道可用的功能,并且他们的代码编辑器已经内置了 PHP 语法突出显示和自动完成功能。此外,纯 PHP 模板往往非常快,因为不需要编译阶段。
每个现代 PHP 框架都使用某种模板系统,其中大多数默认使用纯 PHP。在框架之外,Plates 或Aura.View 等库通过提供现代模板功能(如继承、布局和扩展)使使用普通 PHP 模板更容易。
简单的 PHP 模板的简单示例
使用 Plates 库。 ```php <?php // user_profile.php ?>
<?php $this->insert(‘header’, [‘title’ => ‘User Profile’]) ?>
User Profile
Hello, <?=$this->escape($name)?>
<?php $this->insert(‘footer’) ?>
**使用继承的普通 PHP 模板示例**<br />使用 [Plates](http://platesphp.com/) 库。
```php
<?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、Brainy 或 Smarty,通过提供专门针对模板的新语法来填补这一空白。从自动转义到继承和简化的控制结构,编译后的模板被设计为更容易编写、更清晰的阅读和更安全的使用。编译后的模板甚至可以在不同的语言之间共享,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 %}
延伸阅读
- PHP 中的模板引擎
- CodeIgniter 中的视图和模板简介
- PHP 模板入门
- 在 PHP 中运行你自己的模板系统
- 母版页
- 在 Symfony 2 中使用模板
-
错误与异常
错误
在许多“重异常”的编程语言中,每当出现任何问题时都会抛出异常。这当然是一种可行的做事方式,但 PHP 是一种“轻异常”的编程语言。虽然它确实有异常并且更多的核心在处理对象时开始使用它们,但大多数 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 有几个级别的错误严重性。三种最常见的消息类型是错误、通知和警告。这些具有不同的严重程度;E_ERROR, E_NOTICE, E_WARNING. 错误是致命的运行时错误,通常是由代码中的错误引起的,需要修复,因为它们会导致 PHP 停止执行。通知是由代码引起的建议消息,在脚本执行期间可能会或可能不会导致问题,执行不会停止。警告是非致命错误,不会停止脚本的执行。
编译时报告的另一种错误消息是 E_STRICT 消息。这些消息用于建议对代码的更改,以帮助确保与即将推出的 PHP 版本的最佳互操作性和前向兼容性。
更改 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: foo 或 PHP Notice: Undefined index: bar 错误。
这似乎是一个好主意,但有一些不受欢迎的权衡。PHP 处理带 @ 符号的表达式时性能会受到影响。过早的性能优化可能在所有编程语言中都是争议点,但如果性能对您的应用程序/库特别重要,那么了解错误控制运算符的性能影响就很重要。
其次,错误控制运算符完全吞下错误。不显示错误,也不会将错误发送到错误日志。此外,在正式环境中 PHP 也没有办法关闭错误控制运算符。也许你认为那些错误时无害的,不过那些较具伤害性的错误同时也会被隐藏。
如果有办法避免错误抑制运算符,您应该考虑它。例如,我们上面的代码可以这样重写:<?php // Null Coalescing Operator echo $foo['bar'] ?? '';错误抑制可能有意义的一个实例是 fopen() 无法找到要加载的文件。您可以在尝试加载文件之前检查文件是否存在,但如果在检查之后和之前删除文件(这听起来不可能,但它可能发生)然后 fopen() 将返回 false 并引发错误。这可能是 PHP 应该解决的问题,但在一种情况下,错误抑制似乎是唯一有效的解决方案。
前面我们提到在普通的 PHP 系统中没有办法关闭错误控制操作符。但是,Xdebug有一个xdebug.screamini 设置,它将禁用错误控制操作符。您可以使用以下内容通过您的 php.ini 文件进行设置。xdebug.scream = On您还可以在运行时使用该 ini_set 函数设置此值。
<?php ini_set('xdebug.scream', '1')“ Scream ”PHP 扩展提供与 Xdebug 类似的功能,尽管 Scream 的 ini 设置名为 scream.enabled.
当您正在调试代码并怀疑信息错误被抑制时,这非常有用。小心使用 Scream,并将其作为临时调试工具。有很多 PHP 库代码在禁用错误控制运算符的情况下可能无法工作。 - SitePoint
- Xdebug
- Scream
错误异常
PHP 完全有能力成为一种“重异常”的编程语言,并且只需要几行代码即可进行切换。基本上,您可以利用 ErrorException 类抛出“错误”来当做“异常”,这个类是继承自 Exception 类。
这是大量现代框架(如 Symfony 和 Laravel)实现的常见做法。在调试模式(或开发模式)下,这两个框架都将显示一个漂亮而干净的堆栈跟踪。
还有一些包可用于更好的错误和异常处理和报告。就像Whoops,它带有 Laravel 的默认安装,也可以在任何框架中使用。
通过在开发中将错误作为异常抛出,您可以比通常的结果更好地处理它们,并且如果您在开发过程中看到异常,您可以将其包装在 catch 语句中,并提供有关如何处理这种情况的具体说明。您立即捕获的每个异常都会使您的应用程序更加健壮。
有关这方面的更多信息以及有关如何使用ErrorException错误处理的详细信息,请参见ErrorException Class。
- Error Control Operators
- Predefined Constants for Error Handling
- error_reporting()
- 报告
异常
异常是大多数流行编程语言的标准部分,但它们经常被 PHP 程序员忽略。像 Ruby 这样的语言极度重视异常,所以每当出现问题时,例如 HTTP 请求失败、数据库查询出错,或者即使找不到图片资源,Ruby(或正在使用的 gem)都会抛出异常到屏幕意味着您立即知道有一个错误。
PHP 本身对此相当松懈,调用 tofile_get_contents() 通常只会给您 FALSE 一个警告。许多较旧的 PHP 框架(如 CodeIgniter)只会返回错误,将消息记录到其专有日志中,并且可能让您使用诸如$this->upload->get_error() 查看问题所在的方法。这里的问题是你必须去寻找一个错误并检查文档以查看这个类的错误方法是什么,而不是让它变得非常明显。
另一个问题是当类自动向屏幕抛出错误并退出进程时。当您这样做时,您会阻止另一个开发人员能够动态处理该错误。应该抛出异常以使开发人员意识到错误;然后他们可以选择如何处理。例如: ```php <?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) { // The validation failed } catch(Fuel\Email\SendingFailedException $e) { // The driver could not send the email } finally { // Executed regardless of whether an exception has been thrown, and before normal execution resumes }
**SPL 异常**<br />原生 Exception 类为开发人员提供了非常少的调试上下文;然而,为了解决这个问题,可以 Exception通过子类继承原生 Exception 类来创建一个专门的类型:
```php
<?php
class ValidationException extends Exception {
}
这意味着您可以添加多个 catch 块并以不同的方式处理不同的异常。这可能会导致创建大量自定义异常,其中一些可以使用SPL 扩展中提供的 SPL 异常来避免。
例如,如果您使用 __call() 魔术方法并且请求了一个无效方法,那么您可以不抛出一个模糊的标准异常,或者为此创建一个自定义异常,您可以抛出 throw new BadMethodCallException;.
- 阅读异常情况
- 阅读有关 SPL 异常的信息
- PHP中的嵌套异常
安全
我发现的关于 PHP 安全性的最佳资源是 Paragon Initiative 的 The 2018 Guide to Building Secure PHP Software。Web 应用程序安全
对于每个 PHP 开发人员来说,学习 Web 应用程序安全的基础知识是非常重要的,这些基础知识可以分为几个广泛的主题:
- 代码数据分离。
- 当数据作为代码执行时,您会获得 SQL 注入、跨站点脚本、本地/远程文件包含等。
- 当代码作为数据打印时,您会得到信息泄漏(源代码泄露,或者在 C 程序的情况下,有足够的信息绕过ASLR)。
- 应用逻辑。
- 缺少身份验证或授权控制。
- 输入验证。
- 操作环境。
- PHP 版本。
- 第三方库。
- 操作系统。
- 密码学的弱点。
有坏人准备并利用您的 Web 应用程序进行攻击。采取必要的预防措施来加强 Web 应用程序的安全性很重要。幸运的是, 开放 Web 应用程序安全项目(OWASP) 的优秀人员编制了一份全面的已知安全问题列表和保护自己免受这些问题影响的方法。对于有安全意识的开发人员来说,这是一本必读的书。Padraic Brady 的《生存手册:PHP 安全》也是 PHP 的另一本优秀的 Web 应用程序安全指南。
-
密码哈希
最终,每个人都构建了一个依赖于用户登录的 PHP 应用程序。用户名和密码存储在数据库中,稍后用于在登录时对用户进行身份验证。
在存储密码之前正确散列密码很重要。散列和加密是两个非常不同的东西 ,经常被混淆。
散列是一种不可逆的单向函数。这会产生一个固定长度的字符串,无法将其反转。这意味着您可以将哈希与另一个进行比较以确定它们是否都来自同一源字符串,但您无法确定原始字符串。如果密码没有经过哈希处理,并且您的数据库被未经授权的第三方访问,那么所有用户帐户现在都会受到威胁。
与散列不同,加密是可逆的(前提是您拥有密钥)。加密在其他领域很有用,但对于安全存储密码来说是一种糟糕的策略。
密码还应该通过在散列之前为每个密码添加一个随机字符串来单独加盐)。这可以防止字典攻击和使用“彩虹表”(常见密码的加密哈希的反向列表。)
散列和加盐至关重要,因为用户经常为多个服务使用相同的密码,而且密码质量可能很差。
此外,您应该使用专门的密码散列算法,而不是快速、通用的加密散列函数(例如 SHA256)。可接受的密码散列算法(截至 2018 年 6 月)的简短列表是: Argon2(在 PHP 7.2 和更新版本中可用)
- Scrypt
- Bcrypt(PHP 为您提供了这个;见下文)
- 带有 HMAC-SHA256 或 HMAC-SHA512 的 PBKDF2
幸运的是,现在 PHP 使这变得容易。
哈希密码 password_hash
password_hash() 在 PHP 5.5 中被引入。此时它使用的是 BCrypt,这是 PHP 目前支持的最强算法。它将在未来进行更新,以根据需要支持更多算法。创建该 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() 提取它以确定如何检查密码,因此您不需要单独的数据库字段来存储您的盐。
- 学习关于 password_hash()
- password_compat for PHP >= 5.3.7 && < 5.5
- 了解有关密码学的散列
- 了解盐)
-
数据过滤
永远不要相信引入到您的 PHP 代码中的外部输入。在代码中使用外部输入之前,请始终对其进行清理和验证。filter_var() 和 filter_input() 函数可以净化文本并验证文本格式(例如电子邮件地址)。
外部输入可以是任何东西:$_GET 和 $_POST 表单输入数据、$_SERVER 超全局中的一些值以及通过 fopen(‘php://input’, ‘r’)。请记住,外部输入不仅限于用户提交的表单数据。上传和下载的文件、会话值、cookie 数据以及来自第三方 Web 服务的数据也是外部输入。
虽然外部数据可以在以后存储、组合和访问,但它仍然是外部输入。每次在代码中处理、输出、连接或包含数据时,都要问问自己数据是否被正确过滤以及是否可以信任。
数据可能会根据其目的进行不同的过滤。例如,当未经过滤的外部输入传递到 HTML 页面输出时,它可以在您的站点上执行 HTML 和 JavaScript!这称为跨站点脚本 (XSS),可能是一种非常危险的攻击。避免 XSS 的一种方法是在将所有用户生成的数据输出到您的页面之前对其进行清理,方法是使用 strip_tags() 函数删除 HTML 标记,或者使用 htmlentities() 或 htmlspecialchars() 函数将具有特殊含义的字符转义到它们各自的 HTML 实体中。
另一个示例是传递要在命令行上执行的选项。这可能非常危险(通常是个坏主意),但您可以使用内置 escapeshellarg() 函数来清理已执行命令的参数。
最后一个示例是接受外部输入以确定要从文件系统加载的文件。这可以通过将文件名更改为文件路径来利用。您需要从文件路径中删除”/“, “../“, null bytes或其他字符,以便它无法加载隐藏、非公共或敏感文件。 - 学习关于 filter_var
- 学习关于 filter_input
- 了解如何处理空字节
清理
清理从外部输入中删除(或转义)非法或不安全的字符。
例如,您应该在将输入包含在 HTML 中或将其插入到原始 SQL 查询中之前清理外部输入。当您将绑定参数与 PDO 一起使用时,它会为您清理输入。
有时,在 HTML 页面中包含输入时,需要在输入中允许一些安全的 HTML 标记。这很难做到,许多人通过使用其他更受限制的格式(如 Markdown 或 BBCode)来避免它,尽管出于这个原因存在诸如 HTML Purifier 之类的白名单库。
反序列化
unserialize() 来自用户或其他不受信任来源的数据是危险的。这样做可以允许恶意用户实例化将执行其析构函数的对象(具有用户定义的属性),即使对象本身没有被使用。因此,您应该避免对不受信任的数据进行反序列化。
如果您必须反序列化来自不受信任来源的数据,请使用 PHP 7 的 allowed_classes 选项来限制允许反序列化的对象类型。
验证
验证确保外部输入是您所期望的。例如,您可能希望在处理注册提交时验证电子邮件地址、电话号码或年龄。
-
配置文件
为您的应用程序创建配置文件时,最佳实践建议遵循以下方法之一:
建议您将配置信息存储在无法直接访问并通过文件系统上传的位置。
- 如果您必须将配置文件存储在文档根目录中,请使用.php扩展名命名文件。这确保了即使直接访问脚本,也不会以纯文本形式输出。
- 配置文件中的信息应通过加密或组/用户文件系统权限得到相应的保护。
确保您不会将包含敏感信息(例如密码或 API 令牌)的配置文件提交给源代码控制是一个好主意。
注册全局变量
注意:从 PHP 5.4.0 开始,register_globals 设置已被删除,无法再使用。这仅作为对升级旧应用程序过程中的任何人的警告。
启用后,配置设置会在应用程序的全局范围内提供 register_globals 多种类型的变量(包括来自 $_POST 和 $_GET 的变量)。$_REQUEST 这很容易导致安全问题,因为您的应用程序无法有效地判断数据的来源。
例如:$_GET[‘foo’] 可以通过 $foo 使用,它可以覆盖已声明的变量。
如果您使用的是 PHP < 5.4.0 ,请确保 register_globals 处于关闭状态。-
错误报告
错误日志有助于查找应用程序中的问题点,但它也可以将有关应用程序结构的信息暴露给外界。为了有效地保护您的应用程序免受这些消息的输出可能导致的问题,您需要在开发和生产(实时)中以不同的方式配置您的服务器。
开发
要在开发过程中显示每个可能的错误,请在您的 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 -1 或 E_ALL
- 5.3 -1 或 E_ALL | E_STRICT
5.3 -1 或 E_ALL
生产
要隐藏生产环境中的错误,请将您的 php.ini 配置为:
display_errors = Off
display_startup_errors = Off
error_reporting = E_ALL
log_errors = On
在生产环境中使用这些设置,错误仍将记录到 Web 服务器的错误日志中,但不会显示给用户。有关这些设置的更多信息,请参阅 PHP 手册:
- 错误报告
- 显示错误
- 显示启动错误
- 日志错误
测试
为您的 PHP 代码编写自动化测试被认为是一种最佳实践,并且可以带来构建良好的应用程序。自动化测试是一个很好的工具,可以确保您的应用程序在您进行更改或添加新功能时不会中断,并且不应被忽视。
有几种不同类型的测试工具(或框架)可用于 PHP,它们使用不同的方法——所有这些都试图避免手动测试和对大型质量保证团队的需求,只是为了确保最近的更改不会破坏现有的功能。测试驱动开发
来自维基百科:测试驱动开发 (TDD) 是一种依赖于重复非常短的开发周期的软件开发过程:首先,开发人员编写一个失败的自动化测试用例,定义所需的改进或新功能,然后生成代码以通过该测试并最后将新代码重构为可接受的标准。因开发或“重新发现”该技术而备受赞誉的 Kent Beck 在 2003 年表示,TDD 鼓励简单的设计并激发信心。
您可以为您的应用程序执行几种不同类型的测试:
单元测试
单元测试是一种编程方法,用于确保函数、类和方法按预期工作,从您构建它们的那一刻开始,一直到开发周期。通过检查各种函数和方法的进出值,您可以确保内部逻辑正常工作。通过使用依赖注入和构建“模拟”类和存根,您可以验证依赖项是否正确用于更好的测试覆盖率。
当您创建一个类或函数时,您应该为它必须具有的每个行为创建一个单元测试。在一个非常基本的级别上,如果你发送错误的参数,你应该确保它出错,如果你发送有效的参数,你应该确保它有效。这将有助于确保当您在开发周期后期对此类或函数进行更改时,旧功能继续按预期工作。唯一的替代方法是var_dump() 在 test.php 中,这不是构建应用程序的方法——无论大小。
单元测试的另一个用途是为开源做出贡献。如果您可以编写一个显示功能损坏(即失败)的测试,然后修复它并显示测试通过,那么补丁更有可能被接受。如果您运行一个接受拉取请求的项目,那么您应该将其作为一项要求提出建议。
PHPUnit 是为 PHP 应用程序编写单元测试的事实上的测试框架,但有几种替代方案
集成测试
来自维基百科:
集成测试(有时称为集成和测试,缩写为“I&T”)是软件测试中的一个阶段,其中单个软件模块被组合并作为一个组进行测试。它发生在单元测试之后和验证测试之前。集成测试将经过单元测试的模块作为其输入,将它们分组到更大的聚合中,将集成测试计划中定义的测试应用于这些聚合,并将集成系统作为其输出交付给系统测试。
许多可用于单元测试的相同工具可用于集成测试,因为使用了许多相同的原则。
功能测试
有时也称为验收测试,功能测试包括使用工具来创建实际使用您的应用程序的自动化测试,而不仅仅是验证各个代码单元的行为是否正确以及各个单元是否可以正确地相互通信。这些工具通常使用真实数据并模拟应用程序的实际用户。
功能测试工具
- Selenium
- Mink
- Codeception 是一个包含验收测试工具的全栈测试框架
Storyplayer 是一个全栈测试框架,包括支持按需创建和销毁测试环境
行为驱动开发
有两种不同类型的行为驱动开发 (BDD):SpecBDD 和 StoryBDD。SpecBDD 侧重于代码的技术行为,而 StoryBDD 侧重于业务或功能行为或交互。
使用 StoryBDD,您可以编写描述应用程序行为的人类可读故事。然后可以将这些故事作为针对您的应用程序的实际测试运行。StoryBDD 的 PHP 应用程序中使用的框架是 Behat ,它受到 Ruby 的Cucumber 项目的启发,并实现了 Gherkin DSL 来描述特性行为。
使用 SpecBDD,您可以编写描述实际代码行为方式的规范。您不是在测试函数或方法,而是在描述该函数或方法的行为方式。PHP 为此提供了 PHPSpec 框架。这个框架的灵感来自 Ruby 的 RSpec 项目。
BDD 链接- PHPSpec 是 PHP 的 SpecBDD 框架,灵感来自 Ruby 的 RSpec 项目;
Codeception 是一个使用 BDD 原则的全栈测试框架。
其它测试工具
除了单独的测试和行为驱动框架之外,还有许多通用框架和辅助库可用于任何首选方法。
工具链接Selenium 是一个可以与 PHPUnit 集成的浏览器自动化工具。
- Mockery 是一个模拟对象框架,可以与 PHPUnit 或 PHPSpec 集成。
- Prophecy 是一个自以为是但非常强大和灵活的 PHP 对象模拟框架。它与 PHPSpec 集成,可以与PHPUnit 一起使用。
- php-mock 是一个帮助模拟 PHP 原生函数的库。
Infection 是变异测试的 PHP 实现,用于帮助衡量测试的有效性。
服务器与部署
PHP 应用程序可以通过多种方式在生产 Web 服务器上部署和运行。
平台即服务 (PaaS)
PaaS 提供了在 Web 上运行 PHP 应用程序所需的系统和网络架构。这意味着几乎不需要配置来启动 PHP 应用程序或 PHP 框架。
最近,PaaS 已成为部署、托管和扩展各种规模的 PHP 应用程序的流行方法。您可以在我们的资源部分找到PHP PaaS“平台即服务”提供商的列表。虚拟或专用服务器
如果您对系统管理感到满意,或者有兴趣学习它,虚拟或专用服务器可以让您完全控制应用程序的生产环境。
nginx 和 PHP-FPM
PHP 通过内置 FastCGI 进程管理器 (FPM) 与 nginx 完美搭配,后者是一个轻量级、高性能的 Web 服务器。它比 Apache 使用更少的内存,并且可以更好地处理更多的并发请求。这对于没有太多可用内存的虚拟服务器尤其重要。- 阅读更多关于 PHP-FPM
- 阅读有关安全设置 nginx 和 PHP-FPM 的更多信息
Apache 和 PHP
PHP 和 Apache 有着悠久的历史。Apache 具有广泛的可配置性,并且有许多可用的模块来扩展功能。它是共享服务器的流行选择,也是 PHP 框架和 WordPress 等开源应用程序的简单设置。不幸的是,Apache 默认使用比 nginx 更多的资源,并且无法同时处理尽可能多的访问者。
Apache 有几种可能的配置来运行 PHP。最常见和最容易设置的是 mod_php5 带有的 prefork MPM 。虽然它不是最高效的内存,但它是最简单的工作和使用。如果您不想深入研究服务器管理方面,这可能是最佳选择。请注意,如果您使用 mod_php5,您必须使用 prefork MPM。
或者,如果您想从 Apache 中获得更多的性能和稳定性,那么您可以利用与 nginx 相同的 FPM 系统,并使用 mod_fastcgi 或 mod_fcgid 运行 worker MPM 或 event MPM 。此配置将显著提高内存效率和速度,但设置工作量更大。
如果您运行的是 Apache 2.4 或更高版本,则可以使用 mod_proxy_fcgi 来获得易于设置的出色性能。
- 阅读更多关于 Apache
- 阅读有关多进程模块的更多信息
- 阅读更多关于 mod_fastcgi
- 阅读有关 mod_fcgid 的更多信息
- 阅读有关 mod_proxy_fcgi 的更多信息
阅读有关使用 mod_proxy_fcgi 设置 Apache 和 PHP-FPM 的更多信息
共享服务器
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 文件中完成了简单的配置。它支持多个服务器和环境、原子部署,并具有一些内置任务,您可以将这些任务用于常用工具和框架。
进一步阅读:
- 使用 Apache Ant 自动化您的项目
- 部署 PHP 应用程序- 关于 PHP 部署的最佳实践和工具的付费书籍。
服务器配置
面对许多服务器时,管理和配置服务器可能是一项艰巨的任务。有一些工具可以解决这个问题,因此您可以自动化您的基础架构,以确保您拥有正确的服务器并且它们配置正确。它们通常与更大的云托管服务提供商(Amazon Web Services、Heroku、DigitalOcean 等)集成以管理实例,这使得扩展应用程序变得更加容易。
Ansible 是一个通过 YAML 文件管理基础设施的工具。它很容易上手,并且可以管理复杂和大规模的应用程序。有一个用于管理云实例的 API,它可以使用某些工具通过动态清单来管理它们。
Puppet 是一种工具,它有自己的语言和文件类型,用于管理服务器和配置。它可以在主/客户端设置中使用,也可以在“无主”模式下使用。在主/客户端模式下,客户端将按设定的时间间隔轮询中央主控以获取新配置,并在必要时自行更新。在无主模式下,您可以将更改推送到您的节点。
Chef 是一个强大的基于 Ruby 的系统集成框架,您可以使用它构建整个服务器环境或虚拟盒子。它通过名为 OpsWorks 的服务与 Amazon Web Services 很好地集成。
进一步阅读:
- Ansible 教程
- Ansible for DevOps - 关于一切 Ansible 的付费书籍
- Ansible for AWS - 关于集成 Ansible 和 Amazon Web Services 的付费书籍
- 关于使用 Chef、Vagrant 和 EC2 部署 LAMP 应用程序的三部分博客系列
- 安装和配置 PHP 和 PEAR 包管理系统的 Chef Cookbook
- Chef 视频教程系列
持续集成
持续集成是一种软件开发实践,团队成员经常集成他们的工作,通常每个人至少每天进行一次集成——导致每天进行多次集成。许多团队发现这种方法可以显著减少集成问题,并允许团队更快地开发有凝聚力的软件。
有多种方法可以实现 PHP 的持续集成。Travis CI 在使持续集成成为现实方面做得非常出色,即使对于小型项目也是如此。Travis CI 是开源社区的托管持续集成服务。它与 GitHub 集成,并为包括 PHP 在内的多种语言提供一流的支持。
进一步阅读:
- 与 Jenkins 持续集成
- 与 PHPCI 的持续集成
- 与 PHP Censor 的持续集成
-
虚拟化
在开发和生产的不同环境中运行应用程序可能会导致上线时出现奇怪的错误。在与开发团队合作时,让不同的开发环境使用相同版本的所有库保持最新也是很棘手的。
如果您在 Windows 上开发并部署到 Linux(或任何非 Windows)或在团队中开发,您应该考虑使用虚拟机。这听起来很棘手,但除了 VMware 或 VirtualBox 等广为人知的虚拟化环境之外,还有其他工具可以帮助您通过几个简单的步骤设置虚拟环境。Vagrant
Vagrant 帮助您在已知的虚拟环境之上构建您的虚拟盒子,并将基于单个配置文件配置这些环境。这些盒子可以手动设置,或者您可以使用 Puppet 或 Chef 等“配置”软件为您执行此操作。配置基础 Box 是确保以相同方式设置多个 Box 的好方法,并且无需维护复杂的“设置”命令列表。您还可以“销毁”您的基础 Box 并重新创建它,而无需许多手动步骤,从而轻松创建“全新”安装。
Vagrant 创建用于在主机和虚拟机之间共享代码的文件夹,这意味着您可以在主机上创建和编辑文件,然后在虚拟机中运行代码。
一点帮助
如果你在开始使用 Vagrant 时需要一点帮助,这里有一些服务可能有用: Puphpet:简单的 GUI,用于设置 PHP 开发的虚拟机。高度专注于 PHP。除了本地虚拟机,它还可以用于部署到云服务。使用 Puppet 进行配置。
Phansible:提供易于使用的界面,可帮助您为基于 PHP 的项目生成 Ansible Playbook。
Docker
Docker - 一个完整虚拟机的轻量级替代品 - 之所以如此称呼,是因为它全都与“容器”有关。容器是一个构建块,在最简单的情况下,它执行一项特定的工作,例如运行 Web 服务器。“镜像”是你用来构建容器的包——Docker 有一个完整的存储库。
一个典型的 LAMP 应用程序可能有三个容器:一个 Web 服务器、一个 PHP-FPM 进程和 MySQL。与 Vagrant 中的共享文件夹一样,您可以将应用程序文件留在原处,并告诉 Docker 在哪里可以找到它们。
您可以从命令行生成容器(参见下面的示例),或者为了便于维护,docker-compose.yml 为您的项目构建一个文件,指定要创建的容器以及它们如何相互通信。
如果您正在开发多个网站并希望通过将每个网站安装在自己的虚拟机上来实现分离,但没有必要的磁盘空间或时间来保持一切都是最新的,那么 Docker 可能会有所帮助。高效:安装和下载速度更快,您只需存储每个映像的一个副本,无论使用频率如何,容器需要更少的 RAM 并共享相同的 OS 内核,因此您可以同时运行更多服务器,这需要一个问题几秒钟即可停止和启动它们,无需等待完整的服务器启动。
示例:在 Docker 中运行 PHP 应用程序
在您的机器上安装 docker后,您可以使用一个命令启动 Web 服务器。以下将下载具有最新 PHP 版本的全功能 Apache 安装,映射 /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-webserverand docker start my-php-webserver(不再需要其他参数)。
了解有关 Docker 的更多信息
上面的命令显示了运行基本服务器的快速方法。您还可以做更多事情(以及 Docker Hub 中的数千个预构建镜像)。花时间学习术语并阅读Docker 用户指南以充分利用它,并且不要在未检查其安全性的情况下运行您下载的随机代码——非官方镜像可能没有最新的安全补丁。如果有疑问,请坚持使用官方存储库。
PHPDocker.io 站点将自动生成功能齐全的 LAMP/LEMP 全栈所需的所有文件,包括您选择的 PHP 版本和扩展。- Docker 安装
- Docker 用户指南
- Docker 中心
-
缓存
PHP 本身非常快,但是当您进行远程连接、加载文件等时可能会出现瓶颈。值得庆幸的是,有各种工具可用于加速应用程序的某些部分,或减少这些各种耗时任务所需要运行的次数。
Opcode 缓存
执行 PHP 文件时,必须首先将其编译为操作码(CPU 的机器语言指令)。如果源代码不变,那么操作码也是一样的,所以这个编译步骤就变成了对 CPU 资源的浪费。
操作码缓存通过将操作码存储在内存中并在后续调用中重用它们来防止冗余编译。它通常会首先检查文件的签名或修改时间,以防有任何更改。
操作码缓存很可能会显著提高您的应用程序的速度。从 PHP 5.5 开始,有一个内置的 Zend OPcache。根据您的 PHP 包/发行版,它通常默认打开 - 检查 opcache.enable 和输出 phpinfo() 以确保。对于早期版本,有一个 PECL 扩展。
阅读有关操作码缓存的更多信息: Zend OPcache(自 5.5 起与 PHP 捆绑)
- Zend OPcache(以前称为 Zend Optimizer+)现已开源
- APC - PHP 5.4 及更早版本
- XCache
- WinCache(MS Windows 服务器的扩展)
- 维基百科上的 PHP 加速器列表
- PHP 预加载- PHP >= 7.4
对象缓存
有时在代码中缓存单个对象可能是有益的,例如获取昂贵的数据或结果不太可能改变的数据库调用。您可以使用对象缓存软件将这些数据保存在内存中,以便以后快速访问。如果在检索这些项目后将它们保存到数据存储中,然后直接从缓存中提取它们以进行后续请求,则可以显着提高性能并减少数据库服务器上的负载。
许多流行的字节码缓存解决方案也允许您缓存自定义数据,因此更有理由利用它们。APCu、XCache 和 WinCache 都提供 API 来将 PHP 代码中的数据保存到它们的内存缓存中。
最常用的内存对象缓存系统是 APCu 和 memcached。APCu 是对象缓存的绝佳选择,它包含一个简单的 API,用于将您自己的数据添加到其内存缓存中,并且非常易于设置和使用。APCu 的一个真正限制是它与安装它的服务器相关联。另一方面,Memcached 作为单独的服务安装,可以通过网络访问,这意味着您可以将对象存储在中央位置的超高速数据存储中,并且许多不同的系统都可以从中提取。
请注意,当在您的网络服务器中将 PHP 作为(快速)CGI 应用程序运行时,每个 PHP 进程都有自己的缓存,即 APCu 数据不会在您的工作进程之间共享。在这些情况下,您可能需要考虑改用 memcached,因为它不依赖于 PHP 进程。
在网络配置中,APCu 在访问速度方面通常会优于 memcached,但 memcached 将能够更快、更好地扩展。如果您不希望有多个服务器运行您的应用程序,或者不需要 memcached 提供的额外功能,那么 APCu 可能是您进行对象缓存的最佳选择。
使用 APCu 的示例逻辑: ```php <?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)。<br />**了解有关流行对象缓存系统的更多信息:**
- [APCu](https://github.com/krakjoe/apcu)
- [APC Functions](https://secure.php.net/ref.apc)
- [Memcached](https://memcached.org/)
- [Redis](https://redis.io/)
- [XCache API](https://xcache.lighttpd.net/wiki/XcacheApi)
- [WinCache Functions](https://secure.php.net/ref.wincache)
<a name="Mo3if"></a>
## 代码注释
<a name="jUmv8"></a>
### PHPDoc
PHPDoc 是注释 PHP 代码的非正式标准。有很多不同的[标签](https://docs.phpdoc.org/latest/guide/references/phpdoc/tags/index.html)可用。标签和示例的完整列表可以在[PHPDoc 手册](https://docs.phpdoc.org/latest/index.html)中找到。<br />下面是一个示例,说明如何使用一些方法记录一个类;
```php
<?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标签。第二种和第三种方法的 doc 块之间的重要区别是@return标签的包含/排除。 @return void明确告知我们没有返回;历史上省略@return void语句也会导致相同的(不返回)操作。
资源
框架
许多 PHP 开发人员没有重新发明轮子,而是使用框架来构建 Web 应用程序。框架抽象出许多低级关注点,并提供有用的、易于使用的接口来完成常见任务。
您不需要为每个项目都使用一个框架。有时纯 PHP 是正确的方法,但如果您确实需要一个框架,那么可以使用三种主要类型:
- 微框架
- 全栈框架
- 组件框架
微框架本质上是一个包,用于尽可能快地将 HTTP 请求路由到回调、控制器、方法等,有时还会附带一些额外的库来协助开发,例如基本的数据库包等。它们主要用于构建远程 HTTP 服务。
许多框架在微框架中的可用功能之上添加了相当多的功能;这些被称为全栈框架。这些通常与 ORM、身份验证包等捆绑在一起。
基于组件的框架是专门的和单一用途的库的集合。不同的基于组件的框架可以一起使用来构建微栈或全栈框架。
组件
如上所述,“组件”是实现创建、分发和实现共享代码的共同目标的另一种方法。存在各种组件存储库,其中主要的两个是:
这两个存储库都有与之关联的命令行工具,以帮助安装和升级过程,并在“依赖管理”部分中进行了更详细的解释。
还有一些基于组件的框架和组件供应商根本不提供框架。这些项目提供了另一种包来源,理想情况下,它们几乎不依赖于其他包或特定框架。
例如,您可以使用FuelPHP 验证包,而无需使用 FuelPHP 框架本身。
- Aura
- CakePHP Components
- FuelPHP
- Hoa Project
- Symfony Components
- The League of Extraordinary Packages
- Laravel Illuminate Components
Laravel 的Illuminate 组件将更好地与 Laravel 框架解耦。目前,上面只列出了与 Laravel 框架解耦的最佳组件。
其它有用的资源
备忘单
- PHP Cheatsheets - 用于各种 PHP 版本中的变量比较、算术和变量测试。
- Modern PHP Cheatsheet - 在一个统一的文档中记录了现代(PHP 7.0+)习语。
- OWASP 安全备忘单 - 提供有关特定应用程序安全主题的高价值信息的简明集合。
更多最佳实践
