面向方面編程(AOP)對(duì)于PHP來(lái)說(shuō)是一個(gè)新的概念。現(xiàn)在PHP對(duì)于 AOP 并沒(méi)有官方支持,但有很多擴(kuò)展和庫(kù)實(shí)現(xiàn)了這個(gè)特性。本課中,我們將使用 Go! PHP library 來(lái)學(xué)習(xí) PHP 如何進(jìn)行 AOP 開(kāi)發(fā),或者在需要的時(shí)候,可以回來(lái)看一眼。
AOP簡(jiǎn)史Aspect-Oriented programming is like a new gadget for geeks.
面向方面編程的思想在二十世紀(jì)90年代中期,于施樂(lè)帕洛阿爾托研究中心(PARC)成型。同很多有趣的新技術(shù)一樣,由于缺少明確的定義,起初 AOP 備受爭(zhēng)議。因此相關(guān)小組決定將未完成的想法公之于眾,以便接受廣大社區(qū)的反饋。關(guān)鍵問(wèn)題在于“關(guān)注點(diǎn)分離(Separation of Concerns)”的概念。AOP 是一種可以分離關(guān)注的可行系方案。
AOP 于90年代末趨于成熟,標(biāo)識(shí)為施樂(lè) AspectJ 的發(fā)布,IBM 緊隨其后,于2001年發(fā)布了 Hyper/J。現(xiàn)在,AOP是一種對(duì)于常用編程語(yǔ)言來(lái)說(shuō)都是一種成熟的技術(shù)。
基本詞匯AOP 的核心就是“方面”,但在我們定義「方面『aspect』」之前,我們需要先討論兩個(gè)術(shù)語(yǔ);「切點(diǎn) 『 point-cut』 」和「通知『advise』」。切點(diǎn)代表我們代碼中的一個(gè)時(shí)間點(diǎn),特指運(yùn)行我們代碼的某個(gè)時(shí)間。在切點(diǎn)運(yùn)行代碼被稱(chēng)為通知,結(jié)合一個(gè)活多個(gè)切點(diǎn)及通知的即為方面。
通常,每個(gè)類(lèi)都會(huì)有一個(gè)核心的行為或關(guān)注點(diǎn),但有時(shí),類(lèi)可能存在次要的行為。例如,類(lèi)可能會(huì)調(diào)用一個(gè)日志記錄器或是通知一個(gè)觀(guān)察員。因?yàn)轭?lèi)中的這些功能是次要的,其行為通常都是相同的。這種行為被稱(chēng)為“交叉關(guān)注點(diǎn)”;使用 AOP 可以避免。
PHP的各種AOP工具Chris Peters 已經(jīng)討論過(guò)在PHP中實(shí)現(xiàn) AOP 的Flow 框架。 Lithium 框架也提供了對(duì)AOP的實(shí)現(xiàn)。
另一個(gè)框架采用了不同的方法,創(chuàng)建了一個(gè) C/C++ 編寫(xiě)的PHP擴(kuò)展,在PHP解釋器的層級(jí)上宣示著它的魔力。名為AOP PHP Extension,我會(huì)在后續(xù)文章中討論它。
但正如我之前所言,本文將檢閱Go! AOP-PHP 庫(kù)。
安裝并配置 Go!Go! 庫(kù)并未擴(kuò)展;它完全由PHP編寫(xiě),并為PHP5.4或更高版本使用。作為一個(gè)純PHP庫(kù),它部署簡(jiǎn)易,即使是在不允許編譯安裝你自己的PHP擴(kuò)展的受限及共享主機(jī)環(huán)境,也可以輕易安裝。
使用 Composer 安裝 Go!Composer 是安裝 PHP 包的首選方法。如果你沒(méi)有使用過(guò) Composer,你可以在Go! GitHub repository下載。
首先,將下面幾行加入你的 composer.json 文件。
{      "require": {          "lisachenko/go-aop-php": "*"      }  } 之后,使用 Composer 安裝 go-aop-php。在終端中運(yùn)行下面命令:
$ cd /your/project/folder $ php composer.phar update lisachenko/go-aop-php
Composer 將會(huì)在之后數(shù)秒中內(nèi)安裝引用的包以及需求。如果成功,你將看到類(lèi)似下面的輸出:
Loading composer repositories with package informationUpdating dependencies- Installing doctrine/common (2.3.0)Downloading: 100%- Installing andrewsville/php-token-reflection (1.3.1)Downloading: 100%- Installing lisachenko/go-aop-php (0.1.1)Downloading: 100%Writing lock fileGenerating autoload files整合到我們的項(xiàng)目
我們需要?jiǎng)?chuàng)建一個(gè)調(diào)用,介于路由/html' target='_blank'>應(yīng)用程序的入口點(diǎn)。自動(dòng)裝彈機(jī)的然后自動(dòng)包括類(lèi)。開(kāi)始吧!引用作為一個(gè)切面內(nèi)核。
use Go/Core/AspectKernel;use Go/Core/AspectContainer;class ApplicationAspectKernel extends AspectKernel {protected function configureAop(AspectContainer $container) {}protected function getApplicationLoaderPath() {}}
現(xiàn)在,AOP是一種在通用編程語(yǔ)言中相當(dāng)成熟的技術(shù)。
例如,我創(chuàng)建了一個(gè)目錄,調(diào)用應(yīng)用程序,然后添加一個(gè)類(lèi)文件: ApplicationAspectKernel.php 。
我們開(kāi)始切面擴(kuò)展!AcpectKernel 類(lèi)提供了基礎(chǔ)的方法用于完切面內(nèi)核的工作。有兩個(gè)方法,我們必須知道:configureAop()用于注冊(cè)頁(yè)面特征,和 getApplicationLoaderPath() 返回自動(dòng)加載程序的全路徑。
現(xiàn)在,一個(gè)簡(jiǎn)單的建立一個(gè)空的 autoload.php 文件在你的程序目錄。和改變 getApplicationLoaderPath() 方法。如下:
// [...]class ApplicationAspectKernel extends AspectKernel {// [...]protected function getApplicationLoaderPath() {return __DIR__ . DIRECTORY_SEPARATOR . 'autoload.php';}}別擔(dān)心 autoload.php 就是這樣。我們將會(huì)填寫(xiě)被省略的片段。
當(dāng)我們第一次安裝 Go語(yǔ)言!和達(dá)到這一點(diǎn)我的過(guò)程中,我覺(jué)得需要運(yùn)行一些代碼。所以開(kāi)始構(gòu)建一個(gè)小應(yīng)用程序。
創(chuàng)建一個(gè)簡(jiǎn)單的日志記錄器我們的「方面」為一個(gè)簡(jiǎn)單的日志記錄器,但在繼續(xù)我們應(yīng)用的主要部分之前,有些代碼需要看一下。
創(chuàng)建一個(gè)最小的應(yīng)用我們的小應(yīng)用是一個(gè)電子經(jīng)紀(jì)人,能夠購(gòu)買(mǎi)和出售股票。
class Broker {private $name;private $id;function __construct($name, $id) {$this->name = $name;$this->id = $id;}function buy($symbol, $volume, $price) {return $volume * $price;}function sell($symbol, $volume, $price) {return $volume * $price;}}這些代碼非常簡(jiǎn)單,Broker 類(lèi)擁有兩個(gè)私有字段,儲(chǔ)存經(jīng)紀(jì)人的名稱(chēng)和 ID。
這個(gè)類(lèi)同時(shí)提供了兩個(gè)方法,buy() 和 sell(),分別用于收購(gòu)和出售股票。每個(gè)方法接受三個(gè)參數(shù):股票標(biāo)識(shí)、股票數(shù)量、每股價(jià)格。sell() 方法出售股票,并計(jì)算總收益。相應(yīng)的,buy()方法購(gòu)買(mǎi)股票并計(jì)算總支出。
考驗(yàn)我們的經(jīng)紀(jì)人通過(guò)PHPUnit 測(cè)試程序,我們可以很容易的考驗(yàn)我們經(jīng)紀(jì)人。在應(yīng)用目錄內(nèi)創(chuàng)建一個(gè)子目錄,名為 Test,并在其中添加 BrokerTest.php 文件。并添加下面的代碼:
	require_once '../Broker.php';		class BrokerTest extends PHPUnit_Framework_TestCase {		function testBrokerCanBuyShares() {	$broker = new Broker('John', '1');	$this->assertEquals(500, $broker->buy('GOOGL', 100, 5));	}		function testBrokerCanSellShares() {	$broker = new Broker('John', '1');	$this->assertEquals(500, $broker->sell('YAHOO', 50, 10));	}		}這個(gè)檢驗(yàn)程序檢查經(jīng)紀(jì)人方法的返回值。我們可以運(yùn)行這個(gè)檢查程序檢驗(yàn)我們的代碼,至少是不是語(yǔ)法正確。
