Aspects

Aspect oriented programming is the idea that some logic abstractions can be applied across various type hierarchies "after the fact", altering the behavior 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.

Wikipedia provides a much more in-depth explanation.

In the context of the SilverStripe Dependency Injector, Aspects are achieved thanks to PHP's __call magic method combined with the Proxy Design Pattern.

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.

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.

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.

mysite/code/MySQLWriteDbAspect.php

	<?php

	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;
				}
			}
		}
	}

object that's made use of above.

mysite/_config/app.yml

	WriteMySQLDatabase:
	  class: MySQLDatabase
	  constructor:
	    - type: MySQLDatabase
	      server: write.hostname.db
	      username: user
	      password: pass
	      database: write_database

of type MySQLDatabase, configured to point at the 'write_database'.

Next, this should be bound into an instance of the Aspect class

mysite/_config/app.yml

	MySQLWriteDbAspect:
	  properties:
	    writeDb: %$WriteMySQLDatabase

Next, we need to define the database connection that will be used for all non-write queries

mysite/_config/app.yml

	ReadMySQLDatabase:
	  class: MySQLDatabase
	  constructor:
	    - type: MySQLDatabase
	    server: slavecluster.hostname.db
	    username: user
	    password: pass
	    database: read_database

object when the framework creates the database connection.

mysite/_config/app.yml

	MySQLDatabase:
	  class: AopProxyService
	  properties:
	    proxied: %$ReadMySQLDatabase
	    beforeCall:
	      query: 
	        - %$MySQLWriteDbAspect
  • proxied : This is the 'read' database connection 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

mysite/_config/app.yml

	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 &$alternateReturn variable, and returns false after doing so.

	$alternateReturn = $this->writeDb->query($sql, $code);

	return false;

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.

API Documentation