PHP7-高性能开发学习手册(全)

PHP7 高性能开发学习手册(全)

原文:zh.annas-archive.org/md5/57463751f7ad4ac2a29e3297fd76591c

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

PHP 社区在几十年来面临着一个巨大的问题:性能。无论他们拥有多么强大的硬件,最终 PHP 本身都成为了瓶颈。随着 PHP 5.4.x、5.5.x 和 5.6.x 的推出,PHP 的性能开始改善,但在高负载应用中仍然是一个巨大的问题。社区开发了诸如Alternative PHP CacheAPC)和 Zend OpCache 之类的缓存工具,这些工具对性能产生了良好的影响。

为了解决 PHP 的性能问题,Facebook 构建了他们自己的开源工具HHVMHipHop 虚拟机)。根据他们的官方网站,HHVM 使用即时(JIT)编译来实现卓越的性能,同时保持 PHP 提供的开发灵活性。与 PHP 相比,HHVM 的性能非常出色,并且广泛用于像 Magento 这样的重型应用的生产环境。

PHP 通过PHP Next GenerationPHPNG)与 HHVM 展开了竞争。PHPNG 的整个目的是提高性能,并专注于重写和优化 Zend 引擎内存分配和 PHP 数据类型。世界各地的人开始对 PHPNG 和 HHVM 进行基准测试,据他们称,PHPNG 的性能优于 HHVM。

最后,PHPNG 与 PHP 的主分支合并,经过大量优化和完全重写,PHP 7 发布,性能大幅提升。PHP 7 仍然不是 JIT,但其性能很好,与 HHVM 类似。这是与旧版本 PHP 相比的巨大性能提升。

本书涵盖的内容

第一章,设置环境,介绍了如何设置不同的开发环境,包括在 Windows、不同的 Linux 发行版上安装 NGINX、PHP 7 和 Percona Server,以及为开发目的设置 Vagrant 虚拟机。

第二章,PHP 7 的新特性,介绍了 PHP 7 引入的主要新特性,包括类型提示、组使用声明、匿名类和新操作符,如太空船操作符、空合并操作符和统一变量语法。

第三章,改善 PHP 7 应用程序性能,介绍了不同的技术来增加和扩展 PHP 7 应用程序的性能。在本章中,我们涵盖了 NGINX 和 Apache 的优化、CDN 和 CSS/JavaScript 的优化,如合并和最小化它们,全页面缓存以及安装和配置 Varnish。最后,我们讨论了应用开发的理想基础架构设置。

第四章,改善数据库性能,介绍了优化 MySQL 和 Percona Server 配置以实现高性能的技术。还介绍了不同的工具来监控数据库的性能。还介绍了用于缓存对象的 Memcached 和 Redis。

第五章,调试和性能分析,介绍了调试和性能分析技术,包括使用 Xdebug 进行调试和性能分析,使用 Sublime Text 3 和 Eclipse 进行调试,以及 PHP DebugBar。

第六章,压力/负载测试 PHP 应用程序,介绍了不同的工具来对应用程序进行压力和负载测试。涵盖了 Apache JMeter、ApacheBench 和 Siege 用于负载测试。还介绍了如何在 PHP 7 和 PHP 5.6 上对 Magento、Drupal 和 WordPress 等不同开源系统进行负载测试,并比较它们在 PHP 7 和 PHP 5.6 上的性能。

第七章,PHP 编程的最佳实践,介绍了一些生产高质量标准代码的最佳实践。涵盖了编码风格、设计模式、面向服务的架构、测试驱动开发、Git 和部署。

附录 A, 使生活更轻松的工具,更详细地讨论了其中三种工具。我们将讨论的工具是 Composer、Git 和 Grunt watch。

附录 B, MVC 和框架,涵盖了 PHP 开发中使用的 MVC 设计模式和最流行的框架,包括 Laravel、Lumen 和 Apigility。

您需要为本书准备什么

任何符合运行以下软件的最新版本的硬件规格都应足以完成本书的学习:

  • 操作系统:Debian 或 Ubuntu

  • 软件:NGINX、PHP 7、MySQL、PerconaDB、Redis、Memcached、Xdebug、Apache JMeter、ApacheBench、Siege 和 Git

这本书适合谁

这本书适合那些具有 PHP 编程基础经验的人。如果您正在开发性能关键的应用程序,那么这本书适合您。

约定

在本书中,您会发现一些文本样式,用于区分不同类型的信息。以下是这些样式的一些示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名显示如下:“我们可以通过使用include指令来包含其他上下文。”

代码块设置如下:

location ~ \.php$ {
  fastcgi_pass    127.0.0.1:9000;
  fastcgi_param    SCRIPT_FILENAME complete_path_webroot_folder$fastcgi_script_name;
  include    fastcgi_params;
}

当我们希望引起您对代码块的特定部分的注意时,相关行或项会以粗体显示:

server {
  …
  …
 **root html;**
 **index index.php index.html index.htm;**
  …

任何命令行输入或输出都以以下方式编写:

**php-cgi –b 127.0.0.1:9000**

新术语重要单词以粗体显示。例如,在屏幕上看到的单词,比如菜单或对话框中的单词,会在文本中出现,就像这样:“点击下一步按钮会将您移动到下一个屏幕。”

注意

警告或重要说明会以这样的方式出现在一个框中。

提示

提示和技巧会以这种方式出现。

第一章:设置环境

PHP 7 终于发布了。很长一段时间以来,PHP 社区一直在谈论它,而且仍未停止。PHP 7 的主要改进是其性能。很长一段时间以来,PHP 社区在大规模应用程序中面临性能问题。甚至一些高流量的小型应用程序也面临性能问题。服务器资源增加了,但并没有太大帮助,因为最终瓶颈是 PHP 本身。使用了不同的缓存技术,如 APC,这有所帮助。然而,社区仍然需要一个能够在应用程序性能达到顶峰时提升性能的 PHP 版本。这就是 PHPNG 的用武之地。

PHPNG代表PHP 下一代。它是一个完全独立的分支,主要针对性能。有些人认为 PHPNG 是JIT即时编译),但实际上,PHPNG 是基于经过高度优化的Zend 引擎重构而成的。PHPNG 被用作 PHP 7 开发的基础,根据官方 PHP 维基页面,PHPNG 分支现在已经合并到主分支中。

在开始构建应用程序之前,应该完成并配置好开发环境。在本章中,我们将讨论在不同系统上设置开发环境,如 Windows 和不同版本的 Linux。

我们将涵盖以下主题:

  • 设置 Windows

  • 设置 Ubuntu 或 Debian

  • 设置 CentOS

  • 设置 Vagrant

其他所有环境都可以跳过,我们可以设置我们将使用的环境。

设置 Windows

有许多可用的工具,它们在 Windows 上捆绑了 Apache、PHP 和 MySQL,提供了简单的安装,并且非常易于使用。这些工具中的大多数已经提供了对 PHP 7 与 Apache 的支持,例如 XAMPP、WAMPP 和 EasyPHP。EasyPHP 是唯一一个还提供对NGINX的支持,并提供了从 NGINX 切换到 Apache 或从 Apache 切换到 Nginx 的简单步骤。

注意

XAMPP 也适用于 Linux 和 Mac OS X。但是,WAMP 和 EasyPHP 仅适用于 Windows。这三种工具中的任何一种都可以用于本书,但我们建议使用 EasyPHP,因为它支持 NGINX,并且在本书中,我们主要使用 NGINX。

可以使用这三种工具中的任何一种,但我们需要更多地控制我们的 Web 服务器工具的每个元素,因此我们将单独安装 NGINX、PHP 7 和 MySQL,然后将它们连接在一起。

注意

可以从nginx.org/en/download.html下载 NGINX Windows 二进制文件。我们建议使用稳定版本,尽管使用主线版本也没有问题。可以从windows.php.net/download/下载 PHP Windows 二进制文件。根据您的系统下载 32 位或 64 位的非线程安全版本。

执行以下步骤:

  1. 下载信息框中提到的 NGINX 和 PHP Windows 二进制文件。将 NGINX 复制到合适的目录。例如,我们有一个完全独立的 D 盘用于开发目的。将 NGINX 复制到这个开发驱动器或任何其他目录。现在,将 PHP 复制到 NGINX 目录或任何其他安全文件夹位置。

  2. 在 PHP 目录中,将有两个.ini文件,php.ini-developmentphp.ini-production。将其中一个重命名为php.ini。PHP 将使用这个配置文件。

  3. 按住Shift键并在 PHP 目录中右键单击以打开命令行窗口。命令行窗口将在相同的位置路径中打开。发出以下命令启动 PHP:

php-cgi –b 127.0.0.1:9000

-b选项启动 PHP 并绑定到外部FastCGI服务器的路径。上述命令将 PHP 绑定到回环127.0.0.1IP 的端口9000。现在,PHP 可以在这个路径上访问。

  1. 要配置 NGINX,打开nginx_folder/conf/nginx.conf文件。首先要做的是在服务器块中添加 root 和 index,如下所示:
server {
 **root html;**
 **index index.php index.html index.htm;**

提示

下载示例代码

您可以从 http://www.packtpub.com 的帐户中下载本书的示例代码文件。如果您在其他地方购买了这本书,可以访问 http://www.packtpub.com/support 并注册,以便将文件直接发送到您的电子邮件。

您可以按照以下步骤下载代码文件:

  • 使用您的电子邮件地址和密码登录或注册到我们的网站。

  • 将鼠标指针悬停在顶部的 SUPPORT 选项卡上。

  • 单击代码下载和勘误。

  • 在搜索框中输入书名。

  • 选择您要下载代码文件的书。

  • 从下拉菜单中选择您购买本书的地方。

  • 单击代码下载。

下载文件后,请确保使用最新版本的以下软件解压或提取文件夹:

  • Windows 的 WinRAR / 7-Zip

  • Mac 的 Zipeg / iZip / UnRarX

  • Linux 的 7-Zip / PeaZip

  1. 现在,我们需要配置 NGINX 以在启动时使用 PHP 作为 FastCGI 的路径。在nginx.conf文件中,取消注释以下位置块以用于 PHP:
location ~ \.php$ {
  fastcgi_pass    127.0.0.1:9000;
  fastcgi_param    SCRIPT_FILENAME **complete_path_webroot_folder$fastcgi_script_name;**
include    fastcgi_params;
}

注意fastcgi_param选项。突出显示的complete_path_webroot_folder路径应该是nginx文件夹内 HTML 目录的绝对路径。假设您的 NGINX 放置在D:\nginx路径,那么HTML文件夹的绝对路径将是D:\nginx\html。但是,对于前面的fastcgi_param选项,\应该替换为/

  1. 现在,在 NGINX 文件夹的根目录中发出以下命令重新启动 NGINX:
**nginx –s restart**

  1. 在重新启动 NGINX 后,打开浏览器,输入 Windows 服务器或机器的 IP 或主机名,我们将看到 NGINX 的欢迎消息。

  2. 现在,要验证 PHP 安装并与 NGINX 一起工作,请在 webroot 中创建一个info.php文件,并输入以下代码:

<?php
  phpinfo();
?>
  1. 现在,在浏览器中访问your_ip/info.php,我们将看到一个充满 PHP 和服务器信息的页面。恭喜!我们已经成功配置了 NGINX 和 PHP,使它们完美地配合工作。

注意

在 Windows 和 Mac OS X 上,我们建议您使用安装有 Linux 版本的所有工具的虚拟机,以获得服务器的最佳性能。在 Linux 中管理一切很容易。有现成的 vagrant boxes 可供使用。另外,可以在puphpet.com上制作包括 NGINX、Apache、PHP 7、Ubuntu、Debian 或 CentOS 等工具的自定义虚拟机配置,这是一个易于使用的 GUI。另一个不错的工具是 Laravel Homestead,这是一个带有很棒工具的Vagrant box。

设置 Debian 或 Ubuntu

Ubuntu 是从 Debian 派生的,因此对于 Ubuntu 和 Debian,过程是相同的。我们将使用 Debian 8 Jessie 和 Ubuntu 14.04 Server LTS。相同的过程也适用于两者的桌面版本。

首先,为 Debian 和 Ubuntu 添加存储库。

Debian

截至我们撰写本书的时间,Debian 没有为 PHP 7 提供官方存储库。因此,对于 Debian,我们将使用dotdeb存储库来安装 NGINX 和 PHP 7。执行以下步骤:

  1. 打开/etc/apt/sources.list文件,并在文件末尾添加以下两行:
deb http://packages.dotdeb.org jessie all
deb-src http://packages.dotdeb.org jessie all
  1. 现在,在终端中执行以下命令:
**wget https://www.dotdeb.org/dotdeb.gpg**
**sudo apt-key add dotdeb.gpg**
**sudo apt-get update**

前两个命令将向 Debian 添加dotdeb存储库,最后一个命令将刷新源的缓存。

Ubuntu

截至撰写本书的时间,Ubuntu 也没有在官方存储库中提供 PHP 7,因此我们将使用第三方存储库进行 PHP 7 的安装。执行以下步骤:

  1. 在终端中运行以下命令:
**sudo add-apt-repository ppa:ondrej/php**
**sudo apt-get update**

  1. 现在,存储库已添加。让我们安装 NGINX 和 PHP 7。

注意

其余的过程对于 Debian 和 Ubuntu 大部分是相同的,所以我们不会单独列出它们,就像我们为添加存储库部分所做的那样。

  1. 要安装 NGINX,请在终端中运行以下命令(Debian 和 Ubuntu):
**sudo apt-get install nginx**

  1. 安装成功后,可以通过输入 Debian 或 Ubuntu 服务器的主机名和 IP 来验证。如果看到类似下面的屏幕截图,那么我们的安装是成功的:Ubuntu

以下是三个有用的 NGINX 命令列表:

  • service nginx start:这将启动 NGINX 服务器

  • service nginx restart:这将重新启动 NGINX 服务器

  • service nginx stop:这将停止 NGINX 服务器

  1. 现在,是时候通过发出以下命令来安装 PHP 7 了:
**sudo apt-get install php7.0 php7.0-fpm php7.0-mysql php7.0-mcrypt php7.0-cli**

这将安装 PHP 7 以及其他提到的模块。此外,我们还为命令行目的安装了 PHP Cli。要验证 PHP 7 是否已正确安装,请在终端中发出以下命令:

**php –v**

  1. 如果显示 PHP 版本以及其他一些细节,如下面的屏幕截图所示,那么 PHP 已经正确安装:Ubuntu

  2. 现在,我们需要配置 NGINX 以与 PHP 7 一起工作。首先,通过在终端中使用以下命令将 NGINX 默认配置文件/etc/nginx/sites-available/default复制到/etc/nginx/sites-available/www.packt.com.conf

**cd /etc/nginx/sites-available**
**sudo cp default www.packt.com.conf**
**sudo ln –s /etc/nginx /sites-available/www.packt.com.conf /etc/ nginx/sites-enabled/www.packt.com.conf**

首先,我们复制了默认配置文件,创建了另一个虚拟主机配置文件www.packt.com.conf,然后在 sites-enabled 文件夹中为这个虚拟主机文件创建了一个符号链接文件。

注意

为每个虚拟主机创建一个与域名相同的配置文件是一个很好的做法,这样可以很容易地被其他人识别。

  1. 现在,打开/etc/nginx/sites-available/www.packt.com.conf文件,并添加或编辑高亮显示的代码,如下所示:
server {
  server_**name your_ip:80**;
  root /var/www/html;
  index **index.php** index.html index.htm;
  **location ~ \.php$ {**
 **fastcgi_pass unix:/var/run/php/php7.0-fpm.sock;**
 **fastcgi_index index.php;**
 **include fastcgi_params;**
 **}**
}

上述配置不是一个完整的配置文件。我们只复制了那些重要的配置选项,我们可能想要更改的选项。

在上述代码中,我们的 webroot 路径是/var/www/html,我们的 PHP 文件和其他应用程序文件将放在这里。在索引配置选项中,添加index.php,这样如果 URL 中没有提供文件,NGINX 就可以查找并解析index.php

我们添加了一个用于 PHP 的位置块,其中包括一个fastcgi_pass选项,该选项具有指向 PHP7 FPM 套接字的路径。在这里,我们的 PHP 运行在 Unix 套接字上,比 TCP/IP 更快。

  1. 在进行这些更改后,重新启动 NGINX。现在,为了测试 PHP 和 NGINX 是否正确配置,创建一个info.php文件放在webroot文件夹的根目录,并在其中放入以下代码:
<?php
  phpinfo();
 ?>
  1. 现在,在浏览器中输入server_ip/info.php,如果看到一个 PHP 配置页面,那么恭喜!PHP 和 NGINX 都已正确配置。

注意

如果 PHP 和 NGINX 在同一系统上运行,那么 PHP 会监听端口9000的环回 IP。端口可以更改为任何其他端口。如果我们想要在 TCP/IP 端口上运行 PHP,那么在fastcgi_pass中,我们将输入127.0.0.1:9000

现在,让我们安装Percona Server。Percona Server 是 MySQL 的一个分支,经过优化以获得更高的性能。我们将在第三章中更多地了解 Percona Server,提高 PHP 7 应用程序性能。现在,让我们通过以下步骤在 Debian/Ubuntu 上安装 Percona Server:

  1. 首先,让我们通过在终端中运行以下命令将 Percona Server 仓库添加到我们的系统中:
**sudo wget https://repo.percona.com/apt/percona-release_0.1-3.$(lsb_release -sc)_all.deb**
**sudo dpkg -i percona-release_0.1-3.$(lsb_release -sc)_all.deb**

第一个命令将从 Percona 仓库下载软件包。第二个命令将安装已下载的软件包,并在/etc/apt/sources.list.d/percona-release.list创建一个percona-release.list文件。

  1. 现在,通过在终端中执行以下命令来安装 Percona Server:
**sudo apt-get update**

  1. 现在,通过发出以下命令来安装 Percona Server:
**sudo apt-get install percona-server-5.5**

安装过程将开始。下载需要一段时间。

注意

为了本书的目的,我们将安装 Percona Server 5.5。也可以安装 Percona Server 5.6,而且不会出现任何问题。

在安装过程中,将要求输入root用户的密码,如下图所示:

Ubuntu

输入密码是可选的但建议的。输入密码后,在下一个屏幕上重新输入密码。安装过程将继续。

  1. 安装完成后,可以使用以下命令验证 Percona Server 的安装:
**mysql –-version**

它将显示 Percona Server 的版本。如前所述,Percona Server 是 MySQL 的一个分支,因此可以使用相同的 MySQL 命令、查询和设置。

设置 CentOS

CentOS 是Red Hat Enterprise LinuxRHEL)的一个分支,代表Community Enterprise Operating System。它是服务器上广泛使用的操作系统,特别是由托管公司提供共享托管服务。

让我们首先为我们的开发环境配置 CentOS。执行以下步骤:

安装 NGINX

  1. 首先,我们需要将 NGINX RPM 添加到我们的 CentOS 安装中,因为 CentOS 没有提供任何默认的 NGINX 仓库。在终端中输入以下命令:
**sudo rpm -Uvh http://nginx.org/packages/centos/7/noarch/RPMS/nginx-release-centos-7-0.el7.ngx.noarch.rpm**

这将向 CentOS 添加 NGINX 仓库。

  1. 现在,输入以下命令以查看可供安装的 NGINX 版本:
**sudo yum --showduplicates list Nginx**

这将显示最新的稳定版本。在我们的情况下,它显示 NGINX 1.8.0 和 NGINX 1.8.1。

  1. 现在,让我们使用以下命令安装 NGINX:
**sudo yum install Nginx**

这将安装 NGINX。

  1. 在 CentOS 上,NGINX 在安装或重新启动后不会自动启动。因此,首先,我们将使用以下命令使 NGINX 在系统重新启动后自动启动:
**systemctl enable Nginx.service**

  1. 现在,让我们通过输入以下命令来启动 NGINX:
**systemctl start Nginx.service**

  1. 然后,打开浏览器,输入 CentOS 服务器的 IP 或主机名。如果您看到与我们在 Debian 章节中看到的欢迎屏幕相同的屏幕,则 NGINX 已成功安装。

要检查安装了哪个版本的 NGINX,请在终端中输入以下命令:

**Nginx –v**

在我们的服务器上,安装的 NGINX 版本是 1.8.1。

现在,我们的 Web 服务器已准备就绪。

安装 PHP 7

  1. 下一步是安装 PHP 7 FPM 并配置 NGINX 和 PHP 7 一起工作。在撰写本书时,PHP 7 没有打包在官方的 CentOS 仓库中。因此,我们有两种选择来安装 PHP 7:要么从源代码构建,要么使用第三方仓库。从源代码构建有点困难,所以让我们选择简单的方式,使用第三方仓库。

注意

对于本书,我们将使用 webtatic 仓库来安装 PHP 7,因为它们为新版本提供快速更新。还有一些其他仓库,只要它能正常工作,读者可以自行选择使用任何仓库。

  1. 现在,让我们通过输入以下命令向我们的 CentOS 仓库添加 webtatic 仓库:
**rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm**
**rpm -Uvh https://mirror.webtatic.com/yum/el7/webtatic-release.rpm**

  1. 成功添加仓库后,输入以下命令以查看可供安装的版本:
**sudo yum –showduplicates list php70w**

在我们的情况下,可以安装 PHP 7.0.3。

  1. 现在,输入以下命令以安装 PHP 7 以及可能需要的一些模块:
**sudo yum install php70w php70w-common php70w-cli php70w-fpm php70w-mysql php70w-opcache php70w-mcrypt**

  1. 这将安装核心 PHP 7 和一些可用于 PHP 7 的模块。如果需要其他模块,可以轻松安装;但是首先搜索以检查其是否可用。在终端中输入以下命令以查看所有可用的 PHP 7 模块:
**sudo yum search php70w-**

将显示所有可用的 PHP 7 模块的长列表。

  1. 现在,假设我们要安装 PHP 7 gd 模块;输入以下命令:
**sudo yum install php70w-gd**

这将安装 gd 模块。可以使用相同的命令安装多个模块,并通过空格分隔每个模块,就像我们在最初安装 PHP 时所做的那样。

现在,要检查安装了哪个版本的 PHP,请输入以下命令:

**php –v**

在我们的情况下,安装了 PHP 7.0.3。

  1. 要启动、停止和重新启动 PHP,请在终端中输入以下命令:
**sudo systemctl start php-fpm**
**sudo systemctl restart php-fpm**
**sudo systemctl stop php-fpm**

  1. 现在,让我们配置 NGINX 以使用 PHP FPM。使用vinano或您选择的任何其他编辑器打开位于/etc/Nginx/conf.d/default.conf的默认 NGINX 虚拟主机文件。现在,请确保服务器块中设置了两个选项,如下所示:
