PHP命名空间的简介
##一小段历史##
在PHP5.3之前的版本(2009年以前),我们定义的所有类都在同一个全局性的层级下。
User
类,Contact
类,StripeBiller
类,它们都在同一个全局命名空间下。
这看起来很简单,但是这将让代码结构变得冗杂,所以PHPer开始使用在类名里使用下划线。例如:如果我写了一个包叫做“Cacher”
,我可能将包里的一个类命名为Mattstauffer_Cacher
,以区分出它和其他Cacher
的异同——例如Mattstauffer_Database_Cacher
,好表明它就是一个API缓存器。
为了看起来更舒服,甚至通过自动加载的方式,将类名以下划线分割作为文件夹名来存放文件,例如 Mattstauffer_Database_Cacher
存放在目录Mattstauffer/Database/Cacher.php
下
自动加载是一段用于在整个php应用中代替require或者include方法的代码,PHP有一个特别的机制去需找代码的位置。在后文会有更详尽的介绍
但是这仍然是相当混乱,类有被命名为叫做Zend_Db_Statement_Oracle_Exception
,或者更糟糕的名字。值得庆幸的是,在PHP5.3版本中,引入了命名空间。
##命名空间的基础##
在类的结构上,命名空间就像一个虚拟的字典。class Mattstauffer_Database_Cacher可以在Mattstauffer\Database
的命名空间下写作class Cacher
:
<?php
class Mattstauffer_Database_Cacher {}
而现在是
<?php namespace Mattstauffer\Database;
class Cacher {}
而我们在应用的任何一个地方都能以Mattstauffer\Database\Cacher
引用它。
##一个真实的例子##
让我们来看看Karani——这是一个带财务模块的CRM系统,所以系统中其他任何地方都可能用到捐赠者donors
和收据receipts
Karani
被设置为最高级的命名空间,(就像顶级文件夹,通常用你的应用或包的名字命名)。有些类可能与Contacts
有关,有些可能与Billing
有关,所以需要为二者都创建子命名空间:Karani\Billing
和Karani\Contacts
。
就像这样
<?php namespace Karani\Billing;
class Receipt {}
<?php namespace Karani\Billing;
class Subscription{}
<?php namespace Karani\Contacts;
class Donor {}
现在,我们可以画出这样一个字典结构:
Karani
Billing
Receipt
Subscription
Contacts
Donor
##引用同一个命名空间下的类##
所以,这样写能让“给捐赠(Subscription
)开出收据(Receipt
)”变得十分容易:
<?php namespace Karani\Billing;
class Subscription{
public function sendReceipt()
{
$receipt = new Receipt;
}}
由于Receipt
类和Subscription
类在同一个命名空间里,你可以直接像上面那样写,就像没有使用过命名空间一样。
##引用不同命名空间下的类##
好的,如果我想在捐赠者(Donor
)里访问收据(Receipt
)呢
<?php namespace Karani\Contacts;
class Donor{
public function sendReceipt()
{
// This won't work!
$receipt = new Receipt;
}}
你可能会想:这样肯定会出错。
现在代码是在Karani\Contacts
命名空间下,所以如果我们用new Receipt
,PHP假设我们引用的是Karani\Contacts\Receipt
,但是这个类却是不存在的。这不是我们想要的结果
所以,你会得到一个Class Karani\Contacts\Receipt not found
的错误。
你可能想到要改为$receipt = new Karani\Billing\Receipt
,但是也不会生效。我们现在在Karani\Contacts
的命名空间下,任何你写的代码都会默认处在这个命名空间下,那么Karani\Billing\Receipt
将作为名为Karani\Contacts\Karani\Billing\Receipt
的类加载,所以这样也是不对的。
##使用块或者完全限定类名##
你可以有两个选择:
第一种方案,你可以把一个反斜杠加载类名的开头,使用完全限定类名的方式(FQCN (Fully Qualified Class Name)
): $receipt = new \Karani\Billing\Receipt;
这将会在PHP查找类时发出一个信号,让PHP不在当前的命名空间下查找。
如果你使用这种方案,你可以在你应用中任意一个地方使用而不必担心你所在的命名空间的问题。
或者,第二种方案,你可以在类文件的最开头添加上use
,只要这样写就可以正常引用Receipt
:
<?php namespace Karani\Contacts;
use Karani\Billing\Receipt;
class Donor{
public function sendReceipt()
{
$receipt = new Receipt;
}}
在当前命名空间使用use
引入一个不同命名空间下的类,这样让编写代码更加方便。只要你写了这个引用,你在这个类文件里任何时候使用Receipt
,PHP都会认为你是指引入的那个类。
##别名##
但是,要是代码中还有一个Receipt
类在当前的命名空间下呢?要是你的类需要同时使用Karani\Contacts\Receipt
和Karani\Billing\Receipt
两个类呢?
你不能只引用Karani\Billing\Receipt
类,这样还是不能同时使用这两个类——他们在类里面的名字是相同的。
你可以使用别名实现。你可以将use
这种声明方式改为use Karani\Billing\Receipt as BillingReceipt;
。对类起了一个别名,就可以在当前类中使用BillingReceipt
代替Karani\Billing\Receipt
了
##PSR-0/PSR-4中的自动加载(Autoloading)##
你想想上面我用文件夹做的例子?
这很容易想到类的结构,但事实上在命名空间或文件结构之间没有任何内在联系。如果你不使用了自动加载(autoloader
),PHP是不会知道这些类存放在目录结构的什么位置。
幸运的是,自动加载的标准, PSR-0(现在已经过时啦)和PSR-4能够是实现从命名空间匹配到文件夹位置。所以,如果你使用PSR-0或者PSR-4,就非常像你在使用Composer或者其他的现代框架,有一个兼容性的自动加载器,你就能像你的所有类都在同一个文件夹一样使用它们,不用其他require
或者include
了。
##Composer和PSR-4自动加载##
现在我想让Karani
命名空间存在于我的src
文件夹下
例如一个普通的,独立框架项目的文件夹结构
app
public
src
Billing
Contacts
vendor
从上面可以看出,src
文件夹代表Karani
的顶级命名空间。只要我使用composer作为自动加载器,我所需要做的只是教会composer怎么从文件夹匹配命名空间,就能够实现在应用中自动加载。我们现在用PSR-4尝试一下。
我打开composer.json配置文件,然后添加上PSR-4自动加载节点
{
"autoload": {
"psr-4": {
"Karani\\": "src/"
}
}}
你可以看到:左边是我们定义的命名空间(注意你在这需要使用双反斜杠),右边是目录。
##结语##
那还有很多内容,但它们都十分简单:98%的时间你都会享受到PSR-4-structured带来的工作便利,Composer自动实现加载,设置类。
在这98%的时间里,你只需修改composer.json,在里面写明顶级命名空间的根目录,然后你可以有实现目录中的命名空间和文件夹/文件之间有一对一的关系。
记住,下次再出现Class SOMETHING not found
的错误,你很可能只需要用use字段在文件定引入这个类就可以了。