添加一個(gè)自動(dòng)加載器讓我們創(chuàng)建一個(gè)自動(dòng)加載器,在應(yīng)用需要的時(shí)候加載類(lèi)。這是一個(gè)簡(jiǎn)單的加載器,基于PSR-0 autoloader.
	ini_set('display_errors', true);		spl_autoload_register(function($originalClassName) {	$className = ltrim($originalClassName, '//');	$fileName = '';	$namespace = '';	if ($lastNsPos = strripos($className, '//')) {	$namespace = substr($className, 0, $lastNsPos);	$className = substr($className, $lastNsPos + 1);	$fileName = str_replace('//', DIRECTORY_SEPARATOR, $namespace) . DIRECTORY_SEPARATOR;	}	$fileName .= str_replace('_', DIRECTORY_SEPARATOR, $className) . '.php';		$resolvedFileName = stream_resolve_include_path($fileName);	if ($resolvedFileName) {	require_once $resolvedFileName;	}	return (bool) $resolvedFileName;	});這就是我們 autoload.php 文件中的全部?jī)?nèi)容。現(xiàn)在,變更 BrokerTest.php, 改引用Broker.php 為引用自動(dòng)加載器 。
	require_once '../autoload.php';		class BrokerTest extends PHPUnit_Framework_TestCase {	// [...]	}運(yùn)行 BrokerTest,驗(yàn)證代碼運(yùn)行情況。
連接到應(yīng)用方面核心我們最后的一件事是配置Go!.為此,我們需要連接所有的組件讓們能和諧工作。首先,創(chuàng)建一個(gè)php文件AspectKernelLoader.php,其代碼如下:
include __DIR__ . '/../vendor/lisachenko/go-aop-php/src/Go/Core/AspectKernel.php'; include 'ApplicationAspectKernel.php'; ApplicationAspectKernel::getInstance()->init(array( 'autoload' => array( 'Go' => realpath(__DIR__ . '/../vendor/lisachenko/go-aop-php/src/'), 'TokenReflection' => realpath(__DIR__ . '/../vendor/andrewsville/php-token-reflection/'), 'Doctrine//Common' => realpath(__DIR__ . '/../vendor/doctrine/common/lib/') ), 'appDir' => __DIR__ . '/../Application', 'cacheDir' => null, 'includePaths' => array(), 'debug' => true ));
我們需要連接所有的組件讓們能和諧工作!
這個(gè)文件位于前端控制器和自動(dòng)加載器之間。他使用AOP框架初始化并在需要時(shí)調(diào)用autoload.php
第一行,我明確地載入AspectKernel.php和ApplicationAspectKernel.php,因?yàn)椋涀。谶@個(gè)點(diǎn)我們還沒(méi)有自動(dòng)加載器。
接下來(lái)的代碼段,我們調(diào)用ApplicationAspectKernel對(duì)象init()方法,并且給他傳遞了一個(gè)數(shù)列參數(shù):
autoload 定義了初始化AOP類(lèi)庫(kù)的路徑。根據(jù)你實(shí)際的目錄機(jī)構(gòu)調(diào)整為相應(yīng)的值。appDir 引用了應(yīng)用的目錄cacheDir 指出了緩存目錄(本例中中我們忽略緩存)。includePaths 對(duì)aspects的一個(gè)過(guò)濾器。我想看到所有特定的目錄,所以設(shè)置了一個(gè)空數(shù)組,以便看到所有的值。debug 提供了額外的調(diào)試信息,這對(duì)開(kāi)發(fā)非常有用,但是對(duì)已經(jīng)要部屬的應(yīng)用設(shè)置為false。為了最后實(shí)現(xiàn)各個(gè)不同部分的連接,找出你工程中autoload.php自動(dòng)加載所有的引用并且用AspectKernelLoader.php替換他們。在我們簡(jiǎn)單的例子中,僅僅test文件需要修改:
	require_once '../AspectKernelLoader.php';		class BrokerTest extends PHPUnit_Framework_TestCase {		// [...]	}對(duì)大一點(diǎn)的工程,你會(huì)發(fā)現(xiàn)使用bootstrap.php作為單元測(cè)試但是非常有用;用require_once()做為autoload.php,或者我們的AspectKernelLoader.php應(yīng)該在那載入。
記錄Broker的方法創(chuàng)建BrokerAspect.php文件,代碼如下:
	use Go/Aop/Aspect;	use Go/Aop/Intercept/FieldAccess;	use Go/Aop/Intercept/MethodInvocation;	use Go/Lang/Annotation/After;	use Go/Lang/Annotation/Before;	use Go/Lang/Annotation/Around;	use Go/Lang/Annotation/Pointcut;	use Go/Lang/Annotation/DeclareParents;		class BrokerAspect implements Aspect {		/**	* @param MethodInvocation $invocation Invocation	* @Before("execution(public Broker->*(*))") // This is our PointCut	*/	public function beforeMethodExecution(MethodInvocation $invocation) {	echo "Entering method " . $invocation->getMethod()->getName() . "()/n";	}	}我們?cè)诔绦蜷_(kāi)始指定一些有對(duì)AOP框架有用的語(yǔ)句。接著,我們創(chuàng)建了自己的方面類(lèi)叫BrokerAspect,用它實(shí)現(xiàn)Aspect。接著,我們指定了我們aspect的匹配邏輯。
