mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 12:05:37 +00:00
commit
d7d7cf0280
@ -22,6 +22,8 @@ interface AfterCallAspect {
|
|||||||
* The name of the method being called
|
* The name of the method being called
|
||||||
* @param string $args
|
* @param string $args
|
||||||
* The arguments that were passed to the method call
|
* 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);
|
||||||
}
|
}
|
||||||
|
@ -13,14 +13,35 @@ class AopProxyService {
|
|||||||
public $afterCall = array();
|
public $afterCall = array();
|
||||||
|
|
||||||
public $proxied;
|
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) {
|
public function __call($method, $args) {
|
||||||
if (method_exists($this->proxied, $method)) {
|
if (method_exists($this->proxied, $method)) {
|
||||||
$continue = true;
|
$continue = true;
|
||||||
|
$result = null;
|
||||||
|
|
||||||
if (isset($this->beforeCall[$method])) {
|
if (isset($this->beforeCall[$method])) {
|
||||||
$result = $this->beforeCall[$method]->beforeCall($this->proxied, $method, $args);
|
$methods = $this->beforeCall[$method];
|
||||||
if ($result === false) {
|
if (!is_array($methods)) {
|
||||||
$continue = false;
|
$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);
|
$result = call_user_func_array(array($this->proxied, $method), $args);
|
||||||
|
|
||||||
if (isset($this->afterCall[$method])) {
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,9 @@ interface BeforeCallAspect {
|
|||||||
* The name of the method being called
|
* The name of the method being called
|
||||||
* @param string $args
|
* @param string $args
|
||||||
* The arguments that were passed to the method call
|
* 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);
|
||||||
}
|
}
|
||||||
|
189
docs/en/reference/aspects.md
Normal file
189
docs/en/reference/aspects.md
Normal 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.
|
||||||
|
|
||||||
|
|
@ -171,7 +171,7 @@ class DB {
|
|||||||
self::$connection_attempted = true;
|
self::$connection_attempted = true;
|
||||||
|
|
||||||
$dbClass = $databaseConfig['type'];
|
$dbClass = $databaseConfig['type'];
|
||||||
$conn = new $dbClass($databaseConfig);
|
$conn = Injector::inst()->create($dbClass, $databaseConfig);
|
||||||
|
|
||||||
self::setConn($conn, $label);
|
self::setConn($conn, $label);
|
||||||
|
|
||||||
|
107
tests/injector/AopProxyTest.php
Normal file
107
tests/injector/AopProxyTest.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user