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 constructions. This can be logging operations, working with a cache or checking access rights. Here
AOP comes to the rescue.
For PHP, there are several implementations of this programming paradigm. Unfortunately, among them I did not manage to find a solution that could be easily incorporated into an already 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 "
magic methods " __call and __callStatic. These methods are called (if defined in a class) when accessing a nonexistent class method. As arguments, they get the name of a nonexistent method and the parameters passed to it.
In this case, the application is built in such a way that the actual methods have a different name from the name specified in their constructs. End-to-end functionality 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 method names in the definition and in the calls are different, difficulties are created when using code completion in the IDE.
Preliminary code review
This method implies the existence of a mediator 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 the intermediary. Based on the parsing, a result code is generated that contains inserts of the end-to-end functional in the required places.
Pros:
- It works quickly, because the output is plain PHP code, just generated for you automatically.
Minuses:
- Difficult to implement in a big project;
- Code analysis is required after each change to make corrections to the resulting code.
Replacing Application Code at Run Time
The well-known extension
runkit allows
you to change the script code during its execution. Based on it, I developed a small library that allows you to quite simply solve the problem.
Meet the:
Annotator .
Annotator features
The library implements 4 types of handlers:
Info
An Info type handler receives information about a method during class processing. This allows you to "register" the method for future use in the application. For example, with its help, you can assign a method to process a specific URL and thus implement routing in an application.
Before
A handler of the type Before is executed before the called method. It gets all the necessary information about the called method including the input parameters that can be changed before transferring control to the called method.
After
An After type handler is executed after the method being called. In addition to information about the method and its parameters, it also receives the result of the execution of the called method, which, if necessary, can be replaced.
Around
An Around handler is executed instead of the method being called. Inside the handler, it is possible to manually transfer control to the called method, if necessary.
Installation
Annotator requires PHP 5.4 and a runkit module.
- Downloading the extension from here: https://github.com/zenovich/runkit ;
- We assemble and install it:
phpize && ./configure && make && sudo make install
- If everything went well, we include the runkit.so module in conf.d or php.ini;
- We download the class Annotator.php and connect it to the project.
Examples of using
The class provides 4 pre-reserved annotations for all types of processors, while it is possible to register your annotations of any of the 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 { 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 the array ('hello', 'world') array.
Before
In this example, we register our annotation instead of using the standard one, and use the object method instead of the static class method as the handler.
<?php require_once __DIR__ . '/Annotator.php'; class Advice { public function before($point, $params, $options) { $params['string'] = 'bar'; } } class Test { 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 you call testBefore, the control will be passed to the handler, which will replace the $ string parameter and instead of the expected “foo”, the script will output “bar”.
After
<?php require_once __DIR__ . '/Annotator.php'; class Advice { public function power($point, $params, $options, $result) { return pow($result, $options[0]); } } class Test { 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)) {
This example represents an implementation of a simple caching mechanism. The testAround method is called 3 times in a row, but it will be executed only once. The remaining 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 hang multiple handlers, including different types, on one 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 { 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 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 hung;
- $ options - an array of parameters specified in the annotation.
Before
- $ point - the method on which the handler is hung;
- $ 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 hung;
- $ params - an array of parameters passed to the method when called;
- $ options - an array of parameters specified in the annotation;
- $ result is a variable containing the result of the method on which the handler is hung.
Around
- $ point - the method on which the handler is hung;
- $ params - an array of parameters passed to the method when called;
- $ options - an array of parameters specified in the annotation;
- $ proceed - the function transferring control back to the method on which the handler is hung.
Afterword
On the basis of the Annotator, you can very easily and quickly implement convenient mechanisms that allow you to greatly reduce the code and improve the structure of the application, simplifying its support. For example, in addition to the implementation of AOP, it can be used to quite easily implement the
Dependency injection pattern.
It must be remembered that replacing methods during the execution of the script takes some time. You should not call Annotator :: compile for classes that will not be used in this query. The easiest way to do this is through
automatic loading of php classes .
For long-lived applications (simple daemons or applications based on
phpDaemon ), the overhead applied will have almost no effect on performance, since classes will be loaded and processed only 1 time.