iT邦幫忙

DAY 6
5

逐步提昇PHP技術能力系列 第 6

逐步提昇PHP技術能力 - PHP的語言特性 : Namespaces 與 Class Autoloading

  • 分享至 

  • xImage
  •  

Namespaces也是PHP5.3才加入的功能,最主要的目的是用來組織類別與函數,並且避免名稱的衝突。

autoload則是從PHP5開始就有的功能。為了類別的組織方便,許多人都是以一個類別一個PHP檔的方式來開發,但是如果在使用類別時還需要一個一個include,那就太麻煩了。所以PHP設計了autoload機制,讓開發者自己定義怎樣載入類別的檔案。

類別、組織的命名方式是隨個人喜好的,到了PHP5.3還會加入Namespace,所以實作autoload的時候,除了自己的命名規則,也需要考慮到Namespaces。
參考:
* PHP: Namespaces - Manual
* PHP: Autoload Classes - Manual

不像Java,PHP一直沒有統一的命名規則,所以...需要看情況調整怎麼做autoloading。在專案中會碰到幾個狀況:

  1. 使用到的類別程式檔,分散在數個目錄中(尤其已經多個人接手,然後大家都用自己的習慣來加東西...)
  2. 使用到的第三方程式庫,命名規則不一致。流行的命規則至少有:PEAR、PSR-0等,會影響到檔案名的prefix
  3. 不同的人或不同的程式庫,也可以會使用不同的副檔名規則,例如有人用.php,也有人會加上.class.php等

這些是在還沒Namespace機制時,就已經會有的問題。要再加上Namespace一起考量的話就更複雜了。還是先看看Namespace的怎麼使用的:

* Namespaces

首先來看看語法,在想要定義namespace時:

namespace namespace1\sub1

class a {}
function b(){}

這樣就定義了在namespace1\sub1這個namespace下的類別a以及函數b。如果要使用,可以:

use namespace1\sub1\a;
use namespace1\sub1\b;
$a = new a;
$b = b();

或是加上完整的namespace來識別:

$a = new namespace1\sub1\a;
$b = namespace1\sub1\b();

如果namespace很長,也可以給他先取個別名來用:

use namespace1\sub1 as ns1;
$a = new ns1\a;
$b = new ns1\b();

如果一些類別沒有使用namespace來定義,那他就放在global namespace。這樣要用時,需要在前面加上\,代表這是定義在global namespace中的東西。(下面例子是兩個檔案)

//1-5b.php
<?php
class a {
  function __construct() {
    echo 'a'."\n";
  }
}
function f(){
  echo 'f'."\n";
}

//1-5a.php
<?php
namespace namespace1\sub1;
require '1-5b.php';
class a {
  function __construct() {
    echo 'namespace1\\sub1\\a'."\n";
  }
}
class b {
  function abc() {
    $a1 = new \a;
    $a2 = new a;
    \f();
    f();
  }
}
function f(){
  echo 'namespace1\\sub1\\f'."\n";
}
$b = new b;
$b->abc();

執行結果:

Feng-Hsu-Pingteki-MacBook-Air:ironman6 fillano$ php 1-5a.php 
a
namespace1\sub1\a
f

在前面的語法中,namespace是以檔案為單位的。如果需要在一個檔案中宣告多的namespace,那最好把同一個namespace的程式包裝在一個大括號中:

<?php
namespace {
  class a {
    function __construct() {
      echo 'a'."\n";
    }
  }
  function f(){
    echo 'f'."\n";
  }
}

namespace namespace1\sub1 {
  class a {
    function __construct() {
      echo 'namespace1\\sub1\\a'."\n";
    }
  }
  class b {
    function abc() {
      $a1 = new \a;
      $a2 = new a;
      \f();
      f();
    }
  }
  function f(){
    echo 'namespace1\\sub1\\f'."\n";
  }
  $b = new b;
  $b->abc();  
}

如果程式都有明確的namespace,其實並不一定需要使用大括號,不過這樣會無法定義global namespace的程式,因為namespace必須使用在程式開頭,這樣之後的程式就不是在global namespace了。另外,不使用大括號就在同一個檔案中定義多個namespace,分界也不是那麼明顯,所以還是用大括號來把同樣namespace的程式集合在一起比較好。

不過雖然可以在同一個檔案中定義多個namespace,為了管理以及autoloading,建議還是一個檔案一個namespace就好。

跟Java有一點不一樣,就是PHP的namespace沒有*通配符,所以如果只想用類別名稱來使用定義在其他namespace的類別,在use的語句中一定要包含完整的namespace與類名。(之前被這個卡到過XD)要稍微省事的話,還是得使用as來定義別名,然後透過別名來使用。像這樣:

<?php
namespace namespace1\sub1 {
  class a {
    function show() {
      echo "namespace:\t".__NAMESPACE__."\n";
      echo "class:    \t".__CLASS__."\n";
      echo "method:   \t".__METHOD__."\n";
    }
  }
}
namespace namespace1\sub2 {
  use namespace1\sub1\a;
  class b {
    function show() {
      echo "[".__CLASS__."]\n";
      $a = new a;
      $a->show();
    }
  }
  $b = new b;
  $b->show();
}
namespace namespace2\sub1 {
  use namespace1\sub1 as ns1;
  class c {
    function show() {
      echo "[".__CLASS__."]\n";
      $a = new ns1\a;
      $a->show();
    }
  }
  $c = new c;
  $c->show();
}

namespace1\sub1的類別a,定義了show()方法,會在執行時顯示自己的namespace、class、method資訊。在namespace1\sub2\b以及namespace2\sub1\c中都會使用到這個類別的show()方法。
執行結果:

Feng-Hsu-Pingteki-MacBook-Air:ironman6 fillano$ php 1-5d.php 
[namespace1\sub2\b]
namespace:	namespace1\sub1
class:    	namespace1\sub1\a
method:   	namespace1\sub1\a::show
[namespace2\sub1\c]
namespace:	namespace1\sub1
class:    	namespace1\sub1\a
method:   	namespace1\sub1\a::show

PHP定義了幾個常數,透過他可以知道一些資訊。__NAMESPACE__會顯示當前的namespace,__CLASS__會顯示當前的class,__METHOD__則是當前執行的method,從這些可以看出,namespace資訊,並不會受到別名的影響,這對於使用autoloading來說是很重要的。

* class autoloading

PHP的autoloading機制,原理其實很簡單。在程式產生一個類別實體時,如果找不到類別定義,就會來呼叫使用者定義的autoloading函數,把完整的類別名稱當做參數丟給他。在函數中,需要做的事情就是,依照類別名稱,include或require定義類別的檔案。

如果要配合namespace,最簡單明瞭的對應方式,是利用目錄來對應namespace。假設所有類別檔案都放在vendors目錄,副檔名是.php那:

class a {
	
}

就是vendors/a.php。

namespace namespace1\sub1;
class b {
	
}

就是vendors/namespace1/sub1/b.php等等,依此類推。只要依照個個規則,寫好一個autoloading函數,就可以依照類別名稱去引入正確的類別程式檔。

其實spl中有定義好預設的autoload函數,他可以依照上述的規則找到類別程式檔,所以用這個例子來看:

類別檔的目錄組織:

vendors
├── a.php
├── namespace1
│   ├── sub1
│   │   └── b.php
│   └── sub2
│       └── c.class.php
└── namespace2
    └── sub1
        └── demo
            └── d.php

每個class中都在constructor顯示自己的類別名稱,這樣在new時會顯示類別資訊:

function __construct() {
  echo __CLASS__."\n";
}

然後用預設的spl_autoload來自動載入看看:

<?php
define('CLASS_DIR', 'vendors/');
set_include_path(get_include_path().PATH_SEPARATOR.CLASS_DIR);
spl_autoload_extensions(".php,.class.php");
spl_autoload_register();
$a = new a;
$b = new namespace1\sub1\b;
$c = new namespace1\sub2\c;
$d = new namespace2\sub1\demo_d;

執行的結果:

Feng-Hsu-Pingteki-MacBook-Air:ironman6 fillano$ php 1-5e.php
a
namespace1\sub1\b
namespace1\sub2\c
PHP Fatal error:  spl_autoload(): Class namespace2\sub1\demo_d could not be loaded in /Users/fillano/builds/ironman6/1-5e.php on line 9

結果最後一個會失敗,這是因為demo_d類別是模仿Zend Framework的命名規則,類別名稱如果含有底線的話,例如demo_c,則程式檔是demo/c.php。如果碰到像這樣的例外規則,內建的spl_autoload就無法應付,這時候就需要自己寫,然後呼叫spl_autoload_register來加入。

加入一個自定的autoload函數來處理像這樣特別的命名規則:

<?php
define('CLASS_DIR', 'vendors/');
set_include_path(get_include_path().PATH_SEPARATOR.CLASS_DIR);
spl_autoload_extensions(".php,.class.php");
spl_autoload_register();
spl_autoload_register(function($class) {
  $class = str_replace('_', '/', $class);
  $class = str_replace('\\', '/', $class);
  echo $class."\n";//如果跑到這裡的話,秀一下結果
  include $class.".php";
}, false);
$a = new a;
$b = new namespace1\sub1\b;
$c = new namespace1\sub2\c;
$d = new namespace2\sub1\demo_d;

執行結果:

Feng-Hsu-Pingteki-MacBook-Air:ironman6 fillano$ php 1-5f.php
a
namespace1\sub1\b
namespace1\sub2\c
namespace2/sub1/demo/d
namespace2\sub1\demo_d

OK,順利完成,就先這樣吧,好像花太多時間玩autoload了XD


上一篇
逐步提昇PHP技術能力 - PHP的語言特性 : Generators / Iterators
下一篇
逐步提昇PHP技術能力 - PHP的語言特性 : PHP內建的interface與class
系列文
逐步提昇PHP技術能力30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言