PHP8-对象-模式和实践-十-

45 阅读54分钟

PHP8 对象、模式和实践(十)

原文:PHP 8 Objects, Patterns, and Practice

协议:CC BY-NC-SA 4.0

二十、Vagrant

你在哪里运行你的代码?

也许你有一个开发环境,你已经用一个最喜欢的编辑器和许多有用的开发工具磨练到完美。当然,您编写代码的完美设置可能与运行代码的最佳系统大相径庭。这是一个挑战,流浪者可以帮助你。使用 vagger,您可以在本地机器上工作,并在与您的生产服务器完全相同的系统上运行您的代码。在这一章中,我将告诉你如何做。我们将涵盖以下内容:

  • 基本设置:从安装到选择第一个盒子

  • 登录:用 ssh 调查你的虚拟机

  • 挂载主机目录:在你的主机上编辑代码,并让它透明地出现在你的流浪者盒子里

  • Provisioning :编写脚本安装软件包,配置 Apache 和 MySQL

  • 设置主机名:配置您的机顶盒,以便您可以使用自定义主机名访问它

问题

像往常一样,让我们花一点时间来定义问题空间。如今,在大多数台式机或笔记本电脑上配置灯组相对容易。即便如此,个人电脑也不太可能与您的生产环境相匹配。它运行的是同一版本的 PHP 吗?Apache 和 MySQL 呢?如果你正在使用 Elasticsearch,你可能也需要考虑 Java。这个名单很快就变长了。如果您的产品堆栈非常不同,在特定平台上使用一组工具进行开发有时会有问题。

你可能会放弃,将你的开发转移到一台远程机器上——有很多云供应商可以让你快速运转机器。但是这不是一个免费的选项,而且,根据您选择的编辑器,远程系统可能无法与您希望使用的开发工具很好地集成。

因此,尽可能地将您计算机上的软件包与安装在生产系统上的软件包匹配起来可能是值得的。匹配不会是完美的,但也许会足够好,并且您可能会在登台服务器上发现大多数问题。

然而,当你开始进行另一个需求完全不同的项目时,会发生什么呢?我们已经看到 Composer 在分离依赖项方面做得很好,但是仍然有像 PHP、MySQL 和 Apache 这样的全局包需要保持一致。

Note

如果您决定在远程系统上开发,我建议您努力学习如何使用 vim 编辑器。尽管它有些古怪,但它非常强大,您可以 99%确定 vim 或它的更基本的祖先 vi 可以在您遇到的任何类 Unix 系统上使用。

虚拟化是一个潜在的解决方案,也是一个很好的解决方案。不过,安装操作系统可能是一件痛苦的事,而且可能会有相当多的配置问题。

要是有一种工具能让在本地机器上创建类似生产的开发环境变得非常简单就好了。好了,很明显,现在我要说的就是这样一个工具的存在。嗯,有一个是。它叫《流浪》,真的很神奇。

一个小陷阱

很有诱惑力的说法是,流浪者给你一个单一命令的开发环境。那个可能是真的——但是你必须先安装必要的软件。考虑到这一点,以及您可以从项目的版本控制存储库中签出的配置文件,启动一个新环境确实只需要一个命令。

让我们先开始设置。流浪者需要一个虚拟化平台。它支持几个,但我会使用 VirtualBox。我的主机运行 Fedora,但你可以在任何 Linux 发行版和 OSX 或 Windows 上安装 VirtualBox。您可以在 www.virtualbox.org/wiki/Downloads 找到下载页面,以及针对您的平台的说明。

当然,一旦你安装了 VirtualBox,你将需要一个流浪者。下载页面在 www.vagrantup.com/downloads.html 。一旦我们安装了这些应用,我们的下一个任务将是选择运行代码的机器。

选择和安装流浪盒

大概最简单的获得流浪盒子的方法就是使用 https://app.vagrantup.com/boxes/search 的搜索界面。由于许多生产系统运行 CentOS,这就是我要寻找的。你可以在图 20-1 中看到我的研究成果。

img/314621_6_En_20_Fig1_HTML.jpg

图 20-1

寻找一个流浪的盒子

CentOS 7 看起来很适合我的需求。我可以点击我感兴趣的框的列表来获取设置说明。这给了我足够的信息来运行一个流浪环境。通常当你运行 named 时,它会读取一个名为Vagrantfile的配置文件——但由于我是从头开始,所以我需要让 named 生成一个:

$ vagrant init bento/centos-7

A `Vagrantfile` has been placed in this directory. You are now
ready to `vagrant up` your first virtual environment! Please read
the comments in the Vagrantfile as well as documentation on
`vagrantup.com` for more information on using Vagrant.

如您所见,我将我想要使用的机器的名称传递给了 vagger,它使用这些信息来生成一些最小的配置。

如果我打开生成的Vagrantfile文档,我可以看到这个(在许多其他样板文件中):

# Every Vagrant development environment requires a box. You can search for
# boxes at https://vagrantcloud.com/search.
config.vm.box = "bento/centos-7"

至此,我只完成了生成配置的工作。接下来,我必须运行非常重要的vagrant up命令。如果你经常和流浪者一起工作,你会很快发现这个命令非常熟悉。它通过下载和提供您的新盒子(如果需要)来启动您的漫游会话,然后启动它:

$ vagrant up

因为我是第一次用bento/centos-7虚拟机运行这个命令,所以 vagger 从下载盒子开始:

==> default: Box 'bento/centos-7' could not be found. Attempting to find and install...
    default: Box Provider: virtualbox
    default: Box Version: >= 0
==> default: Loading metadata for box 'bento/centos-7'
    default: URL: https://vagrantcloud.com/bento/centos-7
==> default: Adding box 'bento/centos-7' (v202008.16.0) for provider: virtualbox
    default: Downloading:
https://vagrantcloud.com/bento/boxes/centos-7/versions/202008.16.0/providers/ virtualbox.box
    default: Download redirected to host: vagrantcloud-files-production.s3.amazonaws.com
==> default: Successfully added box 'bento/centos-7' (v202008.16.0) for 'virtualbox'!
...

vagger 存储了这个盒子(如果你运行的是 Linux,在~/.vagrant.d/boxes/下),这样你就不必在你的系统上再次下载它——即使你运行多个虚拟机。然后它配置并引导机器(它提供了很多细节)。一旦它运行完毕,我就可以登录到我的新机器上进行测试了:

$ vagrant ssh
$ pwd

/home/vagrant

$ cat /etc/redhat-release

CentOS Linux release 7.8.2009 (Core)

我们进去了。那么我们赢得了什么?嗯,我们可以使用一台有点像我们生产环境的机器。还有别的吗?事实上,相当多。我之前说过,我想在本地机器上编辑文件,但在类似生产的空间中运行它们。我们来设置一下。

是时候再次离开盒子,回到主机上了:

$ exit

在流浪者盒子上安装本地目录

让我们把一些样本文件放在一起。我在一个我命名为infrastructure的目录中运行了我的第一个vagrant initvagrant up命令。我将重新启用我在第十八章中使用的webwoo项目(我为第十二章开发的系统的精简版)。综上所述,我的开发环境看起来有点像这样:

ch20/
    infrastructure/
        Vagrantfile
    webwoo/
        AddVenue.php
        index.php
        Main.php
        AddSpace.php

我们面临的挑战是设置环境,以便我们可以在本地处理webwoo文件,但使用 CentOS box 上安装的堆栈透明地运行它们。根据我们的配置,vagger 将尝试在来宾系统中的主机上安装目录。事实上,流浪者已经为我们安装了一个目录。让我们来看看:

$ vagrant ssh

Last login: Wed Sep 23 16:46:53 2020 from 10.0.2.2

$ ls -a /vagrant

.  ..    .vagrant    Vagrantfile

所以流浪者把infrastructure目录挂载为盒子上的/vagrant。当我们编写一个脚本来配置机器时,这将派上用场。不过现在,让我们集中精力安装webwoo目录。我们可以通过编辑Vagrantfile来做到这一点。不过,首先,现在可能是再次退出虚拟机的好时机。一旦我这样做了,我打开Vagrantfile,添加这一行:

config.vm.synced_folder "../webwoo", "/var/www/poppch20"

通过搜索带注释的样板文件中的字符串synced_folder,我可以找到放置这一行的最佳位置。我发现一个示例配置行看起来很像我自己的。通过这个指令,我告诉流浪者在/var/www/poppch20的访客箱上安装webwoo目录。为了看到效果,我需要重启机器。为此有一个新命令(应该在主机系统上运行,而不是在虚拟机中运行):

$ vagrant reload

虚拟机会完全关闭并重新启动。流浪者挂载infrastructure ( /vagrant)和webwoo ( /var/www/poppch20)目录。以下是该命令输出的摘录:

==> default: Mounting shared folders...
    default: /vagrant => /home/mattz/localwork/popp/ch20-vagrant/infrastructure
    default: /var/www/poppch20 => /home/mattz/localwork/popp/ch20-vagrant/webwoo

我可以快速登录以确认/var/www/poppch20已就位:

$ vagrant ssh
$ ls /var/www/poppch20/

AddSpace.php  AddVenue.php  index.php  Main.php

所以现在我可以在我的本地机器上运行一个性感的 IDE,并让它所做的更改透明地出现在来宾操作系统上!

Note

来自技术评论者和 Windows 用户 Paul Tregoing 的注释:如果运行 Windows 主机,不要使用 VirtualBox 共享文件系统(在本例中,它支撑着 vagger 的同步文件夹)。如果这样做,您可能会遇到区分大小写和缺少符号链接支持的问题。在这种情况下,最好在客户操作系统上运行 Samba(大多数发行版都将其安装为smbd)并在主机上映射一个网络驱动器,以获得更无缝的体验。这方面有很多在线指南。

当然,将文件放在 CentOS 虚拟机上并不等同于运行系统。一个典型的流浪者盒子没有太多的预装。假设开发者想要根据需要和环境定制环境。

下一步是配置我们的机器。

准备金提取