請(qǐng)注意匹配機(jī)制不可否認(rèn)有點(diǎn)笨拙。你在規(guī)則的每一部分僅可以使用一個(gè)星號(hào)‘*‘。例如public Broker->匹配一個(gè)叫做Broker的類(lèi);public Bro*->匹配以Bro開(kāi)頭的任何類(lèi);public *ker->匹配任何ker結(jié)尾的類(lèi)。
public *rok*->將匹配不到任何東西;你不能在同一個(gè)匹配中使用超過(guò)一個(gè)的星號(hào)。
緊接著匹配程序的函數(shù)會(huì)在有時(shí)間發(fā)生時(shí)調(diào)用。在本例中的方法將會(huì)在每一個(gè)Broker公共方法調(diào)用之前執(zhí)行。其參數(shù)$invocation(類(lèi)型為MethodInvocation)子自動(dòng)傳遞到我們的方法的。這個(gè)對(duì)象提供了多種方式獲取調(diào)用方法的信息。在第一個(gè)例子中,我們使用他獲取了方法的名字,并且輸出。
注冊(cè)切面僅僅定義一個(gè)切面是不夠的;我們需要把它注冊(cè)到AOP架構(gòu)里。否則,它不會(huì)生效。編輯ApplicationAspectKernel.php同時(shí)在容器上的configureAop()方法里調(diào)用registerAspect():
	use Go/Core/AspectKernel;	use Go/Core/AspectContainer;		class ApplicationAspectKernel extends AspectKernel	{		protected function getApplicationLoaderPath()	{	return __DIR__ . DIRECTORY_SEPARATOR . 'autoload.php';	}		protected function configureAop(AspectContainer $container)	{	$container->registerAspect(new BrokerAspect());	}	}運(yùn)行測(cè)試和檢查輸出。你會(huì)看到類(lèi)似下面的東西:
PHPUnit 3.6.11 by Sebastian Bergmann. .Entering method __construct() Entering method buy() .Entering method __construct() Entering method sell() Time: 0 seconds, Memory: 5.50Mb OK (2 tests, 2 assertions)
就這樣我們已設(shè)法讓代碼無(wú)論什么時(shí)候發(fā)生在broker上時(shí)都會(huì)執(zhí)行。
查找參數(shù)和匹配@After讓我們加入另外的方法到BrokerAspect。
	// [...]	class BrokerAspect implements Aspect {		// [...]		/**	* @param MethodInvocation $invocation Invocation	* @After("execution(public Broker->*(*))")	*/	public function afterMethodExecution(MethodInvocation $invocation) {	echo "Finished executing method " . $invocation->getMethod()->getName() . "()/n";	echo "with parameters: " . implode(', ', $invocation->getArguments()) . "./n/n";	}	}這個(gè)方法在一個(gè)公共方法執(zhí)行后運(yùn)行(注意@After匹配器)。染污我們加入另外一行來(lái)輸出用來(lái)調(diào)用方法的參數(shù)。我們的測(cè)試現(xiàn)在輸出:
PHPUnit 3.6.11 by Sebastian Bergmann. .Entering method __construct() Finished executing method __construct() with parameters: John, 1. Entering method buy() Finished executing method buy() with parameters: GOOGL, 100, 5. .Entering method __construct() Finished executing method __construct() with parameters: John, 1. Entering method sell() Finished executing method sell() with parameters: YAHOO, 50, 10. Time: 0 seconds, Memory: 5.50Mb OK (2 tests, 2 assertions)獲得返回值并操縱運(yùn)行
目前為止,我們學(xué)習(xí)了在一個(gè)方法執(zhí)行的之前和之后,怎樣運(yùn)行額外的代碼。當(dāng)這個(gè)漂亮的實(shí)現(xiàn)后,如果我們無(wú)法看到方法返回了什么的話(huà),它還不是非常有用。我們給aspect增加另一個(gè)方法,修改現(xiàn)有的代碼:
	//[...]	class BrokerAspect implements Aspect {		/**	* @param MethodInvocation $invocation Invocation	* @Before("execution(public Broker->*(*))")	*/	public function beforeMethodExecution(MethodInvocation $invocation) {	echo "Entering method " . $invocation->getMethod()->getName() . "()/n";	echo "with parameters: " . implode(', ', $invocation->getArguments()) . "./n";	}		/**	* @param MethodInvocation $invocation Invocation	* @After("execution(public Broker->*(*))")	*/	public function afterMethodExecution(MethodInvocation $invocation) {	echo "Finished executing method " . $invocation->getMethod()->getName() . "()/n/n";	}		/**	* @param MethodInvocation $invocation Invocation	* @Around("execution(public Broker->*(*))")	*/	public function aroundMethodExecution(MethodInvocation $invocation) {	$returned = $invocation->proceed();	echo "method returned: " . $returned . "/n";		return $returned;	}		}僅僅定義一個(gè)aspect是不夠的;我們需要將它注冊(cè)到AOP基礎(chǔ)設(shè)施。
