# Aop (Aspect oriented programming)

# Content

This extension is here to provide AOP functionality (opens new window) into Nette Framework (opens new window) DI Container (opens new window).

# Setup

composer require contributte/aop
1

Enable the extension using your neon config.

extensions:
	aop: Contributte\Aop\DI\AopExtension
	aspects: Contributte\Aop\DI\AspectsExtension
1
2
3

# Configuration

This extension creates new configuration section aspects and it should behave exactly like services, but all the services are marked as aspects.

aspects:
	- MyApp\LoggingAspect(@dep, %param%)
1
2

Never give the aspects any names, keep them anonymouse.

This internally works exactly like services section, but it tags all the aspects with contributte.aspect tag. So if you don't want to, or cannot use the section, just tag the aspect and you're good to go.

# IAspectsProvider

Implement this interface to your CompilerExtension if you want it to provide aspects. Example:

class AclExtension extends Nette\DI\CompilerExtension implements \Contributte\Aop\DI\IAspectsProvider
{
	public function getAspectsConfiguration()
	{
		return \Contributte\Aop\DI\AspectsExtension::loadAspects(__DIR__ . '/aspects.neon', $this);
	}
}
1
2
3
4
5
6
7

The aspects.neon file should be list of unnamed services as in aspects section.

# AspectsExtension

There are two extensions?!

Yeah, why not? I needed the section aspects for services and section aop for configuration.

# Upgrade to v3

There are few breaking changes you have to deal with when upgrading to v3 from v2

  • Dropped nette\reflection so all methods that were returning Nette\Reflection objects are now returning raw php \Reflection* objects
  • Dropped nettrine\annotations we're using native php attributes now

# Dictionary

Aspect
The object that extends behaviour of other objects
<dt>Advice</dt>
<dd>The action that is taken when you're extending the behaviour. You can read it as "The object Application is being advised by Logger Aspect"</dd>

<dt>Join point</dt>
<dd>The exact moment at the runtime, where your advice is connected.</dd>

<dt>Pointcut</dt>
<dd>The special syntax for join point definition</dd>

# Advice types

Before
This advice can be used for reading/logging of method arguments, or their modification
<dt>After</dt>
<dd>Think of this as the `finally` keyword, it should get called even if the method throws, but it cannot change what is returned.</dd>

<dt>After returning</dt>
<dd>You can read/log or modify the return value here</dd>

<dt>After throwing</dt>
<dd>You can read the exception here</dd>

<dt>Around</dt>
<dd>The most powerful advice, if it's defined, it can prevent the original method from being called, change it's arguments or return value, damn, even the exception.</dd>

Choose wisely, great powers comes with a (performance) cost.

# Pointcut Syntax

# method([public|protected] ns\class->method(argument == value))

Examples:

  • method(public Nette\Application\Application->processRequest())
  • method(Nette\Application\UI\Presenter->[render|action|handle]*()) - should match all three variants of methods, meaning all render*(), action*() and handle*() (presenters have to be registered in DIC)
  • method(Nette\Application\UI\*->[handle]*()) - should match all Presenter, Control and PresenterComponent signals (they have to be registered in DIC)
  • method(*->*()) - matches all methods of all services in DIC - You should never do this, it would be really painful!!!

Keep those conditions as simple as possible! The more complex they are, the longer it will take to compile!

The arguments are evaluated at runtime, read more at evaluate pointcut.

# class(ns\class)

Examples:

  • class(Nette\Application\UI\Presenter)
  • class(Nette\Application\IPresenter) - yeah, it can match also interfaces or parent classes
  • class(Nette\Application\UI\*) - matches all classes in namespace Nette\Application\UI
  • class(*) - matches all classes - You should never do this, it would be really painful!!!

Keep in mind, that exact class name can be be optimized, to analyze only those services, that matches it exactly. When you use wildmark, all the services has to be scanned it they match and this can literary kill your application in development mode!

# within(ns\class)

This is basically an alias for class pointcut. But it expresses better the nature of this pointcut. It scans all the types of the service and if it implements an interface, or one of parent classes matches, than this will also match.

# filter(filterClass)

Argument of this pointcut should be name of class that implements Contributte\Aop\Pointcut\Filter. You can basically write your own pointcut filter here.

# setting(%foo.bar% == TRUE)

Wanna have the ability to turn on and off the advices based on DIC parameters? No problem!

# evaluate(this.foo.bar == TRUE)

This is really advanced runtime pointcut and it's also used in method arguments. What does it mean, runtime? Well, it's serialised to condition and every time you run the method, the condition is evaluated and decides it the advice gets called.

Examples:

  • evaluate($argument == 1) - this is for arguments matching
  • evaluate(this.dave.lister[kryten] == TRUE) - this translates to $this->dave->lister['kryten'] but it's little smarter than that, have a look at Symfony/PropertyAccess (opens new window), it's used for (surprisingly) property access.
  • evaluate(context.httpRequest.post == TRUE) - this is translated to $context->getService('httpRequest')->isPost() but also here, for property access is used Symfony/PropertyAccess
  • evaluate(context.Nette\Http\IRequest.post == TRUE) - this is translated to $context->getByType('Nette\Http\IRequest')->isPost()
  • evaluate(%foo.bar% == TRUE) - you can write this, but it's a nonsense, there is setting pointcut for DIC parameters

And don't forget to have a look at Symfony/PropertyAccess (opens new window).

# classAttributedWith(Some\Attribute)

Matches all classes, that have this attribute.

# methodAttributedWith(Some\Attribute)

Matches all methods, that have this attribute.

# Join points

When join point is invoked, an instance of concrete JoinPoint class is also created and passed to your advice (the method on aspect). For every type of advice, there is a join point class in namespace Contributte\Aop\JoinPoint.

  • BeforeMethod provides arguments and can be used for arguments modification
  • AfterMethod provides return value or exception
  • AfterReturning provides return value and can be used for it's modification
  • AfterThrowing provides the exception (only if there is some thrown)
  • AroundMethod provides everything and allows you to change it completely

# Aspect examples

Let's utilize what we've learned so far and write some aspects.

use Contributte\Aop;

class BeforeAspect
{
	private $db;

	public function __construct(Contributte\Doctrine\Connection $db)
	{
		$this->db = $db;
	}

	 #[Aop\Attributes\Before("method(CommonService->magic)")]
	public function log(Aop\JoinPoint\BeforeMethod $before)
	{
		$this->db->insert('log', array('something' => $before->arguments[1]));
		$before->setArgument(1, "changed value");
	}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

This aspect will add an advice log to method magic of class CommonService, that will log it's second argument (index 1) and always change it.

class AroundAspect
{

	 #[Contributte\Aop\Attributes\Around("method(CommonService->magic)")]
	public function log(Aop\JoinPoint\AroundMethod $around)
	{
		// I can change the arguments here

		$result = $around->proceed();

		// I can change the result here

		return $result;
	}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

In around aspect, you must manually call the method ->proceed() which will either invoke another around advice in chain, or the method itself. You can never know and you shouldn't even care.