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.
In the context of the Silverstripe CMS 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 CMS so is not a complete solution, more just a guide to how it would be used.
// app/src/Aspect/MySQLWriteDbAspect.php
namespace App\Aspect;
use SilverStripe\Core\Injector\BeforeCallAspect;
use SilverStripe\ORM\Connect\MySQLDatabase;
class MySQLWriteDbAspect implements BeforeCallAspect
{
private MySQLDatabase $writeDb;
private array $writeQueries = [
'insert',
'update',
'delete',
'replace',
];
public function setWriteDB(MySQLDatabase $db): static
{
$this->writeDb = $db;
return $this;
}
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.
# app/_config/aspects.yml
SilverStripe\Core\Injector\Injector:
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
# app/_config/aspects.yml
SilverStripe\Core\Injector\Injector:
App\Aspect\MySQLWriteDbAspect:
properties:
writeDb: '%$WriteMySQLDatabase'
Next, we need to define the database connection that will be used for all non-write queries
# app/_config/aspects.yml
SilverStripe\Core\Injector\Injector:
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.
# app/_config/aspects.yml
SilverStripe\Core\Injector\Injector:
MySQLDatabase:
class: AopProxyService
properties:
proxied: '%$ReadMySQLDatabase'
beforeCall:
query:
- '%$App\Aspect\MySQLWriteDbAspect'
The two important parts here are in the properties
declared for the object.
- 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
# app/_config/aspects.yml
SilverStripe\Core\Injector\Injector:
ReadMySQLDatabase:
class: MySQLDatabase
constructor:
- type: MySQLDatabase
server: slavecluster.hostname.db
username: user
password: pass
database: read_database
App\Aspect\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:
- '%$App\Aspect\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.
namespace App\Aspect;
use SilverStripe\Core\Injector\BeforeCallAspect;
use SilverStripe\ORM\Connect\MySQLDatabase;
class MySQLWriteDbAspect implements BeforeCallAspect
{
private MySQLDatabase $writeDb;
// ...
public function beforeCall($proxied, $method, $args, &$alternateReturn)
{
// ...
$alternateReturn = $this->writeDb->query($sql, $code);
return false;
// ...
}
}
By returning false
from the beforeCall()
method, the wrapping proxy class willnot 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.