Aspect-Oriented Programming (AOP) is a programming paradigm which complements Object-Oriented Programming (OOP) by separating concerns of a software application to improve modularization. The separation of concerns (SoC) aims for making a software easier to maintain by grouping features and behaviour into manageable parts which all have a specific purpose and business to take care of.
OOP already allows for modularizing concerns into distinct methods, classes and packages. However, some concerns are difficult to place as they cross the boundaries of classes and even packages. One example for such a cross-cutting concern is security: Although the main purpose of a Forum package is to display and manage posts of a forum, it has to implement some kind of security to assert that only moderators can approve or delete posts. And many more packages need a similar functionality for protect the creation, deletion and update of records. AOP enables you to move the security (or any other) aspect into its own package and leave the other components with clear responsibilities, probably not implementing any security themselves.
Aspect-Oriented Programming has been around in other programming languages for quite some time now and sophisticated solutions taking advantage of AOP exist. FLOW3's AOP framework allows you to use of the most popular AOP techniques in your own PHP application. In contrast to other approaches it doesn't require any special PHP extensions, additional compile steps or modification of the target code – and it's a breeze to configure.
In case you are unsure about some terms used in this introduction or later in this chapter, it's a good idea looking them up (for example at wikipedia). Don't think that you're the only one who has never heard of a Pointcut or SoC[1] – we had a hard time learning these too. However, it's worth the hassle, as a common vocabulary improves the communication between developers a lot.
Let's stay with the example of a Forum for a
while. The classes of the forum don't implement security themselves, but
somehow we have to make sure that whenever a method
deletePost() is called, a security check takes
place. The class containing the delete method is called the
target class. We have a new
aspect called "security" which we'd like to
weave into that class. Whenever the method
deletePost() is called, a method
interceptor defined by an around
advice will intercept the target
method and only proceed if the operation is allowed in the
current security context.
At the first (and the second, third, ...) glance, the terms used in the AOP context are not really intuitive. But, similar to most of the other AOP frameworks, we better stick to them, to keep a common language between developers. Here they are:
An aspect is the part of the application which cross-cuts the core concerns of multiple objects. In FLOW3, aspects are implemented as regular classes which are tagged by the @aspect annotation. The methods of an aspect class represent advices, the properties act as an anchor for introductions.
A join point is a point in the flow of a program. Examples
are the execution of a method or the throw of an exception. In
FLOW3, join points are represented by the
F3_FLOW3_AOPJoinPoint object which contains
more information about the circumstances like name of the called
method, the passed arguments or type of the exception thrown. A
join point is an event which occurs during the program flow, not a
definition which defines that point.
An advice is the action taken by an aspect at a particular join point. Advices are implemented as methods of the aspect class. These methods are executed before and / or after the join point is reached.
The pointcut defines a set of joinpoints which need to be matched before running an advice. The pointcut is configured by a pointcut expression which defines when and where an advice should be executed. FLOW3 uses methods in an aspect class as anchors for pointcut declarations.
A poincut expression is the condition under which a joinpoint should match. It may, for example, define that joinpoints only match on the execution of a (target-) method with a certain name. Pointcut expressions are used in pointcut- and advice declarations.
A class or method being adviced by one or more aspects is referred to as a target class /-method.
An introduction redeclares the target class to implement an additional interface. By declaring an introduction it is possible to introduce new interfaces and an implementation of the required methods without touching the code of the original class.
The following terms are related to advices:
A before advice is executed before the target method is being called, but cannot prevent the target method from being executed.
An after returning advice is executed after returning from the target method. The result of the target method invocation is available to the after returning advice, but it can't change it. If the target method throws an exception, the after returning advice is not executed.
An after throwing advice is only executed if the target method throwed an exception. The after throwing advice may fetch the exception type from the join point object.
An after advice is executed after the target method has been called, no matter if an exception was thrown or not.
An around advice is wrapped around the execution of the target method. It may execute code before and after the invocation of the target method and may ultimately prevent the original method from being executed at all. An around advice is also responsible for calling other around advices at the same join point and returning either the original or a modified result for the target method.
If more than one around advice exists for a join point, they are called in an onion-like advice chain: The first around advice probably executes some before-code, then calls the second around advice which calls the target method. The target method returns a result which can be modified by the second around advice, is returned to the first around advice which finally returns the result to the initiator of the method call. Any around advice may decide to proceed or break the chain and modify results if necessary.
Aspect-Oriented Programming was, of course, not invented by us[2]. Since the initial release of the concept, dozens of implementations for various programming languages evolved. Although a few PHP-based AOP frameworks do exist, they followed concepts which did not match the goals of FLOW3 (to provide a powerful, yet developer-friendly solution) when the development of TYPO3 5.0 began. We therefore decided to create a sophisticated but pragmatic implementation which adopts the concepts of AOP but takes PHP's specialities and the requirements of typical FLOW3 applications into account. In a few cases this even lead to new features or simplifications because they were easier to implement in PHP compared to Java.
FLOW3 pragmatically implements a reduced subset of AOP, which satisfies most needs of web applications. The join point model allows for intercepting method executions but provides no special support for advising field access[3]. For the sake of simplicity and performance, pointcuts don't allow criteria which have to be evaluated at runtime (such as matching argument values of a method) and pointcut expressions are based on well-known regular expressions instead of requiring the knowledge of a dedicated expression language. Pointcut filters and join point types are modularized and can be extended if more advanced requirements should arise in the future.
FLOW3's AOP framework does not require a pre-processor or an aspect-aware PHP interpreter to weave in advices. It is implemented and based on pure PHP and doesn't need any specific PHP extension. However, it does require the Component Manager to fulfill its task.
FLOW3 uses PHP's reflection capabilities to analyze declarations of aspects, pointcuts and advices and implements method interceptors as a dynamic proxy. In accordance to the GoF patterns[4], the proxy classes act as a placeholders for the target object. They are true subclasses of the original and override adviced methods by implementing a interceptor method. The proxy classes are generated automatically by the AOP framework and cached in their own files. If a class has been adviced by some aspect, the Component Manager will only deliver instances of the proxy class instead of the original.
The approach of storing generated proxy classes in files provides the whole advantage of dynamic weaving with a minimum performance hit. Debugging of proxied classes is still easy as they truly exist in real files.
Aspects are abstract containers which accommodate pointcut-, introduction- and advice declarations. In most frameworks, including FLOW3, aspects are defined as plain classes which are tagged (annotated) as an aspect. The following example shows the definition of a hypothetical FooSecurity aspect:
Example 1.34. Declaration of an aspect
/**
* An aspect implementing security for Foo
*
* @package MySecurityPackage
* @author John Doe <john@typo3.org>
* @aspect
*/
class F3_MySecurityPackage_FooSecurityAspect {
}As you can see,
F3_MySecurityPackage_FooSecurityAspect is just a
regular PHP class which may (actually must) contain methods and
properties. What it makes it an aspect is solely the
@aspect annotation mentioned the class comment. The AOP
framework recognizes this tag and registers the class as an aspect.
A void aspect class doesn't make any sense and if you try to run the above example, the AOP framework will throw an exception complaining that no advice, introduction or pointcut has been defined.
If we want to add security to foo, we need a method which carries out the security checks and a definition where and when this method should be executed. The method is an advice which we're going to declare in a later section, the “where and when” is defined by a pointcut expression in a pointcut declaration.
A pointcut is represented by a method of an aspect class. It contains two pieces of information: The pointcut name, defined by the methodname, and the poincut expression, declared by an annotation. The following pointcut will match the execution of methods whose name starts with “delete”, mo matter in which class they are defined:
As already mentioned, the pointcut expression configures the
filters which are used to match against join points. It is comparable to
an if condition in PHP: Only if the whole condition
evaluates to TRUE, the statement is executed -
otherwise it will be just ignored. If a pointcut expression evaluates to
TRUE, the pointcut matches and advices which refer
to this poincut become active.
The AOP framework AspectJ provides a complete pointcut language with dozens of pointcut types and expression constructs. FLOW3 makes do with only a small subset of that language, which we think already suffice for even complex enterprise applications. If you're interested in the original feature set, it doesn't hurt throwing a glance at the AspectJ Programming Guide.
A pointcut expression always consists of two parts: The poincut designator and its parameter(s). The following designators are supported by FLOW3:
The method() designator matches on the
execution of methods with a certain name. The parameter specifies the
class and method name, regular expressions can be used for more
flexibility[5]. It follows the following scheme:
method(public|protected|private ClassName->methodName())
Specifying the visibility modifier (public, protected or private) is optional - if none is specified, any visibility will match. The class- and method name can be specified as a regular expression. Here are some examples for matching method executions:
Example 1.36. method() pointcut designator
Matches all public methods in class
F3_MyPackage_MyComponent:
method(public
F3_MyPackage_MyComponent->.*())
Matches all delete methods (even protected and private ones) in any class of the package MyPackage:
method(F3_MyPackage_.*->delete.*())
In other AOP frameworks, incuding
AspectJ™ and Spring™,
the method designator does not exist. They
rather use a more fine grained approach with designators such as
execution, call and
cflow. As FLOW3 only supports matching to
method execution join points anyway, we decided to simplify things
by allowing only a more general method
designator.
The class() designator matches on the
execution of methods defined in a class with a certain name. The
parameter specifies the class name, again regular expressions are
allowed here. The class() designator follows this
simple scheme:
class(classname)An example for the usage of this designator:
Example 1.37. class() pointcut designator
Matches all methods in class
F3_MyPackage_MyComponent:
class(F3_MyPackage_MyComponent)
The within() designator matches on the
execution of methods defined in a class of a certain type. A type
matches if the class is a subclass of or implements an interface of
the given name. The within() designator has this
simple syntax:
within(type)An example for the usage of
within():
Example 1.38. within() pointcut designator
Matches all methods in classes which implement the logger interface:
within(F3_Log_LoggerInterface)
Matches all methods in classes which are part of the Foo layer:
within(F3_FLOW3_FooLayerInterface)
The classTaggedWith() designator matches on
classes which are tagged with a certain annotation. As with class and
method names, a regular expression can be used to describe the
matching tags. The syntax of this designator is as follows:
classTaggedWith(tag)Example 1.39. classTaggedWith() pointcut designator
Matches all classes which are tagged with an "@entity" annotation:
classTaggedWith(entity)
Matches all classes which are tagged with an annotation starting with "@cool":
classTaggedWith(cool.*)
All pointcut expressions mentioned in previous sections can be
combined into a whole expression, just like you may combine parts to an
overall condition in an if construct. The supported
operators are “&&”, “||” and
“!” and they have the same meaning as in PHP. Nesting
expressions with parentheses is not supported but you may refer to other
pointcuts by specifying their full name (ie. class- and method name).
This final example shows how to combine and reuse pointcuts and
ultimately build a hierarchy of pointcuts which can be used conveniently
in advice declarations:
Example 1.40. Combining pointcut expressions
/**
* Fixture class for testing poincut definitions
*
* @package TestPackage
* @aspect
*/
class F3_TestPackage_PointcutTestingAspect {
/**
* Pointcut which includes all method executions in pointcutTestingTargetClasses except those from Target Class number 3.
*
* @pointcut method(F3_TestPackage_PointcutTestingTargetClass.*->.*()) && !method(F3_TestPackage_PointcutTestingTargetClass3->.*())
*/
public function pointcutTestingTargetClasses() {}
/**
* Pointcut which consists of only the F3_TestPackage_OtherPointcutTestingTargetClass.
*
* @pointcut method(F3_TestPackage_OtherPointcutTestingTargetClass->.*())
*/
public function otherPointcutTestingTargetClass() {}
/**
* A combination of both above pointcuts
*
* @pointcut F3_TestPackage_PointcutTestingAspect->pointcutTestingTargetClasses || F3_TestPackage_PointcutTestingAspect->otherPointcutTestingTargetClass
* @author Robert Lemke <robert@typo3.org>
*/
public function bothPointcuts() {}
/**
* A pointcut which matches all classes from the service layer
*
* @pointcut within(F3_FLOW3_ServiceLayerInterface)
*/
public function serviceLayerClasses() {}
/**
* A pointcut which matches any method from the BasicClass and all classes from the service layer
*
* @pointcut method(F3_TestPackage_Basic.*->.*()) || within(F3_FLOW3_Service.*)
*/
public function basicClassOrServiceLayerClasses() {}
}
With the aspect and pointcuts in place we are now ready to declare the advice. Remember that an advice is the actual action, the implementation of the concern you want to weave in to some target. Advices are implemented as interceptors which may run before and / or after the target method is called. Four advice types allow for these different kinds of interception: Before, After returning, After throwing and Around.
Other than being of a certain type, advices always come with a pointcut expression which defines the set of join points the advice applies for. The pointcut expression may, as we have seen earlier, refer to other named pointcuts.
A before advice allows for executing code before the target method is invoked. However, the advice cannot prevent the target method from being executed, nor can it take influence on other before advices at the same join point.
Example 1.41. Declaration of a before advice
/**
* Before advice which is invoked before any method call within the News package
*
* @before class(F3_News_.*->.*())
*/
public function myBeforeAdvice(F3_FLOW3_AOPJoinPointInterface $joinPoint) {
}The after returning advice becomes active after the target method normally returns from execution (ie. it doesn't throw an exception). After returning advices may read the result of the target method, but can't modify it.
Example 1.42. Declaration of an after returning advice
/**
* After returning advice
*
* @afterreturning method(public F3_News_FeedAgregator->[import|update].*()) || F3_MyPackage_MyAspect->someOtherPointcut
*/
public function myAfterReturningAdvice(F3_FLOW3_AOPJoinPointInterface $joinPoint) {
}Similar to the “after returning” advice, the after throwing advice is invoked after method execution, but only if an exception was thrown.
Example 1.43. Declaration of an after throwing advice
/**
* After throwing advice
*
* @afterthrowing within(F3_News_ImportantLayer)
*/
public function myAfterThrowingAdvice(F3_FLOW3_AOPJoinPointInterface $joinPoint) {
}The after advice is a combination of “after returning” and “after throwing”: These advices become active after method execution, no matter if an exception was thrown or not.
Example 1.44. Declaration of an after advice
/**
* After advice
*
* @after F3_MyPackage_MyAspect->justAPointcut
*/
public function myAfterAdvice(F3_FLOW3_AOPJoinPointInterface $joinPoint) {
}Finally, the around advice takes total control over the target method and intercepts it completely. It may decide to call the original method or not and even modify the result of the target method or return a completely different one. Obviously the around advice is the most powerful and should only be used if the concern can't be implemented with the alternative advice types. You might already guess how an around advice is declared:
Example 1.45. Declaration of an around advice
/**
* Around advice
*
* @around F3_MyPackage_MyAspect->justAPointcut
*/
public function myAroundAdvice(F3_FLOW3_AOPJoinPointInterface $joinPoint) {
}The final step after declaring aspects, pointcuts and advices is to fill the advices with life. The implementation of an advice is located in the same method it has been declared. In that regard, an aspect class behaves like any other component in FLOW3 – you therefore can take advantage of dependency injection in case you need other components to fullfil the task of your advice.
As you have seen in the previous section, advice methods always
expect an argument of the type
F3_FLOW3_AOPJoinPointInterface. This join
point object contains all important information about the current join
point. Methods like getClassName() or
getMethodArguments() let the advice method
classify the current context and enable you to implement advices in a
way that they can be reused in different situations. For a full
description of the join point object refer to the API
documentation.
Around advices are a special advice type in that they have the power to completely intercept the target method. For any other advice type, the advice methods are called by the proxy class one after another. In case of the around advice, the methods form a chain where each link is responsible to pass over control to the next.
Let's put our knowledge into practice and start with a simple example. First we would like to log each access to methods within certain package. The following code will just do that:
Example 1.46. Simple logging with aspects
/**
* A logging aspect
*
* @package MyPackage
* @aspect
*/
class F3_MyPackage_LoggingAspect {
/**
* @var F3_Log_LoggerInterface A logger implementation
protected $logger;
/**
* Constructor of this aspect. For logging we need a logger, which we will get
* injected automatically by the Component Manager
*
* @param F3_Log_LoggerInterface $logger: An instance of a logger
* @return void
*/
public function __construct(F3_Log_LoggerInterface $logger) {
$this->logger = $logger;
}
/**
* Before advice, logs all access to methods of our package
*
* @param F3_FLOW3_AOPJoinPointInterface $joinPoint: The current join point
* @return void
* @before method(F3_MyPackage_.*->.*())
*/
public function logMethodExecution(F3_FLOW3_AOPJoinPointInterface $joinPoint) {
$logMessage = 'The method ' . $joinPoint->getMethodName() . ' in class ' . $joinPoint->getClassName() . ' has been called.';
$this->logger->log($logMessage, 'F3_MyPackage_LoggingAspect');
}
}Note that we are using dependency injection for getting a logger instance to stay independent from any specific logging implementation. We don't have to care about the kind of logger (file based, syslog, ...) and where it comes from.
Finally an example for the implementation of an around advice: For a guestbook, we want to reject the last name “Sarkosh” (because it should be “Skårhøj”), every time it is submitted. Admittedly you probably wouldn't implement this great feature as an aspect, but it's easy enough to demonstrate the idea. For illustration purposes, we don't define the pointcut expression in place but refer to a named pointcut.
Example 1.47. Implementation of an around advice
/**
* A lastname rejection aspect
*
* @package MyPackage
* @aspect
*/
class F3_MyPackage_LastNameRejectionAspect {
/**
* A pointcut which matches all guestbook submission method invocations
*
* @pointcut method(F3_Guestbook_SubmissionHandlingThingy->submit())
*/
public function guestbookSubmissionPointcut() {}
/**
* Around advice, rejects the lastname "Sarkosh"
*
* @param F3_FLOW3_AOPJoinPointInterface $joinPoint: The current join point
* @return mixed Result of the target method
* @around F3_MyPackage_LastNameRejectionAspect->guestbookSubmissionPointcut
*/
public function rejectLastName(F3_FLOW3_AOPJoinPointInterface $joinPoint) {
if ($joinPoint->getMethodArgument('lastName') == 'Sarkosh') {
throw new Exception('Sarkosh is not a valid lastname - should be Skårhøj!');
}
$result = $joinPoint->getAdviceChain()->proceed($joinPoint);
return $result;
}
}Please note that if the last name is correct, we proceed with the remaining links in the advice chain. This is very important to assure that the original (target-) method is finally called. And don't forget to return the result of the advice chain ...
Introductions (also known as Inter-type Declarations) allow to subsequently implement an interface in a given target class. The (usually) newly introduced methods (required by the new interface) can then be implemented by declaring an advice. If no implementation is defined, an empty placeholder method will be generated automatically to satisfy the contract of the introduced interface.
Like advices, introductions are declared by annotations. But in contrast to advices, the anchor for an introduction declaration is a property of the aspect class. The annotation tag follows this syntax:
@introduce NewInterfaceName,
PointcutExpression
Although the PoincutExpression is just
a normal pointcut expression, which may also refer to named pointcuts,
be aware that only expressions filtering for
classes make sense. You cannot use the
method() pointcut designator in this context and
will typically take the class() designator
instead.
The following example introduces a new interface
NewInterface to the class
OldClass and also provides an implementation of
the method newMethod.
Example 1.48. Declaring introductions
/**
* An aspect for demonstrating introductions
*
* @package MyPackage
* @aspect
*/
class F3_MyPackage_IntroductionAspect {
/**
* Introduces F3_MyPackage_NewInterface to the class F3_MyPackage_OldClass:
*
* @introduce F3_MyPackage_NewInterface, class(F3_MyPackage_OldClass)
*/
public $newInterface;
/**
* Around advice, implements the new method "newMethod" of the "NewInterface" interface
*
* @param F3_FLOW3_AOPJoinPointInterface $joinPoint: The current join point
* @return void
* @around method(F3_MyPackage_OldClass->newMethod())
*/
public function newMethod(F3_FLOW3_AOPJoinPointInterface $joinPoint) {
// We call the advice chain, in case any other advice is declared for this method,
// but we don't care about the result.
$someResult = $joinPoint->getAdviceChain->proceed($joinPoint);
$a = $joinPoint->getMethodArgument('a');
$b = $joinPoint->getMethodArgument('b');
return $a + $b;
}
}The following diagram illustrates the building process of a proxy class:
[1] SoC could, by the way, also mean “Self-organized criticality” or “Service-oriented Computing” or refer to Google's “Summer of Code” ...
[2] AOP was rather invented by Gregor Kiczalesand his team at the Xerox Palo Alto Research Center. The original implementation was called AspectJ and is an extension to Java. It still serves as a de-facto standard and is now maintained by the Eclipse Foundation.
[3] Intercepting setting and retrieval of properties can easily be achieved by declaring a before-, after- or around advice.
[4] GoF means Group of Four and refers to the authors of the classic book Design Patterns – Elements of Reusable Object-Oriented Software
[5] Internally, PHP's preg_match() function
is used to match the method name. The regular expression will be
enclosed by /^
(without the dots of course)....$/