Merge pull request #2703 from nyeholt/3.1

AspectProxy updates
This commit is contained in:
Simon Welsh 2014-08-07 17:03:37 +12:00
commit d7d7cf0280
6 changed files with 340 additions and 9 deletions

View File

@ -22,6 +22,8 @@ interface AfterCallAspect {
* The name of the method being called
* @param string $args
* The arguments that were passed to the method call
* @param mixed $result
* The result of calling the method on the real object
*/
public function afterCall($proxied, $method, $args);
public function afterCall($proxied, $method, $args, $result);
}

View File

@ -13,14 +13,35 @@ class AopProxyService {
public $afterCall = array();
public $proxied;
/**
* Because we don't know exactly how the proxied class is usually called,
* provide a default constructor
*/
public function __construct() {
}
public function __call($method, $args) {
if (method_exists($this->proxied, $method)) {
$continue = true;
$result = null;
if (isset($this->beforeCall[$method])) {
$result = $this->beforeCall[$method]->beforeCall($this->proxied, $method, $args);
if ($result === false) {
$continue = false;
$methods = $this->beforeCall[$method];
if (!is_array($methods)) {
$methods = array($methods);
}
foreach ($methods as $handler) {
$alternateReturn = null;
$proceed = $handler->beforeCall($this->proxied, $method, $args, $alternateReturn);
if ($proceed === false) {
$continue = false;
// if something is set in, use it
if ($alternateReturn) {
$result = $alternateReturn;
}
}
}
}
@ -28,11 +49,20 @@ class AopProxyService {
$result = call_user_func_array(array($this->proxied, $method), $args);
if (isset($this->afterCall[$method])) {
$this->afterCall[$method]->afterCall($this->proxied, $method, $args, $result);
$methods = $this->afterCall[$method];
if (!is_array($methods)) {
$methods = array($methods);
}
foreach ($methods as $handler) {
$return = $handler->afterCall($this->proxied, $method, $args, $result);
if (!is_null($return)) {
$result = $return;
}
}
}
return $result;
}
return $result;
}
}
}

View File

@ -20,6 +20,9 @@ interface BeforeCallAspect {
* The name of the method being called
* @param string $args
* The arguments that were passed to the method call
* @param mixed $alternateReturn
* An alternative return value that should be passed
* to the caller. Only has effect of beforeCall returns false
*/
public function beforeCall($proxied, $method, $args);
public function beforeCall($proxied, $method, $args, &$alternateReturn);
}

View File

@ -0,0 +1,189 @@
# Aspects
## Introduction
Aspect oriented programming is the idea that some logic abstractions can be
applied across various type hierarchies "after the fact", altering the
behaviour of the system without altering the code structures that are already
in place.
> In computing, aspect-oriented programming (AOP) is a programming paradigm
> which isolates secondary or supporting functions from the main program's
> business logic. It aims to increase modularity by allowing the separation of
> cross-cutting concerns, forming a basis for aspect-oriented software
> development.
[The wiki article](http://en.wikipedia.org/wiki/Aspect-oriented_programming)
provides a much more in-depth explanation!
In the context of this dependency injector, AOP is achieved thanks to PHP's
__call magic method combined with the Proxy design pattern.
## In practice
* Assume an existing service declaration exists called MyService
* An AopProxyService class instance is created, and the existing MyService object is bound in as a member variable of the AopProxyService class
* Objects are added to the AopProxyService instance's "beforeCall" and "afterCall" lists; each of these implements either the beforeCall or afterCall method
* When client code declares a dependency on MyService, it is actually passed in the AopProxyService instance
* Client code calls a method `myMethod` that it knows exists on MyService - this doesn't exist on AopProxyService, so __call is triggered.
* All classes bound to the beforeCall list are executed; if any explicitly returns 'false', `myMethod` is not executed.
* Otherwise, myMethod is executed
* All classes bound to the afterCall list are executed
## A worked example
To provide some context, imagine a situation where we want to direct all 'write' queries made in the system to a specific
database server, whereas all read queries can be handled by slave servers. A simplified implementation might look
like the following - note that this doesn't cover all cases used by SilverStripe so is not a complete solution, more
just a guide to how it would be used.
```
<?php
/**
* Redirects write queries to a specific database configuration
*
* @author <marcus@silverstripe.com.au>
* @license BSD License http://www.silverstripe.org/bsd-license
*/
class MySQLWriteDbAspect implements BeforeCallAspect {
/**
*
* @var MySQLDatabase
*/
public $writeDb;
public $writeQueries = array('insert','update','delete','replace');
public function beforeCall($proxied, $method, $args, &$alternateReturn) {
if (isset($args[0])) {
$sql = $args[0];
$code = isset($args[1]) ? $args[1] : E_USER_ERROR;
if (in_array(strtolower(substr($sql,0,strpos($sql,' '))), $this->writeQueries)) {
$alternateReturn = $this->writeDb->query($sql, $code);
return false;
}
}
}
}
```
To actually make use of this class, a few different objects need to be configured. First up, define the `writeDb`
object that's made use of above
```
WriteMySQLDatabase:
class: MySQLDatabase
constructor:
- type: MySQLDatabase
server: write.hostname.db
username: user
password: pass
database: write_database
```
This means that whenever something asks the injector for the `WriteMySQLDatabase` object, it'll receive an object of
type `MySQLDatabase`, configured to point at the 'write' database
Next, this should be bound into an instance of the aspect class
```
MySQLWriteDbAspect:
properties:
writeDb: %$WriteMySQLDatabase
```
Next, we need to define the database connection that will be used for all non-write queries
```
ReadMySQLDatabase:
class: MySQLDatabase
constructor:
- type: MySQLDatabase
server: slavecluster.hostname.db
username: user
password: pass
database: read_database
```
The final piece that ties everything together is the AopProxyService instance that will be used as the replacement
object when the framework creates the database connection in DB.php
```
MySQLDatabase:
class: AopProxyService
properties:
proxied: %$ReadMySQLDatabase
beforeCall:
query:
- %$MySQLWriteDbAspect
```
The two important parts here are in the `properties` declared for the object
- **proxied** : This is the 'read' database connectino that all queries should be initially directed through
- **beforeCall** : A hash of method\_name => array containing objects that are to be evaluated _before_ a call to the defined method\_name
Overall configuration for this would look as follows
```
Injector:
ReadMySQLDatabase:
class: MySQLDatabase
constructor:
- type: MySQLDatabase
server: slavecluster.hostname.db
username: user
password: pass
database: read_database
MySQLWriteDbAspect:
properties:
writeDb: %$WriteMySQLDatabase
WriteMySQLDatabase:
class: MySQLDatabase
constructor:
- type: MySQLDatabase
server: write.hostname.db
username: user
password: pass
database: write_database
MySQLDatabase:
class: AopProxyService
properties:
proxied: %$ReadMySQLDatabase
beforeCall:
query:
- %$MySQLWriteDbAspect
```
## Changing what a method returns
One major feature of an aspect is the ability to modify what is returned from the client's call to the proxied method.
As seen in the above example, the `beforeCall` method modifies the byref `&$alternateReturn` variable, and returns
`false` after doing so.
```
$alternateReturn = $this->writeDb->query($sql, $code);
return false;
```
By returning false from the `beforeCall()` method, the wrapping proxy class will _not_ call any additional `beforeCall`
handlers defined for the called method. Assigning the $alternateReturn variable also indicates to return that value
to the caller of the method.
Similarly the `afterCall()` aspect can be used to manipulate the value to be returned to the calling code. All the
`afterCall()` method needs to do is return a non-null value, and that value will be returned to the original calling
code instead of the actual return value of the called method.

View File

@ -171,7 +171,7 @@ class DB {
self::$connection_attempted = true;
$dbClass = $databaseConfig['type'];
$conn = new $dbClass($databaseConfig);
$conn = Injector::inst()->create($dbClass, $databaseConfig);
self::setConn($conn, $label);

View File

@ -0,0 +1,107 @@
<?php
/**
*
*
* @author <marcus@silverstripe.com.au>
* @license BSD License http://www.silverstripe.org/bsd-license
*/
class AopProxyTest extends SapphireTest {
public function testBeforeMethodsCalled() {
$proxy = new AopProxyService();
$aspect = new BeforeAfterCallTestAspect();
$proxy->beforeCall = array(
'myMethod' => $aspect
);
$proxy->proxied = new ProxyTestObject();
$result = $proxy->myMethod();
$this->assertEquals('myMethod', $aspect->called);
$this->assertEquals(42, $result);
}
public function testBeforeMethodBlocks() {
$proxy = new AopProxyService();
$aspect = new BeforeAfterCallTestAspect();
$aspect->block = true;
$proxy->beforeCall = array(
'myMethod' => $aspect
);
$proxy->proxied = new ProxyTestObject();
$result = $proxy->myMethod();
$this->assertEquals('myMethod', $aspect->called);
// the actual underlying method will NOT have been called
$this->assertNull($result);
// set up an alternative return value
$aspect->alternateReturn = 84;
$result = $proxy->myMethod();
$this->assertEquals('myMethod', $aspect->called);
// the actual underlying method will NOT have been called,
// instead the alternative return value
$this->assertEquals(84, $result);
}
public function testAfterCall() {
$proxy = new AopProxyService();
$aspect = new BeforeAfterCallTestAspect();
$proxy->afterCall = array(
'myMethod' => $aspect
);
$proxy->proxied = new ProxyTestObject();
$aspect->modifier = function ($value) {
return $value * 2;
};
$result = $proxy->myMethod();
$this->assertEquals(84, $result);
}
}
class ProxyTestObject {
public function myMethod() {
return 42;
}
}
class BeforeAfterCallTestAspect implements BeforeCallAspect, AfterCallAspect {
public $block = false;
public $called;
public $alternateReturn;
public $modifier;
public function beforeCall($proxied, $method, $args, &$alternateReturn) {
$this->called = $method;
if ($this->block) {
if ($this->alternateReturn) {
$alternateReturn = $this->alternateReturn;
}
return false;
}
}
public function afterCall($proxied, $method, $args, $result) {
if ($this->modifier) {
$modifier = $this->modifier;
return $modifier($result);
}
}
}