server {
    listen  80;
    server_name  localhost;
 **root   /usr/share/nginx/html;**
**index  index.php index.html index.htm;**

root选项表示我们的网站源代码文件将放置的 Web 文档根目录。Index 表示将与扩展名一起加载的默认文件。如果找到任何这些文件,默认情况下将执行它们,而不管 URL 中提到的任何文件。

  1. NGINX 中的下一个配置是用于 PHP 的位置块。以下是 PHP 的配置:
location ~ \.php$ {
    try_files $uri =404;
    fastcgi_split_path_info ^(.+\.php)(/.+)$;
    fastcgi_pass 127.0.0.1:9000;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME$document_root$fastcgi_script_name;
      include fastcgi_params;
    }

上述块是最重要的配置,因为它使 NGINX 能够与 PHP 通信。行fastcgi_pass 127.0.0.1:9000告诉 NGINX,PHP FPM 可以在端口9000上的127.0.0.1环回 IP 上访问。其余细节与我们讨论 Debian 和 Ubuntu 的内容相同。

  1. 现在,为了测试我们的安装,我们将创建一个名为info.php的文件,其中包含以下内容:
<?php
  phpinfo();
?>

保存文件后,输入http://server_ip/info.phphttp://hostname/info.php,我们将得到一个包含有关 PHP 的完整信息的页面。如果您看到此页面,恭喜!PHP 与 NGINX 一起运行。

安装 Percona Server

  1. 现在,我们将在 CentOS 上安装 Percona Server。安装过程相同,只是它有一个单独的存储库。要将 Percona Server 存储库添加到 CentOS,请在终端中执行以下命令:
**sudo yum install http://www.percona.com/downloads/percona-release/redhat/0.1-3/percona-release-0.1-3.noarch.rpm**

存储库安装完成后,将显示一条消息,指示安装完成。

  1. 现在,为了测试存储库,发出以下命令,它将列出所有可用的 Percona 软件包:
**sudo yum search percona**

  1. 要安装 Percona Server 5.5,请在终端中发出以下命令:
**sudo yum install Percona-Server-server-55**

安装过程将开始。其余的过程与 Debian/Ubuntu 相同。

  1. 安装完成后,将看到完成消息。

设置 Vagrant

Vagrant 是开发人员用于开发环境的工具。Vagrant 提供了一个简单的命令行界面,用于设置带有所有所需工具的虚拟机。Vagrant 使用称为 Vagrant Boxes 的框,可以具有 Linux 操作系统和根据此框的其他工具。Vagrant 支持 Oracle VM VirtualBox 和 VMware。为了本书的目的,我们将使用 VirtualBox,我们假设它也安装在您的机器上。

Vagrant 有几个用于 PHP 7 的框,包括 Laravel Homestead 和 Rasmus PHP7dev。因此,让我们开始配置 Windows 和 Mac OS X 上的 Rasmus PHP7dev 框。

注意

我们假设我们的机器上都安装了 VirutalBox 和 Vagrant。可以从www.virtualbox.org/wiki/Downloads下载 VirtualBox,可以从www.vagrantup.com/downloads.html下载 Vagrant,适用于不同的平台。有关 Rasmus PHP7dev VagrantBox 的详细信息,请访问github.com/rlerdorf/php7dev

执行以下步骤:

  1. 在其中一个驱动器中创建一个目录。例如,我们在D驱动器中创建了一个php7目录。然后,通过按住Shift键,右键单击,然后选择在此处打开命令窗口,直接在此特定文件夹中打开命令行。

  2. 现在,在命令窗口中输入以下命令:

**vagrant box add rasmus/php7dev**

它将开始下载 Vagrant 框,如下截图所示:

设置 Vagrant

  1. 现在,当下载完成时,我们需要初始化它,以便为我们配置并将该框添加到 VirtualBox 中。在命令窗口中输入以下命令:
**vagrant init rasmus/php7dev**

这将开始将框添加到 VirtualBox 并对其进行配置。完成该过程后,将显示一条消息,如下截图所示:

设置 Vagrant

  1. 现在,输入以下命令,这将完全设置 Vagrant 框并启动它:
**vagrant up**

这个过程会花一点时间。当完成后,你的框已经准备好并且可以使用了。

  1. 现在,启动后的第一件事是更新所有内容。这个框使用 Ubuntu,所以在相同的php7dev目录中打开命令窗口,并输入以下命令:
**vagrant ssh**

它将通过 SSH 将我们连接到虚拟机。

注意

在 Windows 中,如果 SSH 未安装或未在PATH变量中配置,可以使用 PuTTY。可以从www.chiark.greenend.org.uk/~sgtatham/putty/download.html下载。对于 PuTTY,主机将是127.0.0.1,端口将是2222Vagrant是 SSH 的用户名和密码。

  1. 当我们登录到框的操作系统时,输入以下命令来更新系统:
**sudo apt-get update**
**sudo apt-get upgrade**

这将更新核心系统、NGINX、MySQL、PHP 7 和其他安装的工具,如果有新版本的话。

  1. 现在,框已经准备好用于开发目的。可以通过在浏览器窗口中输入其 IP 地址来访问框。要找到框的 IP 地址,在 SSH 连接的命令窗口中输入以下命令:
**sudo ifconfig**

这将显示一些细节。在那里找到 IPv4 的细节并取得框的 IP。

总结

在本章中,我们为开发目的配置了不同的环境。我们在 Windows 机器上安装了 NGINX 和 PHP 7。我们还配置了 Debian/Ubuntu 并安装了 NGINX、PHP 和 Percona Server 5.5。然后,我们配置了 CentOS 并安装了 NGINX、PHP 和 Percona Server 5.5。最后,我们讨论了如何在 Windows 机器上配置 Vagrant Box。

在下一章中,我们将学习 PHP 7 的新功能,比如类型提示、命名空间分组和声明、太空船操作符等其他功能。

第二章:PHP 7 中的新功能

PHP 7 引入了一些新功能,可以帮助程序员编写高性能和有效的代码。此外,一些老式的功能已经完全移除,如果使用 PHP 7 将抛出错误。现在大多数致命错误都是异常,因此 PHP 不会再显示丑陋的致命错误消息;相反,它将通过可用的详细信息进行异常处理。

在本章中,我们将涵盖以下主题:

  • 类型提示

  • 命名空间和组使用声明

  • 匿名类

  • 旧式构造函数弃用

  • 太空船运算符

  • 空合并运算符

  • 统一变量语法

  • 其他更改

面向对象编程特性

PHP 7 引入了一些新的面向对象编程功能,使开发人员能够编写干净而有效的代码。在本节中,我们将讨论这些功能。

类型提示

在 PHP 7 之前,不需要声明传递给函数或类方法的参数的数据类型。此外,也不需要提及返回数据类型。任何数据类型都可以传递给函数或方法,并从函数或方法返回。这是 PHP 中的一个巨大问题,不清楚应该传递或接收哪些数据类型。为了解决这个问题,PHP 7 引入了类型提示。目前,引入了两种类型提示:标量和返回类型提示。这些将在以下部分讨论。

类型提示是面向对象编程和过程式 PHP 中的一个特性,因为它可以用于过程式函数和对象方法。

标量类型提示

PHP 7 使得可以为整数、浮点数、字符串和布尔值的函数和方法使用标量类型提示。让我们看下面的例子:

class Person
{
  public function age(int $age)
  {
    return $age;
    }

  public function name(string $name)
  {
    return $name;
    }

  public function isAlive(bool $alive)
  {
    return $alive;
    }

}

$person = new Person();
echo $person->name('Altaf Hussain');
echo $person->age(30);
echo $person->isAlive(TRUE);

在上面的代码中,我们创建了一个Person类。我们有三种方法,每种方法接收不同的参数,其数据类型在上面的代码中进行了定义。如果运行上面的代码,它将正常工作,因为我们将为每种方法传递所需的数据类型。

年龄可以是浮点数,例如30.5岁;因此,如果我们将浮点数传递给age方法,它仍然可以工作,如下所示:

echo $person->age(30.5);

为什么?这是因为默认情况下,标量类型提示是非限制性的。这意味着我们可以将浮点数传递给期望整数的方法。

为了使其更加严格,可以将以下单行代码放在文件的顶部:

declare(strict_types = 1);

现在,如果我们将浮点数传递给age函数,我们将得到一个未捕获的类型错误,这是一个致命错误,告诉我们Person::age必须是给定浮点数的整数类型。如果我们将字符串传递给不是字符串类型的方法,将生成类似的错误。考虑以下例子:

echo $person->isAlive('true');

由于传递了字符串,上面的代码将生成致命错误。

返回类型提示

PHP 7 的另一个重要特性是能够为函数或方法定义返回数据类型。它的行为与标量类型提示的行为相同。让我们稍微修改我们的Person类以理解返回类型提示,如下所示:

class Person
{
  public function age(float $age) : string
  {
    return 'Age is '.$age;
  }

  public function name(string $name) : string
  {
    return $name;
    }

  public function isAlive(bool $alive) : string
  {
    return ($alive) ? 'Yes' : 'No';
  }

}

类中的更改已经突出显示。使用:数据类型语法定义了返回类型。返回类型是否与标量类型相同并不重要。只要它们与各自的数据类型匹配即可。

现在,让我们尝试一个带有对象返回类型的例子。考虑之前的Person类,并向其添加一个getAddress方法。此外,我们将在同一个文件中添加一个新的类Address,如下所示:

**class Address** 
**{**
 **public function getAddress()**
 **{**
 **return ['street' => 'Street 1', 'country' => 'Pak'];**
 **}**
**}**

class Person
{
  public function age(float $age) **: string**
  {
    return 'Age is '.$age;
  }

  public function name(string $name) **: string**
  {
    return $name;
  }

  public function isAlive(bool $alive) : string
  {
    return ($alive) ? 'Yes' : 'No';
  }

 **public function getAddress() : Address**
 **{**
 **return new Address();**
 **}**
}

添加到Person类和新的Address类的附加代码已经突出显示。现在,如果我们调用Person类的getAddress方法,它将完美地工作,不会抛出错误。然而,假设我们改变返回语句,如下所示:

public function getAddress() : Address
{
  return ['street' => 'Street 1', 'country' => 'Pak'];
}

在这种情况下,上面的方法将抛出类似于以下内容的未捕获异常:

Fatal error: Uncaught TypeError: Return value of Person::getAddress() must be an instance of Address, array returned

这是因为我们返回的是一个数组,而不是一个Address对象。现在,问题是:为什么使用类型提示?使用类型提示的重要优势是它将始终避免意外地传递或返回错误和意外的数据到方法或函数。

如前面的例子所示,这使得代码清晰,通过查看方法的声明,可以准确知道应该传递哪些数据类型到每个方法,以及通过查看每个方法的代码或注释,返回什么类型的数据。

命名空间和组使用声明

在一个非常庞大的代码库中,类被划分到命名空间中,这使得它们易于管理和使用。但是,如果一个命名空间中有太多的类,而我们需要使用其中的 10 个类,那么我们必须为所有这些类输入完整的使用语句。

注意

在 PHP 中,不需要根据其命名空间将类分成子文件夹,这与其他编程语言不同。命名空间只是提供类的逻辑分离。但是,我们不限于根据我们的命名空间将我们的类放在子文件夹中。

例如,我们有一个Publishers/Packt命名空间和类BookEbookVideoPresentation。此外,我们有一个functions.php文件,其中包含我们的常规函数,并且在相同的Publishers/Packt命名空间中。另一个文件constants.php包含应用程序所需的常量值,并且在相同的命名空间中。每个类和functions.phpconstants.php文件的代码如下:

//book.php
namespace Publishers\Packt;

class Book 
{
  public function get() : string
  {
    return get_class();
  }
}

现在,Ebook类的代码如下:

//ebook.php
namespace Publishers\Packt;

class Ebook 
{
  public function get() : string
  {
    return get_class();
  }
}

Video类的代码如下:

//presentation.php
namespace Publishers\Packt;

class Video 
{
  public function get() : string
  {
    return get_class();
  }
}

同样,presentation类的代码如下:

//presentation.php
namespace Publishers\Packt;

class Presentation 
{
  public function get() : string
  {
    return get_class();
  }
}

所有四个类都有相同的方法,这些方法使用 PHP 内置的get_class()函数返回类的名称。

现在,将以下两个函数添加到functions.php文件中:

//functions.php

namespace Publishers\Packt;

function getBook() : string
{
  return 'PHP 7';
}
function saveBook(string $book) : string
{
  return $book.' is saved';
}

现在,让我们将以下代码添加到constants.php文件中:

//constants.php

namespace Publishers/Packt;

const COUNT = 10;
const KEY = '123DGHtiop09847';
const URL = 'https://www.Packtpub.com/';

functions.phpconstants.php中的代码是不言自明的。请注意,每个文件顶部都有一行namespace Publishers/Packt,这使得这些类、函数和常量属于这个命名空间。

现在,有三种方法可以使用类、函数和常量。让我们逐一考虑每一种。

看一下下面的代码:

//Instantiate objects for each class in namespace

$book = new Publishers\Packt\Book();
$ebook = new Publishers\Packt\Ebook();
$video = new Publishers\Packt\Video();
$presentation = new Publishers\Packt\Presentation();

//Use functions in namespace

echo Publishers/Packt/getBook();
echo Publishers/Packt/saveBook('PHP 7 High Performance');

//Use constants

echo Publishers\Packt\COUNT;
echo Publishers\Packt\KEY;

在前面的代码中,我们直接使用命名空间名称创建对象或使用函数和常量。代码看起来不错,但是有点混乱。命名空间到处都是,如果我们有很多命名空间,它看起来会很丑陋,可读性也会受到影响。

注意

我们在之前的代码中没有包含类文件。可以使用include语句或 PHP 的__autoload函数来包含所有文件。

现在,让我们重新编写前面的代码,使其更易读,如下所示:

use Publishers\Packt\Book;
use Publishers\Packt\Ebook;
use Publishers\Packt\Video;
use Publishers\Packt\Presentation;
use function Publishers\Packt\getBook;
use function Publishers\Packt\saveBook;
use const Publishers\Packt\COUNT;
use const Publishers\Packt\KEY;

$book = new Book();
$ebook = new Ebook(();
$video = new Video();
$pres = new Presentation();

echo getBook();
echo saveBook('PHP 7 High Performance');

echo COUNT; 
echo KEY;

在前面的代码中,我们在顶部使用了 PHP 语句来指定命名空间中特定的类、函数和常量。但是,我们仍然为每个类、函数和/或常量编写了重复的代码行。这可能导致我们在文件顶部有大量的使用语句,并且整体冗长度不好。

为了解决这个问题,PHP 7 引入了组使用声明。有三种类型的组使用声明:

  • 非混合使用声明

  • 混合使用声明

  • 复合使用声明

非混合组使用声明

假设我们在一个命名空间中有不同类型的特性,如类、函数和联系人。在非混合组使用声明中,我们使用use语句分别声明它们。为了更好地理解它,请看下面的代码:

use Publishers\Packt\{ Book, Ebook, Video, Presentation };
use function Publishers\Packt\{ getBook, saveBook };
use const Publishers\Packt\{ COUNT, KEY };

在一个命名空间中,我们有三种特性:类、函数和常量。因此,我们使用单独的组use声明语句来使用它们。现在,代码看起来更清晰、有组织、可读性更好,而且不需要太多重复输入。

混合组使用声明

在这个声明中,我们将所有类型合并到一个use语句中。看看以下代码:

use Publishers\Packt\{ 
  Book,
  Ebook,
  Video,
  Presentation,
  function getBook,
  function saveBook,
  const COUNT,
  const KEY
};

复合命名空间声明

为了理解复合命名空间声明,我们将考虑以下标准。

假设我们在Publishers\Packt\Paper命名空间中有一个Book类。此外,我们在Publishers\Packt\Electronic命名空间中有一个Ebook类。VideoPresentation类位于Publishers\Packt\Media命名空间中。因此,为了使用这些类,我们将使用以下代码:

use Publishers\Packt\Paper\Book;
use Publishers\Packt\Electronic\Ebook;
use Publishers\Packt\Media\{Video,Presentation};

在复合命名空间声明中,我们可以使用前面的命名空间,如下所示:

use Publishers\Packt\{
  Paper\Book,
  Electronic\Ebook,
  Media\Video,
  Media\Presentation
};

这更加优雅和清晰,如果命名空间名称很长,它不需要额外的输入。

匿名类

匿名类是在声明和实例化同时进行的类。它没有名称,并且可以具有普通类的全部特性。当需要执行一次性的小任务并且不需要为此编写完整的类时,这些类非常有用。

注意

在创建匿名类时,它没有名称,但在 PHP 内部使用基于内存块中的地址的唯一引用来命名。例如,匿名类的内部名称可能是class@0x4f6a8d124

这个类的语法与命名类的语法相同,但类的名称缺失,如下所示:

new class(argument) { definition };

让我们看一个匿名类的基本和非常简单的例子,如下所示:

$name = new class() {
  public function __construct()
  {
    echo 'Altaf Hussain';
  }
};

前面的代码只会显示Altaf Hussain

参数也可以传递给匿名类构造函数,如下所示的代码:

$name = new class('Altaf Hussain') {
  public function __construct(string $name)
  {
    echo $name;
  }
};

这将给我们与第一个示例相同的输出。

匿名类可以扩展其他类,并且具有与普通命名类相同的父子类功能。让我们看另一个例子;看看以下内容:

class Packt
{
  protected $number;

  public function __construct()
  {
    echo 'I am parent constructor';
  }

  public function getNumber() : float
  {
    return $this->number;
  }
}

$number = new class(5) extends packt
{
  public function __construct(float $number)
  {
    parent::__construct();
    $this->number = $number;
  }
};

echo $number->getNumber();

前面的代码将显示I am parent constructor5。可以看到,我们扩展Packt类的方式与我们扩展命名类的方式相同。此外,我们可以在匿名类中访问publicprotected属性和方法,并且可以使用匿名类对象访问公共属性和方法。

匿名类也可以实现接口,与命名类一样。让我们首先创建一个接口。运行以下代码:

interface Publishers
{
  public function __construct(string $name, string $address);
  public function getName();
  public function getAddress();
}

现在,让我们修改我们的Packt类如下。我们添加了突出显示的代码:

class Packt
{
  protected $number;
  protected $name;
  protected $address;
  public function …
}

代码的其余部分与第一个Packt类相同。现在,让我们创建我们的匿名类,它将实现前面代码中创建的Publishers接口,并扩展新的Packt类,如下所示:

$info = new class('Altaf Hussain', 'Islamabad, Pakistan')extends packt implements Publishers
{
  public function __construct(string $name, string $address)
  {
    $this->name = $name;
    $this->address = $address;
  }

  public function getName() : string
  {
  return $this->name;
  }

  public function getAddress() : string
  {
  return $this->address;
  }
}

echo $info->getName(). ' '.$info->getAddress();

前面的代码是不言自明的,并将输出Altaf Hussain以及地址。

可以在另一个类中使用匿名类,如下所示:

class Math
{
  public $first_number = 10;
  public $second_number = 20;

  public function add() : float
  {
    return $this->first_number + $this->second_number;
  }

  public function multiply_sum()
  {
    return new class() extends Math
    {
      public function multiply(float $third_number) : float
      {
        return $this->add() * $third_number;
      }
    };
  }
}

$math = new Math();
echo $math->multiply_sum()->multiply(2);

前面的代码将返回60。这是如何发生的?Math类有一个multiply_sum方法,返回匿名类的对象。这个匿名类是从Math类扩展出来的,并且有一个multiply方法。因此,我们的echo语句可以分为两部分:第一部分是$math->multiply_sum(),它返回匿名类的对象,第二部分是->multiply(2),在这里我们链接了这个对象来调用匿名类的multiply方法,并传入值2

在前面的情况下,Math类可以被称为外部类,匿名类可以被称为内部类。但是,请记住,内部类不需要扩展外部类。在前面的例子中,我们扩展它只是为了确保内部类可以通过扩展外部类来访问外部类的属性和方法。

旧式构造函数弃用

回到 PHP 4 时,类的构造函数与类的同名方法。它仍然被使用,并且在 PHP 的 5.6 版本之前是有效的。然而,现在在 PHP 7 中,它已被弃用。让我们看一个示例,如下所示:

class Packt
{
  public function packt()
  {
    echo 'I am an old style constructor';
  }
}

$packt = new Packt();

前面的代码将显示输出我是一个旧式构造函数,并附带一个弃用消息,如下所示:

Deprecated: Methods with the same name as their class will not be constructors in a future version of PHP; Packt has a deprecated constructor in…

然而,仍然调用旧式构造函数。现在,让我们向我们的类添加 PHP __construct方法,如下所示:

class Packt
{
  public function __construct()
  {
    echo 'I am default constructor';
  }

  public function packt()
  {
    echo 'I am just a normal class method';
  }
}

$packt = new Packt();
$packt->packt();

在前面的代码中,当我们实例化类的对象时,会调用普通的__construct构造函数。packt()方法不被视为普通的类方法。

注意

旧式构造函数已经被弃用,这意味着它们在 PHP 7 中仍然可以工作,并且会显示一个弃用的消息,但它将在即将推出的版本中被移除。最好不要使用它们。

可抛出接口

PHP 7 引入了一个基本接口,可以作为可以使用throw语句的每个对象的基础。在 PHP 中,异常和错误可能会发生。以前,异常可以被处理,但无法处理错误,因此,任何致命错误都会导致整个应用程序或应用程序的一部分停止。为了使错误(最致命的错误)也可以被捕获,PHP 7 引入了throwable接口,它由异常和错误都实现。

注意

我们创建的 PHP 类无法实现可抛出接口。如果需要,这些类必须扩展异常。

我们都知道异常,因此在这个主题中,我们只讨论可以处理丑陋的致命错误的错误。

错误

现在几乎所有致命错误都可以抛出错误实例,类似于异常,错误实例可以使用try/catch块捕获。让我们来看一个简单的例子:

function iHaveError($object)
{
  return $object->iDontExist();
  {

//Call the function
iHaveError(null);
echo "I am still running";

如果执行前面的代码,将显示致命错误,应用程序将停止,并且最终不会执行echo语句。

现在,让我们将函数调用放在try/catch块中,如下所示:

try 
{
  iHaveError(null);
} catch(Error $e)
{
  //Either display the error message or log the error message
  echo $e->getMessage();
}

echo 'I am still running';

现在,如果执行前面的代码,catch体将被执行,之后,应用程序的其余部分将继续运行。在前面的情况下,echo语句将被执行。

在大多数情况下,错误实例将被抛出,用于最致命的错误,但对于一些错误,将抛出错误的子实例,例如TypeErrorDivisionByZeroErrorParseError等。

现在,让我们看一个以下示例中的DivisionByZeroError异常:

try
{
  $a = 20;
  $division = $a / 20;
} catch(DivisionByZeroError $e) 
{
  echo $e->getMessage();
}

在 PHP 7 之前,前面的代码会发出有关除以零的警告。然而,现在在 PHP 7 中,它将抛出一个DivisionByZeroError,可以处理。

新运算符

PHP 7 引入了两个有趣的运算符。这些运算符可以帮助编写更少、更清晰的代码,因此最终的代码将比使用传统运算符更易读。让我们来看看它们。

太空船运算符(<=>)

太空船或组合比较运算符对于比较值(字符串、整数、浮点数等)、数组和对象非常有用。这个运算符只是一个包装器,执行与三个比较运算符==<>相同的任务。这个运算符也可以用于为usortuasortuksort的回调函数编写干净和少量的代码。这个运算符的工作方式如下:

  • 如果左右两侧的操作数相等,则返回 0

  • 如果右操作数大于左操作数,则返回-1

  • 如果左操作数大于右操作数,则返回 1

让我们通过比较整数、字符串、对象和数组来看几个例子,并注意结果:

$int1 = 1;
$int2 = 2;
$int3 = 1;

echo $int1 <=> $int3; //Returns 0
echo '<br>';
echo $int1 <=> $int2; //Returns -1
echo '<br>';
echo $int2 <=> $int3; //Returns 1

运行前面的代码,你将得到类似以下的输出:

0
-1
1

在第一个比较中,我们比较了$int1$int3,两者都相等,所以它将返回0。在第二个比较中,比较了$int1$int2,它将返回-1,因为右操作数($int2)大于左操作数($int1)。最后,第三个比较将返回1,因为左操作数($int2)大于右操作数($int3)。

上面是一个简单的例子,我们在其中比较了整数。我们可以以相同的方式检查字符串、对象和数组,并且它们是按照标准的 PHP 方式进行比较的。

注意

关于<=>运算符的一些例子可以在wiki.php.net/rfc/combined-comparison-operator找到。这是一个 RFC 出版物,其中有关于其用法的更多有用细节。

这个运算符在对数组进行排序时更有用。看看下面的代码:

Function normal_sort($a, $b) : int 
{
  if( $a == $b )
    return 0;
  if( $a < $b )
    return -1;
  return 1;
}

function space_sort($a, $b) : int
{
  return $a <=> $b;
}

$normalArray = [1,34,56,67,98,45];

//Sort the array in asc
usort($normalArray, 'normal_sort');

foreach($normalArray as $k => $v)
{
  echo $k.' => '.$v.'<br>';
}

$spaceArray = [1,34,56,67,98,45];

//Sort it by spaceship operator
usort($spaceArray, 'space_sort');

foreach($spaceArray as $key => $value)
{
  echo $key.' => '.$value.'<br>';
}

在前面的代码中,我们使用了两个函数来对具有相同值的两个不同数组进行排序。$normalArray数组通过normal_sort函数进行排序,normal_sort函数使用if语句来比较值。第二个数组$spaceArray具有与$normalArray相同的值,但是这个数组通过space_sort函数进行排序,space_sort函数使用了太空船运算符。两个数组排序的最终结果是相同的,但回调函数中的代码是不同的。normal_sort函数有if语句和多行代码,而space_sort函数只有一行代码,就是这样!space_sort函数的代码更清晰,不需要多个 if 语句。

空合并运算符(??)

我们都知道三元运算符,并且大多数时候都会使用它们。三元运算符只是if-else语句的单行替代。例如,考虑以下代码:

$post = ($_POST['title']) ? $_POST['title'] : NULL;

如果$_POST['title']存在,则$post变量将被赋予它的值;否则,将被赋予NULL。但是,如果$_POST$_POST['title']不存在或为 null,则 PHP 将发出未定义的索引的通知。为了解决这个通知,我们需要使用isset函数,如下所示:

$post = isset($_POST['title']) ? $_POST['title'] : NULL;

大多数情况下,看起来都很好,但当我们需要在多个地方检查值时,特别是在使用 PHP 作为模板语言时,情况就会变得非常棘手。

在 PHP 7 中,引入了合并运算符,它很简单,如果第一个操作数(左操作数)存在且不为 null,则返回其值。否则,返回第二个操作数(右操作数)。考虑以下例子:

$post = $_POST['title'] ?? NULL;

这个例子与前面的代码完全相似。合并运算符检查$_POST['title']是否存在。如果存在,运算符返回它;否则,返回NULL

这个运算符的另一个很棒的特性是它可以链接起来。以下是一个例子:

$title = $_POST['title'] ?? $_GET['title'] ?? 'No POST or GET';

根据定义,它将首先检查第一个操作数是否存在并返回它;如果不存在,它将返回第二个操作数。现在,如果第二个操作数上使用了另一个合并运算符,同样的规则将被应用,如果左操作数存在,则返回它的值。否则,将返回右操作数的值。

因此,上面的代码与以下代码相同:

If(isset($_POST['title']))
  $title = $_POST['title'];
elseif(isset($_GET['title']))
  $title = $_GET['title'];
else
  $title = 'No POST or GET';

正如前面的例子中所示,合并运算符可以帮助编写干净、简洁和更少的代码。

统一变量语法

大多数情况下,我们可能会遇到这样一种情况,即方法、变量或类名存储在其他变量中。看看下面的例子:

$objects['class']->name;

在前面的代码中,首先会解释$objects['class'],然后会解释属性名。如前面的例子所示,变量通常是从左到右进行评估的。

现在,考虑以下情景:

$first = ['name' => 'second'];
$second = 'Howdy';

echo $$first['name'];

在 PHP 5.x 中,这段代码将被执行,并且输出将是Howdy。然而,这与从左到右的表达式评估是不一致的。这是因为$$first应该首先被评估,然后是索引名称,但在前面的情况下,它被评估为${$first['name']}。很明显,变量语法不一致,可能会造成混淆。为了避免这种不一致,PHP 7 引入了一种称为统一变量语法的新语法。如果不使用这种语法,前面的例子将引起注意,并且不会产生期望的结果。为了使其在 PHP 7 中工作,应添加大括号,如下所示:

echo ${$first['name']};

现在,让我们举一个例子,如下所示:

class Packt
{
  public $title = 'PHP 7';
  public $publisher = 'Packt Publisher';

  public function getTitle() : string
  {
    return $this->title;
  }

  public function getPublisher() : string
  {
    return $this->publisher;
  }
}

$mthods = ['title' => 'getTitle', 'publisher' => 'getPublisher'];
$object = new Packt();
echo 'Book '.$object->$methods['title']().' is published by '.$object->$methods['publisher']();

如果在 PHP 5.x 中执行上述代码,它将正常工作并输出我们想要的结果。但是,如果我们在 PHP 7 中执行此代码,将会产生致命错误。错误将出现在代码的最后一行,这是突出显示的。PHP 7 将首先尝试评估$object->$method。之后,它将尝试评估['title'];依此类推;这是不正确的。

为了使其在 PHP 7 中工作,应添加大括号,如下所示:

echo 'Book '.$object**->{$methods['title']}**().' is published by '.$object->**{$methods['publisher']}**();

在进行了前面提到的更改之后,我们将得到我们想要的输出。

其他功能和更改

PHP 7 还引入了一些其他新功能和小的更改,比如数组常量的新语法、switch语句中的多个默认情况、session_start中的选项数组等。让我们也看看这些。

常量数组

从 PHP 5.6 开始,可以使用const关键字初始化常量数组,如下所示:

const STORES = ['en', 'fr', 'ar'];

现在,从 PHP 7 开始,可以使用define函数初始化常量数组,如下所示:

define('STORES', ['en', 'fr', 'ar']);

在 switch 语句中的多个默认情况

在 PHP 7 之前,允许在 switch 语句中有多个默认情况。请看下面的例子:

switch(true)
{
  default: 
    echo 'I am first one';
    break;
  default: 
    echo 'I am second one';
}

在 PHP 7 之前,允许上述代码,但在 PHP 7 中,这将导致类似以下的致命错误:

Fatal error: Switch statements may only contain one default clause in…

session_start函数的选项数组

在 PHP 7 之前,每当我们需要启动会话时,我们只是使用session_start()函数。这个函数不带任何参数,并且使用php.ini中定义的所有设置。现在,从 PHP 7 开始,可以传递一个可选的选项数组,它将覆盖php.ini文件中的会话设置。

一个简单的例子如下所示:

session_start([
  'cookie_lifetime' => 3600,
  'read_and_close'  => true
]);

如前面的例子所示,很容易覆盖会话的php.ini设置。

过滤反序列化函数

序列化和反序列化对象是常见的做法。然而,PHP 的unserialize()函数并不安全,因为它没有任何过滤选项,并且可以反序列化任何类型的对象。PHP 7 在这个函数中引入了过滤。默认的过滤选项是反序列化所有类或类型的对象。其基本工作如下:

$result = unserialize($object, ['allowed_classes' => ['Packt', 'Books', 'Ebooks']]);

总结

在本章中,我们讨论了新的面向对象编程功能,如类型提示、匿名类、可抛出接口、命名空间的组合使用声明以及两个重要的新运算符,太空船或组合比较运算符和 null 合并运算符。此外,我们还讨论了统一的变量语法和其他一些新功能,如联系数组定义的新语法、session_start()函数的选项数组以及在 switch 语句中删除多个默认情况。

在下一章中,我们将讨论如何提高应用程序的性能。我们将讨论 Apache 和 NGINX 以及它们的不同设置以提高性能。

我们将讨论不同的 PHP 设置,以提高其性能。还将讨论 Google 页面速度模块、CSS/JavaScript 合并和压缩、CDN 等。

第三章:提高 PHP 7 应用程序性能

PHP 7 已经完全重写,基于PHP Next GenerationphpngPHPNG)进行性能优化。然而,总是有更多的方法来提高应用程序的性能,包括编写高性能代码、使用最佳实践、Web 服务器优化、缓存等。在本章中,我们将讨论以下列出的这些优化:

  • NGINX 和 Apache

  • HTTP 服务器优化

  • 内容交付网络(CDN)

  • JavaScript/CSS 优化

  • 完整页面缓存

  • Varnish

  • 基础设施

NGINX 和 Apache

有太多的 HTTP 服务器软件可用,每个都有其优缺点。最常用的两个 HTTP 服务器是 NGINX 和 Apache。让我们来看看它们两个,并注意哪一个更适合我们的需求。

Apache

Apache 是最广泛使用的 HTTP 服务器,大多数管理员都喜爛它。管理员选择它是因为它的灵活性、广泛的支持、强大的功能以及对大多数解释性语言(如 PHP)的模块支持。由于 Apache 可以处理大量的解释性语言,它不需要与其他软件通信来满足请求。Apache 可以在 prefork(进程在线程之间生成)、worker(线程在进程之间生成)和事件驱动(与 worker 进程相同,但为keep-alive连接设置专用线程和为活动连接设置单独线程)中处理请求;因此,它提供了更大的灵活性。

正如前面讨论的,每个请求将由单个线程或进程处理,因此 Apache 消耗了太多资源。当涉及高流量应用程序时,Apache 可能会减慢应用程序的速度,因为它不提供良好的并发处理支持。

NGINX

NGINX 是为解决高流量应用程序的并发问题而构建的。NGINX 提供了异步、事件驱动和非阻塞的请求处理。由于请求是异步处理的,NGINX 不会等待请求完成以阻塞资源。

NGINX 创建工作进程,每个工作进程可以处理成千上万的连接。因此,少量进程可以同时处理高流量。

NGINX 不提供任何解释性语言的内置支持。它依赖外部资源来实现这一点。这也是好的,因为处理是在 NGINX 之外进行的,NGINX 只处理连接和请求。大多数情况下,NGINX 被认为比 Apache 更快。在某些情况下,例如处理静态内容(提供图像、.css.js文件等),这可能是真的,但在当前高性能服务器中,Apache 并不是问题;PHP 是瓶颈。

注意

Apache 和 NGINX 都适用于各种操作系统。在本书中,我们将使用 Debian 和 Ubuntu,因此所有文件路径都将根据这些操作系统进行提及。

如前所述,我们将在本书中使用 NGINX。

HTTP 服务器优化

每个 HTTP 服务器都提供了一些功能,可以用来优化请求处理和提供内容。在本节中,我们将分享一些适用于 Apache 和 NGINX 的技术,用来优化 Web 服务器并提供最佳性能和可伸缩性。通常,应用这些优化后,需要重新启动 Apache 或 NGINX。

缓存静态文件

大多数静态文件,如图像、.css.js和字体,不经常更改。因此,最佳做法是在最终用户的机器上缓存这些静态文件。为此,Web 服务器会在响应中添加特殊标头,告诉用户浏览器将静态内容缓存一段时间。以下是 Apache 和 NGINX 的配置代码。

Apache

让我们来看看 Apache 配置如何缓存以下静态内容:

<FilesMatch "\.(ico|jpg|jpeg|png|gif|css|js|woff)$">
  Header set Cache-Control "max-age=604800, public
</FileMatch>

在前面的代码中,我们使用了 Apache 的FilesMatch指令来匹配文件的扩展名。如果请求了所需的扩展名文件,Apache 会将头设置为缓存控制七天。然后浏览器会将这些静态文件缓存七天。

NGINX

以下配置可以放置在/etc/nginx/sites-available/your-virtual-host-conf-file中:

Location ~* .(ico|jpg|jpeg|png|gif|css|js|woff)$ {
  Expires 7d;
}

在前面的代码中,我们使用了 NGINX 的Location块和不区分大小写的修饰符(~*)来设置七天的Expires。此代码将为所有定义的文件类型设置七天的缓存控制头。

进行这些设置后,请求的响应头将如下所示:

NGINX

在前面的图中,可以清楚地看到.js文件是从缓存中加载的。它的缓存控制头设置为七天或 604,800 秒。到期日期也可以清楚地在expires头中注意到。到期日期后,浏览器将从服务器加载此.js文件,并根据缓存控制头中定义的持续时间再次缓存它。

HTTP 持久连接

在 HTTP 持久连接或 HTTP keep-alive 中,单个 TCP/IP 连接用于多个请求或响应。与正常连接相比,它具有巨大的性能改进,因为它只使用一个连接,而不是为每个单独的请求或响应打开和关闭连接。HTTP keep-alive 的一些好处如下:

  • 由于一次只打开了较少的 TCP 连接,并且对于后续的请求和响应不会打开新的连接,因为这些 TCP 连接用于它们,所以 CPU 和内存的负载减少了。

  • 在建立 TCP 连接后,减少了后续请求的延迟。当要建立 TCP 连接时,用户和 HTTP 服务器之间进行了三次握手通信。成功握手后,建立了 TCP 连接。在 keep-alive 的情况下,仅对初始请求进行一次握手以建立 TCP 连接,并且对于后续请求不进行握手或 TCP 连接的打开/关闭。这提高了请求/响应的性能。

  • 网络拥塞减少了,因为一次只打开了少量 TCP 连接到服务器。

除了这些好处,keep-alive 还有一些副作用。每个服务器都有并发限制,当达到或消耗此并发限制时,应用程序的性能可能会大幅下降。为了解决这个问题,为每个连接定义了超时,超过超时后,HTTP keep-alive 连接将自动关闭。现在,让我们在 Apache 和 NGINX 上都启用 HTTP keep-alive。

Apache

在 Apache 中,keep-alive 可以通过两种方式启用。您可以在.htaccess文件或 Apache 配置文件中启用它。

要在.htaccess文件中启用它,请在.htaccess文件中放置以下配置:

<ifModule mod_headers.c>
  Header set Connection keep-alive
</ifModule>

在前面的配置中,我们在.htaccess文件中将连接头设置为 keep-alive。由于.htaccess配置会覆盖配置文件中的配置,这将覆盖 Apache 配置文件中对 keep-alive 所做的任何配置。

要在 Apache 配置文件中启用 keep-alive 连接,我们必须修改三个配置选项。搜索以下配置并将值设置为示例中的值:

KeepAlive On
MaxKeepAliveRequests 100
KeepAliveTimeout 100

在前面的配置中,我们通过将KeepAlive的值设置为On来打开了 keep-alive 配置。

接下来是MaxKeepAliveRequests,它定义了同时向 Web 服务器保持活动连接的最大数量。在 Apache 中,默认值为 100,并且可以根据要求进行更改。为了获得高性能,应该保持这个值较高。如果设置为 0,将允许无限的 keep-alive 连接,这是不推荐的。

最后一个配置是KeepAliveTimeout,设置为 100 秒。这定义了在同一 TCP 连接上等待来自同一客户端的下一个请求的秒数。如果没有请求,则连接将关闭。

NGINX

HTTP keep-alive 是http_core模块的一部分,默认情况下已启用。在 NGINX 配置文件中,我们可以编辑一些选项,如超时。打开nginx配置文件,编辑以下配置选项,并将其值设置为以下值:

keepalive_requests 100
keepalive_timeout 100

keepalive_requests配置定义了单个客户端在单个 HTTP keep-alive 连接上可以发出的最大请求数。

keepalive_timeout配置是服务器需要等待下一个请求的秒数,直到关闭 keep-alive 连接。

GZIP 压缩

内容压缩提供了一种减少 HTTP 服务器传送的内容大小的方法。Apache 和 NGINX 都支持 GZIP 压缩,同样,大多数现代浏览器都支持 GZIP。启用 GZIP 压缩后,HTTP 服务器会发送压缩的 HTML、CSS、JavaScript 和大小较小的图像。这样,内容加载速度很快。

当浏览器发送有关自身支持 GZIP 压缩的信息时,Web 服务器才会通过 GZIP 压缩内容。通常,浏览器在Request标头中发送此类信息。

以下是启用 GZIP 压缩的 Apache 和 NGINX 代码。

Apache

以下代码可以放置在.htaccess文件中:

<IfModule mod_deflate.c>
SetOutputFilter DEFLATE
 #Add filters to different content types
AddOutputFilterByType DEFLATE text/html text/plain text/xml    text/css text/javascript application/javascript
    #Don't compress images
    SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png)$ no-gzip dont-   
    vary
</IfModule>

在上述代码中,我们使用了 Apache 的deflate模块来启用压缩。我们按类型进行过滤,只压缩特定类型的文件,如.html,纯文本,.xml.css.js。此外,在结束模块之前,我们设置了一个条件,不压缩图像,因为压缩图像会导致图像质量下降。

NGINX

如前所述,您必须将以下代码放置在 NGINX 的虚拟主机配置文件中:

gzip on;
gzip_vary on;
gzip_types text/plain text/xml text/css text/javascript application/x-javascript;
gzip_com_level 4;

在上述代码中,通过gzip on;行激活了 GZIP 压缩。gzip_vary on;行用于启用不同的标头。gzip_types行用于定义要压缩的文件类型。根据要求可以添加任何文件类型。gzip_com_level 4;行用于设置压缩级别,但要小心这个值;不要设置得太高。它的范围是 1 到 9,所以保持在中间。

现在,让我们检查压缩是否真的有效。在下面的截图中,请求发送到一个未启用 GZIP 压缩的服务器。下载或传输的最终 HTML 页面的大小为 59 KB:

NGINX

在启用 Web 服务器上的 GZIP 压缩后,传输的 HTML 页面的大小减小了 9.95 KB,如下截图所示:

NGINX

此外,还可以注意到加载内容的时间也减少了。因此,您的内容越小,页面加载速度就越快。

将 PHP 用作独立服务

Apache 使用mod_php模块来处理 PHP。这样,PHP 解释器集成到 Apache 中,所有处理都由这个 Apache 模块完成,这会消耗更多的服务器硬件资源。可以使用 PHP-FPM 与 Apache 一起使用,它使用 FastCGI 协议并在单独的进程中运行。这使得 Apache 可以处理 HTTP 请求处理,而 PHP 处理由 PHP-FPM 完成。

另一方面,NGINX 不提供任何内置支持或模块支持 PHP 处理。因此,在 NGINX 中,PHP 始终用作独立服务。

现在,让我们看看当 PHP 作为独立服务运行时会发生什么:Web 服务器不知道如何处理动态内容请求,并将请求转发到另一个外部服务,从而减少了 Web 服务器的处理负载。

禁用未使用的模块

Apache 和 NGINX 都内置了许多模块。在大多数情况下,您不需要其中一些模块。最好的做法是禁用这些模块。

最好的做法是制作一个启用的模块列表,逐个禁用这些模块,并重新启动服务器。之后,检查您的应用程序是否工作。如果工作正常,继续;否则,在应用程序再次停止正常工作之后,启用模块。

这是因为您可能会发现某个模块可能不需要,但其他一些有用的模块依赖于这个模块。因此,最好的做法是制作一个列表,启用或禁用模块,如前所述。

Apache

要列出为 Apache 加载的所有模块,请在终端中发出以下命令:

**sudo apachectl –M**

这个命令将列出所有加载的模块,如下截图所示:

Apache

现在,分析所有加载的模块,检查它们是否对应用程序有用,并禁用它们,如下所示。

打开 Apache 配置文件,找到加载所有模块的部分。这里包括一个示例:

LoadModule access_compat_module modules/mod_access_compat.so
LoadModule actions_module modules/mod_actions.so
LoadModule alias_module modules/mod_alias.so
LoadModule allowmethods_module modules/mod_allowmethods.so
LoadModule asis_module modules/mod_asis.so
LoadModule auth_basic_module modules/mod_auth_basic.so
#LoadModule auth_digest_module modules/mod_auth_digest.so
#LoadModule auth_form_module modules/mod_auth_form.so
#LoadModule authn_anon_module modules/mod_authn_anon.so

在前面加上#符号的模块是未加载的。因此,要在完整列表中禁用模块,只需放置一个#符号。#符号将注释掉该行,模块将不再加载。

NGINX

要检查 NGINX 编译时使用了哪些模块,请在终端中发出以下命令:

**sudo Nginx –V**

这将列出 NGINX 安装的完整信息,包括版本和 NGINX 编译时使用的模块。请查看以下截图:

NGINX

通常情况下,NGINX 只启用了 NGINX 工作所需的模块。要启用已安装的任何其他模块,我们可以在nginx.conf文件中为其添加一些配置,但是没有单一的方法来禁用任何 NGINX 模块。因此,最好搜索特定模块,并查看 NGINX 网站上的模块页面。在那里,我们可以找到有关特定模块的信息,如果可用,我们可以找到有关如何禁用和配置该模块的信息。

Web 服务器资源

每个 Web 服务器都有自己的通用最佳设置。但是,这些设置可能不适用于您当前的服务器硬件。Web 服务器硬件上最大的问题是 RAM。服务器的 RAM 越多,Web 服务器就能处理的请求就越多。

NGINX

NGINX 提供了两个变量来调整资源,即worker_processesworker_connectionsworker_processes设置决定了应该运行多少个 NGINX 进程。

现在,我们应该使用多少worker_processes资源?这取决于服务器。通常情况下,每个处理器核心使用一个工作进程。因此,如果您的服务器处理器有四个核心,这个值可以设置为 4。

worker_connections的值显示每秒每个worker_processes设置的连接数。简单地说,worker_connections告诉 NGINX 可以处理多少个同时请求。worker_connections的值取决于系统处理器核心。要找出 Linux 系统(Debian/Ubuntu)上核心的限制,请在终端中发出以下命令:

**Ulimit –n**

这个命令将显示一个应该用于worker_connections的数字。

现在,假设我们的处理器有四个核心,每个核心的限制是 512。然后,我们可以在 NGINX 主配置文件中设置这两个变量的值。在 Debian/Ubuntu 上,它位于/etc/nginx/nginx.conf

现在,找出这两个变量并设置如下:

Worker_processes 4;
Worker_connections 512

前面的值可能会很高,特别是worker_connections,因为服务器处理器核心有很高的限制。

内容交付网络(CDN)

内容交付网络用于托管静态媒体文件,如图像、.css.js文件,以及音频和视频文件。这些文件存储在地理网络上,其服务器位于不同的位置。然后,这些文件根据请求位置从特定服务器提供给请求。

CDN 提供以下功能:

  • 由于内容是静态的,不经常更改,CDN 会将它们缓存在内存中。当对某个文件发出请求时,CDN 会直接从缓存中发送文件,这比从磁盘加载文件并发送到浏览器要快。

  • CDN 服务器位于不同的位置。所有文件都存储在每个位置,取决于您在 CDN 中的设置。当浏览器请求到达 CDN 时,CDN 会从最近可用的位置发送请求的内容到请求的位置。例如,如果 CDN 在伦敦、纽约和迪拜都有服务器,并且来自中东的请求到达时,CDN 将从迪拜服务器发送内容。这样,由于 CDN 从最近的位置提供内容,响应时间得到了缩短。

  • 每个浏览器对向同一域发送并行请求有限制。通常是三个请求。当对请求的响应到达时,浏览器会向同一域发送更多的请求,这会导致完整页面加载的延迟。CDN 提供子域(可以是它们自己的子域或您主域的子域,使用您主域的 DNS 设置),这使得浏览器可以向从不同域加载的相同内容发送更多的并行请求。这使得浏览器可以快速加载页面内容。

  • 通常,动态内容的请求量很小,静态内容的请求量更多。如果您的应用的静态内容托管在单独的 CDN 服务器上,这将极大地减轻服务器的负载。

使用 CDN

那么,您如何在应用中使用 CDN 呢?在最佳实践中,如果您的应用流量很大,为每种内容类型在 CDN 上创建不同的子域是最好的选择。例如,为 CSS 和 JavaScript 文件创建一个单独的域,为图像创建一个子域,为音频/视频文件创建另一个单独的子域。这样,浏览器将为每种内容类型发送并行请求。假设我们对每种内容类型有以下 URL:

  • 对于 CSS 和 JavaScripthttp://css-js.yourcdn.com

  • 对于图像http://images.yourcdn.com

  • 对于其他媒体http://media.yourcdn.com

现在,大多数开源应用程序在其管理控制面板中提供设置以设置 CDN URL,但如果您使用的是开源框架或自定义构建的应用程序,您可以通过将上述 URL 放在数据库中或全局加载的配置文件中来定义自己的 CDN 设置。

对于我们的示例,我们将把上述 URL 放在一个配置文件中,并为它们创建三个常量,如下所示:

Constant('CSS_JS_URL', 'http://css-js.yourcdn.com/');
Constant('IMAGES_URL', 'http://images.yourcdn.com/');
Constant('MEDiA_URL', 'http://css-js.yourcdn.com/');

如果我们需要加载 CSS 文件,可以按以下方式加载:

<script type="text/javascript" src="<?php echo CSS_JS_URL ?>js/file.js"></script>

对于 JavaScript 文件,可以按以下方式加载:

<link rel="stylesheet" type="text/css" href="<?php echo CSS_JS_URL ?>css/file.css" />

如果我们加载图像,可以在img标签的src属性中使用上述方式,如下所示:

<img src="<?php echo IMAGES_URL ?>images/image.png" />

在上述示例中,如果我们不需要使用 CDN 或想要更改 CDN URL,只需在一个地方进行更改即可。

大多数知名的 JavaScript 库和模板引擎都在其自己的个人 CDN 上托管其静态资源。谷歌在其自己的 CDN 上托管查询库、字体和其他 JavaScript 库,可以直接在应用程序中使用。

有时,我们可能不想使用 CDN 或负担不起它们。为此,我们可以使用一种称为域共享的技术。使用域分片,我们可以创建子域或将其他域指向我们在同一服务器和应用程序上的资源目录。这种技术与之前讨论的相同;唯一的区别是我们自己将其他域或子域指向我们的媒体、CSS、JavaScript 和图像目录。

这可能看起来不错,但它不会为我们提供 CDN 的最佳性能。这是因为 CDN 根据客户的位置决定内容的地理可用性,进行广泛的缓存,并在运行时对文件进行优化。

CSS 和 JavaScript 优化

每个网络应用程序都有 CSS 和 JavaScript 文件。如今,大多数应用程序都有大量的 CSS 和 JavaScript 文件,以使应用程序具有吸引力和互动性。每个 CSS 和 JavaScript 文件都需要浏览器向服务器发送请求来获取文件。因此,你拥有的 CSS 和 JavaScript 文件越多,浏览器就需要发送的请求就越多,从而影响其性能。

每个文件都有一个内容大小,浏览器下载它需要时间。例如,如果我们有 10 个每个 10KB 的 CSS 文件和 10 个每个 50KB 的 JavaScript 文件,CSS 文件的总内容大小为 100KB,JavaScript 文件的总内容大小为 500KB,两种类型的文件总共为 600KB。这太多了,浏览器会花费时间来下载它们。

注意

性能在网络应用程序中扮演着至关重要的角色。即使是 Google 在其索引中也计算性能。不要认为一个文件只有几 KB 并且需要 1 毫秒下载,因为在性能方面,每一毫秒都是被计算的。最好的方法是优化、压缩和缓存一切。

在这一部分,我们将讨论两种优化我们的 CSS 和 JS 的方法,如下所示:

  • 合并

  • 压缩

合并

在合并过程中,我们可以将所有的 CSS 文件合并成一个文件,JavaScript 文件也是同样的过程,从而创建一个 CSS 和 JavaScript 的单一文件。如果我们有 10 个 CSS 文件,浏览器会发送 10 个请求来获取所有这些文件。然而,如果我们将它们合并成一个文件,浏览器只会发送一个请求,因此节省了九个请求所花费的时间。

压缩

在压缩过程中,CSS 和 JavaScript 文件中的所有空行、注释和额外的空格都被移除。这样,文件的大小就减小了,文件加载速度就快了。

例如,假设你在一个文件中有以下 CSS 代码:

.header {
  width: 1000px;
  height: auto;
  padding: 10px
}

/* move container to left */
.float-left {
  float: left;
}

/* Move container to right */
.float-right {
  float: right;
}

压缩文件后,我们将得到类似以下的 CSS 代码:

.header{width:100px;height:auto;padding:10px}.float-left{float:left}.float-right{float:right}

同样地,对于 JavaScript,假设我们在一个 JavaScript 文件中有以下代码:

/* Alert on page load */
$(document).ready(function() {
  alert("Page is loaded");
});

/* add three numbers */
function addNumbers(a, b, c) {
  return a + b + c;
}

现在,如果前述文件被压缩,我们将得到以下代码:

$(document).ready(function(){alert("Page is loaded")});function addNumbers(a,b,c){return a+b+c;}

可以注意到在前面的例子中,所有不必要的空格和换行都被移除了。它还将完整的文件代码放在一行中。所有的代码注释都被移除了。这样,文件大小就减小了,有助于文件快速加载。此外,这个文件将消耗更少的带宽,如果服务器资源有限的话,这是有用的。

大多数开源应用程序,如 Magento、Drupal 和 WordPress,提供内置支持或支持第三方插件/模块的应用程序。在这里,我们不会涵盖如何在这些应用程序中合并 CSS 或 JavaScript 文件,但我们将讨论一些可以合并 CSS 和 JavaScript 文件的工具。

Minify

Minify 是一组完全用 PHP 编写的库。Minify 支持 CSS 和 JavaScript 文件的合并和压缩。它的代码完全是面向对象和命名空间的,因此可以嵌入到任何当前的或专有的框架中。

注意

Minify 主页位于minifier.org。它也托管在 GitHub 上,网址为github.com/matthiasmullie/minify。值得注意的是,Minify 库使用了一个路径转换库,这个库是由同一作者编写的。路径转换库可以从github.com/matthiasmullie/path-converter下载。下载这个库并将其放在与 minify 库相同的文件夹中。

现在,让我们创建一个小项目,用来缩小和合并 CSS 和 JavaScript 文件。项目的文件夹结构将如下截图所示:

Minify

在上面的截图中,显示了完整的项目结构。项目名称是minifycss文件夹中包含了所有的 CSS 文件,包括缩小或合并的文件。同样,js文件夹中包含了所有的 JavaScript 文件,包括缩小或合并的文件。libs文件夹中包含了Minify库和Converter库。Index.php包含了我们用来缩小和合并 CSS 和 JavaScript 文件的主要代码。

注意

项目树中的data文件夹与 JavaScript 缩小有关。由于 JavaScript 有需要在其前后加上空格的关键字,这些.txt文件用于识别这些运算符。

因此,让我们从index.php中使用以下代码来缩小我们的 CSS 和 JavaScript 文件:

include('libs/Converter.php');
include('libs/Minify.php');
include('libs/CSS.php');
include('libs/JS.php');
include('libs/Exception.php');

use MatthiasMullie\Minify;

/* Minify CSS */
$cssSourcePath = 'css/styles.css';
$cssOutputPath = 'css/styles.min.css';
$cssMinifier = new Minify\CSS($cssSourcePath);
$cssMinifier->minify($cssOutputPath);

/* Minify JS */
$jsSourcePath = 'js/app.js';
$jsOutputPath = 'js/app.min.js';
$jsMinifier = new Minify\JS($jsSourcePath);
$jsMinifier->minify($jsOutputPath);

前面的代码很简单。首先,我们包含了所有需要的库。然后,在Minify CSS块中,我们创建了两个路径变量:$cssSourcePath,它包含了我们需要缩小的 CSS 文件的路径,以及$cssOutputPath,它包含了将要生成的缩小 CSS 文件的路径。

之后,我们实例化了CSS.php类的一个对象,并传递了我们需要缩小的 CSS 文件。最后,我们调用了CSS类的缩小方法,并传递了输出路径以及文件名,这将为我们生成所需的文件。

JS 缩小过程也是同样的解释。

如果我们运行上述 PHP 代码,所有文件都就位,一切顺利,那么将会创建两个新的文件名:styles.min.cssapp.min.js。这些是它们原始文件的新缩小版本。

现在,让我们使用 Minify 来合并多个 CSS 和 JavaScript 文件。首先,在项目中的相应文件夹中添加一些 CSS 和 JavaScript 文件。之后,我们只需要在当前代码中添加一点代码。在下面的代码中,我将跳过包含所有库,但是每当您需要使用 Minify 时,这些文件都必须被加载。

/* Minify CSS */
$cssSourcePath = 'css/styles.css';
**$cssOutputPath = 'css/styles.min.merged.css';**
$cssMinifier = new Minify\CSS($cssSourcePath);
**$cssMinifier->add('css/style.css');**
**$cssMinifier->add('css/forms.js');**
$cssMinifier->minify($cssOutputPath);

/* Minify JS */
$jsSourcePath = 'js/app.js';
**$jsOutputPath = 'js/app.min.merged.js';**
$jsMinifier = new Minify\JS($jsSourcePath);
**$jsMinifier->add('js/checkout.js');**
$jsMinifier->minify($jsOutputPath);

现在,看一下高亮显示的代码。在 CSS 部分,我们将缩小和合并的文件保存为style.min.merged.css,但命名并不重要;这完全取决于我们自己的选择。

现在,我们只需使用$cssMinifier$jsMinifier对象的add方法来添加新文件,然后调用minify。这将导致所有附加文件合并到初始文件中,然后进行缩小,从而生成单个合并和缩小的文件。

Grunt

根据其官方网站,Grunt 是一个 JavaScript 任务运行器。它自动化了某些重复的任务,这样你就不必重复工作。这是一个很棒的工具,在 Web 程序员中被广泛使用。

安装 Grunt 非常容易。在这里,我们将在 MAC OS X 上安装它,大多数 Linux 系统,如 Debian 和 Ubuntu,使用相同的方法。

注意

Grunt 需要 Node.js 和 npm。安装和配置 Node.js 和 npm 超出了本书的范围,因此在本书中,我们将假设这些工具已经安装在您的计算机上,或者您可以搜索它们并弄清楚如何安装它们。

如果 Node.js 和 npm 已经安装在您的计算机上,只需在终端中输入以下命令:

**sudo npm install –g grunt**

这将安装 Grunt CLI。如果一切顺利,那么以下命令将显示 Grunt CLI 的版本:

**grunt –version**

上述命令的输出是grunt-cli v0.1.13;,在撰写本书时,这个版本是可用的。

Grunt 为您提供了一个命令行,可以让您运行 Grunt 命令。一个 Grunt 项目在您的项目文件树中需要两个文件。一个是package.json,它被npm使用,并列出了项目需要的 Grunt 和 Grunt 插件作为 DevDependencies。

第二个文件是GruntFile,它存储为GruntFile.jsGruntFile.coffee,用于配置和定义 Grunt 任务并加载 Grunt 插件。

现在,我们将使用相同的项目,但我们的文件夹结构将如下所示:

Grunt

现在,在项目根目录中打开终端并发出以下命令:

**sudo npm init**

这将通过询问一些问题生成package.json文件。现在,打开package.json文件并修改它,使最终的package.json文件的内容看起来类似于以下内容:

{
  "name" : "grunt"  //Name of the project
  "version : "1.0.0" //Version of the project
  "description" : "Minify and Merge JS and CSS file",
  "main" : "index.js",
  "DevDependencies" : {
    "grunt" : "0.4.1", //Version of Grunt

    //Concat plugin version used to merge css and js files
    "grunt-contrib-concat" : "0.1.3"

    //CSS minifying plugin
    "grunt-contrib-cssmin" : "0.6.1",

    //Uglify plugin used to minify JS files.
    "grunt-contrib-uglify" : "0.2.0" 

   },
"author" : "Altaf Hussain",
"license" : ""
}

我在package.json文件的不同部分添加了注释,以便易于理解。请注意,对于最终文件,我们将从该文件中删除注释。

可以看到,在DevDependencies部分,我们添加了用于不同任务的三个 Grunt 插件。

下一步是添加GruntFile。让我们在项目根目录创建一个名为GruntFile.js的文件,内容类似于package.json文件。将以下内容放入GruntFile

module.exports = function(grunt) {
   /*Load the package.json file*/
   pkg: grunt.file.readJSON('package.json'),
  /*Define Tasks*/
  grunt.initConfig({
    concat: {
      css: {
        src: [
        'css/*' //Load all files in CSS folder
],
         dest: 'dest/combined.css' //Destination of the final combined file.

      }, //End of CSS
js: {
      src: [
     'js/*' //Load all files in js folder
],
      dest: 'dest/combined.js' //Destination of the final combined file.

    }, //End of js

}, //End of concat
cssmin:  {
  css: {
    src : 'dest/combined.css',
    dest : 'dest/combined.min.css' 
}
},//End of cssmin
uglify: {
  js: {
    files: {
      'dest/combined.min.js' : ['dest/combined.js'] // destination Path : [src path]
    }
  }
} //End of uglify

}); //End of initConfig

grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-cssmin');
grunt.registerTask('default', ['concat:css', 'concat:js', 'cssmin:css', 'uglify:js']);

}; //End of module.exports

前面的代码简单易懂,需要时添加注释。在顶部,我们加载了我们的package.json文件,之后,我们定义了不同的任务以及它们的源文件和目标文件。请记住,每个任务的源文件和目标文件语法都不同,这取决于插件。在initConfig块之后,我们加载了不同的插件和 npm 任务,然后将它们注册到 GRUNT 上。

现在,让我们运行我们的任务。

首先,让我们合并 CSS 和 JavaScript 文件,并将它们存储在 GruntFile 中任务列表中定义的各自目标中,通过以下命令:

**grunt concat**

在您的终端中运行上述命令后,如果看到完成,无错误的消息,则任务已成功完成。

同样,让我们使用以下命令来压缩我们的 css 文件:

**grunt cssmin**

然后,我们将使用以下命令来压缩我们的 JavaScript 文件:

**grunt uglify**

现在,使用 Grunt 可能看起来需要很多工作,但它提供了一些其他功能,可以让开发人员的生活变得更轻松。例如,如果您需要更改 JavaScript 和 CSS 文件怎么办?您应该再次运行所有前面的命令吗?不,Grunt 提供了一个 watch 插件,它会激活并执行任务中目标路径中的所有文件,如果发生任何更改,它会自动运行任务。

要了解更多详细信息,请查看 Grunt 的官方网站gruntjs.com/

完整页面缓存

在完整页面缓存中,网站的完整页面存储在缓存中,对于下一个请求,将提供此缓存页面。如果您的网站内容不经常更改,则完整页面缓存更有效;例如,在一个简单的博客上,每周添加新帖子。在这种情况下,可以在添加新帖子后清除缓存。

如果您有一个网站,其中页面具有动态部分,例如电子商务网站怎么办?在这种情况下,完整页面缓存会带来问题,因为每个请求的页面总是不同;用户登录后,他/她可能会向购物车中添加产品等。在这种情况下,使用完整页面缓存可能并不容易。

大多数流行的平台都提供对完整页面缓存的内置支持或通过插件和模块。在这种情况下,插件或模块会为每个请求处理页面的动态块。

Varnish

如官方网站所述,Varnish 可以让您的网站飞起来;这是真的!Varnish 是一个开源的 Web 应用程序加速器,运行在您的 Web 服务器软件的前面。它必须配置在端口 80 上,以便每个请求都会经过它。

现在,Varnish 配置文件(称为带有.vcl扩展名的 VCL 文件)有一个后端的定义。后端是配置在另一个端口(比如说 8080)上的 Web 服务器(Apache 或 NGINX)。可以定义多个后端,并且 Varnish 也会负责负载均衡。

当请求到达 Varnish 时,它会检查该请求的数据是否在其缓存中可用。如果它在缓存中找到数据,则将缓存的数据返回给请求,不会发送请求到 Web 服务器或后端。如果 Varnish 在其缓存中找不到任何数据,则会向 Web 服务器发送请求并请求数据。当它从 Web 服务器接收数据时,首先会缓存这些数据,然后将其发送回请求。

正如前面的讨论所清楚的那样,如果 Varnish 在缓存中找到数据,就不需要向 Web 服务器发送请求,因此也不需要在那里进行处理,响应会非常快速地返回。

Varnish 还提供了负载平衡和健康检查等功能。此外,Varnish 不支持 SSL 和 cookies。如果 Varnish 从 Web 服务器或后端接收到 cookies,则不会缓存该页面。有不同的方法可以轻松解决这些问题。

我们已经讲了足够的理论;现在,让我们通过以下步骤在 Debian/Ubuntu 服务器上安装 Varnish:

  1. 首先将 Varnish 存储库添加到sources.list文件中。在文件中加入以下行:
    deb https://repo.varnish-cache.org/debian/ Jessie varnish-4.1
  1. 之后,输入以下命令以更新存储库:
**sudo apt-get update**

  1. 现在,输入以下命令:
**sudo apt-get install varnish**

  1. 这将下载并安装 Varnish。现在,首先要做的是配置 Varnish 以侦听端口 80,并使您的 Web 服务器侦听另一个端口,例如 8080。我们将在这里使用 NGINX 进行配置。

  2. 现在,打开 Varnish 配置文件位置/etc/default/varnish,并进行更改,使其看起来类似于以下代码:

    DAEMON_OPS="-a :80 \
      -T localhost:6082 \ 
      -f /etc/varnish/default.vcl \
      -S /etc/varnish/secret \
      -s malloc,256m"
  1. 保存文件并在终端中输入以下命令重新启动 Varnish:
**sudo service varnish restart**

  1. 现在我们的 Varnish 在端口80上运行。让 NGINX 在端口8080上运行。编辑应用程序的 NGINX vhost文件,并将侦听端口从80更改为8080,如下所示:
    listen 8080;
  1. 现在,在终端中输入以下命令重新启动 NGINX:
**sudo service nginx restart**

  1. 下一步是配置 Varnish VCL 文件并添加一个将与我们的后端通信的后端,端口为8080。编辑位于/etc/varnish/default.vcl的 Varnish VCL 文件,如下所示:
    backend default {
      .host = "127.0.0.1";
      .port = "8080";
    }

在上述配置中,我们的后端主机位于 Varnish 运行的同一台服务器上,因此我们输入了本地 IP。在这种情况下,我们也可以输入 localhost。但是,如果我们的后端在远程主机或另一台服务器上运行,则应输入该服务器的 IP。

现在,我们已经完成了 Varnish 和 Web 服务器的配置。重新启动 Varnish 和 NGINX。打开浏览器,输入服务器的 IP 或主机名。初始响应可能会很慢,因为 Varnish 正在从后端获取数据,然后对其进行缓存,但其他后续响应将非常快,因为 Varnish 已经对其进行了缓存,并且现在正在发送缓存的数据而不与后端通信。

Varnish 提供了一个工具,我们可以轻松监视 Varnish 缓存状态。这是一个实时工具,会实时更新其内容。它被称为 varnishstat。要启动 varnishstat,只需在终端中输入以下命令:

**varnishstat**

上述命令将显示类似于以下截图的会话:

Varnish

如前面的截图所示,它显示非常有用的信息,例如运行时间和开始时的请求数,缓存命中,缓存未命中,所有后端,后端重用等。我们可以使用这些信息来调整 Varnish 以获得最佳性能。

注意

Varnish 的完整配置超出了本书的范围,但可以在 Varnish 官方网站www.varnish-cache.org找到很好的文档。

基础设施

我们讨论了太多关于提高应用性能的话题。现在,让我们讨论一下我们应用的可扩展性和可用性。随着时间的推移,我们应用的流量可能会增加到同时使用数千名用户。如果我们的应用在单个服务器上运行,性能将受到严重影响。此外,将应用保持在单一点上并不是一个好主意,因为如果该服务器宕机,我们的整个应用将会宕机。

为了使我们的应用程序更具可扩展性和可用性,我们可以使用基础架构设置,在其中我们可以在多个服务器上托管我们的应用程序。此外,我们可以将应用程序的不同部分托管在不同的服务器上。为了更好地理解,看一下以下图表:

基础设施

这是基础设施的一个非常基本的设计。让我们谈谈它的不同部分以及每个部分和服务器将执行的操作。

注意

可能只有负载均衡器(LB)连接到公共互联网,其余部分可以通过机架内的私有网络相互连接。如果有一个机架可用,这将非常好,因为所有服务器之间的通信都将在私有网络上进行,因此是安全的。

Web 服务器

在上图中,我们有两个 Web 服务器。可以有任意数量的 Web 服务器,并且它们可以轻松连接到 LB。Web 服务器将托管我们的实际应用程序,并且应用程序将在 NGINX 或 Apache 和 PHP 7 上运行。我们将在本章讨论的所有性能调整都可以在这些 Web 服务器上使用。此外,并不一定要求这些服务器应该在端口 80 上监听。最好是我们的 Web 服务器应该在另一个端口上监听,以避免使用浏览器进行任何公共访问。

数据库服务器

数据库服务器主要用于安装 MySQL 或 Percona Server 的数据库。然而,基础架构设置中的一个问题是将会话数据存储在一个地方。为此,我们还可以在数据库服务器上安装 Redis 服务器,它将处理我们应用的会话数据。

上述基础设施设计并不是最终或完美的设计。它只是为了给出多服务器应用托管的想法。它有很多改进的空间,比如添加另一个本地负载均衡器、更多的 Web 服务器和数据库集群的服务器。

负载均衡器(LB)

第一部分是负载均衡器LB)。负载均衡器的目的是根据每个 Web 服务器上的负载将流量分配给 Web 服务器。

对于负载均衡器,我们可以使用广泛用于此目的的 HAProxy。此外,HAProxy 会检查每个 Web 服务器的健康状况,如果一个 Web 服务器宕机,它会自动将该宕机的 Web 服务器的流量重定向到其他可用的 Web 服务器。为此,只有 LB 将在端口 80 上监听。

我们不希望在可用的 Web 服务器上(在我们的情况下,有两个 Web 服务器)上加重 SSL 通信的加密和解密负载,因此我们将使用 HAProxy 服务器在那里终止 SSL。当我们的负载均衡器接收到带有 SSL 的请求时,它将终止 SSL 并将普通请求发送到其中一个 Web 服务器。当它收到响应时,HAProxy 将加密响应并将其发送回客户端。这样,与使用两个服务器进行 SSL 加密/解密相比,只需使用一个单独的负载均衡器服务器来实现此目的。

注意

Varnish 也可以用作负载均衡器,但这并不是一个好主意,因为 Varnish 的整个目的是 HTTP 缓存。

HAProxy 负载均衡

在上述基础设施中,我们在 Web 服务器前放置了一个负载均衡器,它会平衡每个服务器上的负载,检查每个服务器的健康状况,并终止 SSL。我们将安装 HAProxy 并配置它以实现之前提到的所有配置。

HAProxy 安装

我们将在 Debian/Ubuntu 上安装 HAProxy。在撰写本书时,HAProxy 1.6 是最新的稳定版本。执行以下步骤安装 HAProxy:

  1. 首先,在终端中发出以下命令更新系统缓存:
**sudo apt-get update**

  1. 接下来,在终端中输入以下命令安装 HAProxy:
**sudo apt-get install haproxy**

这将在系统上安装 HAProxy。

  1. 现在,在终端中发出以下命令确认 HAProxy 安装:
**haproxy -v**

HAProxy 安装

如果输出与上述截图相同,则恭喜!HAProxy 已成功安装。

HAProxy 负载均衡

现在是使用 HAProxy 的时候了。为此,我们有以下三个服务器:

  • 第一个是负载均衡器服务器,安装了 HAProxy。我们将其称为 LB。对于本书的目的,LB 服务器的 IP 是 10.211.55.1。此服务器将在端口 80 上进行监听,并且所有 HTTP 请求将发送到此服务器。此服务器还充当前端服务器,因为我们的应用的所有请求都将发送到此服务器。

  • 第二个是 Web 服务器,我们将其称为 Web1。NGINX、PHP 7、MySQL 或 Percona Server 都安装在上面。此服务器的 IP 是 10.211.55.2。此服务器将在端口 80 或任何其他端口上进行监听。我们将其保持在端口 8080 上进行监听。

  • 第三个是第二个 Web 服务器,我们将其称为 Web2,IP 为 10.211.55.3。这与 Web1 服务器的设置相同,并将在端口 8080 上进行监听。

Web1 和 Web2 服务器也称为后端服务器。首先,让我们配置 LB 或前端服务器在端口 80 上进行监听。

打开位于/etc/haproxy/haproxy.cfg文件,并在文件末尾添加以下行:

frontend http
  bind *:80
  mode http
  default_backend web-backends

在上述代码中,我们将 HAProxy 设置为在任何 IP 地址(本地回环 IP 127.0.0.1 或公共 IP)上监听 HTTP 端口 80。然后,我们设置默认的后端。

现在,我们将添加两个后端服务器。在同一文件中,在末尾放置以下代码:

backend web-backend 
  mode http
  balance roundrobin
  option forwardfor
  server web1 10.211.55.2:8080 check
  server web2 10.211.55.3:8080 check

在上述配置中,我们将两个服务器添加到 Web 后端。后端的引用名称是web-backend,在前端配置中也使用了它。我们知道,我们的两个 Web 服务器都在端口 8080 上进行监听,因此我们提到这是每个 Web 服务器的定义。此外,我们在每个 Web 服务器的定义末尾使用了check,告诉 HAProxy 检查服务器的健康状况。

现在,在终端中发出以下命令重新启动 HAProxy:

**sudo service haproxy restart**

注意

要启动 HAProxy,可以使用sudo service haproxy start命令。要停止 HAProxy,可以使用sudo service haproxy stop命令。

现在,在浏览器中输入 LB 服务器的 IP 或主机名,我们的 Web 应用页面将显示为来自 Web1 或 Web2。

现在,禁用任何一个 Web 服务器,然后再次重新加载页面。应用程序仍将正常工作,因为 HAProxy 自动检测到其中一个 Web 服务器已关闭,并将流量重定向到第二个 Web 服务器。

HAProxy 还提供了一个基于浏览器的统计页面。它提供有关 LB 和所有后端的完整监控信息。要启用统计信息,打开haprox.cfg,并在文件末尾放置以下代码:

listen stats *:1434
  stats enable
  stats uri /haproxy-stats
  stats auth phpuser:packtPassword

统计信息在端口1434上启用,可以设置为任何端口。页面的 URL 是stats uri。它可以设置为任何 URL。auth部分用于基本的 HTTP 身份验证。保存文件并重新启动 HAProxy。现在,打开浏览器,输入 URL,例如10.211.55.1:1434/haproxy-stats。统计页面将显示如下:

HAProxy 负载均衡

在上述截图中,可以看到每个后端 Web 服务器,包括前端信息。

此外,如果一个 Web 服务器宕机,HAProxy 统计信息将突出显示此 Web 服务器的行,如下截图所示:

HAProxy 负载均衡

对于我们的测试,我们停止了 Web2 服务器上的 NGINX,并刷新了统计页面,然后在后端部分中,Web2 服务器行被突出显示。

要使用 HAProxy 终止 SSL,非常简单。我们只需在 SSL 端口 443 绑定上添加 SSL 证书文件位置。打开haproxy.cfg文件,编辑前端块,并在其中添加高亮显示的代码,如下所示的块:

frontend http 
bind *:80
**bind *:443 ssl crt /etc/ssl/www.domain.crt**
  mode http
  default_backend web-backends

现在,HAProxy 也在 443 端口监听,当 SSL 请求发送到它时,它在那里处理并终止它,以便不会将 HTTPS 请求发送到后端服务器。这样,SSL 加密/解密的负载就从 Web 服务器中移除,并由 HAProxy 服务器单独管理。由于 SSL 在 HAProxy 服务器上终止,因此无需让 Web 服务器在 443 端口监听,因为来自 HAProxy 服务器的常规请求会发送到后端。

总结

在本章中,我们讨论了从 NGINX 和 Apache 到 Varnish 等多个主题。我们讨论了如何优化我们的 Web 服务器软件设置以获得最佳性能。此外,我们还讨论了 CDN 以及如何在客户应用程序中使用它们。我们讨论了优化 JavaScript 和 CSS 文件以获得最佳性能的两种方法。我们简要讨论了完整页面缓存和 Varnish 的安装和配置。最后,我们讨论了多服务器托管或基础架构设置,以使我们的应用程序具有可伸缩性和最佳可用性。

在下一章中,我们将探讨如何提高数据库性能的方法。我们将讨论包括 Percona Server、数据库的不同存储引擎、查询缓存、Redis 和 Memcached 在内的多个主题。

第四章:提高数据库性能

数据库在动态网站中扮演着关键角色。所有进出数据都存储在数据库中。因此,如果 PHP 应用程序的数据库设计和优化不好,将会极大地影响应用程序的性能。在本章中,我们将探讨优化 PHP 应用程序数据库的方法。本章将涵盖以下主题:

  • MySQL

  • 查询缓存

  • MyISAM 和 InnoDB 存储引擎

  • Percona DB 和 Percona XtraDB 存储引擎

  • MySQL 性能监控工具

  • Redis

  • 内存缓存

MySQL 数据库

MySQL 是 Web 上最常用的关系型数据库管理系统(RDMS)。它是开源的,有免费的社区版本。它提供了企业级数据库可以提供的所有功能。

MySQL 安装提供的默认设置可能对性能不太好,总是有方法可以微调这些设置以获得更好的性能。另外,记住你的数据库设计在性能方面起着重要作用。设计不良的数据库会影响整体性能。

在本节中,我们将讨论如何提高 MySQL 数据库的性能。

注意

我们将修改 MySQL 配置的my.cnf文件。这个文件在不同的操作系统中位于不同的位置。另外,如果您在 Windows 上使用 XAMPP、WAMP 或任何其他跨平台 Web 服务器解决方案堆栈包,这个文件将位于相应的文件夹中。无论使用哪个操作系统,只要提到my.cnf,就假定文件是打开的。

查询缓存

查询缓存是 MySQL 的一个重要性能特性。它缓存SELECT查询以及结果数据集。当出现相同的SELECT查询时,MySQL 会从内存中获取数据,以便查询执行得更快,从而减少数据库的负载。

要检查 MySQL 服务器上是否启用了查询缓存,请在 MySQL 命令行中输入以下命令:

**SHOW VARIABLES LIKE 'have_query_cache';**

上述命令将显示以下输出:

查询缓存

上一个结果集显示查询缓存已启用。如果查询缓存被禁用,值将为NO

要启用查询缓存,打开my.cnf文件并添加以下行。如果这些行已经存在并被注释掉了,就取消注释:

query_cache_type = 1
query_cache_size = 128MB
query_cache_limit = 1MB

保存my.cnf文件并重新启动 MySQL 服务器。让我们讨论一下前面三个配置的含义:

  • query_cache_type:这起着一种令人困惑的作用。

  • 如果query_cache_type设置为1query_cache_size为 0,则不分配内存,查询缓存被禁用。

如果query_cache_size大于 0,则查询缓存已启用,分配了内存,并且所有不超过query_cache_limit值或使用SQL_NO_CACHE选项的查询都被缓存。

  • 如果query_cache_type的值为 0,query_cache_size0,则不分配内存,缓存被禁用。

如果query_cache_size大于 0,则分配了内存,但没有缓存——即缓存被禁用。

  • query_cache_sizequery_cache_size:这表示将分配多少内存。有些人认为使用的内存越多,效果就越好,但这是一个误解。这完全取决于数据库大小、查询类型和读写比例、硬件、数据库流量和其他因素。query_cache_size的一个好值在 100MB 到 200MB 之间;然后,您可以监视性能和其他影响查询缓存的变量,并调整大小。我们在一个中等流量的 Magento 网站上使用了 128MB,效果非常好。将此值设置为0以禁用查询缓存。

  • query_cache_limit:这定义了要缓存的查询数据集的最大大小。如果查询数据集的大小大于此值,则不会被缓存。可以通过找出最大的SELECT查询和其返回数据集的大小来猜测此配置的值。

存储引擎

存储引擎(或表类型)是 MySQL 核心的一部分,负责处理表上的操作。MySQL 提供了几种存储引擎,其中最常用的是 MyISAM 和 InnoDB。这两种存储引擎都有各自的优缺点,但总是优先考虑 InnoDB。从 5.5 开始,MySQL 开始使用 InnoDB 作为默认存储引擎。

注意

MySQL 提供了一些其他具有自己目的的存储引擎。在数据库设计过程中,可以决定哪个表应该使用哪种存储引擎。MySQL 5.6 的存储引擎的完整列表可以在dev.mysql.com/doc/refman/5.6/en/storage-engines.html找到。

可以在数据库级别设置存储引擎,然后将其用作每个新创建的表的默认存储引擎。请注意,存储引擎是表的基础,单个数据库中的不同表可以具有不同的存储引擎。如果已经创建了一个表并且想要更改其存储引擎怎么办?很容易。假设我们的表名是pkt_users,其存储引擎是 MyISAM,我们想将其更改为 InnoDB;我们将使用以下 MySQL 命令:

**ALTER TABLE pkt_users ENGINE=INNODB;**

这将把表的存储引擎值更改为INNODB

现在,让我们讨论两种最常用的存储引擎 MyISAM 和 InnoDB 之间的区别。

MyISAM 存储引擎

以下是 MyISAM 支持或不支持的功能的简要列表:

  • MyISAM 旨在提高速度,最适合与SELECT语句一起使用。

  • 如果表更加静态,即该表中的数据更新/删除较少,大部分情况下只是获取数据,那么 MyISAM 是该表的最佳选项。

  • MyISAM 支持表级锁定。如果需要对表中的数据执行特定操作,那么可以锁定整个表。在此锁定期间,无法对该表执行任何操作。如果表更加动态,即该表中的数据经常更改,这可能会导致性能下降。

  • MyISAM 不支持外键。

  • MyISAM 支持全文搜索。

  • MyISAM 不支持事务。因此,不支持COMMITROLLBACK。如果对表执行查询,则执行查询,没有回头的余地。

  • 支持数据压缩、复制、查询缓存和数据加密。

  • 不支持集群数据库。

InnoDB 存储引擎

以下是 InnoDB 支持或不支持的功能的简要列表:

  • InnoDB 旨在在处理大量数据时具有高可靠性和高性能。

  • InnoDB 支持行级锁定。这是一个很好的特性,对性能非常有利。与 MyISAM 锁定整个表不同,它仅锁定SELECTDELETEUPDATE操作的特定行,在这些操作期间,该表中的其他数据可以被操作。

  • InnoDB 支持外键并强制外键约束。

  • 支持事务。可以进行 COMMIT 和 ROLLBACK,因此可以从特定事务中恢复数据。

  • 支持数据压缩、复制、查询缓存和数据加密。

  • InnoDB 可以在集群环境中使用,但它并没有完全支持。然而,InnoDB 表可以通过将表引擎更改为 NDB 来转换为 MySQL 集群中使用的 NDB 存储引擎。

在接下来的部分中,我们将讨论与 InnoDB 相关的一些性能特性。以下配置的值在my.cnf文件中设置。

innodb_buffer_pool_size

此设置定义了用于 InnoDB 数据和加载到内存中的索引的内存量。对于专用的 MySQL 服务器,推荐值是服务器上安装内存的 50-80%。如果此值设置得太高,操作系统和 MySQL 的其他子系统,如事务日志,将没有内存。因此,让我们打开我们的my.cnf文件,搜索innodb_buffer_pool_size,并将值设置在推荐值(即 RAM 的 50-80%)之间。

innodb_buffer_pool_instances

这个特性并不是那么广泛使用。它使多个缓冲池实例能够共同工作,以减少 64 位系统和innodb_buffer_pool_size较大值的内存争用的机会。

有不同的选择来计算innodb_buffer_pool_instances的值。一种方法是每 GB 的innodb_buffer_pool_size使用一个实例。因此,如果innodb_bufer_pool_size的值为 16GB,我们将把innodb_buffer_pool_instances设置为 16。

innodb_log_file_size

innodb_log_file_size是存储执行的每个查询信息的日志文件的大小。对于专用服务器,最多可以设置为 4GB,但如果日志文件太大,崩溃恢复所需的时间可能会增加。因此,在最佳实践中,它保持在 1 到 4GB 之间。

Percona Server - MySQL 的一个分支

根据 Percona 网站的说法,Percona 是一个免费、完全兼容、增强、开源的 MySQL 替代品,提供卓越的性能、可伸缩性和工具。

Percona 是一个具有增强性能功能的 MySQL 分支。MySQL 中可用的所有功能在 Percona 中也是可用的。Percona 使用一个名为 XtraDB 的增强存储引擎。根据 Percona 网站的说法,这是 MySQL 的 InnoDB 存储引擎的增强版本,具有更多功能,在现代硬件上具有更快的性能和更好的可伸缩性。Percona XtraDB 在高负载环境中更有效地使用内存。

如前所述,XtraDB 是 InnoDB 的一个分支,因此 InnoDB 中可用的所有功能在 XtraDB 中也是可用的。

安装 Percona Server

Percona 目前仅适用于 Linux 系统。目前不支持 Windows。在本书中,我们将在 Debian 8 上安装 Percona Server。对于 Ubuntu 和 Debian,安装过程是相同的。

注意

要在其他 Linux 版本上安装 Percona Server,请查看 Percona 安装手册www.percona.com/doc/percona-server/5.5/installation.html。目前,他们提供了 Debian、Ubuntu、CentOS 和 RHEL 的安装说明。他们还提供了从源代码和 Git 安装 Percona Server 的说明。

现在,让我们通过以下步骤安装 Percona Server:

  1. 使用终端中的以下命令打开您的源列表文件:
**sudo nano /etc/apt/sources.list** 

如果提示输入密码,请输入您的 Debian 密码。文件将被打开。

  1. 现在,将以下存储库信息放在sources.list文件的末尾:
deb http://repo.percona.com/apt jessie main
deb-src http://repo.percona.com/apt jessie main
  1. 按下CTRL + O保存文件,按下CTRL + X关闭文件。

  2. 使用终端中的以下命令更新系统:

**sudo apt-get update**

  1. 通过在终端中发出以下命令开始安装:
**sudo apt-get install percona-server-server-5.5**

  1. 安装将开始。该过程与安装 MySQL 服务器的过程相同。在安装过程中,将要求输入 Percona Server 的 root 密码;您只需输入即可。安装完成后,您将可以像使用 MySQL 一样使用 Percona Server。

  2. 根据之前的章节配置和优化 Percona Server。

MySQL 性能监控工具

始终需要监视数据库服务器的性能。为此,有许多可用的工具,使监视 MySQL 服务器和性能变得容易。其中大多数是开源和免费的,并且一些提供了图形界面。命令行工具更加强大,是最好的选择,尽管需要一点时间来理解和习惯它们。我们将在这里讨论一些。

phpMyAdmin

这是最著名的基于 Web 的开源免费工具,用于管理 MySQL 数据库。除了管理 MySQL 服务器外,它还提供了一些很好的工具来监视 MySQL 服务器。如果我们登录到 phpMyAdmin,然后点击顶部的状态选项卡,我们将看到以下屏幕:

phpMyAdmin

服务器选项卡向我们显示了关于 MySQL 服务器的基本数据,例如启动时间,自上次启动以来处理的流量量,连接信息等。

接下来是查询统计。这部分提供了关于所有执行的查询的完整统计信息。它还提供了一个饼图,可视化显示每种查询类型的百分比,如下面的截图所示。

如果我们仔细观察图表,我们会发现我们有 54%的SELECT查询正在运行。如果我们使用某种缓存,比如 Memcached 或 Redis,这些SELECT查询不应该这么高。因此,这个图表和统计信息为我们提供了分析我们的缓存系统的手段。

phpMyAdmin

下一个选项是所有状态变量,列出了所有 MySQL 变量及其当前值。在这个列表中,可以很容易地找出 MySQL 的配置情况。在下面的截图中,显示了我们的查询缓存变量及其值:

phpMyAdmin

phpMyAdmin 提供的下一个选项是监视器。这是一个非常强大的工具,以图形方式实时显示服务器资源及其使用情况。

phpMyAdmin

如前面的截图所示,我们可以在一个漂亮的图形界面中看到问题连接/进程系统 CPU 使用率流量系统内存系统交换

最后一个重要部分是顾问。它为我们提供有关性能设置的建议。它尽可能多地为您提供细节,以便调整 MySQL 服务器以提高性能。以下截图显示了顾问部分的一个小节:

phpMyAdmin

如果应用了所有这些建议,就可以获得一些性能提升。

MySQL 工作台

这是 MySQL 的桌面应用程序,配备了管理和监控 MySQL 服务器的工具。它为我们提供了一个性能仪表板,可以以美观和图形的方式查看与服务器相关的所有数据,如下面的截图所示:

The MySQL workbench

Percona Toolkit

之前提到的所有工具都很好,并提供了一些关于我们数据库服务器的可视化信息。然而,它们还不足以向我们显示一些更有用的信息或提供更多可以简化我们生活的功能。为此,还有另一个命令行工具包可用,名为 Percona Toolkit。

Percona Toolkit 是一套包括用于分析慢查询、存档、优化索引等的 30 多个命令行工具。

注意

Percona Toolkit 是免费开源的,可在 GPL 下使用。它的大多数工具在 Linux/Unix 系统上运行,但也有一些可以在 Windows 上运行。安装指南可以在www.percona.com/doc/percona-toolkit/2.2/installation.html找到。完整的工具集可以在www.percona.com/doc/percona-toolkit/2.2/index.html找到。

现在,让我们在接下来的小节中讨论一些工具。

pt-query-digest

该工具分析来自慢查询、一般查询和二进制日志文件的查询。它生成有关查询的复杂报告。让我们使用以下命令对慢查询运行此工具:

**Pt-query-digest /var/log/mysql/mysql-slow.log**

在终端中输入上述命令后,我们将看到一个很长的报告。在这里,我们将讨论报告的一小部分,如下屏幕截图所示:

pt-query-digest

在前面的屏幕截图中,慢查询按最慢的顺序列出。第一个查询是一个SELECT查询,花费最长的时间,大约占总时间的 12%。第二个查询也是一个SELECT查询,占总时间的 11.5%。从这份报告中,我们可以看到哪些查询很慢,以便优化它们以获得最佳性能。

此外,pt-query-digest 显示每个查询的信息,如下屏幕截图所示。屏幕截图中提到了第一个查询的数据,包括总时间;时间百分比(pct);最小、最大和平均时间;发送的字节数;以及其他一些参数:

pt-query-digest

pt-duplicate-key-checker

该工具查找指定表集或完整数据库中的重复索引和重复外键。让我们在终端中使用以下命令再次执行此工具:

**Pt-duplicate-key-checker –user packt –password dbPassword –database packt_pub**

执行时,将打印以下输出:

pt-duplicate-key-checker

在报告末尾,显示了指标摘要,这是不言自明的。此工具还打印出每个重复索引的ALTER查询,可以作为 MySQL 查询执行以修复索引,如下所示:

**Pt-variable-advisor**

该工具显示每个查询的 MySQL 配置信息和建议。这是一个可以帮助我们正确设置 MySQL 配置的好工具。我们可以通过运行以下命令来执行此工具:

**Pt-variable-advisor –user packt –password DbPassword localhost**

执行后,将显示以下输出:

pt-duplicate-key-checker

Percona Toolkit 还提供了许多其他工具,超出了本书的范围。但是,www.percona.com/doc/percona-toolkit/2.2/index.html上的文档非常有帮助且易于理解。它为每个工具提供了完整的详细信息,包括描述和风险,如何执行以及其他选项(如果有)。如果您希望了解 Percona Toolkit 中的任何工具,这份文档值得一读。

Percona XtraDB Cluster(PXC)

Percona XtraDB Cluster 提供了一个高性能的集群环境,可以帮助轻松配置和管理多台服务器上的数据库。它使数据库可以使用二进制日志相互通信。集群环境有助于在不同的数据库服务器之间分担负载,并在服务器宕机时提供故障安全性。

要设置集群,我们需要以下服务器:

  • 一台带有 IP 10.211.55.1 的服务器,我们将其称为 Node1

  • 第二台带有 IP 10.211.55.2 的服务器,我们将其称为 Node2

  • 第三台带有 IP 10.211.55.3 的服务器,我们将其称为 Node3

由于我们已经在我们的资源中有 Percona 存储库,让我们开始安装和配置 Percona XtraDB Cluster,也称为 PXC。执行以下步骤:

  1. 首先,在终端中发出以下命令在 Node1 上安装 Percona XtraDB Cluster:
**apt-get install percona-xtradb-cluster-56**

安装将类似于正常的 Percona Server 安装开始。在安装过程中,还将要求设置 root 用户的密码。

  1. 安装完成后,我们需要创建一个具有复制权限的新用户。在登录到 MySQL 终端后,发出以下命令:
**CREATE USER 'sstpackt'@'localhost' IDENTIFIED BY 'sstuserpassword';**
**GRANT RELOAD, LOCK TABLES, REPLICATION CLIENT ON *.* TO 'sstpackt'@'localhost';**
**FLUSH PRIVILEGES;**

第一个查询创建一个用户名为sstpackt,密码为sstuserpassword的用户。用户名和密码可以是任何内容,但建议使用一个好的和强大的密码。第二个查询为我们的新用户设置适当的权限,包括锁定表和复制。第三个查询刷新权限。

  1. 现在,打开位于/etc/mysql/my.cnf的 MySQL 配置文件。然后,在mysqld块中放置以下配置:
#Add the galera library
wsrep_provider=/usr/lib/libgalera_smm.so

#Add cluster nodes addresses
wsrep_cluster_address=gcomm://10.211.55.1,10.211.55.2,10.211.55.3

#The binlog format should be ROW. It is required for galera to work properly
binlog_format=ROW

#default storage engine for mysql will be InnoDB
default_storage_engine=InnoDB

#The InnoDB auto increment lock mode should be 2, and it is required for galera
innodb_autoinc_lock_mode=2

#Node 1 address
wsrep_node_address=10.211.55.1

#SST method
wsrep_sst_method=xtrabackup

#Authentication for SST method. Use the same user name and password created in above step 2
wsrep_sst_auth="sstpackt:sstuserpassword"

#Give the cluster a name
wsrep_cluster_name=packt_cluster

在添加上述配置后保存文件。

  1. 现在,通过发出以下命令启动第一个节点:
**/etc/init.d/mysql bootstrap-pxc**

这将引导第一个节点。引导意味着启动初始集群并定义哪个节点具有正确的信息,其他所有节点都应该同步到哪个节点。由于 Node1 是我们的初始集群节点,并且我们在这里创建了一个新用户,因此我们只需引导 Node1。

注意

SST代表State Snapshot Transfer。它负责从一个节点复制完整数据到另一个节点。仅在向集群添加新节点并且此节点必须从现有节点获取完整的初始数据时使用。Percona XtraDB Cluster中有三种 SST 方法,mysqldumprsyncxtrabackup

  1. 在第一个节点上登录 MySQL 终端,并发出以下命令:
**SHOW STATUS LIKE '%wsrep%';**

将显示一个非常长的列表。以下是其中的一些:

Percona XtraDB Cluster (PXC)

  1. 现在,对所有节点重复步骤 1 和步骤 3。每个节点需要更改的唯一配置是wsrep_node_address,它应该是节点的 IP 地址。编辑所有节点的my.cnf配置文件,并将节点地址放在wsrep_node_address中。

  2. 通过在终端中发出以下命令来启动两个新节点:

**/etc/init.d/mysql start**

现在可以通过重复步骤 7 来验证每个节点。

要验证集群是否正常工作,请在一个节点中创建一个数据库,并向表中添加一些表和数据。之后,检查其他节点是否有新创建的数据库、表和每个表中输入的数据。我们将把所有这些数据同步到每个节点。

Redis - 键值缓存存储

Redis 是一个开源的内存键值数据存储,广泛用于数据库缓存。根据 Redis 网站(www.Redis.io)的说法,Redis 支持诸如字符串、哈希、列表、集合和排序列表等数据结构。此外,Redis 支持复制和事务。

注意

Redis 安装说明可以在redis.io/topics/quickstart找到。

要检查 Redis 在服务器上是否正常工作,请在终端中运行以下命令启动 Redis 服务器实例:

**redis server**

然后在不同的终端窗口中发出以下命令:

**redis-cli ping**

如果上述命令的输出如下,则 Redis 服务器已准备就绪:

Redis - 键值缓存存储

Redis 提供了一个命令行,其中提供了一些有用的命令。在 Redis 服务器上执行命令有两种方法。您可以使用以前的方法,也可以只输入redis-cli并按Enter;然后我们将看到 Redis 命令行,然后我们可以输入要执行的 Redis 命令。

默认情况下,Redis 使用 IP 127.0.0.1 和端口 6379。虽然不允许远程连接,但可以启用远程连接。Redis 存储已在数据库中创建的数据。数据库名称是整数,如 0、1、2 等。

我们不会在这里详细讨论 Redis,但我们将讨论一些值得注意的命令。请注意,所有这些命令都可以以前面的方式执行,或者我们可以只输入redis-cli命令窗口并输入命令,而不输入redis-cli。此外,以下命令可以直接在 PHP 中执行,这样就可以直接从我们的 PHP 应用程序中清除缓存:

  • 选择:此命令更改当前数据库。默认情况下,redis-cli 将在数据库 0 打开。因此,如果我们想要转到数据库 1,我们将运行以下命令:
**SELECT 1**

  • FLUSHDB:此命令刷新当前数据库。当前数据库中的所有键或数据将被删除。

  • FLUSHALL:此命令刷新所有数据库,无论在哪个数据库中执行。

  • KEYS:此命令列出与模式匹配的当前数据库中的所有键。以下命令列出当前数据库中的所有键。

**KEYS ***

现在,是时候在 PHP 中与 Redis 进行一些操作了。

注意

在撰写本主题时,PHP 7 尚未内置对 Redis 的支持。为了本书的目的,我们为 PHP 7 编译了 PHPRedis 模块,并且它运行得非常好。该模块可以在github.com/phpredis/phpredis找到。

与 Redis 服务器连接

如前所述,默认情况下,Redis 服务器在 IP 127.0.0.1 和端口 6379 上运行。因此,为了建立连接,我们将使用这些详细信息。请看以下代码:

$redisObject = new Redis();
if( !$redisObject->connect('127.0.0.1', 6379))
  die("Can't connect to Redis Server");

在第一行中,我们通过名称redisObject实例化了一个 Redis 对象,然后在第二行中使用它连接到 Redis 服务器。主机是本地 IP 地址 127.0.0.1,端口是 6379。connect()方法如果连接成功则返回TRUE;否则返回FALSE

从 Redis 服务器存储和获取数据

现在,我们已连接到我们的 Redis 服务器。让我们在 Redis 数据库中保存一些数据。例如,我们想要在 Redis 数据库中存储一些字符串数据。代码如下:

//Use same code as above for connection.
//Save Data in to Redis database.
$rdisObject->set('packt_title', 'Packt Publishing');

//Lets get our data from database
echo $redisObject->get('packt_title');

set方法将数据存储到当前 Redis 数据库,并接受两个参数:键和值。键可以是任何唯一名称,值是我们需要存储的内容。因此,我们的键是packt_title,值是Packt Publishing。除非显式设置,否则默认数据库始终设置为 0(零)。因此,上述set方法将保存我们的数据到数据库 0,并使用packt_title键。

现在,get方法用于从当前数据库中获取数据。它以键作为参数。因此,上述代码的输出将是我们保存的字符串数据Packt Publishing

那么,来自数据库的数组或一组数据怎么办?我们可以以多种方式在 Redis 中存储它们。让我们首先尝试正常的字符串方式,如下所示:

//Use same connection code as above.

/* This $array can come from anywhere, either it is coming from database or user entered form data or an array defined in code */

$array = ['PHP 5.4', PHP 5.5, 'PHP 5.6', PHP 7.0];

//Json encode the array
$encoded = json_encode($array);

//Select redis database 1
$redisObj->select(1);

//store it in redis database 1
$redisObject->set('my_array', $encoded);

//Now lets fetch it
$data = $redisObject->get('my_array');

//Decode it to array
$decoded = json_decode($data, true);

print_r($decoded); 

上述代码的输出将是相同的数组。为了测试目的,我们可以注释掉set方法,并检查get方法是否获取数据。请记住,在上述代码中,我们将数组存储为json字符串,然后将其作为json字符串获取,并解码为数组。这是因为我们使用了字符串数据类型可用的方法,不可能将数组存储在字符串数据类型中。

此外,我们使用select方法选择另一个数据库并在 0 之外使用它。这些数据将存储在数据库 1 中,如果我们在数据库 0,则无法获取它们。

注意

对 Redis 的完整讨论超出了本书的范围。因此,我们提供了一个简介。请注意,如果您使用任何框架,都可以轻松使用 Redis 提供的内置库,并且可以轻松使用任何数据类型。

Redis 管理工具

Redis 管理工具提供了一种简单的方式来管理 Redis 数据库。这些工具提供了功能,以便可以轻松检查每个键并清除缓存。Redis 自带一个默认工具,称为 Redis-cli,我们之前已经讨论过。现在,让我们讨论一个视觉工具,非常好用,叫做Redis Desktop ManageRDM)。RDM 的主窗口的屏幕截图如下所示:

