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

原文:PHP 8 Objects, Patterns, and Practice

协议:CC BY-NC-SA 4.0

十四、好的(和坏的)实践

到目前为止,在这本书里,我一直专注于编码,特别是设计在构建灵活的、可重用的工具和应用中的作用。然而,开发并没有随着代码而结束。有可能从书本和课程中获得对语言的扎实理解,但在运行和部署项目时仍然会遇到问题。

在这一章中,我将超越代码,介绍一些构成成功开发过程基础的工具和技术。本章将涵盖以下内容:

  • 第三方软件包:从哪里获得,何时使用

  • 构建:创建和部署包

  • 版本控制:给开发过程带来和谐

  • 文档:编写易于理解、使用和扩展的代码

  • 单元测试:一个自动化缺陷检测和预防的工具

  • 标准:为什么从众有时是好的

  • 一个使用虚拟化的工具,这样所有的开发者都可以在一个类似于生产环境的系统中工作,不管他们的硬件或者操作系统是什么

  • 持续集成(Continuous integration):使用这种实践和工具集来自动化项目构建和测试,并在出现问题时得到提醒

超越代码

当我第一次从独立工作中毕业并在一个开发团队中任职时,我惊讶于其他开发人员似乎必须知道这么多东西。善意的争论在看似至关重要的问题上无休止地酝酿着:哪个是最好的文本编辑器?团队应该标准化集成开发环境吗?我们应该强加一个编码标准吗?我们应该如何测试我们的代码?我们应该在开发的时候记录吗?有时,这些问题似乎比代码本身更重要,我的同事们似乎通过某种奇怪的渗透过程获得了该领域的百科知识。

我读过的关于 PHP、Perl 和 Java 的书当然没有在很大程度上偏离代码本身。正如我已经讨论过的,大多数关于编程平台的书籍很少偏离它们对代码设计中函数和语法的紧密关注。如果设计偏离了主题,你可以肯定,更广泛的问题,如版本控制和测试,很少被讨论。这不是批评——如果一本书声称涵盖了一种语言的主要特征,那么这就是它所做的一切也就不足为奇了。

然而,在学习代码的过程中,我发现我忽略了项目日常生活中的许多机制。我发现其中一些细节对我参与开发的项目的成败至关重要。在这一章中,以及在接下来的章节中的更详细的内容中,我将超越代码来探索一些工具和技术,你的项目的成功可能依赖于它们。

借一个轮子

当在项目中面临一个具有挑战性但又不连续的需求时(可能需要解析一种特定的格式,或者在与远程服务器的对话中使用一种新的协议),构建一个满足这种需求的组件有很多好处。这也是学习手艺的最好方法之一。在创建包的过程中,您深入了解了一个问题,并将可能有更广泛应用的新技术归档。

你立刻投资于你的项目和你自己的技能。通过将功能保留在系统内部,您可以让用户不必下载第三方软件包。偶尔,你也可以回避棘手的许可问题。当你测试你自己设计的一个组件并发现,奇迹中的奇迹,它工作了——它完全按照你在罐子上写的那样工作时,没有什么比这更令人满意的了。

当然,这一切都有黑暗的一面。许多软件包代表了数千个工时的投资:一种您手头可能没有的资源。您可以通过只开发项目特别需要的功能来解决这个问题,而第三方工具也可以满足无数的其他需求。然而,问题仍然存在:如果一个免费的工具存在,为什么你要浪费你的天赋去复制它呢?您有时间和资源来开发、测试和调试您的包吗?这一次部署在别处不是更好吗?

说到轮子发明,我是最糟糕的罪犯之一。找出问题并发明解决方案是我们作为程序员的基本职责。与编写一些胶水将三四个现有组件粘在一起相比,着手一些严肃的架构是一个更有回报的前景。当这种诱惑来临时,我提醒自己过去的项目。尽管从零开始构建的选择在我的经历中从未扼杀过一个项目,但我看到它吞噬了时间表,扼杀了利润空间。我坐在那里,眼里闪着狂热的光芒,策划着情节,旋转着类图,当我沉迷于我的组件的细节时,没有注意到大画面已经成为遥远的记忆。

现在,当我规划一个项目时,我会试着对代码库中的内容和第三方需求有一个感觉。例如,您的应用可能会生成(或读取)一个 RSS 提要;您可能需要验证电子邮件地址并自动发送邮件、验证用户身份或读取标准格式的配置文件。所有这些需求都可以通过外包来满足。

