Laravel - 为 WEB 艺术家创造的 PHP 框架。

PHP THAT DOESN'T HURT. CODE HAPPY & ENJOY THE FRESH AIR.

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\BillingKarani\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\ReceiptKarani\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字段在文件定引入这个类就可以了。

雨师
关于作者 雨师
广州
由于学艺不精,内容可能有误。如有错误欢迎联系邮箱312841925@qq.com指出。谢谢!