這個(gè)新的代碼把參數(shù)信息移動(dòng)到@Before方法。我們也增加了另一個(gè)特殊的@Around匹配器方法。這很整潔,因?yàn)樵嫉钠ヅ浞椒ㄕ{(diào)用被包裹于aroundMethodExecution()函數(shù)之內(nèi),有效的限制了原始的調(diào)用。在advise里,我們要調(diào)用$invocation->proceed(),以便執(zhí)行原始的調(diào)用。如果你不這么做,原始的調(diào)用將不會(huì)發(fā)生。www.it165.net
這種包裝也允許我們操作返回值。advise返回的就是原始調(diào)用返回的。在我們的案例中,我們沒(méi)有修改任何東西,輸出應(yīng)該看起來(lái)像這樣:
PHPUnit 3.6.11 by Sebastian Bergmann. .Entering method __construct() with parameters: John, 1. method returned: Finished executing method __construct() Entering method buy() with parameters: GOOGL, 100, 5. method returned: 500 Finished executing method buy() .Entering method __construct() with parameters: John, 1. method returned: Finished executing method __construct() Entering method sell() with parameters: YAHOO, 50, 10. method returned: 500 Finished executing method sell() Time: 0 seconds, Memory: 5.75Mb OK (2 tests, 2 assertions)
我們?cè)黾右稽c(diǎn)變化,賦以一個(gè)具體的broker一個(gè)discount。返回到測(cè)試類(lèi),寫(xiě)如下的測(cè)試:
	require_once '../AspectKernelLoader.php';		class BrokerTest extends PHPUnit_Framework_TestCase {		// [...]		function testBrokerWithId2WillHaveADiscountOnBuyingShares() {	$broker = new Broker('Finch', '2');	$this->assertEquals(80, $broker->buy('MS', 10, 10));	}		}這會(huì)失敗:
Time: 0 seconds, Memory: 6.00Mb There was 1 failure: BrokerTest::testBrokerWithId2WillHaveADiscountOnBuyingSharesPHP編程
鄭重聲明:本文版權(quán)歸原作者所有,轉(zhuǎn)載文章僅為傳播更多信息之目的,如作者信息標(biāo)記有誤,請(qǐng)第一時(shí)間聯(lián)系我們修改或刪除,多謝。
新聞熱點(diǎn)
疑難解答
圖片精選
網(wǎng)友關(guān)注