Redis 管理工具

RDM 提供以下功能:

  • 它连接到远程多个 Redis 服务器

  • 以不同格式显示特定键中的数据

  • 它向所选数据库添加新键

  • 它向选定的键添加更多数据

  • 它编辑/删除键和它们的名称

  • 它支持 SSH 和 SSL,并且可以在云中使用

还有一些其他工具可以使用,但 RDM 和 Redis-cli 是最好和最容易使用的。

Memcached 键值缓存存储

根据 Memcached 官方网站的说法,它是一个免费、开源、高性能、分布式内存对象缓存系统。Memcached 是一个内存中的键值存储,可以存储来自数据库或 API 调用的数据集。

与 Redis 类似,Memcached 也在加速网站方面有很大帮助。它将数据(字符串或对象)存储在内存中。这使我们能够减少与外部资源(如数据库或 API)的通信。

注意

我们假设 Memcached 已经安装在服务器上。同时,也假设 PHP 7 的 PHP 扩展也已安装。

现在,让我们在 PHP 中稍微玩一下 Memcachd。看一下下面的代码:

//Instantiate Memcached Object
$memCached = new Memcached();

//Add server
$memCached->addServer('127.0.0.1', 11211);

//Lets get some data
$data = $memCached->get('packt_title');

//Check if data is available
if($data)
{
  echo $data;
}
else
{
  /*No data is found. Fetch your data from any where and add to   memcached */

  $memCached->set('packt_title', 'Packt Publishing');

}

