[Modern PHP] 第三章 标准
PHP组件和框架的数量多的让人难以置信。有像Symfony和Laravel这样的巨型框架,也有像Silex和Slim这样的微型框架。甚至还有些在现代PHP组件出现之前就存在很久的传统框架,例如CodeIgniter。现代PHP生态系统是一个名副其实的代码大熔炉,这有助于开发者们构造令人惊奇的应用。
不幸的是,那些老的PHP框架都是在相对封闭的环境下开发出来的,它们没法与其他的PHP框架共享代码。如果你的项目使用某个老的PHP框架,你会牢牢被困在框架之中,陷入框架自身的生态系统而不能自拔。如果你对框架本身提供的工具还算满意,那么这种相对集中的环境倒也没有什么问题。但是,如果使用了CodeIgniter框架的同时还想从Symfony框架中挑选几个优秀的类库来使用,那你可能就没有那么幸运了,除非你自己专门为你的项目写个一次性的适配器。
what we've got here is a failure to communicate(在这里我们完全无法沟通)
--电影铁窗喋血的经典台词
看到问题所在了吗?封闭的环境中开发出来的框架它们的设计初衷就不是为了和其它框架进行互通。无论是对开发者来说(被所选择的框架限制了创造力)还是对框架自身(他们重复开发着某些已经存在的代码)来说,这种设计方式是非常低效的。尽管如此,我还是有好消息要告诉大家。PHP社区对框架的认识已经从集中式框架模型过渡到了由高效、公用、专一的组件构成的分布式生态系统。
PHP-FIG的营救计划
一些PHP框架的开发者们意识到这个问题,于是他们在2009年的php|tek(著名的PHP会议)上进行了讨论。讨论的核心是如何提升框架内部的互通性和开发效率。譬如,如果某个PHP框架可以分享出一个monolog这样的非耦合的日志类,那么我们是不是就不用每次都去开发一个新的紧耦合的日志记录类了?如果某个PHP框架可以使用Symfony框架的symfon/httpfoundation组件中的优秀的HTTP request和response类,那么我们是不是就不需要开发自己的HTTP request和response类了?为了实现这个目标,PHP框架需要使用某种通用的语言来实现交互以及分享它们的框架。而它们需要的就是标准。
这些在php|tek会议上偶遇的PHP框架的开发者们最终创建了PHP Framwork Interop Group(PHP-FIG)。PHP-FIG就是由一群PHP框架项目的代表组成的,按照PHP-FIG的官方网站的说法,他们的职责就是“讨论各自项目的共性从而找到可以协同工作的方式”。PHP-FIG制定了一些推荐(recommendations),PHP的框架可以自愿选择是否实现这些recommendations以提高交互性并共享各自的框架。
PHP-FIG是一群PHP框架项目的代表自创的组织。它的会员并不是通过选举产生的,除了满怀着发展PHP社区的意愿外这些会员并没有什么特别之处。任何人都可以申请会员资格。任何人都可以对处于提议阶段的PHP-FIG recommendations提交自己的反馈。最终的PHP-FIG recommendations通常被采纳并被许多最大最流行的PHP框架实现。我非常鼓励你参与到PHP-FIG之中,你只要做到发送你的反馈、帮助你最喜爱的PHP框架一起打造未来。
我们常说PHP-FIG负责提供各种recommendations,理解这一点非常重要,因为推荐不等同于规则,推荐不是必须要执行的。这些recommendations都是经过审慎考虑、精雕细琢的建议,它们可以为PHP开发者们(甚至PHP框架的作者们)带来更多的便利。
框架协作
PHP-FIG的任务是实现框架协作。而框架协作意味着通过接口、自动加载和编码风格来实现协同工作。
接口
PHP框架之间通过共享的接口协同工作。框架通过PHP接口可以确认第三方依赖库提供的了哪些方法,而无需考虑这些依赖库是如何实现这些接口的。
参考第二章,可以了解PHP接口的详细说明。
举个例子,某个PHP框架很开心的分享了一个第三方的logger对象,假设这个对象实现了emergency()、alert()、critical()、error()、warning()、notice()、info()和debug()这几个方法。这些方法具体是怎么实现的无关紧要。每个框架只需要关心第三方依赖库是否实现了这些方法就行。
接口使得PHP开发者们可以构造、分享和使用功能专一的组件取代庞大的框架。
自动加载
PHP框架之间通过自动加载协同工作。通过自动加载功能,PHP解释器在运行时可以自动定位并根据需要来加载一个PHP类。
在PHP标准制定之前,PHP组件和框架使用魔术函数\_autoload()或者更高级点的spl_autoload_register()方法来实现它们自己独立的自动加载器。这迫使我们需要去熟悉每个组件和框架各自特有的自动加载器才能使用它们。现如今,绝大多数现代PHP组件和框架都与公用的自动加载器标准兼容。这意味着我们可以使用一个统一的自动加载器将多个PHP组件组合并匹配在一起使用。
编码风格
PHP框架之间通过编码风格来协同工作。你的编码风格决定了空格、大小写以及括号的位置(尤其重要)。如果所有的PHP框架都能统一一套标准的编码风格,那么PHP开发者们就不需要每次在使用新的PHP框架时还得去适应新的编码风格,PHP框架的代码就不会显得那么陌生。一套标准的编码风格还能降低项目中新人的入门门槛,这样他们就可以集中精力去解决bug,而不用花大量的事件去学习陌生的编码风格。
标准的编码风格还能改进我们自己的项目。每个开发者都有在编码上的独特癖好,当开发者们在同一个代码库上一起工作时麻烦就会出现。一套标准的编码风格能够帮助所有的团队成员立刻明白同一个代码库中彼此的代码而不用去管代码的作者是谁。
PSR是什么?
PSR是PHP standards recommendation(PHP推荐标准)的缩写。如果你最近阅读过一些PHP方面的博客,你或许已经看到了一些诸如PSR-1、PSR-2、PSR-3这样的术语。这些都是PHP-FIG的推荐。他们的名字以PSR-开头,结尾跟着一个数字编号。每个PHP-FIG的推荐都是用来解决在大多数PHP框架中经常遇到的特定问题的。为了不让各种PHP框架不断的去解决同样的问题,它们可以采纳这些PHP-FIG的推荐,在共享的解决方案基础上构建各自框架。
截止到本书发行为止,PHP-FIG已经发布了5个推荐:
如果你数一数会发现只有4个推荐,没错,PHP-FIG已经废弃了最早的PSR-0推荐。最初的推荐已经被最新的PSR-4所取代。
注意PHP-FIG的这些推荐正好完美匹配了我之前提到的协同工作的三种方式:接口、自动加载和编码风格。这不是巧合。
对于PHP-FIG的这些推荐我真的感到很兴奋。它们是现代PHP生态系统下的基石。它们定义了PHP组件和框架协同工作的方法。我承认,讨论PHP标准并不是一个吸引眼球的话题,但是它们(在我心中)是了解现代PHP的前提。
PSR-1:基本编码风格
如果你想让自己写的代码能够兼容PHP社区标准,从PSR-1起步。这是使用起来最简单的标准。它非常简单,以至于你都没有意识到或许自己已经在使用这些标准了。PSR-1提供了不需要花太大精力就可以实施的简单指导原则。PSR-1的设计目的就是提供一个可以参与到PHP框架中的基准编码风格。要与PSR-1标准兼容你需要满足下面的几个需求:
PHP标签
你必须将你的PHP代码包含在或者= ?>标签之间。你不可以使用其它任何PHP标签语法。
字符编码
所有的PHP文件必须使用不包含byte order mark(字节顺序标记)(BOM)的UTF-8编码。这听起来有点麻烦,但是大多数编辑器或者IDE都可以替你自动处理
文件用途
在一个单独的PHP文件中,要么在里面定义声明(一个类、trait、函数或者常量等),要么执行一个能产生结果的操作(譬如输出信息或者修改数据)。但是不可以两者兼顾。对于你来说这是一个简单的任务,只需要一点点的前瞻性和计划性。
自动加载
你的PHP名字空间和类必须支持PSR-4自动加载标准。你要做的就是为你的PHP声明起一个合适的名字并保证它们的定义文件保存在正确的路径。我们稍后会讨论PSR-4。
类名
你的PHP类名必须使用公用的CamelCase(大写驼峰)格式。这个格式也称作TitleCase。例如CoffeeGrinder、CoffeeBean和PourOver。
常量名
你的PHP常量必须全部大写。如果需要的话可以使用下划线来分隔单词。例如WOOT、LET_OUR_POWERS_COMBINE和GREAT_SCOTT。
方法名
你的PHP方法名必须使用公用的camelCase(小写驼峰)格式。这意味着方法名的首字母都是小写,而随后的每个子单词的首字母仍然是大写。例如 phpIsAwesome、iLoveBacon和tennantIsMyFavoriteDoctor。
PSR-2:严格编码风格
在实现了PSR-1标准后,下一步就是实现PSR-2标准。PSR-2标准定义了包含更严格的指导原则的PHP编码风格。
PSR-2编码风格是全世界的众多贡献者赐予PHP框架的礼物,他们分享了各自特有的编码风格和偏好。公用的严格的编码风格使得开发者们能够写出对其他人来说简单易懂的代码。
与PSR-1不同,PSR-2推荐包含了更严格的指导原则。有些PSR-2的指导原则可能并不如你所愿。然而PSR-2还是大多数流行PHP框架首选的编码风格。没必要一定得使用PSR-2,但是如果使用了的话,对别的开发者来说你的PHP代码的可读性和可用性会获得到大大的提升。
建议你使用更严格的PSR-2编码风格,即使我用了严格这个词,但是写起来还是很简单的,事实上多写写就习惯了。另外,还有一些可以自动将代码自动格式化为PSR-2风格的工具可供我们使用。
实现PSR-1
PSR-2编码风格的前提是你必须先实现PSR-1编码风格
缩进
这是两大阵营之间的热门话题。一方更偏爱使用一个tab制表符来进行代码缩进。而另一方(更酷的一方)更偏爱使用几个空格来实现代码缩进。PSR-2推荐规定PHP代码应该使用四个空格进行缩进
根据个人经验,空格字符更适合缩进,因为空格在不同的代码编辑器中的宽度几乎都是一样的。而一个tab制表符的宽度在不同的代码编辑器中却各不相同。使用四个空格字符来缩进代码可以保证你的代码的最佳的视觉一致性。
文件和行
你的PHP文件必须使用Unix换行(LF)作为每行的结束,文件末尾必须有一个空行,不可以使用 ?> PHP标签作为结束。每行代码不应该超过80个字符。即使放宽标准,但是说到底每行的代码也不可以超过120个字符。每行的末尾都不可以有多余的空格。这些听起来工作量不少,但是这都是必不可少的。大多数的代码编辑器都可以自动的将代码按照宽度换行、过滤末尾的空字符以及使用Unix换行结尾。所有的这些问题都应该自动处理,而不需要你每次来考虑。
最初我认为忽略结尾的 ?> PHP标签是个很奇怪的要求。但是忽略结束标签来避免未知的输出错误确实是一个很好的习惯。如果你在文件末尾使用了?>结束标签,之后又在后面多了一个空行,这个空行会被当作输出从而导致错误(譬如当你使用header函数设置HTTP头时)。
关键词
我知道许多PHP开发者们会以全大写的形式来书写TRUE、FALSE和NULL。如果你是这么做的,请尝试忘掉这种习惯从现在开始转而使用全小写的写法。PSR-2推荐指出所有的PHP关键词都需要以全小写的形式书写。
名字空间
每个名字空间的声明之后必须有一个空行。同样的,当你使用use关键词来导入名字空间或者其别名时,你也必须在use声明之后加上一个空行,请看下面的例子:
namespace My\Component;use Symfony\Components\HttpFoundation\Request;use Symfony\Components\HttpFoundation\Response;class App{ // 类的定义体}
和缩进一样,类定义中的括号位置也是引发另一个强烈争议的话题。有些人喜欢将开始括号放在类名后的同一行中,而另一些人更喜欢另起一行,将开始括号放在类名后新的一行中。PSR-2推荐指出,类定义的开始括号必须像下面的例子中一样放在类名的定义语句后的新的一行中。类定义的结束括号必须放置在类定义体末尾之后新的一行中。或许你早就按照我们说的做了,所以没什么大不了的。如果你的类继承自其它的类或者实现了其它的接口,extends和implements关键词必须与类名保持在统一行中:
<?php namespace My\App;class Administrator extends User{ // 类的定义体}
方法
方法定义中的括号位置与类定义的括号位置一样。方法定义的开始括号放到方法名之后新的一行中。方法定义的结束括号放到方法定义体末尾的新的一行中。注意方法的参数。第一个圆括号之后不可以有空格,而最的圆括号之前也不可以有空格。每个方法的参数(除了最后一个)后都紧跟着一个逗号和一个空格:
<?phpnamespace Animals;class StrawNeckedIbis{ public function flapWings($numberOfTimes = 3, $speed = 'fast') { // 方法定义体 }}
你必须给每个类的属性和方法声明一个作用域。作用域是public、protected或者private之一,作用域决定了一个属性或者方法在类的内部或者外部是否可以被访问。老派的PHP开发者们可能习惯了在类的属性前加上var关键词以及在私有方法前加上下划线_。千万别学他们这么做,你必须使用前面介绍的三个作用域之一。如果你需要将一个类的属性或者方法声明为abstract或者final,那么abstract和final修饰符必须放到作用域之前。如果你将一个属性或者方法声明为静态的,static修饰符必须放在作用域之后:
<?phpnamespace Animals;class StrawNeckedIbis{ // 设置了作用域的静态属性 public static $numberOfBirds = 0; // 设置了作用域的方法 public function __construct() { static::$numberOfBirds++; }}
这可能是我最容易犯错的指导原则。所有的控制结构的关键词都必须跟随一个单独的空字符。控制结构关键词包括了:if、elseif、else、switch、case、while、do while、for、foreach、try、catch。如果控制结构关键词需要使用一对圆括号,请确保第一个圆括号后面不要加空格,以及最后一个圆括号前面不要加空格。与类和方法的定义不同,开始括号必须放在控制结构关键词之后,并保持与控制结构关键词在同一行中。控制结构关键词的结束括号必须另起一行。下面用一个简单的例子来说明这些指导原则:
<?php $gorilla = new \Animals\Gorilla;$ibis = new \Animals\StrawNeckedIbis;if ($gorilla->isAwake() === true) { do { $gorilla->beatChest(); } while ($ibis->isAsleep() === true); $ibis->flyAway();}
你还可以使用Fabien Potencier的PHP-CS-Fixer来自动修正绝大多数的代码不兼容性。这个工具虽然还不够完美,但是有了它你只需要花很少的精力就可以保证尽可能的遵循PSR兼容性。
PSR-3:Logger接口
第三个PHP-FIG的推荐标准并不像前面那些一样,它不是一组指导原则。PSR-3是一个接口,它指定了可以用来实现PHP logger组件的方法。
一个logger是一个可以依照各种不同的重要程度来将信息写到给定输出的对象。记录下的信息可以用来分析、检查和修正应用程序的操作、稳定性和性能。例如在开发过程中将调试信息写入到文本文件中,将网站访问统计保存到数据库中,或者将致命错误的分析报告通过邮件发送网站管理员。目前最流行的PHP logger组件是monolog/monolog,作者是Jordi Boggiano。
许多PHP框架在日志记录上有着不同的需求。在PHP-FIG成立之前,每个框架解决日志记录的方式都各不相同,通常都会有一套自己特有的实现。本着协同工作和专业化(现代PHP不变的主题)的精神,PHP-FIG建立了PSR-3 logger接口。框架接受兼容PSR-3的logger来实现两个重要的目标:将日志记录的开发工作交给第三方来完成,框架的使用者可以选择他们喜欢的logger组件。对大家来说这是一个双赢的结果。
写一个PSR-3的Logger类
一个兼容PSR-3推荐标准的PHP logger组件必须包含一个实现了Psr\Log\LoggerInterface接口的类。PSR-3接口复制了RFC 5424 系统日志协议并且定义了九个方法:
<?phpnamespace Psr\Log;interface LoggerInterface{ public function emergency($message, array $context = array()); public function alert($message, array $context = array()); public function critical($message, array $context = array()); public function error($message, array $context = array()); public function warning($message, array $context = array()); public function notice($message, array $context = array()); public function info($message, array $context = array()); public function debug($message, array $context = array()); public function log($level, $message, array $context = array())}
使用$context参数可以构造出复杂的logger信息。你可以在信息中使用placeholders(占位符)。一个占位符看起来就像{placeholder_name}这样,它有开始花括号、占位符名称、结束花括号组成。占位符中不可以包含空格。而$context参数是一个关联数组,它的键就对应着占位符的名称(两个括号中间部分),而它的值可以用来替换消息文本中对应的占位符。
要写一个PSR-3的logger,创建一个新的PHP类,实现Psr\Log\Logger接口并为每个接口方法提供具体的实现就可以了。
使用PSR-3 Logger
如果你在开发自己的PSR-3 logger,赶紧收手,好好考虑一下你这样浪费时间是否明智。我强烈不建议你去开发自己的logger,为什么?因为现在已经有了一些堪称完美的PHP logger组件可供使用。
如果你需要一个PSR-3的logger,直接使用monolog/monolog就好了。别浪费事件到处寻找了。Monolog组件全面实现了PSR-3接口,而且它非常易于使用自定义消息格式化工具和handlers进行扩展。Monolog的消息handlers允许你将日志消息发送到文本文件、syslog、邮箱、HipChat、Slack、网络服务器、远程API、数据库以及任何你可以想象的地方。在某种极端的情况下,如果Monolog无法提供一个handler来满足你奇葩的输出目标,开发一个你自己的Monolog消息handler并集成到Monolog中也是非常简单的。例子 3-1 演示了设置Monolog并将消息记录到文本文件中是多么的简单:
例子 3-1 使用Monolog
<?phpuse Monolog\Logger;use Monolog\Handler\StreamHandler;date_default_timezone_set('America/New_York');// 准备 logger$log = new Logger('myApp');$log->pushHandler(new StreamHandler('logs/development.log', Logger::DEBUG));$log->pushHandler(new StreamHandler('logs/production.log', Logger::WARNING));// 使用 logger$log->debug('This is a debug message');$log->warning('This is a warning message');
第四个PHP-FIG的推荐标准描述了一个标准化的autoloader策略。autoloader是一个用来在运行时按需求查找PHP类、接口或者trait并将它们加载到PHP解释器中的策略。支持PSR-4autoloader标准的PHP组件和框架可以通过一个唯一的autoloader定位并加载到PHP解释器中。很多可协同工作的组件能够关联到现代PHP生态系统中,PSR-4功不可没。
为什么autoloader这么重要
你是不是经常在你的PHP文件顶部看到这样的代码?
<?phpinclude 'path/to/file1.php';include 'path/to/file2.php';include 'path/to/file3.php';
在PHP-FIG引入PSR-4推荐标准之前,PHP组件和框架的作者们使用__autoload()和spl_autoload_register()函数来注册自定义的autoloader策略。不幸的是,每个PHP组件和框架的autoloader都是特有的,每个autoloader使用了不同的逻辑来定位和加载PHP类、接口和trait。开发者们被迫不得不在自己的PHP应用程序初始化时去调用每个组件的autoloader之后才能使用这些组件和框架。我总是喜欢使用Sensio Labs的Twig模板组件。这个组件真的很棒。然而,如果没有PSR-4的话,我就不得不去阅读Twig的文档才能弄明白如何才能将它自定义的autoloader注册到我自己应用程序的初始化文件中,就像下面这样:
<?phprequire_once '/path/to/lib/Twig/Autoloader.php';Twig_Autoloader::register();
PSR-4 Autoloader策略
如果其它PHP的autoloader一样,PSR-4描述了一个在运行时定位和加载PHP类、接口以及trait的策略。PSR-4推荐标准并不要求你取修改自己的代码的实现。事实上,PSR-4只是给出了如何将你的代码按文件系统目录和PHP名字空间进行组织的建议。PSR-4 autoloader策略依赖于PHP名字空间和文件系统目录来定位和加载PHP类、接口和trait。
PSR-4的实质是将一个顶级的名字空间前缀映射到一个特定的文件系统目录上。例如,我可以告诉PHP,所有在\Oreilly\ModernPHP名字空间下的类、接口或者trait都存放在src/这个物理文件系统目录下。那么PHP现在就能知道,任何使用了\Oreilly\ModernPHP这个名字空间前缀的类、接口或者trait都能对应到src/目录下的子目录和文件。例如,\Oreilly\ModernPHP\Chapter1这个名字空间对应到src/Chapter1目录,而\Oreilly\ModernPHP\Chapter1\Example类对应到src/Chapter1/Example.php文件。
PSR-4允许你将一个名字空间前缀映射到一个文件系统目录上。名字空间的前缀可以是一个顶级的名字空间。名字空间前缀也可以是一个顶级的名字空间加上任意多个子名字空间。相当的灵活。
还记得我们在第二章中提到的vendor名字空间吗?PSR-4 autoloader策略与发布代码给其他开发者的组件和框架作者息息相关。一个PHP组件的代码都在一个唯一的vendor名字空间之下,组件的作者可以指定哪个文件系统目录可以与组件的vendor名字空间相对应,就像我前面演示的那样。我们将在第四章中探索这个概念。
如何开发一个PSR-4 Autoloader(以及为什么不建议你这么做)
我们知道PSR-4的兼容代码都要有一个与基本文件系统路径对应的名字空间。我们也知道名字空间前缀下的子名字空间对应着基本文件系统目录的子目录。例子3-2展示了一个从PHP-FIG网站借来的autoloader的实现,它可以用来查找并加载基于PSR-4 autoloader策略的类、接口和trait。
例子 3-2 PSR-4 autoloader
<?php /** * An example of a project-specific implementation. * * After registering this autoload function with SPL, the following line * would cause the function to attempt to load the \Foo\Bar\Baz\Qux class * from /path/to/project/src/Bar/Baz/Qux.php: * * new \Foo\Bar\Baz\Qux; * * @param string $class The fully-qualified class name. * @return void */spl_autoload_register(function ($class) { // project-specific namespace prefix $prefix = 'Foo\\'; // base directory for the namespace prefix $base_dir = __DIR__ . '/src/'; // does the class use the namespace prefix? $len = strlen($prefix); if (strncmp($prefix, $class, $len) !== 0) { // no, move to the next registered autoloader return; } // get the relative class name $relative_class = substr($class, $len); // replace the namespace prefix with the base directory, replace namespace // separators with directory separators in the relative class name, append // with .php $file = $base_dir . str_replace('\\', '/', $relative_class) . '.php'; // if the file exists, require it if (file_exists($file)) { require $file; }});