在这本书的前几个版本中,我建议 PEAR (PHP 扩展和应用存储库)是软件包的发展方向。然而,时代变了,PHP 世界已经非常明确地转向了 Composer 依赖管理器及其默认的存储库 packagest(https://packagist.org)。因为 Composer 基于每个项目来管理包,所以它不太可能出现可怕的依赖地狱综合症(不同的包需要相同库的不兼容版本)。此外,所有动作都转移到了 Composer/Packagist,这意味着您更有可能在那里找到您想要的东西。此外,许多 PEAR 包都可以通过 Packagist(包装商)( https://packagist.org/packages/pear/ )获得。

所以,一旦你确定了你的需求,你的第一站应该是 Packagist 网站。然后,您可以使用 Composer 来安装您的软件包并管理软件包依赖性。我将在下一章更详细地介绍 Composer。

为了让您对使用 Composer 和 Packagist 可以做些什么有所了解,下面是您可以在那里找到的软件包可以做的一些事情:

  • 使用pear/cache_lite缓存输出

  • 使用athletic/athletic基准库测试代码的效率

  • doctrine/dbal抽象数据库访问的细节

  • 使用simplepie/simplepie提取 RSS 提要

  • pear/mail发送带有附件的邮件

  • symfony/config解析配置文件格式

  • league/uri解析和操作 URL

Packagist 网站提供了一个强大的搜索工具。你可能会在那里找到满足你需求的软件包,或者你可能需要使用搜索引擎扩大搜索范围。无论哪种方式,您都应该在着手重新发明轮子之前花时间评估现有的包。

你们有一种需求——并且有解决这种需求的一揽子方案——这一事实不应成为你们审议的起点和终点。虽然最好使用一个包,这样可以节省不必要的开发,但是在某些情况下,它会增加开销而没有真正的好处。例如,您的客户需要您的应用发送邮件,这并不意味着您应该自动使用 pear/mail 包。PHP 提供了一个非常好的mail()函数,所以这可能是您的第一站。一旦您意识到您需要根据 RFC822 标准验证所有电子邮件地址,并且设计团队希望通过电子邮件发送图像附件,您就可以开始以不同的方式权衡这些选项。碰巧的是,pear/mail 支持这两个特性(后者与mail_mime结合使用)。

许多程序员,包括我自己,经常过分强调原始代码的创建,有时会损害他们的项目。

Note

不愿使用第三方工具和解决方案通常是机构层面的固有问题。这种以怀疑的态度对待外部产品的倾向有时被称为不是这里发明的综合症。作为进一步的说明,技术评论家和科幻迷保罗·特里戈指出不是这里发明的也是伊恩·M·班克斯文化系列中一艘船的名字。

这种对作者身份的强调可能是可重用代码的创造似乎多于实际使用的一个原因。

卓有成效的程序员将原始代码视为帮助他们设计项目成功结果的工具之一。这样的程序员看着他们手头的资源,并有效地部署它们。如果有一个方案可以承受一定的压力,那么这就是胜利。借用 Perl 世界的一句格言:好的程序员是懒惰的。

友好相处

萨特的名言“地狱是其他人”的真实性在一些软件项目中每天都得到证明。这可能描述了客户和开发人员之间的关系,这种关系的典型表现是缺乏沟通会导致特性的蔓延和优先级的扭曲。但是这个上限也适用于快乐交流和合作的团队成员,当谈到共享代码的时候。

一旦一个项目有多个开发人员,版本控制就成了一个问题。一个单独的编码员可能就地处理代码,在开发的关键点保存她的工作目录的副本。引入另一个程序员,这个策略在几分钟内就失效了。如果新的开发人员在同一个开发目录中工作,那么一个程序员在保存时很有可能会覆盖他同事的工作,除非两个人都非常小心地总是在不同的文件上工作。

或者,我们的两个开发人员可以各自开发一个版本的代码库。这很好,直到调和两个版本的时刻到来。除非开发人员已经处理了完全不同的文件集,否则合并两个或更多开发链的任务会变得非常令人头疼。

这就是 Git、Subversion 和类似工具的用武之地。使用版本控制系统,你可以检查出你自己版本的代码库,然后继续工作,直到你对结果满意为止。然后,您可以用同事所做的任何更改来更新您的版本。版本控制软件会自动将这些更改合并到您的文件中,并通知您它无法处理的任何冲突。一旦您测试了这个新的混合体,您就可以将它保存到中央存储库中,让其他开发人员可以使用它。

版本控制系统为您提供了其他好处。它们保存了项目所有阶段的完整记录,因此您可以回滚到项目生命周期中的任何一点,或者获取其快照。您还可以创建分支,这样您就可以在维护一个公开发布版本的同时维护一个前沿的开发版本。

一旦你在一个项目中使用了版本控制,你就不想在没有版本控制的情况下尝试另一个项目。同时处理一个项目的多个分支可能是一个概念性的挑战,尤其是在开始的时候,但是好处很快就变得明显了。版本控制太有用了,离不开它。我在第十七章中介绍了 Git。

Note

这本书的当前版本是使用 Git 作为协作工具用纯文本编写和编辑的。

赋予你的代码翅膀

你见过你的代码因为太难构建而搁浅吗?对于就地开发的项目来说尤其如此。这样的项目融入到它们的上下文中,密码和目录、数据库以及助手应用调用都被编程到代码中。部署这类项目可能是一项艰巨的任务,程序员团队需要通过挑选源代码来修改设置,以便适应新的环境。

通过提供一个集中的配置文件或类,可以在一个地方更改设置,从而在一定程度上缓解这个问题。但即便如此,构建也可能是一件苦差事。安装的难易程度将对您发布的任何应用的受欢迎程度产生重大影响。它还会阻碍或鼓励开发过程中的多次频繁部署。

与任何重复且耗时的任务一样,构建应该是自动化的。构建工具可以确定安装位置的默认值、检查和更改权限、创建数据库、初始化变量以及其他任务。事实上,构建工具可以做您需要的任何事情,从发布到完全部署的源目录中获取应用。

当然,这并不能免除用户向代码中添加环境信息的责任,但是它可以使这个过程变得简单,只需回答几个问题或提供几个命令行开关。

亚马逊的 AWS Elastic Beanstalk 等云产品使得根据需要创建测试和试运行环境成为可能。为了充分利用这些资源,良好的构建和安装解决方案是必不可少的。如果您不能即时部署您的系统,那么自动配置服务器是没有用的。

开发人员可以使用各种构建工具。PEAR 和 Composer 都管理安装(PEAR 集中管理,Composer 管理本地vendor目录)。您可以为任一系统创建自己的包,然后用户可以轻松地下载和安装这些包。然而,构建不仅仅是将文件 A 放在位置 B 的过程。

在第十九章,我会看一个叫 Phing 的应用。这个开源项目是流行的 Ant 构建工具的一个移植,它是用 Java 编写的,也是为 Java 编写的。Phing 是用 PHP 编写的,但它在架构上类似于 Ant,并为其构建文件使用相同的 XML 格式。

Composer 非常好地执行有限数量的任务,并提供尽可能简单的配置。Phing 一开始更令人生畏,但是它有着巨大的灵活性。您不仅可以使用 Phing 来自动化从文件复制到 XSLT 转换的任何事情,如果您需要扩展该工具,还可以轻松编写和合并您自己的任务。Phing 是使用 PHP 的面向对象特性编写的,其设计强调模块化和易于扩展。

构建工具和那些为包或依赖管理而设计的工具并不互相排斥。通常,在开发过程中使用构建工具来运行测试,执行项目内务管理,并准备最终通过 PEAR、Composer 甚至基于发行版的包管理系统(如 RPM 和 Apt)部署的包。

标准

我以前提到过,这本书已经把它的重点从梨转移到作曲家。这是因为 Composer 比 PEAR 好太多了吗?我确实喜欢很多关于 Composer 的东西,这些东西可能会影响我的决定。然而,这本书改变的主要原因是其他人都改变了。Composer 已经成为依赖管理的标准。这是至关重要的,因为这意味着当我在 Packagist 找到一个包时,我也有可能找到它的所有依赖项和相关包。我甚至会在那里找到很多梨包。

因此,为依赖关系管理选择一个标准可以确保可用性和互操作性。但是标准不仅仅适用于包和依赖,还适用于系统工作的方式和我们编码的方式。如果我们在协议上达成一致,那么我们的系统和团队可以无缝地相互集成。而且,随着越来越多的组件跨越来越多的系统混合,这变得越来越重要。

如果需要一种明确的处理方式,比如说日志记录,那么我们采用最好的协议显然是理想的。但是建议的质量(将规定格式、日志级别等。)可能没有我们都遵守它这一事实重要。如果只有你一个人在执行最好的标准,那是没有好处的。

在第十五章中,我将更详细地讨论标准,特别是 PHP-FIG 小组管理的一组建议。这些 PSR(PHP 标准建议)涵盖了从缓存到安全的所有内容。在这一章中,我将关注 PSR-1 和 PSR-12,这两个建议解决了编码风格的棘手问题(你喜欢把括号放在哪里?你对别人告诉你改变做事方式有什么感觉?).然后我继续讨论 PSR-4 的绝对优势,包括自动加载(对 PSR-4 的支持是 Composer 擅长的另一个领域)。

Vagrant

你的团队使用什么操作系统?当然,有些组织要求特定的硬件和软件组合。不过,通常会有混合。一个开发人员可能有一台运行 Fedora 的开发机器。另一个人可能会信赖他的 MacBook,第三个人可能会坚持使用他的外星人 Windows box(他可能喜欢用它来玩游戏)。

很有可能制作系统将完全在别的东西上运行——也许是 CentOS。

让一个系统跨多个平台工作可能是一件痛苦的事情,如果这些平台都不像生产系统,这可能是一个风险。您真的不想在上线后发现与生产操作系统相关的问题。当然,在实践中,您可能会首先部署到一个临时环境。即便如此,早点抓住这些问题不是更好吗?

流浪者是一种利用虚拟化给所有团队成员一个尽可能接近生产的开发环境的技术。启动和运行应该像调用一两个命令一样简单,最棒的是,每个人都可以坚持使用他们最喜欢的机器和发行版(我是 Fedora 迷,请记住)。

我在第二十章中讲述流浪者。

测试

当你创建一个类时,你可能非常确定它是有效的。毕竟,您将在开发过程中测试它的速度。您还将在组件就位的情况下运行您的系统,检查它是否集成良好,以及您的新功能是否可用并按预期执行。

你能确定你的类会像预期的那样继续工作吗?这似乎是一个愚蠢的问题。毕竟,你已经检查过你的代码一次;为什么它要任意停止工作?嗯,当然不会;没有什么事情是随意发生的,如果您从不在系统中添加任何代码,您就可以轻松地呼吸了。另一方面,如果您的项目是活动的,那么不可避免的是,您的组件的上下文将会改变,并且组件本身很可能会以多种方式改变。

让我们依次来看这些问题。首先,改变一个组件的上下文是如何引入错误的?即使在一个组件彼此很好地解耦的系统中,它们仍然是相互依赖的。您的类使用的对象返回值、执行操作并接受数据。如果这些行为中的任何一个发生变化,对您的类的操作的影响可能会导致一种很容易捕捉到的错误——这种类型的错误,您的系统会显示一个方便的错误消息,其中包括文件名和行号。然而,更阴险的是,这种变化不会导致引擎级别的错误,但仍然会混淆您的组件。如果您的类基于另一个类的数据做出假设,该数据的变化可能会导致它做出错误的决策。您的类现在有错误,并且没有更改一行代码。

很可能你会继续改变你刚刚完成的课程。通常,这些变化是微小而明显的——事实上,如此微小,以至于您不需要仔细检查您在开发过程中执行的检查。不管怎样,你可能已经把它们都忘记了,除非你以某种方式保存它们(也许像我有时做的那样,在你的类文件的底部注释掉了)。然而,小的变化会导致大的意想不到的后果——如果您想在适当的地方放置一个测试工具,这些后果可能已经被发现了。

一个测试工具是一组自动化的测试,它可以被应用到你的系统中作为一个整体,也可以被应用到它的单独的类中。部署好了,测试工具可以帮助你防止错误的发生和重复出现。一个简单的改变可能导致一连串的错误,测试工具可以帮助您定位并消除这些错误。这意味着你可以自信地做出改变,不会破坏任何东西。对你的系统进行改进,然后看到一个失败测试的列表,这是非常令人满意的。这些都是可能在您的系统中传播的错误,但现在它不会再遭受这些错误了。

持续集成

你有没有制定过让一切都好起来的时间表?你从一个任务开始:也许是代码或者一个学校项目。又大又吓人,潜伏着失败。但是你拿出一张纸,把它分成容易处理的任务。你决定要读的书和要写的组件。也许你用不同的颜色突出显示任务。事实证明,就个人而言,没有一项任务真的那么可怕。渐渐地,随着你的计划,你征服了最后期限。只要每天做一点点,就没事了。你可以放松。

不过,有时候,这个时间表有一种魔力。你把它像盾牌一样举起来,保护自己免受怀疑和潜移默化的恐惧,也许这一次你会崩溃和燃烧。只有在几周之后,你才会意识到日程表本身并不神奇。你实际上也必须做这项工作。当然,到那时,在时间表令人安心的力量的催眠下,你已经让事情顺其自然了。除了制定一个新的时间表,别无他法。这一次,就不那么让人放心了。

测试和构建也是如此。你必须进行测试。你必须定期在新鲜的环境中构建你的项目;否则,魔法不起作用。

如果编写测试是一件痛苦的事情,那么运行测试也是一件苦差事,尤其是当它们变得越来越复杂,并且失败会打断你的计划的时候。当然,如果你更经常地运行它们,你可能会有更少的失败,并且那些你确实有过的失败会有很好的机会与你头脑中新鲜的新代码相关联。

在沙盒里很容易变得舒服。毕竟,您已经有了所有的玩具:让您的生活变得简单的小 scriptlets、开发工具和有用的库。问题是你的项目也可能在你的沙箱里太舒服了。它可能开始依赖于您在构建文件中遗漏的未提交代码或依赖项。这意味着除了你工作的地方,其他地方都坏了。

唯一的答案是建设,建设,再建设。每次都在一个相当原始的环境中进行。

当然,提出这样的建议当然很好;做这件事完全是另一回事。作为一个品种,程序员往往喜欢编码。他们想把会议和家务减到最少。这就是持续集成(CI)的用武之地。CI 既是一种实践,也是一套使实践尽可能简单的工具。理想情况下,构建和测试应该是完全自动的,或者至少可以通过一个命令或点击来启动。任何问题都会被跟踪,在问题变得太严重之前,您会得到通知。我会在第二十一章里多讲讲 CI。

摘要

开发人员的目标总是交付一个工作系统。写好代码是实现这一目标的重要部分,但不是全部。

在这一章中,我介绍了 Composer 和 Packagist 的依赖管理。我还讨论了协作的两大辅助工具:流浪者和版本控制。我介绍了为什么版本控制需要自动化构建,还介绍了 Phing,它是 Ant 的 PHP 实现,是一个 Java 构建工具。最后,我讨论了软件测试并介绍了 CI,这是一套自动化构建和测试的工具。

十五、PHP 标准

除非你是律师或卫生检查员,否则标准的话题可能不会让你心跳加速。然而,帮助我们实现的标准是值得兴奋的。标准促进了互操作性,这使我们能够访问大量兼容的工具和框架组件。

本章将涵盖标准的几个重要方面:

  • 为什么是标准:什么是标准以及它们为什么重要

  • PHP 标准建议:它们的起源和目的

  • PSR-1:基本编码标准

  • PSR-12:扩展编码风格

  • PSR-4:自动加载

为什么是标准?

设计模式互操作。这是他们的核心。设计模式中描述的一个问题提出了一个特定的解决方案,这反过来又产生了架构上的结果。这些都可以通过新的模式得到很好的解决。模式还有助于开发人员进行互操作,因为它们提供了共享的词汇表。面向对象的系统倾向于遵循友好的原则。

随着我们越来越多地共享彼此的组件,这种非正式的互操作性趋势并不总是足够的。正如我们所看到的,Composer(或者我们选择的包管理系统)允许我们在项目中混合和匹配工具。这些组件可以被设计成独立的库,也可以是一个更大的框架的一部分。无论哪种方式,一旦部署到我们的系统中,它们必须能够与任意数量的其他组件一起工作和协作。通过坚持核心标准,我们降低了工作遇到兼容性问题的可能性。

从某种意义上来说,标准的本质不如它被遵守的事实重要。例如,就我个人而言,我并不喜欢 PSR-12 风格指导原则的每一个方面。在大多数情况下,包括这本书,我都采用了这个标准。我团队中的其他开发人员会希望发现我的代码更容易使用,因为他们会发现它是一种熟悉的格式。对于其他标准,比如自动加载,不遵守通用标准将导致组件在没有额外中间件的情况下根本无法协同工作。

标准可能不是编程中最令人兴奋的方面。然而,他们的核心有一个有趣的矛盾。标准似乎会扼杀创造力。毕竟标准告诉你什么能做什么不能做。你必须服从。你可能会认为这算不上创新。然而,我们把互联网带给我们生活的创造力的巨大繁荣归功于这样一个事实,即这个网络中的每个节点都符合开放标准。困在围墙花园里的专有系统必然在范围和寿命上受到限制——不管它们的代码有多聪明,它们的界面有多光滑。互联网通过其共享协议,确保任何站点都可以链接到任何其他站点。大多数浏览器支持标准的 HTML、CSS 和 JavaScript。我们可以在这些标准中构建的接口并不总是我们想象中最令人印象深刻的(尽管限制比以前少得多);尽管如此,遵守这些原则使我们能够最大限度地扩大工作范围。

如果使用得当,标准可以促进开放、合作,并最终促进创造力。这是真的,即使标准本身有一些限制。

PHP 标准推荐有哪些?

在 2009 年 php[tek]大会上,一群框架开发人员成立了一个组织,他们称之为 php 框架互操作组(PHP-FIG)。从那以后,开发人员从其他关键组件加入进来。他们的目的是建立标准,以便他们的系统能够更好地共存。

该小组对标准提案进行投票,这些提案从起草到审查,最后获得通过。

表 15-1 列出了撰写本文时的现行标准。

表 15-1

被接受的 PHP 标准建议

|

PSR 数

|

名字

|

描述

|
| — | — | — |
| one | 基本编码标准 | PHP 标签和基本命名约定等基础知识 |
| three | 记录器接口 | 日志级别和记录器行为的规则 |
| four | 自动装载标准 | 命名类和名称空间的约定,以及它们到文件系统的映射 |
| six | 缓存接口 | 缓存管理规则,包括数据类型、缓存项生命周期、错误处理等。 |
| seven | HTTP 消息接口 | HTTP 请求和响应的约定 |
| Eleven | 容器接口 | 依赖注入容器的公共接口 |
| Twelve | 扩展编码风格指南 | 代码格式,包括大括号、参数列表等的放置规则。 |
| Thirteen | 链接定义接口 | 描述超媒体链接的接口 |
| Fourteen | 事件调度程序 | 事件管理的定义 |
| Fifteen | HTTP 处理程序 | HTTP 服务器请求处理程序的公共接口 |
| Sixteen | 简单缓存 | 缓存库的公共接口(PSR-6 的简化) |
| Seventeen | HTTP 工厂 | 创建符合 PSR 7 标准的 HTTP 对象的工厂的通用标准 |
| Eighteen | 客户端 | 用于发送 HTTP 请求和接收 HTTP 响应的接口 |

为什么特别是 PSR?

那么,为什么选择一个标准而不是另一个呢?碰巧 PHP 框架互操作小组 PSRs 的发起者——有一个非常好的血统,因此标准本身是有意义的。但是,这些也是主要框架和组件正在采用的标准。如果您正在使用 Composer 向您的项目添加功能,您已经在使用符合 PSRs 的代码。通过使用它的自动加载惯例和样式指南,您很可能构建了准备好与其他人和组件协作的代码。

Note

一套标准本身并不优于另一套标准。当您选择是否采用一个标准时,您的选择可能是由您对推荐标准优点的判断所决定的。或者,你可以根据你工作的环境做出务实的选择。例如,如果你在 WordPress 社区工作,你可能想要采用在 https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/ 的核心贡献者手册中定义的风格。这样的选择是标准的一部分,它是关于人和软件的合作。

PSR 是一个很好的选择,因为它们受到关键框架和组件项目的支持,包括 Phing、Composer、PEAR、Symfony 和 Zend 2。像模式一样,标准也是有感染力的——你可能已经从中受益了。

PSR 是给谁的?

表面上,PSR 是为框架的创建者设计的。然而,PHP-FIG 小组的成员迅速扩大,包括了工具和框架的创建者,这一事实表明标准具有广泛的相关性。也就是说,除非您正在创建一个日志记录器,否则您可能不需要太担心 PSR-3 的细节(除了确保您使用的任何日志记录工具本身是兼容的)。另一方面,如果你已经读完了这本书的其余部分,那么你很有可能在使用工具的同时也在创造工具。因此,您也可能会在当前的标准或未来的标准中找到与您相关的内容。

然后是对我们所有人都很重要的标准。例如,尽管风格指南很乏味,但它们与每个程序员都有关系。虽然管理自动加载的规则实际上适用于那些创建自动加载器的人(并且最主要的游戏可能是 Composer 的),但是它们也从根本上影响我们如何组织我们的类、我们的包和我们的文件。

出于这些原因,在本章的剩余部分,我将集中讨论编码风格和自动加载。

风格编码

我倾向于发现像“你的牙套放错地方了”这样的拉请求评论非常令人恼火。这种投入通常看起来吹毛求疵,危险地接近自行车脱落。

Note

如果你没有遇到它,动词“自行车棚”指的是倾向于在一些评论家批评不重要的元素,一个项目在审查。这意味着选择这些元素是因为它们符合评论者的能力范围。因此,考虑到要评估的是一座摩天大楼,一个特别的经理可能不会关注巨大而复杂的玻璃钢塔,而是关注后面更容易理解的自行车棚。维基百科有一个很好的历史术语: https://en.wikipedia.org/wiki/Law_of_triviality

然而,我逐渐认识到,遵循一种共同的风格有助于提高代码的质量。这主要是可读性的问题(不考虑特定规则背后的推理)。如果团队遵守相同的缩进、括号位置、参数列表等规则,那么开发人员可以快速评估并贡献同事的代码。

因此,对于本书的这个版本,我承诺编辑所有代码示例,使它们符合 PSR-1 和 PSR-12。我也请我的同事兼技术编辑保罗·特雷哥让我这样做。这是一个在计划阶段就很容易做出的承诺——比我预期的要多得多。这让我想起了我学到的第一堂风格指南课。如果可能的话,尽早为你的项目采用一个标准。重构一种代码风格可能会占用资源,并使检查跨越大重组时代的代码差异变得困难。

那么我必须应用什么变化呢?让我们从基础开始。

PSR-1 基本编码标准

这些是 PHP 代码的基础。你可以在 www.php-fig.org/psr/psr-1/找到它们的详细信息。让我们把它们分解开来。

开始和结束标签

首先,一个 PHP 部分应该用<?php或者<?=打开。换句话说,不应该使用简短的开始标记<?,也不应该使用任何其他变体。一个部分应该只以?>结束(或者,正如我们将在下一部分看到的,根本没有标记)。

Note

PSR 遵循一套词汇定义,如应该必须来确定指令应该遵守的程度。虽然本章将依赖于这些单词的简单英语含义,但在 PSR 上下文中的绝对预期含义是在 www.ietf.org/rfc/rfc2119.txt. 处定义的

副作用

一个 PHP 文件应该声明类、接口、函数之类的,或者应该执行一个动作(比如读写一个文件或者向浏览器发送输出);然而,它不应该两者兼而有之。如果你习惯于使用require_once()来包含其他的类文件,这会让你马上出错,因为包含另一个文件是一个副作用。正如模式产生模式一样,标准往往需要其他标准。处理类依赖的正确方法是通过 PSR-4 兼容的自动加载程序。

那么,你声明的一个类用它的一个方法写文件合法吗?这是完全可以接受的,因为文件的包含不会产生这种效果。换句话说,这是一种执行效果,而不是副作用。

那么什么样的文件可能执行动作而不是声明类呢?想想启动应用的脚本。

以下是作为包含的直接结果而执行操作的列表:

// listing 15.01
namespace popp\ch15\batch01;

require_once(__DIR __ . "/../../../vendor/autoload.php");

$tree = new Tree();
print "loaded " . get_class($tree) . "\n";

下面是一个 PHP 文件,它声明了一个没有副作用的类:

// listing 15.02
namespace popp\ch15\batch01;

class Tree
{
}

Note

在其他章节中,我在很大程度上省略了namespace声明和use指令,以便专注于代码。因为这一章是关于格式化类文件的机制,我将在适当的地方包含namespaceuse语句。

命名

类必须用大写字母声明,也称为 studly caps 或 PascalCase。换句话说,类名应该以大写字母开头。名称的其余部分应该小写,除非它由多个单词组成。在这种情况下,每个单词都应该以大写字母开头,如下所示:

class MyClassName

属性可以用任何方式命名,尽管需要一致性。我倾向于使用 camel case,这种方法类似于 studly caps,但没有前导大写字母:

private $myPropertyName

方法必须在 camel case 中声明:

public function myMethodName()

类常量必须大写,单词之间用下划线分隔:

public const MY_NAME_IS = 'matt';

更多规则和示例

应该根据 PSR-4 自动加载标准来声明类、命名空间和文件。不过,我们将在本章的后半部分谈到这一点。PHP 文档必须保存为 UTF-8 编码的文件。

最后,对于 PSR 一号,让我们把它全部弄错——然后把它纠正过来。下面是一个打破所有规则的类文件:

// listing 15.03
<?
require_once("conf/ConfFile.ini");

class conf_reader {
    const ModeFile = 1;
    const Mode_DB = 2;

    private $conf_file;
    private $confValues= [];

    function read_conf() {
        // implementation
    }
}
?>

你能发现所有的问题吗?首先,我使用了一个简短的开始标记。我也没有声明一个namespace(尽管我们还没有详细讨论这个需求)。在给我的类命名时,我使用下划线,没有大写字母,而不是大写字母。我对常量名称使用了两种格式,这两种格式都不是必需的——所有大写字母都应该用下划线分隔。虽然我的两个财产名称都是合法的,但我没能使它们保持一致;具体来说,我对$conf_file使用了下划线,对$confValues使用了驼色。在给我的方法命名为read_conf()时,我使用了下划线而不是大小写。

// listing 15.04
<?php
namespace popp\ch15\batch01;

class ConfReader {
    const MODEFILE = 1;
    const MODE_DB = 2;

    private $conf_file;
    private $confValues= [];

    function readConf() {
        // implementation
    }
}
?>

PSR-12 扩展编码风格

扩展的编码风格(PSR-12)建立在 PSR-1 的基础上,并取代了一个废弃的标准:PSR-2。让我们来看看一些规则。

开始和结束一个 PHP 文档

我们已经看到,PSR-1 要求 PHP 块用<``?php打开。PSR-12 规定纯 PHP 文件不应该有结尾的?>标签,而应该以一个空行结束。用一个结束标记结束一个文件,然后让一个额外的新行悄悄进入,这太容易了。这可能会导致设置 HTTP 头时出现格式错误和错误(在内容已经发送到浏览器后,您不能这样做)。

表 15-2 按顺序描述了可能构成有效 PHP 文档的语句。

表 15-2

PHP 语句

|

声明

|

例子

|
| — | — |
| 打开 PHP 标签 | <?php |
| 文件级文档块 | /**``* File doc``*/ |
| 声明语句 | declare(strict_types=1); |
| 命名空间声明 | namespace popp; |
| 使用导入语句(类) | use other\Service; |
| 使用导入语句(函数) | use function other\{getAll, calculate}; |
| 使用导入语句(常量) | use const other\{NAME, VERSION}; |

一个 PHP 文档应该遵循表 15-2 中的结构(尽管任何合法 PHP 代码不必要的元素都可以省略)。namespace声明后面应该有一个空行,一组use声明后面应该有一个空行。不要在同一行中放置多个use声明:

// listing 15.05

namespace popp\ch15\batch01;

use popp\ch10\batch06\PollutionDecorator;
use popp\ch10\batch06\DiamondDecorator;
use popp\ch10\batch06\Plains;

// begin class

开始和结束课程

关键字class、类名以及extendsimplements必须都放在同一行。当一个类实现多个接口时,每个接口名可以包含在类声明的同一行中,也可以缩进在自己的行中。如果您选择将您的接口名称放在多行上,那么第一项必须放在它自己的行上,而不是直接放在implements关键字之后。类括号应该在类声明的之后的行开始,并在它们自己的行结束(直接在类内容之后)。因此,类声明可能看起来像这样:

// listing 15.06
class EarthGame extends Game implements
    Playable,
    Savable
    {

        // class body
    }

但是,您同样可以将接口名称放在一行中:

// listing 15.07
class EarthGame extends Game implements Playable, Savable
{

// class body

}

使用特征

当添加一个特征到一个类中时,你必须将use语句直接添加到类的左括号之后。尽管 PHP 允许你将你的特征分组到一行中,但 PSR-12 要求你将每个use语句放在自己的一行中。如果你的类除了提供use语句之外还提供了自己的元素,你必须在处理非信任内容之前留出一个空行。否则,必须直接在最后一个use语句后关闭该行的类块。

这里有一个类,它导入了两个特征并提供了自己的方法:

// listing 15.08
namespace popp\ch15\batch01;

class Tree
{
    use GrowTools;
    use TerrainUtil;

    public function draw(): void
    {
        // implementation
    }
}

如果为asinsteadof语句声明一个块,它应该分布在多行上。左大括号应该与use语句在同一行开始。然后,该块应该在每个语句中使用一行。最后,右大括号应该在它自己的一行结束,就像这样:

// listing 15.09
namespace popp\ch15\batch01;
class Marsh
{
    use GrowTools {
        GrowTools::dimension as size;
    }
    use TerrainUtil;

    public function draw(): void
    {
        // implementation
    }
}

声明属性和常数

属性和常量必须具有声明的可见性(publicprivateprotected)。var关键字是不可接受的。我们已经在 PSR 协议 1 中介绍了属性和常量名称的格式。

开始和结束方法

所有方法都必须具有声明的可见性(publicprivateprotected)。可见性关键字必须在 abstractfinal之后,但必须在 static之前。带有默认值的方法参数应该放在参数列表的末尾。

单行声明

方法括号应该从方法名后面的行开始,并在它们自己的行(直接在方法代码后面)结束。方法参数列表不应该以空格开始或结束(也就是说,它们应该紧挨着括号)。对于每个参数,逗号应该与前面的参数名(或默认值)齐平,但其后应该跟一个空格。让我们用一个例子来说明:

// listing 15.10
final public static function generateTile(int $diamondCount, bool $polluted = false): array
{
    // implementation
}

多行声明

在有许多参数的情况下,单行方法声明是不实际的。在这种情况下,您可以打破参数列表,使每个参数(包括类型、参数变量、默认值和逗号)都缩进在自己的行上。在这种情况下,右括号应该放在参数列表后面的一行,与方法声明的开始对齐。左大括号应该跟在同一行的右括号后面,用空格隔开。方法体应该在新的一行开始。同样,这听起来比实际情况复杂得多。举个例子应该更清楚:

// listing 15.11
public function __construct(
    int $size,
    string $name,
    bool $wraparound = false,
    bool $aliens = false
) {
    // implementation
}

返回类型

返回类型声明应该与右括号在同一行。冒号应该直接跟在右括号后面。冒号应该用一个空格与返回类型分开。对于多行声明,返回类型声明应该在同一行的左大括号之前,用空格隔开。

// listing 15.12
final public static function findTilesMatching(
    int $diamondCount,
    bool $polluted = false
): array {
    // implementation
}

PSR-12 并不强制要求使用返回类型声明。但是,由于引入了 void、mixed 和 nullable 类型,应该可以提供一个匹配所有情况的声明。

线条和缩进

你应该使用四个空格而不是制表符来缩进。值得检查一下你的编辑器设置——你可以配置好的编辑器在你按下Tab键时使用空格而不是制表符。您还应该在每行达到 120 个字符之前换行(尽管这不是强制性的)。行必须以 Unix 换行符结尾,而不是其他特定于平台的组合(如 MAC 中的 CR 和 Windows 中的 CR/LF)。再次,检查你的编辑器的设置,因为它可能会使用你的操作系统的默认行尾字符。

调用方法和函数

不要在方法名和左括号之间加空格。您可以对方法调用中的参数列表应用与方法声明中的参数列表相同的规则。换句话说,对于单行调用,在左括号之后或右括号之前不要留空格。每个参数后应紧跟一个逗号,下一个参数前应留出一个空格。如果需要对一个方法调用使用多行,每个参数应该缩进在自己的行上,右括号应该换行:

// listing 15.13
$earthgame = new EarthGame(
    5,
    "earth",
    true,
    true
);
$earthgame::generateTile(5,  true);

控制流程

流量控制关键字(ifforwhile等)。)后面必须跟一个空格。但是,左括号后面不能有空格。同样,右括号前面不能有空格。所以,里面的东西应该放在它们的支架里。与类和(单行)函数声明不同,流控制块的左括号必须与右括号在同一行开始。右大括号应该自成一行。这里有一个简单的例子:

// listing 15.14
$tile = [];
for ($x = 0; $x < $diamondCount; $x++) {
    if ($polluted) {
        $tile[] = new PollutionDecorator(new DiamondDecorator(new Plains()));
    } else {
        $tile[] = new DiamondDecorator(new Plains());
    }
}

注意forif后面的空格。forif表达式与包含它们的括号对齐。在这两种情况下,右括号后面是一个空格,然后是流量控制体的左括号。

括号中的表达式可以拆分成多行,每行至少缩进一次。在表达式被破坏的地方,布尔运算符可以放在每一行的开头或结尾,但是您的选择必须一致。

// listing 15.15
$ret = [];
for (
    $x = 0;
    $x < count($this->tiles);
    $x++
) {
    if (
        $this->tiles[$x]->isPolluted() &&
        $this->tiles[$x]->hasDiamonds() &&
        ! ($this->tiles[$x]->isPlains())
    ) {
        $ret[] =  $x;
    }
}
return $ret;

检查和修复您的代码

即使这一章涵盖了《PSR 协议 12》中的每一条指令(实际上并没有),你也很难记住所有的指令。毕竟,我们还有其他事情要考虑——比如我们系统的设计和实现。那么,假设我们已经接受了编码标准的价值,我们如何在不花费太多时间和精力的情况下遵守呢?当然,我们使用工具。