上面的代码是一个非常简单的使用 Memcached 的例子。每行代码都有注释,很容易理解。在实例化一个 Memcached 对象之后,我们必须添加一个 Memcached 服务器。默认情况下,Memcached 服务器在本地主机 IP 上运行,即 127.0.0.1,端口号为 11211。之后,我们使用一个键来检查一些数据,如果数据可用,我们可以处理它(在这种情况下,我们将其显示出来,也可以返回它,或者进行其他所需的处理)。如果数据不可用,我们可以直接添加它。请注意,数据可以来自远程服务器 API 或数据库。

注意

我们刚刚介绍了 Memcached 以及它如何帮助我们存储数据和提高性能。在本标题中无法进行完整的讨论。关于 Memcached 的一本好书是 Packt Publishing 出版的《Getting Started with Memcached》。

摘要

在本章中,我们涵盖了 MySQL 和 Percona Server。此外,我们详细讨论了查询缓存和其他 MySQL 性能配置选项。我们提到了不同的存储引擎,比如 MyISAM、InnoDB 和 Percona XtraDB。我们还在三个节点上配置了 Percona XtraDB 集群。我们讨论了不同的监控工具,比如 PhpMyAdmin 监控工具、MySQL Workbench 性能监控和 Percona Toolkit。我们还讨论了 Redis 和 Memcached 对 PHP 和 MySQL 的缓存。

