Using annotations in PHP 5.4 for AOP and more

When developing large projects, quite often a situation arises when the end-to-end functionality, weakly related to business logic, is greatly inflated, filling the code with similar designs. This can be logging operations, working with the cache or checking access rights. Here AOP comes to our aid .

For PHP, there are several implementations of this programming paradigm. Unfortunately, among them I could not find a solution that could be easily implemented in an existing large project and satisfying the aesthetic requirements for the code.

AOP Implementation Technologies in PHP


Magic methods

The simplest solution is to use the __call and __callStatic “ magic methods ”. These methods are called (if they are defined in the class) when accessing a non-existent class method. As arguments, they get the name of the nonexistent method and the parameters passed to it.
In this case, the application is built in such a way that the real methods have a name different from the name specified in the constructions calling them. Cross-functional is implemented in “magic methods”, which, if necessary, transfer control to real class methods.

Pros:
  • Easy to start using;
  • The implementation does not require additional modules (native PHP).

Minuses:
  • It is not convenient to use with a large number of end-to-end functionality;
  • Because the names of the methods in the definition and in the calls are different, difficulties are created when using code completion in the IDE.

Code parsing

This method involves the presence of an intermediary that allows the use of "syntactic sugar." The necessary functionality is described by auxiliary syntax (xml / json configuration, additional php classes or annotations in the code), which is parsed by an intermediary. Based on the parsing, the resulting code is generated that contains the inserts of the through functionality in the necessary places.

Pros:
  • It works fast because the output is regular PHP code, just automatically generated for you.

Minuses:
  • It is difficult to implement in a large project;
  • Parsing the code after each change is required to make adjustments to the resulting code.

Replacing application code at run time

The notorious runkit extension allows you to change the script code during its execution. Based on it, I developed a small library that allows you to quite easily solve the task.

Meet: Annotator .

Annotator Features


The library implements 4 types of handlers:

Info

An Info type handler receives method information during class processing. This allows you to "register" the method for future use in the application. For example, using it, you can assign a method to process a specific URL and thus implement routing in the application.

Before

A handler of type Before is executed before the method being called. It receives all the necessary information about the called method, including input parameters that can be changed before transferring control to the called method.

After

A handler of type After is executed after the called method. In addition to information about the method and its parameters, it also receives the result of executing the called method, which, if necessary, can be replaced.

Around

A handler of type Around is executed instead of the called method. Inside the handler, it is possible to manually transfer control to the called method, if necessary.

Installation


Annotator requires PHP 5.4 and the runkit module to work.

  1. Download the extension from here: https://github.com/zenovich/runkit ;
  2. We collect and install it:
    phpize && ./configure && make && sudo make install
    
  3. If everything went well, we connect the runkit.so module in conf.d or php.ini;
  4. Download the Annotator.php
  5. class and connect it to the project.

Examples of using


The class provides 4 pre-reserved annotations for all types of handlers, while it is possible to register your annotations of any of 4 types.

Info

<?php

require_once __DIR__ . '/Annotator.php';

class Advice {
	public static function infoStatic($point, $options) {
		var_dump($point);
		var_dump($options);
	}
}

class Test {
	/**
	 * @info Advice::infoStatic hello world
	 */
	public static function testInfoStatic() {
		return 'info';
	}
}

Annotator::compile('Test');

Already during the call Annotator :: compile ('Test'); the infoStatic handler of the Advice class will be called, which will receive information about the testInfoStatic method of the Test class and the handler parameters as an array array ('hello', 'world').

Before

In this example, we will register our annotation instead of using the standard one, and use the object method instead of the static class method as a handler.

<?php

require_once __DIR__ . '/Annotator.php';

class Advice {
	public function before($point, $params, $options) {
		$params['string'] = 'bar';
	}
}

class Test {
	/**
	 * @registered_before
	 */
	public function testBefore($string) {
		return $string;
	}
}

$advice = new Advice();

Annotator::register('registered_before', array($advice, 'before'), Annotator::BEFORE);

Annotator::compile('Test');

$test = new Test();

echo $test->testBefore('foo');

Using the Annotator :: register method, we created the @registered_before annotation associated with the before method of the $ advice object. When testBefore is called, control will be passed to the handler, which will replace the $ string parameter and instead of the expected "foo", as a result of the script, "bar" will be displayed.