PHP_CodeSniffer允许您检测甚至修复违反标准的行为——不仅仅是针对 PSR。在 https://github.com/squizlabs/PHP_CodeSniffer 按照说明就可以拿到。有 Composer 和 PEAR 选项,但您可以通过以下方式下载 PHP 归档文件:

curl -OL https://squizlabs.github.io/PHP_CodeSniffer/phpcs.phar
curl -OL https://squizlabs.github.io/PHP_CodeSniffer/phpcbf.phar

为什么下载两次?第一个用于phpcs,它诊断和报告违规行为。第二个是给phpcbf的,可以修很多。让我们测试一下这些工具。首先,这是一段格式杂乱的代码:

// listing 15.16

namespace popp\ch15\batch01;
class ebookParser {

    function __construct(string $path , $format=0 ) {
        if ($format>1)
            $this->setFormat( 1 );
    }

    function setformat(int $format) {
        // do something with $format
    }
}

与其在这里讨论这些问题,不如让PHP_CodeSniffer来帮我们解决:

$ php phpcs.phar --standard=PSR12 src/ch15/batch01/phpcsBroken.php

FILE: /var/popp/src/ch15/batch01/phpcsBroken.php
---------------------------------------------------------------------------
FOUND 16 ERRORS AFFECTING 6 LINES
---------------------------------------------------------------------------
  5 | ERROR | [x] Header blocks must be separated by a single blank line
  6 | ERROR | [ ] Class name "ebookParser" is not in PascalCase format
  6 | ERROR | [x] Opening brace of a class must be on the line after the definition
  8 | ERROR | [ ] Visibility must be declared on method " construct"
  8 | ERROR | [x] Expected 0 spaces between argument "$path" and comma; 1 found
  8 | ERROR | [x] Incorrect spacing between argument "$format" and equals sign; expected 1 but found 0
  8 | ERROR | [x] Incorrect spacing between default value and equals sign for argument "$format"; expected 1 but found 0
  8 | ERROR | [x] Expected 0 spaces before closing parenthesis; 1 found
  8 | ERROR | [x] Opening brace should be on a new line
  9 | ERROR | [x] Inline control structures are not allowed
  9 | ERROR | [x] Expected at least 1 space before ">"; 0 found
  9 | ERROR | [x] Expected at least 1 space after ">"; 0 found
 10 | ERROR | [x] Space after opening parenthesis of function call prohibited
 10 | ERROR | [x] Expected 0 spaces before closing parenthesis; 1 found
 13 | ERROR | [ ] Visibility must be declared on method "setformat"
 13 | ERROR | [x] Opening brace should be on a new line
---------------------------------------------------------------------------
PHPCBF CAN FIX THE 13 MARKED SNIFF VIOLATIONS AUTOMATICALLY
---------------------------------------------------------------------------

Time: 82ms; Memory: 6MB

对于几行代码来说,这是一个令人疲惫的问题。幸运的是,正如输出所示,我们可以不费吹灰之力就修复很多错误(应用于副本,以便下次保留我的格式错误):

$ php phpcbf.phar --standard=PSR12 src/ch15/batch01/EbookParser.php

PHPCBF RESULT SUMMARY
----------------------------------------------------------------------
FILE                                             FIXED       REMAINING
----------------------------------------------------------------------
/var/popp/src/ch15/batch01/EbookParser.php       13          3
----------------------------------------------------------------------
A TOTAL OF 13 ERRORS WERE FIXED IN 1 FILE
----------------------------------------------------------------------

Time: 96ms; Memory: 6MB

现在,如果我们再次运行phpcs,我们将会看到情况有了很大的改善:

$ php phpcs.phar --standard=PSR2 src/ch15/batch01/EbookParser.php

FILE: /var/popp/src/ch15/batch01/EbookParser.php
----------------------------------------------------------------------
FOUND 3 ERRORS AFFECTING 3 LINES
----------------------------------------------------------------------
  7 | ERROR | Class name "ebookParser" is not in PascalCase format
 10 | ERROR | Visibility must be declared on method " construct"
 17 | ERROR | Visibility must be declared on method "setformat"
----------------------------------------------------------------------

Time: 76ms; Memory: 6MB

我将继续添加可见性声明,然后更改类名——这是一项快速的工作!现在我有了一个时髦的代码文件:

// listing 15.17
namespace  popp\ch15\batch01;
class EbookParser
{

    public function __construct(string $path, $format = 0)
    {
        if ($format > 1) {
            $this->setFormat(1);
        }
    }

    private function setformat(int $format): void
    {
        // do something with $format
    }
}

PSR-4 自动装弹

我们在第五章中看到了 PHP 对自动加载的支持。在那一章中,我们看到了如何使用spl_autoload_register()函数根据一个尚未卸载的类的名称自动请求文件。这虽然厉害,但也是一种幕后魔术。这在单个项目中是没问题的,但是如果多个组件聚集在一起,并且都使用不同的约定来加载类文件,就会造成很大的混乱。

自动加载标准(PSR-4)要求框架符合一组通用的规则,从而为魔术增加了一些纪律。

这对开发者来说是个好消息。这意味着我们或多或少可以忽略需要文件的机制,转而关注类依赖。

对我们很重要的规则

PSR-4 的主要目的是为自动装载机开发者定义规则。然而,这些规则不可避免地决定了我们必须声明名称空间和类的方式。这里是一些基本的。

完全限定的类名(即,类名,包括其名称空间)必须包括初始的“供应商”名称空间。因此,一个类必须至少有一个名称空间。

假设我们的供应商名称空间是popp。我们可以这样声明一个类:

// listing 15.18
namespace popp;

class Services
{
}

这个类的完全限定类名是popp\Services

路径中的初始命名空间必须对应于一个或多个基目录。我们可以用它将一组子名称空间映射到一个起始目录。例如,如果我们想使用名称空间popp\library(而不是名称空间popp下的任何东西),那么我们可以将它映射到一个顶级目录,这样我们就不必维护一个空的popp/目录。

让我们设置一个composer.json文件来执行映射:

{
    "autoload":  {
        "psr-4": {
            "popp\\library\\": "mylib"
        }
    }
}

注意,我甚至不需要调用基目录,"library"。这是从popp\librarymylib目录的任意映射。现在我可以在mylib目录下创建一个类文件:

// listing 15.19
// mylib/LibraryCatalogue.php

namespace popp\library;
use popp\library\inventory\Book;
class LibraryCatalogue
{
    private array $books = [];

    public function addBook(Book $book): void
    {
        $this->books[] = $book;
    }
}

为了被找到,LibraryCatalogue类必须放在一个完全相同名称的文件中(带有明显增加的.php扩展名)。

在基本目录(mylib)已经与初始名称空间(popp\library)相关联之后,在后续目录和子名称空间之间必须有直接关系。碰巧我已经在我的LibraryCatalogue类中引用了一个名为popp\library\inventory\Book的类。因此,该类文件应该放在mylib/inventory目录中:

// listing 15.20
// mylib/library/inventory/Book.php
namespace popp\library\inventory;

class Book
{
    // implementation
}

还记得路径中的初始名称空间必须对应一个或多个基目录的规则吗?到目前为止,我们已经在popp\librarymylib之间建立了一一对应的关系。实际上,我们没有理由不能将popp\library名称空间映射到多个基本目录。让我们将名为additional的目录添加到映射中;下面是对composer.json的修正:

{
    "autoload":  {
        "psr-4": {
            "popp\\library\\": ["mylib", "additional"]
        }
    }
}

现在,我可以创建额外的/inventory 目录和一个要放入其中的类:

// listing 15.21
// additional/inventory/Ebook.php
namespace popp\library\inventory;
class Ebook extends Book
{
    // implementation
}

接下来,让我们创建一个顶级 runner 脚本index.php,来实例化这些类:

// listing 15.22
require_once("vendor/autoload.php");

use popp\library\LibraryCatalogue;

// will be found under mylib/
use popp\library\inventory\Book;

// will be found under additional/
use popp\library\inventory\Ebook;

$catalogue = new LibraryCatalogue();
$catalogue->addBook(new Book());
$catalogue->addBook(new Ebook());

Note

您必须使用 Composer 来生成自动加载文件vendor/autoload.php,并且在您访问您在composer.json中声明的逻辑之前,必须以某种方式包含该文件。您可以通过运行命令composer install来做到这一点(或者如果您只想在已经安装的环境中重新生成自动加载文件,可以通过运行composer dump-autoload)。你可以在第十六章中了解更多关于作曲家的知识。

还记得关于副作用的规则吗?一个 PHP 文件应该声明类、接口、函数等等;或者,它应该执行一个操作。然而,它不应该两者兼而有之。这个脚本属于采取行动类别。重要的是,它调用require_once()来包含使用composer.json文件中的配置生成的自动加载代码。由于这个原因,所有的类都被定位了,尽管Ebook已经被放置在一个完全独立于其他类的基本目录中。

为什么我要为同一个核心名称空间维护两个独立的目录?一个可能的原因是您希望将单元测试与生产代码分开。您还可以管理并非每个系统版本都附带的插件和扩展。

Note

务必在 www.php-fig.org/psr/时刻关注所有 PSR 标准。这是一个快速发展的领域,你可能会发现与你相关的标准正在路上。

摘要

在这一章中,我稍微考虑了一下标准并不那么令人兴奋的可能性,然后为它们的力量做了一个案例。标准解决了我们的集成问题,这样我们就可以继续工作,做令人惊奇的事情。我研究了 PSR-1 和 PSR-12,它们是基本编码和更广泛编码风格的标准。接下来,我继续讨论 PSR-4,自动装载机的标准。最后,我通过一个基于 Composer 的例子展示了 PSR-4 兼容的自动加载。

十六、用 Composer 使用和创建组件

程序员渴望产生可重用的代码。这是面向对象编码的伟大目标之一。我们喜欢从特定环境的混乱中抽象出有用的功能,将特定的解决方案变成可以反复使用的工具。从另一个角度来看,如果程序员喜欢可重用,他们讨厌重复。通过创建可重新应用的库,程序员无需在多个项目中实现类似的解决方案。

即使我们在自己的代码中避免了重复,还有一个更广泛的问题。对于您创建的每个工具,有多少其他程序员实现了相同的解决方案?这是大规模的浪费努力:对于程序员来说,与其在一个主题上产生成百上千个变体,不如合作并把精力集中在改进一个工具上,这样不是更明智吗?

为了做到这一点,我们需要获得现有的库。但是我们需要的包可能需要其他库来完成它们的工作。因此,我们需要一个工具,可以处理下载和安装包,以及管理他们的依赖关系。这就是作曲家的用武之地;除此之外,它还能做更多的事情。

本章将涵盖几个关键问题:

  • 安装:下载并设置作曲家

  • 需求:使用composer.json获取包

  • 版本:指定版本,以便在不破坏系统的情况下获得最新代码

  • 打包师:为公共访问配置你的代码

  • 私有存储库:利用使用私有存储库的 Composer

什么是作曲家?

严格来说,Composer 是一个依赖管理器,而不是一个包管理器。这似乎是因为它在本地基础上处理组件关系,而不是像 Yum 和 Apt 那样集中处理。如果你认为这是一个过于细微的区别,你可能是对的。无论我们如何定义它,Composer 都允许您指定包。它将它们下载到一个本地目录(vendor),找到并下载所有的依赖项,然后通过一个自动加载器将这些代码提供给你的项目。

一如既往,我们需要从获得工具开始。

安装作曲者

您可以在 https://getcomposer.org/download/ 下载作曲家。你会在那里找到一个安装机制。您也可以安装一个稳定的 phar 文件,如下所示:

$ wget https://getcomposer.org/composer-stable.phar
$ chmod 755 composer-stable.phar
$ sudo mv composer-stable.phar ~/bin/composer

我下载存档文件并运行chmod以确保它是可执行的。然后我将它复制到一个中心位置,这样我就可以在系统的任何地方轻松运行它。现在我可以测试这个命令了:

$ composer --version
Composer version 2.0.8 2020-12-03 17:20:38

安装(一组)软件包

为什么我用括号做了那个奇怪的动作?因为包不可避免地产生包——有时是很多包。

不过,让我们从一个独立的库开始。想象一下,我们正在构建一个需要与 Twitter 通信的应用。稍微研究一下,我就想到了abraham/twitteroauth包。为了安装它,我需要生成一个名为composer.json的 JSON 文件,然后定义一个require元素:

{
    "require": {
        "abraham/twitteroauth": "2.0.*"
    }
}

我从一个除了composer.json文件以外为空的目录开始。但是,一旦我运行 Composer 命令,我们将看到一个变化:

$ composer update

Loading composer repositories with package information
Updating dependencies
Lock file operations: 2 installs, 0 updates, 0 removals
  - Locking abraham/twitteroauth (2.0.1)
  - Locking composer/ca-bundle (1.2.8)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 2 installs, 0 updates, 0 removals
  - Installing composer/ca-bundle (1.2.8): Extracting archive
  - Installing abraham/twitteroauth (2.0.1): Extracting archive
    Generating autoload files

那么产生了什么呢?让我们来看看:

$ ls

composer.json    composer.lock    vendor

Composer 将软件包安装到vendor/中。它还生成一个名为composer.lock的文件。这指定了安装的所有软件包的确切版本。如果您使用版本控制,您应该提交这个文件。如果另一个开发人员用一个composer.lock文件运行composer install,包版本将完全按照指定安装在她的系统上。通过这种方式,团队可以彼此保持同步,并且您可以确保您的生产环境与开发和测试环境完全匹配。

您可以通过再次运行composer update来覆盖锁定文件。这将生成一个新的锁文件。通常情况下,您将运行这个来保持最新的包版本(如果您使用通配符,就像我一样,或者使用范围)。

从命令行安装软件包

如您所见,我可以使用编辑器创建composer.json文件。但是你也可以让作曲家为你做。如果您需要从单个包开始,这将特别有用。当您在命令行上调用composer require时,Composer 会下载指定的包并安装到vendor/中。它还会生成一个composer.json文件,然后您可以编辑和扩展该文件:

$ composer require abraham/twitteroauth

Using version ².0 for abraham/twitteroauth
./composer.json has been created
Running composer update abraham/twitteroauth
Loading composer repositories with package information Updating dependencies
Lock file operations: 2 installs, 0 updates, 0 removals
  - Locking abraham/twitteroauth (2.0.1)
  - Locking composer/ca-bundle (1.2.8)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 2 installs, 0 updates, 0 removals
  - Installing composer/ca-bundle (1.2.8): Extracting archive
  - Installing abraham/twitteroauth (2.0.1): Extracting archive
    Generating autoload files

版本

Composer 旨在支持语义版本控制。本质上,这涉及到用三个数字定义一个包的版本,用点分隔:主要版本次要版本补丁。如果您修复了一个 bug,没有添加任何功能,并且没有破坏向后兼容性,那么您应该增加补丁的编号。如果添加新功能,但不破坏向后兼容性,应该增加中间的次要号。如果您的新版本破坏了向后兼容性(换句话说,如果这个新版本突然切换,客户端代码将会中断),那么您应该增加第一个版本号。

Note

您可以在 https://semver.org 阅读更多关于语义版本控制的约定。

在您的composer.json文件中指定版本时,您应该记住这一点:如果您在范围或通配符方面过于自由,您可能会发现您的系统在更新时会崩溃。

表 16-1 显示了使用 Composer 指定版本的一些方法。

表 16-1

作曲者和软件包版本

|

类型

|

例子

|

笔记

|
| — | — | — |
| 确切的 | 1.2.2 | 仅安装给定的版本 |
| 通配符 | 1.2.* | 安装精确指定的数字,但要找到与通配符匹配的最新可用版本 |
| 范围 | 1.0.0–1.1.7 | 安装一个不低于第一个数字且不高于最后一个数字的版本 |
| 比较 | >1.2.0<=1.2.2 | 使用<<=>>=指定复杂范围。您可以用一个空格(相当于“和”)或者用&#124;&#124;来指定“或”来组合这些指令 |
| 颚化符(主要版本) | ~1.3 | 给定的数字是最小值,指定的最终数字可以增加。所以对于~1.3,1.3 是最小值,在 2.0.0 或更高版本上不可能有匹配 |
| 脱字号 | ¹.3 | 将匹配下一个重大更改,但不包括下一个重大更改。因此,虽然~1.3.1 与 1.4 及更高版本不匹配,但¹.3.1 将与 1 . 3 . 1 至 2.0.0 版本匹配,但不包括 2 . 0 . 0 版本。这通常是最有用的捷径 |

Note

通过向版本约束字符串添加稳定性后缀,您可以进一步影响 composer 选择包的方式。通过添加@,后跟devalphabetaRC(从最不稳定到最稳定),您将允许 composer 在其计算中考虑不稳定的版本。Composer 可以通过查看 git 标记名来解决这个问题。所以1.2.*@dev可以匹配标签1.2.2-dev。您还可以使用稳定性标志stable来表示您不希望包含前沿代码。这将匹配未定义为devbeta等的版本标签。

要求-开发

通常,您在开发过程中需要在生产环境中不必要的包。例如,您可能希望在本地运行测试,但是您不太可能需要在您的公共站点上提供 PHPUnit。

Composer 通过支持单独的require-dev元素来解决这个问题。您可以在这里添加包,就像您可以为require元素添加包一样:

{
    "require-dev": {
        "phpunit/phpunit": "*"
    },
    "require": {
        "abraham/twitteroauth": "².0",
        "ext-xml": "*"
    }
}

现在,当我们运行composer update时,PHPUnit 和所有种类的依赖包都被下载和安装:

$ composer update

Loading composer repositories with package information Updating dependencies
Lock file operations: 36 installs, 0 updates, 0 removals
  - Locking abraham/twitteroauth (2.0.1)
  - Locking composer/ca-bundle (1.2.8)
  - Locking doctrine/instantiator (1.4.0)

...

Writing lock file
Installing dependencies from lock file (including require-dev) Package operations: 36 installs, 0 updates, 0 removals
  - Installing composer/ca-bundle (1.2.8): Extracting archive
  - Installing abraham/twitteroauth (2.0.1): Extracting archive

...

6 package suggestions were added by new dependencies, use `composer suggest` to see details.
Generating autoload files

但是,如果您在生产环境中安装,您可以将--no-dev标志传递给composer install,Composer 将只下载那些在require元素中指定的包:

$ composer install --no-dev

Installing dependencies from lock file
Verifying lock file contents can be installed on current platform.
Package operations: 2 installs, 0 updates, 0 removals
  - Installing composer/ca-bundle (1.2.8): Extracting archive
  - Installing abraham/twitteroauth (2.0.1): Extracting archive Generating autoload files

Note

当您运行composer install命令时,Composer 会创建一个名为composer.lock的文件。这记录了你在vendor/下安装的每个文件的确切版本。如果您在composer.json旁边有一个composer.lock文件的情况下运行composer install,如果它们不存在,Composer 将获取它记录的包版本。这很有用,因为您可以将一个composer.lock文件提交到您的版本控制库,并确保您的团队将下载您已经安装的所有包的相同版本。如果您需要覆盖composer.lock,要么是为了获得最新版本的包,要么是因为您已经更改了composer.json,您应该运行composer update来覆盖锁文件。

作曲和自动加载

我们在第十五章中详细介绍了自动装载。然而,为了完整起见,还是值得简单地看一下。Composer 生成一个名为autoload.php的文件,该文件为它下载的包处理类加载。您也可以通过包含autoload.php(通常使用require_once())来为您自己的代码利用这一功能。一旦你这样做了,只要你的目录和文件名反映了你的命名空间和类名,你在你的系统中声明的任何类都会在你的代码中被自动找到。

换句话说,名为poppbook\megaquiz\command\CommandContext的类必须放在poppbook/megaquiz/command/目录中名为CommandContext.php的文件中。

如果您想把事情搞混(可能通过省略一两个冗余的前导目录或者通过在搜索路径中添加一个测试目录),那么您可以使用autoload元素将一个名称空间映射到您的文件结构,如下所示:

    "autoload": {
        "psr-4": {
            "poppbook\\megaquiz\\": ["src", "test"]
        }
    }

为了生成最新的autoload.php文件,我需要运行composer install(也将安装锁文件中指定的任何东西)或composer update(也将安装与composer.json中的规范相匹配的最新包)。如果你不想安装或更新任何软件包,你可以使用composer dump-autoload,它只会生成自动加载文件。