在下一章中,我们将讨论基准测试和不同的工具。我们将使用 XDebug、Apache JMeter、ApacheBench 和 Siege 来对不同的开源系统进行基准测试,比如 WordPress、Magento、Drupal 以及不同版本的 PHP,并将它们与 PHP 7 的性能进行比较。

第五章:调试和性能分析

在开发过程中,每个开发人员都会遇到问题,不清楚到底发生了什么,以及为什么会产生问题。大多数时候,这些问题可能是逻辑性的或者与数据有关。要找到这样的问题总是很困难。调试是一个找到这样的问题并解决它们的过程。同样,我们经常需要知道脚本消耗了多少资源,包括内存消耗,CPU 以及执行所需的时间。

在本章中,我们将涵盖以下主题:

  • Xdebug

  • 使用 Sublime Text 3 进行调试

  • 使用 Eclipse 进行调试

  • 使用 Xdebug 进行性能分析

  • PHP DebugBar

Xdebug

Xdebug 是 PHP 的一个扩展,为 PHP 脚本提供调试和性能分析信息。Xdebug 显示错误的完整堆栈跟踪信息,包括函数名称,行号和文件名。此外,它提供了使用不同 IDE(如 Sublime Text,Eclipse,PHP Storm 和 Zend Studio)交互式调试脚本的能力。

要检查 Xdebug 是否已安装并在我们的 PHP 安装中启用,我们需要检查 phpinfo()的详细信息。在 phpinfo 详细信息页面上搜索 Xdebug,您应该看到类似以下屏幕截图的详细信息:

![Xdebug](graphics / B05225_05_01.jpg)

这意味着我们的 PHP 安装已经安装了 Xdebug。现在,我们需要配置 Xdebug。Xdebug 配置要么在php.ini文件中,要么有自己的单独的.ini文件。在我们的安装中,我们将在/etc/php/7.0/fpm/conf.d/路径下放置一个单独的20-xdebug.ini文件。

注意

