PHP-代码整洁指南-全-

PHP 代码整洁指南(全)

原文:zh.annas-archive.org/md5/68701152c2b302da8947f7b1336937bd

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

PHP 在几十年的时间里从创建 HTML 页面的简单脚本语言发展成为一个功能丰富的语言,拥有庞大的生态系统。由于绝大多数网站仍然由 PHP 驱动,它是互联网的基石之一。

虽然仍然适合初学者,但它可以用于实现从小型网站到全球使用的企业应用程序的一切。然而,PHP 的低入门门槛有时会导致难以理解和难以维护的代码。

通过这本书,我们希望带你进入干净的代码的世界。你将学习很多理论知识,以及如何在现实世界中应用你所学的知识。你将了解哪些工具将支持你的这一旅程,以及你应该使用哪些最佳实践来成功地在团队中实施干净的代码。

这本书面向的对象

这本书面向的是希望理解高质量 PHP 代码基础的初级 PHP 开发者,以及希望更新自己最新最佳实践的资深 PHP 开发者。

这本书涵盖了什么内容

第一章, 什么是干净的代码以及为什么你应该关心?,介绍了本书的主要内容。

第二章, 谁有权决定“良好实践”是什么?,解释了这些“规则”是如何决定的。

第三章, 代码,不要做花哨的动作,说明了为什么你应该考虑实用主义而不是试图炫耀技能。

第四章, 不仅仅是代码, 解释了为什么干净的代码边界不仅仅是指编写源代码。

第五章, 优化你的时间和分离责任,解释了如何通过培养新习惯来提高生产力。

第六章, PHP 正在发展——弃用和革命,快速概述了 PHP 中引入的最受期待的功能,有助于编写干净的代码。

第七章, 代码质量工具,教你了解将帮助你编写干净、可维护代码的工具。

第八章, 代码质量指标,审视了你需要了解的所有指标来评估你的代码质量。

第九章, 组织 PHP 质量工具,展示了如何保持你的工具井然有序。

第十章, 自动化测试,介绍了自动化测试并解释了为什么你应该进行它。

第十一章, 持续集成,探讨了如何持续维护代码质量。

第十二章, 团队协作, 介绍了在开发者团队中工作的最佳实践。

第十三章 创建有效的文档展示了如何创建有用且活跃的文档。

要充分利用本书

您需要在您的计算机上安装 PHP 8.0 或更高版本。所有代码示例都已使用 Linux 和 macOS 上的 PHP 8.1 进行测试。经过少量调整,它们也应该在 Windows 上运行,具体取决于您的配置。

本书涵盖的软件/硬件 操作系统要求
PHP 8.0 和以上 Linux、macOS 或 Windows

要阅读所有章节,您还需要安装 Composer。您可以在第九章 组织 PHP 质量工具中找到更多关于它的信息,或者访问getcomposer.org

如果您使用的是本书的数字版,我们建议您亲自输入代码或从书的 GitHub 仓库(下一节中有一个链接)获取代码。这样做将帮助您避免与代码复制粘贴相关的任何潜在错误。

下载示例代码文件

您可以从 GitHub 下载本书的示例代码文件github.com/PacktPublishing/Clean-Code-in-PHP。如果代码有更新,它将在 GitHub 仓库中更新。

我们还提供其他代码包,这些代码包来自我们丰富的书籍和视频目录,可在github.com/PacktPublishing/找到。查看它们吧!

下载彩色图像

我们还提供了一份包含本书中使用的截图和图表的彩色图像的 PDF 文件。您可以从这里下载:packt.link/b08Jl

使用的约定

本书使用了多种文本约定。

文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账号。以下是一个示例:“因此,我们将自然创建一个像 UserRemover 这样的服务,它将依次执行这两个任务。”

代码块应如下设置:

<?php
class Example 
{
    public function doSomething() bool
    {
        return true;
    }
}

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

{
  ...
  "scripts": {
      "analyse": [
          "tools/vendor/bin/php-cs-fixer fix src",
          "tools/vendor/bin/phpstan analyse --level 1 src"
        ],
        "post-update-cmd": "composer update -d tools",
        "post-install-cmd": "composer update -d tools"
    }
}

任何命令行输入或输出都应如下编写:

$ php phploc src

粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“将鼠标指针悬停在TestClass上会显示一个弹出窗口,其中解释说TestClass 未定义类型。”

提示或重要注意事项

看起来像这样。

联系我们

我们欢迎读者的反馈。

一般反馈:如果您对本书的任何方面有疑问,请通过电子邮件customercare@packtpub.com联系我们,并在邮件主题中提及书名。

勘误: 尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,如果您能向我们报告,我们将不胜感激。请访问 www.packtpub.com/support/errata 并填写表格。

盗版: 如果您在互联网上发现我们作品的任何非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过 copyright@packt.com 联系我们,并提供材料的链接。

如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问 authors.packtpub.com

分享您的想法

读完 PHP 清洁代码 后,我们很乐意听听您的想法!请点击此处直接进入此书的亚马逊评论页面 并分享您的反馈。

您的评论对我们和科技社区都至关重要,并将帮助我们确保我们提供高质量的内容。

第一部分 – 介绍清洁代码

本部分的目的在于介绍清洁代码的概念及其背后的理论。虽然随着章节的推进将讨论具体例子,但本部分旨在相当理论化。清洁代码的第一种方法被提出,以解释其有用性以及为什么它对开发者的生活是必要的。

本节包括以下章节:

  • 第一章, 什么是清洁代码,为什么你应该关心?

  • 第二章, 谁有权决定“良好实践”是什么?

  • 第三章, 代码,不要做特技

  • 第四章, 不仅仅关于代码

  • 第五章, 优化您的时间和分离责任

  • 第六章, PHP 正在演变:弃用和革命

第一章:什么是代码整洁性以及为什么你应该关心?

如果你刚开始接触PHP:超文本预处理器PHP)以及一般的软件开发,那么你还没有遇到过代码整洁性的可能性很大。我们所有人都是从这里开始的;每个人都必须意识到这一点。代码整洁性不仅仅是一套规则。它是一种真正的思维方式——一种思考你所创造事物的方式。

我们经常遇到那些在信息技术IT)领域工作多年,甚至几十年,却从未需要担心过代码整洁性的开发者。这可能是由于多种原因,比如多年来独自在一个项目上工作,或者在遗留代码库上进行开发,这些都是“代码整洁性”没有进入他们的耳朵或者不是他们关注的问题的几个原因。

早日意识到代码整洁性的重要性——越早越好。就像许多其他习惯一样,一旦你习惯了某种代码工作方式,就很难改变。可能是因为你对代码整洁性的理解并不完全确定,你不确定它是否适用于你,或者你可能只是对应用新的“规则”感到懒惰,因为你的方法已经对你有效(请放心,每个人都会经历这个过程)。但是,如果你关心它并且开始注意——那么,你已经完成了最困难的部分,因为这意味着你已经准备好改变你的习惯了。不要误解我们——我们的目的不是改变你迄今为止所学的一切;更多的是改进你已有的技能。

你可能没有意识到这一点,但可以肯定的是,你已经有一些完全符合代码整洁性原则的习惯,这些习惯对你来说是自然而然的,但对其他开发者来说却是全新的。你应该注意,代码整洁性原则的目标根本不是为了引起争议。

本章我们将涵盖的主要主题如下:

  • 本书将涵盖的内容

  • 理解什么是代码整洁性

  • 代码整洁性在团队中的重要性

  • 代码整洁性在个人项目中的重要性

本书将涵盖的内容

你可能已经听说过软件工艺书籍、代码整洁性书籍等等。这本书与其他书籍有什么区别呢?好吧,当我们作为开发新手,在探索新的编程语言,甚至第一次接触编程和源代码时,可能很难理解如何应用你到处都能看到的这些原则。编程语言并不都提供相同的功能——有些发展迅速,而有些则不再发展。一个语言中的概念和命名方式可能在其他语言中不适用。这就是为什么这本书专注于 PHP 中的代码整洁性。

你无需思考如何在 PHP 中实现这些原则和规则,这在吸收知识时是一种较少需要记住的事情。你会发现直接适用于你、在专业项目中使用并被行业证明的例子和工具。这样,我们可以直接进入我们希望您理解的重要观点。您可以在阅读这本书的同时立即将您的知识付诸实践,从而更快地学习。此外,您不需要 10 年以上的经验来理解将要展示的内容。只需要具备基本的 PHP 知识,就能完全理解将要展示的内容。

这本书是多年经验的汇编——多年的实践领域,面对必须根据技术约束、功能约束、时间和金钱约束解决的实际问题。这不会是另一本充满乌托邦原则的书,这些原则无法应用于现实生活中。

这本书不仅仅通过只谈论 PHP 来直接适应你的环境,你可以按照你想要的顺序阅读。它分为两个不同的部分。第一部分(你现在正在阅读的部分)将展示一些关于什么是清洁代码以及它的基本原理的理论,直接应用于 PHP 语言,直到版本 8.2 等。第二部分将专注于你可以使用的实用工具,以确保你正确地遵循正确的规则,设置环境以及你的集成开发环境IDE),以便尽可能高效和清洁,并对你的代码进行度量、自动化测试、编写文档等。这意味着你可以跳过部分内容,按照你想要的顺序阅读,以你想要的速率学习。

在开始实践之前,你不必阅读整个理论部分——你现在就可以直接深入研究工具和具体例子。你还可以在进入实践方面之前专注于第一部分,以确保你完全理解稍后将要解释的内容。这取决于你。

理解什么是清洁代码

清洁代码的行为是通过考虑未来来编写代码——也就是说,考虑那些将与你或项目合作但不在你身边的人。当我们谈论“将与你合作的人”时,这甚至可能包括你自己。如果你从未这样做过,你应该尝试阅读和维护你几个月(或几年)前编写的源代码。这不是很有挑战性吗?好吧,不要担心——这对每个人来说都很困难。你肯定会发现这很复杂,而且没有快速解决问题的方法。这是对每个人来说都是同样的过程

如下所述,有多个原因:

  • 你每天都在学习新的编码方式

  • 你正在练习,并且你在编码方面越来越擅长

  • 你的思维方式可能因为遇到的人或仅仅因为你变老了而正在演变

  • 每天都有创新的技术和库发布,一些精确完成任务的方法正在被弃用

  • 你正在使用的语言(这里指 PHP)正在进化,新的标准也在出现

谈到 PHP,最近几年看到这种语言在快速且大幅地进化,这确实非常有趣。几年前,很多人呼吁 PHP 的消亡。确实,在 PHP 7 之前,这门语言确实不太顺利。PHP 7 及更新的版本对我们来说是一股清新的空气。新版本正在频繁发布,PHP(只需看看 PHP 8.1 中的match表达式和对枚举的支持!)也在不断增加新特性。严格的类型检查也被引入,这也是一个巨大的进步。我们将在稍后深入探讨 PHP 的进化,这些进化有助于编写干净的代码。

无论你使用哪种编程语言——不可否认,每次你看到自己写的旧代码时都会感到害怕。以“干净”的方式编写你的代码将帮助你最大限度地减少这种影响,并立即更好地理解它,多年后也是如此。通过这样做,你是在给自己一个奖励,因为你将用一种你越来越习惯的方式——许多人习惯的编写方式来编写代码。

这可能是关于干净代码最令人印象深刻的事情:它无关乎你使用哪种技术、哪个库,甚至无关乎你使用的语言。它不取决于你居住的国家,也不取决于你的工作经验,甚至无关乎你是否以编程为生或作为爱好进行编程而没有背后的经济意图。干净代码是一种技能,在某个时刻,每个开发者都会在他们的生活中提高并面对。当然,如果你专注于它,你会变得更好,但仅仅从你在 IT 项目中遇到的成功和失败的经验中学习,就已经是在某种程度上学习干净代码了。除了这本书中你会找到的规则、工具和技巧之外,干净代码是一个关于经验和多年实践的问题。在你的开发者生涯中,通过结识不同的人和与众多不同的团队合作,你将遇到不同的挑战和不同的思维方式。

你应该始终记住,编码是一项团队工作。现在独自在一个项目上工作而不需要其他开发者的干预是很罕见的——如果这种情况还发生的话。这可能是一个专业项目、开源项目,甚至是一个私人个人项目。说其他开发者会帮助你的项目并不意味着他们会编写一些代码。他们也可以尝试与你一起解决你将面临的技术挑战,这适用于所有类型的项目。

在任何情况下,正确地做事都将简化我们所有人进入新项目时都会经历的至关重要的一步:入职流程。

干净代码在团队中的重要性

入职过程从来都不容易。你正在进入一个新的项目,周围很多人已经熟悉它。你必须学习一切:团队中谁负责什么,确切的要求是什么,团队经历过的技术挑战,以及接下来会发生什么。这个过程可能持续几周,在某些情况下甚至可能持续几个月(在一些巨大的遗留项目中,这个过程可能持续超过一年,但这又是另一个话题)。如果你能够通过了解清洁代码原则来节省学习编码约定的过程,那么这就是你不必担心的一件事,你可以专注于其他事情。

能够编写清洁的代码就像在团队中流利地使用相同的语言一样:它使得沟通和以相同的方式编写代码变得更容易,而不会注意到是谁编写了哪一部分。

清洁的代码有助于代码的长期维护。当作为一个团队在项目上工作时,源代码至少会维护几年。代码将在你离职多年后继续被维护。正确地做事很重要,因为你不会永远在这里向仍在项目上工作的人解释你所做的一切。

清洁地编写代码将避免我们所说的单点故障SPOFs)的出现。这些 SPOFs 是任何项目的最恶梦。它们也是遗留项目的症状。它们可以被描述为一个实体(一个开发者、一项技术、一个库等等)独自支撑整个项目。如果这个实体失败了,一切都会崩溃。

一个实体失败的具体例子是开发者的离职。如果他们是唯一一个完全理解项目运作方式的人,并被认为是该项目的“超级英雄”,那么这将会是一个大问题。这意味着没有他们,项目将无法继续进行,而且在他们离职后维护项目将会非常痛苦。

SymfonyWorld Online 2021 冬季版 大会期间,Symfony 框架的创造者 Fabien Potencier 给出了一个避免 SPoF 的绝佳例子。即使 Symfony 是一个开源项目,但他自从项目创建以来一直是投入最多的人。他在一次会议上解释说,他现在的首要任务是将尽可能多的关于框架的知识传授给尽可能多的人,这样 Symfony 就不再完全依赖于他。他绝对不想成为 Symfony 的 SPOF,并且自然希望 Symfony 在他不再亲自工作后仍能被维护和开发。

避免这种问题的众多方法之一是编写清晰、简洁、易于理解的代码——以这种方式编写代码,让每个人都能轻松理解正在发生的事情。在阅读这本书后,你可能会对如何实现这一点有更好的理解。

谈到团队合作,另一个关键点是代码审查。即使我们将在本书的后面部分详细讨论这些内容,但让我们在这里快速了解一下。如果你从未听说过代码审查,它就是一个过程,其他项目开发者会审查、阅读并评论你想要对代码库做出的更改。这个过程在大多数开源项目中是强制性的,并且鉴于它带来的好处,对于任何项目都强烈推荐。现在你可以很容易地想象,如果每个人都用自己的方式编写代码,那么这些审查将会花费更长的时间。这包括你格式化代码的方式,或者你是如何分离文件、类等等。如果我们都使用同一种语言,那么我们就可以在代码审查时专注于真正重要的事情:是否尊重了功能需求,是否存在任何错误等等。

在下一章中,我们将看到干净的代码定义通常取决于你正在与之合作的团队。即使有一些通用规则,这主要还是取决于你合作的开发者习惯于什么。再次强调,原因是习惯和一致性。这样,团队可以更快、更好地一起工作。

如果你恰好在一个有几个团队聚集在同一地点工作的领域工作,你不可避免地会注意到每个团队都有自己的规则,这些规则对他们来说很有效,但也有共同的规则,这将使你能够用同一种语言与所有人交谈。因为解决问题的方案并不总是来自你直接所在的团队,有时也来自那些没有直接与你一起在同一个项目上工作的人,如果人们之间共享基本规则,那么互相帮助将会容易得多。

个人项目中干净代码的重要性

你可能认为对于个人项目来说,干净的代码不那么重要,甚至只是一点点。如果你这样想,有很多原因,如以下所述:

  • 你如何确保未来没有人会参与到开发中来?

  • 如果你将你的项目开源,你难道不希望全世界看到你编写代码干净整洁,并为此感到自豪吗?

  • 你可能想要反复改进你的项目。如果你编写了糟糕的基础,那么在没有担心破坏任何东西的情况下维护和添加新功能将会是一场噩梦。有时,由于糟糕的代码编写,你甚至无法添加新功能。在最坏的情况下,你可能不得不完全重写你的应用程序。你不想这样做。

如前所述,尝试阅读你几年前编写的代码。有很大可能性你将无法简要地理解你当时想要做什么。你可能认为你放置的注释(如果你这样做)是为了帮助,但让我们提出两个问题,如下:

  • 谁真正阅读代码注释?

  • 关于编写无需注释且清晰得像一本书一样可读的代码呢?

这就是一切的核心:能够像阅读简单句子一样阅读源代码,而无需停下来理解它。此外,关于团队中提到的干净代码:仅仅因为你是在独自创建项目,并不意味着你不会需要外部帮助。实际上,几乎可以肯定的是,总有一天,你将需要某人的帮助来完成某事。再次强调,如果你共享一套共同的规则,那么帮助者引入他们的想法将会容易得多。即使在个人时刻和项目中,开发者的工作也是团队合作。始终如此。

干净代码是一种心态,这种心态包括为我们所创造的东西感到自豪——工作完成的滋味。也许需要几年时间才能完全理解它是什么,我们总是有新事物要学习,有新情况要面对。干净代码是朝着这样一个时刻迈进,当你看着你的代码时,你会告诉自己你对向任何人展示你的代码感到完全放松。

正如俗话所说,“编写代码就像下一个运行它的开发者知道你的地址一样”。这也应该适用于个人项目,因为你就是那个下一个开发者。

摘要

我们的理论讨论完了吗?嗯,有点。我们一起定义了干净代码的构成。我们给出了干净代码的共同定义。通过拥有相同的干净代码定义,我们更深入了一步,并准备好在下一节深入更高级的原则。

当然,我们还没有完成。即使你同意我们提出的定义,你当然可能已经有很多问题。最重要的是以下问题:谁为你决定这些规则?谁有权力强加这种愿景?这就是我们在下一章将要一起探讨的内容。

第二章:谁有权决定“良好实践”是什么?

良好的实践固然很好,但了解谁决定它们以及它们从何而来则更为重要。当你完全理解自己在做什么时,你会立刻感到更好、更自在,这一点同样适用于良好的实践。你为何要无条件的相信那些你并不了解且从未与你合作过的人所决定的这些原则呢?

你可能会说,制定这些原则的人比你有更多经验,并且比你更了解这个世界。两点。首先,也许有一天你的经验会比他们多。也许你会做得更好。也许你已经做到了。其次,多年的经验并非一切。我们经常看到有 20 年或 30 年经验的开发者,他们完全过时或有着上个世纪的习惯。多年的经验可以是一个论据,但不是唯一的论据。计算机发展速度极快,而网络世界更是受到这种影响。

在本章中,我们将一起探讨最佳实践的起源:它们真的是由一个精确的小组发明和决定的吗?你现在就可以应用到你的项目中的不同现有清洁代码原则有哪些?一旦你了解了它们,你的思维方式可能会改变。

本章我们将涵盖的主要主题如下:

  • 谁来决定这些事情呢?

  • 最佳实践——它们真正从何而来?

  • 保持情境意识

  • 保持一致性——更快地获得结果

谁来决定这些事情呢?

我们将要看到的一点是,你应该始终质疑“良好实践”,永远不要将它们视为一个你必须尊重而不理解其为何的普遍真理。当你不同意某人的评审或观点时,询问是一个提升自己的极好方式。开发者们非常迷人,因为他们可以找到无限种解决同一个问题的方法——为同一个结果提供无限种解决方案。即使这有时可能看起来有点累人,但了解为什么开发者想要以与你不同的方式解决问题总是很有趣的。这有几个目标,如下所示:

  • 你会提高你的沟通技巧:如果你想进行沟通并被理解,你必须清楚地解释你的问题。

  • 你可能会学习到做事的新方法:我们都在使用同一种语言,但我们每个人都有不同的使用经验。这些不同的职业和生活道路可以为会议带来极好的想法。

  • 你正在加强与这位开发者的关系,这将使得未来讨论更多主题变得更加容易。

  • 你正在提高你的团队合作技能,并且对话中的所有参与者都在相应地提高他们的技能。

这确实是一个相当长的列表。许多软技能仅仅通过与其他开发者讨论解决特定问题的方法就能得到提升。能够清楚地解释一个情况比看起来要困难得多。

这也是为什么你应该总是在你不同意某人的观点时要求进一步解释的原因。首先,可能存在对暴露问题的误解。这是一个常见的多方面意见不一致的情况:问题一开始并不明确。每个方面都在试图证明他们理解的内容。你可以很容易地想象这会带来多大的混乱。

成为一名优秀的开发者(也)意味着能够证明和解释你所有的选择。不再有:“我们总是这样做;没有其他理由这样做。”当你决定或告诉某人遵循某些指南时,你必须始终能够清楚地证明和解释为什么你的方式对你来说是最好的。这可能不是客观上做这件事的最佳方式,但如果你能够解释为什么它最适合你,这将使你看起来更加开放。

话虽如此,你现在可能已经明白我们想要表达的意思了:没有人掌握绝对的真理。如果有人足够自信地说出这一点,你应该始终保持警惕。

最佳实践——它们真正来自哪里?

当我们谈论“最佳实践”时,我们可以区分三种情况,如下所示:

  • 经过几十年证明有效的原则,这些原则也源于常识:在这个类别中,例如我们可以找到设计模式。简而言之,如果你不知道它们,这些是解决重复编程问题的工具。它们已经存在了几十年,并且被数百万开发者所熟知。

  • 因为我们不得不做出的选择:在这里,我们可以找到诸如代码风格、命名约定等等。从技术上讲,如果你想要使用camelCasesnake_case来命名你的文件,这并不重要。但如果每个人都遵循相同的规则,那么每个人理解彼此会更容易。

  • getsetis等等。如果每个人都对命名访问器和修改器有自己的规则,你可以确信总有一天,没有任何警告,事情会失控。

设计模式原则

设计模式案例描述了解决问题的客观解决方案。你可能不喜欢它们以及它们如何组织代码,但你不能说它们在客观上是不好的。因为你的想法并不重要,它们是有效的。

讨论了几十年一直存在的原则,我们可以突出四个著名的:DRYKISSYAGNISOLID

DRY

DRY代表不要重复自己。这个原则简单地说,你永远不应该在你的应用程序中有两个权限做完全相同的事情。这听起来可能很显然,但应用这个原则可能并不总是本能的,尤其是当你刚开始应用编程时。在代码的两个不同地方有相同的责任意味着每次你修复某事时都要维护这两个地方。这意味着每次更改时都必须考虑这两个地方(而且肯定有一天你会忘记其中一个)。此外,如果两件事有相同的责任,任何开发者如何维护你的源代码才能知道该使用哪一个呢?

KISS

KISS代表保持简单,傻瓜。有时,我们会使自己的生活复杂化。我们可以看到有两个主要原因,如下:

  • 首先,我们试图用我们的代码做复杂的事情,但这些花招并没有带来任何有价值的东西,反而使代码复杂化。我们将在本书的后面详细说明为什么我们绝对需要避免这样做。

  • 第二个原因是缺乏对我们所做的事情的视角。我们花费了很多小时试图解决问题,我们太“投入”了。一些休息是必要的,以获得这种视角,有时甚至需要从头开始。

这两种情况都是常见的,阻止我们直接切入要点并保持简单。当你觉得你“走得太远”时,想想这个缩写词,以便回到正轨。

YAGNI

YAGNI代表你不需要它。在某种程度上,它与 KISS 原则相辅相成。想要通过考虑未来来找到问题的解决方案是很常见的(不用说,这是开发者日常生活的部分),我们经常有这样的想法:“如果明天我需要做这样或那样的事情,至少这已经准备好了。”现实是,总的来说——不会——我们永远不会需要我们想要预见的需求。然后,试图提前完成一个可能永远不存在的任务,并且其功能约束是未知的,不仅浪费时间,而且通过过于前瞻性的思考使我们的生活复杂化。我们偏离了最初的目标,即找到快速、可行和健壮的解决方案。

我们不是先知,无法预知预测的任务会出现所有问题。你很快就会意识到,如果你保持简单,不添加任何多余的代码来试图领先于明天的需求,你将拥有一个健康、无装饰的代码库。这意味着更快地理解代码,更容易在其中导航,并在真正需要时进行更改。此外,你可能会节省很多错误。如果因为你在源代码中开发并花费时间在未要求的事情上而造成错误,那么总是很复杂(如果不是不可能的话)来证明这个错误。如果你的工作作为开发者需要你直接与客户合作,你应该知道客户不会为你没有要求的事情付钱。你将免费工作,这从来不是理想的情况。

注意:显然,我们必须根据每个案例来处理。我们可以以魔法数字为例。魔法数字是常量值,主要是数字,硬编码且没有任何解释其含义。

结论:两周后,每个人都忘记了这个数字代表什么。然后我们会考虑使用命名良好的代码常量。然而,有很大可能性这个代码常量的值永远不会改变,因为需求已经发生了变化。乍一看,想要声明一个常量并在所有地方使用它似乎很奇怪。代码常量的目的是为固定值添加语义,并允许我们轻松地在代码中任何使用该值的地方一次性更改它。这在某种程度上与 YAGNI 原则相悖(因为这些值可能永远不会改变)。

然而,我们可以看到使用常量的价值。无论应用了哪些清洁代码原则,视角和反思总是必要的。

SOLID

最后,可能是最著名的一个,SOLID。让我们看看这些字母代表什么:

  • S 代表 单一职责原则(通常缩写为 SRP)。非常简单地说,这意味着你的代码中的类必须只响应一个任务。显然,这个任务的大小是这里的关键点。我们不是在谈论创建一个只有一个可用方法的类。而是在谈论创建一个逻辑分解。一个非常具体的例子是 模型-视图-控制器MVC)架构。重要的是要记住,你必须避免有万能的类,将数据库操作、超文本标记语言HTML)渲染、业务逻辑等组合在一起。分解必须是逻辑的。分解的一个例子可以是用于生成 HTML 的类,用于特定对象的数据库交互的类,等等。

  • ifelse语句。确实,如果你使用条件分支,你将修改类,如果你有超过两个情况,这可能会很快变得难以管理。通过扩展类并重载你感兴趣的方法,你可以得到简洁的代码,很好地分割,并且没有数百行的分支。

  • 如果foo方法和另一个实现返回一个对象,这将变得复杂)。幸运的是,返回值的类型在 PHP 的最新版本中存在,限制了违反此原则的可能性。

  • null值。当你这样表达时,你会意识到这听起来并不像非常“干净的代码”。

  • 最后是MailerInterface接口。这样,你将为每种邮件服务有一个实现。通过使用接口的参数,该方法能够接收任何实现,并为你当前的情况使用正确的电子邮件服务。

我们可以很快地意识到这些原则之间有着非常紧密的联系。它们共同工作,并在你编写代码时考虑到这些原则,允许有强大的解耦、责任分离和流畅的思维。记住这些原则可能非常有帮助;至少,了解它们的存在是一个特别好的事情。除了 SOLID 原则外,你还可以看到 KISS、DRY 和 YAGNI 都是非常直观和逻辑的。时不时地记住它们可能有益,并有助于我们在稍微偏离轨道时设置障碍。

奖励 - 指导员原则

还可以添加的是,这也是一个常识性原则,即“指导员原则”。我们都知道那些有善意的年轻人和青少年群体,他们表现出极大的利他主义。指导员在树林里露营,生火,并在那里过夜。一旦他们早上起床,他们可能会熄灭火焰并收拾东西,但最重要的是,他们会清理这个地方,使其比他们到来之前更干净(至少,在理论上)。

作为一名开发者,情况也是一样的。成为一名指导员。在探索和浏览代码时,如果时间和环境允许,清理你看到的一些技术债务通常是一个特别聪明的想法。如果你正在遍历代码库中的地方,并想“这真的很糟糕”,这可能是一个机会使其更易于管理和更干净。如果每个人都参与进来,项目的源代码质量可以迅速提高。

当然,这个“童子军原则”必须符合你的项目约束、时间约束和客户需求。此外,这相当有风险,你必须知道何时停止。当你将你的更改发送给团队进行审查时,你所做的更改和清理必须保持一致。你不想每次发现一点小问题就重写一半的应用程序,这会导致另一个问题,然后又是另一个,以此类推。这更多的是关于清理与你所做事情相关的事情。保持专注并固定在你的环境中可能会非常复杂。关于“何时停止”没有真正的答案;这将很大程度上取决于你拥有的时间和你的任务。然而,没有什么可以阻止你写下你想要回来但不幸地与你的工作无关的事情,这些事情似乎太耗费精力和时间,或者需要与团队进行进一步的反思。

相反,代码风格、命名约定和类似的事情都是主观的,取决于个人品味和习惯。每个人都有自己的品味和习惯,所以需要做出决定。正如我们之前讨论的,当我们都遵循相同的规则时,一起讨论要容易得多。

那么,谁决定团队或组织中的最佳实践呢?嗯,通常情况下,这是在项目开始时关于该主题长时间讨论后的共识。因为是的——“最佳实践”并不是你可以在任何地方都适用的东西,你应该意识到这一点。你应该意识到你所处的环境。

保持情境意识

在我们讨论清洁代码时,这里是我们进入最重要的部分之一。如果我们只能记住一件事,那就会是这件事。我们可能会经常谈论其他开发者定义的规则、面向对象原则和清洁代码原则,但没有什么会比我们即将讨论的内容更好:这是关于意识到你的情境。许多关于清洁代码的书籍和文章中缺少的是这种感觉,即它与日常生活相关。开发者的生活由意外事件、技术约束、无法完成某些事情或被迫做某些其他事情组成。

做事情的方式和项目一样多。每个项目都有其自己的历史、技术决策和约束。因此,我们最终得到了许多理论原则,这些原则可能不适用,或者会破坏项目的连贯性。良好的实践可能会指导你如何命名变量、如何命名你的类和方法、如何命名你的文件,或者如何构建你的项目树结构。然而,如果这与项目中的设置相矛盾怎么办?这是一个非常常见的问题,尤其是在所谓的“遗留”项目中。

答案既简单又复杂。它之所以简单,是因为可以总结为一句话:与你的团队和其他参与项目的人讨论。这也是答案可能变得复杂的地方,因为它很可能会在团队中引发辩论(有时是热烈的),这是很正常的。但重要的是:你不仅要找到一个大家都会尊重的共识以保持与项目的连贯性,还要找到对你和你的团队最有效的方法。

这可能是所有这些讨论的基础:你可能有一些良好实践和清洁代码原则,但也许有些规则值得调整,因为它们对你和你的团队来说效果更好。这是偏离某些原则(当然不是所有原则,否则就是完全的无政府状态)的一个完全合理的理由。有时,找不到共识,这时最佳实践和其他原则可以占上风,制定出每个人都必须遵守的规则,如果不是对每个人都适用的规则。

这时,我们回到了之前提到的一个观点。在开发者团队中拥有共同规则有助于使项目更容易理解。在项目中的导航,以及文件中的导航都变得更简单。通过使用相同的语言,我们更好地理解彼此。无论是帮助他人还是被他人帮助,总是同一个故事。如果遵守了共同的命名和缩进规则,你不仅可以避免那些会分散你注意力、远离最初目标的枯燥辩论,而且如果你觉得你的队友编写的代码就像你自己的代码一样,那将是一个双赢的局面。

我们可以这样总结决定采用良好实践和“经典”清洁代码原则的选择:

你的情况在项目过去是否已经出现过?

  • 是的。它是否遵循了你使用的工具(如 Symfony 的良好实践)所规定的清洁代码原则和良好实践?

    • 是的。在这种情况下,你只需要遵循过去处理这种情况的方式。

    • 。你应该与你的团队交谈,找出原因。可能由于功能和/或技术限制存在历史原因,或者可能根本没有任何原因。在这种情况下,如果你同意,你可以遵循良好的实践(如果你有修改相关代码部分的可能性,并且有时间,可以尊重我们之前讨论的“童子军原则”),。

  • 。是否可以应用良好实践和清洁代码原则?

    • 是的。太完美了!你只需要尽你所能将它们应用到团队和项目中现有的其他实践之中。

    • 。在这种情况下,你应该与你的团队讨论,也许还应该与项目外有类似经验的人讨论。再次,这些讨论可能需要相当长的时间,而且答案不会总是立即找到。辩论将会被提出,这将是件好事。一旦你找到了共识或进行了深入的讨论,你既可以重新思考清洁代码原则和良好实践的应用,也可以应用团队为这种情况设定的规则。

在这里,我们再次非常清楚地看到,成功的关键将是沟通和辩论。每个人都有自己的观点和解决问题的方法,所以事情永远不会是全黑或全白。但请记住一点:你应该避免就一个可能被辩论的实践单独做出决定。避免单独做出决定并不意味着你不应该做出决定。相反,我们这里的含义是思考选项,权衡每个选项的利弊。再次,能够为每个你提出的建议提供合理的依据给项目中的其他人。如果你提供几个解决方案,并很好地解释为什么这些解决方案是合适的,同时也解释了这些解决方案所涉及的风险,你将很快意识到这项工作单独做可能会很复杂,因此与团队讨论其重要性。每个大脑都有其独特的工作方式。

顺便说一下,这一切不仅适用于 IT、清洁代码和 PHP。这就是为什么在上一章中提到,清洁代码不仅仅是一套规则:它是一种生活方式——它是一种思维方式。

保持一致性——更快地获得结果

在你所做的事情上保持完美的一致性将迫使你理解你在做什么。然后,一切都将变成习惯。如果你有这些良好的习惯,以至于它们对你来说已经变得自然,那么在以下两种具体情况下,结果将会更快地到来,如下所述:

  • 正如我们从一开始就能看到的,你将能够更快地在团队内部相互理解——开发者将拥有相同的习惯。更少的情况下,但这种情况可能发生:你有时可能需要与项目中的非技术人员讨论代码或展示事物。尽管这些人——例如产品负责人——可能有一些基本的技术知识,但最好假设你需要回到最基本的基本知识。如果你在工作中的事情简单、干净、毫不犹豫,你将更容易向非技术人员解释复杂和技术的主题。

  • 第二种情况是在自动化检查期间。自动化检查是已经与团队设置和讨论过的任务,每次你想提出更改时都会执行。这些检查可以在多个地方进行。它可能是在你编写代码的软件中(如 NetBeans、PhpStorm 或Visual Studio CodeVS Code)),通过持续集成CI)工具(GitHub Actions、GitLab CI/CD)等。

自动审计任务可以包括你想要的任何任务。我们将在本书的第七章代码质量工具部分)中详细介绍如何执行这些任务(第七章, 代码质量工具),但这里是最常见的几个:

  • 检查代码样式和缩进

  • 运行测试套件(单元测试、功能测试…)

  • 对你的代码进行静态分析,以确保你使用的变量定义良好,使用正确的类型等

  • 通过选择一个渠道(如电子邮件、即时消息如 Slack 等)设置警报,以通知你任务的成功或失败

  • 将代码部署到测试环境

  • 安装依赖和供应商

  • 将文件复制到服务器并进行一些远程操作

  • 无论你想要什么!

你明白了。这些工具允许你执行你想要和需要的任务。实际上,它们只是执行你定义的命令的协调者。就这么简单。之后,如果你的命令很复杂,那就另当别论了。但你意识到这些自动化检查可以无限扩展。

关于源代码分析工具

通过养成良好的代码习惯,你肯定会加快这个过程。也许最具体和有说服力的例子是样式代码检查。如果你不知道如何在你的团队中编写代码,并且你的团队对你想要更改的每一项内容都设置了自动检查,你可能会花几个小时来找出一个地方缺少了一个空格,另一个地方缺少了一个换行符,等等。不用担心——大多数工具都提供自动纠正这些错误的选择。

然而,静态分析工具的情况并非如此,例如我们将在关于静态分析工具的章节中设置的工具,第七章代码质量工具。实际上,静态分析将审查你的代码,并确保不会犯最常见的错误。我们不是在检查你的制表符中的空格数量,而是在真正解析 PHP 代码,试图理解它并确保一切井然有序。这些工具有时可能会过于严格,正确理解它们可能需要花费大量时间。此外,这些工具并不完美,静态分析工具可能无法理解你的意图。尽管这是一个单独的问题,但如果你在源头处理它,你会省去很多麻烦:培养良好的编码习惯。要彻底,不要留下任何侥幸心理。PHP 是一种非常宽松的语言,允许你用变量做几乎所有的事情——例如,允许你在不皱眉的情况下进行类型转换。正如我们所知,这类操作的结果可能是随机的,甚至非常令人惊讶,看起来完全不合逻辑。无论如何,PHP 就是 PHP。尽管静态分析工具大多数时候可以识别这些风险案例,但你可能需要花费数小时来纠正这些可能迅速增加到数十的小问题。

此外(这可能听起来很傻),通过实施清洁代码实践和随之而来的内容,你的代码越干净,你实现的错误就越少。通过自信,你可以避免很多惊吓。此外,你还允许未来的开发者在你代码上工作时不会受到欺骗,并使他们的工作更容易。如果你足够幸运(或者如果你已经应用了清洁代码及其相关原则!),你将拥有确保应用程序正确运行的测试。也许你对测试不太熟悉,所以让我们一起来了解一下。

关于测试及其多种形式

测试主要是开发者编写的代码行。这些测试确保对于某些输入数据,会返回特定的输出。这些输入和输出I/Os)可以是各种类型和大小。它可以是一个整数,也可以是一个生成的 HTML 页面,甚至是一张图片。为了使事情更容易,并简化概念,自动化测试通常被分为三个主要类别,如下所示:

  • 单元测试,这是粒度最细的测试。它们通常评估代码中方法的返回值,而忽略它们周围的一切。重要的是函数返回的结果,仅此而已。

  • 功能测试具有中等粒度。它们将评估完整的功能,其中可能涉及多个参与方和方法。最明显的例子是对应用程序编程接口API)的测试:我们检查如果我们用特定的参数调用一个特定的统一资源定位符URL),API 是否会返回预期的结果。

  • 端到端E2E)测试是最难维护的。这些测试通常会模拟一个网络浏览器,并由自动控制。一个经典的例子是登录表单的测试。机器人会自动填写字段,点击按钮,确保我们被重定向并且 HTML 页面上有成功消息,等等。

所有这些测试都存在一个特定的原因:非回归。

没有疑问,非回归测试是能保住你在公司中作为开发者的位置的事情。好吧——这可能有点夸张。然而,我们不能统计有多少应用程序得益于它。当你的测试覆盖率足够高(被一个或多个测试覆盖的代码行和特性的比例)时,你几乎可以修改任何东西,并且如果测试总是绿色的,你就可以确信永远不会破坏任何东西。确实,你可以打破特性以用不同的方式重写它们并测试新的做事方式。只要测试是绿色的,你就可以确信应用程序的行为是正确的,就像你修改之前一样。当然,还有其他一些因素需要考虑。

首先,测试必须测试某些东西。这听起来可能有些奇怪,但实际上,你最终会得到很多实际上并没有评估任何东西的测试。最典型的例子是一个类中设置器和获取器的单元测试。当你为这些编写测试时,你是在评估一个变量赋值是否完成以及一个方法调用是否完成。你是在测试... PHP!而 PHP 已经有了自己的测试。编写相关的测试本身就是一本大书,并且是一种可以在多年中掌握和精炼的艺术。为此,没有什么比反复练习更好的了。

当你修改应用程序时,需要考虑的第二件事是,你设置的测试已经不再是最新的,必须进行修改。有时,可能很难理解一个测试是自愿失败(即,它与你的更改不兼容)还是无意中失败(因为当它应该以与之前相同的方式表现时,应用程序没有这样做)。这完全取决于你的情况。记住一件事:如果你的应用程序被正确测试,你可以在任何时候,毫不犹豫地更改任何代码行并部署你的应用程序,即使闭上眼睛也可以。这很令人感兴趣,不是吗?

现在你已经看到了测试在你应用程序中的好处,我们可以讨论一个在清洁代码实践中相当常见的做法。你可能已经听说过它:TDD(测试驱动开发)。

TDD代表测试驱动开发。这是一种在编写其余代码之前先编写测试的方法。一开始,它非常令人困惑。理解它是如何发生的,甚至是否可能,都很复杂。它涉及到逆向思考和质疑我们的思维习惯,尽管原则本身相当简单。首先,我们考虑测试——也就是说,我们将要发送的数据(到单元测试中的方法,到功能测试中的 API 端点等),以及我们想要的输出(单元测试中的精确对象或值;例如,在 API 功能测试中的精确JavaScript 对象表示法JSON)返回值)。显然,因为你还没有编写其余的代码,所以所有测试都会失败。这是故意的。现在的目标是让这些测试一个接一个地变为绿色。

如果你尝试这种实践,你会意识到一种魔法般的事情在不经意间发生了。你会以你最初绝不会这样做的方式组织你的代码。它将被切割成清晰而精确的方式,以便你的测试可以尽可能快、尽可能容易地通过。除了将带来的非凡的智力满足感之外,你最终会得到更易于阅读和更干净的代码。而且(与普遍的信念和直觉相反),开发速度会更快。确实有一个适应期,让它变得相当自然,你可能会觉得一开始进展非常缓慢。然而,正因为如此,你的代码更简单,因此编写、理解、适应和扩展的速度更快。你必须尝试它来体验这一点,因为它听起来可能有点神奇。实际上,它确实在某些方面是神奇的。

此外,随着你的开发,代码覆盖率会越来越广泛。这直接影响了你应用程序的维护,正如我们之前所说的:你将更有信心修改你的代码,以及所有将不得不阅读和修改它的人。测试将保护你。作为额外的好处,阅读测试对于理解复杂代码非常有价值。通过阅读测试,你可以从给定的 I/O 中了解编写测试的开发者想要去哪里。在大多数应用程序中,尤其是遗留应用程序中,这是无价的。此外,许多开发者在审查你的代码时首先查看测试。当你进入某个代码库的更改时,这是一个惊人的切入点。把你的测试看作是证明你刚刚所做事情真正工作的唯一方式。这是一个现实:大多数对干净代码敏感的开发者认为测试是证明你的更改工作的唯一有价值的证据。

例如,可以注意到,对于大多数(如果不是所有)像 PHP 这样的开源项目,在做出更改时你必须添加测试。无论是添加新功能还是修复问题,测试将是强制性的,你的更改在没有测试的情况下永远不会被批准。这些测试将再次成为你新功能行为或你确实修复了所讨论的 bug 的不可辩驳的证据。所有这些工作都是很多,但有了这些,代码覆盖率变得巨大,你完全为软件的稳定性做出了贡献。

测试对于你代码的质量和获得结果的快速速度都是无价的。多亏了它,你将保持一致性,并且会更快地得到结果。

摘要

我们刚刚一起学到了很多新知识。如果你理解了它们,你可以确信你已经比上一章的你自己成为一个更好的开发者了。

了解 SOLID 原则在专业领域和工业级项目中是一项真正的资产。即使每个案例都不同,每个项目都有其特殊性,但这些原则的优势在于几乎适用于任何地方,至少是受到了极大的启发。

记住 KISS、DRY 和 YAGNI 原则将帮助你保持脚踏实地,在接下来的开发中不要过于分散精力。它们强调思考当下,以帮助为未来做准备,而不是试图适应当下而思考未来。你绝对应该记住这一点。我们不知道未来将对我们施加的技术或功能限制,因此考虑如何使处理这些限制更容易更有意义,而不是猜测它们。因为说实话:我们几乎没有击中靶心的机会。

如果你有机会和可能性,在 TDD 策略中实施“童子军原则”可以带来更多的好处,并且始终是一个极好的主意。如果你从未实践过 TDD,尽管本章提供了解释,但对其有用性和——尤其是——其有效性感到完全怀疑是很正常的。这是正常的,我们都有过这样的经历。然而,结果就在那里,关于这个主题所进行的各种案例研究都证明了这一点。可能是时候尝试这种做事方式了,这种方式得到了清洁代码资深人士的高度认可和赞赏!

尽管如此,我们必须记住,干净代码也关乎适应其环境。这并不是一个完全重写应用程序并改变开发团队所有习惯的问题,理由是项目外的人决定这样做。你必须意识到你的环境,并处理你的环境。你必须能够适应需求和周围的环境。这就是让你成为一个优秀的“干净程序员”的关键。记住,当感知到习惯的改变时,尽可能与你的团队沟通,并能够证明你所有选择。如果可能的话,始终提出几个解决方案,以及每个方案的优缺点。

在所有这些理论之后,我们可以继续进入一个稍微更实用的部分。编写干净代码的方法有哪些?代码的目的是什么?尽管我们已经看到了一些高级原则,但我们不应忘记基础知识,同时也应该质疑我们已知的内容。

第三章:代码,不要玩杂技

清洁代码的高级原则实际上会帮助你成为一个易于理解的开发者,能够编写更干净的代码。它们教你保持选择的一致性,考虑其他开发者和你的团队,并将沟通作为我们工作的主要工具。甚至在源代码之前。

事实是:虽然源代码在开发者的工作中占据了主导地位,但我们不应将其作为我们存在的首要理由。这是一个现实:开发者的工作不是编写代码。它关于在适应会阻碍我们的限制条件的同时,找到解决给定问题的方法。这是我们工作的基础,我们必须绝对牢记。尽管我们在上一章中看到的原理,如 SOLID,似乎与代码紧密相关,但我们必须尝试对所有这些事物有一个更“元”的视角,跳出思维定式,退后一步。这些提到的原理,从客观上来说,是允许我们以高效和直接的方式解决问题的工具。

我们可以问自己以下问题:源代码的真正目的是什么?它的目的是什么,我们是否可以允许自己在语言的最基本事物上做任何事情?

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

  • 理解代码

  • 被理解,而不是聪明

  • 关于可维护性的说明

理解代码

让我们先问问自己代码的重要性。对于我们开发者来说,它在我们的日常生活中真正的重要性是什么?为了回答这个问题,让我们回顾一下过去。

一点历史

计算机编程实际上是一个电流通过或不过的晶体管。因此,我们得到了一个二进制系统,如果电流不通过晶体管,则值为0,如果电流通过,则值为1。如果你将这个晶体管的数量乘以数十亿,你最终得到今天的处理器。它工作得非常好,我们的世界已经由这个系统统治了几十年。然而,有一个明显的局限性:仅用 0 和 1 来理解和创建应用是不可能的。因此,我们必须找到一种新的编写这些程序的方法,使它们变得对人类来说是可能的并且可管理的。

我们接下来转向第一个人类可读的源代码:汇编语言(通常缩写为ASM)。这种语言在 20 世纪 40 年代末开始流行,它终于使得人们能够用一种与我们的自然语言或多或少相似的语言来读取文件,尽管汇编语言是一个非常低级的语言(这意味着它与机器语言——即二进制语言——非常接近)。一环扣一环,更高级的语言随之出现——其中 C 语言最为知名,它在 1972 年首次发布了官方版本。其原理很简单:能够用越来越自然的人类语言来编写计算机程序。然后,一个工具会自动将这种高级语言翻译成机器可以解释的汇编语言和二进制语言。

C 语言是编程语言的有用性和主要目的的一个绝佳例子。确实,这种由 Dennis Ritchie 和 Brian Kernighan 创建的语言最初被用来开发 Unix 操作系统。关键在于,用 C 语言编写 Unix 操作系统比用当时的工具——即汇编器(即使 Unix 操作系统的某些部分是用汇编语言编写的,但绝大多数源代码是用 C 语言编写的)编写操作系统要容易得多。接下来,我们将探讨编程语言和代码的实际目的。

代码的目的

正是这里,一切都有了意义。编程语言的存在是为了帮助我们尽可能容易地将我们的想法记录下来,并且它们可以被机器理解。但编程语言不仅仅局限于将我们的想法转录给机器——它们的目的是让其他人通过阅读我们的代码就能理解我们的想法,而无需我们亲自解释。编程语言是一种微妙混合体,它既具有被人类理解的可能性,又给我们提供了与机器通信的自由度,以及利用其全部潜力的可能性。这就是当我们谈论高级语言和低级语言时,定义编程语言水平的原因:光标的位置在“易用性和理解性”与“语言提供的性能和可能性”之间。在启动新项目时选择最合适的编程语言时,需要做出必要的权衡。一个完美的例子是PHP:Hypertext PreprocessorPHP)语言中捆绑的所有工具,用于处理Hypertext Transfer ProtocolHTTP)请求和响应,这使得它成为创建网络应用的绝佳选择。大多数你需要的功能都已经内置,无需安装任何东西就可以处理最基本的一些相当高级的网络应用功能。

PHP 是用 C 语言编写的;它是一种比 C 语言更高级的语言。因此,它更容易理解,更宽容,但与 C 语言相比,它提供的性能扩展更少,可能性也更少。如果你需要在 PHP 中编写一些汇编代码来与某些定制硬件通信,例如,声明很简单:你不能。你必须编写一个 PHP 扩展,这个扩展将用 C 语言编写(这然后允许你在汇编语言中编写源代码部分)。这是一个超级高级的案例,当然,但你的观点已经明确了。

让我们进一步比较 PHP 和超文本标记语言HTML)。尽管 HTML 不是一种编程语言而是一种描述性语言,但它与 PHP 仍有相似之处:这两种语言都用于表达人类思想,这些思想可以被机器解释。关键是简单的:对于一个真正不了解技术和编程语言的人来说,你当然能够解释 HTML 文件的内容,它代表什么,它的语义以及它的目的。在 PHP 中,情况就不同了。实际上,在文件分割、类分割、面向对象编程OOP)以及所有这些其他概念之间,你肯定需要更多的时间来让你的非技术对话者理解所有这些的目的。然而,尽管 HTML 不允许 OOP,它也不允许条件分支、写入文件、管理发送到服务器的请求等等。因此,我们最终得到一种对人类来说更容易理解的语言,因为它非常接近我们的自然语言,但可能性却少得多。

尽管存在这些差异,我们必须记住以下事情——这些语言的主要目标完全相同:被尽可能多的人和计算机理解。编写代码意味着要易于理解。这是要表达思想。就像在日常生活中当你表达你的思想时,你越简单、越直接,就越多人能够理解你。

被理解,而不是聪明

在面对技术挑战,尤其是在源代码面前,我们常常想要以一种精致、漂亮,甚至“性感”的方式做事,正如有些人所说。这是完全正常的——因为代码是我们作为开发者生活的重要组成部分,我们有时想展示我们能力的极限。虽然有时可以证明这是合理的,但想要完全展示这些才能通常是极其糟糕的想法。显然,我们的自尊心会受到打击——我们有时必须克制自己。你刚刚学习了新的做事方式,新的编码方式,以及你坚信的新原则。你花了一个周末学习这种新的代码和项目组织方式,你把它体验为一种启示,你确信这一点:你必须向你的同事和团队展示这个新发现;它将彻底改变项目,只会带来好事。此外,你将因这个新事物而获得赞誉,你将成为它的标杆。然而,这根本不是正确的做法。

不要误解。每天学习,无论是在自己的时间还是不在,都是一件非凡的事情。如果你有机会这样做,你会变得更好。顺便说一句:这绝对不是强制性的!没有任何事情应该强迫你在业余时间编码。继续为工作编码和编程是完全正常的。

与同行分享你的发现和经验是正常的——甚至更甚,这是健康的。通过分享知识来提升你爱的人是最好不过的事情,而解释某件事是自我学习最好的方式。错误在于想要立即、无处不在地应用它。每种方法都有其优点和缺点。绝对有必要意识到它带来的缺点。一般来说,最常见的是在当前项目中应用,以及来自项目其他参与者的变化抵抗。只需以著名的 SOLID 原则为例:尽管它们的有效性得到了证明,但新来者可能难以接触。

对变化的抵抗对所有人来说都是正常且自然的。我们的大脑喜欢规律性——它喜欢周期性,不喜欢意外。这一点在工作环境中表现得尤为明显,但在生活的各个方面:饮食、锻炼和睡眠中也是如此。对于代码和我们的工作习惯来说,情况完全相同。再次强调,一致性和规律性是关键。

如果你将你的新发现带入项目,你将不得不考虑现有人员的培训。如果现有的习惯是合适的并且已经满足需求,他们可能并不想改变习惯。新习惯也意味着培训所有不知道这些做事方式的人。这需要个人投入,在某些情况下甚至需要大量的投入。这变成了个人时间或工作时间中的学习小时数,因此这些小时数将是生产力降低的小时数,这是不可否认的。有时这是必要的,有时甚至是一个有希望的想法。但那时,你必须能够向每个人证明这一点,包括项目中的非技术方,这显然是一个关键部分,尤其是如果你时间紧迫。

此外,一些编程实践可以产生奇迹,被证明是有效的,并使生活变得更加容易。然而,它们有一个巨大的缺点:项目的入职时间。以“无 if 编程”的实践为例。这种编程方法建议绝对不要在代码中使用if和条件分支。这要求大量和纯粹地使用面向对象编程OOP)。从纸面上看,这看起来很好,这种技术的智力满足感必须非常特别。一旦掌握,其效率非常明显。一切都会变得更智能,代码也会变得明显清晰。简而言之,一切都在为你的下一个项目从开始就采用无 if 编程做好准备。

然而,当有人来到你的项目来帮助你并试图理解你所做的一切(甚至与你一起维护和演进项目)时,观察结果将会是压倒性的:如果这个人不了解这种编程技术(这是一种远非普遍性的技术),引入项目的整个过程将会很痛苦。除了需要培训这个人在项目功能限制方面的知识外,他们还需要接受一种新的编程技术的培训,他们可能并不熟悉。这意味着理解项目、理解风险、改变习惯、改变做事和工作方式,以及重塑思维方式。我们很快就能理解这种操作的代价。它可以被证明是合理的,但你必须非常确信自己,并且事先了解所有风险。

我们在这里讨论的是无 if 编程,但同样的情况也适用于其他不是一般规则的做事方式。测试驱动开发TDD)就是其中之一!将 TDD 整合到项目中可能会很痛苦且复杂,正如我们之前所看到的。然而,TDD 主要影响做事的顺序,而不是学习一种完整的编码方式。这取决于你根据自己的环境和限制,判断这些风险是否值得承担。

在任何情况下,如果你选择了一种可以描述为异国情调的新编程技术,你可能会拥有典范性的代码,既干净又高效,且易于维护。问题是没有人能够理解它。记住前一个章节中提到的内容:代码是用来表达和传达思想的。它是用来被机器和,尤其是人类理解的。牺牲第二点将会是一件遗憾的事情,这也是为什么高级编程语言被发明出来的原因。

关于可维护性的说明

而这正是事情变得复杂的地方。你的代码已经准备好了——它运行正常。你已经遵循了一种新的编程方法,项目的初步开发已经顺利进行了几个月。很明显:可能没有为你的项目打下基础;你很幸运地从一张白纸开始。然而,可维护性的问题很快就会出现。无论你选择哪种编程技术,无论谁在从事这项工作,错误总是会出现。你可能需要新的人来修复所有这些问题(并且因此教会他们你的工作方法)。你确定你已经足够熟练地掌握了你的新方法,以确保应用程序在几年内的后续维护吗?这是完全可能的,但你必须意识到这一点,并且知道如果你在应用程序的维护上遇到困难时应该怎么做。

本章的目的并不是要阻止创新和测试新的工作方法。更多的是要充分意识到选择新工作方法的风险,尤其是在长期来看。我们稍后会看到,我们必须对一夜之间可能消失的最新趋势格外小心。

同样适用于那些乍一看似乎很优雅但实际上却难以维护的编程语法。在这些做法中,我们可以找到一些被突出显示的,以下是一些非详尽的例子。

使用二进制运算符和八进制、十六进制和二进制表示法

通常,在整数上使用二进制运算符进行操作(左移和右移、逻辑 AND、逻辑 OR、位反转等)比其他任何东西都更无意义。它们的罕见性使它们成为执行某些操作的语法看起来很优雅。然而,事实并非如此,掌握二进制操作不应是理解 PHP 代码的先决条件。

在某些情况下,使用八进制、十六进制和二进制表示法是有道理的。例如,当你想玩弄文件权限时,可以使用八进制表示法。如果你想在你方法中使用标志,以及使用二进制表示法,可以使用十六进制。但总的来说,除了使你的代码难以阅读之外,并没有太多其他的作用。

赋值变量和使用 goto 语句

变量可以在测试其值的同时被赋值。以下是一个例子:

if null === ($var = method()))

最多只能节省一行代码。但自从我们关心应用程序源文件的大小以来已经很久了,这些文件最终由 PHP 解释器在运行时优化。在测试之前分配变量没有成本,而且你的代码会立即变得更加易读。

goto 指令允许你跳过代码的整个部分,甚至“向上”跳转。尽管在某些极其特定的情况下可能有用,但它绝对不应该在大多数情况下使用。多年来,在大多数编程语言中,goto 语句的使用一直受到谴责。确实,它们给代码流程的理解带来了极大的复杂性。当goto的使用过多时,有一个专门的名称:意大利面代码。

过度使用注释

有时,我们会看到注释的滥用,有时甚至有几百行来解释所有变量的类型、它们的用途、函数引发的所有异常、详细的返回值等等。我们甚至可以找到注释比代码还多的源文件。大多数情况下,通过清晰命名你的方法和变量,这些细节都可以省略。此外,PHP 最新版本中变量的类型、参数和函数返回值的类型也解决了这个问题。然而,使用注释生成文档是完全合理的,并且在可能的情况下应该使用。没有人会抱怨“文档太多”。自由地写几十行关于类、接口、方法等等是什么,以及更普遍的关于它的派生、技术性和/或功能性选择等等。当我们谈论“注释的滥用”时,我们指的是解释代码中发生什么的注释。

使用三元比较

这里是一个三元比较的例子:

$var === null ? 'is null' : 'is not null' 

虽然三元比较可以使代码更加简洁,并将条件包含在例如函数参数传递中,但它们不应该被滥用,尤其是嵌套的三元比较,一旦嵌套到第一层,就会变得难以阅读,令人头疼。你可以在以下例子中看到这一点的证明:

$var === null ? 'is null' : is_int($var) ? 'is int' : 'is not null'

这不是一条非常清晰或易于阅读的代码行,当条件不是基本且简单的时候,例如在第一个例子中,三元条件就会变得难以阅读。

使用缩写

这里可能是最常见且应被劝阻的做法:到处使用缩写。再次强调,在上个世纪的末尾,我们可能有一些使用缩写的理由:空间和存储比今天要有限得多,而且代码编辑器没有我们今天所拥有的所有自动完成功能那么智能。因此,将变量命名为$userPasswordRequest而不是$usr会让每个人的生活变得更轻松:既包括你,也包括将来回到你的代码的开发者,他们不需要问你这些缩写代表什么。再次强调,有了我们今天所拥有的自动完成工具,这样命名我们的变量是没有意义的。

将微优化引入你的代码

微优化是对代码进行的非常小的更改,可能会损害其可读性,但这是为了优化代码,从而使其执行速度更快。问题是这通常并不有用,首先是因为你不需要优化指令到纳秒级别(因为现在的处理器很强大),而且因为很多优化都是由语言解释器和编译器完成的。所以,你牺牲了代码的一部分可读性,为了那些不实用且会被自动执行的事情。此外,这通常会引起无谓的争论,其中没有人比任何人更有道理。在这些针对 PHP 的特定微优化争论中,我们发现特别是关于增量运算符(++)和减量运算符(--)在变量之前或之后的位置、在标准 PHP 库SPL)的方法前使用反斜杠,或者匿名函数的静态声明或不声明的问题。再次强调,答案是:考虑与代码其他部分的连贯性,并保持实用主义。你当然不需要通过优化节省的 10 纳秒,这可能会在某些情况下节省,但可能会在开发团队中引发一场热烈的争论。

重新编码 SPL 的方法

我们对一个非常广泛的标准库语言有很多用途。标准库是 PHP 每个安装提供的类和方法集合。不幸的是,我们很快就会意识到它相当不为人知,并且提供了比你想象的更多可能性。因此,我们经常发现自己项目中使用了 SPL 方法,因为相关的开发者不知道标准方法的存在。这非常不幸,在某些情况下,这确实是一个真正的问题,原因如下:

  • SPL 方法无处不在。无需担心它们是否在某个安装或设置中可用。

  • 这些方法是由 PHP 解释器的开发者测试的,而你的方法则不一定如此。

  • 如果这些方法中的任何一个可以被优化或加强,这将得益于语言的数千名贡献者和研究人员。

  • 这些方法被认为是尽可能高效的,由那些工作就是创建尽可能高效算法的人所构思和概念化。

  • SPL 方法可以直接用 C 语言编写。这意味着无论你在 PHP 中做什么,它们的性能都将无与伦比。不利用这一显著的好处将是遗憾的,尤其是在执行时间可能至关重要的应用中频繁使用的方法。此外,由于它们是用 C 编写的,C 编译器可以对这些方法提供非常底层的优化,直接使用汇编代码。你无法通过在 PHP 中编写方法来实现这一点。

随意查看官方 PHP 文档;一些方法,如 natsort(),可能会让你感到惊讶,并为你节省数小时的开发时间!

列举可以继续下去,但重点是,虽然你可能喜欢使用这些工具,但你将是唯一会感到满足的人。一个初级开发者可能会在这些实践面前完全迷失,而一个资深开发者则不会理解在更清晰、更简单的方法可用时使用这些实践的价值。你的代码必须尽可能被更多的人理解。通过为看似过于复杂的问题编写一些简单、琐碎且易于阅读的代码,来展示你的技能、知识和熟练程度。

摘要

你理解“建立在我们已知的基础上”是什么意思吗?我们稍作回顾后意识到,这又是一种常识和利他主义,考虑到未来会阅读我们代码的开发者。在这里,没有关于侦察兵、SOLID、简单至上,傻瓜也明白KISS)或其他原则的问题。这是关于重新思考我们编写代码的方法。

我们必须记住,基础的东西可以(并且应该)被质疑,不应被视为一成不变。自信是一种美好的东西,如果你能够结合这种持续改进的习惯,你就能走上成为优秀开发者的正确道路,能够自然地编写干净的代码,并带领你的合作者一起实践。

积极主动是一件了不起的事情;了解风险并在你的环境中评估它们是追求完美的关键。这样,你才能知道这是否真的值得,同时也能向项目管理者证明你的选择是合理的。再次强调,当我们谈到干净代码时,我们回到能够证明我们所有选择和行动的能力。干净的代码不仅仅是避免使用二进制运算符或十六进制表示法。这意味着要考虑我们的项目环境、约束和周围环境。干净的代码不仅仅是关于代码。幸运的是,这正是我们将在下一章中看到的。

第四章:这不仅仅是关于代码

当你想到将 PHP: Hypertext PreprocessorPHP)描述为一种编程语言是否有点过于简化?我们必须面对事实:PHP 不仅仅是一种简单的编程语言。它是一个完整的生态系统,拥有庞大的社区、数千名贡献者,并且定期提出和发布新功能。但不仅如此:数百万个库和 应用程序编程接口API)都是由于 PHP 而被编写和发布的。甚至许多命令行工具也是完全依靠 PHP 语言开发的。PHP 是一个独立的世界。让我们先看看 PHP 不仅仅是一种编写网站的语言的理由。

这些是我们将在本章中涉及的主题:

  • PHP 作为生态系统

  • 选择合适的库

  • 关于语义版本控制的一个词

  • 稳定性 versus 趋势

PHP 作为生态系统

这可以从我们共同列出的一些事情中看出:

  • PHP 仍然是 2020 年代初最常用的用于网络应用开发的服务器端语言。当你知道网络应用在我们日常使用中的主导(甚至可以说是压倒性)地位时,这真是一个真正的荣誉!

  • 这种语言继续非常强烈地发展,尤其是在近年来。在 PHP 6(从未发布)的开发过程中,它经历了一段低谷,然后从版本 7 开始经历了其受欢迎程度的真正爆炸。PHP 5 和 7 的基准测试在最初发布时简直疯狂。发展势头强劲,新功能被非常频繁地提出。

  • PHP 拥有一个名为 Composer 的杰出依赖管理器。简单、开源且效率惊人,它通常被用户认为是市场上最好的依赖管理器,包括所有编程语言在内。尽管这可能是一种主观观点,但我们无法否认它的可靠性。

  • 谈到依赖关系,你只需访问 Packagist 网站(Composer 获取依赖的仓库)就能意识到 PHP 拥有异常出色的社区,可以提供如此多的库,每个都比另一个更令人难以置信,其中绝大多数都是免费且无使用限制的。如果你有需求,肯定有一个外部库可以解决你的问题。

  • PHP 扩展是扩展语言的一个真正的金矿。与您使用 Composer 安装的库不同,这些扩展是用 C 语言编写的,并直接插入到 PHP 解释器的源代码中。这使我们能够以令人印象深刻的表现力扩展语言。这也意味着 PHP 的默认安装可以非常精简,几乎不需要任何东西就能工作。然后我们可以根据我们的需求单独安装扩展。这在我们的应用程序必须在资源有限的服务器上运行时尤其有用。

  • 世界各地的多个会议都表现出对 PHP 的强烈承诺。同样令人难以置信且享有盛誉的工具,如 Symfony、Drupal 和 Laravel 框架,显示出将语言推向极限的真正愿望。这些框架本身组织国际会议,并被跨国公司(如 Airbnb、Spotify、TheFork 等)使用。据记录,Symfony 在 2022 年仍然是所有开源项目中为框架和文档贡献者数量最多的开源项目之一。

  • PHP 核心的开发至今仍在全速进行,而且比以往任何时候都要活跃。新特性的提案和评论请求RFCs)(提出语言变更的第一步)正在以惊人的速度出现,并且被迅速实施。许多贡献者参与了其中,并且也观察到主要贡献者的更新换代。一些较老且重要的贡献者,如知名的 Nikita Popov,正在离开这个项目,而新的贡献者正在加入。社区始终处于不断的活跃状态。

PHP 是一个有着良好声誉的语言。其稳健性和效率使其成为世界上一些最大网站的首选语言。在过去或目前,替代方案已经实施或正在实施的地方,例如 Python(在撰写本文时,大约有 1.2% 的所有网站使用 Python 用于一些 Google 网站),PHP 仍然是主流。显然,许多有吸引力的技术正在兴起,并从 PHP 中夺取市场份额,例如 Node.js 或 C# 和 .NET 框架。PHP 仍然有着光明的未来。知道如何用 PHP 编写网站可以确保您将能够阅读世界上绝大多数现有网站的源代码。

由于所有这些原因,PHP 是一个生态系统。进一步思考……如果 PHP 不仅仅是一种编程语言,如果 PHP 不仅仅是一种代码,那么我们为什么要把清洁代码仅限于代码呢?

清洁代码也可能包含,通过扩展,安装到您的项目中的外部依赖项和库的正确选择。让我们看看为什么您应该明智地选择依赖项,以及如何选择它们以限制风险。

选择合适的库

选择合适的第三方库进行安装可能是一项真正的挑战。这是我们每个人都面临过或将来会面临的挑战。原因很简单:没有必要重新发明轮子。我们想要安装外部库的原因通常是一样的。我们有一个具体的问题,我们希望尽可能干净利落地解决它。这里出现了两种情况:

  • 我们知道如何解决这个问题,但我们不想在已经存在解决我们问题的工具时重写一切。

  • 我们不知道如何解决这个问题,因为我们缺乏理论或实践知识。

那么调用一个外部库来为我们提供非常具体的解决方案是非常有趣的。这里列出的优势是多方面的:

  • 开发外部库的人可能已经思考了几天或几周,关于提供解决方案的最佳方式。这甚至可能是他们的日常工作。无论他们在这上面花费了多少时间,通常都比我们允许的冷静和清晰地思考解决方案的时间要多。

  • 如果依赖项仍在积极开发中,那么维护工作将由除你之外的其他人负责。这意味着你将定期收到错误修复和新功能,这些都是这些志愿者提供的最宝贵的资产:他们的时间。

  • 如果你正在使用的是开源的外部库,那么你将获得额外的益处,如下所述:

    • 源代码对每个人都是可见的。这意味着任何人,比如其他开发者甚至安全研究人员,都可以分析源代码以加强它并修复安全漏洞(或者至少通知作者)。请别误会:开源对安全并没有坏处。事实上,恰恰相反。通过混淆(理解为隐藏诸如源代码之类的信息以确保“安全”)来实现安全是最糟糕的事情。安全必须通过其他手段来实现。没有缺乏证据:世界上使用最广泛的加密和加密算法是众所周知的,它们的运作方式在互联网上有成千上万的解释。这并不损害它们的安全性。

    • 如果开源项目被遗弃,那么可能会出现一些后续行动(称为“分支”)。简单来说,分支就是那些复制了项目源代码并在自己的独立环境中进行开发的人,与原始项目的开发无关。从理论上讲,这可以确保项目的无限寿命。

    • 如果项目的主要维护者没有时间再照顾项目,但开发者希望这样做,由于源代码对所有开放,他们可以这样做。

  • 如果你好奇,可以深入研究源代码,了解库是如何解决问题的!

我们可以清楚地看到,选择开源依赖项是不可避免的。如果您想要确保不会突然得到一个不可用的工具,开源正是为您准备的,因为您可以存储源代码的副本,而无需担心。这是选择外部库的第一个绝佳方式。

需要考虑的第二个因素是项目的更新频率。显然,如果一个开源项目已经几个月甚至几年没有更新,那么要小心:它可能已经被遗弃。在这种情况下,这意味着该项目可能不会支持 PHP 的下一个版本,或者不再修复错误和安全漏洞。正如这里所述,有两种简单的方法可以知道一个项目是否仍在维护中:

  • 首先,您可以检查库的最后一个版本日期。再次提醒,因为一些项目有(非常)缓慢的发布过程,所以建议将此技术与第二种技术结合起来。

  • 这是第二种技术。看看源代码的最后修改时间。这可以非常容易地完成,尤其是如果源代码托管在 GitHub 等网站上。通过浏览文件,您可以查看文件夹或文件的最后修改时间。这可以是一个很好的项目开发动态的指示。

需要考虑的第三个因素是库的文档。您可能想确保项目有最小且足够的文档来设置基础知识。如果没有提供文档,那么可以肯定的是,使用外部库将会是一个系统的痛苦。实际上,所有的代码维护都将变成一场记住项目如何工作的战斗,没有文档来帮助您或分享知识。此外,这也非常与您想要使用的技术的社区密切相关。如果您想要集成的项目使用的人很少,社区相当不活跃,甚至不存在,那么没有人能够以最佳方式帮助您。这可以是一个决定是否在代码中使用此或那个依赖项的有效方法。

第四个因素是库本身依赖的依赖项数量。一般来说,我们更喜欢依赖项很少的库。依赖项越少,需要更新的包就越少,第三方就越少,因此在这些第三方中出现问题的情况就越少。

最后,许多项目在其主页上有持续集成CI)徽章。这些徽章让您可以一眼看出测试覆盖率(提醒一下:被测试覆盖的代码比例)、测试数量、最新版本等。显然,选择尽可能多测试和高测试覆盖率的项目来限制更新期间的问题会更好。

关于语义版本控制的一番话

说到更新,让我们谈谈版本控制和——特别是——语义版本控制。如果你想要使用的外部库遵循语义版本控制的规则,这可能会对你的开发和更新产生极其积极和令人放心的影响。让我们看看这究竟意味着什么。

什么是语义版本控制?

版本控制简单来说就是给源代码的版本加上一个数字。我们都熟悉像1.01.5.02.0.0这样的版本。语义版本控制为这些数字添加了语义——也就是说,精确的含义。以版本 2.3.15 为例。以下是语义版本控制如何分解这个版本号:

  • “2”表示主版本。主版本可以引入新功能、错误修复,并且——最重要的是——破坏向后兼容性的变更。这一点最为重要。确实,从一个主版本到另一个主版本,方法签名甚至完整的类名都可能发生变化,有些也可能消失。因此,当你迁移到高于当前版本的主版本时,你必须非常小心,并且必须测试一切是否仍然正常工作。通常,发布变更日志提供了你需要进行的更改,以便与新的主版本兼容。

  • “3”表示次版本。与主版本一样,次版本也可以引入新功能和错误修复。主要区别是次版本不能进行破坏向后兼容性的变更。这意味着你可以升级依赖到下一个次版本,以利用所有新功能和错误修复,而无需担心你的代码在升级时会中断。然而,在次版本更新时运行所有测试永远不会多余。你永远不知道。通常,次版本会触发代码弃用消息。这些消息告诉你哪些方法你不再应该使用,因为它们肯定会在下一个主版本中删除。在开发过程中考虑到这些弃用消息,当更新依赖到下一个主版本时,你可以节省很多工作。

  • “15”表示补丁号。补丁只包含错误修复和安全修复。它不包含新功能。你应该考虑始终在你的项目中安装依赖项的新补丁。

我们可以看到语义版本控制的优点:宁静、逻辑和一致性。显然还有其他变体,如 Alpha、Beta、发布候选和 Golden Master。但这些比较少见。

如何处理语义版本控制

语义版本控制还包括一种特定的标记法,允许你的依赖管理器知道如何安装新版本以及何时更新你的依赖。以 Composer 用于安装依赖项的文件片段为例(这同样适用于许多其他依赖管理器):

{
    "require": {
        "php": ">=7.3",
        "symfony/dotenv": "3.4.*",
        "symfony/event-dispatcher-contracts": "~1.1",
        "symfony/http-client": "⁴.2.2"
    }
}

这个片段描述了四个依赖项:PHP 的最小版本以及三个外部库。这些库具体是什么并不重要。我们在这里可以指出四种定义我们想要接受的依赖项版本的不同方法。让我们看看它们是什么。

观察到的版本的第一种写法是使用>=运算符。这个运算符是最容易理解的之一:我们希望接受所有大于或等于指定版本的版本。在这里,我们的应用程序接受所有高于版本 7.3 的 PHP 版本,以及版本 7.3 本身。当然,依赖关系管理器接受其他这样的运算符:=, <, >, 和 <=。您也可以组合这些运算符以获得非常精确的版本约束——例如,通过编写“>=1.2.0 <2.0.0”

第二个运算符相当知名,因为它在许多其他上下文中都被使用。它是通配符,表示为*。这个符号简单地表示你可以用任何你想要的东西来替换它。在上面的例子中,我们接受依赖项 3.4 版本的补丁版本。这允许它仅从错误修复中受益,而不更新次要版本。这个通配符可以放在版本号中的任何位置。例如,表示3.*的记法将受益于主要版本 3 的所有次要版本。

以下记法是使用波浪线运算符,表示为~。这个运算符意味着你将只从给定版本的补丁中受益。在例子中,我们将从依赖项版本 1.1 的所有补丁版本中受益(即 1.1.0、1.1.1、1.1.2 等等)。这与通配符运算符非常相似,除了通配符运算符不能放在版本号的任何位置,并且只关注补丁。还值得注意的是,Composer 对波浪线有稍微不同的解释:它还允许次要版本,而不仅仅是补丁。如果你使用 Composer,并且只想从补丁版本中受益而不包括次要版本,你必须使用通配符运算符。

最后,我们将看到的最后一个运算符是撇号运算符,表示为^。在上面的例子中,撇号运算符允许所有补丁版本以及主要版本 4 的次要版本(即 4.2.2、4.2.3、4.4.0 等等)。如果你想定义一个依赖项的最小版本,同时接受新的补丁和次要版本,但在更新外部库时自动拒绝主要版本(这可能会带来破坏性变化),这是一个特别好的选择。这就是为什么它是最受欢迎的运算符之一。

可能性是无限的,一旦你掌握了这种表示法,你就可以自信地更新你项目的依赖项。至于良好的实践,接受依赖项的所有新补丁和小版本总是一个明智的想法。你永远不应该在没有条件或更新可能性的情况下将依赖项锁定到非常具体的版本。实际上,如果一个外部库严格遵守语义版本控制,你将不会与现有代码发生冲突。破坏性变化仅限于主要版本。因此,在更新依赖项时,你不应该自动接受主要版本:你可能会不得不调整你的代码以使其正常工作。

稳定性 versus 趋势

让我们用关于最新版本的一些话来结束这一章,但也要谈谈流行的外部技术和库。

首先,让我们谈谈外部库的最新版本。当然,我们可能会倾向于使用最新的版本,那些几个小时前刚刚发布的版本。值得记住的是,可能会出现错误,如果这种情况发生,不久的将来可能会发布一个新的补丁版本。或者也可能不会。在这种情况下,错误可能会持续一段时间。因此,编写测试尤为重要。想象一下这种舒适感:你更新了所有依赖项,你运行了你的测试套件,如果所有指示灯都是绿色的(并且你的应用程序得到了适当的测试),你就可以相当确信一切正常。

话虽如此,如果你更新了外部库,任何测试结果变成红色,你将不得不调查原因。在任何情况下,你不应该认为如果你的依赖项得到了很好的固定和约束,或者你只接受补丁和/或小版本,你就不会遇到任何问题。补丁也可能带来错误——你永远不知道。

至于 Alpha 版本,让我们明确一点:这些版本不是为生产应用程序准备的。不同的库对此都很清楚:代码可能会在一天之内发生变化,带来未警告的破坏性变化。简而言之,你必须非常小心。话虽如此,如果你想亲自评估这些版本,库的开发者将非常乐意收到你的反馈。Beta 版本应该更加稳定,不会带来更多的破坏性变化。使用它们时,你仍然必须非常小心。

作为一条一般规则,只有在生产环境中使用最终的、稳定的版本。如果你想为稳定发布的当天做好生产部署的准备,请将 Alpha 和 Beta 版本保留用于开发和测试环境。新特性总是令人兴奋的事情,但它们永远不值得牺牲你应用程序的稳定性。你的用户并不关心你使用的第三方库的新特性:只有稳定性才是最重要的——它就是能正常工作的事实。

现在,让我们来谈谈流行的技术(一个外部的 PHP 库、一个新工具,甚至是一种新的编程语言)。你听到周围的人都谈论某种特定的技术。这种技术像野火一样蔓延,你在互联网的每个角落都能听到关于它的消息,大公司都开始涉足其中,技术会议也都在讨论它。你必须对此类事物保持警惕。即使这些技术的承诺可能令人兴奋且具有革命性,首先考虑的应该是:你的用户。

这种技术是否真的会对你的最终用户产生影响?它真的值得培训并花费数周甚至数月来弄清楚它是如何工作的吗?你必须确信它将对你的项目产生真正的积极影响。你还必须记住,创新技术将拥有一个较小的社区。影响是即时的,正如以下所述:

  • 你将不得不培训所有加入你项目的人

  • 文档可能并不完整,这可能会使理解变得困难

  • 你可能会发现自己独自坐在屏幕前,找不到解决问题的方法:你是这项技术的第一批使用者之一,因此也是第一批面临其遇到的障碍的人

最后,你必须确保项目是健壮的,这样你就不会在没有任何警告的情况下放弃最新的技术。这种情况比你想的更常见,而且(如果不是全部)你的工作可能都白费了。所以,要警惕最新的未经证实的科技,并确保项目的健壮性和严肃性。等待一段时间后再考虑。再次强调,你的用户肯定可以在这项技术成熟之前不使用它(他们甚至可能不知道这项技术)。

摘要

将 PHP 限制为编程语言是过于简化的。我们刚刚看到——它是一个真实且充满活力、丰富的生态系统,并且与埋葬其最喜欢的语言相去甚远。围绕 PHP 的发展是无数的,而且近年来,这种语言本身也在以最美好的方式进化。功能的贡献给了它真正的第二次生命,使它至今仍能在服务器端用于 Web 应用的最常用编程语言中占据首位。

所有这一切都离不开外部库数量的激增。你遇到问题;总有一个解决方案。我们很幸运,大多数外部库都是开源的。数千名开发者自愿且免费地提供了他们数小时、数周甚至数年的工作成果。

从这些库中选择一个可能会很困难且具有挑战性。在确保做出正确选择之前,进行实际的研究工作是非常重要的,甚至是强制性的。我们并非对障碍和事件免疫,但这一章节已经为你提供了工具和现成的解决方案来降低风险。最重要的是,不要盲目追求最时尚的技术。如果你想吸引用户并让他们比其他应用更频繁地使用你的应用程序,关键词是“健壮性”和“稳定性”!

我们已经讨论了很多关于别人的工作,但我们不应该忘记自己的成就。当你需要理解你最喜欢的外部库的内部工作原理时,你是如何设法在代码中找到自己的路径,就像你能够轻松地在源代码中找到路径一样?我们回到我们在第一章中提到的话:通过拥有相同的习惯,我们更容易理解彼此。这显然适用于项目的组织、文件的命名、文件夹的结构等等。这正是我们将在下一章中看到的内容。

第五章:优化您的时间和分离责任

在经历了所有这些理论之后,是时候进行一点实践了!我们已经一起看到了很多:关于编写清洁代码的高级原则、如何为您的应用程序选择正确的外部库,以及如何在不过度风险的情况下利用这些库的最新补丁。但我们不应忘记,“清洁代码”中的“代码”一词(显然)。因此,在本章中,我们将更专注于您应用程序的源代码,并探讨以下要点:

  • 文件和文件夹的命名规范和组织

  • 为什么将责任分离以尊重 SOLID 原则中的“S”很重要?这对您有什么好处?

  • 我们将发现一种优雅的方式来通过事件系统管理责任分离

  • 我们将以一些多态性结束——即抽象类和接口:为什么、如何以及何时使用它们?

命名和组织规范

我们必须首先声明,本章中给出的命名规范和组织思想并非绝对真理。正如我们之前所看到的,最重要的是尊重您项目中的现有规范,并与您的团队保持一致。如果觉得有必要,可以对这些规则进行适应以满足您的需求。再次强调,重要的是要使用常识和逻辑,并尽可能清晰。

让我们先谈谈源文件的命名。显然,命名规范因技术而异(例如,根据您是否使用某个框架或另一个框架,良好的实践可能会改变)。尽管如此,我们仍可以注意一些几乎在所有地方都可以找到的规范。

类文件和接口文件

Foo 类应该在 Foo.php 文件中定义。这种命名技术不仅仅是一种规范,它还具有真正的技术意义。确实,PHP 的自动加载机制将假设您的文件定义了一个与文件名相同的类。自动加载允许 PHP 自动发现您应用程序中定义的类,特别是由于命名空间(我们将在稍后回到这一点,因为它们与您项目中的文件组织直接相关)。如果您为文件和它们定义的类使用不同的名称,自动加载可能会失败并抛出错误。在各个语言和全球开发者社区中,命名类的最常见风格是 MySuperServiceClass,如果我们使用 PascalCase。还有其他命名风格——我们将看到其中一些,以及它们适用的场景。

可执行文件

PHP 文件作为可执行命令行脚本,有很强的趋势使用小写命名。apt-getdocker-composegit cherry-pick是完美的使者。没有任何东西阻止你以其他方式命名你的可执行文件,一切都会正常工作。然而,通过这种方式命名用 PHP 编写的命令行应用程序,你为大多数命令提供了一个统一的命令行界面CLI)体验。这正是我们在 PHP 中开发命令行应用程序时想要的:让它与传统系统命令融为一体。

网络资源和资产

还有一种情况使用 kebab-case 风格,这主要用于公共网络资源,尤其是 JavaScript,例如/contact-us,与/contactus/ContactUs这样的命名方式相比。这在你必须处理前端文件时尤其重要。

命名类、接口和方法

正如我们在前面的章节中看到的,缩写应该从你的代码中禁止。例如,Abstract(例如,AbstractMailer)以及你的接口应该以Interface后缀结尾(例如,MailerInterface)。这使得名称略微变长,但在使用上没有混淆。它们的目的清晰、定义明确且一目了然。如果为了理解它们的必要性,不要害怕给你的类起长名字。AbstractWebDeveloperConsoleStreamWrapperExtension对于一个类名来说可能非常长,但在项目上下文中,它立即清楚其用途,无需过多提问。再次强调,利用你 IDE 的自动完成功能,你只需输入前几个字母,就能在几秒钟内使用它。同样适用于你的属性和方法名称。要明确。

讨论属性和方法命名时,我们倾向于喜欢myGreatMethod这样的命名。一些语言使用 PascalCase 来命名方法,例如 C#。让我们说实话——这并没有真正的理由或论据,两种命名风格实际上是一样的。一次,这真的是一种约定——一种特定语言的习惯。

命名文件夹

文件夹的命名约定与文件类似。PascalCase 主要用于文件夹。也可以为公开的文件夹使用其他命名风格,例如网络资源。你不应该犹豫创建一个大的树状结构并赋予其意义。因此,简单地命名为 ManagerServiceWrapper 的文件夹应避免使用。这些术语过于通用,不能轻易理解它们所定义的内容。我们更喜欢更明确的变体,如 Mail 及其子文件夹 ProviderLogger 等。这些子文件夹可以具有更通用的名称,因为它们包含在一个命名上下文的文件夹中:一个领域。一种巧妙的方法是使用文件夹将源代码分离到不同的领域。你的应用程序将更好地切片,架构更清晰。关于同一主题的内容将位于同一位置。随着这种习惯变得自然,你会更加高效。许多开源项目和库都使用这种方式来分割它们的源代码。因此,你将能够浏览自己的代码,也能浏览他人的代码。有时找到一个合适的元素名称,无论是类、文件、变量还是其他任何东西,都可能是极其复杂和耗时的工作。然而,这一步尤其重要,不应被忽视。注意不要对自己说:“我给出一个不一定非常清晰的名称,但我稍后会更改它。” 很可能你会忘记它,而且在你甚至没有意识到的情况下,技术债务就会产生。

分离职责

让我们看看代码中职责分离的组成部分,使其更干净、易于理解、可维护和可扩展。这是 SOLID 原则的第一点。在第二章中,我们这样定义了单一职责原则:“这意味着你的代码中的类必须只响应一个任务。

作为提醒,SOLID 是一组已知的清洁代码规则,当一起应用时,会使你的代码更加清晰和准确。与其试图逐字遵循每个 SOLID 原则所描述的五个原则,不如在编码时全局考虑这一点。

尊重这一点的第一步实际上是……命名,就像我们刚才看到的!确实,通过恰当地、清晰地、最重要的是精确地命名一个类,你已经在确保它不会变成一个混乱的地方,你可以把所有能想到的东西都放进去。正是出于这个原因,我们有必要不要用像ManagerService这样过于通用的术语来命名方法。这会导致一个大问题:如果我们最终得到一个名为EmailManager的类,我们显然都会首先想到添加所有处理电子邮件管理的方法。这就是混乱开始的原因。这就是为什么我们更愿意创建像EmailFactoryAbstractEmailSender等类,以绝对避免有包含数百个不同方法的类。

我们开始更好地理解单一职责原则。让我们再重复一遍:目标不是创建只包含一个方法的类。这样做没有意义。你必须智能地将其拆分。拆分类的没有通用规则。正确拆分类的方法会随着经验自然出现,并且会自然而然地出现。如果你有帮助,你可以把文件夹看作是领域,而文件看作是子领域。使用接下来的例子,我们有一个名为Email的领域(或文件夹),以及专门针对特定任务的子领域:创建电子邮件,定义一个基类以使用特定的电子邮件提供商发送电子邮件,等等。我们甚至可以在职责分离上更进一步。确实,存在一些工具可以帮助我们轻松地解决这个问题。我们将发现(或重新发现!)事件分发。

事件分发

事件分发通常是通过观察者(Observer)和中介者(Mediator)设计模式实现的,就像在 Symfony 的EventDispatcher组件中那样。这些信息只是为了一般了解。确实,设计模式一开始可能会显得晦涩,甚至令人恐惧。此外,解释它们需要一本自己的书。所以,我们将不谈论设计模式,而将这一切通俗化。此外,我们不会实现一个事件分发器:这是关于理解它如何帮助我们的问题。

非常简单地说,事件分发的原则是在特定实体状态发生变化时,通知所有对此感兴趣的各方。各方将通过以下方式通知中央调解者:“我对知道这个特定事件何时发生感兴趣;当它发生时通知我,因为如果发生,我有一些事情要做。”调解者将保留这些信息。当所述事件发生时,调解者将遍历对此感兴趣的各方列表,并说:“事件刚刚发生;做你需要做的事情。”更进一步,如果必要,感兴趣的各方甚至可以声明一个优先级,以便在其他所有人之前执行。请注意——我们谈论的是同步事件,也就是说,对事件感兴趣的各方将依次执行,跟随他人,而不是并行执行。异步事件管理是另一回事。

事件分发就是这样简单。但是,它如何帮助我们强化单一责任原则呢?让我们看看一个具体的例子:用户从你的应用程序中删除他们的账户。然后你需要执行两个任务,如下所示:

  • 从数据库中删除账户

  • 给用户发送最后一封电子邮件,说一声悲伤的再见

因此,我们自然会创建一个名为UserRemover的服务,它将依次执行这两个任务。它工作得很好。UserRemover是一个明确的名称,定义了一个非常精确的任务。到目前为止没有问题。然后,有一天,你的应用程序变得流行起来。你想要给管理员发送一封电子邮件,通知他们用户已经离开。我们的UserRemover类最终删除数据并发送了两封电子邮件,内容非常具体,收件人也非常明确。

之后,你想要给用户删除账户的可能性,以尊重实际上不删除任何内容、发送电子邮件以及可能执行许多其他任务的UserRemover服务。我们遇到了一个真正的问题:这个类没有做它应该做的事情,并且它现在可能已经有一千多行代码,有几十个方法。

如果你从一开始就使用事件分发,情况将会大不相同。以下是一个解决方案的例子。当用户想要离开你的应用程序时,你将分发一个名为UserRemovalRequestEvent的事件。然后,随着你的应用程序的增长,你将创建对此事件感兴趣的各方:事件监听器。我们将为每个任务创建一个,如下所示:

  • 一个用于从数据库中删除数据的监听器

  • 一个用于发送给用户的告别邮件监听器

  • 一个用于发送给管理员的邮件监听器

关于匿名化?这很简单:我们也将为这个任务创建一个监听器,并将删除数据的监听器“断开连接”。因此,每个任务都有一个类。每个类都有其独特的责任(向管理员发送电子邮件、匿名化数据等等)。如果在将来你需要添加一个任务,你将创建一个新的类(或监听器)来执行特定的任务,而无需触及其他类。这样代码就变得干净且极具可扩展性。类名保持清晰且简洁。如果一个任务已经过时,你可以简单地将其从对相关事件的感兴趣方列表中移除。单一责任原则得到了尊重。

如果你想要使用现成的事件分发器,我们推荐使用symfony/event-dispatcher包。这正是框架用于其操作所使用的组件。它非常健壮和高效,并且已经经过了几年的验证。

多态性的揭秘——接口和抽象类

就责任分离而言,事件分发是一个已经相当高级的概念。如果你知道这个机制,理解它,并且有机会使用它,那么你可以认为你在干净代码的世界中的水平已经大大提高。所有这些显然都需要一些设置。要么你自己实现这个系统,要么使用外部库。在后一种情况下,显然有一个完整的学习阶段需要包括。无论如何,这显然不是提高你责任分离的唯一方法。有一种方法是 PHP 的本地方法,可以用来提高这种分离,有时没有得到足够的利用,有时被误解,并且经常被低估。我们在这里谈论的是多态性,或者通俗地说:抽象类和接口。

首先,为什么是“多态性”这个词?Poly来自希腊语,意思是“许多”,而morphism意味着“形式”或“形状”。抽象类和接口只是实现代码中多态性的一种方式,以及面向对象编程(OOP)。为了简化问题,让我们只考虑接口的情况。

接口

接口为后来实现它们的类定义了一个通用的形式/形状。它们确定了每个实现应该为其情况定义的方法。实现必须必然定义其接口(或接口)的所有方法。这通常是我们听到以下声明的原因:“接口是一种契约”。我们可以这样理解:如果你实现了一个接口,你承诺实现它定义的方法。你没有其他选择。

这就是多态的强大之处所在。在你的代码中,你可以告诉 PHP,一个方法的一个参数必然是实现了精确接口的对象的实例。你可以操作这个接口的不同方法并调用它们。你不必担心它在被使用时如何实现:我们对此不感兴趣。举个例子,一张图片胜过千言万语,让我们以我们的邮件系统为例。

你有MailerInterface,它只定义了一个方法:一个发送邮件的方法。我们之前可以将其命名为sendEmail。当用户在你的应用程序中被删除时,发送告别邮件的事件监听器被调用。在这种情况下,你感兴趣的是邮件简单地被发送,而不是发送的内部工作原理。顺便说一句,这些内部工作原理是否可能根据某些条件而不同?你不必找太远就能找到一个例子:你的主要电子邮件提供商可能不可用,但你绝对需要发送你的消息。然后你必须使用另一个电子邮件提供商,它有不同的应用程序编程接口API)、不同的选项等等。没有多态,事情可能会很快变得极其复杂。

解决方案是创建两个MailerInterface的实现,每个实现根据所使用的电子邮件提供商定义sendEmail方法。但结果是相同的:邮件被发送。当用户删除他们的账户时,你执行检查以确保你的主要电子邮件提供商是可用的并实例化其实现。如果它不可用,你实例化备份电子邮件提供商的实现。另一方面,在电子邮件发送事件监听器中,你只需不断调用在MailerInterface中定义的sendEmail方法,无需担心其他问题。代码干净、清晰;责任分离;你节省了时间。而且更重要的是,它已经变得对失败具有弹性。

如果你愿意,你可以用 10、15 或 20 个电子邮件提供商来做这件事。优势在于,如果某个提供商的 API 发生变化或你在你的实现中找到一个错误,你只需修改有问题的那个实现。其他所有实现都不会移动,就像调用接口时的那样。你大大降低了错误的风险,你的代码也更容易测试:你可以为每个实现编写特定的测试。这比那些试图评估所有可能情况的通用、无休止的测试要稳健得多!节省的时间是异常的,无价的。

抽象类

那么抽象类在这个体系中是如何定位的呢?我们可以将其视为接口及其实现之间的一个中间层。虽然显然抽象类不一定要实现接口,但通常在抽象类之上创建一个接口是一个明智的想法。确实,抽象类比接口更宽容:你可以部分定义声明的方法,声明属性,并决定类的方法和属性的可见性(接口只允许public可见性,不允许privateprotected)。使用接口,你有一个干净的合同,没有任何“以防万一”的信息,并且只有当你尊重 SOLID 原则中的“I”(接口的隔离)时,才包含最基本的信息。作为一个提醒,简单来说,这个原则表明接口不应该包含“以防万一”声明的方

抽象类允许我们为扩展它们的类定义共同的行为,这些类希望利用多态性的力量。特别是,这避免了代码冗余、错误来源和无休止的复制粘贴。确实,在我们的上一个例子中,MailerInterface的不同实现很可能有共同的行为,例如创建与电子邮件提供商 API 通信的 HTTP 客户端或创建一个在实现内部操作中使用的通用Message对象。

在这种情况下,我们会声明实现MailerInterfaceAbstractMailer并定义不同实现中的共同行为。然后,不同的实现会扩展AbstractMailer以享受你刚刚定义的共同行为。

请注意——这并不意味着在所有地方、所有时候和所有情况下都必须创建接口和抽象类。我们不应该忽视这对比单一类对代码复杂性的影响。此外,仅仅因为你还没有为某个情况创建接口,并不意味着它是不可变的和固定不变的。很多时候,我们发现自己在重构代码,创建接口和抽象类,并调整现有类以实现和扩展它们。正如我们所看到的,我们最初需要保持代码的简单性(尊重YAGNIKISS原则)。我们无法预测未来,业务约束在不断发展。

如果在创建类的时候,没有迹象表明需要不同的实现,这并不是一个需要担心的问题。这是一个以后会完成的任务。另一方面,如果在开发过程中,你发现自己正在从一边复制代码到另一边,并且感觉到强烈的冗余,那么考虑多态性将是一个极好的反应。

摘要

我们刚刚覆盖了这本书理论部分的最高级内容。我们现在拥有了知识,可以在保持代码可维护和可扩展的同时,为未来的开发者编写干净的代码。它也将通过强烈地开放扩展和封闭修改(如SOLID原则之一所述)为未来做好准备。

我们已经回顾了许多你可能在开发 PHP 应用程序时遇到的关于文件、类和方法命名的案例。此外,我们还看到文件夹必须有特定的名称,并且可以用来将你的应用程序划分为不同的域。

职责分离也是一个重要的话题。理解为什么这种分离在项目中是有用的,甚至是至关重要的,尤为重要。这是构建一个易于导航的、良好架构项目的真正关键。正如我们所看到的,事件分发是实现这一目标的绝佳方式。事件分发是某些关键 Web 项目(如 Symfony 框架)的基石之一。这个框架在很大程度上依赖于这种机制,使其成为一个以稳健性、效率和——尤其是——灵活性著称的工具。这也归功于其内部的泛型和不同接口的声明。你可以通过这种方式重新声明框架的几乎所有部分,并将其适应到你的最高级需求。

并非总是容易理解何时创建一个接口或抽象类。这需要实践和经验。很快,这就会显得很自然。如果有疑问,不妨和你的同行交流一下!

我们将在下一章以一个轻松的部分结束这本书的理论部分,这部分将讨论 PHP 的新特性。这些特性使我们更加严谨,成为更好的开发者,尤其是在过去几年里。

第六章:PHP 正在发展——弃用和革命

我们做得很好。PHP 社区做得很好;我们很幸运。的确,PHP 在过去的几年里一直在非常强烈地发展。但这种强烈的演变并不是一直存在的。这主要是因为 PHP 6 开发期间的问题,这也是为什么这个版本从未发布的原因。这也解释了为什么许多项目(现在仍然如此)卡在 PHP 5 上。

PHP 7 彻底清除了旧账,为这门语言带来了真正的复兴。此外,它就像一股清新的空气,将语言推向了新的境界。

PHP 从几乎是一门死去的语言发展成为一个正在迎头赶上并面向未来的语言。在本章的最后,我们将专注于以下内容,这是专门针对清洁代码理论的:

  • PHP 与其过去版本的不同之处

  • 这些变化将如何帮助你成为一个更加严谨和更好的开发者,而不仅仅是 PHP 开发者

  • PHP 最新版本中的主要新特性是什么

旧 PHP 与新 PHP

多年来,PHP 可能已经帮助你成为一个更加严谨的开发者。如果在 PHP 存在的最初几十年里,PHP 允许你按照自己的意愿编写代码,而不对你进行任何限制,并且只带来(非常)少的优势,那么从后视镜来看,这主要是提供了尽可能多的编写代码的方式,就像有尽可能多的开发者一样(这很少能产生卓越的结果),这使得它变得流行。正如我们现在所知道的那样,这可能是无穷无尽的和地狱般的调试 bug 的源头。幸运的是,在过去的几年里,语言的演变已经修复了许多这些 bug,这对我们的应用程序来说是一大好处。

严格类型

首先,让我们看看从 7.4 版本开始的新版 PHP 中最重要的一些特性之一——属性的严格类型。

曾经,你可以随意将任何数据传递给任何变量,并且可以随意将变量转换为任何类型,而没有真正和原生的方式来阻止这种行为——例如将数组变量转换为字符串,将字符串转换为整数等。这可能会非常令人困惑,并且可能是许多问题的源头。例如,如果你将一个字符串乘以一个整数,会发生什么?嗯,结果是完全出乎意料的。这不像 Python 那样管理得很好,在 Python 中,这类操作是允许的,但受到很好的控制。如果你的 PHP 代码依赖于弱类型的能力,那么你的代码架构可能存在问题,你必须绝对审查需要这种弱类型的部分。

PHP 现在允许在某些情况下严格类型化变量。从 PHP 8.1 开始,如果变量不是类属性或方法参数,则不能对其进行类型化。话虽如此,你应该对所有类属性和方法参数进行类型化——这样可以减少混淆,增加严谨性。你可能需要重新思考代码的一些部分,但你会得到更干净、更易于理解的代码的好处。由于意外的类型转换,运行时不会出现任何意外。如果你真的需要将任何类型的数据传递给方法,你仍然可以依赖 mixed 关键字,它告诉 PHP 这个变量可以是任何类型的数据,或者方法可以返回任何类型的数据。当然,如果你能避免这种情况,就避免使用它,并且只在非常精确的情况下使用它(例如接口方法定义,其中接口的实现可以返回几种类型的数据)。

错误报告

PHP 8 默认显示弃用和更严格的错误。在 PHP 的早期版本中,错误报告级别较低。随着这一变化,你将能够更容易地看到需要关注弃用的地方,例如。当你需要操作 PHP 版本升级时,你会感谢这一变化。如果你在弃用出现时立即着手修复它们,升级到另一个版本的 PHP 将会变得容易。此外,几乎总是有一个来自弃用的消息,告诉你如何精确地修复它。尽早处理这些错误,特别是弃用,绝对是一个明智的选择,并使你养成干净代码的思维模式。

属性

源代码中的注释是为了更好地理解复杂的代码部分而发明的。这意味着如果我们从源代码中移除所有注释,它应该也能完美运行,因为编译器和解释器不应该考虑注释。这就是源代码注释被发明的主要原因和第一个原因

然后,创建了注解。逻辑和机制被引入到注释部分。请别误会——注解非常实用。你可以在需要的地方和需要的时候获得有关元素的所有信息和元数据。但当你这么想的时候,它似乎是一种异常。而且记住我们在前面的章节中说过的话:如果你编写干净的代码,那么你几乎永远不需要写一行注释,或者最多只有几行(编写复杂的代码部分是无法避免的,即使是最好的干净代码编写者也不例外)。

属性自 PHP 8.0 版本以来一直是 PHP 语言的一部分。简单来说,属性与注解具有相同的作用:为不同的元素添加元数据,包括类、属性和方法。不同之处在于它们使用另一种语法,这不是用于注释的类型。除了更好的可读性之外,注释将回归其最初用途:提供信息。在实例中,你可以立即区分元数据(用属性描述)和注释,以更好地理解你正在工作的代码部分。

让我们看看属性的样子:

<?php
namespace App\Controller;
class ExampleController
{
    public function home(#[CurrentUser] User $user)
    {
        // ...
    }
}

很明显,当前登录的用户很可能会被注入到 $user 变量中。代码看起来很锐利,我们有了所有需要的信息来一眼看出正在发生的事情。如果我们需要在 home() 方法上面的注释部分添加信息,我们也有一个完美的空白空间。你现在可以清楚地看到属性如何帮助你更加严谨——移除所有注释块,在添加新的注释之前三思。

从版本 8.0 开始,PHP 的最新版本中显然还有很多事情在发生。以下是一个非详尽的列表:

  • 联合类型

  • 匹配语法

  • 命名参数

  • 枚举

  • JIT 编译器

  • 纤程

  • 数字分隔符

让我们在下一节中看看其中的一些。

第 8 版革命

正如我们所见,PHP 在过去几年中经历了非凡的势头。虽然我们认为版本 7 是语言的真正重生,但版本 8 证明这只是开始。以下是一些主要的新特性,这些特性将帮助你编写清晰简洁的代码,并帮助你进一步推动我们在这些章节中看到的清洁代码原则。

匹配语法

匹配语法是经典 switch/case 的简略版本。它不应该到处使用,因为它可以很快变得难以阅读。然而,如果你选择谨慎使用它的地方,你的代码可以立即变得更加清晰。以下是一个匹配语法的示例:

$foo = match($var) {
    ‹value 1› => Bar::myMethod1(),
    ‹value 2› => Bar::myMethod2(),
};

它的工作方式与 switch 一样。然而,请注意代码长度和可读性提高的差异。你还可以立即看到这种语法的局限性:如果每个案例有多个要执行的语句,它根本不适应,你面临的结果可能是难以阅读的。因此,坚持更经典的 switch/case 块是更好的选择。

命名参数

如果你习惯于经常使用其他语言,你可能熟悉 PHP 的这种演变。从版本 8.0 开始,你可以按照你想要的顺序传递参数给方法。这是通过在参数值之前指定参数名称来完成的。

$this->myMethodCall(needle: 'Bar', enabled: true);

我们可以快速看到两种情况下这很有用:

  • 首先,我们有时会遇到具有很多可选参数的方法,这些参数已经定义了默认值。有时,我们想要更改列表中可能是第 5 个或第 11 个的参数的值。这随后是一个漫长的过程,需要重写所有参数的默认值。这显然不是理想的,但这是一个可以通过使用命名参数解决的问题。你只需指定你想要提供的参数的名称及其值,然后你就完成了。

  • 第二种情况是,即使方法没有可选参数,你也可以通过添加清晰度来改进方法调用。如果你的方法有很多参数,你可能想考虑使用命名参数来明确显示你发送给方法的参数。然而,如果情况如此,真正使代码更易读的解决方案是对代码进行重构,这样你就不需要向方法传递这么多参数。这可以通过将方法拆分成几个方法,或者通过创建值对象VOs)来实现。这些对象只包含简单的属性,没有其他方法或逻辑,用于在代码中将数据从一个点传输到另一个点。这避免了无休止的参数担忧,并且它还通过强类型属性添加了一层验证,并为携带的数据添加了一些上下文。

只读类和属性

这里有一个小谜题来介绍只读类和属性。在一个 PHP 类中,你如何确保一个属性只会被分配一次——也就是说,它只能分配一次值,所有未来的尝试分配它都会失败?请注意,这也适用于类内部的赋值。这意味着在调用中抛出异常的修改器无法工作,因为我们不能 100%确定开发者会在类作用域内通过修改器。

实际上,答案很简单:这是不可能的。如果你使用的是 PHP 8.0 或更早版本,这根本不可能。PHP 8.1 为我们带来了一个原生解决方案来解决这个问题:只读属性。通过使用readonly关键字声明类属性,变量只能被赋值一次,无论上下文如何。这特别有用,当定义和使用 DTOs 和 VOs 时。通过限制对这些属性的修改访问,使用它们的开发者将不得不仔细思考对象的使用。如果他们想要进行更改,他们将不得不创建一个新的对象。原始对象不能被修改,这可以保证代码有更好的稳定性和健壮性。以下是声明只读属性的方法:

<?php
namespace App\Model;
class MyValueObject
{
    protected readonly string $foo;
    public function __construct(string $foo)
    {
        $this->foo = $foo; // First assignment, all good
        // Any further assignment of $this->foo will result
          in a fatal error
    }
}

自 PHP 8.2 以来,甚至可以通过在 class 关键字之前放置此修饰符来声明整个类为 readonly,这样你就不必在类包含的每个属性上使用该关键字。这也给你带来了很大的优势:声明为 readonly 的类将无法动态地声明新属性。尽管这种行为已被弃用,并且应该不惜一切代价避免(在调试、稳定性和代码复杂性方面,动态声明类属性是一个噩梦),但在 PHP 9.0 版本中仍然可以这样做,如果发生这种行为,将触发致命错误。如果你不使用 PHP 9.0,将你的类声明为 readonly 将保护你免受这种行为的影响。

这对于 PHP 开发者来说是一个巨大的进步。确实,多亏了这个特性,我们再次需要越来越严格地对待我们与对象及其属性交互的方式。

将资源迁移到适当的类中

根据你在 PHP 中的开发经验长短,你应该或多或少熟悉我们所说的资源。资源是一种特殊类型的变量,它代表了对外部资源的引用。这听起来可能有些模糊,但实际上相当简单。资源可以是以下内容,例如:

  • 打开的文件

  • 数据库连接

  • cURL 调用

  • 连接到 LDAP 目录(通常是一种在公司中管理用户账户的方式)

  • 用于 GD 图像操作扩展的字体

这已经很好地工作了数十年,但我们理解,术语 资源 由于过于通用,尤其是现在在现代代码中类和对象占主导地位,已经不再适用了。为什么这些资源不能像任何其他对象一样简单呢?好吧,至少目前没有特别的理由来证明这一点。这就是为什么 PHP 8.0 开始了一个漫长但必要的迁移过程,将旧资源转换为完整的类。这样做更有意义。

资源的使用很复杂。它们难以调试,并且理解它们的工作方式和内部状态也很复杂。它们只能通过处理资源的特殊函数来调用。这对于将会有更多控制和更多工具来提高开发严谨性和代码健壮性的开发者来说是一个巨大的进步。

随着新版本的 PHP 发布,这种变化被实现。例如,PHP 8.0 内嵌了以下资源的迁移:

  • GD,用于图像操作

  • cURL

  • OpenSSL

  • XML

  • Sockets

相反,PHP 8.1 内嵌了以下资源的迁移:

  • GD 字体

  • FTP

  • IMAP

  • finfo,用于文件管理

  • Pspell,用于拼写检查

  • LDAP

  • PostgreSQL

以下版本的 PHP 继续执行相同的任务——最终移除标准 PHP 库中资源的使用。此外,自 PHP 8.1 以来,核心 PHP 开发者决定在命名空间下创建一些这些类。我们可以看到,经过长时间的滞后,这种行动和开发使得语言努力恢复其声誉。PHP 清楚地表明:这种语言将长期存在。

保护你的敏感参数不被泄露

如果你已经用 PHP 开发了一段时间,你肯定知道有很多种方法可以显示变量或函数参数的内容。特别是你可以想到var_dumpprint_r。还有其他场合可以显示参数及其值:当显示堆栈跟踪(或调用堆栈)时。这可能是通过手动调用debug_print_backtrace等方法,也可能是抛出异常时,这种情况很常见。无论如何,如果变量或调用堆栈中的方法调用参数包含敏感信息,这可能会成为问题。你可能认为这种情况只发生在开发环境中,但这是一种错误。你很可能在服务器(们)上的某个地方编写了异常消息到错误日志中。这可能导致敏感信息显示在你的日志中。这显然是不推荐的。敏感信息不应在任何地方以明文形式写入。此外,尽管这是一个错误,但应用程序日志的安全性通常不如数据库,例如。此外,项目中的许多开发者(如果不是所有开发者)都可能有权访问这些日志以调试应用程序。威胁并不总是来自外部。

幸运的是,PHP 8.2 包含了一个新的属性来解决这个问题。确实可以在任何函数参数之前指定#[SensitiveParameter]属性。这将告诉 PHP 不要在var_dump、堆栈跟踪等中显示参数值。如果巧妙地放置,你就可以确保不会在错误消息中泄露敏感值,例如。实际上,网站直接在前端显示服务器错误并不罕见。显然,这应该尽快被禁止,但至少它有助于限制损害。让我们看看这个新属性如何使用:

<?php
namespace App\Controller;
class SecurityController
{
    public function authenticate(string $username,
      #[SensitiveParameter] string $password)
    {
        // In case of any exception occurring or var_dump
          being called in here, the value of $password will
          be hidden in the different outputs
    }
}

在 PHP 的内部工作原理中,此属性将用类型为SensitiveParameterValue的对象替换参数,这将隐藏参数的真实值。参数将在输出中显示和存在,但其值将被隐藏。将此属性添加到你的敏感方法参数中是一种聪明且受欢迎的方法,可以增加代码的严谨性并使其更具抗攻击性。

摘要

无论如何强调都不为过:PHP 正在以最美好的方式发展,并在网络世界中迎头赶上其竞争对手。该语言通过提供社区和开发者需要的工具,以最可行的方式解决现代问题,倾听他们的声音。

我们已经从一个允许一切、对当今网络应用挑战非常(过于)宽松的语言走了很长的路。尽管前端框架和技术爆炸式增长,旨在用面向前端的语言(如 Node.js 和 JavaScript)取代服务器端语言,但 PHP 没有任何可耻之处。它令人印象深刻的性能、其快速的发展速度以及多年来建立的名誉都表明,它面前还有光明的未来。

尽管如我们所见,干净的代码是一种心态,在某种程度上是一种哲学,但语言本身的原生解决方案正在大量涌现,帮助我们尽可能好地应用它们。更好的是,这些引入 PHP 的新特性使我们能够看到一些我们最初可能没有想到的新可能性,以使我们的代码更加健壮、可维护和长期可行。只需想想命名参数、只读类和属性、严格类型,或者简单地回顾一下本章最后讨论的主题:保护敏感参数不会泄露到应用程序日志和异常消息中。

话虽如此,现在是时候开始着手工作了。我们将从查看能够快速、定量地概述您代码质量的工具开始。这类指标的拥有使您能够看到您将如何改进代码,或者是否真的到了采取行动的时候,因为随着您的发展,质量正在下降。那么,让我们进入下一章,本章将重点介绍针对 PHP 语言的代码质量工具。

第二部分 – 维护代码质量

第二部分的目标是让您能够不断改进您的项目,并最终保持代码质量的一致高水平。它将为您提供使用最先进工具和技术方面的指导,这将有助于减少实现这一目标所需的努力。最后,我们将介绍一些最佳实践,这将帮助您与其他开发者一起在干净且可维护的代码库上协作。

本节包括以下章节:

  • 第七章, 代码质量工具

  • 第八章, 代码质量指标

  • 第九章, 组织 PHP 质量工具

  • 第十章, 自动化测试

  • 第十一章, 持续集成

  • 第十二章, 团队合作

  • 第十三章, 创建有效的文档

第七章:代码质量工具

在本书的前几部分,我们学习了编写清洁代码的基础知识。现在,是时候将那些知识应用到我们的日常工作中了。对于 PHP 生态系统,实际上有数十种工具可以帮助我们检测缺陷和潜在的 bug,应用正确的代码风格,并通常让我们了解质量方面的问题。

为了确保在代码质量工具的世界中快速轻松地开始,本节将向你介绍最常用的工具。对于每一个,你将学习如何安装、配置和使用它直接在你的代码上。你还将了解它们提供的一些有用的额外功能。

我们将探讨以下工具组:

  • 语法检查和代码风格

  • 静态代码分析

  • IDE 扩展

技术要求

对于本章,你只需要设置好最基本的一组工具。如果你之前曾经使用过 PHP 代码,那么你很可能已经安装了它们:

  • 本地安装最新版本的 PHP(建议使用 PHP 8.0 或更高版本)。

  • 一个代码编辑器——通常被称为集成开发环境IDE)。

  • Composer,无论是作为二进制安装还是全局安装。如果你还不熟悉 Composer,请查看getcomposer.org/

请注意,本书的其余部分,所有示例都是基于 Linux 环境,如 Ubuntu 或 macOS。如果你使用 Windows 进行开发,你很可能需要做一些调整,如这里所述:www.php.net/manual/en/install.windows.commandline.php

本章的代码文件可以在以下位置找到:github.com/PacktPublishing/Clean-Code-in-PHP

语法检查和代码风格

我们想要讨论的第一组工具帮助我们保持代码在语法上正确(即,可以被 PHP 正确执行)并以结构化的方式格式化。代码需要无错误地编写似乎是显而易见的,但总是好的进行双重检查,因为一些工具可以主动更改你的代码。在我们稍后在本书中自动化整个代码质量过程时,有一个简单快捷的方式来确保这一点将是至关重要的。

按照一个通用的风格指南格式化你的代码可以减少阅读和理解代码,以及他人代码所需的努力。特别是当你在一个团队中工作时,一个被接受的风格指南可以节省你数小时的讨论,关于如何正确格式化代码。

我们将学习以下工具:

  • PHP 内置的 linter

  • PHP CS Fixer 工具

PHP 内置的 linter

我们要首先探讨的工具实际上并不是一个独立的代码质量工具,而是一个内置在 PHP 二进制文件中的选项:Linter。它检查任何代码的语法错误而不执行它。这在确保代码在重构会话之后或代码被外部工具更改后仍然可以工作特别有用。

安装和使用

由于 Linter 已经是 PHP 安装的一部分,我们可以通过查看一个示例立即开始使用它。如果您仔细观察,您可能会注意到作者在以下类示例中犯的错误:

<?php
class Example 
{
    public function doSomething() bool
    {
        return true;
    }
}

如果您没有立即发现错误,请不要担心——这正是 Linter 存在的原因!只需使用 -l 选项将待检查文件的完整名称和路径传递给 PHP 二进制文件。通过添加 -f 选项,PHP 还会检查致命错误,这是我们想要的。这两个选项可以组合为 -lf

假设前面的类可以在当前文件夹中的 example.php 文件中找到——那么,我们只需要输入以下内容:

$ php -lf example.php

我们将得到以下输出:

PHP Parse error: syntax error, unexpected identifier
  "bool", expecting ";" or "{" in example.php on line 5
Errors parsing example.php

您可以让代码检查器检查整个目录:

$ php -lf src/*

注意

内置的 PHP 代码检查器会在第一个错误处停止——也就是说,它不会给出所有检测到的错误的全列表。所以,在解决问题后,您最好再次运行命令。

PHP 内置 Linter 的回顾

内置的 PHP 代码检查器是一个方便的工具,可以快速检查代码,但它的功能并不仅限于此。还有其他更复杂的代码检查器,例如 github.com/overtrue/phplint。这个工具不仅会返回一个完整的错误列表,还可以并行运行多个进程,这在大型代码库上会明显更快。然而,其他代码质量工具已经包含了代码检查器,例如我们将在下一节检查的工具。

PHP CS Fixer:一个代码嗅探器

另一个必不可少的工具是代码嗅探器。它扫描 PHP 代码中的编码标准违规和其他不良实践。PHP CS Fixer (github.com/FriendsOfPHP/PHP-CS-Fixer) 是一个可行的起点,因为正如其名所暗示的,它不仅报告发现的问题,还可以立即修复它们。

其他代码嗅探器

PHP CS Fixer 并不是唯一的代码嗅探器。另一个广为人知的是 PHP_CodeSniffer (github.com/squizlabs/PHP_CodeSniffer),我们也可以强烈推荐使用它。

安装和使用

使用 Composer,安装过程非常简单:

$ composer require friendsofphp/php-cs-fixer --dev

Composer 的替代方案

在这本书中,我们将介绍多种安装工具的方法。我们将在本书的后面部分检查更多选项。

代码嗅探器的典型用途是处理括号的放置和缩进的数量,无论是空格还是制表符。让我们检查以下具有丑陋格式的 PHP 文件:

<?php
class Example
{
  public function doSomething(): bool { return true; }
}

如果我们使用默认设置运行代码嗅探器,命令既简洁又简短:

$ vendor/bin/php-cs-fixer fix example.php

这将一次性扫描并修复 example.php 文件,使我们的代码整洁且光亮:

<?php
class Example
{
    public function doSomething(): bool
    {
        return true;
    }
}

如果您不想立即修复文件,可以使用 --dry-run 选项仅扫描问题。同时添加 -v 选项,以显示发现的内容:

$ vendor/bin/php-cs-fixer fix example.php --dry-run -v

与所有代码质量工具一样,你还可以在文件夹中的所有文件上运行它。以下命令将递归地扫描 src 文件夹,因此所有子文件夹也会被扫描:

$ vendor/bin/php-cs-fixer fix src

规则和规则集

到目前为止,我们使用的是 PHP CS Fixer 的默认设置。在我们改变这些默认设置之前,让我们更仔细地看看它是如何知道要检查和修复什么的。

在代码质量工具中,规则在规则集中的组织是一个常见的模式。规则是一个简单的指令,告诉 PHP CS Fixer 我们应该如何格式化代码的某个方面。例如,如果我们想在 PHP 中使用严格类型,每个 PHP 文件都应该包含 declare(strict_types=1); 指令。

PHP CS Fixer 中有一个规则可以强制执行:

$ vendor/bin/php-cs-fixer fix src 
  --rules=declare_strict_types

这个命令将检查 src 中的每个文件,并在 PHP 标签后添加 declare(strict_types=1);

由于像 PSR-12 (www.php-fig.org/psr/psr-12/) 这样的编码标准包含了大量关于代码应该如何格式化的指令,将这些规则全部添加到前面的命令中会显得很繁琐。这就是为什么引入了规则集,它们只是规则的组合,甚至还可以是其他规则集的组合。

如果我们想明确按照 PSR-12 格式化代码,我们可以直接运行这个命令:

$ vendor/bin/php-cs-fixer fix src --rules=@PSR12

如您所见,规则集是通过 @ 符号指示的。

规则和规则集文档

在本书的范围内不可能讨论 PHP CS Fixer 的每个规则和规则集。如果您对它还能提供什么感兴趣,请查看官方 GitHub 仓库:github.com/FriendsOfPHP/PHP-CS-Fixer/tree/master/doc

配置

手动执行命令可以作为一个起点,但到了某个阶段,我们可能不想每次都记住所有的选项。这时配置文件就派上用场了:大多数 PHP 代码质量工具允许我们将所需的配置存储在一个或多个文件中,并支持多种格式,例如 YAML、XML 或纯 PHP。

对于 PHP CS Fixer,所有相关设置都可以通过 .php-cs-fixer.dist.php 配置文件来控制。在这里,您将找到一个示例:

<?php
$finder = PhpCsFixer\Finder::create()
    ->in(__DIR__)
    ->exclude('templates');
$config = new PhpCsFixer\Config();
return $config->setRules([
    '@PSR12' => true,
    'declare_strict_types' => true,
    'array_syntax' => ['syntax' => 'short'],
])
->setFinder($finder);

这里发生了许多事情。首先,创建了一个 PhpCsFixer\Finder 的实例,该实例被配置为使用与配置文件相同的目录来查找 PHP 文件。由于应用程序的 root 文件夹通常位于此处,我们可能希望排除某些子目录(例如本例中的 templates)不被扫描。

其次,创建了一个 PhpCsFixer\Config 的实例。在这里,我们告诉 PHP CS Fixer 应该应用哪些规则和规则集。我们已经讨论了 @PSR-12 规则集以及 declare_strict_types 规则。array_syntax 规则强制使用短数组语法。

您可能已经注意到配置文件名,.php-cs-fixer.dist.php,包含缩写 dist。这代表分发,通常表示此文件是项目分发的文件。换句话说,这是添加到 Git 仓库并在检出后立即可用的文件。

如果您想在本地系统上使用自己的配置,可以创建它的副本并将其重命名为 .php-cs-fixer.php。如果此文件存在,PHP CS Fixer 将使用它而不是 dist-file。让 Git 忽略此文件是一个好习惯。否则,您可能会意外地将本地设置添加到仓库中。

高级用法

PHP CS Fixer 的能力不仅限于自动修复编码标准违规。它还可以用于应用小的重构任务。一个很好的用例,例如,是自动迁移到更高的 PHP 版本:PHP CS Fixer 随附迁移规则集,可以将一些新的语言特性引入到您的代码库中。

例如,在 PHP 8.0 中,可以使用 class 关键字代替 get_class() 函数。PHP CS Fixer 可以扫描您的代码并替换某些行——例如,请参见以下内容:

$class = get_class($someClass);

它可以将前面的行替换为以下内容:

$class = $someClass::class;

迁移规则集被分为无风险和有风险两类。有风险的规则集可能会引起副作用,而无风险的规则集通常不会引起任何问题。一个有风险的更改的例子是我们之前讨论过的 declare_strict_types 规则。在应用它们之后,务必彻底测试您的应用程序。

这些迁移的能力有限——您的代码不会突然包含所有新的 PHP 版本特性。

代码修复器不能为我们修复语法错误。例如,我们在上一节中用 PHP 内置的代码检查器检查的 Example 类仍然需要开发者先手动修复。

代码检查

PHP CS Fixer 将检查您希望进行语法检查的文件,这是第一步,如果发现语法错误,将不会应用任何更改。这意味着您不需要作为额外步骤运行 PHP 内置的代码检查器。

PHP CS Fixer 概述

代码嗅探器,如 PHP CS Fixer,应该是每个严肃的 PHP 项目的组成部分。自动修复规则违规的能力可以为您节省许多工作时间。如果您选择不应用任何有风险的修复,几乎不会引起任何问题。

我们现在已经学会了如何确保我们的代码格式良好且语法正确。虽然这是任何高质量代码的基础,但它并不能帮助我们避免错误或维护性问题。在这个时候,静态代码分析工具就派上用场了。

静态代码分析

静态代码分析意味着唯一的信息来源是代码本身。仅通过扫描源代码,这些工具就能发现即使是团队中最资深的开发者也可能在代码审查中遗漏的问题和问题。

这些是我们将在下一节中向您介绍的工具:

  • phpcpd

  • PHPMD

  • PHPStan

  • 诗篇

phpcpd – 复制粘贴检测器

复制粘贴编程可能从简单的烦恼到对项目构成的真实威胁。错误、安全问题和不规范的做法会被复制并因此变得更加难以修复。把它想象成一种通过你的代码传播的瘟疫。

这种编程形式相当常见,尤其是在经验较少的开发者中,或者在截止日期非常紧张的项目中。幸运的是,我们的清洁代码工具包提供了一种补救措施——PHP 复制粘贴检测器phpcpd)。

安装和使用

此工具只能作为自包含的 PHP 存档phar)下载,因此这次我们不会使用 Composer 来安装它:

$ wget https://phar.phpunit.de/phpcpd.phar

处理 phar 文件

第九章“组织 PHP 质量工具”中,我们将学习如何保持phar文件的组织。现在,只需下载它就足够了。

下载后,phpcpd可以立即使用,无需进一步配置。它只需要目标目录的路径作为参数。以下示例展示了如何使用默认设置扫描src目录中的所谓“克隆”(即被多次复制的代码)。让我们首先以默认设置执行它:

$ php phpcpd.phar src
phpcpd 6.0.3 by Sebastian Bergmann.
No clones found.
Time: 00:00, Memory: 2.00 MB

如果phpcpd没有检测到任何克隆,值得检查控制其“挑剔性”的两个选项,min-linesmin-tokens

$ php phpcpd.phar --min-lines 4 --min-tokens 20 src
phpcpd 6.0.3 by Sebastian Bergmann.
Found 1 clones with 22 duplicated lines in 2 files:
- /src/example.php:12-23 (11 lines)
  /src/example.php:28-39
  /src/example3.php:7-18
32.35% duplicated lines out of 68 total lines of code.
Average size of duplication is 22 lines, largest clone has
  11 of lines
Time: 00:00.001, Memory: 2.00 MB

min-lines选项允许我们设置一个代码片段需要具有的最小行数,直到它被视为克隆。

要理解min-tokens的用法,我们首先必须明确在这个上下文中“标记”的含义:当你执行脚本时,PHP 会内部使用所谓的“标记化器”将源代码分割成单个标记。标记是 PHP 程序的一个独立组件,例如关键字、运算符、常量或字符串。把它们想象成人类语言中的单词。因此,min-tokens选项控制代码在被视为克隆之前包含的指令数量。

你可能想尝试调整这两个参数,以找到适合你的代码库的“挑剔性”的良好平衡。代码中的某些冗余并不一定是问题,你也不想过多地打扰你的同事。因此,从默认设置开始是一个不错的选择。

进一步的选项

你还应该了解两个额外的选项:

  • --exclude <path>:从分析中排除一个路径。例如,单元测试通常包含大量的复制粘贴代码,所以你想排除tests文件夹。如果你需要排除多个路径,选项可以多次给出。

  • --fuzzy: 使用这个特别有用的选项,phpcpd 在执行检查时会混淆变量名。这样,即使变量名被一个聪明但懒惰的同事更改,也能检测到克隆。

phpcpd 概述

虽然 phpcpd 使用简单,但它对项目中复制粘贴代码的缓慢传播具有重大帮助。这就是为什么我们建议将其添加到您的清洁编码工具包中。

PHPMD:PHP 混乱检测器

混乱检测器会扫描代码中的潜在问题,也称为“代码异味”——这些是可能导致错误、意外行为或通常更难维护的代码部分。与代码风格一样,有一些规则应该遵循以避免问题。混乱检测器将这些规则应用于我们的代码。PHP 生态系统中的标准工具是 PHPMD,我们将在本节中向您展示。

安装和使用

在我们更详细地了解这个工具为我们提供的内容之前,让我们首先使用 Composer 安装它:

$ composer require phpmd/phpmd --dev

安装完成后,我们可以在命令行上运行 PHPMD。它需要三个参数:

  • 要扫描的文件名或路径(例如,src)。多个位置可以用逗号分隔。

  • 报告应生成的以下格式之一:htmljsontextxml

  • 一个或多个内置规则集或规则集 XML 文件(逗号分隔)。

为了快速入门,让我们扫描 src 文件夹,创建文本输出,并使用内置的 cleancodecodesize 规则集。我们可以通过运行以下命令来完成此操作:

$ vendor/bin/phpmd src text cleancode,codesize

PHPMD 将所有输出写入标准输出(stdout),这在命令行上。然而,除了 text 格式之外的所有输出格式都不适合在那里阅读。如果您想获得一个初步的概述,您可能想使用 html 输出,因为它会生成格式良好且交互式的报告。要将输出存储在文件中,我们将使用以下方式将其重定向到文件:使用 > 操作符。

$ vendor/bin/phpmd src html cleancode,codesize > phpmd_report.html

简单地打开您的浏览器上的 HTML 文件,您将看到类似于 图 7.1 中所示的报告:

图 7.1:浏览器中的 PHPMD HTML 报告

图 7.1:浏览器中的 PHPMD HTML 报告

报告是交互式的,所以请确保点击按钮,如显示详细信息显示代码,以显示所有信息。

规则和规则集

在前面的示例中,我们使用了内置的 cleancodecodesize 规则集。首先,规则集的命名是根据规则检查的问题域来命名的——例如,对于 cleancode 规则,您将只能找到帮助保持代码库清洁的规则。然而,您仍然可能遇到具有许多复杂函数的大类。为了避免这种情况,添加 codesize 规则集是必要的。

以下表格显示了可用的规则集及其用法:

规则集 简称 描述
清洁代码规则 cleancode 强制执行一般清洁代码
代码大小规则 codesize 检查长或复杂的代码块
有争议的规则 controversial 检查存在争议意见的最佳和不良实践
设计规则 design 帮助发现与软件设计相关的问题
命名规则 naming 避免过短或过长的名称
未使用代码规则 unused 检测可以删除的未使用代码

表 7.1:PHPMD 规则集

这些内置规则可以通过将上述简短名称作为函数调用的参数来简单地使用,正如先前的例子所示。

如果你足够幸运,可以从零开始(即从头开始)启动一个项目,那么你可以并且应该从一开始就尽可能多地强制执行规则。这将使你的代码库从一开始就保持清洁。对于现有项目,所需的努力会更大,我们将在下一节中看到。

在遗留项目中使用 PHPMD

通常情况下,你可能会想要在一个现有的项目中使用 PHPMD。在这种情况下,你可能会在第一次运行时被它抛出的无数警告所淹没。不要放弃——有一些选项可以帮助你!

调整规则集

如果你计划将 PHPMD 添加到现有项目中,一次性使用所有规则集肯定会因为报告的问题数量而让你感到沮丧。你可能一次只想集中在一到两个规则集上。

你也很可能最终会得到一些你一开始觉得烦人或反生产力的规则——例如,ElseExpression 规则,它禁止在 if 表达式中使用 else。抛开这个规则是否有用的讨论,重写无数运行良好的语句的努力是不值得的。所以,如果你不想在你的项目中使用那个规则,你需要创建自己的规则集。

规则集通过 XML 文件进行配置,这些文件指定了属于它们的规则。每个规则基本上是一个包含规则逻辑的 PHP 类。以下 XML 文件定义了一个自定义规则集,它仅包括 cleancodecodesize 规则集:

<?xml version="1.0"?>
<ruleset name="Custom PHPMD rule set"
    xmlns=http://pmd.sf.net/ruleset/1.0.0
    xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance
    xsi:schemaLocation=http://pmd.sf.net/ruleset/1.0.0  http://pmd.sf.net/ruleset_xml_schema.xsd
xsi:noNamespaceSchemaLocation=
  "http://pmd.sf.net/ruleset_xml_schema.xsd">
    <description>
        Rule set which contains all codesize and cleancode
        rules
    </description>
    <rule ref="rulesets/codesize.xml" />
    <rule ref="rulesets/cleancode.xml" />
</ruleset>

现在 XML 似乎已经过时了,但它仍然很好地完成了它的任务。你通常不需要担心 <ruleset> 标签的所有属性——只需确保它们存在即可。《description>` 标签可以包含任何你认为是对规则集的良好描述的文本。

<rule> 标签对我们来说很重要。在先前的例子中,我们引用了 codesizecleancode 规则。

小贴士

在这一点上,深入挖掘 GitHub 仓库 https://github.com/phpmd/phpmd/tree/master/src/main/resources/rulesets 中内置的规则集是个好主意。由于 XML 是一个非常冗长的文件格式,你将很快熟悉它。

假设我们想要从检查中移除提到的ElseExpression规则。为了实现这一点,你只需在相应的<rule>标签内添加一个<exclude>标签,如下所示:

<rule ref="rulesets/cleancode.xml">
    <exclude name="ElseExpression" />
</rule>

这样,你可以从规则集中排除必要的规则。如果你只想从不同的规则集中选择某些规则,你也可以反过来直接引用所需的规则。如果你想让你的自定义规则集只包含StaticAccessUndefinedVariable规则,你的 XML 文件应该包含以下两个标签:

<rule ref="rulesets/cleancode.xml/StaticAccess" />
<rule ref="rulesets/cleancode.xml/UndefinedVariable" />

关于 XML 配置文件,还有最后一个重要的事情要知道,那就是如何更改规则的单个属性。同样,了解所有属性的一个好方法是查看实际的规则集文件。或者,你也可以查看每个规则的 PHP 类,具体可以在github.com/phpmd/phpmd/tree/master/src/main/php/PHPMD/Rule找到。

一个典型的例子是为StaticAccess规则定义异常。通常,避免静态访问是一个好的做法,但通常情况下,你无法避免。假设你的团队同意允许DateTimeDateTimezone对象使用静态访问,你可以简单地按照以下方式配置:

<rule ref="rulesets/cleancode.xml/StaticAccess">
    <properties>
        <property name="exceptions">
            <value>
                \DateTime,
                \DateTimezone
            </value>
        </property>
    </properties>
</rule>

要在未来使用此自定义规则集,只需将前面的 XML 保存到文件中(通常称为phpmd.xml),并在下一次运行PHPMD时传递它:

$ vendor/bin/phpmd src text phpmd.xml

配置文件的存储位置

phpmd.xml(包含你想要使用的规则集)放置在项目的root文件夹中,并将其用作配置的唯一来源,这是一种常见的做法。如果将来有任何修改,你只需要调整一个中央文件。

抑制警告

处理遗留代码的另一个有用工具是@SuppressWarnings DocBlock 注解。假设你的项目中有一个类使用了静态方法调用,但目前无法更改。默认情况下,任何静态访问都会抛出警告。由于你不想在代码的其他地方使用静态访问,而只是在当前类中使用,因此移除StaticAccess规则将适得其反。

在这些情况下,你可以使用@SuppressWarnings注解:

/**
* @SuppressWarnings(PHPMD.StaticAccess)
*/
class ExampleClass {
    public function getUser(int $id): User {
        return User::find($id);
    }
}

如果需要,你可以在一个 DocBlock 中使用多个注解。最后,如果你想在一个类上抑制任何警告,只需使用@SuppressWarnings(PHPMD)注解。

请注意,使用Suppress注解应该是你的最后选择。它非常有诱惑力,可以随意添加。然而,它将使输出静音,但不会解决问题。

接受违规

除了在文件级别抑制警告或从规则集中排除规则之外,你也可以选择承认现有的违规。例如,当你想在遗留项目中使用PHPMD时,你可以决定暂时忽略代码中已经存在的所有违规。然而,如果新的类引入了新的违规,它们将被报告。

幸运的是,PHPMD 通过提供一个所谓的基线文件,使得这项任务变得相当简单,该文件会通过运行以下命令自动为您生成:

$ vendor/bin/phpmd src text phpmd.xml --generate-baseline

在前面的命令中,我们期望在项目 root 文件夹中已经存在一个 phpmd.xml 文件。使用前面的命令,PHPMD 现在将创建一个名为 phpmd.baseline.xml 的文件。

现在,您可以运行以下命令:

$ vendor/bin/phpmd src text phpmd.xml

下次,PHPMD 将自动检测之前生成的基线文件,并使用它来抑制所有警告。然而,如果在新位置引入了新的规则违规,它仍然会被检测并报告为违规。

一个警告:与 @SuppressWarning 注解一样,基线功能不是一个可以用一次并安全忽略未来的工具。有问题的代码块仍然是您项目中的技术债务,带有所有负面效应。这就是为什么如果您决定使用基线功能,您应该确保您不会忘记在未来解决这些隐藏问题。

我们将在本书的后面讨论如何处理这些问题。现在,对于您来说,重要的是如何定期更新基线文件。同样,PHPMD 使这项任务变得简单。只需运行以下命令:

$ vendor/bin/phpmd src text phpmd.xml --update-baseline

在您的代码中不再存在的所有违规将被从基线文件中删除。

PHPMD 概述

除非您是从零开始一个项目,否则 PHPMD 的配置将需要更多的时间。特别是如果您在一个团队中工作,您将花费更多的时间争论使用哪些规则以及排除哪些规则。不过,一旦完成,您将拥有一个强大的工具,它将帮助开发者编写高质量、可维护的代码。

PHPStan – PHP 静态分析器

您可能已经注意到,我们在上一节中讨论的 PHPMD 并非非常特定于 PHP,而是通常关注最佳编码实践。虽然这当然非常重要,但我们现在想使用 PHPStan 来分析我们的代码,并考虑到不良的 PHP 实践。

与每个静态分析工具一样,PHPStan 只能使用从代码中获取的信息来工作。因此,它更适合现代面向对象代码。例如,如果代码大量使用严格类型,分析器将处理额外的信息,因此会返回更多结果。但对于旧项目,它也将非常有帮助,正如我们将在下一节中看到的那样。

安装和使用

使用 Composer 安装 PHPStan 仍然只需要一行命令:

$ composer require phpstan/phpstan --dev

与大多数代码质量工具一样,PHPStan 可以使用 PHAR 安装。然而,只有当使用 Composer 时,您才能安装扩展。我们将在本节的稍后部分探讨这些内容。

让我们使用以下简化的示例并将其存储在 src 文件夹中:

<?php
class Vat
{
    private float $vat = 0.19;

    public function getVat(): int
    {
        return $this->vat;
    }
}
class OrderPosition
{
    public function getGrossPrice(float $netPrice): float
    {
        $vatModel = new Vat();
        $vat = $vatModel->getVat();
        return $netPrice * (1 + $vat);
    }
}
$orderPosition = new OrderPosition();
echo $orderPosition->getGrossPrice(100);

要执行扫描,您需要指定 analyse 关键字,以及扫描的路径,在我们的例子中是 src

$ vendor/bin/phpstan analyse src

图 7.2 展示了 PHPStan 生成的输出:

图 7.2:PHPStan 的一个示例输出

图 7.2:PHPStan 的一个示例输出

当我们执行 PHP 脚本时,它将输出 100。不幸的是,这是不正确的,因为将 19% 的税费加到净价上应该返回 119,而不是 100。所以,肯定哪里出了 bug。让我们看看 PHPStan 如何在这里帮助我们。

规则级别

PHPMD 不同,在 PHPMD 中你需要详细配置要应用哪些规则,我们在这里将使用不同的报告级别。这些级别是由 PHPStan 的开发者定义的,从级别 0(仅执行基本检查)到级别 9(对问题非常严格)。为了不让用户一开始就被错误淹没,PHPStan 默认将使用级别 0,这只会执行非常少的检查。

你可以使用 level-l|--level)选项来指定级别。让我们尝试下一个更高的级别:

$ vendor/bin/phpstan analyse --level 1 src

使用级别方法,你可以轻松地逐步提高代码的质量,正如我们将使用以下虚构的示例来展示。级别 1 和 2 也不会返回任何错误。然而,当我们最终达到级别 3 时,我们最终会找到一个问题:

图 7.3:PHPStan 报告了一个级别 3 的错误

图 7.3:PHPStan 报告了一个级别 3 的错误

再次检查我们的代码,我们可以快速发现问题:getVat() 方法返回一个浮点数(0.19),但使用 int 返回类型将其转换为 0。

严格类型

如果我们在示例代码的顶部添加了 declare(strict_types=1); 语句来使用严格模式,PHP 将会抛出一个错误,而不是静默地将返回值转换为 int

这展示了静态代码分析的魅力和力量:修复这个小 bug 将使我们的代码按预期工作,而且由于我们仍然处于开发环境中,我们只需几秒钟就能完成。然而,如果这个 bug 已经到达了生产环境,修复它将花费我们更长的时间,并且会留下一些愤怒的客户。

配置

你可以使用配置文件来确保始终检查相同的级别和相同的文件夹。配置是用 NEON(https://ne-on.org/) 编写的,这是一种与 YAML 非常相似的文件格式;如果你能读写 YAML,它就会正常工作。

基本配置只包含级别和要扫描的文件夹:

parameters:
    level: 4
    paths:
        - src

将此配置保存为名为 phpstan.neon 的文件,放在项目的 root 文件夹中是一个好习惯。这是 PHPStan 默认期望的位置。如果你遵循这个约定,下次你想运行它时,你只需要指定所需的操作:

$ vendor/bin/phpstan analyse

如果你使用了上面的示例配置,PHPStan 现在将扫描 src 文件夹,使用从级别 0 到级别 4 的所有规则。

这并不是你可以在这里配置的所有内容。在下一节中,我们将了解一些额外的参数。

在遗留项目中使用 PHPStan

如果你想在一定年龄的现有项目中使用PHPStan,你可能会遇到数百甚至数千个错误,具体取决于选择哪个级别。当然,你可以选择继续使用较低级别;但这也意味着分析器会错过更多的错误,不仅包括现有的错误,还包括新或修改的代码。

在一个理想的世界里,你会从 0 级开始,解决所有错误,然后继续到 1 级,解决所有新的错误,依此类推。然而,这需要很多时间,而且如果没有可用的自动化测试,最后还需要进行一次完整的手动测试运行。你可能没有那么多时间,所以让我们看看我们还有哪些其他选项。

有两种方式可以告诉PHPStan忽略错误:首先,使用PHPDocs注释,其次,使用配置文件中的特殊参数。

使用 PHPDocs 注释

要忽略一行代码,只需在受影响的行之前或之上添加一个注释,使用特殊的@phpstan-ignore-next-line@phpstan-ignore-line PHPDocs注释:

// @phpstan-ignore-next-line
$exampleClass->foo();
$exampleClass->bar(); // @phpstan-ignore-line

这两行代码将不再被扫描错误。你可以选择你喜欢的任何方式。不过,无法忽略更大的代码块或整个函数或类(除非你想要在每一行添加注释)。

使用 ignoreErrors 参数

PHPDocs 注释非常适合在几个位置快速修复,但如果你希望忽略许多错误,你将需要修改许多文件。然而,在配置文件中使用ignoreErrors参数并不太方便,因为你必须为每个想要忽略的错误编写一个正则表达式。

以下示例将解释它是如何工作的。让我们假设我们持续收到以下错误:

Method OrderPosition::getGrossPrice() has no return type specified.

虽然从理论上讲,这很容易修复,但团队决定不添加类型提示,以避免任何副作用风险。OrderPosition类写得非常糟糕,没有经过测试,但仍然按预期工作。由于它很快就会被替换,我们不愿意承担风险去修改它。

要忽略此错误,我们需要将ignoreErrors参数添加到我们的phpstan.neon配置文件中:

parameters:
    level: 6
    paths:
        - src
    ignoreErrors:
        - '#^Method OrderPosition\:\:getGrossPrice\(\) has no return type specified\.$#'

我们不需要定义一个规则或规则集来忽略,而是需要提供一个正则表达式,以匹配应该忽略的错误消息。

小贴士

编写正则表达式可能具有挑战性。幸运的是,PHPStan网站提供了一个非常有用的工具,可以从错误消息生成必要的phpstan.neon部分:phpstan.org/user-guide/ignoring-errors#generate-an-ignoreerrors-entry

在下一次运行时,无论错误出现在哪里,都不会再显示,因为它与这里的正则表达式匹配。

PHPStan 不会告诉你错误被忽略了的事实。不要忘记在某个时候修复它们!然而,如果你随着时间的推移进一步改进你的代码,PHPStan 将会通知你那些被设置为忽略的错误不再匹配。那时你可以安全地从列表中移除它们。

如果你想要完全忽略某些错误,但只是在文件或路径中,你可以通过使用稍微不同的符号来实现:

ignoreErrors:
    -
        message: '#^Method
          OrderPosition\:\:getGrossPrice\(\) has no return
          type specified\.$#'
        path: src/OrderPosition.php

路径需要相对于 phpstan.neon 配置文件的位置是相对的。当给出时,只有当错误发生在 OrderPosition.php 中时,才会忽略该错误。

基线

正如我们在上一节中看到的,手动将你想要忽略的错误添加到配置文件中是一个繁琐的任务。但有一个更简单的方法:类似于 PHPMD,你可以通过执行以下命令并使用 --generate-baseline 选项一次性自动将所有当前错误添加到忽略错误列表中:

$ vendor/bin/phpstan analyse --generate-baseline

新生成的文件 phpstan-baseline.neon 与配置文件位于同一目录下。尽管如此,PHPStan 不会自动使用它。你必须手动将其包含在 phpstan.neon 文件中,如下所示:

includes:
    - phpstan-baseline.neon
parameters:
    …

下次你运行 PHPStan 时,之前报告的错误将不再被报告。

在内部,基线文件不过是一个自动创建的 ignoreErrors 参数列表。请随意根据你的需求进行修改。你可以通过再次执行带有 --generate-baseline 选项的 phpstan 命令来重新生成它。

扩展

可以扩展 PHPStan 的功能。充满活力的社区已经创建了许多有用的扩展。例如,像 Symfony、Laminas 或 Laravel 这样的框架通常使用魔法方法(如 __get()__set()),这些方法无法自动分析。这些框架有针对 PHPStan 的扩展,为它提供必要的信息。

尽管我们无法在这本书中涵盖这些扩展,但我们鼓励你查看扩展库:phpstan.org/user-guide/extension-library。还有针对 PHPUnit、phpspec 和 WordPress 的扩展。

PHPStan 概述

PHPStan 是一个强大的工具。我们无法在几页中涵盖其所有功能,但我们已经给你一个如何开始使用它的好主意。一旦你熟悉了其基本用法,请访问 phpstan.org 了解更多!

Psalm:一个 PHP 静态分析 linting 机器

我们接下来要介绍的最后静态代码分析器是 Psalm。它将检查我们的代码库中的所谓问题,并报告任何违规行为。此外,它还可以自动解决其中的一些问题。让我们更深入地了解一下。

安装和使用

再次强调,使用 Composer 安装 Psalm 只需要几个按键:

$ composer require --dev vimeo/psalm

它也以 phar 文件的形式提供。

安装完成后,我们并不能直接开始——相反,我们首先需要为当前项目设置一个配置文件。我们可以使用方便的--init选项来创建它:

$ vendor/bin/psalm --init

此命令将在当前目录中写入一个名为psalm.xml的配置文件,这应该是项目根目录。在其创建过程中,Psalm会检查它是否可以找到任何 PHP 代码,并决定从哪个错误级别开始。运行Psalm不需要任何更多选项:

$ vendor/bin/psalm

配置

配置文件已经在安装过程中创建,例如,可能看起来像这样:

<?xml version="1.0"?>
<psalm
    errorLevel="7"
    resolveFromConfigFile="true"
    xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance
    xmlns=https://getpsalm.org/schema/config
    xsi:schemaLocation=https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd
>
    <projectFiles>
        <directory name="src" />
        <ignoreFiles>
            <directory name="vendor" />
        </ignoreFiles>
    </projectFiles>
</psalm>

让我们看看<psalm>节点的属性。你不需要担心与模式和相关信息的名称空间,只需关注以下两点:

  • errorLevel:级别从 8(基本检查)到 1(非常严格)。换句话说,级别越低,应用的规则越多。

  • resolveFromConfigFile:将此设置为true允许Psalm从配置文件的位置解析所有相对路径(如srcvendor)——通常是从项目根目录。

Psalm 文档

Psalm 提供了许多我们在这本书中无法涵盖的配置选项。一如既往,我们建议查看文档(https://psalm.dev/docs)以了解更多关于这个工具的信息。

<psalm>节点内部,你可以找到更多设置。在上一个示例中,Psalm被指示只扫描src文件夹,并忽略vendor文件夹中的所有文件。忽略vendor文件夹很重要,因为我们不想扫描任何第三方代码。

在遗留项目中使用 Psalm

现在,我们将探讨如何调整Psalm以更好地处理现有项目。与之前的工具一样,基本上有两种方式来忽略问题:使用配置文件或 docblock 注释。

有三种代码问题级别:infoerrorsuppress。当发现轻微问题时,info只会打印信息消息,而另一方面,处于error类型级别的问题则需要你采取行动。suppress类型的问题根本不会显示。

持续集成

当构建持续集成管道时,infoerror之间的区别变得更加重要。info问题会让构建通过,而error问题则会中断它。我们稍后会更详细地探讨这个话题。

Docblock 抑制

@psalm-suppress注释可以在函数 docblock 或下一行的注释中使用。前一个示例中的Vat类可能看起来如下:

class Vat
{
    private float $vat = 0.19;
    /**
     * @psalm-suppress InvalidReturnType
     */
    public function getVat(): int
    {
        /**
         * @psalm-suppress InvalidReturnStatement
         */
        return $this->vat;
    }
}

配置文件抑制

如果我们要抑制问题,我们需要为它们配置issueHandler,其中我们可以手动设置抑制类型。这是通过在<psalm>节点内添加<issueHandler>节点来在配置文件中完成的:

<issueHandlers>
    <InvalidReturnType errorLevel="suppress" />
    <InvalidReturnStatement errorLevel="suppress" />
</issueHandlers>

上述配置将抑制整个项目中所有 InvalidReturnTypeInvalidReturnStatement 问题。不过,我们可以使这个设置更加具体:

<issueHandlers>
    <InvalidReturnType>
        <errorLevel type="suppress">
            <file name="Vat.php" />
        </errorLevel>
    </InvalidReturnType>
    <InvalidReturnStatement>
        <errorLevel type="suppress">
            <dir name="src/Vat" />
        </errorLevel>
    </InvalidReturnStatement>
</issueHandlers>

在文档中(psalm.dev/docs/running_psalm/dealing_with_code_issues/),你会发现更多抑制问题的方法——例如,通过变量名。

基准

与我们之前讨论的静态代码分析器一样,Psalm 也提供了一个生成基准文件的功能,该文件将包含所有当前错误,以便在下次运行时忽略它们。请注意,基准功能仅适用于 error 问题,但不适用于 info 问题。让我们首先创建该文件:

$ vendor/bin/psalm --set-baseline=psalm-baseline.xml

Psalm 为此文件没有默认名称,因此你需要将其作为选项传递给命令:

$ vendor/bin/psalm --use-baseline=psalm-baseline.xml

你也可以将其添加为配置文件中 <psalm> 节点的附加属性:

<psalm
    ...
    errorBaseline="./psalm-baseline.xml"
>

最后,你可以更新基准文件——例如,在你对代码进行了一些改进之后:

$ vendor/bin/psalm --update-baseline

自动修复问题

Psalm 不仅会找到问题,还可以自动修复其中许多问题。当这种情况发生时,它会通知你,你可以使用 --alter 选项:

Psalm can automatically fix 1 issues.
Run Psalm again with
--alter --issues=InvalidReturnType --dry-run
to see what it can fix.

让我们按照 Psalm 建议执行命令:

$ vendor/bin/psalm --alter --issues=InvalidReturnType --dry-run

--dry-run 选项告诉 Psalm 只显示它将如何作为 diff 进行更改,但不会应用这些更改。这样,你可以检查更改是否正确:

![图 7.4:Psalm 显示建议的更改

![img/Figure_7.04_B19050.jpg]

图 7.4:Psalm 显示建议的更改

如果你移除了 --dry-run 选项,更改将被应用。

Psalm 概述

Psalm 是清洁编码者工具箱中的标准工具,原因有很多。它速度快,易于使用,功能强大。此外,代码操作功能将为您节省大量时间。当然,它与 PHPStan 有许多相似之处,但通常,你会在没有问题的同一代码库上找到这两个工具共同工作。至少,你应该考虑尝试一下。

IDE 扩展

我们迄今为止查看的工具有一个共同点:在我们编写代码之后,我们需要将它们应用到我们的代码上。当然,这比没有好得多,但如果工具能在我们编写代码时立即给我们反馈,那岂不是更好?

许多其他开发者也有同样的想法,因此他们为最流行的 IDE 创建了扩展,目前是 Visual Studio CodeVS Code)和 PhpStorm

  • PhpStorm 是来自 JetBrains 的一个成熟的商业 IDE,具有多个针对 PHP 的工具、检查和内置集成,用于我们本章讨论的许多代码质量工具。它也有许多有用的扩展可用。你可以免费试用 30 天。

  • VS Code 是微软的一个高度灵活的代码编辑器,拥有大量的第三方(部分为商业)扩展,可以将这些工具变成适用于今天几乎所有相关编程语言的 IDE。由于代码编辑器本身是免费的,因此它变得越来越受欢迎。

其他 PHP IDE

PhpStormVS Code 并不是唯一适用于 PHP 的 IDE。其他替代方案包括 NetBeans (netbeans.apache.org)、Eclipse PDT (www.eclipse.org) 或 CodeLobster (www.codelobster.com)。

在本节中,我们将向您介绍这两个 IDE 的三个扩展:

  • PHP 检查(EA 扩展)for PhpStorm

  • Intelephense for VS Code

PhpStorm 中的代码质量工具集成

PhpStorm 为我们讨论的以下工具提供了无缝集成:PHP CS Fixer、PHPMD、PHPStan 和 Psalm。更多信息请参阅此处:www.jetbrains.com/help/phpstorm/php-code-quality-tools.html

PHP 检查(EA 扩展)

此插件(github.com/kalessil/phpinspectionsea)是为 PhpStorm 设计的。它将为已有的检查类型增加更多种类,涵盖代码风格、架构或可能的错误等方面。

IDE 检查

现代 IDE 已经配备了大量的有用代码检查。在 PHPStorm 中,它们被称为 检查。一些默认启用,更多的可以通过手动激活(www.jetbrains.com/help/phpstorm/code-inspection.html#access-inspections-and-settings)。对于 VS Code,你首先需要安装一个扩展。查看文档(code.visualstudio.com/docs/languages/php)以获取更多信息。

安装

与每一个 PhpStorm 插件一样,安装是通过 文件 -> 设置 -> 插件 对话框完成的。你可以在供应商网站上找到有关如何安装插件的详细信息(www.jetbrains.com/help/phpstorm/managing-plugins.html)。只需搜索 EA 扩展。请注意,此插件还有一个名为 EA Ultimate 的第二个版本,你需要付费。本书中不会涉及它。

安装后,并非所有检查都立即激活。让我们看看 PHPStorm 检查配置,如图 7.4 所示:

![图 7.5:PHPStorm 中检查配置对话框img/Figure_7.05_B19050.jpg

图 7.5:PHPStorm 中检查配置对话框

所有此插件的检查都可以在 Php 检查(EA 扩展) 部分找到。默认未激活的检查可以通过勾选旁边的复选框轻松激活。我们建议在激活任何进一步的检查之前阅读文档(https://github.com/kalessil/phpinspectionsea/tree/master/docs)——否则,你可能会结束时有太多的规则。你可以在以后重新访问它们。

用法

第 7 行的 if 子句:

图 7.6:PHP 检查(EA 扩展)发现问题的示例代码

图 7.6:PHP 检查(EA 扩展)发现问题的示例代码

当你将鼠标指针悬停在突出显示的区域时,PhpStorm 将显示一个弹出窗口,其中包含有关建议改进的进一步说明:

图 7.7:PHP 检查(EA 扩展)建议代码改进

图 7.7:PHP 检查(EA 扩展)建议代码改进

你可以选择直接按 Alt + Shift + Enter 同时修复问题,或者你可以点击突出显示的区域以显示快速修复气泡。如果你点击气泡,你将看到一个包含更多选项的菜单。你也可以通过按 Alt + Enter 调用以下对话框:

图 7.8:快速修复选项菜单

图 7.8:快速修复选项菜单

PhpStorm 现在为你提供了几个修复方案。第一个,标记为 [EA],是由插件提出的建议。再点击一次将应用修复:

图 7.9:应用快速修复后的代码

图 7.9:应用快速修复后的代码

就这样!仅仅几秒钟,你就使你的代码变得更短、更易于阅读。PHP 检查(EA 扩展) 是 PhpStorm 的一个很好的补充,因为它提供了合理的检查,并且无缝地整合了它们。如果你使用这个 IDE,你不应该犹豫安装它。

团队协作中的检查

这些检查是改进你的代码并了解最佳实践的好方法。然而,有一个巨大的缺点:你如何确保每个在你项目上工作的开发者都激活了相同的检查?我们将在 团队协作 中讨论这个话题。

Intelephense

我们要介绍的第二个扩展是 VS Code 的 Intelephense。它是此编辑器中最常下载的 PHP 扩展,提供了许多功能(如代码补全和格式化),将 VS Code 转换成了一个完全功能的 PHP IDE。此扩展还有一个商业的、高级版本,它提供了更多的功能。要安装它,请按照此插件的市场网站上的说明进行操作(https://marketplace.visualstudio.com/items?itemName=bmewburn.vscode-intelephense-client)。

Intelephense 并没有像成熟的商业 IDE 那样提供广泛的功能,但对于一个免费服务来说,它是一个完美的选择。它提供了所谓的诊断功能(与 PhpStorm 中的检查类似),可以在插件设置屏幕中进行配置,如图 7.9 所示:

Figure 7.10: The Intelephense settings screen

img/Figure_7.10_B19050.jpg

图 7.10:Intelephense 设置屏幕

用法

下图展示了 Intelephense 中的诊断功能:

Figure 7.11: A sample class showing how Intelephense highlights issues

img/Figure_7.11_B19050.jpg

图 7.11:一个示例类,展示了 Intelephense 如何突出显示问题

在这里可以看到两件事。首先,并且更为明显的是,在TestClass下面有一条红线。将鼠标指针悬停在TestClass上时,会弹出一个包含解释的窗口:未定义类型 TestClass。这很合理,因为这个类不存在。

其次,并且更为微妙的是,你会注意到$ununsedAttribute$testInstance的颜色比其他变量略深。这表明了另一个问题,可以通过将鼠标悬停在其中一个变量上来揭示:

Figure 7.12: An info popup in Intelephense

img/Figure_7.12_B19050.jpg

图 7.12:Intelephense 中的信息弹出窗口

弹窗告诉我们$unsuserAttribute在代码的其他地方没有被使用。同样也适用于$testInstance

虽然它提供了一些基本的错误检测规则和代码格式化功能,但可以明确地说,在撰写本文时,这个插件的重点并不在于编写干净的代码。然而,鉴于 VS Code 和这个插件都是免费提供的,你已经有了一个不错的 PHP 集成开发环境来开始编码。

VS Code 中的代码质量工具集成

就像在 PhpStorm 中一样,可以通过插件将一些常见的代码质量工具集成到 VS Code 中,例如 PHPStan (marketplace.visualstudio.com/items?itemName=calsmurf2904.vscode-phpstan)、PHP CS Fixer (marketplace.visualstudio.com/items?itemName=junstyle.php-cs-fixer) 和 PHPMD (marketplace.visualstudio.com/items?itemName=ecodes.vscode-phpmd)。因此,如果你想用 VS Code 编码,确保时不时地检查市场中的新插件。

摘要

在本章中,我们学习了最先进的技术来帮助你创建高质量的 PHP 代码。它们将帮助你尽早在软件开发生命周期SDLC)中发现问题,这可以为你节省大量时间。PHP 社区仍然充满活力且非常高效,我们在这本书中无法涵盖所有那些出色的软件。然而,通过本章中我们介绍的工具,你现在已经为你的清洁代码之旅做好了充分准备。

在下一章中,你将学习如何通过使用既定的指标以及当然必要的工具来收集它们来评估代码质量。那里见!

进一步阅读

如果你想要尝试更多的代码质量工具,请考虑以下项目:

  • Exakat (https://www.exakat.io) – 一个也涵盖安全问题和性能的工具,例如。它还可以自动修复问题。

  • Phan (https://github.com/phan/phan) – 一个你可以立即在浏览器中尝试的静态代码分析器

  • PHP Insights (https://phpinsights.com/) – 另一个分析器,但在代码、架构、复杂性和风格方面提供了易于使用的指标

第八章:代码质量指标

如果我们能衡量我们软件的质量会怎么样?软件开发者经常想要不断改进他们的软件——但也许它已经“足够好了”。我们如何知道它达到了良好的状态?

软件质量指标是在编程的早期由聪明人提出的。在 20 世纪 70 年代,他们考虑了这个话题,并产生了今天仍在使用的想法。当然,我们希望从这些知识中受益,并将其应用于我们的项目中。

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

  • 介绍代码质量指标

  • 在 PHP 中收集指标

  • 使用指标的优势和劣势

技术要求

如果你已经阅读了上一章并尝试了所有工具,那么你已经安装了这一章所需的所有内容。如果没有,请在运行即将到来的示例之前确保这样做。

本章的代码文件可以在以下位置找到:github.com/PacktPublishing/Clean-Code-in-PHP

介绍代码质量指标

在本节中,你将了解如何衡量软件的整体质量。我们将探讨 PHP 世界中一些最常用的指标,并解释它们能告诉你关于你代码的什么信息,如何收集它们,以及它们何时有用或无用。

软件质量方面

在我们深入研究数字之前,我们首先需要澄清一个重要的事情:软件质量实际上是什么意思?当然,每个人对质量都有一定的理解,但可能很难用言语表达出来。幸运的是,已经存在一些现成的模型,例如 20 世纪 80 年代在惠普开发的FURPS模型。这个缩写代表以下内容:

  • 功能性:软件能否处理各种用例?它是否在考虑安全性的情况下开发?

  • 可用性:用户体验有多好?它是否被文档化并且易于理解?

  • 可靠性:软件是否始终可用?崩溃或可能影响输出的错误的可能性有多大?

  • 性能:表明软件的速度。它是否有效地使用了可用资源?它是否具有良好的可扩展性?

  • 可维护性:软件能否被很好地测试和维护?它是否易于安装,并且能否被翻译(本地化)成其他语言?

其他质量方面包括,但不仅限于,可访问性和法律合规性。正如你所见,这个模型涵盖了比我们作为 PHP 开发者通常工作的更多方面,如用户体验和文档。这就是为什么我们可以从两个不同的角度来审视软件质量:外部质量和内部质量。让我们更深入地了解一下这意味着什么:

  • 外部质量:外部或面向用户的方面是软件外部质量的一部分。这包括我们之前介绍的大多数方面。它们的共同之处在于,它们可以在不接触或分析代码本身的情况下进行测量——想想性能测试工具,它们测量请求的响应时间,或者端到端测试,它们通过自动在应用程序上执行测试来模拟用户。

  • 内部质量:作为软件开发者,我们通常更关心软件的内部质量。代码是否易于阅读和理解?是否容易扩展?我们能否为其编写测试?虽然用户永远不会看到代码,或者不关心其可测试性,但它会间接地影响他们:高质量的代码包含更少的错误,并且通常(但不总是)更快、更高效。它也更容易扩展和维护。通常,这些方面可以通过自动单元测试或代码分析器来检查。

在这本书中,我们专注于内部代码质量。这就是为什么我们特别谈论代码质量,而不是使用更广泛的概念,即软件质量。

代码质量指标

现在我们对代码质量有了更好的理解,让我们看看在本节中我们想要讨论哪些代码质量指标:

  • 代码行数

  • 圈复杂度

  • NPath 复杂性

  • Halstead 度量

  • 变更风险反模式索引

  • 维护性指标

代码行数

计算项目中的代码行数LOC)不是一个质量指标。然而,它是一个有用的工具,可以帮助我们掌握项目的大小——例如,当你开始工作的时候。此外,正如我们将看到的,它被其他指标用作计算的基础。了解你正在处理的代码行数也是一个有用的概念——例如,当你需要估计某些类的重构工作量时。

正因如此,我们现在想更深入地了解它。首先,我们可以进一步区分 LOC:

  • 代码行数LOC):LOC 简单地计算所有代码行,包括注释和空白行。

  • 注释行代码CLOC):这个指标告诉你你的代码中有多少行是注释。它可以是一个指标,表明源代码的注释是否良好。然而,正如我们所知,注释往往会过时(即,它们很快就会过时,并且通常比有用更有害),所以我们没有推荐任何百分比或其他经验法则。尽管如此,了解这一点仍然很有趣。

  • 非注释行代码NCLOC):如果你想比较一个项目与另一个项目的大小,省略注释将能更好地展示你需要处理的真实代码量。

  • 逻辑行代码LLOC):对于这个指标,假设每个语句等于一行代码。以下代码片段说明了它应该如何工作。考虑以下代码行:

    while($i < 5) { echo “test”; /* Increment by one */
      $i++; }
    

在这里,LOC 将是 1。因为我们在这行中有三个可执行语句,LLOC 将计算为 3,因为代码也可以用每个语句一行的方式编写:

while($i < 5) {
    echo “test”; 
    /* Increment by one */
    $i++; 
}

在前面的示例中,我们强调了可执行语句。注释、空行和如括号之类的语法元素不是可执行语句——这就是为什么整行注释和循环末尾的闭合括号不被计为一个逻辑行。

循环复杂度

我们不仅可以计算代码的行数,还可以测量代码的复杂性——例如,通过计算函数内的执行路径数量。这种度量中的一个常见指标是ifwhileforcase语句。此外,函数入口也计为一个语句。

以下示例说明了该度量是如何工作的:

// first decision point
function someExample($a, $b)
{
    // second decision point
    if ($a < $b) {
        echo "1"; 
    } else {
        echo "2";
    }
    // third decision point
    if ($a > $b) {
        echo "3";
    } else {
        echo "4";
    }
}

上一段代码片段的 CC 值为 3:函数入口算作第一条决策路径,两个if语句各自算作一条决策路径。然而,根据定义,两个else语句没有被考虑在内,因为它们是if子句的一部分。这个度量对于快速评估你还不了解的代码的复杂性特别有用。它通常用于检查单个函数,但也可以应用于类或整个应用程序。如果你有一个 CC 值高的函数,考虑将其拆分为几个更小的函数以降低其值。

NPath 复杂性

代码复杂性的第二个度量是定义在 CC 上的ifwhileforcase等语句。此外,对于这个度量,函数入口点不计为一个决策路径。

观察上述示例,NPath 复杂性将是 4,因为我们有 2 * 2 通过函数的可能路径:两个if语句,以及两个else语句。所以,所有四个echo语句都被视为决策路径。如前所述,函数调用本身不被考虑。现在,如果我们添加另一个if语句,NPath 复杂性将增加到 8。这是因为我们会有 2 * 2 * 2 种可能的路径。换句话说,这个度量是指数增长的,所以它可以迅速变得相当高。

NPath 复杂性比 CC 更好地描述了测试函数的实际工作量,因为它直接告诉我们为了达到 100%的测试覆盖率,我们需要测试函数的多少种可能的结果。

Halstead 度量

莫里斯·哈斯泰德在 20 世纪 70 年代末引入了一套八项度量,这些度量至今仍在使用,并被称为==!=&&等运算符(例如,函数名、变量和常量),但正如你将看到的,它们已经告诉你很多关于检查的代码的信息。

我们不需要确切地知道这些度量是如何工作的。如果你感兴趣,可以在这里了解更多关于这些度量的信息:https://www.verifysoft.com/en_halstead_metrics.html。然而,你应该对现有的 Halstead 度量有一个大致的了解:

  • 长度:计算操作符和操作数总数的总和,告诉我们必须处理多少代码

  • 词汇量:已使用的唯一操作符和操作数的总和已足以表明代码的复杂性

  • 体积:根据长度和词汇描述代码的信息内容

  • 难度:表示错误倾向性(即引入错误的可能程度)

  • 级别:反转难度——即在级别越高,错误倾向性越低

  • 努力度:理解代码所需的努力

  • 时间:告诉我们大致需要多长时间来实现它

  • 错误:估计代码中包含的错误数量

这些值将为您提供关于您正在处理的代码类型的大致指示。是否易于理解?开发它花费了多少时间?可以预期多少错误?然而,如果不将这些值与其他应用程序的结果进行比较,它们对您的帮助不大。

变更风险反模式指数

另一个特别有用的指标是变更风险反模式指数CRAP)。它使用考虑中的代码的 CC 和代码覆盖率。

代码覆盖率

您可能已经多次听说过代码覆盖率这个术语。它是在自动化测试的上下文中使用的指标,描述了单元测试已编写的代码行数(以总行数的百分比表示)。我们将在本书稍后更详细地讨论这个指标及其先决条件。

这两个指标的结合相当有用。既不复杂又有高测试覆盖率的代码,比复杂且测试不多的代码更有可能没有错误且易于维护。

维护性指数

作为本节最后一个指标,我们将查看维护性指数。它将为您提供仅一个值,表示检查的代码的可维护性,换句话说,它告诉您在不引入新错误的情况下更改它的难易程度。有两件事使这个指标对我们特别有趣。

首先,它基于上述指标,并使用 LOC、Halstead 指标和 CC 来计算指数。再次强调,我们并不真的需要知道确切的公式。如果您感兴趣,可以在这里查找:www.verifysoft.com/en_maintainability.html

其次,此指标将返回一个可以直接用于评估代码质量的值:

  • 85 及以上:良好可维护性

  • 65 到 85:中等可维护性

  • 65 以下:差的可维护性

使用此指标,您无需与其他代码进行比较。这就是为什么它特别适用于快速评估代码质量。

在本节中,我们已经讨论了很多理论。到目前为止,做得很好——您一定会后悔学习这些知识的,因为在下一节中,我们将向您展示如何使用更多的 PHP 工具来收集这些指标。

在 PHP 中收集指标

在本节中,我们想看看 PHP 世界中用于收集代码质量指标的工具。如您很快就会看到的,这些指标不仅仅是数字——它们将允许您对重构代码所需的工作量做出明智的猜测。它们还将帮助您识别需要最多关注的代码部分。

再次,我们为您精选了一系列工具:

  • phploc

  • PHP Depend

  • PhpMetrics

phploc

正如我们在上一节中学到的,LOC 的缩写代表代码行数,所以这个名字已经揭示了此工具的主要目的。作为一个基本指标,它已经告诉我们很多关于代码库的信息。phploc还提供了其他指标,如 CC,因此值得更仔细地研究它。

安装和使用

此工具的作者 Sebastian Bergmann 因phpunit而闻名,phpunit是 PHP 世界中事实上的自动化测试标准。他建议不要使用 Composer 安装它,而是直接使用phar。我们将在下一章讨论这种方法的优缺点。现在,让我们遵循作者的建议,直接下载phar

$ wget https://phar.phpunit.de/phploc.phar

这将下载phploc的最新版本到当前目录。下载后,我们可以直接使用它来扫描项目:

$ php phploc.phar src

扫描单个文件

虽然phploc旨在用于整个项目,但也可以指定单个文件进行扫描。虽然平均指标没有意义,因为它们是用于整个项目的,但如果您需要找出 LOC 指标或类的 CC 值,它仍然很有用。

之前的命令将扫描包含所有子文件夹的src文件夹,并收集有关它的信息,这些信息将直接在命令行上显示,如图图 8.1所示:

![图 8.1:phploc 的一个示例输出(摘录)图 8.01_B19050.jpg

图 8.1:phploc 的一个示例输出(摘录)

这比仅仅 LOC 的信息要多得多。信息分为以下类别:

  • 大小: 显然,这个工具存在的主要原因是通过计算代码行数来衡量项目的大小,使用我们在上一节中介绍的各种计数方法。重点在于代码行数(LLOC),你将得到每个类、类方法和函数的此指标平均值。

  • phploc将计算每个代码行数(LLOC)、类和方法的平均 CC 值。

  • 依赖关系:本节告诉你对全局状态进行了多少次访问,以及有多少属性和方法被静态访问。全局和静态访问都被视为实践,应该避免,因此这些数字为你提供了关于代码质量的更多线索。

  • phploc返回有关代码结构的更多详细信息。没有明确的规则来解释它们;然而,你可以从中得出一些结论。例如,参见以下:

    • 关于整体代码大小,使用了多少个命名空间?只有少数命名空间的庞大代码库表明项目结构不佳。

    • 接口是否被使用以及与项目规模相比使用了多少个?接口的使用增加了类的可互换性,并表明代码结构良好。

这就是我们目前需要了解的关于phploc功能的所有内容。这是一个简单易用且有用的工具,可以帮助你快速了解项目的整体代码质量和结构,因此应该成为你的工具包的一部分。尽管如此,它并没有告诉你如何解释这些数字,这需要一些经验。

PHP Depend

如果有一个奖项是授予在单一工具中结合最多指标的人,那么它肯定属于PHP DependPDepend)。它涵盖了我们在上一节中讨论的所有指标,还有更多。然而,它并不是最用户友好的工具。此外,网站和存储库文档并不完美。尽管如此,你应该检查一下。

安装和使用

如前所述,这个工具可以使用 Composer 安装或直接下载phar。我们现在将采用基于 Composer 的安装方式:

$ composer require pdepend/pdepend --dev

如果没有不愉快的惊喜,你可以直接执行:

$ vendor/bin/pdepend --summary-xml=pdepend_summary.xml src

在这里,我们已可以看到 PDepend 的祖先是 JDepend,这是一个 Java 代码质量工具,因为输出被写入 XML 文件。文件名使用--summary-xml选项指定。此外,我们必须指定要扫描的文件夹作为参数。

虽然PDepend确实输出了一些数字,如下面的示例输出所示:

PDepend 2.10.3
Parsing source files:
...............................................          47
Calculating Cyclomatic Complexity metrics:
.................                                        355
Calculating Node Loc metrics:
.............                                            279
Calculating NPath Complexity metrics:
.................                                        355
Calculating Inheritance metrics:
.....                                                    101

这里我们跳过了一些行。数字只会告诉你对于给定的文件夹,每个指标被计算了多少次,所以直接输出并不特别有帮助。要查看实际的指标,我们需要打开 XML 报告。在我们的例子中,生成的文件被命名为pdepend_summary.xml

由于 XML 报告太大,无法在本书中打印,所以你最好亲自尝试一下,看看它的全貌。然而,我们可以向你展示它的结构:

<?xml version="1.0" encoding="UTF-8"?>
<metrics>
  <files>
    <file name="/path/to/Namespace/Classname.php"/>
    <!-- ... -->
  </files>
  <package name="Namespace">
    <class name="Classname" fqname="Namespace\Classname">
      <file name="/path/to/Namespace/Classname.php"/>
      <method name="methodName"/>
      <!-- ... -->
    </class>
    <!-- ... -->
  </package>
</metrics>

<metrics>节点代表完整扫描的目录。它有以下子节点:

  • <files>,它使用<file>子节点列出所有扫描的文件。

  • <package>,列出了所有命名空间。在这个节点中,有进一步 <class> 子节点。对于每个类,都有一个 <method> 节点列表,每个类中有一个方法对应一个 <method> 节点。最后,类的文件名在另一个 <file> 节点中提到。

当然,这并不是 PDepend 将生成的所有输出。对于每个节点,它都会添加数十个属性,这些属性包含计算出的度量的名称和值。这是一个从 PDepend 本身源代码生成的 XML 报告的示例节点:

<method name="setConfigurationFile" start="80" end="89"
  ccn="2" ccn2="2" loc="10" cloc="0" eloc="8" lloc="3"
  ncloc="10" npath="2" hnt="15" hnd="21"
  hv="65.884761341681" hd="7.3125" hl="0.13675213675214"
  he="481.78231731105" ht="26.765684295058"
  hb="0.020485472371812" hi="9.0098818928795"
  mi="67.295865328327"/>

您应该能够识别一些度量,例如 lloc(LOC)或 ccn(CC Number)。对于其他度量,您可以在在线文档中的 XML 报告中找到解释,或者至少是缩写的长名称:pdepend.org/documentation/software-metrics/index.html

进一步选项

PDepend 有两个选项您应该了解:

  • --exclude:这将排除一个命名空间(或在此术语中为包)的扫描。您可以使用多个命名空间,用逗号分隔。确保在命名空间(们)周围加上引号:

$ vendor/bin/pdepend --summary-xml=pdepend_summary.xml src

  • --ignore:允许您忽略一个或多个文件夹。同样,不要忘记引号:

$ vendor/bin/pdepend --summary-xml=pdepend_summary.xml src

它还可以生成带有更多信息的 SVG 格式图像。尽管如此,我们在这本书中不会涉及它们,因为有一个更好的工具,您将在下一节中找到。

PDepend 功能强大,但同时也难以掌握。生成的输出难以阅读,一旦项目变得稍微大一些,除非您使用其他工具来解析 XML 文件,否则变得不可用。然而,您可能有一天需要它提供的先进度量,或者您可能在一个已经使用它的项目中工作。所以,至少您现在已经准备好了。

PhpMetrics

到目前为止,PHP 质量度量世界仅基于文本。现在将会有所改变,因为我们现在将查看 PhpMetrics,它将生成更适合人类眼睛且甚至具有交互性的报告。

安装和使用

让我们使用 Composer 将 PhpMetrics 添加到您的项目中:

$ composer require phpmetrics/phpmetrics --dev

在所有文件下载完毕后,您可以直接开始生成您的第一个报告:

$ vendor/bin/phpmetrics --report-html=phpmetrics_report src

--report-html 选项指定报告将被创建的文件夹。您可以通过提供以逗号分隔的列表来指定要扫描的多个文件夹。然而,在我们的例子中,我们只会使用 src 文件夹。

因此,PhpMetrics 将列出一些统计信息,这将让您对代码有一些了解。图 8.2 展示了输出的摘录,可能会让您想起 phploc 生成的输出:

图 8.2:PhpMetrics 控制台输出(摘录)

图 8.2:PhpMetrics 控制台输出(摘录)

要打开刚刚生成的实际 HTML 报告,只需在浏览器中打开该文件夹中的 index.html 文件。在我们更仔细地查看生成的报告之前,让我们先看看 PhpMetrics 还提供了哪些其他有用的选项:

  • --metrics:此选项将返回可用指标列表。它有助于解释像 mIwoC 这样的缩写。

  • --exclude:使用此选项,你可以指定一个或多个要排除的目录。

  • --report-[csv|json|summary-json|violations]:允许你以不同的报告格式保存结果,而不是 HTML——例如,--report-json

从命令行打开浏览器

如果你使用的是基于 Linux 的操作系统,例如 Ubuntu,你可以按以下方式快速从命令行打开 HTML 文件:

$ firefox phpmetrics_report/index.html

或者,查看以下内容:

$ chromium phpmetrics_report/index.html

理解报告

如果你第一次打开 PhpMetrics 报告,你会看到各种各样的信息。我们不会深入到每一个细节,但会向你展示我们认为开始时最有价值的报告部分。

为了更好地说明 PhpMetrics 的用法,我们随机选择了一个名为 thephpleague/container 的现有开源软件包作为代码库进行工作。它是一个优秀的 PSR-11 兼容的依赖注入容器,大小适中,非常适合作为示例。图 8.3 展示了我们为它生成的示例报告的概览页:

图 8.3:PhpMetrics 报告概览

图 8.3:PhpMetrics 报告概览

关键指标

在左侧,你可以找到菜单,可以访问报告的其他页面。页面的顶部填充了一些关键指标,其中最有趣的是:

  • 代码行数告诉你更多关于这个项目的大小。点击标签后,你将被发送到另一个页面,其中列出了所有类及其相关的尺寸指标,如 LOC。

  • 违规 显示了 PhpMetrics 发现的违规数量。再次点击标签,你将被发送到另一个页面,其中列出了类及其违规情况——例如,如果它们过于复杂(过于复杂的方法代码),有很高的错误概率(可能存在错误),或者使用了过多的其他类或其他依赖(过度依赖)。

  • 平均循环复杂度按类确切地告诉你它所说的内容。详细视图为你提供了关于类级别复杂性的更多信息。

其他框也提供了有趣的信息,但前面的那些已经足够让你快速查看代码中最有问题的地方。

维护性或复杂性

在关键指标下方,PhpMetrics显示了一个图表,以及其他内容,你肯定在第一次打开报告时已经注意到了:可维护性/复杂性图。它由项目每个命名空间的一个彩色圆圈组成,圆圈的大小代表类的 CC(复杂度)。圆圈越大,复杂性越高。颜色显示可维护性指数,从绿色(高)到红色(低)。

如果你将鼠标悬停在圆圈上,你可以看到这个圆圈代表的命名空间以及两个详细指标:

![Figure 8.4: The Maintainability / complexity graph with a popup

![img/Figure_8.04_B19050.jpg]

图 8.4:带有弹出窗口的可维护性/复杂性图

这个图对于快速把握整体代码质量非常有用——红色大圆圈越少,越好。这样,你可以轻松地看到代码中的问题部分。

对象关系

当你从左侧菜单中选择对象关系时,将出现显示每个命名空间之间关系的图表。将鼠标指针悬停在文本标签上会突出显示其关系。由于图表很大,我们无法在这本书中展示其全部美,但我们至少可以给出一个初步印象:

![Figure 8.5: An Object relations graph

![img/Figure_8.05_B19050.jpg]

图 8.5:对象关系图

耦合

类之间的耦合表示它们如何相互依赖。有两个主要指标:

  • 入耦合Ca)告诉你有多少个类依赖于这个类。依赖太多表明这个类对项目的重要性。

  • 出耦合Ce)给你一个关于一个类使用多少依赖的印象。这个值越高,类对其他类的依赖性就越大。

包导向指标

我们想要展示的最后一个是抽象度与不稳定性图。正如其名称所暗示的,它显示了包的抽象度与不稳定性之间的关系。它是由罗伯特·马丁引入的,基于他对面向对象指标的深入研究。图 8.6展示了示例:

![Figure 8.6: An Abstractness vs. Instability graph

![img/Figure_8.06_B19050.jpg]

图 8.6:抽象度与不稳定性图

但这两个术语在软件开发中的确切含义是什么?让我们看看以下定义:

  • 0(具体)到1(抽象)。

  • 0(稳定)到1(不稳定)。

马丁指出,稳定且因此高度独立于其他类的包也应该有高水平的A。反之,不稳定的包应由具体类组成。所以,从理论上讲,类的A抵消了它的I。这意味着理想情况下,A加上I应该是1A + I = 1)。这个等式也画出了从左上角到右下角的斜线。你应该努力使你的包接近这条线。

在实际报告中,你会在图表下方找到一个表格,其中更详细地列出了数值。如果你将鼠标指针悬停在圆圈上,会出现一个弹出窗口,告诉你该圆圈代表的类的名称,以及 A(第一位数字)和 I(第二位数字)。

其他信息

这标志着我们通过 PhpMetrics 的旅程结束。还有很多东西可以探索,例如,例如,ClassRank,其中使用了谷歌著名的 PageRank 算法来根据其重要性(即与其他代码部分的交互数量)对类进行排名。在这本书中我们无法涵盖所有内容——然而,到目前为止,你已经了解了许多指标。它的文档对你非常有帮助。你可以在每一页的右上角找到它的链接。

使用指标的优点和缺点

在本书的前两章中,你已经了解了许多工具和指标,它们的存在只是为了帮助你编写更好的软件。成百上千的软件工程师的知识、智慧和无数小时的努力可以在几分钟内添加到你的项目中。

另一方面,你可能已经感到完全被众多的可能性所压倒。你应该选择哪些工具?你未来应该关注哪些指标?

如果你已经有了这种感觉,请不要担心。我们不会让你在这片混乱中孤立无援,但在接下来的章节中,我们会帮助你找到一个适合你需求的设置。首先,让我们花时间看看使用代码质量指标的优点和缺点。

优点

首先,每个软件项目都是一项独特的工作。它根据某些情况增长,例如开发者的技能组合和当时可用的包或框架,但也受到外部因素的影响,例如截止日期,这些因素往往会对代码质量产生负面影响。

代码指标帮助你了解项目当前的状态。例如,如果你接管了一个前团队成员的项目,你想要知道等待你的是什么。通过了解代码质量,你可以立即调整你对未来票据的预估工作量,无论方向如何。

代码质量指标也有助于你了解代码需要改进的地方。重构代码是一项极好的训练,通过使用指标,你知道何时取得了成功。无论你是在自己的项目上工作,想要为开源项目做出贡献,还是在团队中工作,最终在报告中获得更多的绿灯总是一件令人愉快的事情。

如果你发现了一段代码迫切需要重构,有合理的理由,但你的项目经理不想让你这么做,你可以使用指标向他们展示情况有多么糟糕,以及这仅仅是你个人观点的判断。代码指标是客观的,并且(痛苦地)诚实。

最后,这些指标的一个重要用途是防止你一开始就编写出糟糕的代码。有时候,编写遵循所有这些规则的代码可能会有些烦恼,但请放心,最终这些努力会得到回报。

缺点

此前,我们提到过,截止日期可能会损害代码质量,因为它们让我们无法重构代码异味或添加更多测试。虽然这是真的,但我们必须意识到,一旦他们开始通过衡量代码质量来衡量,一些开发者会开始重构比必要的更多代码,因为他们得到了更好的指标作为奖励。这为什么会成为问题?

例如,想象一下,在你的当前项目中有一个类,它的可维护性指数低,NPath 复杂性高,仅通过观察,你就可以立即看出它有多糟糕。然而,随着时间的推移,它已经变得成熟,经常被修复,到了某个时候,它已经证明可以无 bug 地工作。现在,你的工具告诉你这个类质量不好。你应该仍然跳上去开始重构它吗?

当然,并没有明确的“是”或“否”。如前所述,如果你在业余时间编写代码,那么重构一个类以移除大部分代码异味是有意义的(而且也很有趣)。如果你在从事商业项目,也就是说,作为软件工程师谋生,你并不总是有足够的时间这样做。你需要解决 bug,这会让你的软件用户感到不快,而另一方面,还有需要实现的功能,用户正焦急地等待着。总的来说,满意的客户才是支付你账单的人。在开发速度和代码质量之间找到最佳平衡点从来都不容易——你只需要意识到有时你必须吞下苦果,暂时放弃糟糕的代码。

不要用指标来与同事竞争,或者更糟的是,去说前开发者的坏话,他们已经让你独自承担项目。请记住,每个人都尽其所能工作,基于他们的技能。没有人故意尝试编写糟糕的代码——通常情况下,这是由于开发者从未听说过清洁编码原则,或者他们承受了巨大的时间压力,不得不进行复制粘贴式编程来让他们的经理或客户满意。你的工作环境应该是一个尊重、乐于助人和宽容的地方,而不是竞争。

摘要

本章向您介绍了 PHP 世界中一些最常用的代码质量指标。此外,我们还向您展示了帮助你收集这些指标的工具。当然,还有许多我们在这本书中没有涵盖的工具,但你不必知道它们全部——你现在已经拥有了扎实的代码质量指标理解,这将有助于你在日常工作中。

代码质量工具和指标当然不是所有问题的万能药。一方面,它们对于提高你的代码质量非常有帮助。另一方面,你不应该把它们当作终极衡量标准。有许多成功的软件类型在事先不知道的情况下永远不会通过这些质量检查,例如 WordPress。不过,请确信,如果 WordPress 的创造者在事先知道的话,他们可能会采取不同的做法。

在下一章中,我们将离开理论领域。我们将学习如何将上一两章中介绍的工具组织到我们的项目中。每个项目都是独特的,因此我们将提供不同的版本以满足你的需求。

进一步阅读

  • dePHPend (dephpend.com/) 是一个可以为你绘制 PHP 代码 UML 图形并用于发现你架构中问题的工具。

第九章:组织 PHP 质量工具

在最后两个章节中,你学到了很多关于质量指标及其测量方法的知识。你肯定会在未来的工作环境中使用一些工具,并且这些工具如果无缝集成,你甚至不需要再考虑使用它们。

因此,在本章中,我们将向您展示如何以最有效和最有帮助的方式组织这些工具,以便在您的日常工作中使用。这包括以下主题:

  • 使用 Composer 安装代码质量工具

  • 将代码质量工具作为 phar 文件安装

  • 使用 PHAR 安装和验证环境Phive)管理 phar 文件

技术要求

如果你遵循了前两个章节中的示例,你不需要安装任何其他东西。如果没有,请回到那些章节,首先安装所有必要的工具。

所有代码示例都可以在我们的 GitHub 仓库中找到:github.com/PacktPublishing/Clean-Code-in-PHP

使用 Composer 安装代码质量工具

大多数 require()require_once()。如果包版本之间存在冲突,你必须自己解决这些问题。

Composer 通过解决这些问题极大地简化了这些工作。它引入了一个名为 require() 的中央仓库,仅用于导入 Composer 的自动加载器。

所有这些特性帮助 PHP 与其他网络语言(如 Python 或 Ruby)竞争,如果没有它,PHP 可能不再是今天在 万维网WWW)上最广泛使用的语言了。因此,我们想在本书中给 Composer 应得的篇幅。在本节中,我们将向您展示最常用的安装方法。此外,我们还将探讨在项目中使用 Composer 的另一种不太为人所知的方法。

使用 require-dev 安装代码质量工具

在过去的章节中,我们已经多次使用 Composer 安装工具,所以到现在,你应该已经熟悉了最常见的用例:将依赖项添加到你的项目中。依赖项是由其他开发者编写的代码包,可以快速集成到你的项目中。

回顾一下,这是通过使用 require 关键字和包名称来完成的。例如,如果你想添加 PhpMetrics,你可以通过运行以下命令来实现:

$ composer require phpmetrics/phpmetrics --dev

通常,包通过开发者(所谓的 vendor)的名称来识别,并且通过斜杠与包名称分隔。在上面的例子中,供应商和包名称是相同的,但这并不总是如此。

让我们更详细地看看 --dev 选项。当我们使用 composer require 命令并带上这个选项时,Composer 会将包添加到 composer.json 文件的另一个部分,称为 require-dev。在这里,你可以看到典型的 composer.json 文件的摘录:

{
  "name": "vendor/package",
  ...
  "require": {
    "doctrine/dbal": "².10",
    "monolog/monolog": "².2",
    ...
  },
  "require-dev": {
    "phpunit/phpunit": "⁹.5",
    "phpmetrics/phpmetrics": "².8",
    ...
  },
  ...
}

require-dev部分背后的想法是,这个部分中的所有包对于在生产环境中运行应用程序不是必需的。在本地环境或构建过程中,你肯定会需要 PHPUnit 和所有我们珍视的代码质量工具;在生产环境中,它们不再需要。

实际上,你应该努力在生产环境中使用尽可能少的包。这主要是两个原因,如下所述:

  1. 你添加的每个包都将包含在 Composer 的自动加载机制中,这会在每个请求上消耗性能。内部,Composer 构建了一个所谓的类映射,它是一个简单的数组,将类名映射到相应的文件位置。如果你对此感兴趣,可以查看例如vendor/composer/autoload_classmap.php文件。根据你的项目使用的包的数量,这个文件可能会变得非常大,从而减慢你的应用程序。

  2. 每增加一个包都可能引入安全问题。代码越少,攻击向量就越少。

默认情况下,Composer 将安装所有依赖项。因此,请确保使用--no-dev选项运行它,以排除require-dev中的包在生产构建中被安装。然而,在你的本地环境中,你在这个阶段不需要担心其他任何事情。

之前描述的安装方法是一个很好的起点,也是你遇到最多的方法,原因有很多:它不需要任何额外的工具,并且在生产环境中安装时只有一个额外的选项需要使用。这使得它成为一个完美的起点,对于小型项目来说通常已经足够。另一种值得了解的方法是 Composer 的全局安装,我们将在下一节中讨论。

全局安装

如果你正在本地系统上同时处理多个项目,你可以选择全局安装 Composer 和包,这意味着它们不会安装在任何项目的root文件夹中,因此也不会添加到任何composer.json文件中。相反,Composer 和包都将安装在一个单独的文件夹中,通常是~/.composer。在这个文件夹中,你将找到另一个composer.json文件,它跟踪全局安装的包,以及另一个vendor文件夹,其中安装了它们的代码。

全局安装包只需添加global修饰符,如下所示:

$ composer global require phpmetrics/phpmetrics

同样,更新所有全局包也非常简单,如下所示:

$ composer global update

在全局安装后,工具如PHP 编码标准修复器PHP-CS-Fixer)可以简单地执行,无需指定路径,如下所示:

$ php-cs-fixer fix src

然而,为了使这种方法生效,你需要将这个全局文件夹添加到执行路径中。请参阅 Composer 文档(getcomposer.org/),以获取有关如何为使用的操作系统执行此操作的更多详细信息。

只有在你单独工作在项目上且不使用任何构建管道的情况下,才应选择使用全局安装功能。如果你在一个团队中工作并且/或者使用 持续集成CI)管道,你应该为每个项目单独安装它。

根据如 Twelve-Factor App 原则(12factor.net)等常见最佳实践,所有依赖项都应明确声明,不应依赖任何全局依赖项,因为你永远无法确定将安装哪个版本。尽管代码质量工具包不是实际程序代码的一部分,但它们仍然是构建过程的一部分。安装版本之间的小差异可能导致不可预见的行为,并且在错误无法在本地重现时会产生混淆。

此外,你希望使项目的初始安装尽可能简单。让你的队友手动安装所有必需的工具是一个耗时且容易出错的过程,可能会导致挫败感。

由于上述原因,我们不鼓励使用全局安装方法。

Composer 脚本

一旦你决定了一种安装 Composer 的可能方法,并使用它下载了你想要的工具,你希望以最直接的方式开始使用它们。在第十一章“持续集成”,我们将讨论 CI,我们还将向你展示如何在构建过程中自动运行这些工具。然而,现在我们想向你展示 Composer 如何在需要时手动运行它们。

让我们考虑以下示例:作为第一步,我们希望运行 PHP-CS-Fixer 以自动修复 src 文件夹中的代码。之后,我们希望对代码运行 PHPStan,级别为 1。你当然可以单独运行这两个步骤,但我们希望增加一些便利,一次执行这两个工具。

为了实现这一点,我们可以利用 composer.json 文件中的 scripts 部分在项目根目录下。在那里,我们必须添加我们想要执行的工具体现在一个简洁的命令名下,例如 analyze。以下示例显示了这可能看起来像什么:

{
  ...
  "scripts": {
    "analyze": [
      "vendor/bin/php-cs-fixer fix src",
      "vendor/bin/phpstan analyse --level 1 src"
    ]
  }
}

我们在这里使用了 JavaScript 对象表示法JSON)数组表示法,将每个命令单独放在一行中,这使得它比在一行中写完所有内容更容易阅读和维护。

如果你想要分享这些 Composer 命令,你可能还想添加一段简短的描述文本,当你在执行 composer list 以查看可用命令时,这段文本会被显示。为此,你需要将 script-descriptions 部分添加到你的 composer.json 文件中。对于之前介绍的 analyze 命令,它可能看起来像这样:

{
  ...
    "scripts": {
        ...
    },
    "scripts-descriptions": {
        "analyze": "Perform code cleanup and analysis"
    }
}

通过在子目录中安装工具,我们发现了一种在不干扰我们的应用程序依赖关系的情况下组织我们的代码质量工具的合适方法。但是,如果出于任何原因,您在项目中没有使用 Composer,或者您不喜欢在您的存储库中有两个composer.json文件的事实?在下一节中,我们将介绍一种不使用 Composer 的替代方法。

将代码质量工具作为 phar 文件安装

Composer 并不是添加代码质量工具到您项目的唯一可能方式。在本节中,我们将向您展示如何将工具作为phar文件添加。

我们无需关心 Composer 或依赖关系,就能立即遇到phar文件。此外,phar文件被所有现代 PHP 版本支持。

这样使得phar文件的使用变得非常方便,您可以像处理二进制文件一样处理它们。通常,您可以直接将我们之前介绍给您的许多 PHP 工具作为phar文件下载,并将它们放置在您想要的任何目录中。然而,这些文件没有统一的提供方式,因此请参考每个工具的官方文档。

让我们看看如何为我们在第七章**, 代码质量工具中介绍的phploc工具这样做。根据其 GitHub 仓库,您可以直接从PHPUnit网站下载它,因为它们都来自同一作者。以下代码片段显示了您如何这样做:

$ wget https://phar.phpunit.de/phploc.phar -O phploc

注意,我们以phploc的名称安装工具,而不是phploc.phar-O选项允许您指定与下载的文件名不同的文件名。.phar扩展名不是执行工具所必需的,因此您可以节省一些输入努力。

Phar 和校验和

从互联网下载和执行文件始终存在它们可能被损坏和感染恶意代码的风险。这就是为什么工具的作者通常会生成下载的校验和(例如,通过如安全哈希算法 256SHA256)之类的哈希算法),并在他们的网站上发布它们,以便您可以使用它们来验证下载的完整性。请检查您打算使用的工具的官方网站,以了解它们是否提供校验和以及如何验证它们。

当然,您可以使用您喜欢的任何方法下载它,无论是使用curl还是通过浏览器。一旦下载,您就可以立即使用您本地的 PHP 安装运行它,如下所示:

$ php phploc src

如果您不想每次都输入php,您需要使phar文件可执行,例如在 Linux 上,它会看起来像这样:

$ chmod +x phploc 

之后,您只需运行以下命令来执行phploc

$ ./phploc src

保持您的 phar 文件组织有序

现在,我们不仅想要下载 phar 文件,还希望在我们项目中将它们组织起来,这样任何其他开发者在使用它们之前都不需要做任何手动工作。最明显的选择是将这些文件添加到你的仓库中,这正是我们现在要探讨的。在下面的示例中,我们将使用 Git,但这种方法也可以适用于任何其他 版本控制系统VCS)。

通常不建议在 Git 中存储大文件,因为它们可能会对性能产生负面影响。例如,GitHub 会阻止大于 100 个 phar 文件,我们使用的 phar 文件通常只有几兆大小,所以添加它们不应该有任何负面影响。

Git 大文件存储(Git LFS)

如果你需要在 Git 中存储大文件,请考虑使用 Git LFS,它正是为此类用途而设计的。但就我们的需求而言,我们不必使用它。

你可以自由选择在哪里添加 phar 文件到你的项目中。一个常见的地方是 root 文件夹;然而,由于随着时间的推移这会变得相当拥挤,我们建议使用一个单独的文件夹来存储它们。一个不错的选择是再次使用 tools 文件夹,就像我们在上一节中使用的那样。你不需要考虑其他任何事情;只需像添加任何其他文件一样将它们添加到仓库中。

假设你已将 phploc 文件复制到 tools 文件夹,并按照之前描述的方式使其可执行。然后,你只需按照以下方式执行:

$ tools/phploc src

使用 phar 文件既简单又不会干扰你的应用程序依赖。然而,它们并不完美:如果你想更新它们,你需要查找下载的 phar 文件,并且每次都要手动验证其校验和——针对每个工具。在下一节中,我们将向您展示如何通过引入另一个依赖管理工具:Phive 来简化这个过程。

使用 Phive 管理 phar 文件

在上一节中,我们学习了使用 phar 文件而不是使用 Composer 来安装我们的代码质量工具。这种方法很好,但如果你想要更新它们,它确实需要做一些额外的工作。

Phive 是一个可以接管额外工作的工具。让我们立即安装它。

自然地,Phive 本身也可以作为 phar 下载。以下命令将按 phive 的名称下载它并使其可执行:

$ wget https://github.com/phar-io/phive/releases/download/0.15.1/phive-0.15.1.phar -O phive
$ chmod +x phive

请注意,这种安装方法并不非常安全。请检查工具的网站(https://phar.io)了解如何安全安装以及如何使其全局可用。

为了演示目的,简单的下载就足够了。一旦文件下载并设置为可执行,你就可以直接开始使用 Phive 来安装第一个工具。让我们使用我们在上一章中介绍的 phploc 来演示它是如何工作的,如下所示:

$ ./phive install phploc

下载验证

Phive 不仅负责下载的安装,还负责验证。这是在安装过程中自动完成的。然而,这需要供应商提供校验和,这也是为什么不是所有工具都可以通过 Phive 管理的最主要原因。

如你之前所见,安装一个工具只需使用install命令。现在已经发生了以下四个步骤:

  1. Phive 下载了phploc的最新版本并验证了其校验和。

  2. phar文件被存储在一个共享文件夹中(通常位于你的家目录下,命名为.phive)。

  3. 然后,Phive 创建了一个指向该共享文件夹的符号链接。符号链接是文件系统中的一个引用,使得一个文件或目录可以出现在多个目录中,尽管它只存储在一个地方。默认情况下,这个符号链接存储在tools文件夹中,如果不存在,将会生成。

  4. 在你的项目根目录中创建了一个另一个.phive文件夹,用于存储有关已下载工具的信息。

符号链接在你的目录中看起来就像“真实”的可执行文件一样,而原始文件仍然只存储在一个位置。如果你不想使用符号链接,可以使用--copy选项安装文件副本。

安装后,执行phploc非常简单,正如我们在这里看到的:

$ tools/phploc src

Phive 提供了更多有用的命令。只需运行以下代码(不输入任何命令)即可获取它们的列表:

$ ./phive

在这里,我们介绍最重要的几个:

  • list——列出所有可以通过 Phive 管理的工具

  • update——如果可用,更新所有已安装的phar文件

  • selfupdate——更新phive可执行文件本身

  • outdated——告诉你哪些phar文件可以更新

  • status——列出所有已安装工具的概览

将 Phive 添加到你的项目中

如果你在一个团队中工作,你不仅想要在本地安装phar文件。Phive 在这里也为你提供了支持。将 Phive 正确添加到你的项目中需要以下两个步骤:

  1. 将项目根目录下的.phive文件夹添加到你的仓库中。其中的phars.xml文件包含所有必要的信息(例如composer.lock文件)。

  2. 确保工具文件夹不在版本控制之下(例如,通过使用.gitignore文件)。你明确不希望将phar文件本身添加到你的仓库中。

一旦完成,下次从仓库检出项目时,可以通过执行以下命令来安装工具:

$ ./phive install 

这个命令可以轻松集成到其他工作流程中——例如,作为composer.json文件中的附加post-install-cmd脚本。

这就是你需要了解的所有关于 Phive 的信息,以便开始使用它。像往常一样,我们建议你阅读官方文档,因为我们无法在这本书中涵盖它提供的所有功能。

概述

Composer 是当今 PHP 世界中不可或缺的一部分。通常情况下,向你的项目添加代码质量工具的方法是将它们添加到依赖项的 require-dev 部分,这在许多情况下都运行得很好。

然而,Composer 并非唯一的途径。因此,在本章中,我们介绍了另外两种管理你的代码质量工具的选项:通过手动将 phar 文件添加到你的项目中,或者通过利用 Phive 来管理 phar 文件。

你可能现在急于将所学到的所有知识应用到你的代码中。然而,不懈的重构可能会带来更多的伤害而不是好处,而且每次更改后都要点击应用程序的所有部分来检查是否有什么东西坏了,这会花费你很多时间,并且可能会非常令人沮丧。因此,在下一章中,我们将向你展示自动化测试如何在这里帮助你。

第十章:自动化测试

如果你从这本书的第一章开始就阅读了所有的章节,你不仅会有一个理论背景的概念,而且还会手头有一套很好的工具,这些工具将帮助你编写出色的PHP:超文本预处理器PHP)代码。当然,你也可以直接去重构现有的所有代码,可能使用我们工具提供的某些自动化代码操作功能。

你不可能第一次就写出完美的代码——通常需要多次迭代才能让你满意。而且由于你永远不会停止学习,你甚至几个月或几年后还会重构你的代码。然而,即使是最复杂的代码质量工具也无法阻止你不得不进行一项繁琐的任务:在你对代码进行更改后,确保它仍然按预期工作。这就是为什么在本章中,我们想向你介绍自动化测试

通过自动化测试,你将能够快速且可靠地验证你的代码改进没有破坏其功能。这是编写干净代码的一个基石,因为它使你能够有信心地重构代码。

自动化测试这个话题值得一本或两本书来讨论,所以我们只能触及表面。然而,既然我们确信你将在日常工作中从中受益匪浅,我们希望这一章能让你想要了解更多关于这个令人兴奋的话题。

以下几节将为你提供一个良好的概述:

  • 为什么你需要自动化测试

  • 自动化测试的类型

  • 关于代码覆盖率

技术要求

除了前几章的技术要求之外,你还需要安装Xdebug PHP 扩展。我们将在本章后面的相应部分,关于代码覆盖率,提供更多关于这个主题的信息。

本章的代码文件可以在以下位置找到:github.com/PacktPublishing/Clean-Code-in-PHP

为什么你需要自动化测试

尽管 PHP 的标准单元测试框架PHPUnit自 2006 年以来就已经存在,但时至今日,并非所有 PHP 项目都使用自动化测试。在这里,许多潜力都被浪费了,因为自动化测试具有许多好处,例如以下这些:

  • 速度和可靠性:想象一下,你需要反复执行相同的测试步骤。很快,你可能会犯错,或者在某些时候跳过测试。然而,自动化测试会以更快、更可靠的方式为你完成这些枯燥的工作——而且它们不会抱怨。

  • 文档:通过断言,您可以使用自动化测试间接记录代码的功能,这些断言解释了代码预期要做什么。与注释或维基百科中的文章相比,当某些内容发生重大变化时,您会立即通过失败的测试得到通知。我们将在第十三章“创建有效的文档”中再次讨论这个主题,当我们讨论创建有效的文档时。

  • 入职:一个覆盖我们代码的良好测试套件将帮助新开发者更快地在一个项目中变得高效。测试不仅作为额外的文档,还让开发者有信心进行更改或添加功能。他们可以在将更改部署到任何预发布或生产环境之前验证他们的更改不会破坏任何东西。

  • 持续集成/持续部署(CI/CD):无论是 CI 还是 CD,如果您的测试是自动化的,您就可以通过构建管道信任合并的代码没有错误,这使您能够更快地将代码推送到生产环境,从而更频繁地推送。我们将在下一章深入探讨这个主题。

  • 更好的代码:您不必严格遵循臭名昭著的测试驱动开发TDD)方法,就可以从开发中的测试中受益。编写可单元测试的代码甚至可以提高您的代码质量。为了能够独立测试代码(例如,在没有后台运行真实数据库的情况下),您需要考虑代码的分离。如果使用依赖注入DI)注入外部依赖项,它们比在类函数中实例化它们更容易用测试对象替换。我们将在第十二章“团队合作”中更详细地探讨DI 模式。此外,长而复杂的函数与短函数一样难以测试(例如,考虑一下我们在这里讨论的NPath 复杂性,它出现在第八章中),因此您很快就会开始编写更短的函数,以减少代码中的决策路径数量。

  • 更简单的重构:当您想根据我们在第七章中介绍过的静态代码分析器的结果重构项目时,自动化测试是无价之宝。您可以使用它们的建议或甚至自动化的代码修复,只需运行测试后,您就会知道这是否引入了任何副作用。由于重构是我们在这本书的上下文中最重要的用例,我们将在下一节中更详细地讨论它。

TDD

TDD 是一种同时编写测试和实际代码的编码方式。基本思想很简单,通常被称为红/绿/重构:在为新的功能或甚至是一个错误修复编写任何代码之前,第一步是编写一个检查预期结果的测试。由于你还没有编写任何实际代码,测试将失败(用红色表示)。在第二步中,你编写代码,不必过于关注使其完美,直到测试通过(绿色)。既然你现在已经有了工作的测试,你可以轻松地改进(重构)代码,直到你满意。

TDD 范式确保你的所有代码都将被测试覆盖,并且代码已经以完全可测试的方式编写。不过,不要过于认真:有时你只是想在没有明确目标的情况下进行实验——例如,当你玩一个新的应用程序编程接口API)时。在这种情况下,你不需要遵循 TDD。

带有测试的简化重构

如果你从绿色开始一个项目(也就是说,从头开始编写),你可以在开始编写代码时立即从代码质量工具中获得反馈的舒适感。这是一个很大的帮助,但即使是最好的工具也无法阻止你做出错误的决定,并编写你将来想要撤销的代码。

这对每个人来说都是常态,而且绝对不应该让你气馁。你每天都在学习新东西,随着你个人技能的发展,你的代码也会发展。如果你看看你一年前的代码,你可能会想立即重构它。

当然,不仅仅是你的技能,整个 PHP 生态系统也在不断进步。今天被认为是标准的东西,在以前可能根本不存在。新的包或语言特性不断被引入,你希望在项目中使用它们,而不是永远停留在旧的技术上。

因此,代码随着时间的推移而改变——这是完全正常的,我们作为开发者应该接受变化;我们代码的任何部分永远不会是最终的。我们将更改现有代码称为重构。关于重构有趣的部分是代码被更改了,但软件对用户来说看起来没有变化。所有的工作都在“引擎盖下”完成。例如,如果你更新了项目的框架到最新版本,而用户没有注意到任何直接的变化,那么你就做得很好。

重构有好处;否则,我们不会去做。如果做得正确,它可以带来——例如——性能改进、安全性提高,或者通常允许应用程序在云中可扩展。然而,重构往往带有负面含义。特别是管理者往往认为重构意味着仅仅因为网络开发世界中又有新的炒作,工程师们想要追随,宝贵的工时就被浪费了。

让我们坦诚地说:当然,这种情况也会发生。界限往往难以划分。例如,假设你的职责是维护一个老旧但运行完美的 PHP 应用程序,该应用程序使用单例模式进行对象实例化。如果你只是偶尔需要做些小改动,实际上没有必要将其重构为使用依赖注入。然而,如果你需要实施持续性的改动,比如添加新模块和测试,那么这样做可能是个不错的选择。

通常,你将不得不为你的重构工作提供合理的解释。这时,将代码的维护称为系统健康维护可能更有帮助。每个人对机器需要维护的事实都感到完全正常:部件需要更换,润滑剂必须更新,等等。然而,不知何故,我们的软件似乎应该永远工作。

在现在有了良好的重构理由之后,我们想要了解测试如何帮助我们。为了实现这一点,让我们在下一节中更详细地看看存在哪些不同的测试类型。

自动化测试类型

尽管单元测试可能是最广为人知的自动化测试类型,但其中还有更多值得探索的内容。在本节中,我们将介绍最常见(且最重要的)测试类型。一个著名的测试概念是测试金字塔,如下所示:

图 10.1:测试金字塔

图 10.1:测试金字塔

这个概念基本上展示了三种测试类型——即端到端测试(简称E2E 测试)、集成测试单元测试。我们将在接下来的章节中解释每种测试类型及其在测试金字塔中的位置。

单元测试

正如其名所示,单元测试是关于测试代码的小单元。最佳实践是为一个对象的功能编写一个测试;否则,测试将变得更大,更难以理解和维护。这也使得测试保持小巧,这也是为什么通常会有很多测试。根据项目规模的不同,拥有数百或数千个单元测试是完全正常的,因此保持它们尽可能快速执行非常重要。通常,它们不应该每个测试超过几微秒。

单元测试应在隔离状态下运行,这意味着在测试中,被测试的对象不会与任何其他外部服务交互,例如数据库或 API。这是通过模拟外部依赖项来实现的,在单元测试术语中称为模拟。简单来说,我们用模拟对象(或简称mocks)替换了外部对象——例如,在测试对象内部使用的服务或存储库。这些对象在单元测试的运行时模拟了它们所替代的依赖项的行为。这确保了测试不会因为——例如——数据库中某些数据(我们的测试所依赖的数据)发生了变化而突然失败。

由于这种类型的测试很小、速度快,且不依赖于外部依赖,因此为它们创建测试设置相对容易。它们非常有帮助,因为它们可以在几秒钟内告诉你你的代码的最后更改是否导致了任何问题。这就是为什么它们是测试金字塔的基础。

如果你刚开始接触测试,从 PHPUnit 开始是有意义的,因为它是 PHP 世界的行业标准。如果你开始一个新的项目,很可能 PHPUnit 将会被使用。还有其他具有独特优势的测试框架,例如 Pest (pestphp.com)。一旦你掌握了使用 PHPUnit 进行单元测试的概念,我们鼓励你也尝试一下它们。

单元测试的一个缺点是它们之间不交互。这甚至可能导致所有测试都通过,而你的应用程序却出了问题,仅仅是因为类之间的交互没有经过适当的测试。

为了说明这个问题,我们创建了一个基本的演示应用程序。让我们看看它的最重要的部分。

演示应用程序源代码

你可以在本书的 GitHub 仓库中找到完整的源代码:

https://github.com/PacktPublishing/Clean-Code-in-PHP

首先,我们创建一个基本的类,称为 MyApp,如下所示:

<?php
class MyApp
{
    public function __construct(
        private myRepository $myRepository
    ) {
    }
    public function run(): string
    {
        $dataArray = $this->myRepository->getData();
        return $dataArray['value_1'] .
          $dataArray['value_2'];
    }
}

MyRepository 方法通过构造函数注入。唯一的方法 run 使用仓库来获取数据并将其连接起来。需要注意的是,MyClass 期望 MyRepository 返回一个特定的数组结构。这并不推荐这样做,但你仍然会发现这在“野外”很常见。因此,它完美地作为一个演示。

MyRepository 看起来是这样的:

<?php
class MyRepository
{
    public function getData(): array
    {
        return [
            'value_1' => 'some data...',
            'value_2' => 'and some more data'
        ];
    }
}

在现实生活中,MyRepository 会从外部数据源,如数据库中获取数据。在我们的例子中,它返回一个硬编码的数组。如果 MyClassrun 方法被执行,它将返回一个 some data...and some more data 字符串。

当然,我们也为前面的类添加了测试(使用 PHPUnit)。为了简洁,我们只会在以下代码片段中展示测试案例,而不是整个测试类:

public function testRun(): void
{
    // Arrange
$repositoryMock = 
      $this->createMock(MyRepository::class);
    $repositoryMock
        ->expects($this->once())
        ->method('getData')
        ->willReturn([
            'value_1' => 'a',
            'value_2' => 'b'
        ]);
    // Act
    $appTest = new MyApp($repositoryMock);
    $result = $appTest->run();
    // Assert
    $this->assertEquals('ab', $result);
}
public function testGetDataReturnsAnArray(): void
{
    // Arrange
    $repositoryTest = new MyRepository();
    // Act
    $result = $repositoryTest->getData();
    // Assert
    $this->assertIsArray($result);
    $this->assertCount(2, $result);
}

安排-行动-断言(AAA)模式

你可能已经注意到,我们在两个测试案例中都添加了三行注释:ArrangeActAssert。我们这样做是为了演示编写单元测试最常用的模式:AAA 模式。即使你从未自己编写过任何单元测试,它也能帮助你理解它们是如何工作的。

首先,准备测试对象和所需的先决条件,如模拟对象(Arrange)。其次,执行被测试对象的实际工作(Act)。最后,我们确保测试结果符合我们的预期(Assert)。如果任何一个断言未满足,整个测试将失败。

这里有两点值得注意,如下所述:

  1. testRun()中,我们创建一个$repositoryMock模拟对象而不是使用实际的MyRepository方法。这是因为我们假设MyRepository通常会从外部数据源获取数据,我们不希望编写具有外部依赖的单元测试。

  2. testGetDataReturnsAnArray()并没有很好地测试仓库。我们只是检查结果是否为数组,并且它有两个条目,但没有检查返回的数组键。

现在,假设由于某种原因,一位开发人员决定value_1value_2数组键名太长,并将它们重命名为val1val2。如果我们现在运行我们的应用程序,它当然会崩溃,如下所示:

$ php index.php 
PHP Warning:  Undefined array key "value_1" in
  /home/curtis/clean-
  code/chapter10/unit_tests_fail/src/MyApp.php on line 18
PHP Warning:  Undefined array key "value_2" in
  /home/curtis/clean-
  code/chapter10/unit_tests_fail/src/MyApp.php on line 18

然而,如果你执行这些测试,它们仍然会通过,如下所示:

$ vendor/bin/phpunit tests
PHPUnit 9.5.20 #StandWithUkraine

..                                         2 / 2 (100%)

Time: 00:00.008, Memory: 6.00 MB

OK (2 tests, 4 assertions)

这说明拥有单元测试很重要,但这并不意味着我们不会再引入错误,因为它们可能存在缺陷或测试了错误的内容,就像我们的例子中那样。

经常情况下,像仓库这样的与外部系统交互的对象根本不会被测试,因为这需要更复杂的测试设置——例如,使用带有伪造数据的附加测试数据库。如果我们只是用一个模拟对象替换这样的对象,测试将正确执行。如果原始对象后来有重大变化,而模拟对象没有更新以反映这些变化,我们可能会陷入我们刚才描述的情况。

为了克服这个问题,我们需要一种方法来额外测试我们的类,而无需用模拟对象替换依赖。为此,我们将在下一节介绍测试金字塔的第二个测试类型——集成测试。

集成测试

我们接下来要查看的第二种测试类型是集成测试。与单元测试不同,单元测试不应该使用任何外部依赖,而在这个测试类型中,我们想要做相反的事情:我们想要测试代码的正常运行,而不用模拟对象替换任何内容。

你可能已经见证过使用测试数据库或某些外部 API 的单元测试套件。从技术上讲,这些测试不再是单元测试,而是集成测试(或者也称为功能测试)。理论上,我们也可以使用 PHPUnit 进行这些测试,或者使用特定的测试工具,这些工具为我们处理了很多基础工作。

以下代码片段展示了集成测试的一个示例:

public function productIsSaved(Tester $tester)
{
    $product = new Product();
    $product->setId(123);
    $product->setName('USB Coffee Maker');
    $product->save();

   $this->tester->seeInDatabase(
        'products',
        ['id' => 123, 'name' => 'USB Coffee Maker' ]
    );
}

前面的函数展示了如果我们使用了$tester,一个传递给测试的Helper对象,它提供了我们执行测试所需的功能——例如,数据库检查。在执行$product测试对象的save方法之后,我们使用这个Helper对象来验证我们预期写入数据库的数据实际上是否已经写入。

Codeception

Codeception (codeception.com) 结合了多种测试类型,如单元测试、集成测试,甚至端到端测试,在一个工具中。在底层,它基于现有的工具,如 PHPUnit。它为所有主要框架提供模块,因此可以很好地集成到大多数 PHP 项目中。

使用集成测试使得测试设置更加复杂,因为我们必须确保我们使用的所有外部依赖始终处于可靠状态。例如,如果你需要依赖于数据库中的某个特定用户,你必须确保它始终具有相同的数据,例如你测试的用户标识符(ID);否则,你的测试将会失败。这通常需要在每次测试运行之前创建一个新的测试数据库,以确保之前的测试运行留下的任何残留物不会干扰我们的测试。此外,我们需要运行数据库迁移,以确保测试数据库模式是最新的。最后,我们必须填充测试数据,这被称为播种(seeding)。

这种测试类型的主要缺点是执行速度。数据库事务速度慢(与使用模拟对象相比),并且我们需要在每次测试运行时准备测试数据库。集成测试也更容易出错,或者变得不可靠(不稳定),因为与其他依赖项的交互很快变得非常复杂:如果上一个测试以下一个测试未预料到的方式更改了数据库,你的测试运行将会失败,尽管代码没有变化。

例如,测试套件中添加了一个新的测试,用于检查一个类是否以某种方式更新数据集。由于这是一个集成测试,它将使用测试数据库进行操作并更改特定的数据集。然而,在执行此测试之后,更改后的数据仍然保留在测试数据库中。如果另一个在新的测试之后运行的测试依赖于之前的数据,它将会失败。尽管测试设置增加了复杂性,但集成测试确保了测试对象在应用程序上下文中的集成工作正常。这就是为什么它们应该是测试策略的一个组成部分,成为测试金字塔的第二层。

当涉及到测试仓库、模型或控制器时,你会找到大量的集成测试。然而,它们无法测试 PHP 和浏览器之间的交互。由于我们主要使用 PHP 来构建 Web 应用程序,这是一个我们不应该忘记的方面。幸运的是,测试金字塔中的最后一种测试类型正好解决了这个问题。

端到端测试

对于这种测试类型,我们将暂时离开 PHP 领域。使用端到端测试(E2E tests),我们想要确保从服务器到客户端(例如,浏览器)以及再次返回服务器的整个流程运行良好。我们基本上模拟一个坐在电脑前点击我们应用程序的用户。

要实现这一点,我们首先需要一个可重复的测试环境。就像集成测试一样,我们必须确保我们想要测试的应用程序始终处于相同的状态。这意味着我们需要确保在每次测试运行中都可用相同的集合数据(例如,博客文章或商店中的文章)。

其次,我们需要自动化用户与我们的测试环境之间的交互。这里事情变得有趣:我们不仅需要一个应用程序,还需要一个本地 Web 服务器和一个浏览器来运行它并模拟用户交互。Web 服务器增加了测试设置的复杂性,但通常不会成为障碍。对于用户交互,我们需要使用所谓的无头浏览器。这样的浏览器可以在不打开浏览器窗口的情况下与服务器交互。这是一个极其有用的功能,因为我们可以在命令行中使用它,而无需安装带有图形用户界面GUI)的完整操作系统,例如 Ubuntu 桌面或 Windows。这为我们节省了大量安装时间,并帮助我们不会进一步增加复杂性。

在撰写本文时,Google Chrome是首选选择,因为它不仅是当今最广泛使用的浏览器引擎,还提供了无头模式,换句话说,它可以像无头浏览器一样工作。使用现代框架如Cypress,自动化与我们的应用的用户交互变得轻而易举。把它想象成一个脚本,告诉浏览器打开哪个统一资源定位符URL),点击哪个按钮等等。以下是一个简化的 Cypress 测试示例:

describe('Application Login', function () {
    it('successfully logs in', function () {
        cy.visit('http://localhost:8000/login')
        cy.get('#username')
            .type('test@test.com')
        cy.get('#password')
            .type('supersecret')
        cy.get('#submit')
            .click()
        cy.url()
            .should('contain',
              'http://localhost:8000/home')
    })
})

Cypress

Cypress 测试框架(www.cypress.io/)使得编写端到端测试变得非常容易,因为它为您处理了无头浏览器的设置和通信。是的——测试将用 JavaScript 编写,但这不应该阻止你尝试一下。

cy对象代表测试者,它执行某些步骤。在先前的代码示例中,它首先打开一个虚构应用的登录页面,填写登录表单中的#username#password字段,并通过点击#submit按钮提交。作为最后一步,它检查登录是否成功以及测试者是否被转发到主页。所有这些操作都是在后台运行的实时浏览器中执行的。使用这项技术,我们可以编写测试套件,这些套件可以像人类一样逐字点击我们的应用。它们不仅测试 PHP 代码,还测试前端代码——例如,一个 JavaScript 错误会迅速中断测试。即使你自己无法修复错误,你仍然可以向团队中的前端工程师报告存在问题。

现代框架使得编写测试比使用旧技术,如 Selenium,要容易得多。事实上,现在它非常舒适,甚至不是开发者但拥有坚实的专业技术基础的人,如质量保证QA)工程师,也可以轻松编写他们自己的测试套件。这种方法从团队中减轻了压力,因为开发者需要编写的测试更少,QA 人员可以按照需要设置测试,而无需等待开发者。

当然,端到端测试有一些缺点,这也是为什么它们只是测试金字塔的第三层:测试环境更复杂,需要更多的工作来设置,尤其是在使用数据库或任何外部 API 的情况下。这种测试类型也是最快的,因为它除了前一种测试类型的设置外,还涉及到浏览器。最后,这些测试很容易出错,因为通常测试框架使用idclass属性,甚至文档对象模型DOM)选择器来在 DOM 中导航并找到要与之交互的元素。因此,DOM 上的微小变化可能会迅速破坏你的整个测试套件。

页面对象

如果你感兴趣于创建可维护的端到端测试,你应该检查页面对象的概念(www.martinfowler.com/bliki/PageObject.html)。

实践中的测试金字塔

通过单元测试、集成测试和端到端测试,你现在知道了三种最重要的测试类型,以及它们的优缺点。建议的方法是将单元测试作为巨大的基础,适量的集成测试,最后是一些端到端测试,这是一个好的起点。

然而,你不必始终严格遵循它,因为每个项目都是不同的:例如,如果你想开始测试一个尚未完全测试的应用程序,引入单元测试将需要大量的重构工作来使类可测试。这种重构在开始时可能会引入比解决的问题更多的错误。

在这种情况下,从良好的端到端测试覆盖率开始将更快、更安全。一旦应用程序的主要部分可以自动测试,你就可以安全地开始重构并引入单元测试和/或集成测试。如果你的应用程序因为必要的重构而崩溃,你的端到端测试会为你提供保障。

在本章结束之前,我们将列出一些更多的测试类型供你评估,如果你感兴趣的话。现在,本章中我们讨论的三个测试类型是最重要的,应该足以让你开始。

有一个重要的问题我们还没有真正涉及,那就是:你真正需要测试多少代码?我们将在下一节讨论这个问题。

关于代码覆盖率

现在我们已经探讨了不同的测试类型,你可能想立即开始编写测试。但在你把这本书收起来开始编码之前,让我们以这样一个问题结束本章:你应该测试多少代码?

部分答案在于代码覆盖率的概念,我们已经在第八章**代码质量指标中简要提到了,当我们讨论代码质量指标时。现在让我们更深入地了解一下。

理解代码覆盖率

代码覆盖率衡量的是被测试覆盖的代码比例。覆盖率越高,越好——如果有更多的测试,软件中包含的 bug 就越少,而且更难在不被发现的情况下引入新的 bug。更高的代码覆盖率也可能是更好的代码质量的指标——正如我们在本章前面的一个部分所讨论的,经过测试的代码必须以某种方式编写,这通常会导致更好的质量。

通常,覆盖率程度简单地用测试代码的百分比来表示——也就是说,从 0%(完全未测试)到 100%(完全代码覆盖率)。但我们如何衡量代码覆盖率?为此,我们将使用 PHPUnit,因为它可以为我们生成代码覆盖率报告。然而,它需要一个额外的 PHP 扩展来实现代码覆盖率功能。对于本章,我们决定使用Xdebug,这是标准的 PHP调试器性能分析器

设置 Xdebug

Xdebug 是一个 PHP 扩展,因此需要将其作为模块加载。由于其安装相对复杂,主要取决于你运行 PHP 的操作系统,请参阅xdebug.org上的官方文档,了解如何安装和配置它。互联网上也有大量的教程。

如果你重构了代码,你可能想知道这些更改的性能影响是什么。你的执行时间是否有所改善,或者变得更糟?使用所谓的性能分析器,你可以详细测量每个函数的执行时间,并查看瓶颈隐藏在哪里。

我们无法在我们的书中涵盖这个主题,但既然在本章的过程中我们已经使用了 Xdebug,你可能还想检查它的性能分析功能:xdebug.org/docs/profiler。其他提供更多便利的商业服务包括——例如——Tideways (tideways.com)或Blackfire (www.blackfire.io)。

Xdebug 替代方案

请注意,你可以使用其他扩展来完成这项工作,例如PCOV (github.com/krakjoe/pcov),如果你只想做代码覆盖率报告,它的性能会更好。然而,Xdebug 是一个极其有用的调试器,你应该了解它——如果你不了解,我们鼓励你查看一些关于它的教程。

如何生成代码覆盖率报告

为了演示如何创建代码覆盖率报告,我们将使用本章前一部分中的小演示应用程序。为了跟进这个例子,请从 GitHub 上检出它,运行 composer install,并确保你已经安装了 mode 设置为 coverage 的 Xdebug。

在我们开始生成报告之前,让我们看看 PHPUnit 提供哪些报告格式。它可以生成各种格式的报告,你可能现在不需要,例如 CloverCoberturaCrap4JPHPUnit XML 格式。然而,当你开始将 PHPUnit 与其他工具集成时,它们可能会变得更加相关。

然而,我们不想在这本书中这样做,所以我们只对两种最易访问的格式感兴趣:文本和 HTML。文本格式可以直接在命令行上打印,这在你想立即得到结果或集成 PHPUnit 到你的构建管道时非常有用,而 HTML 格式提供了更多信息。

对于我们的示例,我们希望将两种报告格式都写入项目根目录下名为 reports 的新文件夹中。虽然你可以使用许多 PHPUnit 运行时选项来生成它们,但我们希望使用 phpunit.xml 配置文件来定义每次测试运行时要生成的内容。以下代码片段显示了一个最小版本,为了可读性进行了简化。在我们的 GitHub 仓库中,你可以找到完整的 phpunit.xml 文件:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php">
    <testsuites>
        <testsuite name="default">
            <directory>tests</directory>
        </testsuite>
    </testsuites>
    <coverage>
        <include>
            <directory suffix=".php">src</directory>
        </include>
        <report>
            <html outputDirectory="reports/coverage" />
            <text outputFile="reports/coverage.txt" />
        </report>
    </coverage>
</phpunit>

除了基本的配置之外,这包括定义我们需要的 tests 文件夹以进行常规测试运行,我们还添加了 <coverage> 节点。这个节点包含两个子节点:<includes><report>。在使用 <includes> 节点时,指定用于收集代码覆盖率信息的目录和文件扩展名非常重要。否则,PHPUnit 不会生成任何报告,也不会对缺失的信息提出抱怨。这有时可能会相当令人困惑。

此外,我们还需要告诉 PHPUnit 将哪些报告写入何处。我们使用 <report> 节点来完成这项工作,正如你所看到的,我们指定了 HTML 和文本报告都应写入项目根目录下的 reports 文件夹中,如果它不存在,将会被创建。

PHPUnit 预期配置文件命名为 phpunit.xml 并位于项目根目录。如果已经完成,你可以通过运行以下命令快速执行报告的生成,无需任何其他选项或参数:

$ vendor/bin/phpunit

在执行前面的命令之后,你将在项目根目录下找到一个名为 reports 的文件夹。它应该包含两样东西:首先,一个包含文本格式报告的 coverage.txt 文件,其次,一个包含 HTML 报告的 coverage 文件夹。

代码覆盖率是昂贵的

使用 Xdebug 生成代码覆盖率报告将减慢你的测试套件的执行速度,因为 Xdebug 需要收集大量数据,而且它并不是为了性能而构建的。因此,我们建议仅在必要时启用 Xdebug 和报告生成,但在常规测试运行期间保持禁用。

文本报告虽然简短,但已经告诉你你的测试如何覆盖你的应用程序,如下面的截图所示:

图 10.2:文本代码覆盖率报告

图 10.2:文本代码覆盖率报告

要获取更多详细信息,请打开浏览器中的reports/coverage/index.html文件。它应该看起来像这样:

图 10.3:HTML 代码覆盖率报告

图 10.3:HTML 代码覆盖率报告

你可以在那里找到相同的文本报告信息,但可视化效果更好。此外,报告是交互式的。例如,如果你点击左侧的MyOtherClass.php链接,你将被带到该类的详细报告,如下面的截图所示:

图 10.4:HTML 代码覆盖率报告 – 类视图

图 10.4:HTML 代码覆盖率报告 – 类视图

这里有两点值得关注:首先,在函数和方法部分,你可能已经认出了我们在第八章**,代码质量指标中引入的 CRAP 指标。在这里,你终于可以看到它在实际中的应用。

其次,报告详细显示了在测试期间哪些行被访问过(绿色背景),哪些没有被访问过(红色背景)。如果有任何行完全无法访问(例如,在最后一个return语句之后的另一个语句),它将显示为死代码(黄色背景)。死代码可以安全地删除。

现在,你对项目的代码覆盖率有了很好的概述。如果文件以红色条形图显示,这意味着在测试运行期间根本没有执行,所以你可以在那里改进你的测试套件。

使用@covers 注解

代码覆盖率存在一个问题:它告诉你测试期间哪些代码已被执行,但这并不意味着执行的代码也已被测试(即使用断言)。这是 PHPUnit 无法自动确定的事情。这意味着即使你的代码覆盖率报告显示 100%并且到处都是绿色条形图,这也并不意味着你的代码得到了良好的测试。它只是在测试套件的运行过程中被执行了。

为了克服这个问题,建议在类级别使用@covers注解,如下所示:

/**
 * @covers MyRepository
 */
class MyRepositoryTest extends TestCase
{
    public function testGetDataReturnsAnArray(): void
    {
        // ...
    }
}

这提高了我们测试的准确性,因为我们通过使用 @covers 注解,明确声明了我们的测试要测试哪些代码。例如,假设我们要测试的类使用了一个外部服务。你只想测试这个类,而不是它所使用的服务,因此你只编写检查要测试的类的断言。然而,如果没有 @covers 注解,PHPUnit 仍然会将外部服务包含在代码覆盖率报告中,因为它是测试过程中执行的一部分。

你也可以在方法级别上使用 @covers;然而,如果你——例如——重构一个类并将方法提取到另一个类中,这可能会引起问题。如果你忘记调整这里方法级别的 @covers 注解,覆盖率报告将不再准确。

要强制使用 @covers 注解,请在 phpunit.xml 文件中使用 forceCoversAnnotation 选项。如果它设置为 true,则未使用注解的测试将被标记为有风险;它们不会失败,但在报告中会单独列出,作为需要改进的内容。这样,你的同事(以及你自己)就不会忘记使用它。

需要测试的内容

我们现在知道了如何获取有关我们代码测试程度的详细信息。那么,你现在是否应该追求完整的代码覆盖率?是否应该将 100% 作为目标?

如我们在本章前面部分对示例应用程序的测试中看到的,为一个类编写测试并不意味着你真的测试了它的每个方面。在这里,遗憾的是,即使测量代码覆盖率也无法帮助。然而,它可以帮助你识别那些没有测试任何内容的测试。特别是在测试用例中使用了大量模拟时,可能会发生只有模拟被测试,而没有“真实代码”的情况。考虑以下测试用例,这是一个有效的测试,将会通过:

public function testUselessTestCase(): void
{
    $repositoryMock = 
      $this->createMock(MyRepository::class);
    $repositoryMock
        ->method('getData')
        ->willReturn([
            'value_1' => 'a',
            'value_2' => 'b'
        ]);
    $this->assertEquals(
        [ 
           'value_1' => 'a',
           'value_2' => 'b'
        ],
        $repositoryMock->getData()
    );
}

这个例子虽然简化了,但它说明了代码覆盖率报告如何有助于我们,因为这个测试不会为我们的代码覆盖率比率增加任何已测试的行。不幸的是,目前还没有任何工具能告诉你哪些测试写得很好,哪些应该改进,甚至是有用的,就像我们的例子一样。

根据帕累托法则,目标是 80% 的代码覆盖率应该已经极大地改善了你的代码库,而且这可以通过合理的努力实现。将你的重点放在使你的应用程序特殊的那部分代码上——通常被称为业务逻辑。这是需要你大部分注意力的代码。

帕累托法则

帕累托法则指出,80% 的结果是通过 20% 的总努力实现的。剩下的 20% 的结果需要量化的最大工作量,占用了 80% 的总努力。

还有那些实际上并不需要测试的简单代码。一个常见的例子是测试获取器和设置器。如果这些方法包含进一步的逻辑,当然有测试它们的道理。但如果它们只是设置或返回属性值的简单函数,为它们编写测试就是浪费时间。尽管如此,如果你想要追求 100%的代码覆盖率,你仍然需要这样做。

其他例子包括配置文件、工厂或路由定义。使用端到端(E2E)或集成测试就足够了,这些测试确保应用程序总体上能正常工作。它们隐式地(即,不使用具体的断言)测试了所有粘合代码,这些代码是保持你的应用程序在一起的所有代码,但测试起来却很繁琐。

尤其是端到端(E2E)测试通常不计入代码覆盖率指标,因为技术上很难做到这一点。尽管如此,如果你有这些测试,它们将增加一个额外的测试覆盖率层,这是无法测量的。你不能吹嘘 100%的代码覆盖率,但你清楚所有不同的测试类型都在支持你,而这应该是我们的首要目标。

摘要

在本章中,我们讨论了为什么你应该使用自动化测试以及它是如何提高你的代码质量的。我们涵盖了主要的三个测试类型,即单元测试、集成测试和端到端(E2E)测试,包括它们的优缺点、潜在陷阱以及我们关于如何使用它们的建议。最后,你了解了代码覆盖率的概念,以及如何在你的项目中使用它。

结合前一章关于代码质量工具及其组织方式的知识,在下一章中,我们终于可以开始将这些工具组合成一个流程,帮助以结构化和可靠的方式运行所有这些工具——构建管道。

进一步阅读

本章中涵盖的测试类型远多于我们所能讨论的范围。如果你觉得自动化测试的世界像作者们一样迷人,你可能还想了解其他测试类型,例如以下这些:

  • 突变测试涉及对要测试的代码进行微小的更改(所谓的突变)。如果你的测试能够捕捉到这些突变,它们通常写得很好;否则,它们将让突变逃逸。Infection是目前 PHP 世界中这个测试类型最知名的工具体(infection.github.io)。

  • 视觉回归测试字面上比较测试期间生成的应用程序截图与原始截图,以捕捉层叠样式表CSS)中的问题。虽然这并不是直接与 PHP 相关,但如果你想保持你的 Web 项目的样式完美,这可能会对你很有趣。一个值得检查的好选择是BackstopJS(github.com/garris/BackstopJS)。

  • API 测试可以被视为端到端测试,但仅限于您应用程序可能提供的 API。由于测试基于超文本传输协议HTTP)请求,因此不需要无头浏览器,这使得设置更加简单。开始 API 测试的一个好选择是Codeceptionhttps://codeception.com)。

  • 行为驱动开发BDD)是一个非常有趣的方法,因为它关注利益相关者(例如,项目经理)、QA(如果有)和开发者之间的沟通。这是通过在名为Gherkin的语言中以特殊方式编写测试来实现的,这基本上使非技术人员能够编写测试套件。PHP 的 BDD 工具称为Behatgithub.com/Behat/Behat)。

第十一章:持续集成

您已经学习了关于编写干净的 PHP:超文本预处理器PHP)代码的理论,并且现在您知道必要的工具和指标,这些工具和指标帮助我们实现并保持高质量水平。然而,仍然缺少的是将这些技术集成到一个便于您日常工作的工作流程中。

在以下页面中,我们将详细阐述持续集成CI)并通过示例学习如何设置一个简单但有效的自动化工作流程。

此外,我们将向您展示如何在本地上设置一系列代码质量工具,以便它们以最支持您的方式工作,而无需手动运行它们。此外,我们还将告诉您一些关于如何将这些工作流程添加到现有项目中的最佳实践。

我们将涵盖的主要主题如下:

  • 为什么你需要 CI

  • 构建管道

  • 使用 GitHub Actions 构建管道

  • 您的本地管道——Git 钩子

  • 探索——将 CI 添加到现有软件中

  • 持续交付CD)的展望

技术要求

除了前几章的设置外,您还需要一个 GitHub 账户才能跟随所有示例。不过,这不会带来额外的费用,因为我们只使用免费计划。

我们在本章中将使用的示例应用程序可以从本书的 GitHub 仓库下载:github.com/PacktPublishing/Clean-Code-in-PHP/tree/main/ch11/example-application

为什么你需要 CI

编写软件是一个耗时且因此昂贵的流程。如果您为了乐趣开发软件,它“仅”会消耗您的休闲时间。如果您为一家公司工作(无论是作为承包商还是全职员工),时间就更加宝贵,因为您为此获得报酬。事实上,公司希望降低成本,因此他们不希望在一个功能上花费比必要的更多钱。

我们日常工作中的一大部分是修复缺陷。交付无缺陷的软件是所有开发者可能都希望实现的目标。我们并不是故意犯错误,但它们总会发生。然而,有一些方法可以降低错误成本。

错误的成本

一个错误相当昂贵,因为它对产品没有任何价值。因此,我们的目标是尽早捕捉这些错误——我们捕捉得越早,它们造成的成本就越少。以下截图展示了修复一个错误的成本如何在开发过程中出现得越晚而显著增加:

![图 11.1:根据检测时间估计修复一个错误的相对成本]

![img/Figure_11.1_B19050.jpg]

图 11.1:根据检测时间估计修复一个错误的相对成本

但是什么原因导致成本随时间大幅增加?为什么错误甚至要花钱?

在早期阶段,成本主要来自于解决问题所需的时间。如果通过更好的需求就能避免一个错误,例如,那么在手动测试期间发现它所需的努力就较少。如果在生产中发现了一个错误,那么很多人都会参与修复它:首先,客服人员需要确认客户报告的错误并将其转交给质量保证QA)工程师,该工程师会重现错误并编写适当的错误报告。

然后,这个工单会被分配给产品经理,他会在花时间重现和验证缺陷之后,将其计划到下一个迭代中。最终,这个工单会被分配给一位开发者,他将需要一些时间来重现和修复它。但事情还没有结束,因为错误修复可能需要另一位开发者的代码审查,并且在最终发布之前还需要产品经理或 QA 工程师的双重检查。

一旦“逃离”开发者的本地环境,所有这些开销都会显著提高缺陷的成本。此外,如果错误已经存在于生产环境中,它可能导致客户不再愿意使用该产品,因为他们对它不再满意。这被称为客户流失

即使你不在商业产品上工作,而是在例如开源项目上工作,这个概念也可以转化为时间或努力。一个错误会导致一个问题报告,你首先需要阅读和理解它,可能还会提出更多问题,并等待错误报告作者回复。如果你的软件错误太多,人们就会少用,你之前所有的努力可能在某个时候都白费了。

如何防止错误

幸运的是,我们现在有一整套工具箱在您身边,可以帮助我们在别人之前找到代码中的错误。我们只需要使用它——这本身就是一个问题,因为我们开发者通常是懒惰的人。

当然,你可以在每次部署之前手动运行所有工具。让我们假设你想要将一些代码部署到生产环境中。在将代码合并到main分支后,应执行以下步骤以确保不会将损坏的代码交付到生产环境中:

  1. 使用 PHP 代码检查器以确保代码的语法正确性

  2. 执行代码风格检查器和修复器以保持代码风格一致

  3. 使用静态代码分析查找潜在问题

  4. 执行所有自动化测试套件以确保你的代码仍然工作

  5. 为使用的代码质量指标创建报告

  6. 清理构建文件夹并创建要部署的代码存档

这是一个需要记住的相当长的清单。没有人会在更长的时间内不犯错误,所以,自然地,你会开始编写脚本以帮助你在一次操作中执行这些步骤。这已经是一个很好的改进,我们将在本章的后面也稍微利用一下它。

引入持续集成(CI)

在你的本地环境中运行上一节中所有步骤将花费一些时间,并且在检查运行期间,你几乎无法做其他任何事情,所以你必须等待它们完成。那么,为什么不把这个整个工作流程加载到另一个专用服务器上呢?

这正是 CI 所做的:它描述了将你应用程序的所有必要组件自动组合成一个可交付产品的过程,以便它可以部署到所需的环境。在这个过程中,自动检查将确保代码的整体质量。重要的是要记住,如果其中一个检查失败,整个构建将被视为失败。

有许多 CI 工具 可用,例如 Jenkins,它通常是自己托管的(也就是说,由你或你的团队或公司中的某人运营)。或者,你可以选择付费服务,如 GitHub Actions、GitLab CI、Bitbucket Pipelines 或 CircleCI。

你经常会看到缩写 CI/CD,我们也会在这本书中一直使用它。CD 代表 持续交付,这是一个我们将在本章末尾讨论的概念。现在,你不需要关心它。

设置这些工具之一听起来像是一项繁重的工作,但它也有一些显著的优点,例如以下这些:

  • 可扩展性:如果你在一个团队中工作,使用本地设置会很快引起问题。对构建过程的任何更改都需要在每个开发者的计算机上完成。尽管构建脚本将是你的存储库的一部分,但人们可能会忘记在部署之前从其中拉取最新更改,或者可能发生其他问题。

  • 速度:自动测试或静态代码分析是一项相当消耗资源的任务。尽管今天的计算机很强大,但它们必须执行许多并发任务,你不希望在你的本地系统上额外运行构建管道。CI/CD 服务器只做这项工作,而且它们通常做得很快。即使它们速度较慢,它们仍然会从你的本地系统卸载负载。

  • 非阻塞:你需要一个构建环境来运行你代码上的所有工具和检查。使用你的本地开发环境来做这件事将简单地阻塞它,直到构建完成,尤其是当你使用较慢的测试类型,如集成测试或端到端E2E)测试时。在你的本地系统上运行两个环境——一个用于开发,一个用于 CI/CD——是不推荐的,因为你很快就会陷入配置地狱(想想阻塞数据库或网络服务器端口)。

  • 监控:使用专门的 CI/CD 服务器将让你对谁部署了什么以及何时部署有一个全面的了解。想象一下,如果你的生产系统突然出现故障——使用 CI/CD 服务器,你可以立即看到最新的更改,并通过几个点击部署你应用程序的上一版本。此外,CI/CD 工具会让你保持最新状态,并通过电子邮件或你喜欢的消息应用等方式通知你任何构建和部署活动。

  • 处理:一个手写的部署脚本当然可以完成工作,但让它变得像现代 CI/CD 解决方案一样舒适和灵活需要花费很多时间。此外,如果你遵循业务标准,那么你的团队中的其他开发者很可能已经对它有经验。

前面的几点可能让你对使用 CI 将会受益多少有一个概念。每个 CI/CD 工具的一个基本组成部分是所谓的构建管道,我们将在下一节中详细解释。

构建管道

在上一节中,我们列出了许多必要的步骤,以便使我们的代码准备好被部署到生产环境中。在 CI 的背景下,这些步骤的组合就是我们所说的构建管道:它接收输入(在我们的案例中,是所有应用程序代码),通过几个工具运行它,并从中创建所谓的构建工件。它们是构建的结果——通常,这包括可交付成果(一个准备移动到目标环境的应用程序代码包),以及额外的数据,如构建日志、报告等。

下面的图表为你提供了一个典型的构建管道的概要图。由于它不是在本地环境中执行的,因此需要额外的两个步骤:创建构建环境构建应用程序

图 11.2:CI 管道架构图

图 11.2:CI 管道架构图

管道中的其他语言

在这本书中,我们只会查看与管道相关的 PHP 部分,但一个现代的 Web 应用程序不仅仅由 PHP 组成。特别是对于面向前端的代码,还有另一个完整的工具集需要成为管道的一部分。然而,最终的过程是非常相似的。

在接下来的几节中,我们将更详细地介绍每个构建阶段。一开始我们将保持理论性,然后在本章的后面部分给出技术实现的例子。

第 1 阶段:构建项目

CI 管道需要一个专门的应用程序构建实例,我们可以在其中运行所有工具和检查,使其隔离。这可以大致分为两个步骤:创建构建环境和运行必要的构建工具。

创建构建环境

要在本地开发系统之外构建应用程序,我们首先需要创建一个构建环境。具体如何提供环境取决于所使用的 CI/CD 工具。这可以是一个为每个项目提供独立工作空间的专用服务器,或者是一个完全容器化的 Docker 环境,每次需要时都会启动,并且只持续构建的持续时间。

一旦存在构建环境,我们需要在那里下载所有源代码,但现在不要下载外部包或其他依赖项。很可能是你的代码存储在 Git 仓库中,并托管在私有 Git 服务器上或商业服务上。下载仓库特定分支的副本称为检出

我们必须注意代码是从仓库的哪个分支检出的。这取决于你想要构建什么。如果你打算检查main分支的代码。

主分支与 master 分支

在计算机的历史中,术语masterslave被广泛使用,无论是硬盘配置、数据库复制,还是当然——Git。然而,这些术语对许多人来说是有害的,所以现在的main分支简单地称为main而不是master。你仍然会找到使用旧分支名称的仓库。然而,在这本书中,我们将坚持使用新的术语。

然而,如果你的项目不是使用 Git 托管,不要担心——这一步仍然是必要的,因为我们需要在 CI/CD 服务器上获取代码。无论是通过 Git、Mercurial、SubversionSVN)还是甚至直接文件下载,最终并不重要。这一步骤的结果是我们想要的代码在 CI/CD 服务器上准备好,以便我们可以开始安装依赖项。

构建应用程序

构建应用程序类似于在新系统上安装它。在上一个步骤中,我们确保源代码在环境中可用。在这个步骤中,我们需要执行任何必要的构建步骤。这通常包括以下内容:

  • 安装外部依赖项:你的仓库应只包含你自己的代码,不包含外部依赖项。我们通过 Composer 或PHAR 安装和验证环境Phive)等来管理这些依赖项。

  • 在这个阶段,例如.env文件。

  • 准备测试数据库:为了运行集成或端到端测试,构建实例需要一个可工作的数据库。通常,这是通过创建测试数据库、导入数据库模式、运行任何额外的数据库迁移,并最终用测试数据填充数据库来完成的。

为了减少构建时间,许多现代 CI/CD 工具提供了缓存功能。如果已启用,它们将在第一次下载后将依赖项保存在临时存储中。如果不默认启用,通常是一个好主意将其打开。

第二阶段——代码分析

我们在第七章**,代码质量工具中详细介绍了代码质量工具。现在,是时候将这些工具添加到我们的流程中,以确保它们会在引入的每个更改时执行。

PHP 代码检查器

如果你将代码合并到另一个分支中,代码总是可能断裂。Git 有非常复杂的合并算法,但仍然不能做魔法。考虑到你有一个高覆盖率的测试套件,如果合并导致了语法错误,一些测试肯定会失败。那么,为什么我们要运行这个额外的步骤呢?我们推荐这样做,因为 PHP 代码检查器有两个优点:它运行得非常快,并且会检查所有 PHP 文件,无论是否有测试。

我们希望我们的管道在检测到任何问题后快速失败。因此,在执行任何长时间运行的任务之前,在开始时运行一个快速的语法检查是有意义的。无论如何,它们都会中断,你也会浪费一些宝贵的时间。作为一个经验法则,检查运行得越快,它就会越早出现在管道中。

代码风格检查器

在检查代码语法之后,是时候检查代码风格了。这个操作也很快,所以在管道的早期运行它是有意义的。在我们的例子中,我们将使用PHP 编码标准修复器PHP-CS-Fixer),这是我们已经在第七章**,代码质量工具中介绍过的。

在本地运行 PHP-CS-Fixer 和在 CI/CD 管道中运行之间有一个微妙但重要的区别:对于后者,我们只会用它来检查代码,但不会修复它。我们不希望管道更改我们的代码,只是分析它。换句话说,我们的管道只会检查代码是否正确格式化(根据我们定义的规则),但不会尝试自动修复;如果违反了任何规则,构建就会失败。

没有规则说 CI/CD 管道不应该更改代码。然而,在过程中自动提交更改到仓库会增加复杂性。此外,它需要一个经过充分测试的应用程序,并且你需要信任你选择的工具不会破坏任何东西。通常,它们工作得很好,但你愿意冒险吗?

在您的本地环境中,同时运行修复器和代码风格检查器是有意义的。我们将在本章的下一节中讨论本地设置。

静态代码分析

在这个阶段,我们知道我们的代码在语法上是正确的,并且按照我们的规则进行了格式化。这两个之前的检查通常都很快,所以如果发生了那些容易检测到的问题,我们的构建过程会在早期失败。

现在,是时候运行较慢的任务了。静态代码分析通常比前两个阶段花费的时间要长一些,但它远远没有运行自动测试那么慢。本质上,这一步与代码检查和代码风格检查没有太大区别:如果我们之前定义的规则被违反,构建将失败。

如果你正在将 CI 引入现有项目,挑战在于找到错误报告的最佳平衡点。一方面,你希望让开发者满意,不要强迫他们在每次触摸文件时修复其他开发者引入的数十个问题。另一方面,你需要设置一个足够严格的阈值,以确保每次代码更改至少进行一些重构。

很遗憾,这里没有金科玉律,你需要对设置进行实验。在稍后,当大多数静态代码分析报告的问题都解决后,你需要稍微收紧错误报告规则,这样你的项目就不会在某一个水平上停滞不前。

第 3 阶段 – 测试

当我们的代码达到管道的这个阶段时,我们确信它是语法正确的,遵循我们的代码风格指南,并且根据我们的静态代码分析规则没有一般性的缺陷。因此,我们现在将运行管道中的这一步,这通常需要最长时间:自动测试。

正如我们在上一章中介绍的,除了单元测试之外,还有更多需要考虑的。通常,像网络服务这样的项目至少有一些集成测试来确保整个服务运行良好,包括数据库事务。或者,如果你的项目是一个传统的网络应用程序,你可能有一个端到端测试套件,它使用浏览器来模拟点击。

我们希望在这里应用与构建步骤相同的方法:从快速运行的测试开始,然后继续进行较慢的测试。如果单元测试已经失败,你不需要等待端到端测试的结果。因此,对于测试,执行顺序通常是这样的:

  1. 单元测试

  2. 集成测试

  3. 端到端测试

如果任何类型的测试失败,构建将被标记为失败。如果它们都通过,我们已经通过了管道中最关键的部分。我们的应用程序准备就绪,可以部署了,现在是时候清理和准备交付物了。

第 4 阶段 – 部署

我们已经通过各种工具彻底检查了我们的代码,并且我们确信它符合我们的标准。我们现在可以准备构建工件,并最终部署应用程序。让我们看看为此需要做些什么。

收集数据

在之前的阶段,我们使用的所有工具都产生了一些数据,无论是通过写入标准输出stdout),还是如果你配置了,通过创建总结执行操作的报告。

例如,您可以上传生成的报告到专用存储或将其推送到您的仓库。或者,您可以使用 PHPUnit 的代码覆盖率报告并自动从它创建一个代码覆盖率徽章,您可以将它添加到 GitHub 项目的 README 中。

尽管如此,最重要的用例是调试任何阶段是否失败。在出现问题时,您永远都不可能有足够的调试输出,因此将工具的详细程度设置为较高级别是一个好主意。您的 CI/CD 工具通常会确保在构建管道执行后,所有写入到 stdout 的内容都可用。

清理

在我们将应用程序上传到某个地方之前,我们想要确保它不包含任何不必要的冗余。这包括删除前一个阶段的日志或报告,或者删除代码质量工具。记住,我们只应该部署运行应用程序所必需的代码——例如 PHPUnit 这样的开发工具并不是以安全性为设计目标的(phpunit.readthedocs.io/en/9.5/installation.html#webserver)。

部署

要将代码部署到目标环境,我们需要将其打包成一个可以轻松移动的资产。这个资产也被称为交付物。我们选择的交付物类型取决于应用程序如何部署到生产环境。这种交付物的常见类型就是需要部署的代码的存档。

例如,如果您的生产环境运行在传统的本地 Web 服务器上,我们需要创建应用程序代码的存档,将其上传到目标服务器,并从那里提取它。

当前的既定标准是使用 Docker 的容器化环境。一旦构建实例经过彻底测试,就可以从它创建一个Docker 镜像。然后,这个镜像将被上传到镜像仓库,例如Amazon Web Services Elastic Container RepositoryAWS ECR)。这样的镜像仓库托管了所有您的镜像,因此当需要时,可以使用它们启动新的容器。这种方法为今天我们所拥有的高度可扩展的 Web 应用铺平了道路,因此如果您的应用程序在某个时刻需要扩展,从一开始就设计应用程序为可 Docker 化的将会带来回报。

容器和镜像

如果您是 Docker 的新手,容器和镜像的概念可能会令人困惑。简而言之,镜像包含所有数据,但它是只读的,不能单独使用。为了使其在 Docker 环境中可用,它需要一个包含镜像所有信息的容器,并提供连接到您设置中其他容器的所需功能。由于您可以从一个镜像创建所需数量的容器,您也可以将其视为一个容器模板。

如果你想了解更多关于 Docker 的信息,我们建议你查看官方文档docs.docker.com/get-started/overview。互联网上还有大量的教程、在线课程和其他信息可以找到。

我们现在对构建管道可能的样子有了很好的了解。在我们开始设置示例管道之前,还有一件事需要明确——它将在何时执行?

将管道集成到你的工作流程中

在设置所有必要的步骤之后,我们最终需要将管道集成到你的工作流程中。CI/CD 工具通常会提供不同的选项来决定何时执行管道。最初,当然可以通过点击按钮手动完成。但这并不方便。如果你使用 GitHub、GitLab 或 Bitbucket 等托管 Git 仓库,你可以将它们与你的构建管道连接起来,并在创建 PR 或分支合并到 main 分支时开始构建。

对于需要数小时构建的大型项目,在夜间(所谓的是夜间构建)运行当前代码库的构建也很常见。开发者将在第二天从管道中获得反馈。

运行构建需要一些时间,当然,开发者不应该坐在屏幕前等待,直到他们可以继续工作。他们更应该在被构建成功或失败时立即得到通知。如今,所有 CI/CD 工具都提供了多种方式来通知开发者,主要是通过电子邮件和在 Slack 或 Microsoft Teams 等聊天工具中的消息。此外,它们通常还提供仪表板视图,你可以在一个屏幕上看到所有构建的状态。

你现在应该对你的项目构建管道可能的样子有了很好的了解。因此,现在是时候向你展示一个实际示例了。

使用 GitHub Actions 构建管道

在了解了 CI 的所有阶段之后,是时候进行实践了。本书的范围不包括介绍一个或多个 CI/CD 工具的所有功能;然而,我们仍然想向你展示设置一个工作构建管道是多么容易。为了尽可能降低你的入门门槛并避免任何费用,我们决定使用 GitHub Actions

GitHub Actions 不是一个像 Jenkins 或 CircleCI 这样的经典 CI 工具,而是一种围绕 GitHub 仓库构建工作流程的方式。只要有点创意,你就可以做比“仅仅”一个经典的 CI/CD 管道更多的事情。当然,我们只会关注这个方面。

你可能已经有了 GitHub 账户,如果没有,注册一个也不会花费你任何费用。截至写作时,你可以免费使用 GitHub Actions,每月最多 2,000 分钟,用于公共仓库,这使得它成为一个极好的游乐场或开源项目的有用工具。

示例项目

我们创建了一个小型的演示应用程序,在本章中使用。您可以在以下位置找到它:github.com/PacktPublishing/Clean-Code-in-PHP/tree/main/ch11/example-application。请注意,它没有其他用途,只是演示 GitHub Actions 和 Git hooks 的基本使用。

GitHub Actions 简而言之

GitHub Actions 提供了没有花哨用户界面的配置方式,您可以通过存储在存储库中的 YAML Ain’t Markup Language (YAML) 文件来配置所有阶段。作为一个 PHP 开发者,您可能已经熟悉了使用 YAML 文件进行各种配置——如果不熟悉,请不要担心,因为它们很容易理解和使用。

GitHub 动作是围绕工作流程组织的。当某些事件发生时,工作流程会被触发,并包含一个或多个需要执行的工作。一个工作由一个或多个执行单个操作的步骤组成。

文件必须存储在存储库的 .github/workflows 文件夹中。让我们看看 ci.yml 文件的最初几行,它将成为我们的 CI 工作流程:

name: Continuous Integration
on:
  workflow_dispatch:
  push:
    branches:
      - main
  pull_request:
jobs:
  pipeline:
    runs-on: ubuntu-latest
    steps:
      - name: ...
        uses: ...

已经有了相当多的信息。让我们逐行过一遍:

  • name 定义了工作流程在 GitHub 中的标签,可以是任何字符串

  • on 指定了哪些事件应该触发此工作流程;这些包括以下内容:

    • workflow_dispatch 允许我们从 GitHub 网站手动触发工作流程,这对于创建和测试工作流程来说非常棒。否则,我们每次都需要向 main 分支推送提交或创建 PR。

    • push 告诉 GitHub 在发生推送时执行此工作流程。我们将其限制为仅在 main 分支上的推送。

    • pull_request 会在每个新的 PR 上额外触发工作流程。配置可能看起来有点不完整,因为冒号后面没有更多信息。

  • jobs 包含了此工作流程要执行的工作列表,具体如下:

    • pipeline_) 或破折号 (-)。

    • runs-on 告诉 GitHub 使用最新的 Ubuntu 版本作为此工作的运行者(即平台)。其他可用的平台是 Windows 和 macOS。

    • steps 标记了执行此工作所需的一系列步骤。在下一节中,我们将更详细地探讨这一点。

现在我们已经配置了工作流程的基本设置,因此我们可以开始添加构建阶段。

第 1 阶段 – 构建项目

步骤是使 GitHub Actions 变得如此强大的原因:在这里,您可以从大量已存在的 actions 中选择用于您的工作流程。它们组织在 GitHub Marketplace (github.com/marketplace) 中。让我们按照以下方式将一些步骤添加到工作流程 YAML 中:

steps:
  ###################
  # Stage 1 - Build #
  ###################
  - name: Checkout latest revision
    uses: actions/checkout@v3
  - name: Install PHP
    uses: shivammathur/setup-php@v2
    with:
      php-version: '8.1'
      coverage: pcov

由 GitHub 维护的动作可以在actions命名空间中找到。在我们的例子中,这是actions/checkout,它用于检出仓库。目前我们不需要指定任何参数,因为这个动作将自动使用此工作流程文件所在的仓库。

@V3注解用于指定要使用的主要版本。对于actions/checkout,这将是指版本3。请注意,总是使用最新的次要版本,在撰写本文时将是版本3.0.2

另一个动作shivammathur/setup-php是由许多提供开源工作的优秀人士之一提供的。对于这一步,我们使用with关键字来指定进一步参数。在这个例子中,我们使用php-version选项来在之前选定的Ubuntu机器上安装 PHP 8.1。使用coverage参数,我们可以告诉setup-php启用pcov扩展以生成代码覆盖率报告。

动作参数

之前介绍的两个动作提供的参数比我们在这里能描述的要多得多。您可以通过在Marketplace中查找它们来获取更多关于它们功能的信息。

关于格式化,我们使用注释和步骤之间的空白行来使文件更易于阅读。没有约定,完全取决于您如何格式化您后面的 YAML 文件。

下一步是安装项目依赖项。对于 PHP 来说,这通常意味着运行composer install。请注意,我们使用--no-dev选项,因为我们需要安装dev依赖项来执行所有质量检查。我们将在管道末尾再次删除它们。

依赖项管理

我们详细使用--no-dev选项。

这就是下一步可能的样子:

- name: Get composer cache directory
  id: composer-cache
  run: echo "::set-output name=dir::$(composer config cache
    files-dir)"
- name: Cache dependencies
  uses: actions/cache@v2
  with:
    path: ${{ steps.composer-cache.outputs.dir }}
    key: ${{ runner.os }}-composer-${{
      hashFiles('**/composer.lock') }}
    restore-keys: ${{ runner.os }}-composer-
- name: Install composer dependencies
  run: composer install

GitHub 动作需要一些手动工作来使 Composer 依赖项的缓存成为可能。在第一步中,我们将从 Composer 使用config cache-files-dir命令获取的 Composer 缓存目录的位置存储在一个名为dir的输出变量中。注意这里id: composer-cache——我们将在下一步中需要这个来引用变量。

然后,我们在下一步通过使用steps.composer-cache.outputs.dir引用(这是我们在上一步设置的id值和变量名的组合)来访问这个变量,以定义应该由actions/cache动作缓存的目录。keyrestore-key用于生成唯一的缓存键——即存储我们的 Composer 依赖项的缓存条目。

最后,我们使用run参数直接执行composer install,就像我们会在 Ubuntu 机器上本地执行它一样。这一点很重要:您可以使用,但不必在每一步都使用现有的 GitHub 动作——您也可以执行纯 shell 命令(或在 Windows 运行器上的等效命令)。

Marketplace中也有一些操作会接管命令的编写,例如php-actions/composer。我们在这里没有首选解决方案;两者都可以正常工作。

因为我们想在示例应用的 API 上运行集成测试,我们需要有一个正在运行的 Web 服务器。对于我们的简单用例,使用 PHP 内置的 Web 服务器就完全足够了,我们可以在以下步骤中开始使用它:

- name: Start PHP built-in webserver
  run: php -S localhost:8000 -t public &

-S选项告诉 PHP 二进制文件启动一个监听在localhost地址和端口8000的 Web 服务器。由于我们从项目的根目录开始,我们需要使用-t选项定义一个文档根文件夹(Web 服务器查找要执行文件的文件夹)。在这里,我们想要使用公共文件夹,它只包含index.php文件。将任何其他代码存储在文档根文件夹中都不是一个好的做法,因为这会使攻击者更难攻击我们的应用程序。

PHP 内置 Web 服务器

请注意,PHP 的内置 Web 服务器仅应用于开发目的。它绝不应该在生产环境中使用,因为它并不是为了性能或安全性而构建的。

你肯定注意到了命令末尾的&符号。这告诉 Linux 执行命令,但不要等待其终止。没有它,我们的工作流程会在这一点上卡住,因为 Web 服务器不会自行终止,因为它需要继续监听请求,直到我们在稍后的阶段运行我们的集成测试

我们的构建环境设置已完成。现在,是时候在我们的示例应用程序上运行第一次代码质量检查了。

第二阶段 – 代码分析

在第一个构建阶段,我们创建了我们的构建环境并检出我们的应用程序代码。在这个阶段,应用程序应该完全功能正常,并准备好进行测试。现在,我们想要进行一些静态代码分析。

标准方法是为每个工具使用专门的 GitHub 动作。好处是我们将开发工具与构建环境分开,因为它们将在单独的 Docker 容器中执行,并在使用后立即丢弃。尽管如此,这种方法也有一些缺点。

首先,每个动作都会引入另一个依赖项,我们依赖于作者保持其更新,并在一段时间后不会失去维护它的兴趣。此外,我们增加了一些开销,因为 Docker 镜像通常比实际工具大得多。最后,当我们的应用程序设置变得更加复杂时,在单独的 Docker 容器中运行代码质量工具可能会引起问题,仅仅是因为它不是与构建环境相同的环境。有时,已经非常小的差异也可能导致问题,这些问题可能会让你花费数小时或数天来解决。

正如我们在上一节中看到的,我们可以在构建环境中简单地执行 Linux shell 命令,所以没有理由在我们的构建环境中直接执行代码质量工具——我们只需要确保之后删除它们,以免它们被发布到生产环境中。

在我们的示例应用程序中,我们将 PHP-CS-Fixer 和 PHPStan 添加到了composer.json文件的require-dev部分。通过将以下代码行添加到我们的工作流程 YAML 中,我们将让它们作为下一步执行:

###########################
# Stage 2 - Code Analysis #
###########################
- name: Code Style Fixer
  run: vendor/bin/php-cs-fixer fix --dry-run
- name: Static Code Analysis
  run: vendor/bin/phpstan

在这里我们不需要很多参数或选项,因为我们的示例应用程序提供了.php-cs-fixer.dist.phpphpstan.neon配置文件,这两个工具默认都会查找。只有对于 PHP-CS-Fixer,我们会使用--dry-run选项,因为我们只想在 CI/CD 管道中检查问题,而不是解决它们。

设置检查范围

对于我们的小型示例应用程序,运行前面的检查在所有文件上是可以的,因为它们会快速执行。然而,如果我们的应用程序增长,或者我们希望将 CI/CD 引入现有的应用程序(我们将在本章后面进一步讨论),则只需在最新提交中只更改的文件上运行这些检查即可。以下操作在这种情况下可能对您有所帮助:github.com/marketplace/actions/changed-files

如果 PHP-CS-Fixer 或 PHPStan 没有报告任何问题,我们就可以安全地执行下一阶段的自动化测试:测试。

第三阶段 – 测试

我们已经彻底分析了代码,并检查了错误和语法错误,但我们还需要检查代码中的逻辑错误。幸运的是,我们有一些自动化测试来确保我们没有无意中引入任何错误。

由于与第二阶段中的代码质量工具相同的原因,我们不希望为运行我们的 PHPUnit 测试套件使用专用操作。我们只是像在本地开发系统中一样执行 PHPUnit。使用phpunit.xml文件在这里显然很有用,因为我们不需要记住所有许多选项。让我们首先看看工作流程 YAML,如下所示:

###################
# Stage 3 - Tests #
###################
- name: Unit Tests
  run: vendor/bin/phpunit --testsuite Unit
- name: Integration Tests
  run: vendor/bin/phpunit --testsuite Api

这里唯一值得注意的事情是我们不仅运行所有测试,而且将它们分成两个测试套件:UnitApi。由于我们的单元测试应该执行得最快,我们希望首先运行它们(并失败),然后是较慢的集成测试。请注意,我们没有添加任何端到端测试,因为我们的应用程序不在浏览器中运行,而是一个简单的 Web 服务。

我们通过使用phpunit.xml配置文件来分割测试。以下代码片段展示了其<testsuites>节点,其中我们根据目录(ApiUnit)来分离测试套件:

<testsuites>
    <testsuite name="Api">
        <directory>tests/Api</directory>
    </testsuite>
    <testsuite name="Unit">
        <directory>tests/Unit</directory>
    </testsuite>
</testsuites>

我们还配置了 PHPUnit 以创建代码覆盖率报告,如下所示:

<coverage processUncoveredFiles="false">
    <include>
        <directory suffix=".php">src</directory>
    </include>
    <report>
        <html outputDirectory="reports/coverage" />
        <text outputFile="reports/coverage.txt" />
    </report>
</coverage>

要创建这些报告,PHPUnit 将自动使用我们在第 1 阶段配置的pcov扩展。它们将被写入reports文件夹,我们将在下一阶段处理。

这就是测试阶段需要做的所有事情。如果我们的测试没有发现任何错误,我们就可以进入管道的最后一个阶段,并完成所有工作。

第 4 阶段 - 部署

我们的应用程序现在已经彻底检查和测试过了。在我们准备好将其部署到我们预想的任何环境中之前,我们首先需要处理移除dev依赖项。幸运的是,这非常简单,正如我们在这里可以看到的:

####################
# Stage 4 - Deploy #
####################
- name: Remove dev dependencies
  run: composer install --no-dev --optimize-autoloader

运行composer install --no-dev将简单地从vendor文件夹中删除所有dev依赖项。另一个值得注意的特性是 Composer 的--optimize-autoloader选项:由于在生产环境中,我们不会像在开发中那样添加或更改任何类或命名空间,因此 Composer 的自动加载器可以通过不检查任何更改来优化,从而减少磁盘访问,稍微提高其速度。

作为最后一步,我们想要创建构建工件:一个是可交付成果——即我们打算部署的代码。另一个是我们在第 3 阶段创建的代码覆盖率报告。GitHub Actions 在执行工作流程 YAML 之后不会保留任何比 GitHub 网站上显示的日志信息更多的数据,因此我们需要确保它们在最后被存储起来。代码如下所示:

- name: Create release artifact
  uses: actions/upload-artifact@v2
  with:
    name: release
    path: |
      public/
      src/
      vendor/
- name: Create reports artifact
  uses: actions/upload-artifact@v2
  with:
    name: reports
    path: reports/

我们使用actions/upload-artifacts操作创建两个 ZIP 存档(在这里称为工件):releasereports。第一个包含我们运行生产环境中的应用程序所需的所有文件和目录,不再需要其他任何东西。我们省略了项目根目录中的所有配置文件,甚至包括composer.jsoncomposer.lock文件。我们不再需要它们,因为我们的vendor文件夹已经存在。

reports工件将仅包含reports文件夹。在构建完成后,你可以在 GitHub 上单独下载这两个 ZIP 存档。更多内容将在下一节中介绍。

将管道集成到你的工作流程中

在将工作流程 YAML 添加到.github/workflows文件夹(例如,.github/workflows/ci.yml)之后,你只需要将其提交并推送到仓库。我们配置了我们的管道,使其在每次打开 PR 或有人向main分支推送提交时运行。

当你打开github.com并进入你的仓库页面时,你将在操作标签页中找到你最后的工作流程运行概览,如下面的截图所示:

![图 11.3:github.com 上的仓库页面图片

图 11.3:github.com 上的仓库页面

绿色勾号标记成功的运行,而红色交叉——当然——标记失败的运行。您还可以看到它们何时执行以及这花了多长时间。通过点击每个条目右侧的三个点,您将找到更多选项——例如,您可以在这里删除工作流程运行。点击运行的标题,即运行的相应提交信息,您将进入 摘要 页面,如下面的截图所示:

图 11.4:工作流程运行摘要页面

图 11.4:工作流程运行摘要页面

在这里,您可以查看工作流程的所有作业。由于我们的示例只包含一个作业,即 pipeline,您只会看到一个。在此页面上,您还可以找到任何生成的工件(例如我们的发布和报告工件)并下载或删除它们。GitHub 免费提供的磁盘空间有限,所以当您快用完空间时,请确保删除它们。

另一个重要的信息是计费时间。尽管我们的作业总共只运行了 43 秒,GitHub 仍将从您的月度使用量中扣除 1 分钟。GitHub 提供了慷慨的免费计划,但您应该时不时地查看您的使用情况。您可以在用户设置页面中的 计费和计划 部分找到更多关于此的信息(github.com/settings/billing)。

如果您想查看工作流程运行期间确切发生了什么——例如,如果出了问题——您可以点击 pipeline 作业以获取所有步骤的详细概述,如下面的截图所示:

图 11.5:作业详情页面

图 11.5:作业详情页面

每个步骤都可以展开和折叠以获取有关其执行期间确切发生了什么的额外信息。在上面的截图中,我们展开了 安装 PHP 步骤以查看动作的详细操作。

恭喜——您现在为您的项目拥有了一个工作的 CI 管道!这标志着我们通过 GitHub Actions 的小型之旅结束。当然,您可以按需扩展管道——例如,通过将发布工件上传到 SSH 文件传输协议SFTP)服务器或 AWS 简单存储服务S3)存储桶。还有更多的事情可以做,所以请确保尝试一下。

在下一节中,我们将向您展示如何设置您本地的管道。这将为您节省一些时间,甚至可能通过避免不必要的早期检查来节省成本。

您的本地管道 – Git 钩子

在我们成功设置了一个简单但已经非常有用的 CI/CD 管道之后,我们现在想看看在将它们提交到仓库之前,如何在本地开发环境中运行一些步骤。这听起来可能像是重复工作——为什么我们要运行相同的工具两次呢?

记住本章开头的图 11.1-根据检测时间估计修复错误的相对成本:我们越早发现错误,它造成的成本或努力就越少。当然,如果在 CI/CD 管道中找到错误,这仍然比在生产环境中要早得多。

虽然管道不是免费的。我们的示例应用程序构建速度快,只需大约一分钟。想象一下,一个完整的 Docker 设置已经花费相当多的时间来创建所有必要的容器。现在,它因为一个小错误而无法构建,如果您在提交代码之前没有忘记执行单元测试,您本可以在 2 分钟内解决这个错误。您可能刚刚享受了一个应得的茶或咖啡休息,结果回来发现构建失败了,这很烦人,也是金钱和计算能力的浪费。

正是那些快速运行的检查,比如单元测试、代码嗅探器或静态代码分析,是我们希望在开始全面构建我们的更改之前执行的。我们不能依赖我们自己自动执行这些检查,因为我们都是人类。我们会忘记事情,但机器不会。

如果您使用 Git 进行开发,像今天的大多数开发者一样,我们可以利用 Git 钩子的内置功能来自动化这些检查。Git 钩子是在某些事件上自动执行的 shell 脚本,例如在每次提交之前或之后。

对于我们的需求,pre-commit钩子特别有用。每次您运行git commit命令时,它都会执行,如果执行的脚本返回了错误,它还可以中止提交。在这种情况下,不会将任何代码添加到仓库中。

设置 Git 钩子

手动设置 Git 钩子确实需要一些 shell 脚本的了解,因此我们希望使用一个名为CaptainHook的包来帮助我们。使用这个工具,我们可以安装任何我们喜欢的钩子,甚至可以使用一些高级功能,而无需掌握 Linux。

您可以使用 Phive 轻松下载 Phar(有关更多信息,请参阅第九章**,组织 PHP 质量工具),或者使用 Composer 安装它,就像我们现在要做的:

$ composer require --dev captainhook/captainhook

接下来,我们需要创建一个captainhook.json文件。此文件包含项目的钩子配置。由于此文件将被添加到仓库中,我们确保我们的团队中的其他开发者可以使用它。要创建此文件,我们可以运行以下命令:

$ vendor/bin/captainhook configure

CaptainHook 将向您提出几个问题,并根据您的答案生成一个配置文件。然而,您可以跳过此步骤,直接创建文件,就像我们现在要做的。打开您最喜欢的编辑器,并写下以下代码:

{
    "config": {
        "fail-on-first-error": true
    },
    "pre-commit": {
        "enabled": true,
        "actions": [
            {
             "action": "vendor/bin/php-cs-fixer
               fix --dry-run"
            },
            {
                "action": "vendor/bin/phpstan"
            }
        ]
    }
}

每个钩子都有自己的部分。在pre-commit钩子部分,enabled可以是truefalse——后者禁用钩子但保留文件中的配置,这在调试时可能很有用。actions包含要执行的命令。正如你所看到的,这些命令就像你从第七章**,代码质量工具中已经了解的那样。

你想要执行的所有操作都需要在单独的操作部分中编写。在上面的例子中,我们配置了 PHP-CS-Fixer 和 PHPStan 在pre-commit时执行。

由于我们为这两个工具都有额外的配置文件,所以我们不需要指定任何其他选项,除了告诉 PHP-CS-Fixer 只进行 dry run——也就是说,只在我们发现代码风格违规时通知我们。

config部分,你可以指定更多的配置参数。我们希望在错误发生后立即停止钩子执行,因此将fail-on-first-error设置为true。否则,CaptainHook 会首先运行所有检查,然后告诉你结果。这当然只是个人喜好的问题。

CaptainHook 文档

我们无法在这本书中列出 CaptainHook 的所有功能。然而,我们鼓励你查看官方文档captainhookphp.github.io/captainhook,以了解更多关于这个工具的信息。

现在我们已经完成了配置,请将此captainhook.json文件存储在项目根目录中。这就是我们关于配置需要做的所有事情。

我们现在只需要安装钩子——也就是说,在.git/hooks中生成钩子文件。这可以简单地这样做:

$ vendor/bin/captainhook install -f

我们在这里使用-f选项,代表强制。没有这个选项,CaptainHook 会单独询问我们是否要安装每个钩子。请注意,CaptainHook 将为它支持的每个 Git 钩子安装一个文件,即使是你没有配置的钩子也不例外。不过,这些钩子不会做任何事情。

要测试pre-commit钩子,你可以使用以下命令手动执行,而无需提交任何内容:

$ vendor/bin/captainhook hook:pre-commit

对于 CaptainHook 支持的其它所有钩子,都有类似的命令可用。如果你对captainhook.json文件进行了修改,别忘了再次使用install -f命令来安装它。

为了确保钩子被安装在本地的开发环境中,你可以在你的composer.json文件的scripts部分添加以下代码:

"post-autoload-dump": [
    "if [ -e vendor/bin/captainhook ]; then
      vendor/bin/captainhook install -f -s; fi"
]

我们使用 Composer 的post-autoload-dump事件来运行install -f命令。该命令将在 Composer 自动加载器每次刷新时执行,这将在每次执行composer installcomposer update时发生。这样,我们确保任何参与此项目的开发者的开发环境中钩子定期安装或更新。通过使用if [ -e vendor/bin/captainhook ],我们检查 CaptainHook 二进制文件是否存在,如果未安装,则避免破坏 CI 构建。

实践中的 Git 钩子

我们完成了pre-commit钩子的配置,并测试和安装了它。现在,我们准备看到它的实际应用:如果你在应用程序代码中做了任何更改——例如,在ProductController.php文件中添加一个空白行——然后尝试提交更改,pre-commit钩子应该被执行。如果更改违反了PSR-12标准,PHP-CS-Fixer 步骤应该失败,如下面的截图所示:

图 11.6:pre-commit 钩子失败

图 11.6:pre-commit 钩子失败

自动修复代码风格问题

当然,在执行 PHP-CS-Fixer 时,你可以移除--dry-run选项,让它自动修复问题。实际上,这是一个常见的做法,我们鼓励你尝试一下。然而,这需要做更多的工作,因为你必须让用户知道他们的更改文件已经被修复,并且需要重新提交。为了使这个例子简单,我们决定省略这一点。

我们现在知道ProductController.php文件需要被修复。我们可以让 PHP-CS-Fixer 来完成这项工作,如下所示:

图 11.7:使用 PHP-CS-Fixer 自动修复代码风格问题

图 11.7:使用 PHP-CS-Fixer 自动修复代码风格问题

ProductController.php文件现在再次被更改,那些额外的更改尚未被暂存——也就是说,它们还没有被添加到提交中。之前的更改仍然被暂存。下面的截图显示了如果你现在运行git status会是什么样子:

图 11.8:未暂存更改

图 11.8:未暂存更改

现在需要做的只是再次添加ProductController.php文件并再次运行git commit,如下面的截图所示:

图 11.9:pre-commit 钩子通过

图 11.9:pre-commit 钩子通过

pre-commit钩子的两个步骤现在都通过了。你现在需要做的只是提交更改并执行git push

高级用法

之前的例子是一个非常基础的例子。当然,在本地开发环境中,你可以做更多的事情。例如,你可以添加更多的工具,如phpcpd复制粘贴检测器或phpmd混乱检测器,这两者我们都在第七章**,代码质量工具中介绍过。

如果你的测试不是太慢(这究竟意味着什么取决于你和你的队友的耐心),你应该考虑在本地运行你的测试。即使你有运行缓慢的测试,你也可以将它们分成几个测试套件,并且只在pre-commit上执行快速运行的测试。

你还应该考虑只对修改过的文件进行代码质量检查,而不是整个项目,就像我们在示例中所做的那样。CaptainHook 提供了有用的{$STAGED_FILES}占位符,它包含所有暂存文件。使用起来非常方便,如下所示:

{
    "pre-commit": {
        "enabled": true,
        "actions": [
            {
                "action": "vendor/bin/php-cs-fixer fix
                  {$STAGED_FILES|of-type:php} --dry-run"
            },
            {
                "action": "vendor/bin/phpstan analyse
                  {$STAGED_FILES|of-type:php}"
            }
        ]
    }
}

之前的示例只对修改过的 PHP 文件运行检查。这有两个主要好处:首先,它更快,因为你不需要检查你未触及的代码。当然,这种加速取决于你的代码库的大小。

其次,特别是如果你正在处理一个现有的项目并且刚刚开始引入这些检查,在整个代码库上运行它们不是一种选择,因为你需要一次性修复太多的文件。我们将在下一节中更详细地讨论这个问题。

远足 - 向现有软件添加 CI

如果你在一个公司工作,你并不总是从“绿色”开始——也就是说,从头开始构建一个新项目。事实上,很可能是相反的:当你加入一个公司时,你将被添加到一个已经为了一或多个项目工作了很长时间的团队中。

你可能已经遇到了遗留软件遗留系统这样的术语。在我们的上下文中,它们描述的是已经存在很长时间并且仍在业务关键过程中使用的软件。它不再符合现代开发标准,因此不能轻易更新或更改。随着时间的推移,它变得如此脆弱和难以维护,以至于没有开发者愿意再碰它。更糟糕的是,由于系统在更长的时间内增长,它具有如此多的功能,以至于没有任何利益相关者(即用户)愿意错过它。因此,替换遗留系统并不那么容易。

毫不奇怪,遗留软件有一个不好的含义,但可能,整个系统需要的只是一些“关注”。想想看,就像修复一台旧机器,旧零件被现代零件所取代,而外观保持不变。一些开发者甚至发现,在这样软件上工作更具挑战性。它已经走了很长的路,赚了钱,并且——最有可能的是(至少部分如此)——推动了公司的成功,因此它值得一些尊重。

因此,如果你必须处理这样的项目,不要轻易放弃。在这本书中,我们为你提供了必要的知识和工具,以开始重新塑造它——这需要更长的时间,你可能永远达不到完美的水平。但完美并不是必要的。

逐步进行

从添加集成和端到端(E2E)测试开始。这两种测试类型通常不需要对代码进行任何或只有很少的更改,但会带来巨大的好处,因为它们可以间接覆盖大量代码,而无需编写单元测试。一旦你用测试覆盖了应用程序的关键路径(即最常用的工作流程),你就可以开始重构类,并开始引入额外的单元测试。这些测试将帮助你快速发现错误和副作用,而无需反复点击应用程序。

如你所知,引入一种代码风格,如PSR-12,就像一次性在整个代码库上运行一个工具,例如PHP-CS-Fixer一样简单。当然,生成的提交将会非常大,所以在你这样做之前,你想要与任何其他开发者达成代码冻结的协议。代码冻结意味着每个人都将他们的更改提交到仓库中,这样你的重构就不会在他们在之后检查更改时引起巨大的合并冲突。

为了决定要重构哪些代码,我们打算使用你现在所知道的许多代码质量工具中的一个或多个。在0级别使用 PHPStan 是一个不错的选择。你也可能想考虑使用 PSalm,因为它也可以自动解决一些问题。根据项目的大小,错误列表可能会非常长。利用如第七章中所述的基线功能,代码质量工具在这里可以提供一些外观上的帮助,但它只会隐藏而不是解决代码问题。

你不需要急于求成。如果你配置你的 CI/CD 管道只检查已修改的文件,你可以随着时间的推移,逐步改进代码。这确实会给你留下一个问题,一旦你接触到一个文件,你必须重构它以满足规则。特别是对于旧但庞大的类,这可能会成为问题。然而,在第七章中,代码质量工具,我们解释了如何排除文件或代码的一部分以进行执行检查。你甚至可以设置一个管道,允许你在提交信息中包含某些关键字(例如,skip ci)时跳过检查。然而,这种方法只能是最后的手段——否则,你永远不会开始重构旧代码。这需要开发者有一定的自我克制,不要滥用这个功能。

随着时间的推移,参与项目工作的团队将积累新的信心,随着测试覆盖率的提高,他们将开始越来越多地重构代码。确保也安装一个本地管道,以缩短等待时间。

对 CD 的展望

最终,你的持续集成(CI)管道将运行得如此之好,以至于你可以完全信任它。它将可靠地防止将损坏的代码部署到生产环境中,在某个时刻,如果你发现部署顺利,你发现自己做的检查越来越少,也越来越不手动。到了这个时候,你可以考虑使用持续部署(CD):这描述了将代码自动部署到任何环境的工具和流程的组合。

常见的流程是,每当更改合并到某个分支(例如,用于生产环境的main分支)时,CI/CD 管道将自动触发。如果更改通过了所有检查和测试,那么过程被信任得如此之高,以至于代码被部署到目标位置,而无需再手动测试构建结果。

如果你曾经有机会在这样的环境中工作,你肯定不希望错过。除了一个出色的 CI/CD 管道和对其 99%的信任之外,还需要一些流程来快速应对部署出现的问题。即使最好的工具也无法防止在更大负载下才会出现的逻辑错误或基础设施问题。

部署后出现问题时,你的团队应该是第一个注意到的人!你不仅需要完全信任管道,还需要信任监控和日志记录设置。外面有众多概念和工具,我们最终将离开代码质量的话题,进入开发运维DevOps)和系统管理的领域。尽管如此,我们仍想给你一些关于一些你可能想要深入了解的关键概念的简要指导,如下所示:

  • 监控收集有关系统状态的信息。在我们的环境中,这通常是有关所有服务器或实例的中央处理单元(CPU)负载、随机存取存储器(RAM)使用或数据库流量的信息。例如,如果 CPU 负载突然大幅增加,这是一个很好的迹象,表明前方可能有麻烦。

  • 日志记录帮助你将应用程序产生的所有日志消息组织在一个单一、易于访问的地方。当系统出现问题时,如果需要先在多个服务器上搜索任何日志文件,那么这并没有帮助,此时所有警报都在响起。

  • 有多种部署方法可供选择。特别是当你的设置已经增长并包含多个服务器或云实例时,你只需在少数实例或甚至一个单独的部署环境中推出新代码,并监控那里的行为。如果一切顺利,你可以继续将部署扩展到剩余的实例。这些方法被称为金丝雀滚动蓝绿部署。你将在本章末尾找到更多关于这些方法的链接。

  • 无论你如何监控你的软件,如果出了问题(而且它们会),你需要回到应用程序的早期版本。这被称为回滚。你应该总是准备好尽可能快、尽可能容易地回到上一个版本。这要求你拥有几个早期版本的交付成果。保留至少 5 个或 10 个版本是个好主意,因为有时并不清楚哪个版本确切地导致了问题。

当然,CD 超出了编写干净 PHP 代码的范围。然而,我们认为这是一个值得追求的目标,因为它将大大加快您的开发速度,并使您接触到各种迷人的工具和概念。

摘要

我们希望,在阅读这一章后,您会像我们一样坚信 CI 极其有用,因此是您工具箱中不可或缺的工具。我们不仅从理论上,而且通过使用 GitHub Actions 构建一个简单但实用的管道,在实践中也解释了这一主题的必要术语以及管道的不同阶段。最后,我们为您展望了 CD。

您现在拥有了编写出色 PHP 代码所需的知识和工具基础。当然,学习永远不会停止,而且还有许多知识等待您去发现,我们无法将它们全部放入这本书中。

如果您将开发 PHP 软件作为职业,那么您通常会在开发团队中工作。即使您在维护自己的开源项目,您也会与其他人互动——例如,当他们向您提交代码更改时。CI 是一个重要的构建块,但不是您在成功团队设置中需要考虑的唯一事情。

对于我们来说,这个话题非常重要,以至于我们专门用接下来的两章来介绍现代协作技术,这些技术将帮助您在团队中编写出色的 PHP 代码。我们希望在下章中见到您!

进一步阅读

如果您想了解更多,请查看以下资源:

第十二章:团队合作

这本书的主要目标是让你能够编写可以被你和其他人理解、维护和扩展的代码。大多数时候,成为一名 PHP 开发者意味着你不会单独在一个项目或工具上工作。即使你开始独自编写代码,也很可能最终会有其他开发者加入你——无论是商业产品,还是你的开源包,其他开发者开始添加新功能或修复错误。

在软件开发中,总会有多种执行任务的方法。这就是当你想要一起编写干净代码时,团队合作变得更加具有挑战性的原因。在本章中,你将找到关于如何设置编码标准编码指南的几个技巧和最佳实践。我们还将讨论代码审查如何改进代码并确保遵循指南。

我们还将在本章末尾更详细地探讨设计模式这一主题。这些模式可以帮助你的团队解决典型的软件开发问题,因为它们提供了经过充分测试的解决方案。

本章将包括以下部分:

  • 编码标准

  • 编码指南

  • 代码审查

  • 设计模式

技术要求

如果你跟随着前面的章节,你不需要进行任何额外的设置。

本章的代码示例可以在我们的 GitHub 仓库中找到:github.com/PacktPublishing/Clean-Code-in-PHP

编码标准

在前面的章节中,你学到了很多关于编写高质量代码的知识。然而,如果你只是自己这样做,那就不够了。当你在一个团队中工作时,你很可能会遇到其他开发者对质量有不同的理解,并且他们的技能水平与你不同。

这对你的代码是有害的,因为它可能会导致懒惰的妥协,其中涉及的各方同意一种方式,只是为了保持和平。因此,如果你想在一个团队中有效地工作,你希望尽可能标准化你的工作。

从低垂的果实开始是有意义的:代码格式化。这涉及到最基本的,比如同意使用多少空格来缩进行,或者大括号应该放在哪里。但为什么这甚至很重要呢?

我们已经在第五章**,优化你的时间和分离责任中简要地提到了这个话题。然而,我们想在这里进一步展开。拥有共同的编码标准(也称为编码风格)的主要优势是减少阅读代码时的认知摩擦

认知摩擦

认知摩擦基本上描述了我们的大脑处理信息所需的精神努力。想象一下,例如,你读一本书,其中每隔一段文字就使用了不同的字体、大小或行间距。你仍然能够阅读它,但很快就会变得令人烦恼或疲劳。同样的情况也适用于阅读代码。

将编码规范引入项目相对容易,多亏了我们在本书前面介绍给你的工具。另一方面,与他人就共同标准达成一致则需要更多的工作。这就是为什么在本节中,我们想向你展示如何轻松地就共同编码标准达成一致。

遵循现有标准

与他人一起制定标准可能是一个漫长而痛苦的过程。然而,如今,你不再争论纸张的大小了。在欧洲国家,DIN A4标准被广泛接受,而在其他国家,如美国,你会使用US Letter Size而无需询问原因。大多数人接受这些措施,遵循这些标准使生活变得稍微容易一些——少了一件需要担心的事情。

同样适用于编码规范,它定义了你的代码应该如何格式化。当然,你可以和你的队友争论几个小时,关于是否应该使用制表符空格进行缩进。双方都会提出有效的论据,但你永远不会找到正确答案,因为这里根本就没有对错之分。一旦你解决了关于缩进的疑问,下一个讨论的话题可能是括号的放置。它们应该出现在同一行,还是下一行?

我们不一定需要同意标准的每一个细节,但无疑,使用现有的规范可以节省时间和精力。在 PHP 生态系统中,已经存在一些你可以利用的编码规范。这样做的一个巨大额外好处是,代码嗅探器为这些标准内置了规则集。在下一节中,我们将讨论 PHP 最著名的编码规范

PHP-FIG 和 PSR

PHP 本身没有官方的编码规范。从历史上看,每个曾经存在或至今仍存在的重大 PHP 框架都引入了一些标准,因为开发者很快意识到使用它们有其好处。

然而,由于每个项目都使用自己的标准,PHP 世界最终形成了一种不同格式标准的混合。回想起 2009 年,当PHP-FIGPHP 框架互操作性组(PHP-FIG))成立时,该组织由当时所有重要 PHP 项目和框架的成员组成,他们想要解决的就是这类问题。

当时,Composer 变得越来越重要,引入了可以在不同框架之间轻松使用的包。为了保持代码的某种一致性,达成了一种共同编写代码的方式:PHP 标准建议PSRs)应运而生。

为了使 Composer 的自动加载器工作,有必要就如何命名类和目录达成一致。这是通过第一个标准建议PSR-0(是的,极客们从 0 开始计数)实现的,它最终被PSR-4取代。

第一个编码标准建议是在PSR-1PSR-2中引入的。PSR-2后来被PSR-12取代,其中包含了针对新 PHP 版本语言特性的规则。

虽然 PSR-12 解决了代码风格问题,但它没有涵盖命名约定或如何组织代码。这通常仍然由你使用的框架预先定义。例如,Symfony框架有自己的基于上述PSR-4PSR-12编码标准,并添加了进一步的指导,例如命名或文档约定。即使你根本不使用框架,只是选择单个组件来构建应用程序,你也可以考虑使用这些在Symfony网站上可以找到的指南:symfony.com/doc/current/contributing/code/standards.html

PER 编码风格

PSR-12于 2019 年发布,因此不再涵盖最新的 PHP 特性。因此,在撰写本书时,PHP-FIG 发布了PER 编码风格 1.0.0PER代表PHP 扩展建议)。它基于PSR-12,并对其进行了某些补充。未来,PHP-FIG 不再计划发布任何与 PSR 相关的新的编码标准,除非需要,才会发布新的 PER 版本。我们很有可能会在本书中介绍的代码质量工具很快就会采用新的 PER。你可以在以下链接中找到更多关于它的信息:www.php-fig.org/per/coding-style

随着时间的推移,PHP-FIG 已经引入了十多个建议,并且还有更多正在制作中。它们涵盖了如何集成日志记录、缓存和 HTTP 客户端等主题。你可以在官方网站上找到完整的列表:www.php-fig.org

PHP-FIG 和 PSR 的问题

PHP-FIG 不应被视为官方的 PHP 权威机构,任何 PSR 也不应被视为无可争议。事实上,许多重要的框架,如SymfonyLaravel,已经不再属于 PHP-FIG,因为推荐的标准对它们的内部结构干扰过多。看看今天可用的所有 PSR,你甚至可以将它们视为它们自己的元框架。但这并不是要贬低许多建议的相关性——我们只是希望你们不要盲目地接受它们为既定事实。

在你的 IDE 中强制执行编码标准

有几种方式可以强制执行编码规范。在上一章第十一章“持续集成”中,我们解释了如何确保没有错误格式的代码会破坏代码库。这效果很好,但即使我们让我们的工具自动修复代码格式,也需要额外的步骤,因为我们需要再次提交这些更改过的文件。所以,如果我们的代码编辑器或 IDE 在编写代码时帮助我们格式化代码,这实际上不是很有用吗?

现代代码编辑器通常具有内置功能,可以帮助你遵守你偏好的编码规范,如果你进行了配置的话。如果没有内置,这种功能至少可以通过插件提供。

你的编辑器可以支持你的两种基本方式:

  • 突出显示编码规范违规:IDE 会标记出需要修正的源代码部分。尽管如此,它不会主动更改代码。

  • PHP_CS_Fixer。这可以在手动请求时进行,或者每次保存文件时进行。

在文件保存时重新格式化代码是一种非常方便的方式来确保你的代码符合编码规范。如何设置这取决于你使用的 IDE,因此我们不会在本书中进一步详细说明。

我们仍然建议使用Git 钩子持续集成作为检查的第二层,以确保没有格式错误的代码被推送到项目仓库。你永远不能确定团队成员是否意外或故意禁用了自动重新格式化,或者是否没有关注代码中突出显示的部分。

编码规范主要涉及如何一致地格式化代码。但在团队工作中,你还需要就其他方面达成一致——在下一节中,我们将向你展示哪些其他方面值得达成共识。

编码指南

在上一节中,我们讨论了为什么你应该引入编码规范。一旦完成这项工作,你应该考虑设置编码指南。这两个话题听起来非常熟悉,确实如此。然而,尽管编码规范通常关注如何格式化代码,编码指南则定义了如何编写代码。这当然包括定义要使用哪个编码规范,但涵盖的内容远不止这些,你将在本节中了解到。

“如何编写代码”究竟意味着什么?在编写软件时,通常有多种方式可以实现目标。以广为人知的模型-视图-控制器MVC)模式为例。它用于将应用程序逻辑划分为三种相互关联的元素——模型、视图和控制器。尽管它没有明确定义将业务逻辑放在哪里。是应该放在控制器内部,还是更合适地放在模型内部?

对于这个问题,没有明确的正确或错误答案。然而,我们的建议是采用胖模型,瘦控制器的方法:业务逻辑不应该在控制器中编写,因为它们是视图和特定问题代码之间的绑定元素。此外,控制器通常包含大量的框架特定代码,尽可能地将这些代码排除在业务逻辑之外是良好的实践。

不论我们的推荐如何,你的项目编码规范中应该定义你如何看待你的团队处理这个问题。否则,你很可能会在你的代码库中同时采用这两种方法。

通常,编码规范涵盖了如何命名方法、函数和属性等问题。正如你可能从那句著名的引语中得知:“计算机科学中只有两件难事:缓存失效和命名事物”,找到合适的名称确实不是一个简单的问题。因此,在这个问题上至少有一些约定可以减少开发者尝试想出合适名称的时间。此外,它们与编码标准一样,有助于减少认知摩擦。

编码规范有助于你团队中经验较少的开发者,或者那些刚开始的人,能够手头上有解决方案,否则他们可能需要在代码或互联网上搜索。它还有助于通过避免我们已经在第三章**,代码,不要做特技*中讨论的坏习惯,来编写可维护的代码。为了帮助你开始设置第一套规则,我们将在下一节给出一些示例。

编码规范示例

从一张空白(虚拟)纸开始是困难的,因此在这一节中,我们收集了一些现实世界的例子,这些例子可能是你编码规范的一部分。请注意,尽管这些规则基于最佳实践,但它们并不是完美的或唯一的真理。我们更希望为你提供一个良好的起点,用于讨论和示例,哪些主题应该通过编码规范来明确。

命名约定

通过使用命名约定,我们确保我们的代码中某些元素以统一和可理解的方式进行命名。这减少了认知摩擦,并使新团队成员的入职更容易。

服务、存储库和模型

使用大驼峰命名法。使用类型作为后缀。

这里有一些示例:

  • UserService

  • ProductRepository

  • OrderModel

事件

使用大驼峰命名法。使用正确的时态来表示事件是在实际事件之前还是之后触发的。

这里有一些示例:

  • DeletingUser是删除事件之前

  • DeleteUser是实际事件

  • UserDeleted是删除事件之后

属性、变量和方法

使用小驼峰命名法

这里有一些示例:

  • $someProperty

  • $longerVariableName

  • $myMethod

测试

使用小驼峰命名法。使用单词test作为前缀。

这里有一些例子:

  • testClassCanDoSomething()

特性

使用UpperCamelCase格式编写。使用形容词来描述特性用途。

这里有一些例子:

  • Loggable

  • Injectable

接口

使用UpperCamelCase格式编写。使用单词Interface作为后缀。

这里有一些例子:

  • WriterInterface

  • LoggerInterface

一般 PHP 约定

即使你已经使用了编码标准PSR-12,也有一些方面它们没有涵盖。我们将在本节中介绍其中的一些。

注释和 DocBlocks

如果可能,避免使用注释,因为它们往往会过时,从而造成比帮助更大的困惑。仅保留那些不能被自解释的名称或简化代码所替代的注释,这样更容易理解,并且不再需要注释。

只有当 DocBlocks 提供信息时才添加,例如代码质量工具的注释。尤其是从 PHP 8 开始,大多数 DocBlocks 都可以被类型提示所替代,所有现代 IDE 都会理解。如果你使用类型提示,大多数 DocBlocks 都可以删除:

// Redundant DocBlock
/**
 * @param int $property
 * @return void 
 */
public function setProperty(int $property): void { 
    // ... 
}

通常,IDE 会自动生成 DocBlocks。如果它们没有更新,它们至多是无用的,甚至可能是明显错误的:

// Useless DocBlock
/**
 * @param $property
 */
public function setProperty(int $property): void { 
    // ... 
}
// Wrong DocBlock
/**
 * @param string $property
 */
public function setProperty(int $property): void { 
    // ... 
}

DocBlocks 仍然应该用于现在 PHP 语言特性无法提供的信息,例如指定数组内容,或标记函数为已弃用:

// Useful DocBlock
/**
 * @return string[]
 */
public function getList(): array { 
    return [
       ‘foo’,
       ‘bar’,
    ]; 
}
/**
 * @deprecated use function fooBar() instead
 */
public function foo(): bool { 
    // ... 
}

关于 DocBlocks

DocBlocks 被引入,部分是为了弥补 PHP 早期版本中弱类型不足的缺点。这个事实上的标准是由phpDocumentor项目引入的(www.phpdoc.org/),因此得到了许多工具的支持,如 IDE 和静态代码分析器。尽管使用严格类型通常不再需要 DocBlocks,除非你希望在项目中使用phpDocumentor

三元运算符

每部分应单独一行编写以提高可读性。对于非常简短的语句可以例外:

// Example for short statement
$isFoo ? ‘foo’ : ‘bar’;
// Usual notation
$isLongerVariable
    ? ‘longerFoo’
    : ‘longerBar’;

避免使用嵌套的三元运算符,因为它们难以阅读和调试:

// Example for nested operators
$number > 0 ? ‘Positive’ : ($number < 0 ? ‘Negative’ :
‘Zero’);

构造函数

对于 PHP 8+,使用构造函数属性提升来缩短类,如果工作在 PHP 8+上。在最后一个属性后保留尾随逗号,因为这将使得添加或注释行更容易:

// Before PHP 8+
class ExampleDTO
{
    public string $name;
    public function __construct(
        string $name
    ) {
        $this->name = $name;
    }
}
// Since PHP 8+
class ExampleDTO
{
    public function __construct(
        public string $name, 
    ) {}
}

数组

总是使用短数组表示法,并在最后一个条目后保留逗号(参见前面的构造函数部分,以获取解释):

// Old notation
$myArray = array(
    ‘first entry’,
    ‘second entry’
);
// Short array notation
$myArray = [
    ‘first entry’,
    ‘second entry’,
];

控制结构

即使是一行代码也要使用括号。这减少了认知摩擦,并使得以后添加更多代码行更容易:

// Bad
if ($statement === true)
    do_something();
// Good
if ($statement === true) {
    do_something();
}

避免使用else语句和尽早返回,因为这更易于阅读并减少了代码的复杂性:

// Bad
if ($statement) {
    // Statement was successful
    return;
} else {
    // Statement was not successful
    return;
}
// Good
if (!$statement) {
    // Statement was not successful
    return;
}
// Statement was successful
return;

异常处理

应避免使用空的catch块,因为它们会静默地吞咽错误信息,从而使得查找错误变得困难。相反,记录错误信息或至少写一个注释来解释为什么可以忽略异常:

// Bad
try {
    $this->someUnstableCode();
} catch (Exception $exception) {}
// Good
try {
    someUnstableCode();
} catch (Exception $exception) {
    $this->logError($exception->getMessage());
}

架构模式

编码指南不仅限于如何格式化代码或命名元素。它们还可以帮助您从架构角度控制代码的编写。

肥胖模型,苗条控制器

如果使用 MVC 模式,业务逻辑应该位于模型或类似的类中,例如服务仓库控制器应该包含尽可能少的代码,以接收或传输视图和模型之间的数据。

与框架无关的代码

肥胖模型,苗条控制器方法中,您可能会遇到框架无关的业务逻辑这个术语。这意味着包含您的业务规则的代码应尽可能少地使用底层框架的功能。这使得框架更新或甚至迁移到其他框架变得更加容易。

单一职责原则

类和方法应该只有一个职责。参见第二章**,谁有权决定“良好实践”是什么?,了解更多关于这个原则的信息。

框架指南

在这本书中,我们希望专注于编写 PHP 的干净代码。然而,通常您会与框架一起工作,尽管将它们包含在指南中也是必要的,但我们不想在这里过多地深入细节。

然而,接下来,您将找到一份问题列表,这将帮助您了解在指南中应包含哪些框架相关主题:

  • 如何访问数据库

  • 如何配置路由

  • 如何注册新服务

  • 在您的项目中如何处理身份验证?

  • 应该如何记录错误或其他调试信息?

  • 如何创建和组织视图文件

  • 如何处理翻译

由于这些问题的答案高度依赖于所使用的框架,我们在这里不能给您提供推荐。您需要与您的团队一起制定指南。在下一节中,我们将为您提供一些关于如何做到这一点的想法。

设置指南

设置编码指南的过程需要时间,通常需要几个研讨会来讨论规则。这需要调解,例如,由技术负责人来执行;否则,您可能会陷入无休止的讨论中。

虽然您可能无法立即就所有主题达成一致,但请不要担心。提醒自己,您的团队成员有不同的背景、经验和技能水平——没有人会直接放弃他们个人的编码方式,仅仅因为突然出现了他们不理解或不接受的规则。

确保设置一个定期检查指南是否需要更新的流程。也许随着时间的推移,某些规则会过时,或者必须包含新的语言特性。在定期举行的团队会议中的行动点将是一个很好的机会来做这件事。

指南应以书面形式轻松获取,例如在维基或公司的内部知识库中,它应该能够跟踪版本历史。每个团队成员都应该能够对其发表评论,以便尽快处理问题或问题。最后,所有团队成员都应该自动收到关于新更改的通知。

一旦你的团队就一套规则达成一致,确保利用你在前面章节中学到的代码质量工具自动检查这些规则是否得到遵守。例如,你可以使用 PHPStan 来检测空的 catch 块,或者使用 PHPMD 来强制执行 if 而不使用 else

我们如何确保我们的编码规范得到应用?显然,我们应该尽可能使用我们的代码质量工具。但如果这些工具不包括我们想要强制执行的规则呢?通过一点互联网研究,你可能会找到它们的第三方实现。或者,如果你找不到任何东西,你也可以自己编写自定义规则,因为所有静态代码分析器都是可扩展的。

对于太复杂而无法自动检查的规则,我们必须手动检查它们是否被正确使用。这可以在代码审查中发生,我们认为它们如此重要,以至于它们值得在这一章中拥有自己的部分。

如果你不能确保它们得到遵守,仅仅设置编码规范将只是浪费时间。我们可以自动化检查所有与编码风格相关的规则,以及相当数量的编码规范。但到目前为止,对于那些针对框架规范或架构方面的规则,自动化不再可能,我们必须人类介入,接管检查。在这个时候,代码审查就派上用场了。让我们在下节中更详细地探讨。

代码审查

手动检查其他开发者的代码的过程称为 代码审查。这包括所有更改,即不仅包括新功能,还包括错误修复甚至简单的配置更改。

审查通常由至少一位同行开发者完成,并且通常发生在 main 分支的上下文中;只有当审查者批准更改时,它们才会成为实际应用程序的一部分。

在本节中,我们将讨论你在代码审查中应该寻找什么,为什么它们如此重要,以及它们应该如何进行,以便使它们成为你工具箱中的成功工具。

为什么你应该进行代码审查

这可能听起来有点明显,因为这正是整本书的主题。然而,这一点不能被强调得足够——代码审查将提高你代码的质量。让我们更仔细地考察一下为什么:

  • 易于引入:引入代码审查通常没有额外的成本(除了所需的时间)。所有主要的 Git 代码库服务,如 BitbucketGitLabGitHub,都内置了内置的审查功能,你可以立即使用。

  • 快速影响:代码审查不仅易于引入,而且一旦引入,它们很快就会显示出其有用性。

  • 知识分享:由于代码审查经常导致开发者之间的讨论,因此它们是传播团队中最佳实践知识的一个很好的工具。当然,初级开发者尤其是会从指导中受益匪浅,但即使是经验丰富的开发者有时也会学到新东西。

  • 持续改进:定期的讨论将导致编码指南的改进,因为它们会不断受到挑战和更新,如果需要的话。

  • 尽早避免问题:代码审查在流程的早期阶段进行(参见第十一章持续集成),因此很可能会在代码甚至达到测试环境之前就发现错误、安全问题或架构问题。

如果你还没有确信代码审查的好处,请查看下一节,我们将更详细地讨论代码审查应涵盖的内容——以及不应涵盖的内容。

代码审查应涵盖哪些内容

在进行代码审查时,我们应该检查哪些方面?

  • 代码设计:代码是否设计良好且与应用程序的其他部分保持一致?它是否遵循通用的最佳实践,例如可重用性、设计模式(参见下一节)或SOLID设计原则(参见*第二章**,谁有权决定“良好实践”是什么?)?

  • 功能性:代码是否执行了它应该执行的操作,或者是否有任何副作用?

  • 可读性:代码是否易于理解,还是过于复杂?注释是否必要?通过重命名函数或变量,或者将代码提取到具有有意义名称的函数中,是否可以提高可读性?

  • 安全性:代码是否引入了潜在的攻击向量?所有输出是否都已转义以防止 XSS 攻击?数据库输入是否已清理以避免 SQL 注入?

  • 测试覆盖率:新代码是否被自动化测试覆盖?它们是否测试了正确的事情?是否需要更多的测试用例?

  • 编码标准和指南:代码是否遵循团队商定的编码标准和编码指南?

你的团队还应考虑是否应该在本地开发环境中测试代码是否是审查过程的一部分。然而,对此并没有明确的建议。

代码审查的最佳实践

尽管代码审查有许多好处并且可以相当容易地实施,但还有一些陷阱你应该注意,以及一些使审查更加成功的既定最佳实践。

谁应该审查代码?

首先,谁应该理想地做 代码审查?当然,这也取决于你的设置。如果你与另一位 PHP 开发者一起工作,那么这个人应该是第一个被询问的人。这样,你可以建立起共享的领域知识;尽管你的同事没有直接处理你的工单,但他们至少能了解你做了什么。

然而,时不时地联系其他团队(如果有)的成员可以避免陷入孤立,并促进知识共享。如果你对某些主题不确定,可以向领域专家寻求帮助。这通常包括性能、架构或安全相关的更改。

自动化

代码审查不应该涵盖的是是否遵守了 编码标准。在第七章**,代码质量工具中,我们介绍了自动执行此操作的必要工具,而在第十一章**,持续集成中,我们将它们集成到了持续集成管道中。

确保只有那些所有检查(例如 代码嗅探器、代码分析器和自动化测试)都通过了的拉取请求才被审查。否则,你将花费大量时间在甚至不应该讨论的话题上。

避免长时间的代码审查

需要审查的代码更改应该有多少行?研究表明,200 到 400 行应该是最大值,因为随着时间的推移,审查者的注意力会逐渐下降。因此,尽量保持单个更改相对较小。对于长串的差异,审查者找到时间审查较小的更改的可能性也更大。

代码审查,即使是较小的,也会占用审查者无法编写代码的时间。但应该花多少时间?同样,这取决于你的设置。一个合理的估计是,最多 60 分钟,以避免审查者的疲劳。为审查者提供足够的空间逐行审查代码。审查应该被视为你日常工作的一部分,否则它们会迅速变成负担,所以没有人应该匆忙完成它们。

保持人性

如何制定反馈对于使审查成功至关重要。注意你的语气,并尽量避免像“这是错的!”或绝对不行的“这是愚蠢的。”这样的指责。开发者,尤其是经验较少的开发者,不应该急于让他们的代码被审查。

记住,有人会阅读你的评论。如果你从“我”的角度来写,通常效果很好,例如,“我不理解这一行,你能解释一下吗?”或“我认为我们也可以这样做...”

不要忘记利用审查来表扬做得好的部分。一个快速的“好主意!”或“我真的很喜欢你的方法”或“感谢代码清理”表明你对其他开发者工作的赞赏,并增加了他们的动力。

通常,代码审查是通过在您使用的 Git 平台上写评论来完成的。但当然,你也可以面对面地进行。一些开发者更欣赏直接的反馈,而不是仅仅的评论,因为书面文本缺乏很多元信息,如语气或面部表情。

不要过度,但也不要粗心大意

记住帕累托原则,不要过度行事。也许代码中还有一些小部分你可能想要更改,但它们并不是明显错误的,因为它们遵循了所有团队标准。编程仍然是一个个人风格的问题,在代码审查中无休止的讨论将导致挫败感而没有任何进一步的好处。

但是,不要接受会降低整体系统健康度的更改。如果你确信某个更改是有害的或违反了编码指南,你必须不批准这些更改。如果有疑问,请让另一位开发者参与。

拥抱变化

最后,如果你觉得在审查中讨论的问题应该包含在指南中,记下来并在下次团队会议上提出,不要直接提及其他开发者。也许你是对的,指南将会被修改以避免未来出现类似问题。

但你也有可能错了,而团队的其他成员并不认为这是一个问题。如果你无法提出令人信服的论据和例子,你必须接受这些决定。

确保代码审查得到执行

在紧张的工作日中,面对高优先级的错误修复和紧迫的截止日期,很容易忘记进行代码审查。幸运的是,所有 Git 服务提供商都提供了一些功能来帮助你:

  • 如果他们至少获得了一个批准,则启用main分支。你绝对应该启用此功能。

  • 轮换审查:如果你的团队规模较大,尝试不要总是从同一个人那里请求审查。一些工具甚至允许随机为你选择审查者。

  • 使用检查清单:检查清单已被证明是有用的,因此你也应该使用它们。为代码审查中需要检查的所有方面设置一个检查清单。在下一节中,我们将展示如何确保它得到使用。

完成定义

如果你使用敏捷方法工作,你可能已经听说过完成定义这个术语。在这里,团队同意在任务完成前应该执行的一系列行动。

典型的完成定义包含检查,例如是否编写了测试或更新了文档。你可以利用这一点来进行代码审查。

再次强调,我们的 Git 工具通过提供拉取请求(也称为合并请求)模板来帮助我们。这些文本将被用来自动预填充拉取请求的描述。

这具体如何操作取决于你使用的软件,所以我们在这里无法提供确切的操作指南。然而,以下文本展示了它可能的样子:

# Definition of Done
## Reviewer 
[ ] Code changes reviewed
    1\. Coding Guidelines kept
    2\. Functionality considered
    3\. Code is well-designed
    4\. Readability and Complexity considered
    5\. No Security issues found
    6\. Coding standard and guidelines kept
[ ] Change tested manually
## Developer 
[ ] Acceptance Criteria met
[ ] Automated Tests written or updated
[ ] Documentation written or updated

检查清单中包含的内容由你和你的团队决定。如果用作模板,这些项目将默认出现在拉取请求描述中。它的目的是供审阅者和开发者使用,以便在批准拉取请求并将其合并到main分支之前不会忘记需要完成的事情。

一些工具,如 GitHub,使用Markdown风格的标记语言来处理这些模板。它们将在浏览器中将复选框(每个项目前的两个方括号)显示为可点击的复选框,并跟踪它们是否被点击。哇!无需太多工作,你就已经设置了一个易于使用且有用的检查清单!

代码审查总结

我们希望这一节能让你对代码审查如何对你的团队和你自己有益有更深的理解。由于它们可以轻松引入,所以值得一试。本节中的最佳实践将帮助你避免代码审查可能带来的某些问题。

但是,就像往常一样,它们也有一些缺点:审查需要花费大量时间,并且可能导致团队成员之间的冲突。我们坚信,所花费的时间是值得的,因为积极方面远远超过了消极方面。冲突很可能会发生,而审查只是释放压力的一种方式。如果你在团队中工作,这无法完全避免,但应该尽早与你的经理讨论。这是他们的工作,处理这类问题。

在本章的最后部分,我们将更详细地探讨设计模式。它们可以作为解决软件开发中一般问题的指南。

设计模式

设计模式是软件开发中经常出现的问题的常用解决方案。作为一名开发者,你迟早会遇到这个术语,即使你之前还没有——这并非没有原因,因为这些模式基于最佳实践,并且已经证明了它们的实用性。

在本节中,我们将告诉你更多关于不同类型的设计模式以及为什么它们如此重要,以至于成为本书的一部分。此外,我们还将介绍一些在 PHP 中广泛使用的常见设计模式。

理解设计模式

让我们更深入地了解设计模式。它们可以被视为解决特定问题的模板,并且根据它们提供的解决方案命名。例如,在本章中,你将了解观察者模式,这可以帮助你实现观察对象变化的方式。这在编写代码时非常有用,同样在与其他开发者设计软件时也很方便。使用简短的名字来命名一个概念,而不是每次都要解释它,要容易得多。

尽管如此,不要将设计模式与算法混淆。算法定义了需要遵循的明确步骤来解决一个问题,而设计模式描述了如何在更高层次上实现解决方案。它们不受任何编程语言的限制。

你也不能像添加 Composer 包那样将设计模式添加到你的代码中。你必须自己实现模式,并且你在实现方式上有一定的自由度。

然而,设计模式并非解决所有问题的唯一方案,它们也不声称提供最有效的解决方案。始终对这些模式持保留态度——通常,开发者只是因为知道某个模式而想要实现它。或者,正如俗话所说:“如果你只有一把锤子,那么一切看起来都像钉子。”

通常,设计模式被分为三个类别:

  • 创建型模式处理如何高效地创建对象,同时提供减少代码重复的解决方案

  • 结构型模式帮助你在灵活且高效的结构中组织实体(即类和对象)之间的关系

  • 行为型模式在保持高灵活性度的同时安排实体之间的通信

在接下来的页面中,我们将查看一些示例实现来解释设计模式背后的理念。

PHP 中的常见设计模式

现在,我们想介绍 PHP 世界中一些最广泛使用的模式。我们从三个类别创建型结构型行为型中各选择了一个模式,这些模式我们在上一节中讨论过。

工厂方法

想象以下问题:你需要编写一个应用程序,该应用程序应该能够使用不同的格式将数据写入文件。在我们的例子中,我们希望支持CSVJSON,但未来也可能支持其他格式。在数据写入之前,我们希望应用一些过滤,这应该始终发生,无论选择哪种输出格式。

解决这个问题的适用模式将是工厂方法。它是一个创建型模式,因为它涉及对象的创建。

这个模式的主要思想是子类可以实现不同的方式来实现目标。需要注意的是,我们在父类中没有使用new运算符来实例化任何子类,如下面的类所示:

abstract class AbstractWriter
{
    public function write(array $data): void
    {
        $encoder = $this->createEncoder();
        // Apply some filtering which should always happen, 
        // regardless of the output format.
        array_walk(
            $data,
            function (&$value) {
                $value = str_replace(‘data’, ‘’, $value);
            }
        );
        // For demonstration purposes, we echo the result
        // here, instead of writing it into a file
        echo $encoder->encode($data);
    }
    abstract protected function createEncoder(): Encoder;
}

注意createEncoder方法——这是赋予模式名称的工厂方法,因为它在某种程度上充当了新实例的工厂。它被定义为抽象函数,因此需要由一个或多个子类实现。

为了足够灵活以适应未来的格式,我们打算为每种格式使用单独的Encoder类。但首先,我们为这些类定义一个接口,以便它们可以轻松交换:

interface Encoder
{
    public function encode(array $data): string;
}

然后,我们为每种格式创建一个实现Encoder接口的Encoder类;首先,我们创建JsonEncoder

class JsonEncoder implements Encoder
{
    public function encode(array $data): string
    {
        // the actual encoding happens here
        // ...
        return $encodedString;
    }
}

然后我们创建 CsvEncoder

class CsvEncoder implements Encoder
{
    public function encode(array $data): string
    {
        // the actual encoding happens here
        // ...
        return $encodedString;
    }
}

现在,我们需要为每个我们想要支持的格式创建一个 AbstractWriter 类的子类。在我们的例子中,首先是 CsvWriter

class CsvWriter extends AbstractWriter
{
    public function createEncoder(): Encoder
    {
        $encoder = new CsvEncoder();
        // here, more configuration work would take place
        // e.g. setting the delimiter
        return $encoder;
    }
}

其次是 JsonWriter

class JsonWriter extends AbstractWriter
{
    public function createEncoder(): Encoder
    {
        return new JsonEncoder();
    }
}

请注意,这两个子类只重写了工厂方法 createEncodernew 操作符只出现在子类中。write 方法保持不变,因为它是从 AbstractWriter 继承而来的。

最后,让我们将这些内容组合到一个示例脚本中:

function factoryMethodExample(AbstractWriter $writer)
{
    $exampleData = [
        ‘set1’ => [‘data1’, ‘data2’],
        ‘set2’ => [‘data3’, ‘data4’],
    ];
    $writer->write($exampleData);
}
echo "Output using the CsvWriter: ";
factoryMethodExample(new CsvWriter());
echo "Output using the JsonWriter: ";
factoryMethodExample(new JsonWriter());

factoryMethodExample 函数首先接收 CsvWriter 参数,在第二次运行时接收 JsonWriter 参数。输出将如下所示:

Output using the CsvWriter:
3,4
1,2

Output using the JsonWriter:
[["3","4"],["1","2"]]

工厂方法模式使我们能够将 Encoder 类的实例化从 AbstractWriter 父类移动到子类中。通过这样做,我们避免了 WriterEncoder 之间的紧密耦合,获得了更大的灵活性。作为缺点,代码变得更加复杂,因为我们必须引入接口和子类来实现这个模式。

依赖注入

我们接下来要介绍的模式是一个名为 依赖注入DI)的结构模式。它通过在构造时将依赖项插入到类中,而不是在类内部实例化它们,帮助我们实现松散耦合的架构。

以下代码展示了如何在构造函数中实例化一个依赖项,在这个例子中是一个经典的 Logger

class InstantiationExample
{
    private Logger $logger;
    public function __construct()
    {
        $this->logger = new FileLogger();
    }
}

代码本身运行得很好,但问题出现在你想要用不同的类替换 FileLogger 时。尽管我们已经在 $logger 属性中使用了 Logger 接口,理论上这使得它容易与另一个实现交换,但我们已经在构造函数中硬编码了 FileLogger。现在,想象一下你几乎在每一个类中都使用了那个日志记录器;用不同的 Logger 实现替换它需要一些努力,因为你必须触及使用它的每一个文件。

无法替换 FileLogger 也使得为该类编写 单元测试 更加困难。你不能用模拟对象替换它,但在测试运行期间你也不想将信息写入实际的日志。如果你想测试日志是否正确工作,你必须将一些工作区间的代码构建到你的生产代码中。

依赖注入(DI)迫使我们思考在类中应该使用哪些依赖项,以及使用多少。当构造函数接受的依赖项参数数量明显超过三个或四个时,这被认为是一种 代码异味(即,表示代码结构不良的指标),因为它表明该类违反了 单一职责原则(SOLID 中的“S”)。这也被称为 范围蔓延:类的范围随着时间的推移逐渐但稳步增大。

让我们看看依赖注入如何解决之前提到的问题:

class ConstructorInjection
{
    private Logger $logger;
    public function __construct(Logger $logger)
    {
        $this->logger = $logger;
    }
}

构造函数属性提升

请注意,我们在这里故意没有使用构造函数属性提升,以获得更好的可视化效果。

与之前的代码相比,差异似乎并不大。我们只是将Logger实例作为参数传递给构造函数,而不是直接在那里实例化。然而,好处是巨大的:我们现在可以更改要注入的实例(如果它实现了Logger接口),而无需触及实际类。

假设你不再希望类将日志记录到文件系统,而是记录到GraylogLogger,它也实现了Logger接口,但将日志写入该系统而不是写入文件。然后,你只需将GraylogLogger注入到所有应该使用它的类中即可——恭喜你,你刚刚改变了应用程序记录信息的方式,而无需触及实际类。

同样,我们可以在单元测试中轻松地用一个模拟对象替换依赖。这在可测试性方面是一个巨大的改进。

然而,无论你选择哪种实现方式,Logger的实例化仍然需要在其他地方发生。我们只是将其从InjectionExample类中移出。依赖注入发生在类实例化时:

$constructorInjection = new ConstructorInjection(
     new FileLogger()
);

通常,你会在Factory类中找到这种类型的实例化。这是一个实现例如简单工厂模式的类,其唯一任务是创建特定类的实例,并带有所有必要的依赖项。

简单工厂模式

我们在这本书中不会更详细地讨论这个模式,因为它真的很简单。你可以在这里找到更多关于它的信息:designpatternsphp.readthedocs.io/en/latest/Creational/SimpleFactory/README.html

注入不一定要通过构造函数进行。另一种可能的方法是所谓的setter 注入

class SetterInjection
{
    private Logger $logger;
    public function __construct()
    {
        // ....
    }
    public function setLogger(Logger $logger): void
    {
        $this->logger = $logger;
    }
}

依赖注入将通过setLogger方法进行。与Factory类相同。

以下是一个这样的工厂可能的样子:

class SetterInjectionFactory
{
    public function createInstance(): SetterInjection
    {
        $setterInjection = new SetterInjection();
        $setterInjection->setLogger(new FileLogger());
        return $setterInjection;
    }
}

依赖注入容器

你可能已经想知道如何管理所有必要的工厂,尤其是在一个较大的项目中。为此,发明了DI 容器。它不是 DI 模式的一部分,但与之密切相关,因此我们在这里介绍它。

DI 容器充当所有通过 DI 模式引入其目标类的对象的中央存储库。它还包含实例化对象所需的所有必要信息。

它还可以存储创建的实例,因此不需要重复实例化。例如,你不会为每个使用它的类创建一个FileLogger实例,因为这会导致大量相同的实例。你更希望只创建一次,然后通过引用传递给目标类。

DI 容器

展示现代 DI 容器所有功能会超出本书的范围。如果你对了解这个概念感兴趣,我们建议你查看phpleague/container包:container.thephpleague.com。它体积小但功能丰富,拥有优秀的文档,可以介绍你更多令人兴奋的概念,如服务提供者或屈折词。

现今,DI(依赖注入)容器的概念已被所有主要的 PHP 框架所采用,因此你很可能已经使用过这样的容器了。你可能没有注意到它,因为通常它隐藏在你的应用程序的深处,有时也被称为服务容器

PSR-11 – 容器接口

DI 容器对 PHP 生态系统来说非常重要,以至于它拥有自己的 PSR:www.php-fig.org/psr/psr-11

观察者

本书要介绍的最后一个模式是观察者模式。作为一种行为模式,其主要目的是允许对象之间进行高效的通信。一个常见的实现任务是,当一个对象的状态发生变化时,在另一个对象上触发某个动作。状态变化可能只是类属性值的改变这么简单。

让我们再举一个例子:每当客户取消订阅时,你必须向销售团队发送一封电子邮件,以便他们得到通知并采取措施留住客户。

你最好怎么做?例如,你可以设置一个定期任务,在特定的时间间隔内(例如,每 5 分钟)检查是否有任何取消操作。这会起作用,但根据你的客户群规模,这个任务可能大部分时间都不会返回任何结果。另一方面,如果两次检查之间的间隔太长,你可能会错过宝贵的时间直到下一次检查。

现在,销售可能不是世界上最重要的事情(销售人员通常不同意这一点),但你应该明白了。如果我们能在客户取消订阅时立即发送电子邮件,那不是很好吗?所以,而不是定期检查变化,我们只在变化发生时得到通知?

代码可能看起来像这个简化的例子:

class CustomerAccount
{
    public function __construct(
        private MailService $mailService
    ) {}
    public function cancelSubscription(): void
    {
        // Required code for the actual cancellation
        // ...
        $this->mailService->sendEmail(
            ‘sales@example.com’,
            ‘Account xy has cancelled the subscription’
        );
    }
}

简化示例

这个例子已经被简化了。例如,你不应该硬编码电子邮件地址。

这种方法当然会起作用,但它有一个缺点:MailService的调用被直接编码到类中,因此与它紧密耦合。现在,CustomerAccount类必须关心另一个依赖项,这增加了维护工作量,例如,测试必须扩展。如果我们以后不想再发送这封电子邮件,甚至要发送给其他部门的额外电子邮件,CustomerAccount类又必须再次更改。

使用松耦合的方法,CustomerAccount对象将只存储一个列表,该列表包含在发生更改时应通知的其他对象。这个列表不是硬编码的,需要得到通知的对象必须在引导阶段附加到该列表。

我们想要观察的对象(在前面的例子中是CustomerAccount),被称为主题。主题负责通知观察者。在主题上添加或删除观察者不需要任何代码更改,所以这种方法非常灵活。

以下代码显示了CustomerAccount类如何实现观察者模式的示例:

use SplSubject;
use SplObjectStorage;
use SplObserver;
class CustomerAccount implements SplSubject
{
    private SplObjectStorage $observers;
    public function __construct()
    {
        $this->observers = new SplObjectStorage();
    }
    public function attach(SplObserver $observer): void
    {
        $this->observers->attach($observer);
    }
    public function detach(SplObserver $observer): void
    {
        $this->observers->detach($observer);
    }
    public function notify(): void
    {
        foreach ($this->observers as $observer) {
            $observer->update($this);
        }
    }
    public function cancelSubscription(): void
    {
        // Required code for the actual cancellation
        // ...
        $this->notify();
    }
}

在这里发生了很多事情,所以让我们一点一点地过一遍。首先值得注意的是,这个班级使用了SplSubjectSplObserver接口,以及SplObjectStorage类。由于CustomerAccount类实现了SplSubject接口,它必须提供attachdetachnotify方法。

我们还使用构造函数来初始化$observers属性为SplObjectStorage,这将存储CustomerAccount类的所有观察者。幸运的是,SPL 已经提供了这个存储的实现,所以我们不需要自己来做。

标准 PHP 库

我们在第三章**,代码质量指标中已经讨论了标准 PHP 库SPL)。SPL 包括这些实体表明了观察者模式的重要性以及这个库的有用性。

attachdetach方法是由SplSubject接口要求的。它们用于添加或删除观察者。它们的实现很简单——我们只需要在两种情况下将SplObserver对象转发到SplObjectStorage,它就会为我们完成必要的工作。

notify方法必须调用存储在SplObjectStorage中的所有SplObserver对象的update方法。这就像使用foreach循环遍历所有SplObserver条目并调用它们的update方法,传递一个引用到主题使用$this

以下代码显示了这样一个观察者可能的样子:

class CustomerAccountObserver implements SplObserver
{
    public function __construct(
        private MailService $mailService
    ) {}
    public function update(CustomerAccount|SplSubject
      $splSubject): void
    {
        $this->mailService->sendEmail(
            ‘sales@example.com’,
            ‘Account ‘ . $splSubject->id . ‘ has cancelled
              the subscription’
        );
    }
}

毫不奇怪,观察者实现了SplObserver接口。唯一需要的方法是update,它会在notify方法中被主题调用。由于接口要求使用$splSubject参数来实现SplSubject接口,我们必须使用该参数类型提示。否则会导致 PHP 错误。

由于我们知道在这种情况下,对象实际上是一个CustomerAccount对象,我们还可以添加这个类型提示。这将使我们的 IDE 能够帮助我们完成正确的代码补全;尽管如此,添加它并不是必需的。

如您所见,现在所有关于发送电子邮件的逻辑都已经移动到了CustomerAccountObserver中。换句话说,我们成功地消除了CustomerAccountMailService之间的紧密耦合。

我们最后需要做的是附加CustomerAccountObserver

$mailService = new MailService();
$observer = new CustomerAccountObserver($mailService);
$customerAccount = new CustomerAccount();
$customerAccount->attach($observer);

同样,这个代码示例被简化了。在实际应用中,所有三个对象都会在专门的工厂中实例化,并通过 DI 容器组合在一起。

观察者模式可以帮助你通过相对较少的工作量解耦对象。尽管如此,它也有一些缺点。观察者更新的顺序无法控制;因此,你不能用它来实现顺序至关重要的功能。其次,通过解耦类,仅通过查看代码就不再明显知道哪些观察者被附加到它上。

总结一下设计模式的话题,我们将看看那些至今仍然相当常见但已经证明有太多显著缺点而不被推荐的模式。帷幕拉开,反模式登场!

反模式

并非每个设计模式都经得起时间的考验。一切都在发展,软件开发和 PHP 也是如此。一些在过去取得成功的模式已经被更新和/或更好的版本所取代。

几年前解决某个问题的标准方法可能现在已经不再是正确的解决方案了。PHP 社区一直在学习和改进,但这种知识尚未均匀分布。因此,为了更明显地指出哪些模式应该避免,它们通常被称为反模式——这显然听起来像是你不希望出现在代码中的东西,对吧?

这样的反模式是什么样的呢?让我们看看第一个例子。

单例

在依赖注入(DI)在 PHP 世界中变得越来越流行之前,我们早已必须处理如何有效地创建实例以及如何使它们在其他类的范围内可用的问题。单例模式提供了一个快速且简单的解决方案,通常看起来是这样的:

$instance = Singleton::getInstance();

静态的getInstance方法非常简单:

class Singleton
{
    private static ?Singleton $instance = null;
    public static function getInstance(): Singleton
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }
}

如果方法被执行,会检查该类的实例是否已经被创建。如果是,它将被返回;如果不是,它将事先创建。这种方法也被称为延迟初始化。在这里,懒惰是个好事,因为它只有在需要时才会初始化,所以节省了资源。

该方法还将在静态的$instance属性中存储新的实例。这一点很引人注目,因为这种方法的实现仅因为静态属性可以在没有要求类实例的情况下具有值。换句话说,我们可以在其自己的类定义中存储类的实例。此外,在 PHP 中,所有对象都是通过引用传递的,即指向内存中对象的指针。这两个特性帮助我们确保总是返回相同的实例。

单例模式实际上相当优雅;因为它也使用静态方法,所以不需要Singleton类的实例。这样,它可以在你的代码的任何地方直接执行,而不需要任何进一步的准备。

易用性是单例最终成为反模式的主要原因之一,因为它会导致范围蔓延。我们在关于依赖注入的部分中解释了这个问题。

另一个问题在于可测试性:用模拟对象替换实例非常困难,因此为使用单例模式的代码编写单元测试变得更加复杂。

现在,您应该使用依赖注入与依赖注入容器一起使用。它不如单例模式容易使用,但反过来这又帮助我们三思而后行,在类中使用另一个依赖项之前。

然而,这并不意味着单例模式根本不能使用。可能存在有效的理由来实现它,或者至少在遗留项目中保留它。只是要意识到风险。

服务定位器

可能被认为有问题的第二个模式是服务定位器

class ServiceLocatorExample
{
    public function __construct(
        private ServiceLocator $serviceLocator
    ) {}
    public function fooBar(): void
    {
        $someService = $this->serviceLocator
          ->get(SomeService::class);
        $someService->doSomething();
    }
}

在这个示例类中,我们在对象的构造时注入ServiceLocator。然后在整个类中使用它来获取所需的依赖项。在这方面,依赖注入和服务定位器都是依赖倒置原则(SOLID中的“D”)的实现:它们将控制其依赖项的范围移出类作用域,帮助我们实现松散耦合的架构。

但是,如果我们只需要注入一个依赖项而不是多个,这不是一个好主意吗?嗯,服务定位器模式的缺点是它将类的依赖项隐藏在ServiceLocator实例后面。而使用依赖注入时,您可以通过查看构造函数清楚地看到使用了哪些依赖项,但在仅注入ServiceLocator时您无法做到这一点。

与依赖注入不同,它不会强迫我们质疑一个类中应该使用哪些依赖项,因为对于较大的类,您很快就会失去对类中使用了哪些依赖项的总体了解。这基本上是我们为单例模式识别出的主要缺点之一。

再次强调,我们不想在服务定位器模式的使用上过于教条。可能存在一些情况下使用它是合适的——只是要小心处理。

摘要

在本章中,我们讨论了标准和规范的重要性。编码规范帮助您与同行开发者就代码的格式达成一致,并且您了解了值得采用的一些现有标准。

编码规范有助于您的团队就如何编写软件达成一致。尽管这些规范对每个团队来说都非常个性化,但我们为您提供了一套良好的示例和最佳实践,以构建您团队的规范。通过代码审查,您也知道如何保持质量。

最后,我们向您介绍了设计模式的世界。我们相信,至少了解这些模式的大部分内容将帮助您与团队成员一起设计和编写高质量的代码。关于这个主题还有更多可以探索的,您将在本章末尾找到一些优秀资源的链接。

这几乎结束了我们关于 PHP 中清洁代码多方面内容的激动人心的旅程。我们确信你现在迫不及待地想要尽快将所有新知识应用到日常工作中。然而,在你这样做之前,请耐心等待最后一章,我们将讨论文档的重要性。

进一步阅读

第十三章:创建有效的文档

许多开发者认为文档是一种负担,而不是有意义的活动。这是可以理解的,因为通常文档在编写后就不再更新。很快,它就会充满错误陈述和过时信息,这确实是没有人希望看到的。

我们坚信,文档的重要性不容忽视,不能放弃。如果做得恰当,它将是一个宝贵的补充,也是编写干净代码的重要基石,尤其是在团队工作中。

因此,在本书的最后一章,我们想给你一些关于如何编写实用且可维护的文档的想法。

本章我们将涵盖以下主要主题:

  • 为什么文档很重要

  • 创建文档

  • 内联文档

技术要求

对于本章,没有额外的技术要求。所有代码示例都可以在我们的 GitHub 仓库中找到:github.com/PacktPublishing/Clean-Code-in-PHP

为什么文档很重要

欢迎来到本书的最后一章。你已经走得很远了,在你暂时放下这本书之前,我们想将你的注意力引向经常被忽视的话题——创建文档。让我们在接下来的几页中说服你,文档并不一定必须是枯燥乏味的,反而可以带来宝贵的益处。

为什么文档很重要

我们为什么要创建任何文档呢?我们的代码或测试不是已经足够作为文档了吗?这些想法中有些是真实的,我们将在本节中进一步讨论这个话题。然而,多年来,无数的开发者从未停止过创建无数的文档,这肯定有它的道理。

我们创建文档是因为我们可以让其他人更容易地使用我们的软件。这关乎上下文,而这些上下文往往不能从阅读几个类的代码中轻易提取出来。文档往往不仅关乎“什么”或“如何”,还关乎“为什么”。

了解导致决策的动机或外部因素对于理解和接受为什么项目以某种方式构建至关重要。例如,你可能会抱怨你的前同事实现了一个脆弱的、由 cronjob 触发的从外部文件传输协议FTP)服务器下载逗号分隔值CSV)文件的下载,除非你从文档中了解到客户在项目截止日期前根本无法提供一个表示状态转移REST应用程序编程接口API)端点来提供数据。

一个新同事加入你的项目,如果至少有一些文档可以阅读,就不需要(可能还会打扰)其他开发者询问每一个小问题,这肯定会让他感到高兴。而且让我们不要忘记我们的未来自己,他们已经好几个月没有接触那个项目了,现在必须修复一个关键错误。如果我们当时知道我们过去自己做了什么……以及为什么。

如果你开发开源软件OSS),那么文档也同样重要。如果你需要评估几个第三方软件包以决定在项目中使用哪一个,那么拥有良好文档的软件包更有可能被考虑。如果你在一个工具上投入了无数小时,但没有人使用它,因为它没有或没有良好的文档,这难道不是一件遗憾的事情吗?

最后,如果你以软件开发为职业,你应该将其视为专业开发者职责的一部分,即编写文档。这正是你获得报酬的原因。

开发者文档

当我们想到文档时,通常首先想到的是用户文档:关于如何使用软件产品的每个功能的冗长、难以阅读且无聊的文本,例如——例如——文字处理器。当然,这份文档存在有很好的理由,但在本书的背景下,这不应该引起我们的兴趣,因为它通常不是由(和为)开发者编写的。

软件文档是一个广泛的领域,因此在本章中无法全面涵盖。我们更希望关注那些支持你在开发过程中,并使你能够编写如以下非详尽列表中所述的清洁代码的文档:

  • 管理和配置指南:除了描述如何安装和配置软件的明显需求外,确保包括一个关于代码质量的部分。这应该包含有关本地使用哪些工具以及它们如何配置的信息。

  • 系统架构文档:一旦你的项目变得足够大,以至于基本的服务器设置(通常是一个物理机器上的 Web 服务器和数据库)成为瓶颈,并且你开始对其进行扩展,你应该考虑记录你的基础设施。最终,这将为你和其他人节省大量时间寻找正确的统一资源定位符URLs)或服务器访问,尤其是在关键情况下。也许这是一个添加有关持续集成CI)管道信息的好地方。

  • 软件架构文档:你的软件是如何构建的内部结构?它是否使用事件在模块之间进行通信?是否有应该使用的队列?这些问题应该在软件架构文档中得到解答。这使得其他开发者更容易遵循原则。

  • 编码规范:除了软件架构文档之外,编码规范还提供了关于如何编写代码的建议。我们在第十二章**,团队合作*中深入讨论了这一主题。

  • API 文档:如果你的PHP:超文本预处理器PHP)应用程序有一个由其他开发者甚至客户使用的 API,你需要提供一个关于 API 功能的良好概述。这使他们的生活和你自己的生活都变得更轻松,因为你会收到更少的关于 API 如何工作的询问。你还可以提供如何构建额外 API 端点的良好示例。

在下一节中,我们想更详细地看看如何使编写这些类型的文档变得更加容易。

创建文档

文档可以以多种方式编写。没有一种正确的方法,通常它是由已经使用的工具预先决定的,例如存储库服务或公司维基。尽管如此,还有一些技巧和窍门可以帮助你编写和维护文档,我们希望在本节中向你介绍这些技巧。

文本文档

让我们首先关注典型的、手动编写的文本文档。经典的方法是建立一个维基,因为它们有一个很大的优点,那就是即使是技术不那么熟练的人也可以访问和使用。这使得它们成为公司的绝佳选择。现代维基,无论是自托管还是软件即服务SaaS),提供了许多保证和有用的功能,如内联注释或版本控制。它们还可以连接到许多外部工具,例如票务系统。

另一个选项是将文档添加到存储库中,使其靠近代码——例如,在子文件夹中。这也是一个有效的方法,特别是对于小型团队或开源项目。不过,你不应该使用像Word便携式文档格式PDF)这样的膨胀格式,而应该专注于基于文本的格式,如Markdown。它们通常要小得多,而且通过版本控制历史记录跟踪对它们的更改也很容易。

手动编写文档的关键是要保持其更新。文本文件或维基很有耐心且不会忘记,随着时间的推移,许多页面的文档几乎堆积在它们的存储中。当不清楚哪些文档是正确的,哪些是过时的时,就会变得有问题。一旦产生怀疑,就完全不可信了。

解决这个问题的唯一方法就是建立一个确保文档得到更新的流程。在前一章中,我们已经介绍了一种可能的方法:与完成定义DoD)相结合的代码审查。这确保了每次我们即将向代码库添加一些新代码或更改代码时,都会通过清单提醒我们更新文档(如果需要的话)。

尤其是系统和软件架构通常使用图表进行文档化。因此,在下一节中,我们想向您展示如何有效地创建这些图表。

图表

一个好的图表通常比长篇大论更有说服力。有许多免费使用的图表工具可供选择,您可以选择手动绘制图表或从文本定义中生成它们。

手动绘制图表

创建图表的传统方式是使用允许您手动绘制的图表工具。这些工具专门设计用于协助您在创建过程中的工作——例如,通过提供模板和图标集,或者当对象移动时保持连接箭头。

在本章中,我们想向您介绍的一个多功能工具是diagrams.netwww.diagrams.net)。实际上,我们也用它来创建本书的插图。它提供了一个元素库,例如,可以用来创建统一建模语言UML)图表和流程图。它还提供了最受欢迎的云服务提供商的图标,例如谷歌云平台GCP)、亚马逊网络服务AWS)和微软 Azure。

如果您打算使用它,我们建议将您的图表保存为可缩放矢量图形SVG)。SVG 基于可扩展标记语言XML),尽管 XML 相当冗长,但它仍然比便携式网络图形PNG)等图形格式消耗更少的磁盘空间。

更重要的是,它可以在编辑器中反复加载和修改,因此每次系统发生变化时,您不必重新开始。大多数集成开发环境IDE)和所有浏览器都将 SVG 文件作为可以无限缩放的图形图像显示,如果需要,它们可以轻松地导出为最流行的图像格式。

从定义生成图表

虽然不是每个人都喜欢使用繁琐的编辑器来绘制图表,但幸运的是,有多种图表工具可以从定义中生成图表。为了展示它们的一般工作原理,我们选择了Mermaid.jsmermaid-js.github.io)作为示例。它是用JavaScript编写的,并使用 Markdown 启发的语言来定义图表。

在我们检查这种方法的优势之前,让我们先看看一个简单的流程图示例:

graph LR 
    A{Do you know how to write great PHP code?} --> B[No]
    A --> C[Yes] 
    C --> E(Awesome!) 
    B --> D{Did you read Clean Code in PHP?} --> F[No] 
    D --> G[Yes] 
    G --> H(Please read it again) 
    F --> I(Please read it)

上述代码将渲染一个图表,如下所示:

图片

图 13.1:Mermaid 图表示例

图表生成工具可以帮助您创建多种图表类型,例如序列图甘特图,甚至是众所周知的饼图。您无需考虑如何设计它们或它们的布局。主要工作由图表工具接管。当然,Mermaid.js 提供了许多方法来影响生成图表的外观。

由于图表定义是简单的文本块,它们可以被添加到代码仓库中。对这些更改的追踪通过版本历史记录非常舒适。Mermaid 图表与 Markdown 文档集成得非常好,因为最流行的 IDE 可以通过额外的扩展直接在文档中显示这些图表。

最后,如果您只是想探索 Mermaid 的可能性,可以使用 Mermaid Live Editor (mermaid.live) 来更好地理解它是如何工作的。

Mermaid 的替代方案

其他值得注意的绘图工具包括 PlantUML (plantuml.com),它提供了更多实用的绘图类型来记录软件架构,以及 Diagrams (diagrams.mingrammer.com),它在记录系统架构方面表现强劲。

文档生成器

可能最好的文档是我们不需要自己创建的,但仍然像人类编写的内容一样有用。不幸的是,这现在仍然是一个梦想,尽管我们不知道机器学习(ML)在未来会带我们走向何方。

目前,我们已可以使用工具从我们的代码中创建文档。至少,我们可以使用它们来汇总项目众多类中分散的信息。

API 文档

在本节中,我们将通过 API 文档的示例向您展示如何从代码中创建文档。如果您的应用程序提供了 API,那么拥有最新的文档是至关重要的。编写此类文档是一个耗时且容易出错的过程,但我们至少可以使其变得容易一些。

存在许多记录 API 的方法。在本书中,我们将向您介绍一种越来越受欢迎的格式:OpenAPI。这种格式,以前称为 Swagger,在 YAML Ain’t Markup Language(YAML)文档中描述了 API 的所有方面,可能看起来像这样:

openapi: 3.0.0
info:
  title: 'Product API'
  version: '0.1'
paths:
  /product:
    get:
      operationId: getProductsUsingAnnotations
      parameters:
        -
          name: limit
          in: query
          description: 'How many products to return'
          required: false
          schema:
            type: integer
      responses:
        '200':
          description: 'Returns the product data'

初看起来,这可能会有些信息过多。不过,请放心——它并不那么复杂。简而言之,前面的 YAML 描述了 Product API 的 0.1 版本,它提供了一个端点 /product。这个端点可以通过 GET 方法调用,并接受可选参数 limit,该参数类型为 integer,必须写入 URL 查询中(例如,/product?limit=50)。如果一切顺利,端点将以 HTTP 状态码 200 返回。

OpenAPI 文档

OpenAPI 格式相当广泛,所以我们无法在本书中涵盖它。如果您想了解更多关于它的信息,请查看官方文档:oai.github.io/Documentation

作为一项受欢迎的好处,IDEs,例如默认支持 PhpStorm,会通过检查模式的有效性来帮助你编写这些 YAML 文件。例如,如果你将 operation 错误地写成了 operationId,IDE 会突出显示错误的使用。

你可以手动编写一个 YAML 文件,或者让它自动生成。我们想更仔细地看看后者的情况。为了实现这一点,我们需要一个名为swagger-phpComposer包的帮助(github.com/zircote/swagger-php)。请参考包文档了解如何安装它。

当然,这个包不能从无到有地神奇地创建文档。相反,swagger-php解析直接写在 PHP 代码中的元信息,无论是作为DocBlock 注解,还是从 PHP 8.1开始作为属性。换句话说,我们需要确保在生成 YAML 文件之前,元信息已经存在。

这些信息看起来是什么样子?让我们看看第一个示例,使用注解

/**
 * @OA\Info(
 *     title="Product API",
 *     version="0.1"
 * )
 */
class ProductController
{
    /**
     * @OA\Get(
     *     path="/product",
     *     operationId="getProducts",
     *     @OA\Parameter(
     *         name="limit",
     *         in="query",
     *         description="How many products to return",
     *         required=false,
     *         @OA\Schema(
     *             type="integer"
     *         )
     *     ),
     *     @OA\Response(
     *         response="200",
     *         description="Returns the product data"
     *     )
     * )
     */
    public function getProducts(): array
    {
        // ...
    }
}

基于 DocBlocks 内的信息,swagger-php将返回我们的 API 文档作为一个 YAML 文件,其外观将完全像前面的示例。但为什么我们要使用swagger-php,而不是直接编写 YAML 文件呢?

事实上,并不是每个人都想在代码中包含大块的文档,而且根据你想要记录的详细程度,它们可能会比我们之前的示例大得多。如果你想到一个 API,它有多个端点分散在你的代码中的各种控制器上,那么你可能已经意识到了好处:所有必要的元信息都存储在代码附近,所以如果端点有变化,开发者只需简单地修改 DocBlock 注解,而不是在额外的文档或维基上做这些更改,这要容易得多。由于注释是代码的一部分,这些更改也已经处于版本控制之下。最终,是否使用swagger-php的决定取决于你或你的团队。

在本章的内联文档部分,我们将讨论为什么 DocBlocks 不是存储元信息的最佳位置。自从 PHP 8.0以来,我们幸运地有了更好的地方来存储它们——即属性,我们已经在第六章**,PHP is Evolving- Deprecations and Revolutions中讨论过。

在我们讨论为什么它们是更好的选择之前,让我们看看如何使用属性来记录我们的端点文档:

use OpenApi\Attributes as OAT;
#[OAT\Info(
    version: '0.1',
    title: 'Product API',
)]
class ProductController
{
    #[OAT\Get(
        path: '/v2/product',
        operationId: 'getProducts',
        parameters: [
            new OAT\Parameter(
                name: 'limit',
                description: 'How many products to return',
                in: 'query',
                required: false,
                schema: new OAT\Schema(
                    type: 'integer'
                ),
            ),
        ],
        responses: [
            new OAT\Response(
                response: 200,
                description: 'Returns the product data',
            ),
        ]
    )]
    public function getProducts(): array
    {
        // ...
    }
}

虽然属性语法可能看起来有些不熟悉,但我们仍然建议使用属性而不是注解,因为它们带来了方便的优势。首先,它们是真正的代码;它们会被 PHP 解释器解析,并且你的 IDE 将能够支持你编写它们。在前面的示例的第一行,你可以看到我们需要导入OpenApi\Attributes命名空间才能使这个示例工作。

在这个命名空间中,你可以找到这里引用的实际类。这些文件位于你的项目的 vendor 文件夹中。这使得你可以使用诸如自动完成等功能,如果你的 IDE 发现某些内容不正确,你会立即得到反馈,这使得编写此类文档变得更加容易。

作为最后一步,你需要从代码生成一个 YAML 文件。当然,这一步可以在我们介绍的 CI 流水线 中自动化第十一章**,持续集成。你可以在我们这本书的 Git 仓库中找到使用示例。

你可能会想:我能用这个 API 文档做什么呢?当然,它已经可以作为其他开发者的文档使用,但还有更多。例如,你可以将其导入到 InsomniaPostman 这样的 HTTP 客户端。这样,你就可以立即开始与 API 交互,而无需查找确切的模式。

另一个用例是帮助你为你的 API 编写功能测试。有一些包,如 PHP Swagger Test (github.com/byjg/php-swagger-test) 或 Spectator (github.com/hotmeteor/spectator),可以帮助你编写针对 contract 的测试。

最后,可能是最重要的用例,是使用 OAS 规范与 Swagger UI (github.com/swagger-api/swagger-ui),这是一个 API 的视觉和交互式文档。

以下截图显示了我们的示例 API 的外观:

图片

图 13.2:Swagger UI

探索 OpenAPI 和 Swagger UI 的所有可能性超出了我们这本书的范围。如果你想了解更多关于这两个工具的信息,我们建议你检查一下。

OpenAPI 替代方案

还有其他格式,例如 RESTful API Modeling Language (RAML) (raml.org) 或 API Blueprint (apiblueprint.org),你可以使用,我们不对任何解决方案有偏见。

内联文档

一个特殊情况是我们中的许多人自从开始编写软件以来就一直在做的文档:注释。这些注释直接写在代码中,开发者可以立即看到它们,所以这似乎是一个放置文档的好地方。但是,注释真的应该被视为或用于文档吗?

在我们看来,通常应该避免注释。让我们看看下一页的一些论点。

注解不是代码

注释不是代码的一部分。虽然可以通过 PHP 的反射 API 解析注释,但它们最初并不是为了存储元信息而设计的。理想情况下,你的软件在移除所有注释后仍然应该能够正常工作。

然而,现在这种情况往往不再是这样了。框架和包,如对象关系映射器ORMs)使用 DocBlock 注释来存储信息,例如路由定义或数据库对象之间的关系。一些代码质量工具使用注释来控制它们在代码某些部分的行为。

PHP 无法在您的注释错误时抛出错误信息。如果它们有重要的用途,您的测试可能会在它们部署到生产环境之前发现这些错误。更好的选择是属性,它们是真正的语言结构。我们在本章前面讨论 API 文档时已经详细讨论了这些内容。

不可读的代码

此外,正如我们在第十二章中已经讨论过的,团队合作,注释通常是代码过于复杂的指标。与其解释你的代码,你更应该努力编写不需要注释的代码。

将整个函数压缩成一行代码可能是一种有趣的练习——例如,通过使用一些四重嵌套的三元运算符或一个令人恐惧的复杂if条件,没有人会理解。当你发现生产环境中有一个高优先级的错误,而你又完全不知道你的神秘杰作原本应该做什么时,你将后悔写下它。

或者,更糟糕的是,你的新同事在他们的第一次值班时,有幸在深夜调试,这时警报不断传来。有更好的方式开始一段工作关系。

过时的注释

注释很快就能写出来,但也很快会被忘记。由于 PHP 解释器不会解析注释,当它们不再正确时,你将不会得到通知——例如,当它们应该解释的函数被重写并突然服务于不同的目的时。除了开发者尝试阅读和理解它们的含义,并将其与实际函数代码进行比较之外,没有其他方法来验证注释。

在撰写本文时,这可能听起来不是问题,但想象一下一年后回到一个类,发现你不再理解的注释。你当初为什么要写它?如果你不知道原因,其他人又怎么知道呢?

过时的注释是代码中的错误信息。它们会分散注意力,并且成本高昂,因为开发者的时间不是免费的。因此,在添加它们之前,请三思。

无用的注释

尽量避免那些陈述明显事实且不提供任何额外信息的注释。以下代码片段是这种情况的真实例子:

// write the string to the log file
file_put_contents($logFileName, $someString)

虽然开发者花时间解释file_put_contents函数可能是一种好的姿态,但这并不增加代码的价值。如果你不知道一个函数,你可以查阅它。除此之外,它只是一行不必要的代码,你需要在阅读代码时扫描。

有时很难在有用的和无用的注释之间划清界限。你可以使用代码审查来解决这个问题;如第十二章中讨论的,在团队中工作,让团队中的其他人诚实地审查你的代码将有助于避免这些注释。

错误或无用的 DocBlocks

我们已经讨论了 DocBlocks 以及它们在第十二章中造成的问题,在团队中工作,当我们介绍编码指南时。简而言之,由于 DocBlocks 基本上是注释(但遵循一定的结构),如果函数调用的参数发生变化而 DocBlock 中的必要更改没有更新,它们可能会很快过时或出错。你的 IDE 可能会抛出警告,但 PHP 不会。

随着 PHP 中更好的类型提示的引入,许多 DocBlocks 可以简单地被移除。这种冗余没有任何好处,如果实际代码与注释不符,反而可能会让读者感到困惑。

TODO 注释

注释不是存储任务的合适地方。你可能知道这样的注释:

// TODO refactor once new version of service XY is released

虽然一些 IDE 可以帮助你管理你的TODO注释,但这种方法只有在你是项目唯一成员的情况下才会有效。一旦你在团队中工作,使用 JIRA、Asana 或甚至 Trello 这样的工作管理工具,写这样的注释就只是创建技术债务的一种方式,换句话说,你是在将任务推迟到未来的某个不确定的日子。希望有人有一天能修复它——但大多数情况下,这种情况是不会发生的。

不要用注释,而是考虑在你的首选工作管理工具中创建一个任务。这样,你的同事可以清楚地看到,而且更容易规划这项工作。

当注释有用时

在讨论了不应该注释的内容之后,还有哪些情况下注释是有用的呢?确实,并不多,但仍然有一些场合下注释是有意义的,例如以下情况:

  • 为了避免混淆:如果你能预见到其他开发者可能会对你的实现选择感到疑惑,你应该通过添加注释来提供更多的上下文。

  • 在实现复杂算法时:即使我们试图避免,有时我们不得不编写难以理解的代码——例如,如果我们需要实现某个算法或某些未知的业务逻辑。在这些情况下,简短的注释可能是一个救命稻草。

  • 仅供参考:如果你的代码实现了某些已经在其他地方解释过的逻辑——例如,在维基或工单中——你可以添加一个链接到相应的源,以便其他人更容易找到更多关于它的信息。这应该只是一种例外,而不是规则。

请记住,我们不想过于教条。如果你觉得在某些地方需要注释,就写下来。它仍然可以被删除,可能是在与另一位开发者在代码审查中讨论了该主题之后。

测试作为文档

经常编写测试的开发者常说,这些测试也起到了文档的作用。我们也在第十章**自动化测试中提出了这个观点,当时我们讨论了自动化测试的好处。

如果你不知道一个类的目的是什么,你至少可以从测试中推断出它的预期行为,因为这正是测试所做的:它们对代码进行断言,代码将被测试。通过查看这些断言,你知道代码应该做什么。

如果测试失败,你至少知道断言和实际代码之间存在差异,你现在不能信任它们。除非在你的项目中通常不忽略测试失败,你可以确信有人会很快修复它们——换句话说,隐含的文档会得到更新。

如果所有测试都通过,你就知道你可以信任类的实现——前提是测试写得很好,并且不仅仅测试模拟对象的实现,正如我们在第十章**自动化测试中讨论的,当时我们讨论了单元测试

当然,阅读和理解测试并不是最容易的文档形式,但如果没有其他文档,它们可以是一个可靠的真相来源SOT)。然而,它们不应该成为你项目中唯一的文档类型。

摘要

编写干净的代码不仅要知道如何自己做到,还要确保其他开发者也会遵循这条道路。为了做到这一点,他们需要知道适用于项目的规则。

在本章中,我们讨论了如何创建可以帮助你实现这一目标的文档。我们讨论了手动编写文档的最佳实践,以及创建信息丰富且同时易于维护的图表。最后,我们介绍了从代码生成文档的方法,并详细阐述了内联文档的优缺点。

恭喜!你已经读完了这本书的结尾。我们希望你喜欢阅读它,并且现在已经完全有动力去编写干净的代码。

你可能一开始不会成功。加强你的编码技能是一个可能令人沮丧,有时甚至在商业项目中很难做到的过程。试着保持耐心,随着时间的推移,你会越来越好。

仅仅阅读一本关于编写清洁代码的书肯定是不够的。在这本书的过程中,我们往往只能仅仅触及表面的话题,我们鼓励你深入探索那些你感兴趣的话题——以及那些你一开始可能并不认为有趣的话题。这需要更多的研究、开放的心态,以及愿意接受他人反馈的意愿来提升你的技能。

然而,我们确信,通过这本书,我们为你未来的 PHP 开发者之路提供了不仅仅是坚实的起点。如果你也这样认为,我们将非常高兴。

进一步阅读

如果你想要了解更多关于 Mermaid.js 的信息,我们推荐由 Knut Sveidqvist 和 Ashish Jain 合著,并由 Packt 在 2021 年出版的《Mermaid.js 官方指南》一书。

posted @ 2025-09-07 09:14  绝不原创的飞龙  阅读(3)  评论(0)    收藏  举报