同样,调配由Vagrantfile文档指导。游民支持几个为置备机器设计的工具,包括 Chef ( www.chef.io/chef/ )、Puppet ( https://puppet.com )和 Ansible ( www.ansible.com )。都值得调查。但是,出于这个示例的目的,我将使用一个很好的老式 shell 脚本。

我再一次从Vagrantfile开始:

config.vm.provision "shell", path: "setup.sh"

这应该是相当清楚的。我告诉 vagger 使用一个 shell 脚本来提供我的机器,我指定setup.sh作为应该执行的脚本。

当然,在 shell 脚本中放入什么取决于您的需求。我将从设置几个变量和安装一些包开始:

#!/bin/bash

VAGRANTDIR=/vagrant
SERVERDIR=/var/www/poppch20/

sudo yum -q -y install epel-release yum-utils
sudo yum -q -y install http://rpms.remirepo.net/enterprise/remi-release-7.rpm

yum-config-manager --enable  remi-php80

sudo yum -q -y install mysql-server
sudo yum -q -y install httpd;
sudo yum -q -y install php
sudo yum -q -y install php-common
sudo yum -q -y install php-cli
sudo yum -q -y install php-mbstring
sudo yum -q -y install php-dom
sudo yum -q -y install php-mysql
sudo yum -q -y install php-xml
sudo yum -q -y install php-dom

PHP 8 在 CentOS 7 上默认不可用。然而,通过安装包remi-release-7.rpm,我能够安装 PHP 的新版本。我把我的脚本写到一个名为setup.sh的文件中,我把它放在基础设施目录中Vagrantfile的旁边。

现在,我该如何开始调配流程呢?如果在我运行vagrant upconfig.vm.provision指令和setup.sh脚本都已经就绪,那么供应将是自动的。实际上,我现在需要手动运行它:

$ vagrant provision

setup.sh脚本在 travel box 中运行时,这将会在你的终端上产生大量的信息。让我们看看它是否有效:

$ vagrant ssh
$ php -v

PHP 8.0.0 (cli) (built: Nov 24 2020 17:04:03) ( NTS gcc x86_64 )
Copyright (c) The PHP Group
Zend Engine v4.0.0-dev, Copyright (c) Zend Technologies

设置 Web 服务器

当然,即使安装了 Apache,系统也不能运行。首先要配置 Apache。最简单的方法是创建一个可以复制到 Apache 的conf.d目录中的配置文件。让我们调用文件poppch20.conf并将其放入基础设施目录:

<VirtualHost *:80>
    ServerAdmin matt@getinstance.com
    DocumentRoot /var/www/poppch20
    ServerName poppch20.vagrant.internal
    ErrorLog logs/poppch20-error_log
    CustomLog logs/poppch20-access_log common
</VirtualHost>

<Directory /var/popp/wwwch20>
AllowOverride all
</Directory>

稍后我将返回到该主机名。抛开那些诱人的细节,这足以告诉 Apache 我们的/var/www/poppch20目录并设置日志记录。当然,我还必须更新setup.sh,以便在供应时复制配置文件:

sudo cp $VAGRANTDIR/poppch20.conf /etc/httpd/conf.d/
systemctl start httpd
systemctl enable httpd

我将配置文件复制到适当的位置,并重新启动 web 服务器,这样就可以获得配置了。我还运行systemctl enable来确保服务器将在引导时启动。

进行了这一更改后,我可以重新运行该脚本:

$ vagrant provision

需要注意的是,我们之前提到的安装脚本的那些部分也将被重新运行。创建配置脚本时,必须将其设计为可以重复执行而不会产生严重影响。幸运的是,Yum 检测到我指定的包已经被安装,并发出无害的抱怨,部分原因是我采取了预防措施,传递了它的-q标志,这使得抱怨相对较少。

设置 MariaDB

对于许多应用,您需要确保数据库可用并准备好连接。下面是对我的设置脚本的一个简单补充:

sudo yum -q -y install mariadb-server
systemctl start mariadb
systemctl enable mariadb

/usr/bin/mysqladmin -s -u root password 'vagrant' || echo " -- unable to create pass - probably already done"
domysqldb vagrant poppch20_vagrant vagrant vagrant

ROOTPASS=vagrant
DBNAME=poppch20_vagrant
DBUSER=vagrant
DBPASS=vagrant
MYSQL=mysql
MYSQLROOTCMD="mysql -uroot    -p$ROOTPASS"

echo "creating database $DBNAME..."
echo "CREATE DATABASE IF NOT EXISTS $DBNAME" | $MYSQLROOTCMD || die "unable to create db";

echo "grant all on $DBNAME.* to $DBUSER@'localhost' identified by \"$DBPASS\""  |
$MYSQLROOTCMD || die "unable to grand privs for user $DBUSER"
echo "FLUSH PRIVILEGES" | $MYSQL -uroot -p"$ROOTPASS" || die "unable to flush privs"

我安装了 MariaDB,它是 MySQL 的现代替代品(由 MySQL 开发人员创建,在实现熟悉的 MySQL 工具和命令方面是兼容的)。我运行mysqladmin命令来创建一个 root 密码。这将在第一次运行后失败,因为密码已经设置好了,所以我使用-s标志来隐藏错误消息,并在命令失败时打印我自己的消息。然后我创建一个数据库、一个用户和一个密码。

准备就绪后,我可以再次调配资源,然后测试我的数据库:

$ vagrant provision

# much output

$ vagrant ssh
$ mysql -uvagrant -pvagrant poppch20_vagrant

Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 8
Server version: 5.5.65-MariaDB MariaDB Server

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [poppch20_vagrant]>

我们现在有了一个正在运行的数据库和一个 web 服务器。是时候看看代码是如何运行的了。

配置主机名

我们已经多次登录到我们新的类似生产的开发环境中,因此网络或多或少得到了关注。尽管我已经配置了一个 web 服务器,但我还没有使用过它。这是因为我们仍然需要为我们的虚拟机支持一个主机名。所以我们给Vagrantfile加一:

config.vm.hostname = "poppch20.vagrant.internal"
config.vm.network :private_network, ip: "192.168.33.148"

我发明了一个主机名,并使用config.vm.hostname指令添加它。我还用config.vm.network配置了私有网络,分配了一个静态 IP 地址。您应该为此使用私有地址空间——以192.168开头的未使用的 IP 地址应该可以。

因为这是一个虚构的主机名,所以我们必须配置我们的操作系统来处理这个解析。在类 Unix 系统上,这意味着编辑系统文件/etc/hosts。在这种情况下,我要补充以下内容:

192.168.33.148    poppch20.vagrant.internal

Note

Windows 上的 hosts 文件可以在%windir%\system32\drivers\etc\hosts找到。

不太麻烦,但是我们正在为我们的团队进行一个命令安装,所以有一个自动化这个步骤的方法是很好的。幸运的是,Vagrant 支持插件,hostmanager插件正是我们所需要的。要添加插件,只需运行vagrant plugin install命令:

$ vagrant plugin install vagrant-hostmanager

Installing the 'vagrant-hostmanager' plugin. This can take a few minutes...
Installed the plugin 'vagrant-hostmanager (1.8.9)'!

然后你可以显式的告诉插件更新/etc/hosts,像这样:

$ vagrant hostmanager

[default] Updating /etc/hosts file...

为了让团队成员自动完成这个过程,我们应该在Vagrantfile中显式启用 hostmanager:

config.hostmanager.enabled = true

配置更改就绪后,我们应该运行vagrant reload来应用它们。那就是真相大白的时刻了!我们的系统会在浏览器中运行吗?正如你在图 20-2 中看到的,这个系统应该可以正常工作。

img/314621_6_En_20_Fig2_HTML.jpg

图 20-2

访问流浪盒上的已配置系统

包装它

因此,我们已经从一无所有到一个完全工作的开发环境。考虑到花了整整一章的努力才到达这里,说流浪者又快又容易似乎有点欺骗。对此有两个答案。首先,一旦你这样做了几次,建立另一个浮动设置就变得非常简单了——当然比手动处理多个依赖栈要容易得多。

然而,更重要的是,真正的速度和效率的提高并不在于设置流浪者的人。想象一下,一个新的开发人员来到您的项目,期待几天的下载、配置文件编辑和 wiki 点击。想象一下,你告诉她,“安装流浪者和 VirtualBox。查看代码。从基础结构目录中,运行“向上漫游”。就这样!与你经历过或听到过的一些痛苦的入职流程相比。

当然,在这一章中我们仅仅触及了表面。由于您需要配置流浪者为您做更多的事情, www.vagrantup.com 的官方网站将为您提供所需的一切支持。

表 20-1 提供了本章中我们遇到的浮动命令的快速提示。

表 20-1

一些流浪的命令

|

命令

|

描述

| | --- | --- | | vagrant up | 启动虚拟机并进行配置(如果尚未配置) | | vagrant reload | 暂停系统并重新启动(除非使用--provision标志运行,否则不会再次运行配置) | | vagrant plugin list | 列出已安装的插件 | | vagrant plugin install <plugin-name> | 安装插件 | | vagrant provision | 再次运行预配步骤(如果您已经更新了预配脚本,这将非常有用) | | vagrant halt | 正常关闭虚拟机 | | vagrant suspend | 停止虚拟机进程并保存状态 | | vagrant resume | 恢复先前挂起的虚拟机进程 | | vagrant init | 创建新的流浪者文件文档 | | vagrant destroy | 摧毁虚拟机。别担心,你随时可以用vagrant up重新开始! |

摘要

在这一章中,我介绍了 vagger,这个应用可以让您在一个类似生产的开发环境中工作,而不会牺牲您的创作工具。我讲述了安装、发行版的选择和初始设置——包括安装您的开发目录。一旦我们有了一个可以使用的虚拟机,我就进入了配置过程——包括软件包安装以及数据库和 web 服务器配置。最后,我查看了主机名管理,并展示了我们的系统在浏览器中的工作情况!

二十一、持续集成

在前面的章节中,您已经看到了大量被设计来支持一个管理良好的项目的工具。单元测试、文档、构建和版本控制都非常有用。但是工具,尤其是测试,会很麻烦。

即使你的测试只需要几分钟就能运行,你也经常太专注于编码而不去理会它们。不仅如此,你还有客户和同事在等待新的特性。坚持编码的诱惑总是存在的。但是 bug 在接近孵化时更容易修复。这是因为您更有可能知道哪个变化导致了问题,并且更有能力提出快速解决方案。

在这一章中,我将介绍持续集成,这是一种自动化测试和构建的实践,它将您在最近几章中遇到的工具和技术结合在一起。

本章将涵盖以下主题:

  • 定义持续集成

  • 为 CI 准备项目

  • 看 Jenkins:CI 服务器

  • 用专门的插件为 PHP 项目定制 Jenkins

什么是持续集成?

在过去糟糕的日子里,集成是在你完成有趣的事情后做的事情。这也是你意识到还有多少工作要做的阶段。集成是这样一个过程,通过这个过程,您的项目的所有部分都被打包成可以运输和部署的包。这并不迷人,而且实际上很难。

集成也与 QA 紧密相关。如果产品不符合用途,你就不能发货。这意味着测试。很多测试。如果您在集成阶段之前没有进行太多的测试,这可能也意味着令人讨厌的惊喜。很多人。

从第十八章你知道,最好的做法是尽早并经常进行测试。从第 15 和 19 章中你知道,你应该从一开始就在头脑中设计部署。我们大多数人都认为这是理想状态,但是现实和理想有多匹配呢?

如果你实践面向测试的开发(这个术语我更喜欢测试优先的开发,因为它更好地反映了我见过的大多数好项目的现实),那么编写测试没有你想象的那么难。毕竟,无论如何,你都是一边编码一边写测试。每次开发一个组件时,您都会创建代码片段,可能在类文件的底部,这些代码片段实例化对象并调用它们的方法。如果你收集那些一次性的代码碎片,在开发过程中测试你的组件,你就有了一个测试用例。将它们放入一个类中,并添加到您的套件中。

奇怪的是,人们通常会回避测试的运行。随着时间的推移,测试需要更长的时间来运行。与已知问题相关的故障不断出现,使得诊断新问题变得困难。此外,您怀疑其他人提交了破坏测试的代码,并且您没有时间停下自己的工作来修复其他人造成的问题。最好运行几个与您的工作相关的测试,而不是整个套件。

未能运行测试,因此未能修复它们可能揭示的问题,使得问题越来越难以解决。寻找 bug 的最大开销通常是诊断,而不是治疗。通常,修复可以在几分钟内完成,而不是花几个小时去寻找测试失败的原因。然而,如果测试在提交后的几分钟或几小时内失败,您更有可能知道在哪里查找问题。

软件构建也有类似的问题。如果您不经常安装您的项目,您可能会发现,尽管在您的开发机器上一切运行良好,但安装的实例会出现一个模糊的错误消息。构建间隔的时间越长,失败的原因对你来说就越模糊。

通常是一些简单的事情:对系统上的库的未声明的依赖,或者您未能签入的一些类文件。如果你手头有,这些很容易解决。但是,如果您不在办公室时发生构建失败,该怎么办呢?无论哪个不幸的团队成员得到了构建和发布项目的工作,都不会知道您的设置,也不会容易地访问那些丢失的文件。

项目中涉及的人数越多,集成问题就越严重。你可能喜欢并尊重你所有的团队成员,但是我们都知道他们比你更有可能不进行测试。然后,他们在周五下午 4 点提交一周的开发工作,就在你准备宣布项目可以发布的时候。

持续集成(CI)通过自动化构建和测试过程来减少这些问题。

CI 既是一套实践,也是一套工具。作为一种实践,它需要频繁地提交项目代码(至少每天一次)。每次提交时,都应该运行测试并构建任何包。您已经看到了 CI 所需的一些工具,特别是 PHPUnit 和 Phing。然而,单独的工具是不够的。需要更高级别的系统来协调和自动化该过程。

如果没有更高的系统,CI 服务器,CI 实践很可能会屈服于我们跳过杂务的自然倾向。毕竟,我们宁愿编码。

拥有这样的系统有明显的好处。首先,您的项目经常被构建和测试。这是 CI 的最终目的和效益。然而,它的自动化又增加了两个维度。测试和构建发生在与开发不同的线程中。它发生在幕后,不需要您停止工作来运行测试。同样,和测试一样,CI 鼓励好的设计。为了能够在远程位置自动安装,您必须从一开始就考虑安装的方便性。

我不知道我遇到过多少次这样的项目,安装过程是只有少数开发人员知道的神秘秘密。"你是说你没有设置 URL 重写?"一个老手带着几乎不加掩饰的轻蔑问道。“老实说,重写规则在维基中是,你知道。只需将它们粘贴到 Apache 配置文件中。”开发时考虑 CI 意味着让系统更容易测试和安装。这可能意味着前期工作要多一点,但它让我们以后的生活更轻松。简单多了。

所以,首先,我要打下一些昂贵的基础。事实上,您会发现在接下来的大部分章节中,您已经遇到了这些准备步骤。

为 CI 准备项目

首先,当然我需要一个项目不断整合。现在,我是一个懒惰的人,所以我会寻找一些已经写好的测试附带的代码。显而易见的候选者是我在第十八章中创建的用来说明 PHPUnit 的项目。我要把它命名为userthing,因为它是一个的东西,里面有一个User的物体。

Note

本章中描述的一些工具要么是最近才发布的,要么在撰写本文时还处于测试阶段。这导致了一些怪癖和不兼容性。当你读到这篇文章时,这些问题很可能已经解决了。然而,为了构建一个可靠的 CI 系统,我不得不为这些例子回滚到 PHP 和 PHPUnit 的早期版本。这对显示的代码和配置应该没有影响。

首先,这里是我的项目目录的明细:

$ find src/ test/

src/
src/persist
src/persist/UserStore.php
src/util
src/util/Validator.php
src/domain
src/domain/User.php
test/
test/persist
test/persist/UserStoreTest.php
test/util
test/util/ValidatorTest.php

如您所见,我稍微整理了一下结构,添加了一些包目录。在代码中,我使用名称空间来支持包结构。

现在我有了一个项目,我应该将它添加到版本控制系统中。

CI 和版本控制

版本控制对 CI 至关重要。一个 CI 系统需要在没有人工干预的情况下获得一个项目的最新版本(至少在设置好之后)。

对于这个例子,我将使用我在 Bitbucket 上建立的存储库。我将在本地开发机器上配置代码,添加并提交它,然后推送到远程服务器:

$ cd path/to/userthing
$ git init
$ git remote add origin git@bitbucket.org:getinstance/userthing.git
$ git add build.xml composer.json src/ test/
$ git commit -m 'initial commit'
$ git push -u origin master

Note

在这些例子中,我已经创建并使用了一个位存储库。我可以很容易地使用 GitHub,它现在支持免费的私有库。

我导航到我的开发目录并初始化它。然后,我添加了origin遥控器,并向其推送代码。我喜欢通过执行新的克隆来确认一切正常:

$ git clone git@bitbucket.org:getinstance/userthing.git

Cloning into 'userthing'...
X11 forwarding request failed on channel 0
remote: Counting objects: 16, done.
remote: Compressing objects: 100% (11/11), done.
remote: Total 16 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (16/16), done.
Checking connectivity… done.

现在我有了一个userthing存储库和一个本地克隆。自动化构建和测试的时间到了。

Phing

我们在第十九章遇到了 Phing。对于这一章,我将使用随 composer 一起安装的版本。

    "require-dev": {
        "phing/phing": "3.*"
    },

Note

撰写本章时,Phing 3 仍在 alpha 测试中,composer 安装由于依赖问题而失败。phar 版本和 PHP 8 也有问题。因此,为了本章的目的,我们对 Phing 进行了分叉和修补,以解决各种代码和兼容性问题。希望,当你读到这篇文章时,Phing 又一次稳定了!你可以在首页找到 Phing 的版本信息和安装说明: www.phing.info/#install

我将使用这个构建工具作为我的项目 CI 环境的粘合剂,所以我在我打算用于测试的服务器上运行这个安装(或者,当然,您可以在使用 vagger 和 VirtualBox 的虚拟服务器上尝试这个)。我将定义构建和测试代码的目标,以及运行您将在本章中遇到的各种其他质量保证工具的目标。

让我们构建一个示例任务:

// listing 21.01
<project name="userthing" default="build" basedir=".">
    <property name="build" value="./build" />
    <property name="test" value="./test" />
    <property name="src" value="./src" />
    <property name="version" value="1.1.1" />

    <target name="build">
        <mkdir dir="${build}" />
        <copy todir="${build}/src">
            <fileset dir="${src}">
            </fileset>
        </copy>

        <copy todir="${build}/test">
            <fileset dir="${test}">
            </fileset>
        </copy>

    </target>

    <target name="clean">
        <delete dir="${build}" />
    </target>
</project>

我设置了四个属性。build指的是在生成包之前,我可能会在其中组合我的文件的目录。test指向测试目录。src指来源目录。version定义包的版本号。

build目标将srctest目录复制到构建环境中。在一个更复杂的项目中,我可能还会执行转换,生成配置文件,并在这个阶段组装二进制资产。此目标是项目的默认目标。

clean目标删除构建目录及其包含的任何内容。让我们运行一个构建:

$ ./vendor/bin/phing

Buildfile: /var/popp/src/ch21/build.xml

userthing > build:
    [mkdir] Created dir: /var/popp/src/ch21/build
     [copy] Created 4 empty directories in /var/popp/src/ch21/build/src
     [copy] Copying 3 files to /var/popp/src/ch21/build/src
     [copy] Created 3 empty directories in /var/popp/src/ch21/build/test
     [copy] Copying 2 files to /var/popp/src/ch21/build/test

BUILD FINISHED

Total time: 1.8206 second

单元测试

单元测试是持续集成的关键。成功构建一个包含破碎代码的项目是没有好处的。我在第十八章中介绍了 PHPUnit 的单元测试。但是,如果您没有按顺序阅读,您会希望在继续阅读之前安装这个非常有用的工具。这里有一种全局安装 PHPUnit 的方法:

$ wget https://phar.phpunit.de/phpunit.phar
$ chmod 755 phpunit.phar
$ sudo mv phpunit.phar /usr/local/bin/phpunit

您也可以使用 Composer 安装 PHPUnit:

    "require-dev": {
        "phpunit/phpunit": "⁹"
    }

这是我将在我的例子中采用的方法。因为 PHPUnit 将安装在vendor/目录下,所以我的开发目录将保持独立于更广泛的系统。

我已经将我的测试目录与其余的源代码分开,所以我需要设置我的自动加载规则,以便 PHP 可以在测试期间定位所有的系统类。以下是我的完整composer.json:

    "require-dev": {
        "phpunit/phpunit": "⁹"
    },
    "autoload":  {
        "psr-4": {
            "userthing\\": ["src/", "test/"]
        }
    }

在对composer.json进行任何更改后,不要忘记运行composer update

同样在第十八章中,我为我将在本章中使用的userthing代码版本写了测试。在这里,我再次运行它们(从src目录中),以确保我的重组没有破坏任何新的东西:

$ vendor/bin/phpunit test/

PHPUnit 9.5.0 by Sebastian Bergmann and contributors.

.......                          7 / 7 (100%)

Time: 00:00.002, Memory: 18.00 MB

OK (7 tests, 7 assertions)

这证实了我的测试有效。然而,我想用 Phing 来调用它们。

Phing 提供了一个 exec 任务,我们可以用它来调用 phpunit 命令。然而,如果有的话,最好使用专门的工具。此作业有一个内置任务:

// listing 21.02
<target name="test" depends="build">
    <phpunit bootstrap="${phing.dir}/vendor/autoload.php" printsummary="true">
        <formatter type="plain" usefile="false"/>
        <batchtest>
            <fileset dir="${test}">
                <include name="**/*Test.php"/>
            </fileset>
        </batchtest>
    </phpunit>
</target>

因为这些是单元测试而不是功能测试,所以我们可以在本地的src/目录下运行它们,而不需要一个已安装的实例(带有一个正常运行的数据库或 web 服务器)。在许多其他属性中,phpunit任务接受一个printsummary属性,这将导致输出测试过程的概述。

该任务的大部分功能是使用嵌套元素配置的。元素管理测试信息的生成方式。在这种情况下,我选择输出基本的人类可读数据。batchtest允许您使用嵌套的fileset元素定义多个测试文件。

Note

任务是高度可配置的。Phing 手册在 www.phing.info/guide/chunkhtml/PHPUnitTask.html 提供了完整的文档。

在这里,我用 Phing 运行测试:

$ ./vendor/bin/phing test

Buildfile: /vagrant/poppch21/build.xml

userthing > build:

userthing > test:

  [phpunit] Testsuite: userthing\persist\UserStoreTest
  [phpunit] Tests run: 4, Warnings: 0, Failures: 0, Errors: 0, Incomplete: 0, Skipped: 0, Time elapsed: 0.00684 s
  [phpunit] Testsuite: userthing\util\ValidatorTest
  [phpunit] Tests run: 3, Warnings: 0, Failures: 0, Errors: 0, Incomplete: 0, Skipped: 0, Time elapsed: 0.01188 s
  [phpunit] Total tests run: 7, Warnings: 0, Failures: 0, Errors: 0, Incomplete: 0, Skipped: 0, Time elapsed: 0.02169 s

BUILD FINISHED

Total time: 0.1457 seconds

文件

透明是 CI 的原则之一。因此,当您在持续集成环境中查看构建时,能够检查文档是否是最新的并涵盖了最新的类和方法是很重要的。因为 PHPDocumentor 的最新版本正在开发中,所以目前不鼓励安装 composer。出于同样的原因,PHPDocumentor GitHub 页面可能是获取最新信息的最佳位置。

$ wget -O phpDocumentor https://github.com/phpDocumentor/phpDocumentor/releases/download/v3.0.0-rc/
phpDocumentor.phar
$ chmod 755 ./phpDocumentor
$ mv phpDocumentor /usr/local/bin/phpDocumentor

以 root 用户身份运行,我下载了phpDocumentor v3,使其可执行,并将其移动到我的系统的中央位置。

为了确保万无一失,我最好调用这个工具,这次是从build目录:

$ cd ./build
$ phpDocumentor --directory=src --target=docs --title=userthing

这会生成一些非常简单的文档。一旦它在 CI 服务器上发布,我肯定会羞愧地写一些真正的内联文档。

我想再一次把这个添加到我的build.xml文档中。有一个名为phpdoc2的任务被设计用来与 PHPDocumentor 集成。然而,由于它不能很好地与 PHPDocumentor 的 phar 版本集成,我将使用 cruder exec任务调用该工具。

// listing 21.03
<target name="doc" depends="build">
    <mkdir dir="reports/docs" />
    <exec executable="/usr/local/bin/phpDocumentor" dir="${phing.dir}">
        <arg line=" --directory=build/src --target=reports/docs ​--title=userthing" />
    </exec>
</target>

同样,我的doc目标依赖于build目标。我创建了reports/docs输出目录,然后使用exec任务调用PHPDocumentorexec接受一个嵌套的arg元素,我用它来指定我的命令参数。

代码覆盖率

如果测试不适用于你写的代码,那么依靠测试是没有用的。PHPUnit 包括报告代码覆盖率的能力。以下是 PHPUnit 使用信息的摘录:

--coverage-clover <file>    Generate code coverage report in Clover XML format
...
--coverage-html  <dir>      Generate code coverage report in HTML format

为了使用此功能,您必须安装代码覆盖率扩展,如 Xdebug 或 pcov。我将演示如何用 Xdebug 生成覆盖率报告。使用 pcov 是一个非常相似的过程。您可以在 http://pecl.php.net/package/Xdebughttps://pecl.php.net/package/pcov 找到关于这些扩展的更多信息(安装信息可在 http://xdebug.org/docs/installhttps://github.com/krakjoe/pcov/blob/develop/INSTALL.md 找到)。您也可以使用您的 Linux 发行版的软件包管理系统直接安装这些软件包。在 Fedora 中,这应该对您有用,例如:

$  sudo yum install php-xdebug

在这里,我从src/目录运行 PHPUnit,并启用代码覆盖率:

$ export XDEBUG_MODE=coverage
$ ./vendor/bin/phpunit --whitelist src/ --coverage-html coverage test

PHPUnit 9.5.0 by Sebastian Bergmann and contributors.

.......                          7 / 7 (100%)

Time: 00:00.121, Memory: 12.00 MB

OK (7 tests, 7 assertions)

Generating code coverage report in HTML format ... done [00:00.047]

现在您可以在浏览器中看到该报告(参见图 21-1 )。

img/314621_6_En_21_Fig1_HTML.jpg

图 21-1

代码覆盖率报告

需要注意的是,实现完全覆盖并不等同于充分测试一个系统。另一方面,了解您的测试中的任何差距是很好的。如你所见,我还有一些工作要做。

确认我可以从命令行检查覆盖率之后,我需要将这个功能添加到我的构建文档中:

// listing 21.04
<target name="citest" depends="build">
    <mkdir dir="reports/coverage" />
    <coverage-setup database="reports/coverage.db">
        <fileset dir="${src}">
            <include name="**/*.php"/>
        </fileset>
    </coverage-setup>

    <phpunit haltonfailure="true" codecoverage="true" bootstrap="${phing.dir}/vendor/autoload.php" printsummary="true">
        <formatter type="plain" usefile="false"/>
        <formatter type="xml" outfile="testreport.xml" todir="reports" />
        <formatter type="clover" outfile="cloverreport.xml" todir="reports" />
        <batchtest>
            <fileset dir="${test}">
                <include name="**/*Test.php"/>
            </fileset>
        </batchtest>
    </phpunit>

    <coverage-report outfile="reports/coverage.xml">
        <report todir="reports/coverage" />
    </coverage-report>
</target>

我创建了一个名为citest的新任务。大部分都是你已经看过的test任务的翻版。

我首先创建一个reports目录和一个coverage子目录。

我使用coverage-setup任务为coverage特性提供配置信息。我使用database属性指定原始覆盖率数据应该存储在哪里。嵌套的fileset元素定义了应该接受覆盖率分析的文件。

我已经向phpunit任务添加了两个formatter元素。类型为xmlformatter将生成一个名为testreport.xml的文件,该文件将包含测试结果。clover formatter将生成覆盖信息,也是 XML 格式的。最后,在citest目标中,我部署了coverage-report任务。这将获取现有的覆盖率信息,生成一个新的 XML 文件,然后输出一个 HTML 报告。

Note

CoverageReportTask元素记录在 www.phing.info/guide/chunkhtml/CoverageReportTask.html

编码标准

我在第十五章中详细讨论了编码标准。虽然你的个人风格被一个共同的标准所束缚是令人讨厌的,但是它可以使一个项目更容易被更广泛的团队所使用。由于这个原因,许多团队强制执行一个标准。然而,这很难用肉眼来执行,所以自动化这个过程是有意义的。

我将再次使用 Composer。这一次,我将配置它来安装 PHP_CodeSniffer:

    "require-dev": {
        "phpunit/phpunit": "⁹",
        "squizlabs/php_codesniffer": "3.*"
    },

现在,我将把 PSR-12 编码标准应用到我的代码中:

$ vendor/bin/phpcs --standard=PSR12 src/util/Validator.php

FILE: /vagrant/poppch21/src/util/Validator.php
-----------------------------------------------------------------
FOUND 8 ERRORS AFFECTING 2 LINES
------------------------------------------------------------------
  7 | ERROR | [x] Header blocks must be separated by a single blank line
 22 | ERROR | [ ] Visibility must be declared on method "validateUser"
 22 | ERROR | [ ] Expected "function abc(...)"; found "function abc (...)"
 22 | ERROR | [x] Expected 1 space after FUNCTION keyword; 4 found
 22 | ERROR | [x] Expected 0 spaces after opening parenthesis; 1 found
 22 | ERROR | [x] Expected 0 spaces before opening parenthesis; 3 found
 22 | ERROR | [x] Expected 1 space between comma and argument "$pass"; 0 found
 22 | ERROR | [x] Opening brace should be on a new line
-----------------------------------------------------------------
PHPCBF CAN FIX THE 6 MARKED SNIFF VIOLATIONS AUTOMATICALLY
------------------------------------------------------------------

显然,我需要稍微清理一下我的代码!

自动化工具的一个好处是它的非个人性质。如果你的团队决定强加一套编码惯例,那么有一个没有幽默感的脚本来纠正你的风格可能比一个没有幽默感的同事做同样的事情要好。

如您所料,我想在我的构建文件中添加一个CodeSniffer目标。

// listing 21.05
<target name="sniff" depends="build">
    <exec executable="vendor/bin/phpcs" passthru="true" dir="${phing.dir}">
        <arg line="--report-checkstyle=reports/checkstyle.xml ​--standard=PSR12  build/src"
/>
    </exec>
</target>

虽然 Phing 提供了一个phpcodesniffer任务,但是它与最近版本的 PHP_CodeSniffer 不兼容。因此,我再次使用exec来运行工具。我用--report-checkstyle标志调用phpcs,这样它将在reports目录中生成一个 XML 文件。

所以我有很多有用的工具可以用来监控我的项目。当然,如果让我自己去做,我很快就会对运行它们失去兴趣,即使是使用我有用的 Phing 构建文件。事实上,我可能会回到集成阶段的旧思想,只有在接近发布时才拿出工具,到那时,它们作为早期预警系统的有效性就无关紧要了。我需要的是一个 CI 服务器来为我运行这些工具。

Jenkins(以前叫 Hudson)是一个开源的持续集成服务器。虽然是用 Java 写的,但是 Jenkins 用 PHP 工具很好用。这是因为持续集成服务器位于它构建的项目之外,启动并监控各种命令的结果。Jenkins 还可以很好地与 PHP 集成,因为它被设计为支持插件,并且有一个非常活跃的开发人员社区正在努力扩展服务器的核心功能。

Note

为什么是詹金斯?Jenkins 非常易于使用和扩展。它已经很好地建立起来,并且有一个活跃的用户社区。它是免费和开源的。支持与 PHP 集成的插件(包括您可能想到的大多数构建和测试工具)是可用的。然而,有许多 CI 服务器解决方案。这本书的前一个版本侧重于 CruiseControl ( http://cruisecontrol.sourceforge.net/ ),这仍然是一个不错的选择。

安装 Jenkins

Jenkins 是一个 Java 系统,所以你需要安装 Java。如何做到这一点将因系统而异。詹金斯网站在 www.jenkins.io/doc/book/installing/ 提供了良好的安装说明。

多亏了 vagger,我现在正在 CentOS 7 系统上安装,以下是我的安装步骤:

$ sudo wget -O /etc/yum.repos.d/jenkins.repo \
    https://pkg.jenkins.io/redhat-stable/jenkins.repo
$ sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io.key
$ sudo yum update
$ sudo yum install jenkins java-1.8.0-openjdk-devel
$ sudo systemctl daemon-reload

你可以这样开始詹金斯:

$ sudo systemctl start jenkins

默认情况下,Jenkins 在端口 8080 上运行,因此您可以通过启动浏览器并访问http://yourhost:8080/来确定您是否准备好继续。你应该会看到类似图 21-2 中的屏幕。

img/314621_6_En_21_Fig2_HTML.jpg

图 21-2

安装屏幕

图 21-2 中的说明一目了然。我从/var/lib/jenkins/secrets/initialAdminPassword获取密码(使用sudo是因为受限的读取权限),并将其输入到提供的框中。然后我面临一个选择:用流行的插件安装还是选择我自己的?我选择最流行的插件,我知道它会让我获得对 Git 的支持。如果您想要一个苗条的系统,您可以选择只选择那些您需要的插件。之后,是时候在完成安装之前创建用户名和密码了。

安装 Jenkins 插件

Jenkins 是高度可定制的,我将需要相当多的插件来集成我在本章中描述的特性。在 Jenkins web 界面中,我点击Manage Jenkins,然后点击Manage Plugins。在Available标签下,我发现了一个长长的列表。我在Install列中选择我希望添加到 Jenkins 的所有插件的复选框。

表 21-1 描述了我将要使用的插件。

表 21-1

一些 Jenkins 插件

|

插件

|

描述

| | --- | --- | | Git 插件 | 允许与 Git 存储库交互 | | jUnit 插件 | 与包括 PHPUnit 在内的 xUnit 工具系列集成 | | Phing 插件 | 调用 Phing 目标 | | Clover PHP 插件 | 访问 clover XML 文件和由 PHPUnit 生成的 HTML 文件并生成报告 | | HTML Publisher 插件 | 集成 HTML 报告。用于 PHPDocumentor 输出 | | 警告下一代插件 | 访问 PHPCodeSniffer 生成的 XML 文件并生成报告 |

可以看到图 21-3 中的 Jenkins 插件页面。

img/314621_6_En_21_Fig3_HTML.jpg

图 21-3

詹金斯插件屏幕

安装完这些插件后,我就可以创建和配置我的项目了。

设置 Git 公钥

在使用 Git 插件之前,我需要确保能够访问 Git 存储库。在第十七章中,我描述了为了访问远程 Git 库而生成公钥的过程。我们需要在这里重复这个过程。但是詹金斯把哪里叫做家呢?

这个位置是可配置的,但自然詹金斯会提示你。我点击Manage Jenkins,然后点击Configure System。我发现詹金斯的主目录在那里。当然,我也可以检查/etc/ passwd文件中与jenkins用户相关的信息。在我的例子中,目录是/var/lib/jenkins

现在我需要配置一个SSH目录:

$ sudo su jenkins -s /bin/bash
$ cd ~
$ mkdir .ssh
$ chmod 0700 .ssh
$ ssh-keygen

我切换到jenkins用户,指定要使用的 shell(因为 shell 访问在默认情况下可能被禁用)。我切换到这个用户的主目录。ssh-keygen命令生成 SSH 密钥。当提示输入密码时,我只需按回车键,这样 Jenkins 将只通过其密钥进行身份验证。我确保在.ssh/id_rsa生成的文件既不是全球可读的,也不是群体可读的:

$ chmod 0600 .ssh/id_rsa

我可以从.ssh/id_rsa.pub获取公钥,并将其添加到我的远程 Git 存储库中。详见第十七章。

我还没到那一步。我需要确保我的 Git 服务器是一个已知的 SSH 主机。我可以将这种设置与我的 Git 配置的命令行测试结合起来。当我这样做时,我确保我仍然以jenkins用户的身份登录:

$ cd /tmp
$ git clone git@bitbucket.org:getinstance/userthing.git

我被提示确认我的 Git 主机,然后它被添加到用户的文件中。这可以防止 Jenkins 在稍后建立 Git 连接时被绊倒。

Note

还有各种插件可以用来管理 Git 凭证,包括 SSH 代理OAuth 凭证Kubernetes 凭证

安装项目

在 Jenkins 仪表板页面,我点击New Item。在这个新屏幕上,我终于可以创建我的userthing项目了。在图 21-4 中可以看到设置画面。

img/314621_6_En_21_Fig4_HTML.jpg

图 21-4

项目设置屏幕

我选了Freestyle project,打了OK。这将我带到项目配置屏幕。我的首要任务是链接远程 Git 存储库。我在Source Code Manager section中选择 Git 单选按钮并添加我的存储库。你可以在图 21-5 中看到这一点。

img/314621_6_En_21_Fig5_HTML.jpg

图 21-5

设置版本控制存储库

如果一切顺利,我应该可以访问我的源代码。我可以通过保存并从仪表板页面选择Build Now来检查。然而,为了看到一些有意义的行动,我也应该建立 Phing。这很简单,因为我已经集中安装了 Phing。然而,如果你像我一样使用 Composer,事情就有点复杂了。你必须告诉詹金斯在哪里可以找到 Phing 可执行文件。您可以通过从主菜单中选择Manage Jenkins,然后选择Global Tool Configuration来完成此操作。因为我已经安装了 Phing 插件,所以我将在那里为该工具找到一个配置区域。我点击Add Phing进入表格。在图 21-6 中,我展示了你用来引用 Phing 本地版本的配置区域。

img/314621_6_En_21_Fig6_HTML.jpg

图 21-6

指定 Phing 的位置

我给这个配置起了一个名字,并将路径添加到我的项目中的vendor目录,Phing 就在这个目录中。

一旦我确信 Jenkins 可以找到 Phing,我就可以为我的项目配置它。我返回到userthing项目区和Configure菜单。我滚动到Build部分,从Add build step下拉菜单中选择两个项目。首先,我选择Execute shell。我需要确保 Jenkins 运行composer install,否则我的项目所依赖的工具都不会被安装。图 21-7 为其结构示意图。

img/314621_6_En_21_Fig7_HTML.jpg

图 21-7

设置外壳执行

我从Add build step下拉菜单中选择的下一个项目是Invoke Phing targets。我从下拉菜单中选择我之前配置的 Phing 实例(composer phing)并将我的目标添加到文本字段中。你可以在图 21-8 中看到这个步骤。

img/314621_6_En_21_Fig8_HTML.jpg

图 21-8

配置 Phing

运行第一次构建

我保存配置屏幕并点击Build Now来运行构建和测试过程。这是关键时刻!构建链接应该出现在屏幕的Build History区域。我点击它,然后点击Console Output来确认构建按预期进行。您可以在图 21-9 中看到输出。

img/314621_6_En_21_Fig9_HTML.jpg

图 21-9

控制台输出

Jenkins 从 Git 服务器检查出userthing代码,并运行所有的构建和测试目标。

配置报告

多亏了我的构建文件,Phing 将报告保存在build/reports目录中,将文档保存在build/docs中。我激活的插件可以在项目配置界面的Add post-build action下拉菜单中进行配置。

图 21-10 显示了其中一些配置项目。

img/314621_6_En_21_Fig10_HTML.jpg

图 21-10

配置报告插件项目

而不是让你一个接一个的截图,把配置项压缩成一个表格会更清晰。表 21-2 显示了我的 Phing 构建文件中的一些后期构建动作字段和相应的目标。

表 21-2

报告配置

|

配置项

|

Phing 目标

|

|

价值

| | --- | --- | --- | --- | | 记录编译器警告和静态分析结果 | sniff | 工具 | PHP_CodeSniffer | |   |   | 报告文件模式 | reports/checkstyle.xml | | 发布 Clover PHP 覆盖率报告 | citest | Clover XML 位置 | reports/cloverreport.xml | |   |   | Clover HTML 报告目录 | reports/clovercoverage/ | | 发布 HTML 报告 | doc | HTML 目录到 | reports/docs | |   |   | 存档索引页 | index.html | | 发布 Junit 测试结果报告 | citest | 测试报告 XML | reports/testreport.xml | | 电子邮件通知 |   | 收件人 | someone@somemail.com |

当我构建项目的构建文件时,你会遇到表 21-2 中的所有配置值。全部,也就是除了最后一个。E-mail Notification字段允许您定义一个开发人员列表,当一个构建失败时,他们都会收到通知。

在检查覆盖率之前,我必须创建一个名为XDEBUG_MODE的环境变量,其值为coverage。我可以去Manage Jenkins然后去Configure System屏幕。如图 21-11 所示,我可以在Global Properties部分设置环境变量。

img/314621_6_En_21_Fig11_HTML.jpg

图 21-11

设置环境变量

完成所有设置后,我可以返回到项目屏幕并运行另一个构建。图 21-12 显示了我新增强的输出。

img/314621_6_En_21_Fig12_HTML.jpg

图 21-12

显示趋势信息的项目屏幕

随着时间的推移,项目屏幕将绘制测试性能、覆盖率和风格符合性的趋势。还有到最新 API 文档、详细测试结果和全覆盖信息的链接。

触发构件

如果团队中的某个人必须记得通过手动点击来启动每个构建,那么所有这些丰富的信息几乎都是无用的。当然,Jenkins 提供了自动触发构建的机制。

您可以设置 Jenkins 以固定的时间间隔进行构建,或者以指定的时间间隔轮询版本控制存储库。可以使用 cron 格式设置时间间隔,这提供了对调度的精细控制,尽管有些神秘。幸运的是,Jenkins 为该格式提供了很好的在线帮助,如果您不需要精确调度,还有简单的别名。别名有@hourly@midnight@daily@weekly@monthly。在图 21-13 中,我将构建配置为每天运行一次,或者每当存储库发生变化时运行,基于每小时一次的变更轮询。

img/314621_6_En_21_Fig13_HTML.jpg

图 21-13

调度构建

测试失败

到目前为止,一切似乎进展顺利,即使userthing不会很快赢得任何法规遵从徽章。但是测试失败时就会成功,所以我最好打破一些东西,以确保 Jenkins 报告它。

下面是命名空间userthing\util中名为Validate的类的一部分:

// listing 21.06
public function validateUser(string $mail, string $pass): bool
{
    // make it always fail
    // return false;
    $user = $this->store->getUser($mail);
    if (is_null($user)) {
        return false;
    }
    $testpass = $user->getPass();
    if ($testpass == $pass) {
        return true;
    }

    $this->store->notifyPasswordFailure($mail);
    return false;
}

看看我会在哪里破坏这个方法?如果我取消了方法中第二行的注释,validateUser()将总是返回false

下面的测试应该会让你窒息。就在test/util/ValidatorTest.php里:

// listing 21.07
public function testValidateCorrectPass(): void
{
    $this->assertTrue(
        $this->validator->validateUser("bob@example.com", "12345"),
        "Expecting successful validation"
    );
}

做出改变后,我需要做的就是提交并等待。果不其然,不久之后,项目状态显示了一个由黄色图标标记的构建(表示整个项目的健康状况已经恶化)。单击构建链接后,我会找到更多详细信息。您可以看到图 21-14 中的屏幕。

img/314621_6_En_21_Fig14_HTML.jpg

图 21-14

失败的构建

记住,如果你需要更多关于构建错误的信息,你可以点击构建界面中的Console Output链接。您通常会在那里找到比构建摘要屏幕本身更有用的信息。

摘要

在这一章中,我把你在前面章节中看到的许多工具放在一起,并用 Jenkins 把它们粘在一起。我为 CI 准备了一个小项目,应用了一系列工具,包括 PHPUnit(用于测试和代码覆盖)、PHP_CodeSniffer、phpDocumentor 和 Git。然后,我设置了 Jenkins,并向您展示了如何向系统添加项目。我测试了系统的速度,最后,向您展示了如何扩展 Jenkins,以便它可以用电子邮件来调试您,并测试构建和安装。

二十二、对象、模式、实践

从对象基础到设计模式原则,再到工具和技术,这本书只关注一个目标:成功的 PHP 项目。

在这一章中,我回顾了我在整本书中涉及的一些主题和观点:

  • PHP 和 objects:PHP 如何继续增加对面向对象编程的支持,以及如何利用这些特性

  • 对象和设计:总结一些面向对象的设计原则

  • 模式:是什么让他们变得酷

  • 模式原则(Pattern principles):概述了许多模式背后的面向对象的指导原则

  • 工作所需的工具:重温我描述过的工具,并检查一些我没有用过的工具

目标

正如你在第二章中看到的,在很长一段时间里,对象在 PHP 世界里是一种事后的想法。至少可以说,在 PHP 3 中,支持是初级的,对象只不过是穿了漂亮衣服的关联数组。尽管对于 PHP 4 的对象爱好者来说,事情有了根本性的改善,但是仍然存在一些严重的问题。最重要的是,默认情况下,对象是通过引用来分配和传递的。

PHP 5 的引入最终将对象拖到了舞台中央。你仍然可以不用声明一个类就用 PHP 编程,但是这种语言最终为面向对象的设计进行了优化。PHP 7 完善了这一点,引入了期待已久的特性,如标量和返回类型声明。可能出于向后兼容的原因,一些流行的框架本质上仍然是过程化的(特别是 WordPress);然而,总的来说,今天大多数新的 PHP 项目都是面向对象的。

在第 3 、 4 和 5 章中,我详细考察了 PHP 的面向对象支持。以下是 PHP 自版本 5 以来引入的一些新特性:反射、异常、私有和受保护的方法和属性、__toString()方法、static修饰符、抽象类和方法、最终方法和属性、接口、迭代器、拦截器方法、类型声明、const修饰符、通过引用传递、__clone()__construct()方法、后期静态绑定、名称空间和匿名类。这个不完整列表的长度揭示了 PHP 的未来与面向对象编程的紧密程度。

Zend Engine 2 和 PHP 5 使面向对象设计成为 PHP 项目的核心,向一批新的开发人员开放了这种语言,并为现有的爱好者开辟了新的可能性。

在第六章中,我看到了对象可以给你的项目设计带来的好处。因为对象和设计是本书的中心主题之一,所以有必要详细概括一些结论。

选择

没有法律规定你必须只用类和对象来开发。设计良好的面向对象代码提供了一个清晰的接口,可以从任何客户端代码访问,无论是面向过程的还是面向对象的。即使您对编写对象不感兴趣(如果您仍在阅读这本书,这种可能性不大),您也可能会发现自己在使用它们,即使只是作为 Composer 软件包的客户。

封装和委托

对象们关心自己的事情,关起门来继续完成分配给他们的任务。它们提供了一个接口,通过这个接口可以传递请求和结果。任何不需要暴露的数据,以及实现的肮脏细节,都隐藏在这种正面的背后。

这给了面向对象和过程化项目不同的形状。面向对象项目中的控制器通常出人意料地稀少,由少数几个获取对象的实例化和从一个集合中调用数据并将其传递给另一个集合的调用组成。

另一方面,程序性项目更倾向于干涉主义。控制逻辑在更大程度上下降到实现,引用变量,测量返回值,并根据情况沿着不同的操作路径轮流进行。

退耦

解耦就是消除组件之间的相互依赖,这样对一个组件进行更改就不需要对其他组件进行更改。设计良好的对象是自我封闭的。也就是说,他们不需要参考自身之外的东西来回忆他们在之前的调用中学习到的细节。

通过维护状态的内部表示,对象减少了对全局变量的需求——这是紧耦合的一个众所周知的原因。在使用全局变量时,你将系统的一部分绑定到另一部分。如果一个组件(无论是函数、类还是代码块)引用了一个全局变量,那么另一个组件可能会意外地使用相同的变量名,并用它的值替换第一个变量。第三个组件可能会依赖于第一个组件设置的变量中的值。改变第一个组件的工作方式,可能会导致第三个组件停止工作。面向对象设计的目标是减少这种相互依赖,使每个组件尽可能自给自足。

紧密耦合的另一个原因是代码重复。当您必须在项目的不同部分重复一个算法时,您会发现紧密耦合。你来改算法会怎么样?显然,您必须记住在它出现的任何地方进行更改。忘记这样做,你的系统就有麻烦了。

代码重复的一个常见原因是并行条件。如果您的项目需要根据特定的环境以一种方式做事(例如,在 Linux 上运行),而根据另一种环境以另一种方式做事(例如,在 Windows 上运行),您会经常发现相同的if / else子句出现在系统的不同部分。如果你添加了一个新的环境和处理它的策略(MacOS),你必须确保所有的条件都被更新。

面向对象编程提供了处理这个问题的技术。可以用多态代替条件句。多态性,也称为类切换,是根据情况透明地使用不同的子类。因为每个子类都支持与公共超类相同的接口,所以客户端代码既不知道也不关心它使用的是哪个特定的实现。

条件代码没有从面向对象的系统中消失;它只是被最小化和集中化。必须使用某种条件代码来确定将向客户端提供哪些特定的子类型。不过,这种测试通常只在一个地方进行一次,从而减少了耦合。

复用性

封装促进了解耦,从而促进了重用。自给自足且仅通过公共接口与更广泛的系统进行通信的组件通常可以从一个系统转移到另一个系统中使用,而无需更改。

事实上,这比你想象的要罕见。即使是完美的正交码也可能是特定于项目的。例如,当创建一组用于管理特定网站内容的类时,值得在规划阶段花一些时间来查看那些特定于您的客户的功能,以及那些可能形成以内容管理为核心的未来项目的基础的功能。

重用的另一个技巧是:集中那些可能在多个项目中使用的类。换句话说,不要将一个非常好的可重用类复制到一个新项目中。这将导致宏观上的紧密耦合,因为您将不可避免地在一个项目中改变类,而在另一个项目中忘记这样做。在一个可以被项目共享的中央存储库中管理公共类会更好。

美学

这不会说服任何还没有被说服的人,但是对我来说,面向对象的代码在美学上是令人愉悦的。实现的混乱被隐藏在干净的接口后面,使得对象对其客户来说是一件明显简单的事情。

我喜欢多态性的整洁和优雅,因此 API 允许您操作非常不同的对象,但仍然可以互换和透明地执行——对象可以像儿童积木一样整齐地堆叠或插入另一个对象。

当然,有人认为反之亦然。面向对象的代码可以表现为类的爆炸式增长,这些类彼此之间是如此的解耦,以至于拼凑它们之间的关系是一件令人头疼的事情。这本身就是一种代码味道。建立生产工厂的工厂,生产工厂的工厂,这通常是很诱人的,直到你的代码看起来像一个镜子大厅。有时候,做最简单的工作,然后为了测试和灵活性而进行足够优雅的重构是有意义的。让问题空间决定您的解决方案,而不是最佳实践列表。

Note

严格应用所谓的最佳实践也经常是项目管理中的一个问题。每当一项技术或一个过程的使用开始变得像仪式一样,被自动地、不灵活地应用,就值得花一点时间来研究你当前方法背后的推理。有可能你正从工具领域转向货物崇拜领域。

同样值得一提的是,一个漂亮的解决方案并不总是最好或最有效的。使用成熟的面向对象解决方案是很诱人的,在这种情况下,一个快速脚本或几个系统调用就可以完成工作。

模式

最近,一个 Java 程序员申请了一份工作,这家公司和我有一些关系。在他的求职信中,他为几年来只使用模式而道歉。设计模式是最近的发现——一个变革性的进步——这一假设证明了它们所带来的兴奋。事实上,这位经验丰富的程序员使用模式的时间可能比他想象的要长。

模式描述了常见的问题和经过测试的解决方案。模式命名、编纂和组织真实世界的最佳实践。它们不是发明的组成部分,也不是教义中的条款。如果一个模式没有描述孵化时已经很普遍的实践,那么它将是无效的。

记住,模式语言的概念起源于建筑领域。在模式被提出作为描述空间和功能问题的解决方案之前,人们建造庭院和拱门已经有几千年了。

话虽如此,设计模式确实经常会激起与宗教或政治争议相关的情绪。信徒们在走廊里漫步,眼里闪着福音的光芒,胳膊下夹着一本《四人帮》的书。他们搭讪门外汉,像信仰文章一样一口气说出模式名称。难怪一些批评家认为设计模式是炒作。

在 Perl 和 PHP 等语言中,模式也是有争议的,因为它们与面向对象编程有着紧密的联系。在对象是设计决策而不是给定的情况下,将自己与设计模式联系起来相当于偏好声明,尤其是因为模式产生更多的模式,而对象产生更多的对象。

什么样的模式买给我们

我在第七章中介绍了模式。让我们重申一下模式可以给我们带来的一些好处。

屡试不爽

首先,正如我所提到的,模式是特定问题的成熟解决方案。在模式和配方之间进行类比是危险的:配方可以被盲目地遵循,而模式本质上是“半生不熟的”(马丁·福勒),需要更深思熟虑的处理。尽管如此,食谱和图案都有一个重要的特点:它们在铭刻之前都经过了彻底的试验和测试。

模式暗示了其他模式

图案有相互吻合的凹槽和曲线。某些模式会随着令人满意的咔哒声一起出现。使用模式解决问题将不可避免地产生后果。这些后果可能成为暗示互补模式的条件。当然,当您选择相关模式时,一定要注意解决实际的需求和问题,而不仅仅是构建优雅但无用的互锁代码塔。构建相当于建筑上愚蠢的编程是很有诱惑力的。

常用词汇

模式是开发描述问题和解决方案的通用词汇的一种方式。命名很重要——它代表描述,因此能让我们很快覆盖很多领域。当然,命名也模糊了那些还没有共享词汇的人的意思,这也是为什么模式有时会如此令人愤怒的原因之一。

模式促进设计

正如下一节所讨论的,如果使用得当,模式可以鼓励好的设计。当然,有一个重要的警告。模式不是仙尘。

设计的模式和原则

设计模式本质上与良好的设计有关。如果使用得当,它们可以帮助您构建松散耦合且灵活的代码。然而,当模式批评家说模式可能被新感染者过度使用时,他们说得有道理。因为模式实现形成了漂亮优雅的结构,所以很容易忘记好的设计总是在于符合目的。记住模式的存在是为了解决问题。

当我第一次开始使用模式时,我发现自己在代码中创建了抽象工厂。我需要生成对象,抽象工厂无疑帮助了我。

但事实上,我在懒散地思考,给自己做不必要的工作。我需要产生的对象集确实是相关的,但是它们还没有替代的实现。经典的抽象工厂模式非常适合根据环境生成不同的对象集的情况。要使抽象工厂工作,您需要为每种类型的对象创建工厂类,并创建一个类来提供工厂类。光是描述过程就让人精疲力尽。

如果我创建了一个基本的工厂类,我的代码会干净得多,如果我发现自己需要生成一组并行的对象,只需要重构来实现抽象工厂。

使用模式的事实并不能保证好的设计。在开发时,最好记住同一原则的两种表达方式:KISS(“保持简单,笨蛋”)和“做最简单的工作。”极限程序员还给出了另一个相关的缩写:YAGNI。“你不需要它”,这意味着除非真的需要,否则你不应该实现一个特性。

随着警告的消失,我可以继续我那令人窒息的热情。正如我在第九章中所阐述的,模式倾向于体现一套可以推广并应用于所有代码的原则。

偏爱合成而非遗传

继承关系是强大的。我们使用继承来支持运行时类切换(多态性),这是我在本书中探索的许多模式和技术的核心。但是,通过在设计中仅仅依赖继承,您可能会产生易于重复的不灵活的结构。

避免紧密耦合

我在本章已经谈到了这个问题,但为了完整起见,这里值得一提。您永远无法回避这样一个事实,即一个组件的变更可能需要项目其他部分的变更。但是,您可以通过避免重复(在我们的示例中以并行条件为代表)和过度使用全局变量(或单例)来最小化这种情况。当抽象类型可以用来促进多态性时,也应该尽量减少具体子类的使用。这最后一点引导我们到另一个原则。

接口的代码,而不是实现

用清晰定义的公共接口设计你的软件组件,使每个组件的职责透明。如果你在一个抽象超类中定义你的接口,并且让客户端类请求并使用这个抽象类型,那么你就可以将客户端从具体的实现中分离出来。

说到这里,请记住 YAGNI 原则。如果你开始时只需要一个类型的实现,没有直接的理由去创建一个抽象超类。你也可以在一个具体的类中定义一个清晰的接口。一旦您发现您的单个实现试图同时做多件事情,您可以将您的具体类重新指定为两个子类的抽象父类。客户端代码不会变得更聪明,因为它继续使用单一类型。

您可能需要拆分实现并将结果类隐藏在抽象父类之后的一个典型标志是实现中出现了条件语句。

概括不同的概念

如果你发现你淹没在子类中,也许你应该把所有这些子类化的原因提取到它自己的类型中。如果原因是为了达到某种目的,而这种目的是你的类型的主要目的所附带的,那就更是如此了。

例如,给定一个类型UpdatableThing,您可能会发现自己创建了FtpUpdatableThingHttpUpdatableThingFileSystemUpdatableThing子类型。然而,你的类型的责任是成为一个可更新的东西——存储和检索的机制是附带的。FtpHttpFileSystem是这里变化的东西,它们属于自己的类型——姑且称之为UpdateMechanismUpdateMechanism将有不同实现的子类。然后,您可以添加尽可能多的更新机制,而不会干扰UpdatableThing类型,后者仍然专注于其核心职责。顺便提一下,注意UpdateMechanism也可以被命名为UpdateStrategy。我已经描述了策略模式的一个实现。有关更多信息,请参见第十一章。

还要注意,我在这里用动态运行时安排替换了静态编译时结构,让我们(好像是偶然地)回到了我们的第一个原则:“优先组合而不是继承。”

实践

我在本书的这一部分提到的问题(以及在第十四章中介绍的问题)经常被教科书和编码人员忽略。在我自己作为程序员的生活中,我发现这些工具和技术至少和设计一样与项目的成功相关。毫无疑问,像文档和自动化构建这样的问题在本质上不如组合模式这样的奇迹有启示性。

Note

让我们提醒一下 Composite 的美妙之处:一个简单的继承树,它的对象可以在运行时连接起来,形成同样是树的结构,但是要灵活和复杂得多。多个对象共享一个接口,通过这个接口向外界展示它们。简单与复杂、多重与单一之间的相互作用,一定会让你的脉搏加速——这不仅仅是软件设计,而是诗歌。

即使像文档和构建、测试和版本控制这样的问题比模式更加平淡无奇,它们也同样重要。在现实世界中,如果多个开发人员不能轻松地参与其中或理解其来源,那么一个出色的设计将无法存活。没有自动化测试,系统变得难以维护和扩展。没有构建工具,没有人会费心部署您的工作。随着 PHP 用户群的扩大,我们作为开发人员确保质量和易于部署的责任也在增加。

项目以两种模式存在。项目是其代码和功能的结构,它也是一组文件和目录,一个合作的基础,一组源和目标,以及一个转换的主题。从这个意义上说,一个项目从外部看是一个系统,就像它在代码中一样。构建、测试、文档和版本控制的机制需要像这些机制支持的代码一样关注细节。像关注系统本身一样关注元系统。

测试

尽管测试是从外部应用于项目的框架的一部分,但它与代码本身紧密地集成在一起。因为完全解耦是不可能的,甚至是不可取的,所以测试框架是监控变更结果的强大方法。改变方法的返回类型可能会影响其他地方的客户端代码,导致错误在更改后几周或几个月出现。测试框架让您有一半的机会捕捉到这种错误(测试越好,这里的机会就越大)。

测试也是改进面向对象设计的工具。首先测试(或者至少同时测试)有助于你关注一个类的接口,并仔细考虑每个方法的责任和行为。我在第十八章介绍了用于测试的 PHPUnit。

标准

我天生是个反向投资者。我讨厌别人告诉我该做什么。像合规这样的词会立刻引起我的战斗或逃跑反应。但是,尽管看起来有悖常理,标准推动创新。这是因为它们推动了互操作性。互联网的兴起在一定程度上是因为开放标准已经成为其核心。网站可以相互链接,web 服务器可以在任何域中重用,因为协议是众所周知和受尊重的。筒仓中的解决方案可能比广泛接受和应用的标准更好,但是如果筒仓烧毁了怎么办?如果买了,新主人决定收取访问费怎么办?当一些人决定隔壁的筒仓更好时会发生什么?在第十五章中,我讨论了 PSR,PHP 标准建议。我特别关注了自动加载的标准,它在清理 PHP 开发人员包含类的方式方面做了很多工作。我也看了 PSR-12,编码风格的标准。程序员对大括号的放置和参数列表的部署有强烈的感觉,但同意遵守一组公共规则有助于代码的可读性和一致性,并允许我们使用工具来检查和重新格式化源文件。本着这种精神,我已经将这个版本中的所有代码示例重新格式化为符合PSR-12。

版本控制

合作很难。面对现实吧:人是尴尬的。程序员更惨。一旦你理清了团队中的角色和任务,你最不想处理的就是源代码本身的冲突。正如你在第十七章中看到的,Git(以及类似的工具,如 CVS 和 Subversion)使你能够将多个程序员的工作合并到一个单一的存储库中。在冲突不可避免的地方,Git 会标记出事实并指出问题的根源。

即使你是单飞程序员,版本控制也是必须的。Git 支持分支,因此您可以同时维护一个软件版本和开发下一个版本,将稳定版本中的 bug 修复合并到开发分支中。

Git 还提供了对项目的每次提交的记录。这意味着您可以按日期或标记回滚到任何时刻。相信我,总有一天这会拯救你的项目。

自动化构建

没有自动构建的版本控制是有限的。任何复杂的项目都需要部署工作。需要将各种文件移动到系统的不同位置,需要转换配置文件以获得适合当前平台和数据库的正确值,并且需要设置或转换数据库表。我介绍了两个为安装而设计的工具。第一个是 Composer(参见第十六章),是独立软件包和小型应用的理想选择。我介绍的第二个构建工具是 Phing(参见第十九章),这是一个具有足够能力和灵活性的工具,可以自动安装最大最复杂的项目。

自动化构建将部署从繁琐的工作转变为命令行中的一两行代码。只需很少的努力,您就可以从构建工具中调用您的测试框架和文档输出。如果您的开发人员的需求没有动摇您,请记住当您的用户发现他们不再需要在每次您发布项目的新版本时花费整个下午来复制文件和更改配置字段时,他们可怜的感激叫声。

持续集成

能够测试和构建一个项目是不够的;你必须一直做这件事。随着项目变得越来越复杂,并且您管理着多个分支,这变得越来越重要。您应该构建并测试一个稳定的分支,从这个分支您可以发布较小的 bug 修复版本,一个或两个实验性的开发分支,以及您的主干。如果您试图手动完成所有这些工作,即使有构建和测试工具的帮助,您也永远无法进行任何编码。当然,所有的程序员都讨厌这样,所以构建和测试不可避免地会被忽略。

在第二十一章中,我谈到了持续集成,一种尽可能自动化构建和测试过程的实践和一套工具。

我错过了什么

由于时间和空间的限制,我不得不从本书中省略一些工具类别,尽管如此,它们对任何项目都非常有用。在大多数情况下,对于手头的工作,有不止一个好的工具,所以,尽管我会推荐一两个,但在做出选择之前,您可能需要花一些时间与其他开发人员交流,并使用您最喜欢的搜索引擎进行搜索。

如果您的项目有不止一个开发人员,甚至只有一个活动的客户,那么您将需要一个工具来跟踪 bug 和任务。像版本控制一样,bug 追踪器是一种生产力工具,一旦你在项目中尝试过,你就无法想象不使用它。追踪器允许用户报告项目中的问题,但是它们也经常被用作描述所需特性和将它们的实现分配给团队成员的手段。

您可以随时获得打开任务的快照,根据产品、任务所有者、版本号和优先级缩小搜索范围。每个任务都有自己的页面,您可以在其中讨论任何正在进行的问题。讨论条目和任务状态的更改可以通过邮件复制给团队成员,这样就可以很容易地关注事情,而不用一直去跟踪 URL。

外面有很多工具。尽管过了这么久,我通常还是会回到令人尊敬的 Bugzilla ( www.bugzilla.org )。Bugzilla 是免费的开源软件,拥有大多数开发者可能需要的所有特性。它是一个可下载的产品,所以你必须在你自己的服务器上运行它。它看起来仍然有点 Web 1.0 的味道,但并没有因此而变得更糟。如果你不想拥有自己的追踪器,并且你有或者喜欢你的界面漂亮一点(并且有更深的口袋),你可以看看 Atlassian 的 SAAS 解决方案,吉拉( www.atlassian.com/software/jira )。

对于高层次的任务跟踪和项目规划(尤其是如果你对使用看板系统感兴趣),你可能还会考虑 Trello ( www.trello.com )。

追踪器通常只是您想要用来共享项目信息的一套协作工具中的一个。你可以付费使用 Basecamp ( https://basecamp.com/ )或 Atlassian tools ( www.atlassian.com/ )等集成解决方案。或者您可以选择使用各种工具将工具生态系统缝合在一起。例如,为了促进团队内部的交流,您可能需要一种聊天或消息传递的机制。也许在撰写本文时,最流行的工具是 Slack ( www.slack.com )。Slack 是一个基于网络的多房间聊天环境。如果你像我一样是老派,你可能会立即想到 IRC(互联网中继聊天)——你可能是对的:除了 Slack 基于浏览器,易于使用,并与其他内置服务集成之外,你几乎可以用 Slack 做 IRC 做不到的事情。Slack 是免费的,除非你需要高级功能。

说到老学校,你也可以考虑为你的项目使用邮件列表。我最喜欢的邮件列表软件是 Mailman ( www.gnu.org/software/mailman/ ),免费,相对容易安装,可配置性强。

对于可协同编辑的文本文档和电子表格,Google Docs ( https://docs.google.com/ )可能是最简单的解决方案。

你的代码没有你想象的那么清晰。第一次访问代码库的陌生人可能会面临一项艰巨的任务。即使是你,作为代码的作者,最终也会忘记这一切是如何联系在一起的。对于内联文档,您应该看看 phpDocumentor ( www.phpdoc.org/ ),它允许您随时记录文档,并自动生成超链接输出。phpDocumentor 的输出在面向对象的上下文中特别有用,因为它允许用户从一个类点击到另一个类。由于类通常包含在它们自己的文件中,直接读取源代码可能涉及到从一个源文件到另一个源文件的复杂过程。

虽然内联文档很重要,但是项目也会产生大量的书面材料。这包括使用说明、关于未来方向的咨询、客户资产、会议记录和聚会公告。在一个项目的生命周期中,这样的材料是非常易变的,并且经常需要一种机制来允许人们在他们的发展过程中进行协作。

wiki (wiki 显然源自夏威夷语 wikiwiki 的意思是“非常快”)是创建超链接文档协作网络的完美工具。只需点击一个按钮,就可以创建或编辑页面,并且为匹配页面名称的单词自动生成超链接。wiki 是另一种工具,它看起来如此简单、重要和明显,以至于你确信你可能首先有了这个想法,但只是没有着手做任何事情。有许多维基可供选择。PhpWiki 我用的很好,可以从 https://phpwiki.sourceforge.io/ 下载,DokuWiki 你可以在 www.dokuwiki.org/dokuwiki 找到。

但是,对于文档(以及一般的写作),我越来越倾向于削减到简单的文本文档和版本控制。对于格式,我使用 Markdown,一种轻量级标记语言。它在渲染之前易于阅读,并且通常在渲染之后是干净和平衡的(尽管,和所有渲染一样,你是在渲染器的支配下)。降价的最佳起点是 https://commonmark.org/ 。经过多年与 Word 和 Word 兼容的文字处理器的斗争,我非常感谢 Apress 让我对这本书的这个版本使用 Markdown!

Note

虽然我没有省略这个工具(参见第十七章),但值得一提的是,转换到纯文本格式使我们有可能在本书的开发中广泛使用 Git。

本期的技术评论员(Paul Tregoing)希望看到 Docker ( https://docs.docker.com/ )被添加到持续集成的章节中,但是时间限制阻止了这一点。Jenkins 构建作业在 Docker 容器中运行意味着您可以完全自由地调整构建环境,而不受托管 Jenkins 的系统的限制。您可以使用不同版本的包甚至不同的 Linux 发行版来构建。

摘要

在这一章中,我总结了一下,重温了构成这本书的核心主题。虽然我在这里还没有解决任何具体的问题,比如单个模式或对象函数,但这一章应该是本书关注点的合理总结。

永远没有足够的空间或时间来涵盖一个人想要的所有材料。尽管如此,我还是希望这本书能够证明一个观点:PHP 已经完全成熟了。它现在是世界上最流行的编程语言之一。我希望 PHP 仍然是业余爱好者最喜欢的语言,并且许多新的 PHP 程序员会很高兴地发现他们只用一点代码就能走多远。然而与此同时,越来越多的专业团队正在用 PHP 构建大型系统。这样的项目不应该只有简单的方法。通过它的扩展层,PHP 一直是一种通用语言,提供了数百个应用和库的入口。另一方面,它的面向对象的支持使您可以访问一组不同的工具。一旦你开始用对象来思考,你就可以把其他程序员来之不易的经验绘制成图表。您不仅可以导航和部署参考 PHP 开发的模式语言,还可以参考 Smalltalk、C++、C#或 Java。我们有责任通过精心设计和良好实践来迎接这一挑战。未来是可重复使用的。

第一部分:对象

第二部分:模式

第三部分:实践