为了本书的目的,我们将使用 Laravel 的 Homestead Vagrant 框。它在 Ubuntu 14.04 LTS 安装中提供了完整的工具,包括带有 Xdebug 的 PHP7,NGINX 和 MySQL。对于开发目的,这个 Vagrant 框是一个完美的解决方案。更多信息可以在[https://laravel.com/docs/5.1/homestead](https://laravel.com/docs/5.1/homestead)找到。

现在,打开20-xdebug.ini文件并将以下配置放入其中:

zend_extension = xdebug.so
xdebug.remote_enable = on
xdebug.remote_connect_back = on
xdebug.idekey = "vagrant"

上述是我们应该使用的最低配置,它们启用了远程调试并设置了 IDE 密钥。现在,通过在终端中发出以下命令来重新启动 PHP:

**sudo service php-fpm7.0 restart**

现在我们准备调试一些代码。

使用 Sublime Text 进行调试

Sublime Text 编辑器有一个插件,可以用来使用 Xdebug 调试 PHP 代码。首先,让我们为 Sublime Text 安装xdebug包。

注意

对于这个主题,我们将使用仍处于 beta 阶段的 Sublime Text 3。使用版本 2 还是 3 是你自己的选择。

首先,转到工具 | 命令面板。将显示类似于以下内容的弹出窗口:

![使用 Sublime Text 进行调试](graphics / B05225_05_02.jpg)

选择Package Control:Install Package,将显示类似于以下屏幕截图的弹出窗口:

![使用 Sublime Text 进行调试](graphics / B05225_05_03.jpg)

键入xdebug,将显示Xdebug Client包。单击它,等待一会直到安装完成。

现在,在 Sublime Text 中创建一个项目并保存它。打开 Sublime Text 项目文件并插入以下代码:

{
  "folders":
  [
    {
    "follow_symlinks": true,
    "path": "."
  }
],

**"settings": {**
 **"xdebug": {**
 **"path_mapping": {**
 **"full_path_on_remote_host" : "full_path_on_local_host"**
 **},**
 **"url" : http://url-of-application.com/,**
 **"super_globals" : true,**
 **"close_on_stop" : true,**
 **}**
 **}**
}

突出显示的代码很重要,必须输入 Xdebug。路径映射是最重要的部分。它应该有远程主机应用程序根目录的完整路径和本地主机应用程序根目录的完整路径。

现在,让我们开始调试。在项目的根目录创建一个文件,命名为index.php,并将以下代码放入其中:

$a = [1,2,3,4,5];
$b = [4,5,6,7,8];

$c = array_merge($a, $b);

现在,在编辑器中右键单击一行,然后选择Xdebug。然后,单击添加/删除断点。让我们按照以下屏幕截图中显示的方式添加一些断点:

![使用 Sublime Text 进行调试](graphics / B05225_05_04.jpg)

当在一行上添加断点时,将在左侧靠近行号处显示一个填充的圆圈,如前面的屏幕截图所示。

现在我们已经准备好调试我们的 PHP 代码了。转到工具 | Xdebug | 开始调试(在浏览器中启动)。浏览器窗口将打开应用程序,并附带 Sublime Text 调试会话参数。浏览器窗口将处于加载状态,因为一旦到达第一个断点,执行就会停止。浏览器窗口将类似于以下内容:

使用 Sublime Text 进行调试

一些新的小窗口也会在 Sublime Text 编辑器中打开,显示调试信息以及所有可用的变量,如下面的屏幕截图所示:

使用 Sublime Text 进行调试

在上面的屏幕截图中,我们的$a$b$c数组未初始化,因为执行光标位于第 22 行,并且停在那里。此外,所有服务器变量、cookie、环境变量、请求数据以及 POST 和 GET 数据都可以在这里看到。这样,我们可以调试各种变量、数组和对象,并检查每个变量、对象或数组在某个特定点上持有的数据。这使我们有可能找出那些在没有调试的情况下很难检测到的错误。

现在,让我们将执行光标向前移动。在编辑器代码部分右键单击,然后转到Xdebug | 步入。光标将向前移动,变量数据可能会根据下一行而改变。可以在以下屏幕截图中注意到这一点:

使用 Sublime Text 进行调试

单击工具 | Xdebug | 停止调试即可停止调试。

使用 Eclipse 进行调试

Eclipse 是最自由和功能强大的广泛使用的 IDE。它支持几乎所有主要的编程语言,包括 PHP。我们将讨论如何配置 Eclipse 以使用 Xdebug 进行调试。

首先,在 Eclipse 中打开项目。然后,单击工具栏中小虫图标右侧的向下箭头,如下面的屏幕截图所示:

使用 Eclipse 进行调试

之后,单击调试配置菜单,将打开以下窗口:

使用 Eclipse 进行调试

在左侧面板中选择PHP Web 应用程序,然后单击左上角的添加新图标。这将添加一个新的配置,如上面的屏幕截图所示。给配置命名。现在,我们需要向配置中添加一个 PHP 服务器。单击右侧面板上的新建按钮,将打开以下窗口:

使用 Eclipse 进行调试

我们将服务器名称输入为PHP 服务器。服务器名称可以是任何用户友好的名称,只要以后可以识别。在基本 URL字段中,输入应用程序的完整 URL。文档根应该是应用程序根目录的本地路径。输入所有有效数据后,单击下一步按钮,我们将看到以下窗口:

使用 Eclipse 进行调试

调试器下拉列表中选择XDebug,其余字段保持不变。单击下一步按钮,我们将进入路径映射窗口。将正确的本地路径映射到正确的远程路径非常重要。单击添加按钮,我们将看到以下窗口:

使用 Eclipse 进行调试

在远程服务器上输入应用程序的文档根的完整路径。然后,选择文件系统中的路径,并输入应用程序文档根的本地路径。单击确定,然后单击路径映射窗口中的完成按钮。然后,在下一个窗口中单击完成,以完成添加 PHP 服务器。

现在,我们的配置已经准备好。首先,我们将通过点击行号栏上的小蓝点来向我们的 PHP 文件添加一些断点,如下截图所示。现在,点击工具栏上的小虫子图标,选择Debug As,然后点击PHP Web Application。调试过程将开始,并且浏览器中将打开一个窗口。它将处于加载状态,就像我们在 Sublime Text 调试中看到的一样。此外,Eclipse 中将打开 Debug 视图,如下所示:

使用 Eclipse 进行调试

当我们点击右侧边栏中的小(X)=图标时,我们将看到所有的变量。还可以编辑任何变量数据,甚至是任何数组的元素值、对象属性和 cookie 数据。修改后的数据将在当前调试会话中保留。

要进入下一行,我们只需按下F5,执行光标将移动到下一行。要跳出到下一个断点,我们将按下F6

使用 Xdebug 进行分析

分析提供了有关应用程序中执行的每个脚本或任务的成本的信息。它有助于提供有关任务花费多少时间的信息,因此我们可以优化我们的代码以减少时间消耗。

Xdebug 有一个默认情况下被禁用的分析器。要启用分析器,打开配置文件并在其中放置以下两行:

xdebug.profiler_enable=on
xdebug.profiler_output_dir=/var/xdebug/profiler/

第一行启用了分析器。我们定义了分析器文件的输出目录的第二行非常重要。在这个目录中,当分析器执行时,Xdebug 将存储输出文件。输出文件以cachegrind.out.id的名称存储。这个文件包含了所有的分析数据,以简单的文本格式。

现在,我们准备对 Laravel 应用程序主页的简单安装进行分析。这个安装是全新的和干净的。现在,让我们在浏览器中打开应用程序,并在末尾添加?XDEBUG_PROFILE=on,如下所示:

http://application_url.com?XDEBUG_PROFILE=on

在加载完这个页面后,将在指定位置生成一个cachegrind文件。现在,当我们在文本编辑器中打开文件时,我们将只看到一些文本数据。

注意

cachegrind文件可以用不同的工具打开。Windows 的一个工具是 WinCacheGrind。对于 Mac,我们有 qcachegrind。这些应用程序中的任何一个都将以一种可以轻松分析的交互形式查看文件数据。此外,PHP Storm 有一个用于 cachegrind 的良好分析器。在这个主题中,我们使用了 PHP Storm IDE。

在 PHP Storm 中打开文件后,我们将得到一个类似以下截图的窗口:

使用 Xdebug 进行分析

如前面的截图所示,我们在上面的窗格中有执行统计信息,显示了每个调用脚本单独花费的时间(以毫秒为单位),以及它被调用的次数。在下面的窗格中,我们有调用了这个脚本的调用者。

我们可以分析哪个脚本花费了更多的时间,然后优化这个脚本以减少执行时间。此外,我们可以找出在某个特定点是否需要调用特定的脚本。如果不需要,那么我们可以删除这个调用。

PHP DebugBar

PHP DebugBar 是另一个很棒的工具,它在页面底部显示一个漂亮且完整的信息栏。它可以显示为了调试目的而添加的自定义消息,以及包括$_COOKIE$_SERVER$_POST$_GET数组在内的完整请求信息,以及它们的数据(如果有的话)。此外,PHP DebugBar 还显示了异常的详细信息,执行的数据库查询及其详细信息。它还显示了脚本占用的内存和页面加载的时间。

根据 PHP Debug 网站,DebugBar 可以轻松集成到任何应用项目中,并显示来自应用程序任何部分的调试和分析数据。

它的安装很容易。您可以下载完整的源代码,将其放在应用程序的某个地方,并设置自动加载器来加载所有类,或者使用 composer 来安装它。我们将使用 composer,因为这是安装它的简单和干净的方式。

注意

Composer 是一个用于管理项目依赖关系的 PHP 工具。它是用 PHP 编写的,并且可以从getcomposer.org/免费获取。我们假设 composer 已经安装在您的机器上。

在项目的composer.json文件中,在所需的部分中放置以下代码:

"maximebf/debugbar" : ">=1.10.0"

保存文件,然后发出以下命令:

**composer update**

Composer 将开始更新依赖项并安装 composer。此外,它将生成自动加载器文件和/或 DebugBar 所需的其他依赖项。

注意

前面的 composer 命令只有在系统上全局安装了 composer 才能工作。如果没有,我们必须使用以下命令:

php composer.phar update

前面的命令应该在放置composer.phar的文件夹中执行。

安装后,DebugBar 的项目树可能如下:

PHP DebugBar

目录结构可能有点不同,但通常会如我们之前所述。src目录包含了 DebugBar 的完整源代码。vendor目录包含了一些可能需要的第三方模块或 PHP 工具。还要注意vendor文件夹中有自动加载器来自动加载所有类。

让我们现在检查我们的安装,看看它是否工作。在项目根目录中创建一个新文件,命名为index.php。之后,在其中放置以下代码:

<?php
require "vendor/autoloader.php";
use Debugbar\StandardDebugBar;
$debugger = new StandardDebugBar();
$debugbarRenderer = $debugbar->getJavascriptRenderer();

//Add some messages
$debugbar['messages']->addMessage('PHP 7 by Packt');
$debugbar['messages']->addMessage('Written by Altaf Hussain');

?>

<html>
  <head>
    <?php echo $debugbarRenderer->renderHead(); ?>
  </head>
  <title>Welcome to Debug Bar</title>
  <body>
    <h1>Welcome to Debug Bar</h1>

  <!—- display debug bar here -->
  <?php echo $debugbarRenderer->render();  ?>

  </body>
</html>

在前面的代码中,我们首先包含了我们的自动加载器,这是由 composer 为我们生成的,用于自动加载所有类。然后,我们使用了DebugBar\StandardDebugbar命名空间。之后,我们实例化了两个对象:StandardDebugBargetJavascriptRendererStandardDebugBar对象是一个对象数组,其中包含了不同收集器的对象,例如消息收集器等。getJavascriptRenderer对象负责在页眉处放置所需的 JavaScript 和 CSS 代码,并在页面底部显示栏。

我们使用$debugbar对象向消息收集器添加消息。收集器负责从不同来源收集数据,例如数据库、HTTP 请求、消息等。

在 HTML 代码的头部,我们使用了$debugbarRendererrenderHead方法来放置所需的 JavaScript 和 CSS 代码。之后,在<body>块的末尾之前,我们使用了相同对象的render方法来显示调试栏。

现在,在浏览器中加载应用程序,如果您注意到浏览器底部有一个栏,如下面的屏幕截图所示,那么恭喜!DebugBar 已正确安装并且运行正常。

PHP DebugBar

在右侧,我们有应用程序消耗的内存和加载时间。

如果我们点击消息选项卡,我们将看到我们添加的消息,如下面的屏幕截图所示:

PHP DebugBar

DebugBar 提供数据收集器,用于从不同来源收集数据。这些被称为基本收集器,以下是一些数据收集器:

  • 消息收集器收集日志消息,如前面的示例所示

  • TimeData 收集器收集总执行时间以及特定操作的执行时间

  • 异常收集器显示所有发生的异常

  • PDO 收集器记录 SQL 查询

  • RequestData 收集器收集 PHP 全局变量的数据,例如$_SERVER$_POST$_GET等。

  • 配置收集器用于显示数组的任何键值对

此外,还有一些收集器可以从 Twig、Swift Mailer、Doctrine 等第三方框架中收集数据。这些收集器被称为桥接收集器。PHP DebugBar 也可以轻松集成到著名的 PHP 框架,如 Laravel 和 Zend Framework 2 中。

本书无法对 PHP DebugBar 进行全面讨论。因此,这里只提供了一个简单的介绍。PHP DebugBar 有一个很好的文档,其中提供了完整的详细信息和示例。文档可以在phpdebugbar.com/docs/readme.html找到。

总结

在本章中,我们讨论了调试 PHP 应用程序的不同工具。我们使用 Xdebug、Sublime Text 3 和 Eclipse 来调试我们的应用程序。然后,我们使用 Xdebug 分析器来分析应用程序,以找出执行统计信息。最后,我们讨论了 PHP DebugBar 来调试应用程序。

在下一章中,我们将讨论负载测试工具,我们可以使用这些工具在我们的应用程序上放置负载或虚拟访问者,以对其进行负载测试,并找出我们的应用程序能承受多少负载,以及它如何影响性能。

第六章:压力/负载测试 PHP 应用程序

在应用程序开发、测试、调试和分析之后,是时候将其投入生产了。但是,在投入生产之前,最好的做法是对应用程序进行压力/负载测试。这项测试将为我们提供一个关于服务器在运行应用程序时可以处理多少请求的大致结果。利用这些结果,我们可以优化应用程序、Web 服务器、数据库和缓存工具,以获得更好的结果并处理更多的请求。

在本章中,我们将对 PHP 5.6 和 PHP 7 上的不同开源工具进行负载测试,并比较这些应用程序在 PHP 的两个版本上的性能。

我们将涵盖以下主题:

  • Apache JMeter

  • ApacheBench (ab)

  • Seige

  • 在 PHP 5.6 和 PHP 7 上对 Magento 2 进行负载测试

  • 在 PHP 5.6 和 PHP 7 上对 WordPress 进行负载测试

  • 在 PHP 5.6 和 PHP 7 上对 Drupal 8 进行负载测试

Apache JMeter

Apache JMeter 是一个图形化的开源工具,用于对服务器的性能进行负载测试。JMeter 完全由 Java 编写,因此与安装了 Java 的所有操作系统兼容。JMeter 拥有一套完整的广泛工具,可用于各种负载测试,从静态内容到动态资源和 Web 服务。

安装很简单。我们需要从 JMeter 网站下载它,然后运行应用程序。如前所述,它将需要在计算机上安装 Java。

注意

JMeter 可以测试 FTP 服务器、邮件服务器、数据库服务器、查询等等。在本书中,我们无法涵盖所有这些主题,因此我们只会对 Web 服务器进行负载测试。Apache JMeter 的功能列表可以在jmeter.apache.org/找到。

当我们首次运行应用程序时,将看到以下窗口:

Apache JMeter

要运行任何类型的测试,首先需要创建一个测试计划。测试计划包含执行此测试所需的所有组件。默认情况下,JMeter 有一个名为 Test Plan 的测试计划。让我们将其命名为我们自己的计划,Packt Publisher Test Plan,如下面的屏幕截图所示:

Apache JMeter

现在,保存测试计划,JMeter 将创建一个.jmx文件。将其保存在适当的位置。

下一步是添加一个线程组。线程组定义了测试计划的一些基本属性,这些属性可以在所有类型的测试中通用。要添加线程组,请右键单击左侧面板中的计划,然后导航到Add | Threads (Users) | Thread Group。将显示以下窗口:

Apache JMeter

线程组具有以下重要属性:

  • 线程数:这是虚拟用户的数量。

  • 渐进周期:这告诉 JMeter 它应该花多长时间才能达到线程数的最大容量。例如,在前面的屏幕截图中,我们有 40 个线程和 80 秒的渐进时间;在这里,JMeter 将花 80 秒的时间完全启动 40 个线程,每个线程启动需要 2 秒。

  • 循环计数:这告诉 JMeter 运行此线程组需要多长时间。

  • 调度程序:这用于安排稍后执行线程组。

现在,我们需要添加 HTTP 请求默认值。右键单击Packt Thread Group,然后转到Add | Config Element | HTTP Request Defaults。将出现类似以下的窗口:

Apache JMeter

在前面的窗口中,我们只需输入应用程序的 URL 或 IP 地址。如果 Web 服务器使用 cookie,我们还可以添加 HTTP Cookie Manager,在其中可以添加用户定义的 cookie 及其所有数据,如名称、值、域、路径等。

接下来,我们将通过右键单击并导航到Packt Thread Group | Add | Sampler | HTTP Request来添加一个 HTTP 请求,然后将出现以下窗口:

Apache JMeter

这里的重要字段是路径。我们只想针对主页运行测试,所以对于这个 HTTP 请求,我们只需在路径字段中添加一个斜杠(/)。如果我们想测试另一个路径,比如"联系我们",我们需要添加另一个 HTTP 请求采样器,如上面的屏幕截图所示。然后,在路径中,我们将添加path/contact-us

HTTP 请求采样器也可以用于测试表单,可以通过在方法字段中选择 POST 方法来向 URL 发送 POST 请求。还可以模拟文件上传。

接下来的步骤是添加一些监听器。监听器提供了一些强大的视图来显示结果。结果可以显示在表格视图中,并且不同类型的图表可以保存在文件中。对于这个线程组,我们将添加三个监听器:在表中查看结果,响应时间图和图表结果。每个监听器视图显示不同类型的数据。通过右键单击Packt Thread Group,然后导航到添加 | 监听器来添加所有前面的监听器。我们将有一个所有可用监听器的完整列表。逐个添加所有三个监听器。我们左侧的 JMeter 上的最终Packt Publisher Test Plan面板将类似于以下内容:

Apache JMeter

现在,我们可以通过点击上方工具栏中的开始按钮来运行我们的测试计划,如下面的屏幕截图所示:

Apache JMeter

一旦我们点击开始按钮(指向右侧的绿色箭头),JMeter 将启动我们的测试计划。现在,如果我们在左侧面板上点击在表中查看结果监听器,我们将看到每个请求的数据在表中显示,如下面的屏幕截图所示:

Apache JMeter

上述屏幕截图显示了一些有趣的数据,如样本时间、状态、字节和延迟。

样本时间是服务器提供完整请求所用的毫秒数。状态是请求的状态。它可以是成功、警告或错误。字节是请求接收的字节数。延迟是 JMeter 从服务器接收到初始响应所用的毫秒数。

现在,如果我们点击响应时间图,我们将看到响应时间的可视图表,类似于以下内容:

Apache JMeter

现在,如果我们点击图表结果,我们将看到响应时间数据以及平均值、中位数、偏差和吞吐量图表,如下图所示:

Apache JMeter

Apache JMeter 提供了非常强大的工具,可以通过模拟用户来对我们的 Web 服务器进行负载测试。它可以为我们提供关于使我们的 Web 服务器响应变慢的负载量的数据,并且使用这些数据,我们可以优化我们的 Web 服务器和应用程序。

ApacheBench (ab)

ApacheBench (ab)也由 Apache 提供,是一个命令行工具。这是一个非常适合命令行爱好者的工具。这个工具通常默认安装在大多数 Linux 发行版上。此外,它也随 Apache 一起安装,所以如果你安装了 Apache,你可能也会安装 ab。

ab 命令的基本语法如下:

**ab –n <Number_Requests> -c <Concurrency> <Address>:<Port><Path>**

让我们讨论上述命令的每个部分的含义:

  • n:这是测试的请求数。

  • c:这是并发数,即同时发出的请求数。

  • 地址:这是 Web 服务器的应用程序 URL 或 IP 地址。

  • 端口:这是应用程序运行的端口号。

  • 路径:这是我们可以用来测试的应用程序的 Web 路径。斜杠(/)用于主页。

现在,让我们通过发出以下命令使用 ab 工具进行测试:

**ab –n 500 –c 10 packtpub.com/**

由于 Web 服务器的默认端口是 80,因此不需要提及它。请注意末尾的斜杠;这是必需的,因为它是路径的一部分。

执行上述命令后,我们将获得类似以下内容的输出:

ApacheBench (ab)

我们可以在这里看到一些有用的信息,包括每秒请求的数量,为490.3;测试所用的总时间,为1.020 秒;最短请求为20 毫秒;最长请求为52 毫秒

可以通过增加请求数量和并发级别并检查 Web 服务器的性能来找到服务器负载限制。

Siege

Siege 是另一个命令行开源工具,用于测试负载和性能。Siege 是一个 HTTP/FTP 负载测试工具和基准测试实用程序。它旨在供开发人员和管理员在负载下测量其应用程序的性能。它可以向服务器发送可配置数量的并发请求,并且这些请求会使服务器处于围攻状态。

它的安装简单而容易。对于 Linux 和 Mac OS X,首先通过在终端中发出以下命令来下载 Siege:

**wget http://download.joedog.org/siege/siege-3.1.4.tar.gz**

它将下载 Siege TAR 压缩文件。现在,通过发出以下命令来解压缩它:

**tar –xvf siege-3.1.4.tar.gz**

现在,所有文件都将位于siege-3.1.4文件夹中。通过在终端中依次发出以下命令来构建和安装它:

**cd siege-3.1.4**
**./configure**
**make**
**make install**

现在,Siege 已安装。要确认这一点,请发出以下命令以检查 Siege 版本:

**siege –V**

如果显示带有其他信息的版本,则 Siege 已成功安装。

注意

在撰写本书时,当前的 Siege 稳定版本是 3.1.4。此外,Siege 不原生支持 Windows,当然,可以使用 Siege 测试和基准测试 Windows 服务器。

现在,让我们进行一次负载测试。可以通过运行以下命令来执行基本的负载测试:

**siege some_url_or_ip**

然后 Siege 将开始测试。我们必须输入要进行负载测试的应用程序 URL 或服务器 IP。要停止测试,请按Ctrl + C,然后我们将获得类似以下内容的输出:

Siege

在上述屏幕截图中,我们可以看到事务响应时间事务速率,以及最长事务最短事务

默认情况下,Siege 创建 15 个并发用户。可以通过使用-c选项进行更改,方法是在命令中进行以下更改:

**siege url_or_ip –c 100**

但是,Siege 对并发用户有限制,对于每个操作系统可能不同。这可以在 Siege 配置文件中设置。要找出config文件位置和并发用户限制,请在终端中发出以下命令:

**siege -C**

将显示配置选项列表。还将显示资源文件或config文件位置。打开该文件,找到配置并将其值设置为适当的值。

Siege 的另一个重要功能是可以使用包含所有需要测试的 URL 的文件。该文件每行应包含一个 URL。使用-f标志与 Siege 一起使用如下:

**siege -f /path/to/url/file.txt –c 120**

Siege 将加载文件并开始对每个 URL 进行负载测试。

Siege 的另一个有趣的功能是互联网模式,可以使用以下命令中的-i标志进入:

**siege –if path_to_urls_file –c 120**

在互联网模式下,每个 URL 都会随机访问,并模拟真实生活中无法预测哪个 URL 会被访问的情况。

注意

Siege 有很多有用的标志和功能。详细列表可以在官方文档中找到www.joedog.org/siege-manual/

负载测试真实应用

在本章中,我们研究了三种工具进行负载测试。现在,是时候对一些真实世界的应用进行负载测试了。在本节中,我们将测试 Magento 2、Drupal 8 和 WordPress 4。所有这些开源工具都将使用它们的默认数据。

我们有三个配置了 NGINX 作为 Web 服务器的 VPS。一个 VPS 安装了 PHP 5.5-FPM,第二个安装了 PHP 5.6-FPM,第三个安装了 PHP 7-FPM。所有三个 VPS 的硬件规格相同,我们将测试的所有应用程序都将具有相同的数据和相同的版本。

这样,我们将使用 PHP 5.5、PHP 5.6 和 PHP 7 对这些应用程序进行基准测试,并查看它们在不同版本的 PHP 上运行的速度。

注意

在本主题中,我们不会涉及配置带有 NGINX、PHP 和数据库的服务器。我们将假设 VPS 已配置,并且在其上安装了 Magento 2、Drupal 8 和 WordPress 4。

Magento 2

Magento 2 安装在所有 VPS 上,并且为 Magento 启用了所有缓存。还启用了 PHP OPcache。在运行测试后,我们得到了所有三个 Magento 2 安装的平均结果,如下图所示:

Magento 2

在上图中,垂直线或 Y 轴显示每秒事务数。如图所示,Magento 2 在 PHP 7 上每秒有 29 个事务,而在相同硬件上使用 PHP 5.6 的同一 Magento 2 安装每秒有 12 个事务。此外,在 PHP 5.5 上,相同的 Magento 安装每秒有 9 个事务。因此,在这种情况下,Magento 在 PHP 7 上的运行速度比在 PHP 5.6 上快约 241%,比在 PHP 5.5 上快约 320%。这是 PHP 7 在 PHP 5.6 和 PHP 5.5 上的非常巨大的改进。

WordPress 4

WordPress 安装在所有三个 VPS 上。不幸的是,WordPress 中没有默认缓存,我们也不会安装任何第三方模块,因此不使用缓存。结果仍然很好,如下图所示。启用了 PHP OPcache。

WordPress 4

如前图所示,WordPress 在 PHP 7 上运行速度比在 PHP 5.6 上快 135%,比在 PHP 5.5 上快 182%。

Drupal 8

我们在同一台 VPS 上使用了 PHP 5.5、PHP 5.6 和 PHP 7。默认启用了 Drupal 8 缓存。在对 Drupal 8 的默认主页进行负载测试后,我们得到了以下结果:

Drupal 8

上图显示,Drupal 8 在 PHP 7 上的运行速度比在 PHP 5.6 上快 178%,比在 PHP 5.5 上快 205%。

注意

在上述图表中,所有这些值都是近似值。如果使用低性能硬件,则会生成较小的值。如果我们使用具有 Web 服务器和数据库优化的更强大的多处理器专用服务器,我们将获得更高的值。需要考虑的一点是,我们在 PHP 7 上始终会获得比 PHP 5.6 更好的性能。

这里显示了一个合并的图表,显示了不同应用程序在 PHP 7 上相对于 PHP 5.5 和 PHP 5.6 的性能改进:

Drupal 8

总结

在本章中,我们讨论了一些负载测试和基准测试工具,如 JMeter、ApacheBench(ab)和 Siege。我们使用每个工具进行负载测试,并讨论了输出及其含义。最后,我们对三个著名的开源应用程序进行了负载测试,分别是 Magento 2、WordPress 4 和 Drupal 8,并为每个应用程序在 PHP 7 和 PHP 5.6 中的每秒事务创建了图表。

在下一章中,我们将讨论 PHP 开发的最佳实践。这些实践不仅限于 PHP,还可以用于任何编程语言。

第七章:PHP 编程的最佳实践

到目前为止,我们讨论了与性能相关的主题。现在,在本章中,我们将学习 PHP 应用程序开发和部署的最佳实践。这是一个广泛的主题,但我们将简要介绍。PHP 为所有级别的程序员提供了编写高质量代码的能力。然而,当应用程序变得更加复杂时,我们忘记了遵循最佳实践。为了开发高性能的 PHP 应用程序,有必要在代码的每一行都考虑性能。

我们将涵盖以下主题:

  • 编码风格

  • 设计模式

  • 面向服务的架构(SOA)

  • 测试驱动开发(TDD)和 PHPUnit 测试

  • PHP 框架

  • 版本控制系统和 Git

  • 部署

编码风格

有太多的编码风格,比如 PSR-0、PSR-1、PSR-2、PSR-3 等等。程序员可以按照自己的意愿使用不同的标准,但有必要遵循已经在库或框架中使用的标准,以使代码更易读。例如,Laravel 使用 PSR-1 和 PSR-4 编码标准,所以如果我们在 Laravel 中开发,就应该遵循这些编码标准。一些 PHP 框架,比如 Yii 2 和 Zend Framework 2,遵循 PSR-2 编码标准。然而,这些框架都没有坚持单一标准;大多数都根据自己的需求遵循混合标准。

重要的是要遵循应用程序中使用的库的标准。组织也可以为内部目的使用自己的编码标准。这不是编码的要求,而是可读性和产生他人可以理解的高质量代码的要求。

PHP Framework Interop Group(PHP-FIG)是一个成员定义了 PHP 编码标准的团体。有关 PSR 标准的详细信息可以在他们的网站上找到www.php-fig.org/

与讨论特定编码标准不同,让我们讨论 PHP 编码风格的最佳实践:

  • 类名中每个单词的首字母必须大写。大括号应该在类声明后的行上,结束括号应该在类结束行的下一行。这是一个例子:
class Foo
{
  …
  …
  …
}
  • 类方法和函数名应遵循驼峰命名约定。起始大括号应该在类声明的下一行,结束括号应该在函数定义的最后一行。方法名和括号之间不应有空格。此外,参数和括号之间不应有空格,参数的逗号之后应有一个空格,但逗号和下一个参数之间应有一个空格。这是一个例子:
public function phpBook($arg1, $arg2, $arg3)
{
  …
  …
  …
}
  • 如果有命名空间声明,声明后必须有一个空行。如果有使用声明,所有使用声明都必须放在该命名空间声明之后。每行必须有一个使用声明,并且在使用块之后必须有一个空格。此外,extendsimplements关键字必须与类声明在同一行上。这是一个例子:
namespace Packt\Videos;

use Packt\Books;
use Packt\Presentations;

class PacktClass extends VideosClass implements BaseClass
{
  …
  …
  …
}
  • 所有属性都必须声明可见性,并且属性必须使用驼峰命名法。此外,私有或受保护的属性不得以下划线开头。看下面的例子:
class PacktClass
{
  public $books;
  private $electronicBooks;
  …
  …
  …
}
  • 如果有abstract关键字,它必须在类关键字之前出现,对于方法,final关键字必须在方法的可见性之前出现。另一方面,static关键字必须在方法可见性之后出现。看一个例子:
abstract class PacktClass
{
  final public static function favoriteBooks()
  {
    …
    …
    …
  }
}
  • 所有 PHP 关键字必须使用小写,包括truefalse关键字。常量必须以大写形式声明和使用。

  • 对于所有控制结构,关键字后必须有一个空格。如果有一个表达式用于这个控制结构,那么括号中不应该有空格,后面跟着的代码块也不应该有空格。括号和开始的大括号之间必须有一个空格。开始的大括号必须在同一行上。结束的大括号必须在主体结束的下一行。参考以下代码以更好地理解:

if ($book == "PHP 7") {
  …
  …
  …
} else {
  …
  …
  …
}
  • 在循环的情况下,空格必须如下例所示:
for ($h = 0; $h < 10; $h++) {
  …
  …
  …
}

foreach ($books as $key => $value) {
  …
  …
  …
}

while ($book) {
  …
  …
  …
}

为了本书的目的,我没有遵循大括号在控制结构声明的同一行上的规则,并且总是在声明的下一行使用它。我觉得这样做并不更清晰;这是个人选择,任何人都可以遵循这里提到的标准。

遵循标准是很好的,因为它们使代码更易读和专业。但是,永远不要试图发明自己的新标准;总是遵循已经被社区发明和遵循的标准。

测试驱动开发(TDD)

测试驱动开发是在开发过程中测试应用程序的每个方面。测试可以在开发之前定义,然后进行开发以通过这些测试,或者构建类和库然后进行测试。测试应用程序非常重要,没有测试就启动应用程序就像从 30 层楼高的建筑物上跳下而没有降落伞。

PHP 没有提供任何内置功能来进行测试,但有其他测试框架可以用于此目的。其中最广泛使用的框架或库之一是 PHPUnit。它是一个非常强大的工具,提供了许多功能。现在,让我们来看看它。

PHPUnit 的安装很容易。只需下载并将其放在项目的根目录中,以便可以从命令行访问。

注意

PHPUnit 的安装和基本细节,包括功能和示例,可以在phpunit.de/找到。

让我们举一个简单的例子。我们有一个Book类,如下所示:

class Book 
{
  public $title;
  public function __construct($title)
  {
    $this->title = $title;
}

  public function getBook()
  {
    return $this->title;
  }
}

这是一个简单类的示例,当类被实例化时初始化title属性。当调用getBook方法时,它返回书的标题。

现在,我们想要进行一个测试,检查getBook方法是否返回PHP 7作为标题。因此,执行以下步骤创建测试:

  1. 在项目的根目录下创建一个tests目录。在tests目录中创建一个BookTest.php文件。

  2. 现在,将以下代码放入BookTest.php文件中:

include (__DIR__.'/../Book.php');

class BookTest extends PHPUnit_Framework_TestCase 
{
  public function testBookClass()
  {
    $expected = 'PHP 7';
    $book = new Book('PHP 7');
    $actual = $book->getBook();
    $this->assertEquals($expected, $book);
  }
}
  1. 现在,我们已经编写了我们的第一个测试。请注意,我们将我们的类命名为BookTest,它继承了PHPUnit_Framework_TestCase类。我们可以随意命名我们的测试类。但是,名称应该容易识别,这样我们就知道这是为需要测试的类编写的。

  2. 然后,我们添加了一个名为testBookClass的方法。我们也可以自由选择给这个方法任何名称,但是它应该以单词test开头。如果不是,PHPUnit 将不会执行该方法,并会发出警告——在我们的情况下,对于前面的测试类,没有找到任何测试。

testBookClass方法中,我们创建了一个Book类的对象,并将PHP 7作为我们的标题传递。然后,我们使用Book类的getBook方法获取标题。重要的部分是testBookClass方法的最后一行,它执行断言并检查从getBook返回的数据是否是期望的数据。

  1. 现在,我们准备运行我们的第一个测试。在项目的根目录中打开命令行或终端,并发出以下命令:
**php phpunit.phar tests/BookTest.php**

当命令被执行时,我们将得到类似以下截图的输出:

测试驱动开发(TDD)

我们的测试成功执行,因为它满足了我们测试中定义的标准。

  1. 现在,让我们稍微改变我们的类,并将PHP传递给Book类,如下面的代码所示:
public function testBookClass()
{
  $book = new Book('PHP');
  $title = $book->getBook();
  $this->assertEquals('PHP 7', $book);
}
  1. 现在,我们正在寻找 PHP 7,我们的Book类返回PHP,所以它没有通过我们的测试。执行此测试后,我们将会失败,如下面的截图所示:测试驱动开发(TDD)

如前面的截图所示,我们期望得到PHP 7,而实际结果是PHP 7-符号显示了期望值,+符号显示了实际值。

注意

在前面的主题中,我们讨论了如何对我们的库执行测试。我们只讨论了一个简单的基本测试。PHPUnit 不仅限于这些简单的测试,但完全覆盖 PHPUnit 超出了本书的范围。一本关于 PHPUnit 的很好的书是 Packt Publishing 出版的PHPUnit Essentials

设计模式

设计模式解决特定问题。它不是一个工具;它只是描述或模板,描述如何解决特定问题。设计模式很重要,在编写清晰的代码方面起着很好的作用。

在 PHP 社区中最广泛使用的设计模式之一是模型视图控制器MVC)模式。大多数 PHP 框架都建立在这种模式之上。MVC 建议将业务逻辑和数据操作(即模型)与表示(视图)分开。控制器只是在模型和视图之间充当中间人的角色,并使它们之间的通信成为可能。模型和视图之间没有直接的通信。如果视图需要任何类型的数据,它会向控制器发送请求。控制器知道如何处理这个请求,并在需要时调用模型对数据进行任何操作(获取、插入、验证、删除等)。然后最后,控制器向视图发送响应。

在最佳实践中,使用肥模型和瘦控制器。这意味着控制器仅用于对请求执行特定操作,而不做其他事情。甚至在一些现代框架中,验证被移出控制器,并在模型层执行。这些模型执行所有数据操作。在现代框架中,模型被视为一个层,可以有多个部分,如业务逻辑、创建读取更新删除CRUD)数据库操作、数据映射器模式和服务等。因此,模型和控制器的全部负载只是坐在那里,享受懒惰的工作负载。

另一个广泛使用的设计模式是工厂设计模式。这种模式简单地创建需要使用的对象。另一个好的模式是观察者模式,其中一个对象在特定事件或任务上调用不同的观察者。这主要用于事件处理。另一个广泛使用的模式是单例模式,当需要在应用程序执行期间仅使用类的单个对象时使用。单例对象无法序列化和克隆。

面向服务的架构(SOA)

在面向服务的架构中,应用程序的组件在定义的协议上为彼此提供服务。每个组件之间松散耦合,它们之间的通信方式是通过它们提供的服务。

在 PHP 中,Symfony 提供了拥有 SOA 的最佳方式,因为它主要是一个以 HTTP 为中心的框架。Symfony 是最成熟、经过充分测试的库集合,被其他 PHP 框架广泛使用,如 Zend Framework、Yii、Laravel 等。

让我们考虑一个情景,我们有一个网站和一个移动应用的后端和前端。通常,在大多数应用程序中,后端和前端在同一代码库和单一访问点上运行,并且为移动应用构建了一个 API 或 Web 服务来与后端通信。这很好,但我们需要更好。因此,对于高性能和可扩展的应用程序,各个组件独立运行。如果它们需要相互通信,它们通过 Web 服务进行通信。

Web 服务是前端和后端之间以及后端和移动应用之间的中心通信点。后端是数据和任何其他业务逻辑的主要枢纽。它可以独立运行,并使用任何编程语言构建,比如 PHP。前端可以使用普通的 HTML/CSS、AngularJS、Node.js、jQuery 或任何其他前端技术构建。同样,移动应用可以是原生的,也可以基于跨平台技术构建。后端不关心前端和移动应用是基于什么构建的。

始终是面向对象和可重用的

对于一个小型的单页应用程序来说,这可能看起来很困难,只有少数事情发生,但事实并非如此。类很容易处理,代码始终清晰。此外,类将应用程序逻辑与视图分离。这使得事情更加合乎逻辑。在早期使用结构化代码和必须在视图文件或单独文件中创建一堆函数时,这将变得太容易。然而,当应用程序变得更加复杂时,处理起来就更加困难。

始终尝试创建松耦合的类,使它们在其他应用程序中更具重用性。此外,始终在类的每个方法中执行单个任务。

PHP 框架

我们都知道框架,并且它们对程序员的生活并非必不可少。有很多框架,每个框架在某些功能上都有其自身的优势。所有框架都很好,但使框架不适合应用程序的是应用程序的需求。

假设我们想构建一个企业级的 CRM 应用程序,哪种框架最适合我们?这是最重要、最令人困惑和最浪费时间的问题。首先,我们需要了解 CRM 应用程序的完整需求、使用容量、功能、数据安全性和性能。

版本控制系统(VCS)和 Git

版本控制系统提供了灵活性,可以正确地维护应用程序的代码、更改和版本。使用 VCS,整个团队可以共同在一个应用程序上工作,他们可以从系统中拉取其他团队成员的更改和自己的更改,而不会有太大的麻烦。在灾难发生时,VCS 提供了回退到旧的、更稳定版本的应用程序的能力。

哦等等!我们在谈论版本控制系统吗?我们提到了 Git 吗?没有!所以,让我们从 Git 开始。

Git 是一个强大的工具。它监视分支中每个文件的更改,当推送到远程分支时,只上传更改的文件。Git 保留文件更改的历史记录,并提供您比较更改文件的能力。

注意

关于 Git 的一本非常信息丰富且优秀的书是 Packt Publishing 出版的Git Essentials。此外,关于 Git 的官方免费书籍可以在git-scm.com/book/en/v2找到。

部署和持续集成(CI)

FTP 已经过时。对于今天来说不可行,它会使事情变慢,而且普通的 FTP 连接是不安全的。团队使用 FTP 部署其更改是困难的,因为它会在他们的代码中造成巨大的冲突,这可能会在上传更改时造成问题,并且可能会覆盖彼此的更改。

使用 Git 版本控制系统,如 GitHub、GitLab 和 Bitbucket,我们可以使部署自动化。不同的开发人员使用不同的自动部署设置,这完全取决于他们自己的选择和便利性。使用自动部署的一般规则是使团队易于使用,并且不使用 FTP。

以下是部署设置的一般流程图:

部署和持续集成(CI)

如前面的流程图所示,我们有两个服务器:暂存或测试服务器和生产服务器。在暂存服务器上,我们有网站的精确副本,用于测试新功能和其他内容,而生产服务器则有我们的实时网站。

现在,我们有一个具有两个主要分支的存储库:主分支和生产分支。主分支用于开发和测试目的,而生产分支用于最终生产功能。请注意,生产分支应仅接受合并,不应接受提交,以便生产环境完全安全。

现在,假设我们想要向我们的应用程序添加客户注册功能。我们将执行以下步骤:

  1. 首先,也是最重要的是从生产分支头部创建一个新分支。让我们将此分支命名为customer-registration

  2. 现在,将所有新功能添加到customer-registration分支,并在本地开发服务器上验证后,将此分支合并到本地主分支。

  3. 将新分支合并到本地主分支后,将主分支推送到远程主分支。成功推送将导致新功能移动到暂存服务器。

  4. 现在,在暂存服务器上测试所有新功能。

  5. 当一切正常时,将远程主分支与远程生产分支合并。这将导致所有更改移动到生产分支,并且此合并将导致所有新更改移动到生产服务器。

  6. 类似于前面的设置,一个理想的设置使部署非常容易,整个团队可以在不同的地理位置工作应用程序。如果在部署过程中出现任何问题,可以轻松地回退到旧版本的生产分支。

持续集成CI)是一种技术,团队的所有成员都必须将其代码集成到共享存储库中,然后团队成员的每次检查都经过自动构建进行验证,以捕获早期阶段的错误和问题。

有几种用于 PHP 的 CI 工具;其中一些是 PHPCI、Jenkins、Travis CI 等。

总结

在本章中,我们讨论了一些最佳实践,包括编码标准和风格、PHP 框架、设计模式、Git 和部署。此外,我们还讨论了 PHPUnit 框架,用于对类和库进行测试。此外,我们还讨论了面向服务的设计,在为应用程序创建 API 方面发挥了重要作用。

在本书中,我们研究了设置开发环境,包括 Linux 服务器,特别是 Debian 和 Ubuntu,我们还讨论了 Vagrant。还列出了 PHP 的新功能,并附有示例代码。您可以详细了解我们可以使用的工具,以提高应用程序和数据库的性能。此外,我们还讨论了调试和应力或负载测试我们的应用程序以及编写高质量代码的一些最佳实践。

我们主要总结了工具和技术,并提供了简单的示例,向读者介绍这些工具和技术。每种工具和技术都有可能有自己的书籍,用于更高级的用途。我们建议您跟进这些工具和技术,并进行更多的研究以了解其高级用途。祝您 Php-ing 好运!

附录 A. 使生活更轻松的工具

我们在本书中涵盖了许多内容,从 PHP 7 中的新功能开始,到编程中的最佳技术结束。在每一章中,我们都使用并讨论了一些工具,但由于章节和书籍的有限长度,我们没有对这些工具进行太多详细介绍。在这个附录中,我们将更详细地讨论其中三个工具。我们将讨论的工具如下:

  • Composer

  • Git

  • Grunt watch

所以,让我们开始吧。

Composer – PHP 的依赖管理器

Composer 是 PHP 的依赖管理工具,它使我们能够为 PHP 应用程序定义依赖关系,并安装/更新它们。Composer 完全由 PHP 编写,并且是 PHP 存档(PHAR)格式的应用程序。

注意

Composer 从packagist.org/下载依赖项。只要在 Packagist 上可用,就可以通过 Composer 安装应用程序的任何依赖项。此外,如果在 Packagist 上可用,还可以通过 Composer 安装完整的应用程序。

Composer 安装

Composer 是一个命令行工具,可以在操作系统中全局安装,或者可以将composer.phar文件放在应用程序的根目录中,然后从命令行执行。对于 Windows,提供了一个可执行的安装程序文件,可以用于全局安装 Composer。对于本书,我们将遵循 Debian/Ubuntu 全局安装的说明。执行以下步骤:

  1. 发出以下命令以下载 Composer 安装程序。文件名为installer,安装后只能通过以下代码使用 PHP 执行:
**Wget https://getcomposer.org/installer**

  1. 发出以下命令在 Debian 或 Ubuntu 上全局安装它:
**Php install --install-dir=/usr/local/bin --filename=composer**

此命令将下载 Composer 并将其安装在/usr/local/bin目录中,文件名为composer。现在,我们将能够全局运行 Composer。

  1. 通过在终端中发出以下命令来验证 Composer 安装:
**Composer --version**

如果显示 Composer 版本,则 Composer 已成功全局安装。

注意

如果 Composer 是安装在应用程序本地的,那么我们将有一个composer.phar文件。命令是相同的,但所有命令都应该使用 PHP 执行。例如,php composer.phar --version将显示 Composer 版本。

现在,Composer 已成功安装并且正在工作;是时候使用它了。

使用 Composer

要在我们的项目中使用 Composer,我们将需要一个composer.json文件。该文件包含项目所需的所有依赖项和一些其他元数据。Composer 使用此文件来安装和更新不同的库。

假设我们的应用程序需要以不同的方式记录不同的信息。为此,我们可以使用monolog库。首先,在应用程序的根目录中创建一个composer.json文件,并添加以下代码:

{
  "require": {
    "monolog/monolog": "1.0.*"
  }
}

保存文件后,执行以下命令安装应用程序的依赖项:

Composer install

此命令将下载依赖项并将它们放在vendor目录中,如下面的屏幕截图所示:

使用 Composer

如前面的屏幕截图所示,下载了 monolog 版本 1.0.2,并创建了一个vendor目录。monolog库放在这个目录中。此外,如果一个包需要自动加载信息,Composer 会将库放在 Composer 自动加载器中,该自动加载器也放在vendor目录中。因此,在应用程序执行期间,任何新的库或依赖项都将自动加载。

还可以看到一个新文件,名为composer.lock。当 Composer 下载和安装任何依赖项时,确切的版本和其他信息将写入此文件,以锁定应用程序到这些依赖项的特定版本。这确保所有团队成员或任何想要设置应用程序的人将使用相同的依赖项版本,从而减少使用不同版本的依赖项的可能性。

如今,Composer 被广泛用于包管理。大型开源项目,如 Magento、Zend Framework、Laravel、Yii 等,都可以通过 Composer 轻松安装。我们将在下一个附录中使用 Composer 安装其中一些。

Git-版本控制系统

Git 是最广泛使用的版本控制系统。根据 Git 官方网站,它是一个分布式版本控制系统,能够处理从小型到大型项目的一切事务,并具有速度和效率。

Git 安装

Git 适用于所有主要操作系统。对于 Windows,提供了一个可执行的安装程序文件,可以用于安装 Git 并在命令行中使用它。在 OS X 上,Git 已经安装好了,但如果找不到,可以从官方网站下载。要在 Debian/Ubuntu 上安装 Git,只需在终端中发出以下命令:

**sudo apt-get install git**

安装完成后,发出以下命令检查是否已正确安装:

**git –version**

然后,我们将查看 Git 的当前安装版本。

使用 Git

为了更好地理解 Git,我们将从一个测试项目开始。我们的测试项目名称是packt-git。对于这个项目,我们还创建了一个名为packt-git的 GitHub 存储库,我们将在其中推送我们的项目文件。

首先,我们将通过发出以下命令在我们的项目中初始化 Git:

**git init**

上述命令将在我们的项目根目录中初始化一个空的 Git 存储库,并将头保留在主分支上,这是每个 Git 存储库的默认分支。它将创建一个名为.git的隐藏目录,其中包含有关存储库的所有信息。接下来,我们将添加一个远程存储库,我们将在 GitHub 上创建。我在 GitHub 上创建了一个测试存储库,其 URL 为github.com/altafhussain10/packt-git.git

现在,发出以下命令将 GitHub 存储库添加到我们的空存储库中:

**git remote add origion https://github.com/altafhussain10/packt-git.git**

现在,在项目根目录创建一个README.md文件,并向其中添加一些内容。README.md文件用于显示存储库信息和有关 Git 存储库的其他详细信息。该文件还用于显示有关如何使用创建此存储库的项目的存储库和/或项目的说明。

现在,发出以下命令来查看我们的 Git 存储库的状态:

**git status**

这个命令将显示存储库的状态,如下面的屏幕截图所示:

使用 Git

如前面的屏幕截图所示,我们的存储库中有一个未跟踪的文件尚未提交。首先,我们将通过在终端中发出以下命令来添加要跟踪的文件:

**git add README.md** 

git add命令使用当前工作树中找到的当前内容更新索引。此命令将添加对路径所做的所有更改。有一些选项可用于添加一些特定更改。我们之前使用的命令将只将README.md文件添加到存储库中进行跟踪。因此,如果我们想要跟踪所有文件,那么我们将使用以下命令:

**git add**

这将开始跟踪当前工作目录中的所有文件或当前分支的根目录。现在,如果我们想要跟踪一些特定的文件,比如所有带有.php扩展名的文件,那么我们可以使用如下方式:

**git add '*.php**

这将添加所有带有.php扩展名的文件进行跟踪。

接下来,我们将使用以下命令提交对我们的存储库的更改或添加:

**git commit –m "Initial Commit"**

git commit命令将所有更改提交到本地存储库。-m标志指定要commit的任何日志消息。请记住,更改只提交到本地存储库。

现在,我们将使用以下命令将更改推送到我们的远程存储库:

**git push –u origion master**

上述命令将把本地存储库中的所有更改推送到远程存储库或原始存储库。-u标志用于设置上游,它将我们的本地存储库链接到我们的远程中央存储库。因为我们第一次推送了更改,所以我们必须使用-u选项。之后,我们只需使用以下命令:

**git push**

这将把所有更改推送到我们当前所在的主分支的主存储库。

创建新分支和合并

在开发过程中总是需要新分支。如果需要任何更改,最好为这些更改创建一个新分支。然后,在此分支上进行所有更改,最后提交、合并并推送到远程存储库。

为了更好地理解这一点,让我们假设我们想要修复登录页面上的问题。问题是关于验证错误的。我们将为我们的新分支命名为login_validation_errors_fix。给分支起一个更易理解的名字是一个好习惯。此外,我们希望从主分支头部创建这个新分支。这意味着我们希望新分支继承主分支的所有数据。因此,如果我们不在主分支上,我们必须使用以下命令切换到主分支:

**git checkout master**

上述命令将无论我们在哪个分支,都会将我们切换到主分支。要创建分支,在终端中发出以下命令:

**git branch login_validation_errors_fix**

现在,我们的新分支是从主分支头部创建的,因此所有更改都应该在这个新分支上进行。完成所有更改和修复后,我们必须将更改提交到本地和远程存储库。请注意,我们没有在远程存储库中创建新分支。现在,让我们使用以下命令提交更改:

**git commit -a -m "Login validation errors fix"**

请注意,我们没有使用git add来添加更改或新添加。为了自动提交我们的更改,我们在commit中使用了-a选项,这将自动添加所有文件。如果使用了git add,则在commit中就不需要使用-a选项。现在,我们的更改已经提交到本地存储库。我们需要将更改推送到远程存储库。在终端中发出以下命令:

**git push -u origion login_validation_errors_fix**

上述命令将在远程存储库创建一个新分支,将相同的本地分支跟踪到远程分支,并将所有更改推送到远程存储库。

现在,我们想要将更改与我们的主分支合并。首先,我们需要使用以下命令切换到我们的主分支:

**git checkout master**

接下来,我们将发出以下命令,将我们的新分支login_validation_errors_fix与主分支合并:

**git checkout master**
**git merge login_validation_errors_fix** 
**git push**

重要的是要切换到我们想要合并新分支的分支。之后,我们需要使用git merge branch_to_merge语法将此分支与当前分支合并。最后,我们只需推送到远程存储库。现在,如果我们查看远程存储库,我们将看到新分支以及主分支中的更改。

克隆存储库

有时,我们需要在托管在存储库上的项目上工作。为此,我们将首先克隆此存储库,这将把完整的存储库下载到我们的本地系统,并为此远程存储库创建一个本地存储库。其余的工作与我们之前讨论的一样。要克隆存储库,我们应该首先知道远程存储库的网址。假设我们想要克隆PHPUnit存储库。如果我们转到 PHPUnit 的 GitHub 存储库,我们将在右上角看到存储库的网址,如下面的截图所示:

克隆存储库

HTTPS按钮后面的 URL 是此存储库的网址。复制此 URL 并使用以下命令克隆此存储库:

**git clone https://github.com/sebastianbergmann/phpunit.git**

这将开始下载存储库。完成后,我们将有一个PHPUnit文件夹,其中包含存储库及其所有文件。现在,可以执行前面主题中提到的所有操作。

Webhooks

Git 最强大的功能之一是 webhooks。Webhooks 是在存储库上发生特定操作时触发的事件。如果对Push请求进行了事件或挂钩,那么每次向此存储库进行推送时都会触发此挂钩。

要向存储库添加 webhook,请单击右上角的设置链接。在新页面中,左侧将有一个Webhooks and Services链接。单击它,我们将看到类似以下页面的页面:

Webhooks

如前面的屏幕截图所示,我们必须输入有效负载 URL,每次选择的事件触发时都会调用它。在内容类型中,我们将选择将有效负载发送到我们的 URL 的数据格式。在事件部分,我们可以选择是否只想要推送事件或所有事件;我们可以选择多个事件,希望此挂钩被触发。保存此挂钩后,每次发生所选事件时都会触发它。

Webhooks 主要用于部署。当更改被推送并且推送事件有一个 webhook 时,将调用特定的 URL。然后,此 URL 执行一些命令来下载更改并在本地服务器上处理它们,并将它们放置在适当的位置。此外,webhooks 用于持续集成和部署到云服务。

管理存储库的桌面工具

有几种工具可用于管理 Git 存储库。GitHub 提供了自己的名为 GitHub Desktop 的工具,可用于管理 GitHub 存储库。它可用于创建新存储库,查看历史记录,并推送、拉取和克隆存储库。它提供了我们可以在命令行中使用的每个功能。接下来的屏幕截图显示了我们的测试packt-git存储库:

管理存储库的桌面工具

注意

GitHub Desktop 可以从desktop.github.com/下载,仅适用于 Mac 和 Windows。此外,GitHub Desktop 只能与 GitHub 一起使用,除非使用一些技巧使其与其他存储库(如 GitLab 或 Bitbucket)一起工作。

另一个强大的工具是 SourceTree。SourceTree 可以轻松与 GitHub、GitLab 和 Bitbucket 一起使用。它提供了完整的功能来管理存储库,包括拉取、推送、提交、合并和其他操作。SourceTree 为分支和提交提供了一个非常强大和美观的图形工具。以下是用于连接到我们的packt-git测试存储库的 SourceTree 的屏幕截图:

管理存储库的桌面工具

除了前面介绍的两个好工具外,每个开发 IDE 都提供完整支持的版本控制系统,并提供诸如不同颜色表示修改和新添加文件等功能。

注意

Git 是一个强大的工具;这个附录无法涵盖它。有几本书可供选择,但 Git Book 是一个很好的起点。可以从git-scm.com/book/en/v2以不同格式下载,也可以在线阅读。

Grunt watch

我们在第三章中学习了 Grunt,改进 PHP 7 应用程序性能。我们只用它来合并 CSS 和 JavaScript 文件并对其进行缩小。然而,Grunt 不仅用于此目的。它是一个 JavaScript 任务运行器,可以通过监视特定文件的更改或手动运行任务来运行任务。我们学习了如何手动运行任务,现在我们将学习如何使用 grunt watch 在进行一些更改时运行特定任务。

Grunt watch 非常有用,可以节省大量时间,因为它会自动运行特定任务,而不是每次更改时手动运行任务。

让我们回顾一下第三章中的例子,改进 PHP 7 应用程序性能。我们使用 Grunt 来合并和压缩 CSS 和 JavaScript 文件。为此,我们创建了四个任务。一个任务是合并所有 CSS 文件,第二个任务是合并所有 JavaScript 文件,第三个任务是压缩 CSS 文件,第四个任务是压缩所有 JavaScript 文件。如果我们每次进行一些更改都要手动运行所有这些任务,那将会非常耗时。Grunt 提供了一个名为 watch 的功能,它会监视不同的目标文件夹以检测文件更改,如果发生任何更改,它会执行在 watch 中定义的任务。

首先,检查grunt watch模块是否已安装。检查node_modules目录,看看是否有另一个名为grunt-contrib-watch的目录。如果有这个目录,那么 watch 已经安装。如果没有这个目录,那么只需在项目根目录中包含GruntFile.js的终端中发出以下命令:

**npm install grunt-contrib-watch**

上面的命令将安装 Grunt watch,grunt-contrib-watch目录将与watch模块一起可用。

现在,我们将修改GruntFile.js文件以添加watch模块,它将监视我们定义的目录中的所有文件,如果发生任何更改,它将自动运行这些任务。这将节省大量时间,不再需要手动执行这些任务。看一下以下代码;高亮显示的代码是修改后的部分:

module.exports = function(grunt) {
  /*Load the package.json file*/
  pkg: grunt.file.readJSON('package.json'),
  /*Define Tasks*/
  grunt.initConfig({
    concat: {
      css: {
      src: [
        'css/*' //Load all files in CSS folder
],
      dest: 'dest/combined.css' //Destination of the final combined file.

      },//End of CSS
js: {
      src: [
        'js/*' //Load all files in js folder
],
       dest: 'dest/combined.js' //Destination of the final combined file.

      }, //End of js

}, //End of concat
cssmin:  {
  css: {
    src : 'dest/combined.css',
    dest : 'dest/combined.min.css' 
}
}, //End of cssmin
uglify: {
  js: {
        files: {
        'dest/combined.min.js' : ['dest/combined.js']//destination Path : [src path]
}
}
}, //End of uglify

//The watch starts here
**watch: {**
 **mywatch: {**
 **files: ['css/*', 'js/*', 'dist/*'],**
 **tasks: ['concat', 'cssmin', 'uglify']**
 **},**
**},**
}); //End of initConfig

**grunt.loadNpmTasks('grunt-contrib-watch'); //Include watch module**
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-cssmin');
grunt.registerTask('default', ['concat:css', 'concat:js', 'cssmin:css', 'uglify:js']);
}; //End of module.exports

