11
|
drwxr-xr-x 5 csaba csaba 4096 Feb 2 12:16 go-aop-php
|
整合到我们的项目
我们需要创建一个调用,介于路由/应用程序的入口点。自动装弹机的然后自动包括类。开始吧!引用作为一个切面内核。
01
|
use Go\Core\AspectKernel;
|
02
|
use Go\Core\AspectContainer;
|
04
|
class ApplicationAspectKernel extends AspectKernel {
|
06
|
protected function configureAop(AspectContainer $container) {
|
10
|
protected function getApplicationLoaderPath() {
|
现在,AOP是一种在通用编程语言中相当成熟的技术。
例如,我创建了一个目录,调用应用程序,然后添加一个类文件: ApplicationAspectKernel.php 。
我们开始切面扩展!AcpectKernel 类提供了基础的方法用于完切面内核的工作。有两个方法,我们必须知道:configureAop()用于注册页面特征,和 getApplicationLoaderPath() 返回自动加载程序的全路径。
现在,一个简单的建立一个空的 autoload.php 文件在你的程序目录。和改变 getApplicationLoaderPath() 方法。如下:
02
|
class ApplicationAspectKernel extends AspectKernel {
|
06
|
protected function getApplicationLoaderPath() {
|
07
|
return __DIR__ . DIRECTORY_SEPARATOR . 'autoload.php';
|
别担心 autoload.php 就是这样。我们将会填写被省略的片段。
当我们第一次安装 Go语言!和达到这一点我的过程中,我觉得需要运行一些代码。所以开始构建一个小应用程序。
创建一个简单的日志记录器
我们的「方面」为一个简单的日志记录器,但在继续我们应用的主要部分之前,有些代码需要看一下。
创建一个最小的应用
我们的小应用是一个电子经纪人,能够购买和出售股票。
06
|
function __construct($name, $id) {
|
11
|
function buy($symbol, $volume, $price) {
|
12
|
return $volume * $price;
|
15
|
function sell($symbol, $volume, $price) {
|
16
|
return $volume * $price;
|
这些代码非常简单,Broker 类拥有两个私有字段,储存经纪人的名称和 ID。
这个类同时提供了两个方法,buy() 和 sell(),分别用于收购和出售股票。每个方法接受三个参数:股票标识、股票数量、每股价格。sell() 方法出售股票,并计算总收益。相应的,buy()方法购买股票并计算总支出。
考验我们的经纪人
通过PHPUnit 测试程序,我们可以很容易的考验我们经纪人。在应用目录内创建一个子目录,名为 Test,并在其中添加 BrokerTest.php 文件。并添加下面的代码:
01
|
require_once '../Broker.php';
|
03
|
class BrokerTest extends PHPUnit_Framework_TestCase {
|
05
|
function testBrokerCanBuyShares() {
|
06
|
$broker = new Broker('John', '1');
|
07
|
$this->assertEquals(500, $broker->buy('GOOGL', 100, 5));
|
10
|
function testBrokerCanSellShares() {
|
11
|
$broker = new Broker('John', '1');
|
12
|
$this->assertEquals(500, $broker->sell('YAHOO', 50, 10));
|
这个检验程序检查经纪人方法的返回值。我们可以运行这个检查程序检验我们的代码,至少是不是语法正确。
添加一个自动加载器
让我们创建一个自动加载器,在应用需要的时候加载类。这是一个简单的加载器,基于PSR-0 autoloader.
01
|
ini_set('display_errors', true);
|
03
|
spl_autoload_register(function($originalClassName) {
|
04
|
$className = ltrim($originalClassName, '\\');
|
07
|
if ($lastNsPos = strripos($className, '\\')) {
|
08
|
$namespace = substr($className, 0, $lastNsPos);
|
09
|
$className = substr($className, $lastNsPos + 1);
|
10
|
$fileName = str_replace('\\', DIRECTORY_SEPARATOR, $namespace) . DIRECTORY_SEPARATOR;
|
12
|
$fileName .= str_replace('_', DIRECTORY_SEPARATOR, $className) . '.php';
|
14
|
$resolvedFileName = stream_resolve_include_path($fileName);
|
15
|
if ($resolvedFileName) {
|
16
|
require_once $resolvedFileName;
|
18
|
return (bool) $resolvedFileName;
|
这就是我们 autoload.php 文件中的全部内容。现在,变更 BrokerTest.php, 改引用Broker.php 为引用自动加载器 。
1
|
require_once '../autoload.php';
|
3
|
class BrokerTest extends PHPUnit_Framework_TestCase {
|
运行 BrokerTest,验证代码运行情况。
连接到应用方面核心
我们最后的一件事是配置Go!.为此,我们需要连接所有的组件让们能和谐工作。首先,创建一个php文件AspectKernelLoader.php,其代码如下:
01
|
include __DIR__ . '/../vendor/lisachenko/go-aop-php/src/Go/Core/AspectKernel.php';
|
02
|
include 'ApplicationAspectKernel.php';
|
04
|
ApplicationAspectKernel::getInstance()->init(array(
|
06
|
'Go' => realpath(__DIR__ . '/../vendor/lisachenko/go-aop-php/src/'),
|
07
|
'TokenReflection' => realpath(__DIR__ . '/../vendor/andrewsville/php-token-reflection/'),
|
08
|
'Doctrine\\Common' => realpath(__DIR__ . '/../vendor/doctrine/common/lib/')
|
10
|
'appDir' => __DIR__ . '/../Application',
|
12
|
'includePaths' => array(),
|
我们需要连接所有的组件让们能和谐工作!
这个文件位于前端控制器和自动加载器之间。他使用AOP框架初始化并在需要时调用autoload.php
第一行,我明确地载入AspectKernel.php和ApplicationAspectKernel.php,因为,要记住,在这个点我们还没有自动加载器。
接下来的代码段,我们调用ApplicationAspectKernel对象init()方法,并且给他传递了一个数列参数:
-
autoload 定义了初始化AOP类库的路径。根据你实际的目录机构调整为相应的值。
-
appDir 引用了应用的目录
-
cacheDir 指出了缓存目录(本例中中我们忽略缓存)。
-
includePaths 对aspects的一个过滤器。我想看到所有特定的目录,所以设置了一个空数组,以便看到所有的值。
-
debug 提供了额外的调试信息,这对开发非常有用,但是对已经要部属的应用设置为false。
为了最后实现各个不同部分的连接,找出你工程中autoload.php自动加载所有的引用并且用AspectKernelLoader.php替换他们。在我们简单的例子中,仅仅test文件需要修改:
1
|
require_once '../AspectKernelLoader.php';
|
3
|
class BrokerTest extends PHPUnit_Framework_TestCase {
|
对大一点的工程,你会发现使用bootstrap.php作为单元测试但是非常有用;用require_once()做为autoload.php,或者我们的AspectKernelLoader.php应该在那载入。
记录Broker的方法
创建BrokerAspect.php文件,代码如下:
02
|
use Go\Aop\Intercept\FieldAccess;
|
03
|
use Go\Aop\Intercept\MethodInvocation;
|
04
|
use Go\Lang\Annotation\After;
|
05
|
use Go\Lang\Annotation\Before;
|
06
|
use Go\Lang\Annotation\Around;
|
07
|
use Go\Lang\Annotation\Pointcut;
|
08
|
use Go\Lang\Annotation\DeclareParents;
|
10
|
class BrokerAspect implements Aspect {
|
13
|
* @param MethodInvocation $invocation Invocation
|
14
|
* @Before("execution(public Broker->*(*))") // This is our PointCut
|
16
|
public function beforeMethodExecution(MethodInvocation $invocation) {
|
17
|
echo "Entering method " . $invocation->getMethod()->getName() . "()\n";
|
我们在程序开始指定一些有对AOP框架有用的语句。接着,我们创建了自己的方面类叫BrokerAspect,用它实现Aspect。接着,我们指定了我们aspect的匹配逻辑。
1
|
* @Before("execution(public Broker->*(*))")
|
-
@Before 给出合适应用建议. 可能的参数有@Before,@After,@Around和@After线程.
-
"execution(public Broker->*(*))" 给执行一个类所有的公共方法指出了匹配规则,可以用任意数量的参数调用Broker,语法是:
1
|
[operation - execution/access]([method/attribute type - public/protected] [class]->[method/attribute]([params])
|
请注意匹配机制不可否认有点笨拙。你在规则的每一部分仅可以使用一个星号‘*‘。例如public Broker->匹配一个叫做Broker的类;public Bro*->匹配以Bro开头的任何类;public *ker->匹配任何ker结尾的类。
public *rok*->将匹配不到任何东西;你不能在同一个匹配中使用超过一个的星号。
紧接着匹配程序的函数会在有时间发生时调用。在本例中的方法将会在每一个Broker公共方法调用之前执行。其参数$invocation(类型为MethodInvocation)子自动传递到我们的方法的。这个对象提供了多种方式获取调用方法的信息。在第一个例子中,我们使用他获取了方法的名字,并且输出。
获得返回值并操纵运行
目前为止,我们学习了在一个方法执行的之前和之后,怎样运行额外的代码。当这个漂亮的实现后,如果我们无法看到方法返回了什么的话,它还不是非常有用。我们给aspect增加另一个方法,修改现有的代码:
02
|
class BrokerAspect implements Aspect {
|
05
|
* @param MethodInvocation $invocation Invocation
|
06
|
* @Before("execution(public Broker->*(*))")
|
08
|
public function beforeMethodExecution(MethodInvocation $invocation) {
|
09
|
echo "Entering method " . $invocation->getMethod()->getName() . "()\n";
|
10
|
echo "with parameters: " . implode(', ', $invocation->getArguments()) . ".\n";
|
14
|
* @param MethodInvocation $invocation Invocation
|
15
|
* @After("execution(public Broker->*(*))")
|
17
|
public function afterMethodExecution(MethodInvocation $invocation) {
|
18
|
echo "Finished executing method " . $invocation->getMethod()->getName() . "()\n\n";
|
22
|
* @param MethodInvocation $invocation Invocation
|
23
|
* @Around("execution(public Broker->*(*))")
|
25
|
public function aroundMethodExecution(MethodInvocation $invocation) {
|
26
|
$returned = $invocation->proceed();
|
27
|
echo "method returned: " . $returned . "\n";
|
仅仅定义一个aspect是不够的;我们需要将它注册到AOP基础设施。
这个新的代码把参数信息移动到@Before方法。我们也增加了另一个特殊的@Around匹配器方法。这很整洁,因为原始的匹配方法调用被包裹于aroundMethodExecution()函数之内,有效的限制了原始的调用。在advise里,我们要调用$invocation->proceed(),以便执行原始的调用。如果你不这么做,原始的调用将不会发生。
|
顶 翻译的不错哦!
|
这种包装也允许我们操作返回值。advise返回的就是原始调用返回的。在我们的案例中,我们没有修改任何东西,输出应该看起来像这样:
01
|
PHPUnit 3.6.11 by Sebastian Bergmann.
|
03
|
.Entering method __construct()
|
04
|
with parameters: John, 1.
|
06
|
Finished executing method __construct()
|
09
|
with parameters: GOOGL, 100, 5.
|
11
|
Finished executing method buy()
|
13
|
.Entering method __construct()
|
14
|
with parameters: John, 1.
|
16
|
Finished executing method __construct()
|
18
|
Entering method sell()
|
19
|
with parameters: YAHOO, 50, 10.
|
21
|
Finished executing method sell()
|
23
|
Time: 0 seconds, Memory: 5.75Mb
|
25
|
OK (2 tests, 2 assertions)
|
我们增加一点变化,赋以一个具体的broker一个discount。返回到测试类,写如下的测试:
01
|
require_once '../AspectKernelLoader.php';
|
03
|
class BrokerTest extends PHPUnit_Framework_TestCase {
|
07
|
function testBrokerWithId2WillHaveADiscountOnBuyingShares() {
|
08
|
$broker = new Broker('Finch', '2');
|
09
|
$this->assertEquals(80, $broker->buy('MS', 10, 10));
|
这会失败:
01
|
Time: 0 seconds, Memory: 6.00Mb
|
05
|
1) BrokerTest::testBrokerWithId2WillHaveADiscountOnBuyingShares
|
06
|
Failed asserting that 100 matches expected 80.
|
08
|
/home/csaba/Personal/Programming/NetTuts/Aspect Oriented Programming inPHP/Source/Application/Test/BrokerTest.php:19
|
12
|
Tests: 3, Assertions: 3, Failures: 1.
|
下一步,我们需要修改broker以便提供它的ID。只要像下面所示实现agetId()方法:
06
|
function __construct($name, $id) {
|
现在,修改aspect以调整具有ID值为2的broker的购买价格。
02
|
class BrokerAspect implements Aspect {
|
07
|
* @param MethodInvocation $invocation Invocation
|
08
|
* @Around("execution(public Broker->buy(*))")
|
10
|
public function aroundMethodExecution(MethodInvocation $invocation) {
|
11
|
$returned = $invocation->proceed();
|
12
|
$broker = $invocation->getThis();
|
14
|
if ($broker->getId() == 2) return $returned * 0.80;
|
无需增加新的方法,只要修改aroundMethodExecution()函数。现在它正好匹配方法,称作‘buy‘,并触发了$invocation->getThis()。这有效的返回了原始的Broker对象,以便我们可以执行它的代码。于是我们做到了!我们向broker要它的ID,如果ID等于2的话就提供一个折扣。测试现在通过了。
01
|
PHPUnit 3.6.11 by Sebastian Bergmann.
|
03
|
.Entering method __construct()
|
04
|
with parameters: John, 1.
|
05
|
Finished executing method __construct()
|
08
|
with parameters: GOOGL, 100, 5.
|
09
|
Entering method getId()
|
11
|
Finished executing method getId()
|
13
|
Finished executing method buy()
|
15
|
.Entering method __construct()
|
16
|
with parameters: John, 1.
|
17
|
Finished executing method __construct()
|
19
|
Entering method sell()
|
20
|
with parameters: YAHOO, 50, 10.
|
21
|
Finished executing method sell()
|
23
|
.Entering method __construct()
|
24
|
with parameters: Finch, 2.
|
25
|
Finished executing method __construct()
|
28
|
with parameters: MS, 10, 10.
|
29
|
Entering method getId()
|
31
|
Finished executing method getId()
|
33
|
Finished executing method buy()
|
35
|
Time: 0 seconds, Memory: 5.75Mb
|
37
|
OK (3 tests, 3 assertions)
|
|
最后的思考
这就是为什么我建议你小心使用“方面”。
面向方面编程就像给怪人们的新玩意儿;您可以立即看到其巨大的潜力。方面允许我们在我们的系统的不同部分引入额外的代码,而无需修改原始代码。当你需要实现一些通过紧耦合引用和方法调用会污染你的方法和类的模块时,这会非常有用。
然而,这种灵活性,是有代价的:阴暗朦胧。有没有办法告诉如果一方面表的方法只是在寻找方法或类。例如,在我们的Broker类中执行方法时没有迹象表明发生任何事情。这就是为什么我建议你小心使用“方面”的原因。
我们使用“方面”来给一个特定的经纪人提供折扣是误用的一个例子。不要在一个真实的项目中这样做。经纪人的折扣与经纪人相关;所以,在Broker类中保持这个逻辑。“方面”应该只执行不直接关系到对象主要行为的任务。
乐在其中吧!