现在,只要包含了autoload.php,我的类很容易被发现。多亏了我的autoload配置,poppbook\megaquiz\command\CommandContext现在可以在src/command/CommandContext.php中找到了。不仅如此,因为我引用了不止一个目标(testsrc,我还可以在test/目录下创建属于poppbook\megaquiz\名称空间的测试类。

转到第十五章中的“PSR-4 自动加载”部分,以了解更深入的示例。

创建自己的包

如果您过去曾经使用过 PEAR,那么您可能会期望这里关于创建包的一节包含一个全新的包文件。事实上,我们已经在本章中创建了一个包。我们只需要添加更多的信息,然后找到一种方法使我们的代码对其他人可用。

添加包信息

您真的不需要添加那么多信息来制作一个可行的包,但是您绝对需要一个name,这样您的包才能被找到。我还将包含descriptionauthors元素,并创建一个名为megaquiz的假产品,你会发现它偶尔会在其他章节中出现:

    "name": "poppbook/megaquiz",
    "description": "a truly mega quiz",
    "authors": [
        {
            "name": "matt zandstra",
            "email": "matt@getinstance.com"
        }
    ],

这些字段应该是不言自明的。例外情况可能是前面的名称空间,在本例中是poppbook,它与实际的包名之间用正斜杠隔开。这就是所谓的厂商名称。正如您所预料的,当您的包被安装时,供应商名称将成为vendor/下的一个顶级目录。这通常是 GitHub 或 Bitbucket 中的包所有者使用的组织名称。

所有这些都准备好了,您就可以将您的包提交到您选择的版本控制主机了。如果你不确定这涉及到什么,你可以在第十七章了解更多。

Note

Composer 支持一个version字段,但是在 Git 中使用一个标签来跟踪包的版本被认为是更好的实践。Composer 会自动识别这一点。

请记住,您不应该推送vendor目录(至少通常不应该——该规则有一些有争议的例外)。然而,沿着composer.json跟踪生成的composer.lock文件是一个好主意。

平台包

虽然您不能使用 Composer 来安装系统范围的软件包,但是您可以指定系统范围的需求,这样您的软件包将只安装在准备好的系统中。

一个平台包用一个键来指定,尽管在一些情况下,这个键可以用破折号按类型进一步分解。我在表 16-2 中列出了可用的类型。

表 16-2

平台包

|

类型

|

例子

|

描述

|
| — | — | — |
| 服务器端编程语言(Professional Hypertext Preprocessor 的缩写) | "php": "8.*" | PHP 版本 |
| 延长 | "ext-xml": ">2" | PHP 扩展 |
| 库 | "lib-iconv": "~2" | PHP 使用的系统库 |
| 嗯,嗯 | "hhvm": "~2" | HHVM 版本(HHVM 是支持 PHP 扩展版本的虚拟机) |

让我们试一试:

{
    "require": {
        "abraham/twitteroauth": "2.0.*",
        "ext-xml": "*",
        "ext-gd": "*"
    }
}

在前面的代码中,我指定我的包需要xmlgd扩展名。现在该跑了update:

$ composer update

Loading composer repositories with package information
Updating dependencies
Your requirements could not be resolved to an installable set of packages.

  Problem  1
    - Root composer.json requires PHP extension ext-gd * but it is missing from your system. Install or enable PHP's gd extension.

看起来好像我是为 XML 而设置的;但是,我的系统上没有安装 GD,一个图像操作包,所以 Composer 抛出一个错误。

通过包装商分销

如果你一直在阅读这一章,你可能会想知道我们一直在安装的包实际上是从哪里来的。这感觉很像魔术,但是(如你所料)幕后有一个包存储库。它被称为 Packagist,可以在 https://packagist.org 找到。只要您的代码可以在公共 git 存储库中找到,就可以通过 Packagist 获得。

让我们试一试。我已经将我的megaquiz项目推送到 GitHub,所以现在我需要告诉 Packagist 关于我的存储库。注册后,我只需添加我的存储库的 URL。你可以在图 16-1 中看到这一点。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 16-1

向 Packagist 添加包

一旦我添加了megaquiz,Packagist 就会定位存储库,检查composer.json文件,并显示一个控制面板。你可以在图 16-2 中看到。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 16-2

软件包控制面板

Packagist 告诉我,我还没有设置许可证信息。我可以在任何时候通过向composer.json文件添加一个license元素来修复这个问题:

"license": "Apache-2.0",

Packagist 也找不到任何版本信息。我将通过向 GitHub 存储库添加一个标记来解决这个问题:

$ git tag -a 'v1.0.0' -m 'v1.0.0'
$ git push --tags

Note

如果你认为我浏览这些垃圾是作弊,那你就对了。我在第十七章中详细介绍了 Git 和 GitHub。

现在 Packagist 知道了我的版本号。你可以在图 16-3 中确认。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 16-3

包装商知道版本

现在,任何人都可以包含另一个包中的megaquiz。下面是一个最小的composer.json文件:

{
    "require": {
        "poppbook/megaquiz": "*"
    }
}

我指定了供应商名称和包名称。冒险地说,我很乐意接受任何版本。让我们继续安装:

$ composer update

Loading composer repositories with package information
Updating dependencies
Lock file operations: 3 installs, 0 updates, 0 removals
  - Locking abraham/twitteroauth (2.0.1)
  - Locking composer/ca-bundle (1.2.8)
  - Locking poppbook/megaquiz (v1.0.0)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 3 installs, 0 updates, 0 removals
  - Installing composer/ca-bundle (1.2.8): Extracting archive
  - Installing abraham/twitteroauth (2.0.1): Extracting archive
  - Installing poppbook/megaquiz (v1.0.0): Extracting archive
    Generating autoload files

注意,我在设置megaquiz时指定的依赖项也被下载了。

保密

当然,你并不总是想向全世界发布你的代码。有时,您只需要与一小部分授权用户共享。

这里有一个名为getinstance/wtnlang-php的私有包,其中包含一个脚本语言库:

{
    "name": "getinstance/wtnlang-php",
    "description": "it's a wtn language",
    "license": "private",
    "authors": [
        {
            "name": "matt zandstra",
            "email": "matt@getinstance.com"
        }
    ],
    "autoload":  {
        "psr-4": {
            "getinstance\\wtnlang\\": ["src/", "test/unit"]
        }
    },
    "require": {
        "abraham/twitteroauth": "*",
        "aura/cli": "~2.1.0",
        "monolog/monolog": "¹.23"
    },
    "require-dev": {
        "phpunit/phpunit": "⁷"
    }
}

它托管在一个私有的 Bitbucket 存储库中,所以不能通过 Packagist 使用。那么我如何将它包含在项目中呢?我只需要告诉作曲家去哪里找。我可以通过创建或添加到repositories元素来做到这一点:

{
    "repositories": [
        {
            "type": "vcs",
            "url": "git@bitbucket.org:getinstance/wtnlang-php.git"
        }
    ],
    "require": {
        "poppbook/megaquiz": "*",
        "getinstance/wtnlang-php": "dev-develop"
    }
}

我可以在require块中为getinstance/wtnlang-php指定一个版本,这将对应于 git 存储库中的一个标签,但是,通过使用dev-前缀,我可以调用一个分支。这在开发过程中非常有用。所以现在,只要我可以访问 getinstance/wtnlang-php,我就可以同时安装我的私有包和megaquiz:

$ composer update

Loading composer repositories with package information
Updating dependencies
Nothing to modify in lock file
Installing dependencies from lock file (including require-dev)
Package operations: 7 installs, 0 updates, 0 removals
  - Installing composer/ca-bundle (1.2.8): Extracting archive
  - Installing psr/log (1.1.3): Extracting archive
  - Installing monolog/monolog (1.26.0): Extracting archive
  - Installing aura/cli (2.1.2): Extracting archive
  - Installing abraham/twitteroauth (2.0.1): Extracting archive
  - Installing getinstance/wtnlang-php (dev-develop de3bf14): Cloning de3bf1456c
  - Installing poppbook/megaquiz (v1.0.0): Extracting archive Generating autoload files

摘要

在阅读本章之前,您应该了解利用 Composer 软件包为您的项目增加功能是多么容易。通过composer.json文件,您还可以让其他用户访问您的代码,无论是通过使用 Packagist 公开还是通过指定您自己的存储库。这种方法自动为您的用户下载依赖项,并允许第三方软件包使用您的软件包,而无需捆绑。

十七、将 Git 用于版本控制

所有的灾难都有其临界点,即秩序最终崩溃、事件失控的时刻。你在这样的项目中发现过自己吗?你能发现那个关键时刻吗?

也许是当你“仅仅做了几个改变”就发现你让周围的一切都崩溃了(更糟糕的是,你不太确定如何回到你刚刚破坏的稳定点)。可能是当你意识到你的团队中的三个成员一直在同一个类上工作,并且愉快地分享彼此的工作。或者可能是当你发现一个你已经实现了两次的 bug 修复不知何故又从代码库中消失了。如果有一种工具可以帮助你管理协同工作,允许你拍摄你的项目的快照,如果需要的话回滚它们,然后合并多个开发链,这不是很好吗?在这一章中,我们来看看 Git,一个可以做所有这些事情的工具,以及更多。

本章将涵盖使用 Git 的以下方面:

  • 基本配置:探索设置 Git 的一些技巧

  • 导入:开始新项目

  • 提交变更:将您的工作保存到存储库中

  • 更新:把别人的作品和你自己的融合在一起

  • 分支:维护平行的开发链

为什么要使用版本控制?

如果还没有的话,版本控制将会改变你的生活(如果仅仅是你作为开发人员的生活)。有多少次你在一个项目中到达了一个稳定的时刻,吸了一口气,然后又一次陷入开发混乱中?当展示您正在进行的工作时,恢复到稳定版本有多容易?当然,当项目达到稳定状态时,您可能已经保存了项目的快照,这可能是通过复制您的开发目录实现的。现在,假设您的同事正在处理相同的代码库。也许他和你一样保存了一份稳定的代码副本。不同的是,他的副本是他作品的快照,而不是你的。当然,他也有一个混乱的开发目录。因此,您需要协调项目的四个版本。现在想象一个有四个程序员和一个 web UI 开发人员的项目。你看起来很苍白。也许你想躺下来?

Git 的存在就是为了解决这个问题。使用 Git,所有开发人员都可以从一个中央存储库中克隆他们自己的代码库副本。每当他们的代码达到一个稳定点时,他们可以从服务器上下载最新的代码,并将其与自己最近的工作合并。当他们准备好了,并且在他们修复了任何冲突并且运行了所有测试之后,他们可以把他们的新的稳定合成推回到共享存储库中。

Git 是一个分布式版本控制系统。这意味着,一旦用户获得了一个分支,他们就可以提交给自己的本地存储库,而不需要网络连接。这有许多好处。这意味着日常操作更快,你可以在飞机、火车和汽车上轻松工作。然而,最终,您可以与您的队友共享一个权威的存储库。

事实上,每个开发人员都可以将其工作合并到一个中央存储库中,这意味着协调多个开发链变得更加容易。更好的是,您可以根据日期或标签来检查代码库的版本。因此,当您的代码达到一个稳定点时,例如,适合向客户显示正在进行的工作,您可以用任意标签标记它。然后,当你的客户突然来到你的办公室,想要给投资者留下深刻印象时,你可以使用这个标签来检查正确的代码库。

等等!还有呢!您还可以同时管理多个开发链。如果这听起来不必要的复杂,想象一个成熟的项目。您已经发布了第一个版本,并且正在开发第二个版本。版本 1。n 同时走开?当然不是。您的用户一直在发现错误并要求增强。您可能离发布版本 2 还有几个月的时间,那么您在哪里进行和测试更改呢?Git 允许您维护代码库的不同分支。因此,您可以创建版本 1 的 bug 修复分支。n 用于当前生产代码的开发。在关键点上,这个分支可以合并回版本 2 的代码(主干),这样您的新版本就可以从版本 1 的改进中获益。n

Note

Git 不是唯一可用的版本控制系统。你可能还想看看 Subversion ( http://subversion.apache.org/ )或者 Mercurial ( http://mercurial.selenic.com/ )。这一章必然是对一个大题目的简要介绍。然而,幸运的是,斯科特·沙孔的Pro Git(2014 年出版)深入而清晰地涵盖了这个主题。不仅如此,在 https://git-scm.com/book/en/v2 网站上还有网页版。

让我们来看看实践中的一些特性。

获取 Git

如果您正在使用一个类似 Unix 的操作系统(比如 Linux 或 FreeBSD),那么您可能已经安装了 Git 并可以使用了。

Note

我用一个前导美元符号($)来显示在命令行输入的命令,以表示命令提示符,以区别于它们可能产生的任何输出。

尝试从命令行键入以下内容:

$ git help

您应该会看到一些使用信息,以确认您已经准备好开始使用。如果您还没有 Git,那么您应该查阅发行版的文档。您几乎肯定可以使用 Yum 或 Apt 之类的简单安装机制,或者您可以直接从 http://git-scm.com/downloads获取 Git。

Note

技术编辑 Paul Tregoing 也推荐 Git for Windows(https://gitforwindows.org/)Git 自带的,自然也是一套有用的开源工具。

使用在线 Git 存储库

你可能已经注意到了,这本书经常是单干的。我几乎从不认为你应该重新发明轮子;相反,在购买现成的车轮之前,你至少应该对车轮结构有所了解。出于这个原因,我将在下一节介绍建立和维护您自己的中央 git 存储库的机制。不过,还是现实点吧。您几乎肯定会使用专门的主机来管理您的存储库。有很多这样的软件可供选择,尽管最大的玩家可能是 Bitbucket ( https://bitbucket.org )、GitHub ( https://github.org )和 GitLab ( https://about.gitlab.com/ )。

那么,你应该选择哪个呢?根据经验,GitHub 可能是开源产品的标准。所以,我会和 GitHub 签约我的项目。图 17-1 显示了我的下一个决定,是在公共库还是私有库之间。我将选择公共项目(因为我正在创建一个开源项目)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17-1

GitHub 项目入门

如你所见,在图 17-1 中,我还没有完全打完megaquiz。此时,GitHub 为导入我的项目提供了一些有用的说明。你可以在图 17-2 中看到这些。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17-2

GitHub 的导入说明

不过,我还没有准备好运行这些命令。当我将文件推送到服务器时,GitHub 需要能够验证我。为了做到这一点,它需要我的公钥。我将在下一节“配置 Git 服务器”中描述生成这种密钥的一种方法一旦我有了公钥,我就可以从 GitHub 的用户设置界面的SSH and GPG keys链接添加它。

在图 17-3 中可以看到 GitHub 的 SSH 和 GPG 键的设置画面。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17-3

添加 SSH 密钥

现在,我准备开始向我的存储库添加文件。不过,在我们开始之前,我们应该后退一步,花一些时间来遵循自己动手的路线。

配置 Git 服务器

Git 在两个关键方面不同于传统的版本控制系统。首先,在幕后,它存储文件的快照,而不是在提交之间对文件所做的更改。第二,对用户来说更明显的是,它在您的系统本地运行,直到您选择推送到远程存储库或从远程存储库拉出。这意味着您不需要依赖互联网连接来继续工作。

为了使用 Git,您不需要一个单独的远程存储库;但是在实践中,如果你和一个团队一起工作,拥有一个共享的权力来源几乎总是有意义的。

在这一节中,我将介绍启动和运行远程 Git 服务器所需的步骤。我假设 root 用户可以访问 Linux 机器。

创建远程存储库

为了创建 Git 存储库,我必须首先创建一个包含目录。我通过 SSH 登录到一个新配置的远程服务器。我将在/var/git下创建我的存储库。一般来说,只有 root 用户可以在那里创建和修改目录,所以我使用sudo运行下面的命令:

$ sudo mkdir -p /var/git/megaquiz
$ cd /var/git/megaquiz/

我创建了/var/git,它是我的存储库的父目录和一个名为megaquiz的样例项目的子目录。现在我可以准备目录本身了:

$ sudo git init --bare

Initialized empty Git repository in /var/git/megaquiz/

--bare flag告诉 Git 在没有工作目录的情况下初始化存储库。如果您试图推送到一个不是以这种方式创建的存储库,Git 将会抱怨。

目前只有 root 用户可以在/var/git下乱来。我可以通过创建一个名为git的用户和组并使其成为目录的所有者来改变这一点:

$ sudo adduser git
$ sudo chown -R git:git /var/git

为本地用户准备存储库

尽管这是一个指定的远程服务器,我也应该确保本地用户可以提交到存储库。如果不小心的话,这可能会导致所有权和权限问题(特别是当拥有sudo特权的用户推送代码时)。

$ sudo chmod -R g+rws /var/git

这给予了git组的成员对/var/git的写访问权,并导致在此创建的所有文件和目录都采用git组。现在,只要我确保他们是git组的成员,本地用户就能够写入存储库。不仅如此,创建的任何文件都可以被组中的其他成员写入。

您可以将本地用户添加到git组,如下所示:

$ sudo usermod -aG git bob

现在用户bobgit组的成员。

为用户提供访问权限

前面提到的bob用户的所有者可以登录到服务器,并通过他的 shell 与存储库进行交互。但是,一般来说,您不希望向所有用户提供 shell 访问。在任何情况下,大多数用户都喜欢利用 Git 的分布式特性,并在本地处理他们的克隆数据。

授予用户 SSH 访问权限的一种方式是通过公钥认证。为此,您首先需要获得用户的公共 SSH 密钥。用户可能已经有了这个——在 Linux 机器上,他可能会在配置目录中的一个名为id_rsa.pub的文件中找到这个密钥。否则,他可以很容易地生成新的密钥。在类似 Unix 的机器上,这就是运行ssh-keygen命令并复制它生成的值:

$ ssh-keygen
$ cat .ssh/id_rsa.pub

作为存储库管理员,我会要求您提供这个密钥的副本。一旦有了它,我必须将它添加到存储库服务器上的git用户的 SSH 设置中。这仅仅是将公钥粘贴到.ssh/authorized_keys文件中的问题。我可能需要为我设置的第一个密钥创建.ssh配置目录(我从git用户的主目录运行这些命令):

$ mkdir .ssh
$ chmod 0700 .ssh
# create authorized_keys file and paste in the user's key:
$ vi .ssh/authorized_keys
$ chmod 0700 .ssh/authorized_keys

Note

SSH 访问失败的一个常见原因是创建了权限过于宽松的配置文件。SSH 配置环境应该只对帐户的所有者可读和可写。Michael Stahnke (Apress,2005)的 Pro OpenSSH 全面介绍了 SSH。

关闭 git 用户的 Shell 访问

任何服务器都不应该比它需要的更开放。您可能希望让您的用户能够访问 Git 命令,但可能仅此而已。

通过查看文件/etc/passwd,您可以看到与 Linux 服务器上的用户相关联的 shell。下面是我的远程服务器上的git帐户的相关行:

git:x:1001:1001::/home/git:/bin/bash

Git 提供了一个名为git-shell的特殊 shell,它限制用户只能使用选定的命令。我可以通过编辑/etc/passwd来启用这个登录程序:

git:x:1001:1001::/home/git:/usr/bin/git-shell

现在,如果我尝试通过 SSH 登录,我会被告知分数并被注销:

$ ssh git@poppch17.vagrant.internal

Last login: Thu Dec 31 14:25:05 2020 from 192.168.33.1
fatal: Interactive git shell is not enabled.
hint: ~/git-shell-commands should exist and have read and execute access. Connection to 192.168.33.71 closed.

开始一个项目

现在我有了一个远程 Git 服务器,并且可以从我的本地帐户访问它,是时候在/var/git/megaquiz将我正在进行的工作添加到存储库中了。

在开始之前,我会仔细检查一下我的文件和目录,并删除我可能找到的任何临时项目。

做不到这一点是常见的烦恼。要监视的临时项目包括自动生成的文件,如 composer 包、构建目录、安装程序日志等。

Note

您可以通过在存储库中放置一个名为.gitignore的文件来指定要忽略的文件和模式。在 Linux 系统上,man gitignore命令应该提供文件名通配符的例子,您可以修改这些例子来排除由您的构建过程、编辑器和 ide 创建的各种锁文件和临时目录。该文本也可在 http://git-scm.com/docs/gitignore在线获取。

在继续之前,我应该用 Git 注册我的身份——这样可以更容易地跟踪谁在存储库中做了什么:

$ git config --global user.name "poppbook"
$ git config --global user.email "poppbook@getinstance.com"

现在,我已经建立了我的个人详细信息,并确保我的项目是干净的,我可以设置它并将其代码推送到服务器:

$ cd /home/mattz/work/megaquiz
$ git init

Initialized empty Git repository in /home/mattz/work/megaquiz/.git/

现在是时候添加我的文件了:

$ git add .

Git 现在正在跟踪megaquiz下的所有文件和目录。被跟踪的文件可以有三种状态:未修改修改暂存。您可以通过运行命令git status来检查这一点:

$ git status

# On branch master
#
# Initial commit
#
# Changes to be committed:
#   (use "git rm --cached <file>..." to unstage)
#
#       new file:   composer.json
#       new file:   composer.lock
#       new file:   main.php
#       new file:   src/command/Command.php
#       new file:   src/command/CommandContext.php
#       new file:   src/command/FeedbackCommand.php
#       new file:   src/command/LoginCommand.php
#       new file:   src/quizobjects/User.php
#       new file:   src/quiztools/AccessManager.php
#       new file:   src/quiztools/ReceiverFactory.php
#

感谢我之前的git add命令,我所有的文件都准备好提交了。我现在可以继续执行commit命令了:

$ git commit -m'my first commit'

[master (root-commit) a5ca2d4] my first commit
 10 files changed, 1638 insertions(+)
 create mode 100644 composer.json
 create mode 100644 composer.lock
 create mode 100755 main.php
 create mode 100755 src/command/Command.php
 create mode 100755 src/command/CommandContext.php
 create mode 100755 src/command/FeedbackCommand.php
 create mode 100755 src/command/LoginCommand.php
 create mode 100755 src/quizobjects/User.php
 create mode 100755 src/quiztools/AccessManager.php
 create mode 100644 src/quiztools/ReceiverFactory.php

我通过-m标志添加了一条消息。如果我忽略了这一点,那么 Git 将启动一个编辑器,我可以用它来添加我的签入消息。

如果您习惯于 CVS 和 Subversion 等版本控制系统,您可能会认为我们已经完成了。虽然我可以愉快地从这里继续编辑、添加、提交和分支,但是如果我想使用中央存储库共享这些代码,我还需要考虑一个额外的阶段。正如我们将在本章后面看到的,Git 允许我们管理多个项目分支。多亏了这个特性,我可以为每个版本维护一个分支,而且还可以将我最前沿的高风险开发安全地保留在我的产品代码之外。当我们开始时,Git 建立了一个名为master的分支。我可以用命令git branch确认我的分支的状态:

$ git branch -a

* master

-a标志指定 Git 应该向我们显示所有分支(缺省情况是忽略远程分支)。输出显示master支路。

事实上,我还没有将我的本地存储库与远程服务器关联起来。是时候纠正这个错误了:

$ git remote add origin git@poppch17.vagrant.internal:/var/git/megaquiz

考虑到它所做的工作,这个命令安静得令人失望。事实上,这相当于告诉 Git“将昵称origin与给定的服务器位置相关联”。此外,在本地分支机构master和远程对等机构之间建立跟踪关系。”

为了确认所有这些,我用 Git 检查远程句柄origin是否已经设置好:

$ git remote -v

origin git@poppch17.vagrant.internal:/var/git/megaquiz (fetch)
origin git@poppch17.vagrant.internal:/var/git/megaquiz (push)

当然,如果你使用像 GitHub 这样的服务,你会使用图 17-2 所示的git remote add步骤。在我的例子中,它看起来像这样:

$ git remote add origin git@github.com:poppbook/megaquiz.git

但是,不要运行前面的命令,除非您真的想推送到我的 GitHub repo!我现在坚持使用我的自托管 Git 存储库。

然而,我还没有向我的 Git 服务器发送任何实际的文件,所以这是我的下一步:

$ git push origin master
Counting objects: 16, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (15/15), done.
Writing objects: 100% (16/16), 8.87 KiB | 0 bytes/s, done.
Total 16 (delta 2), reused 0 (delta 0)
remote: Resolving deltas: 100% (2/2), done.
To git@github.com:poppbook/megaquiz.git
* [new branch] master -> master

现在我可以再次运行git branch命令来确认master分支的远程版本已经出现:

$ git branch -a

* master
  remotes/origin/master

或者,只查看远程分支:

$ git branch –r

   origin/master

Note

我建立了一个叫做的跟踪分支。这是与远程 twin 相关联的本地分支。

克隆存储库

为了这一章的目的,我已经虚构了一个名为 Bob 的团队成员。鲍勃正在和我一起做大测验项目。自然,他想要自己版本的代码。我已经将他的公钥添加到 Git 服务器中,所以他可以开始了。在 GitHub 的平行世界中,我邀请了 Bob 加入我的项目,他在自己的帐户中添加了自己的公钥。效果是一样的;Bob 可以使用命令git clone获取存储库:

$ git clone git@github.com:poppbook/megaquiz.git

Cloning into 'megaquiz'...
remote: Enumerating objects: 16, done.
remote: Counting objects: 100% (16/16), done.
remote: Compressing objects: 100% (13/13), done.
remote: Total 16 (delta 2), reused 16 (delta 2), pack-reused 0
Receiving objects: 100% (16/16), 8.87 KiB | 0 bytes/s, done.
Resolving deltas: 100% (2/2), done.

现在我们两个都可以在本地开发,当我们准备好的时候,彼此分享我们的代码。

更新和提交

当然,Bob 是一个优秀且有才华的人——除了一个非常令人讨厌的共同特点:他不能不管别人的代码。

Bob 既聪明又好奇,很容易被闪亮的新开发途径所激发,他热衷于帮助优化新代码。结果,无论我走到哪里,我似乎都能看到鲍勃的手。Bob 补充了我的文档,他实现了我在喝咖啡时提到的一个想法。我可能要杀了鲍勃。然而,与此同时,我必须处理这样一个事实,即我正在处理的代码需要与 Bob 的输入合并。

这里有一个文件叫做quizobjects/User.php。目前,它只包含最少的骨头:

namespace poppbook\megaquiz\quizobjects;
class User
{
}

我决定添加一些文档。我首先向我的文件版本添加一个文件注释:

namespace popp\ch17\megaquiz\quizobjects;

/**
 * @license http://www.example.com Borsetshire Open License
 * @package quizobjects
 */

class User
{
}

记住,一个文件可以有三种状态:未修改已修改已登台User.php文件现在已经从未修改的移动到已修改的。我可以用git status命令看到这一点:

$ git status

# On branch master
# Changes not staged for commit:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#      modified: src/quizobjects/User.php
#
no changes added to commit (use "git add" and/or "git commit -a")

User.php已被修改,但尚未准备提交。我可以使用命令git add改变这种状态:

$ git add src/quizobjects/User.php
$ git status

# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#      modified: src/quizobjects/User.php
#

现在我准备提交:

$ git commit -m'added documentation' src/quizobjects/User.php

[master 997622c] added documentation
1 file changed, 5 insertions(+)

Git 提交只影响我的本地存储库。如果我确信世界已经为我的改变做好了准备,我必须将我的代码推送到远程存储库:

$ git push origin master
Counting objects: 9, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (5/5), 537 bytes | 0 bytes/s, done.
Total 5 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To git@github.com:poppbook/megaquiz.git
    ce5a604..997622c master -> master

与此同时,在自己的沙盒中工作的鲍勃一如既往地热衷于此,他创建了一个类评论:

namespace popp\ch17\megaquiz\quizobjects;

/**
 * @package quizobjects
 */

class User
{
}

现在轮到鲍勃了addcommitpush。因为添加和提交部分经常一起运行,所以 Git 允许您将它们合并到一个命令中:

$ git commit -a -m'my great documentation'

[master 13de456] my great documentation
 1 file changed, 4 insertions(+)

所以我们现在有两个不同版本的User.php。一个是我刚刚推送到远程存储库的版本,另一个是 Bob 的版本,已提交,但尚未推送到远程存储库。让我们看看当 Bob 试图将他的本地版本推送到远程存储库时会发生什么:

$ git push origin master

To git@github.com:poppbook/megaquiz.git
 ! [rejected]        master -> master (fetch first)
error: failed to push some refs to 'git@github.com:poppbook/megaquiz.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first merge the remote changes (e.g.,
hint: 'git pull') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

如您所见,如果有更新要应用,Git 不会让您推送。Bob 必须首先下载我版本的User.php文件:

$ git pull origin master

remote: Enumerating objects: 9, done.
remote: Counting objects: 100% (9/9), done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 5 (delta 1), reused 5 (delta 1), pack-reused 0 Unpacking objects: 100% (5/5), done.
From github.com:poppbook/megaquiz
 * branch            master     -> FETCH_HEAD
Auto-merging src/quizobjects/User.php
CONFLICT (content): Merge conflict in src/quizobjects/User.php
Automatic merge failed; fix conflicts and then commit the result.

Git 很乐意将来自两个源的数据合并到同一个文件中,只要变化不重叠。Git 无法处理影响相同行的更改。它如何决定什么是有优先权的?存储库应该覆盖 Bob 的更改,还是相反?这两种变化应该共存吗?哪个应该先走?Git 别无选择,只能报告冲突,让 Bob 解决问题。

以下是 Bob 打开文件时看到的内容:

/**
<<<<<<< HEAD
 * @package quizobjects
 */
=======
 * @license http://www.example.com Borsetshire Open License
 * @package quizobjects
 */
>>>>>>> f36c6244521dbd137b37b76414e3cea2071958d2

namespace poppbook\megaquiz\quizobjects;

class User
{
}

Git 包括 Bob 的注释和冲突的更改,以及告诉他哪个部分来自哪里的元数据。冲突的信息由一行等号隔开。Bob 的输入由一行小于符号后跟“HEAD”来表示。远程更改包含在分界线的另一边。

现在 Bob 已经确定了冲突,他可以编辑文件来修复冲突:

/**
 * @package quizobjects
 * @license http://www.example.com Borsetshire Open License
 * @package quizobjects
 */

namespace poppbook\megaquiz\quizobjects;

class User
{
}

接下来,Bob 通过暂存文件来解决冲突:

$ git add src/quizobjects/User.php
$ git commit -m'documentation merged'

[master c99d3f5] documentation merged

现在,他终于可以推送到远程存储库了:

$ git push origin master

添加和删除文件和目录

项目在发展过程中会改变形状。版本控制软件必须考虑到这一点,允许用户添加新文件,并删除那些碍事的无用文件。

添加文件

您已经多次看到了add子命令。在我的项目设置期间,我使用它将我的代码添加到空的megaquiz存储库中,并随后准备提交文件。通过在未跟踪的文件或目录上运行git add,您要求 Git 跟踪它——并准备提交。在这里,我将一个名为CompositeQuestion.php的文档添加到项目中:

$ touch src/quizobjects/CompositeQuestion.php
$ git add src/quizobjects/CompositeQuestion.php

在现实世界中,我可能会从给CompositeQuestion.php添加一些内容开始。这里,我将自己限制在使用标准的touch命令创建一个空文件。一旦我添加了一个文档,我仍然必须调用commit子命令来完成添加:

$ git commit -m'initial check in'

[master 323bec3] initial check in
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 src/quizobjects/CompositeQuestion.php

CompositeQuestion.php现在位于本地存储库中。

删除文件

如果我发现我太仓促了,需要删除文档,那么毫不奇怪我可以使用名为rm的子命令:

$ git rm src/quizobjects/CompositeQuestion.php
rm 'src/quizobjects/CompositeQuestion.php'

再次需要一个commit来完成工作。和往常一样,我可以通过运行git status来确认这一点:

$ git status

# On branch master
# Your branch is ahead of 'origin/master' by 1 commit.
#   (use "git push" to publish your local commits)
#
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#       deleted:    src/quizobjects/CompositeQuestion.php
#

$ git commit -m'removed Question'

[master 5bf88aa] removed CompositeQuestion
 1 file changed, 0 insertions(+), 0 deletions(-)
 delete mode 100644 src/quizobjects/CompositeQuestion.php

添加目录

还可以用addrm添加和删除目录。假设 Bob 想要创建一个新目录:

$ mkdir resources
$ touch resources/blah.gif
$ git add resources/
$ git status

# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#       new file: resources/blah.gif
#

注意resources的内容是如何自动添加到存储库中的。现在 Bob 可以提交,然后以通常的方式将全部内容推送到远程存储库。

Note

对目录使用git add时要小心;简直是贪心!该命令将选取给定目录下的任何文件和目录。用git status检查操作总是一个好主意。

删除目录

如您所料,您可以使用rm子命令删除目录。然而,在这种情况下,我必须告诉 Git,我希望它通过向子命令传递一个-r标志来删除目录的内容。在这里,我完全不同意鲍勃增加一个resources目录的决定:

$ git rm -r resources/
rm 'resources/blah.gif'

标记发布

一切顺利的话,一个项目将最终达到准备就绪的状态,您将想要交付或部署它。每当您发布一个版本时,您应该在您的存储库中留下一个书签,这样您就可以随时在那个时候重新访问代码。如您所料,您可以使用git tag命令在代码中创建一个标记:

git tag -a 'v1.0.0' -m'release 1.0.0'

您可以通过运行不带参数的git tag来查看与您的存储库相关联的标签:

$ git tag
v1.0.0

到目前为止,我们一直在当地开展工作。为了将标签放到远程存储库中,我们必须使用带有git push子命令的--tags标志:

$ git push origin --tags

Counting objects: 1, done.
Writing objects: 100% (1/1), 159 bytes | 0 bytes/s, done.
Total 1 (delta 0), reused 0 (delta 0)
To git@github.com:poppbook/megaquiz.git
 * [new tag]         v1.0.0 -> v1.0.0

使用--tags标志导致所有本地标签被推送到远程储存库。

当然,你在 GitHub repo 上的任何操作都可以在网站上被跟踪。你可以在图 17-4 中看到我的发布标签。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17-4

查看 GitHub 上的标签

一旦你可以用一个标签来标记你的代码,你就可以考虑如何重新访问旧版本了。然而,对于这一点,您应该首先花些时间研究分支 Git 在这方面特别擅长。

分支项目

一旦我的项目发布了,我就可以打包离开,去做一些新的事情,对吗?毕竟,它写得如此优雅,以至于不可能出现 bug,更不用说如此详细地说明,没有用户可能需要任何新功能!

同时,回到现实世界,我必须继续在至少两个层面上使用代码库。Bug 报告现在应该会陆续出现,对 1.2.0 版本的需求会随着对奇妙新特性的需求而膨胀。我如何调和这些力量?我需要修复被报告的错误,并且我需要继续主要的开发工作。当下一个版本稳定时,我可以作为开发的一部分修复 bug 并一次性发布所有内容。但是用户可能要等很长时间才能看到问题得到解决。这显然是不能接受的。另一方面,我可以边走边释放。在这种情况下,我会冒着发布不完整代码的风险。显然,我的发展需要两股力量。我将继续向项目的主分支(通常称为主干)添加新的和有风险的特性,但是我现在应该为我的新版本创建一个分支,在这个分支上我只能添加错误修复。

Note

这种管理分行的方式绝不是城里唯一的游戏。开发人员经常争论组织分支和管理发布和错误修复的最佳方式。最流行的方法之一是 git-flow(在 https://danielkummer.github.io/git-flow-cheatsheet/ 中有清晰的描述)。在这种做法下,master就是发布分支。新代码在一个develop分支上运行,并在发布时合并到master中。每个活跃开发的单元都有自己的特性分支,当稳定时,这些特性分支被合并到develop中。

我可以使用git checkout命令创建并切换到一个新的分支。首先,让我们快速看一下我的分支的状态:

$ git branch -a

* master
  remotes/origin/master

如您所见,我有一个单独的分支,master,和它的远程对等物。现在,我将创建并切换到一个新的分支:

$ git checkout -b megaquiz-branch1.0
Switched to a new branch 'megaquiz-branch1.0'

为了跟踪我对分支的使用,我将使用一个特定的文件作为例子,src/command/FeedbackCommand.php。看来我及时创建了我的 bug 修复分支。用户开始报告他们无法使用系统中的反馈机制。我找到了窃听器:

//...
$result = $msgSystem->despatch($email, $msg, $topic);
if (! $user)  {
    $this->context->setError($msgSystem->getError());
//...

事实上,我应该测试$result而不是$user。以下是我的编辑:

//...
$result = $msgSystem->dispatch($email, $msg, $topic);
if (! $result)  {
    $this->context->setError($msgSystem->getError());
//...

因为我正在分支megaquiz-branch1.0上工作,所以我可以提交这个变更:

$ git add src/command/FeedbackCommand.php
$ git commit -m'bugfix'

[megaquiz-branch1.0 6e56ade] bugfix
 1 file changed, 1 insertion(+), 1 deletion(-)

当然,这个提交是本地的。我需要使用git push 命令将分支放到远程存储库上:

$ git push origin megaquiz-branch1.0

Counting objects: 9, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (5/5), done.
Writing objects: 100% (5/5), 456 bytes | 0 bytes/s, done.
Total 5 (delta 3), reused 0 (delta 0)
remote: Resolving deltas: 100% (3/3), completed with 3 local objects. remote:
remote: Create a pull request for 'megaquiz-branch1.0' on GitHub by visiting:
remote: https://github.com/poppbook/megaquiz/pull/new/megaquiz-branch1.0
remote:
To git@github.com:poppbook/megaquiz.git
 * [new branch]     megaquiz-branch1.0 -> megaquiz-branch1.0

现在,鲍勃怎么办?他不可避免地会想参与进来并修复一些错误。首先,他调用git fetch,它从服务器获取任何新信息。然后他可以用git branch -a查看所有可用的分支。

$ git fetch
$ git branch -a

* master
  remotes/origin/HEAD -> origin/master
  remotes/origin/master
  remotes/origin/megaquiz-branch1.0

现在 Bob 可以切换到本地分支,该分支将跟踪远程分支:

$ git checkout megaquiz-branch1.0

Branch megaquiz-branch1.0 set up to track remote branch megaquiz-branch1.0 from origin.
Switched to a new branch 'megaquiz-branch1.0'

鲍勃现在可以走了。他可以添加和提交自己的修复;当他用力时,它们会停在远处的树枝上。

同时,我想在主干上添加一些前沿的增强功能——也就是我的master分支。让我们从我的本地存储库的角度再次查看我的分支的状态:

$ git branch -a

  master
* megaquiz-branch1.0
  remotes/origin/master
  remotes/origin/megaquiz-branch1.0

我可以通过调用git checkout切换到现有的分支:

$ git checkout master

Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.

当我现在看command/FeedbackCommand.php时,我发现我的错误修复已经神奇地消失了。当然还是存放在megaquiz-branch1.0下。稍后,我可以将补丁合并到master分支,所以没有必要担心。相反,我可以专注于添加新代码:

class FeedbackCommand extends Command
{

    public function execute(CommandContext $context): bool
    {

        // new and risky development
        // goes here

        $msgSystem = ReceiverFactory::getMessageSystem();
        $email = $context->get('email');

        // ...

我在这里所做的只是添加一个注释来模拟代码的添加。我现在可以提交并推动这个:

$ git commit -am'new development on master'
$ git push origin master

所以我现在有平行的分支。当然,迟早,我会希望我的主分支从我在megaquiz-branch1.0提交的错误修复中受益。

我可以在命令行上做到这一点,但首先让我们暂停一下,看看 GitHub 和 Bitbucket 等类似服务支持的一个特性。pull 请求(通常缩写为 PR)允许我在合并分支之前请求代码审查。所以在 megaquiz-branch1.0 击中 master 之前,我可以让 Bob 检查我的工作。正如你在图 17-5 中看到的,GitHub 检测到了分支,并给了我一个机会来发出我的拉请求。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17-5

GitHub 使得发布拉请求变得容易

在提交拉取请求之前,我点击了按钮并添加了一条评论。你可以在图 17-6 中看到结果。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17-6

发出拉取请求

现在 Bob 可以检查我的更改,并添加他可能有的任何评论。GitHub 向他展示了到底发生了什么变化。你可以在图 17-7 中看到 Bob 的评论。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17-7

拉请求所涵盖的变更

一旦 Bob 批准了我的 pull 请求,我就可以直接从浏览器进行合并,或者返回到命令行。这很简单。Git 提供了一个名为merge的子命令:

$ git checkout master

Already on 'master'

事实上,我已经在主分支上了——但是确定一下也无妨。现在我执行实际的合并:

$ git merge --no-commit megaquiz-branch1.0
Auto-merging src/command/FeedbackCommand.php
Automatic merge went well; stopped before committing as requested

通过传入--no-commit标志,我保持合并未提交——这给了我另一个检查一切正常的机会。一旦我满意了,我就可以开始行动了。

$ git commit -m'merge from megaquiz-branch1.0'

[master e1b5169] merge from megaquiz-branch1.0

Note

合并还是不合并?选择并不总是像看起来那么简单。例如,在某些情况下,您的 bug 修复可能是一种临时的工作,被主干上更彻底的重构所取代,或者由于规范的变化而不再适用。这是一个必要的判断。然而,我工作过的大多数团队倾向于在可能的情况下合并到主干,同时将分支上的工作保持在最低限度。对我们来说,新特性通常出现在主干上,并通过“尽早和经常发布”的策略快速到达用户手中。

现在,当我在master分支上查看FeedbackCommand的版本时,我确认所有的变更已经被合并:

public function execute(CommandContext $context): bool
{

    // new and risky development
    // goes here

    $msgSystem = ReceiverFactory::getMessageSystem();
    $email = $context->get('email');
    $msg = $context->get('pass');
    $topic = $context->get('topic');
    $result = $msgSystem->despatch($email, $msg, $topic);
    if (! $result) {
        $this->context->setError($msgSystem->getError());
        return false;
    }

execute()方法现在包括我模拟的master开发和 bug 修复。

当我第一次“发布”MegaQuiz 版本时,我创建了一个分支,这就是我们一直在做的事情。但是,请记住,我也在那个阶段创建了一个标记。我当时承诺过,我会告诉你如何访问标签。事实上,您已经看到了。您可以基于标签创建一个本地分支,就像 Bob 创建我们的 bug fix 分支的本地版本一样。不同的是,这个新分支是全新的。它不跟踪现有的远程分支:

$ git checkout -b v1.0.0-branch v1.0.0

Switched to a new branch 'v1.0.0-branch'

然而,现在我有了这个新的分支,我可以像你看到的那样推它和分享它。

Note

Git 是一个非常通用和有用的工具。像所有强大的工具一样,它的使用偶尔会导致意想不到的后果。对于那些你已经把自己逼入绝境,需要快速重置的时刻,科技编辑保罗·特雷哥推荐 https://dangitgit.com/en (实际上,他推荐的是更瑞典的版本!).该网站充满了可能会拯救你的理智的食谱,所以如果你认真使用 Git 的话,它非常值得收藏。

另外两个值得拥有的 git 命令是git stashgit stash apply。当您忙于本地编辑,但被要求切换分支时,您的第一个选择是提交正在进行的工作。但是,您可能不想提交粗糙的代码。您可能认为您唯一的选择是丢弃您的本地更改或将它们复制到临时文件中。但是,如果您运行git stash,所有的本地更改都会隐藏在幕后,您的分支会返回到上次提交时的状态。你可以离开去做你的紧急工作,当你准备好的时候,运行git stash apply来取回你未提交的工作。就像魔法一样!

摘要

Git 包含大量的工具,每一个都有令人望而生畏的选项和功能。我只能希望在有限的篇幅内提供一个简要的介绍。尽管如此,如果您只使用我在本章中介绍的功能,您应该会在自己的工作中看到好处,无论是通过防止数据丢失还是改进协作。

在本章中,我们浏览了 Git 的基础知识。在导入项目之前,我简要地看了一下配置。我签出、提交和更新了代码,然后向您展示了如何标记和导出一个版本。在本章的最后,我简要介绍了分支,展示了它们在维护项目中的并发开发和 bug 修复链中的作用。

在某种程度上,我忽略了一个问题。我们建立了一个原则,即开发人员应该签出他们自己的项目版本。但是,总的来说,项目不会原地运行。为了测试他们的更改,开发人员需要在本地部署代码。有时,这就像复制几个目录一样简单。然而,更常见的情况是,部署必须解决一系列配置问题。在下一章,我们将研究一些自动化这一过程的技术。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值