在上面的高亮代码中,我们添加了一个watch块。mywatch标题可以是任何名称。files块是必需的,它接受一个源路径的数组。Grunt watch 会监视这些目的地的更改,并执行在 tasks 块中定义的任务。此外,tasks 块中提到的任务已经在GruntFile.js中创建。此外,我们必须使用grunt.loadNpmTasks加载watch模块。

现在,在项目根目录打开终端,其中包含GruntFile.js,并运行以下命令:

**grunt watch**

Grunt 将开始监视源文件的更改。现在,在GruntFile.js中的files块中修改任何文件,并保存该文件。一旦保存文件,任务将被执行,并且任务的输出将显示在终端中。以下截图中可以看到示例输出:

Grunt watch

watch块中可以监视尽可能多的任务,但是这些任务应该存在于GruntFile.js中。

总结

在本附录中,我们讨论了 Composer 以及如何使用它来安装和更新软件包。此外,我们详细讨论了 Git,包括推送、拉取、提交、创建分支和合并不同的分支。此外,我们还讨论了 Git 钩子。最后,我们讨论了 Grunt watch,并创建了一个监视器,每当GruntFile.js中定义的文件路径发生更改时,就会执行四个任务。

附录 B. MVC 和框架

我们在不同章节中提到了一些框架的名称,但没有讨论它们。在今天的世界中,我们不会重新发明轮子;我们会在已经构建、测试和广泛使用的工具基础上进行构建。因此,作为最佳实践,如果没有可用的工具来满足需求,我们可以使用最适合需求的框架来构建它。