After

<?php

require_once __DIR__ . '/Annotator.php';

class Advice {
	public function power($point, $params, $options, $result) {
		return pow($result, $options[0]);
	}
}

class Test {
	/**
	 * @power 4
	 */
	public function testAfter($number) {
		return $number + 1;
	}
}

$advice = new Advice();

Annotator::register('power', array($advice, 'power'), Annotator::AFTER);

Annotator::compile('Test');

$test = new Test();

echo $test->testAfter(1);

In this example, the result of the testAfter method will be raised to the power of 4. The script will display the value 16.

Around

<?php

require_once __DIR__ . '/Annotator.php';

class Advice {
	private static $cache = array();

	public function cache($point, $params, $options, $proceed) {
		if (array_key_exists($options[0], self::$cache)) {
			// Если значение ключа содержится в кеше - возвращаем его
			return self::$cache[$options[0]];
		} else {
			// Если значения в кеше нет - выполняем функцию
			$result = $proceed();
			// Перед возвращением значения не забываем положить его в кеш
			self::$cache[$options[0]] = $result;
			return $result;
		}
	}
}

class Test {
	/**
	 * @cache around_cache_key
	 */
	public function testAround($string) {
		return $string;
	}
}

$advice = new Advice();

Annotator::register('cache', array($advice, 'cache'), Annotator::AROUND);

Annotator::compile('Test');

$test = new Test();

echo $test->testAround('foo') . PHP_EOL;
echo $test->testAround('foo') . PHP_EOL;
echo $test->testAround('foo') . PHP_EOL;

This example represents an implementation of a simple caching mechanism. The testAround method is called 3 times in a row, but will be executed only 1 time. The rest 2 times the value will be taken from the static variable $ cache of the Advice class, where it will be saved after the first call.

Using multiple handlers

Annotator allows you to attach multiple handlers, including various types, to a single method.

<?php

require_once __DIR__ . '/Annotator.php';

class Advice {
	public static function before1($point, $params, $options) {
		$params['string'] .= 'before1';
	}

	public static function before2($point, $params, $options) {
		$params['string'] .= ' before1';
	}

	public static function after1($point, $params, $options, $result) {
		return $result . ' after1';
	}

	public static function after2($point, $params, $options, $result) {
		return $result .= ' after2';
	}

	public static function around1($point, $params, $options, $proceed) {
		return $proceed() . ' around1';
	}

	public static function around2($point, $params, $options, $proceed) {
		return $proceed() . ' around2';
	}
}

class Test {
	/**
	 * @before Advice::before1
	 * @after Advice::after1
	 * @around Advice::around1
	 * @before Advice::before2
	 * @after Advice::after2
	 * @around Advice::around2
	 */
	public function testMulti($string) {
		return $string;
	}
}

Annotator::compile('Test');

$test = new Test();

echo $test->testMulti('');

As a result of this example, the string “before1 before1 around1 around2 around1 after1 after2” will be displayed.

Handlers


Each type of handler has its own set of parameters:

Info
  • $ point - the method on which the handler is mounted;
  • $ options - an array of parameters specified in the annotation.


Before
  • $ point - the method on which the handler is mounted;
  • $ params - an array of parameters passed to the method when called;
  • $ options - an array of parameters specified in the annotation.


After
  • $ point - the method on which the handler is mounted;
  • $ params - an array of parameters passed to the method when called;
  • $ options - an array of parameters specified in the annotation.
  • $ result - a variable containing the result of the method on which the handler is mounted.


Around
  • $ point - the method on which the handler is mounted;
  • $ params - an array of parameters passed to the method when called;
  • $ options - an array of parameters specified in the annotation.
  • $ proceed is a function that transfers control back to the method on which the handler is hung.

Afterword


On the basis of Annotator, you can very easily and quickly implement convenient mechanisms that can greatly reduce code and improve the structure of the application, simplifying its support. For example, in addition to implementing AOP, it can be used to implement the Dependency injection pattern quite easily .

It must be remembered that replacing methods during script execution takes some time. You should not call Annotator :: compile for classes that will not be used in this request. The easiest way to do this is by automatically loading the php classes .

For long-lived applications (simple daemons or phpDaemon- based applications ) introduced overhead will have virtually no effect on performance, as classes will be loaded and processed only 1 time.