我们将涵盖以下主题:

  • MVC 设计模式

  • Laravel

  • Lumen

  • Apigility

MVC 设计模式

模型视图控制器MVC)是一种广泛应用于不同编程语言中的设计模式。大多数 PHP 框架使用这种设计模式。这种模式将应用程序分为三层:模型、视图和控制器。每个层都有不同的任务,并且它们都相互连接。MVC 有不同的视觉表示,但是可以在以下图表中看到一个整体和简单的表示:

MVC 设计模式

现在,让我们讨论 MVC 设计模式的每个部分。

模型

模型层是应用程序的支柱,处理数据逻辑。大多数情况下,认为模型负责对数据库进行 CRUD 操作,这可能是真实的,也可能不是。正如我们之前提到的,模型负责数据逻辑,这意味着数据验证操作也可以在这里执行。简单地说,模型为数据提供了一个抽象。其余的应用层不知道或不关心数据来自何处,或者如何对数据执行操作。这是模型的责任,负责处理所有数据逻辑。

在当今复杂的框架结构中,整体 MVC 结构已经改变,不仅模型处理数据操作,而且每个其他应用逻辑也由模型处理。遵循的方法是“胖模型,瘦控制器”,这意味着将所有应用逻辑放在模型中,使控制器尽可能清晰。

视图

视图是最终用户可见的内容。与此用户和公众相关的所有数据都显示在视图中,因此视图可以被称为模型的视觉表示。视图需要数据来显示。它向控制器请求一些特定的数据或操作。视图不知道或不想知道控制器从何处获取这些数据;它只是要求控制器获取它。控制器知道要向谁请求这些特定的数据,并与特定的模型进行通信。这意味着视图没有直接连接到模型。然而,在早期的图表中,我们直接将模型与视图连接起来。这是因为在现今的先进系统中,视图可以直接从模型获取数据。例如,Magento 控制器无法将数据发送回视图。对于数据(即直接从数据库获取数据)和/或与模型通信,视图与块和辅助类进行通信。在现代实践中,视图可以直接连接到模型。

控制器

控制器响应用户在视图中执行的操作,并响应视图。例如,用户填写表单并提交。在这里,控制器介入并开始对表单的提交采取行动。现在,控制器将首先检查用户是否被允许发出此请求。然后,控制器将采取适当的行动,例如与模型或任何其他操作进行通信。简单地说,控制器是视图和模型之间的中间人。正如我们之前在模型部分提到的,控制器应该是精简的。因此,大多数情况下,控制器仅用于处理请求并与模型和视图进行通信。所有类型的数据操作都在模型中执行。

MVC 设计模式的唯一工作是分离应用程序中不同部分的责任。因此,模型用于管理应用程序数据。控制器用于对用户输入进行操作,视图负责数据的视觉表示。正如我们之前提到的,MVC 分离了每个部分的责任,因此无论是从控制器还是视图访问模型都无关紧要;唯一重要的是视图和控制器不应该用于对数据执行操作,因为这是模型的责任,控制器也不应该用于查看任何类型的数据,因为这是视图的责任。

Laravel

Laravel 是最流行的 PHP 框架之一,根据 Laravel 官方网站的说法,它是一个面向 Web 工匠的框架。Laravel 美观、强大,并且拥有大量功能,可以让开发人员编写高效和高质量的代码。Laravel 官方文档写得很好,非常容易理解。所以,让我们来玩一下 Laravel 吧。

安装

安装非常简单。让我们使用 Composer 来安装 Laravel。我们在附录 A 中讨论了 Composer。在终端中输入以下命令来安装并创建一个 Laravel 项目:

**composer create-project --prefer-dist laravel/laravel packt**

如果系统上没有全局安装 Composer,将composer.phar放在应该安装 Laravel 的目录中,并在该目录的根目录下在终端中输入以下命令:

**php composer.phar create-project --prefer-dist laravel/laravel packt**

现在,Laravel 将被下载,并将创建一个名为packt的新项目。此外,Composer 将下载并安装项目的所有依赖项。

打开浏览器,转到项目的 URL,我们将受到一个简单的页面,上面写着Laravel 5

注意

截至撰写本书时,Laravel 5.2.29 是最新版本。但是,如果使用 Composer,则每次使用composer update命令时,Laravel 和所有其他组件都将自动更新。

功能

Laravel 提供了大量的功能,我们在这里只讨论一些。

路由

Laravel 提供了强大的路由。路由可以分组,并且可以为路由组定义前缀、命名空间和中间件。此外,Laravel 支持所有 HTTP 方法,包括POSTGETDELETEPUTOPTIONSPATCH。所有路由都在应用程序的app文件夹中的routes.php文件中定义。看一下以下示例:

Route::group(['prefix' => 'customer', 'namespace' => 'Customer', 'middleware' => 'web'], function() {
    Route::get('/', 'CustomerController@index');
    Route::post('save', 'CustomerController@save');
    Route::delete('delete/{id}', 'CustomerController@delete');
});

在上面的代码片段中,我们创建了一个新的路由组。只有当 URL 有一个前缀为 customer 时才会使用这个组。例如,如果 URL 类似于domain.com/customer,则将使用此组。我们还使用了一个 customer 命名空间。命名空间允许我们使用标准的 PHP 命名空间并将文件分割成子文件夹。在上面的示例中,所有 customer 控制器可以放在Controllers目录中的 Customer 子文件夹中,并且控制器将如下创建:

namespace App\Http\Controllers\Customer

use App\Http\{
Controllers\Controller,
Requests,
};
use Illuminate\Http\Request;

Class CustomerController extends Controller
{
  …
  …
}

因此,对路由组进行命名空间使我们能够将控制器文件放在易于管理的子文件夹中。此外,我们使用了 web 中间件。中间件提供了一种在进入应用程序之前过滤请求的方法,这使我们可以使用它来检查用户是否已登录,CSRF 保护,或者是否有任何其他需要在请求发送到应用程序之前执行的中间件操作。Laravel 带有一些中间件,包括webapiauth等。

如果路由定义为GET,则不能向该路由发送POST请求。这非常方便,使我们不必担心请求方法过滤。但是,HTML 表单不支持DELETEPATCHPUT等 HTTP 方法。为此,Laravel 提供了方法欺骗,其中使用带有name _method和 HTTP 方法值的隐藏表单字段,以使此请求成为可能。例如,在我们的路由组中,为了使删除路由的请求成为可能,我们需要一个类似于以下的表单:

<form action="/customer/delete" method="post">
  {{ method_field('DELETE') }}
  {{ csrf_field() }}
</form>

当提交上述表单时,它将起作用,并且将使用删除路由。此外,我们创建了一个 CSRF 隐藏字段,用于 CSRF 保护。

注意

Laravel 路由非常有趣,是一个大的话题。更深入的细节可以在laravel.com/docs/5.2/routing找到。

Eloquent ORM

Eloquent ORM 提供了与数据库交互的活动记录。要使用 Eloquent ORM,我们只需从 Eloquent 模型扩展我们的模型。让我们看一个简单的用户模型,如下所示:

namespace App;

use Illuminate\Database\Eloquent\Model;

class user extends Model
{
  //protected $table = 'customer';
  //protected $primaryKey = 'id_customer';
  …
  …
}

就是这样;我们现在有一个可以处理所有 CRUD 操作的模型。请注意,我们已经注释了$table 属性,并对$primaryKey做了相同的操作。这是因为 Laravel 使用类的复数名称来查找表,除非表是使用受保护的$table 属性定义的。在我们的情况下,Laravel 将查找表名 users 并使用它。但是,如果我们想使用名为customers的表,我们只需取消注释该行,如下所示:

protected $table = 'customers';

同样,Laravel 认为表将具有列名id的主键。但是,如果需要另一列,我们可以覆盖默认的主键,如下所示:

protected $primaryKey = 'id_customer';

优雅的模型也使时间戳变得容易。默认情况下,如果表具有created_atupdated_at字段,则这两个日期将自动生成并保存。如果不需要时间戳,可以禁用如下:

protected $timestamps = false;

将数据保存到表中很容易。表列被用作模型的属性,因此,如果我们的customer表具有诸如nameemailphone等列,我们可以在路由部分提到的customer控制器中设置它们,如下所示:

namespace App\Http\Controllers\Customer

use App\Http\{
Controllers\Controller,
Requests,
};
use Illuminate\Http\Request;
use App\Customer

Class CustomerController extends Controller
{
  public function save(Request $request)
  {
    $customer = new Customer();
    $customer->name = $request->name;
    $customer->email = $request->email;
    $customer->phone = $request->phone;

    $customer->save();

  }
}

在上面的示例中,我们向我们的控制器添加了save操作。现在,如果提交了POSTGET请求以及表单数据,Laravel 将所有表单提交的数据分配给一个 Request 对象,作为与表单字段相同名称的属性。然后,使用此请求对象,我们可以访问通过POSTGET提交的所有数据。在将所有数据分配给模型属性(与表列的名称相同)之后,我们只需调用 save 方法。现在,我们的模型没有任何保存方法,但是其父类,即 Eloquent 模型,已经定义了此方法。但是,如果需要此方法中的其他功能,我们可以在我们的model类中覆盖此save方法。

从 Eloquent 模型中获取数据也很容易。让我们尝试一个例子。向customer控制器添加一个新操作,如下所示:

public function index()
{
  $customers = Customer::all();
}

我们在模型中使用了all()静态方法,它基本上是在 Eloquent 模型中定义的,反过来获取了我们的customers表中的所有数据。现在,如果我们想要通过主键获取单个客户,我们可以使用find($id)方法,如下所示:

$customer = Customer::find(3);

这将获取 ID 为3的客户。

更新很简单,使用相同的save()方法,如下所示:

$customer = Customer::find(3);
$customer->name = 'Altaf Hussain';

$customer->save();

这将更新 ID 为3的客户。首先,我们加载了customer,然后我们为其属性分配了新数据,然后调用了相同的save()方法。删除模型简单易行,可以按如下方式完成:

$customer = Customer::find(3);
$customer->delete();

我们首先加载了 ID 为3的客户,然后调用了delete方法,这将删除 ID 为3的客户。

注意

Laravel 的 Eloquent 模型非常强大,并提供了许多功能。这些在文档中有很好的解释,网址为laravel.com/docs/5.2/eloquent。Laravel 数据库部分也值得阅读,网址为laravel.com/docs/5.2/database

Artisan CLI

Artisan 是 Laravel 提供的命令行界面,它有一些很好的命令可以用于更快的操作。它有很多命令,可以使用以下命令查看完整列表:

**php artisan list**

这将列出所有可用的选项和命令。

注意

php artisan命令应该在artisan文件所在的同一目录中运行。它被放置在项目的根目录下。

一些基本命令如下:

  • make:controller: 这个命令在Controllers文件夹中创建一个新的控制器。可以如下使用:
**php artisan make:controller MyController**

如果需要一个有命名空间的控制器,就像之前的Customer命名空间一样,可以如下操作:

**php artisan make:controller Customer/CustomerController**

这个命令将在Customer文件夹中创建CustomerController。如果Customer文件夹不存在,它也将创建该文件夹。

  • make:model: 这在app文件夹中创建一个新的模型。语法与make:controller命令相同,如下:
**php artisan make:model Customer**

对于有命名空间的模型,可以如下使用:

**php artisan make:model Customer/Customer**

这将在Customer文件夹中创建Customer模型,并为其使用Customer命名空间。

  • make:event: 这在Events文件夹中创建一个新的event类。可以如下使用:
**php artisan make:event MyEvent**

  • make:listener: 这个命令为事件创建一个新的监听器。可以如下使用:
**php artisan make:listener MyListener --event MyEvent**

上述命令将为我们的MyEvent事件创建一个新的监听器。我们必须始终使用--event选项提及我们需要创建监听器的事件。

  • make:migration: 这个命令在 database/migrations 文件夹中创建一个新的迁移。

  • php artisan migrate: 这将运行所有尚未执行的可用迁移。

  • php artisan optimize: 这个命令优化框架以获得更好的性能。

  • php artisan down: 这将把应用程序置于维护模式。

  • php artisan up: 这个命令将应用程序从维护模式中恢复。

  • php artisan cache:clear: 这个命令清除应用程序缓存。

  • php artisan db:seed: 这个命令用记录填充数据库。

  • php artisan view:clear: 这将清除所有已编译的视图文件。

注意

有关 Artisan 控制台或 Artisan CLI 的更多详细信息可以在文档中找到,网址为laravel.com/docs/5.2/homestead

迁移

迁移是 Laravel 中的另一个强大功能。在迁移中,我们定义数据库模式——它是创建表、删除表或在表中添加/更新列。迁移在部署中非常方便,并且作为数据库的版本控制。让我们为我们的数据库中尚不存在的 customer 表创建一个迁移。要创建一个迁移,在终端中发出以下命令:

**php artisan make:migration create_custmer_table**

database/migrations文件夹中将创建一个新文件,文件名为当前日期和唯一 ID 前缀的create_customer_table。类被创建为CreateCustomerTable。这是一个如下的类:

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateCustomerTable extends Migrations
{
  //Run the migrations

  public function up()
  {
    //schemas defined here
  }

  public function down()
  {
    //Reverse migrations
  }
}

类将有两个公共方法:up()down()up()方法应该包含表的所有新模式。down()方法负责撤销已执行的迁移。现在,让我们将customers表模式添加到up()方法中,如下:

public function up()
{
  Schema::create('customers', function (Blueprint $table)
  {
    $table->increments('id', 11);
    $table->string('name', 250)
    $table->string('email', 50);
    $table->string('phone', 20);
    $table->timestamps();
  });
}
public function down()
{
  Schema::drop('customers');
}

up()方法中,我们定义了模式和表名。表的列是单独定义的,包括列大小。increments()方法定义了自动增量列,在我们的例子中是id列。接下来,我们为nameemailphone创建了三个字符串列。然后,我们使用了timestamps()方法,它创建了created_atupdated_at时间戳列。在down()方法中,我们只是使用了Schema类的drop()方法来删除customers表。现在,我们需要使用以下命令运行我们的迁移:

**php artisan migrate**

上述命令不仅会运行我们的迁移,还会运行尚未执行的所有迁移。当执行迁移时,Laravel 会将迁移名称存储在一个名为migrations的表中,从中决定要执行哪些迁移以及要跳过哪些迁移。

现在,如果我们需要回滚最近执行的迁移,我们可以使用以下命令:

**php artisan migrate:rollback**

这将回滚到最后一批迁移。要回滚应用程序的所有迁移,我们可以使用 reset 命令,如下所示:

**php artisan migrate:reset**

这将回滚完整的应用程序迁移。

迁移使部署变得容易,因为我们不需要每次在表或数据库中创建一些新的更改时上传数据库模式。我们只需创建迁移并上传所有文件,之后我们只需执行迁移命令,所有模式将被更新。

Blade 模板

Laravel 自带了自己的模板语言 Blade。此外,Blade 模板文件支持普通的 PHP 代码。Blade 模板文件被编译为普通的 PHP 文件,并在更改之前被缓存。Blade 还支持布局。例如,以下是我们在 Blade 中的主页面布局,放在resources/views/layout文件夹中,名为master.blade.php。看一下以下代码:

<!DOCTYPE html>
<html>
  <head>
    <title>@yield('title')</title>
  </head>
  <body>
    @section('sidebar')
      Our main sidebar
      @show

      <div class="contents">
        @yield('content')
      </div>
  </body>
</html>

在上面的例子中,我们有一个定义content部分的侧边栏。此外,我们有@yield,它显示部分的内容。现在,如果我们想要使用这个布局,我们需要在子模板文件中扩展它。让我们在resources/views/文件夹中创建customers.blade.php文件,并将以下代码放入其中:

@extend('layouts.master')
  @section('title', 'All Customers')
  @section('sidebar')
  This will be our side bar contents
  @endsection
  @section('contents')
    These will be our main contents of the page
  @endsection

如前面的代码所示,我们扩展了master布局,然后在master布局的每个部分放置了内容。此外,还可以在另一个模板中包含不同的模板。例如,让我们在resources/views/includes文件夹中有两个文件,sidebar.blade.phpmenu.blade.php。然后,我们可以在任何模板中包含这些文件,如下所示:

@include(includes.menu)
@include(includes.sidebar)

我们使用@include来包含一个模板。点(.)表示文件夹分隔。我们可以轻松地从我们的控制器或路由器向 Blade 模板或视图发送数据。我们只需将数据作为数组传递给视图,如下所示:

return view('customers', ['count => 5]);

现在,在我们的customers视图文件中可以访问count,如下所示:

Total Number of Customers: {{ count }}

是的,Blade 使用双花括号来输出变量。对于控制结构和循环,让我们举一个例子。让我们向customers视图发送数据,如下所示:

return view('customers', ['customers' => $allCustomers]);

现在,如果我们想要显示所有customers数据,我们的customers视图文件将类似于以下内容:

…
…
@if (count($customers) > 0)
{{ count($customers) }} found. <br />
@foreach ($customers as $customer)
{{ $customer->name }} {{ $customer->email }} {{ $customer->phone }} <br>
@endforeach

@else
Now customers found.
@endif;
…
…

所有上述语法看起来很熟悉,因为它几乎与普通的 PHP 相同。但是,要显示一个变量,我们必须使用双花括号{{}}

注意

可以在laravel.com/docs/5.2/blade找到一个易于阅读的 Blade 模板文档。

其他特性

在上一节中,我们只讨论了一些基本功能。Laravel 还有许多其他功能,例如身份验证和授权,提供了一种简单的方式来对用户进行身份验证和授权。此外,Laravel 提供了强大的缓存系统,支持基于文件的缓存、Memcached 和 Redis 缓存。Laravel 还为这些事件提供了事件和监听器,当我们想执行特定操作时以及特定事件发生时,这是非常方便的。Laravel 支持本地化,可以使用本地化内容和多种语言。Laravel 还支持任务调度和队列,我们可以在特定时间安排一些任务运行,并在轮到它们时排队运行一些任务。

Lumen

Lumen 是由 Laravel 提供的微框架。Lumen 主要用于创建无状态 API,并具有 Laravel 的最小功能集。此外,Lumen 与 Laravel 兼容,这意味着如果我们只是将我们的 Lumen 应用程序复制到 Laravel 中,它将正常工作。安装很简单。只需使用以下 Composer 命令创建一个 Lumen 项目,它将下载包括 Lumen 在内的所有依赖项:

**composer create-project --prefer-dist laravel/lumen api**

上述命令将下载 Lumen,然后创建我们的 API 应用程序。完成后,将.env.example重命名为.env。还要创建一个 32 个字符长的应用程序密钥,并将其放入.env文件中。现在,基本应用程序已准备好使用和创建 API。

注意

Lumen 与 Laravel 几乎相同,但默认情况下不包括一些 Laravel 功能。更多细节可以在lumen.laravel.com/docs/5.2找到。

Apigility

Apigility 是由 Zend 在 Zend Framework 2 中构建和开发的。Apigility 提供了一个易于使用的 GUI 来创建和管理 API。它非常易于使用,并能够创建复杂的 API。让我们从使用 Composer 安装 Apigility 开始。在终端中输入以下命令:

**composer create-project -sdev zfcampus/zf-apigility-skeleton packt**

上述命令将下载 Apigility 及其依赖项,包括 Zend Framework 2,并将设置我们的名为packt的项目。现在,发出以下命令以启用开发模式,以便我们可以访问 GUI:

**php public/index.php development enable**

现在,打开 URL yourdomain.com/packt/public,我们将看到一个漂亮的 GUI,如下面的屏幕截图所示:

Apigility

现在,让我们创建我们的第一个 API。我们将称此 API 为“books”,它将返回一本书的列表。单击前面图片中显示的New API按钮,将显示一个弹出窗口。在文本框中输入books作为 API 名称,然后单击Create按钮;新 API 将被创建。创建 API 后,我们将看到以下屏幕:

Apigility

Apigility 提供了设置 API 的其他属性的简单方法,例如版本控制和身份验证。现在,通过单击左侧边栏中的New Service按钮来创建一个 RPC 服务。此外,我们可以在前面的屏幕截图中的RPC部分单击Create a new one链接。我们将看到以下屏幕:

Apigility

如前面的屏幕截图所示,我们在booksAPI 中创建了一个名为get的 RPC 服务。输入的路由 URI 是/books/get,将用于调用此 RPC 服务。当我们单击Create service按钮时,将显示 API 创建成功的消息,并且还将显示以下屏幕:

Apigility

如前面的屏幕截图所示,此服务的允许 HTTP 方法仅为GET。让我们保持原样,但我们可以选择全部或任何一个。此外,我们希望将内容协商选择器保持为Json,并且我们的服务将以 JSON 格式接受/接收所有内容。此外,我们可以选择不同的媒体类型和内容类型。

接下来,我们应该为我们的服务添加一些将要使用的字段。点击字段选项卡,我们将看到字段屏幕。点击新建字段按钮,我们将看到以下弹出窗口:

Apigility

如前面的屏幕截图所示,我们可以为字段设置所有属性,如名称描述、是否必填等,以及一些其他设置,包括验证失败时的错误消息。在创建了两个字段titleauthor之后,我们将看到类似以下的屏幕:

Apigility

如前面的屏幕所示,我们也可以为每个单独的字段添加验证器和过滤器。

注意

由于这只是 Apigility 的入门主题,我们将不会在本书中涵盖验证器、过滤器和其他一些主题。

下一个主题是文档。当我们点击文档选项卡时,我们将看到以下屏幕:

Apigility

在这里,我们将记录我们的服务,添加一些描述,还可以为文档目的生成响应主体。这非常重要,因为它将使其他人更好地理解我们的 API 和服务。

现在,我们需要从某个地方获取所有的书。可以是从数据库中获取,也可以是从另一个服务或其他来源获取。然而,现在,我们只是为了测试目的,将使用一组书的数组。如果我们点击来源选项卡,我们会发现我们服务的代码放在module/books/src/books/V1/Rpc/Get/GetController.php中。Apigility 为我们的 APIbooks创建了一个模块,然后根据 API 的版本(默认为 V1),将所有源代码放在这个模块的不同文件夹中。我们可以为我们的 API 添加更多版本,如 V2 和 V3。现在,如果我们打开GetController文件,我们会发现一些代码和一个根据我们的路由 URI 命名为getAction的操作。代码如下,高亮显示的是我们添加的代码:

namespace books\V1\Rpc\Get;

use Zend\Mvc\Controller\AbstractActionController;
**use ZF\ContentNegotiation\ViewModel;**

class GetController extends AbstractActionController
{
  public function getAction()
  {
    **$books = [ 'success' => [**
 **[**
 **'title' => 'PHP 7 High Performance',**
 **'author' => 'Altaf Hussain'**
 **],**
 **[**
 **'title' => 'Magento 2',**
 **'author' => 'Packt Publisher'**
 **],**
 **]**
 **];**

 **return new ViewModel($books);**
  }
}

在上面的代码中,我们使用了ContentNegotiation\ViewModel,它负责以我们在服务设置中选择的格式(在我们的情况下是 JSON)响应数据。然后,我们创建了一个简单的$books数组,其中包含我们为服务创建的字段名,并为它们分配了值。然后,我们使用ViewModel对象返回它们,该对象处理响应数据转换为 JSON。

现在,让我们测试我们的 API。由于我们的服务可以接受GET请求,我们只需在浏览器中输入带有books/get URI 的 URL,就会看到 JSON 响应。最好使用 RestClient 或 Google Chrome 的 Postman 等工具来检查 API,这些工具提供了一个易于使用的界面,可以向 API 发出不同类型的请求。我们使用 Postman 进行了测试,并得到了以下截图中显示的响应:

Apigility

还要注意,我们将我们的服务设置为仅接受GET请求。因此,如果我们发送的请求不是GET,我们将收到HTTP 状态码 405 方法不允许的错误。

Apigility 非常强大,提供了许多功能,如 RESTFul API、HTTP 身份验证、与易于创建的数据库连接器连接的数据库服务,以及服务的表格选择。在使用 Apigility 时,我们不需要担心 API、服务结构安全性和其他事情,因为 Apigility 会为我们处理这些。我们只需要专注于 API 和服务的业务逻辑。

注意

Apigility 无法在本附录中完全涵盖。Apigility 有很多功能,可以在一本完整的书中进行介绍。Apigility 的官方文档网址apigility.org/documentation是一个很好的起点,可以了解更多信息。

摘要

在本附录中,我们讨论了 MVC 设计模式的基础知识。我们还讨论了 Laravel 框架及其一些优秀特性。我们向你介绍了基于 Laravel 的微框架 Lumen。最后,我们对 Apigility 进行了简要介绍,并创建了一个测试 API 和 Web 服务。

在 IT 领域,事物很快就会过时。总是需要学习升级的工具,寻找编程中最佳方法的新途径和技术。因此,完成本书后不应该停止学习,而是开始研究新的主题,以及本书中未完全涵盖的主题。到这一点,你将拥有知识,可以用来建立高性能应用程序的高性能环境。祝你在 PHP 编程中好运和成功!

posted @ 2024-05-05 00:11  绝不原创的飞龙  阅读(42)  评论(0编辑  收藏  举报