📜 ⬆️ ⬇️

Getting rid of duplicate end-to-end code in PHP: code refactoring with AOP

I think every programmer is familiar with the principle of sole responsibility, it is not for nothing that it exists: by observing it, you can write code better, it will be more understandable, it will be easier to refine it.

But the more each of us works with the code, the more comes the understanding that this is impossible at the existing level of the language - object-oriented. And we are hampered by the fact that end-to-end functionality prevents us from observing the principle of sole responsibility.

This article is about how to get rid of duplicate end-to-end code, and how to make it a little better with AOP.
')


End-to-end functionality or wet code


With a probability of about 95%, in any application, you can find pieces of end-to-end functionality that are hidden in the code under the guise of caching, logging, exception handling, transactional control, and access rights. As you already guessed from the name, this functionality lives on all layers of the application (do you have layers?) And forces us to violate several important principles: DRY and KISS . Violating the DRY principle, you automatically begin to use the WET principle and the code becomes “wet”, which is reflected in the form of an increase in metrics Lines of Code (LOC), Weighted Method Count (WMC), Cyclomatic Complexity (CCN).

Let's see how this happens in real life. A technical task comes, a system is designed for it, a decomposition into classes is carried out and the necessary methods are described. At this stage, the system is perfect, the purpose of each class and service is clear, everything is simple and logical. And then the end-to-end functionality begins to dictate its own rules and forces the programmer to make edits to the code of all classes, since it is not possible in OOP to decompose through the end-to-end functionality. This process goes unnoticed, because everyone is accustomed to it as a normal phenomenon, and no one is trying to fix anything. The process proceeds according to the standard scheme worked over the years.

First, the logic of the method itself is written, which contains the necessary and sufficient implementation:

/** * Creates a new user * * @param string $newUsername Name for a new user */ public function createNewUser($newUsername) { $user = new User(); $user->setName($newUsername); $this->entityManager->persist($user); $this->entityManager->flush(); } 


... after that we add 3 more lines of code for checking access rights

 /** ... */ public function createNewUser($newUsername) { if (!$this->security->isGranted('ROLE_ADMIN')) { throw new AccessDeniedException(); } $user = new User(); $user->setName($newUsername); $this->entityManager->persist($user); $this->entityManager->flush(); } 


... then another 2 lines for logging the beginning and end of the method

 /** ... */ public function createNewUser($newUsername) { if (!$this->security->isGranted('ROLE_ADMIN')) { throw new AccessDeniedException(); } $this->logger->info("Creating a new user {$newUsername}"); $user = new User(); $user->setName($newUsername); $this->entityManager->persist($user); $this->entityManager->flush(); $this->logger->info("User {$newUsername} was created"); } 


Do you recognize your code? Not yet? Then let's add 5 more lines to handle a possible exception with several different handlers. In the methods that return data, 5 more lines may be added to save the result in the cache. Thus, from 4 lines of code that really have value, you can get about 20 lines of code. Than it threatens, I think it is clear - the method becomes more difficult, it is harder to read, it takes longer to understand what it actually does, it is more difficult to test, because you have to slip mocks for the logger, cache, etc. Since the example was for one of the methods, it is logical to assume that the statements regarding the size of the method are valid both for the class and for the system as a whole. The older the system code, the more it acquires such garbage and it becomes harder to keep track of it.

Let's look at existing solutions to end-to-end functionality problems.

Clean - the guarantee of health! Health first!


I recommend reading the heading in the context of application development like this: “Clean code is the key to the health of the application! Health applications first! ”. It would be nice to hang such a label in front of each developer to always remember this :)

So, we decided to keep the code clean by all means. What solutions do we have and what can we use?

Decorators


Decorators - the first thing that comes to mind. Decorator is a structural design pattern intended to dynamically connect additional behavior to an object. The Decorator pattern provides a flexible alternative to the practice of creating subclasses to extend functionality.

When it comes to AOP , the first question that OOP programmers usually ask is - why not use a regular decorator? And it is right! Because the decorator can do almost everything that is done with the help of AOP, but ... Counter-example: what if we make a LoggingDecorator on top of CachingDecorator, and the latter, in turn, on top of the main class? How many of the same type code will be in these decorators? How many different classes of decorators will be in the whole system?

It is easy to estimate that if we have 100 classes that implement 100 interfaces, then adding caching decorators will add 100 more classes to our system. Of course, this is not a problem in the modern world (look in the cache folder of any large framework), but why do we need these 100 classes of the same type? Not clear, agree?

Nevertheless, the moderate use of decorators is fully justified.

Proxy classes


Proxy classes - the second thing that comes to mind. Proxy - a design pattern that provides an object that controls access to another object, intercepting all calls (performs the function of a container).

Not a good solution from my point of view, but all developers have a lot of caching proxies, so they can be found in applications so often. The main disadvantages are: a drop in the speed of work (__call, __get, __callStatic, call_user_func_array is often used), and typhingting also breaks because a proxy object comes in instead of a real object. If you try to wrap a caching proxy on top of the logging proxy, which in turn is on top of the main class, the speed will drop by an order of magnitude.

But there is a plus: in the case of 100 classes, we can write one caching proxy to all classes. But! At the cost of abandoning of typhinting over 100 interfaces, which is totally unacceptable when developing modern applications.

Events and the Observer Pattern


It is hard not to recall such a wonderful pattern as the Observer. Observer is a behavioral design pattern. Also known as “subordinates” (Dependents), “Publisher-Subscriber” (Publisher-Subscriber).

In many well-known frameworks, developers are faced with end-to-end functionality and the need to extend the logic of some method over time. Many ideas were tried out, and one of the most successful and understandable was the model of events and subscribers to these events. By adding or removing event subscribers, we can expand the logic of the main method, and changing their order with the help of priorities, we can execute the logic of the handlers in the necessary order. Very nice, almost AOP!

It should be noted that this is the most flexible template, because on its basis you can design a system that will be expanded very easily and will be understandable. If it were not for AOP, it would be the best way to extend the logic of the methods without changing the source code. Not surprisingly, many frameworks use events to extend functionality, such as ZF2, Symfony2. The Symfony2 website has a great article on how you can extend the logic of a method without using inheritance.

Nevertheless, despite all the advantages, there are several big drawbacks, which sometimes outweigh the advantages. The first minus is that you need to know in advance what and where can be expanded in your system. Unfortunately, it is often unknown. The second disadvantage is that it is necessary to write code in a special way, adding template lines for generating an event and processing it (an example from Symfony2):

 class Foo { // ... public function __call($method, $arguments) { // create an event named 'foo.method_is_not_found' $event = new HandleUndefinedMethodEvent($this, $method, $arguments); $this->dispatcher->dispatch('foo.method_is_not_found', $event); // no listener was able to process the event? The method does not exist if (!$event->isProcessed()) { throw new \Exception(sprintf('Call to undefined method %s::%s.', get_class($this), $method)); } // return the listener returned value return $event->getReturnValue(); } } 


Signals and Slots


This pattern , in its essence, is an implementation of the Observer pattern, but reduces the number of duplicate code.

Of the most interesting implementations of this pattern, I would point out the core of the Lithium framework, the study of which can give a lot of new, even to advanced developers. In short, Lithium allows you to hang up callback filter functions on any important methods in the system and perform additional processing. Would you like to write a log of database requests to a file in debug mode - there is nothing easier:

 use lithium\analysis\Logger; use lithium\data\Connections; // Set up the logger configuration to use the file adapter. Logger::config(array( 'default' => array('adapter' => 'File') )); // Filter the database adapter returned from the Connections object. Connections::get('default')->applyFilter('_execute', function($self, $params, $chain) { // Hand the SQL in the params headed to _execute() to the logger: Logger::debug(date("DM j G:i:s") . " " . $params['sql']); // Always make sure to keep the filter chain going. return $chain->next($self, $params, $chain); }); 


I strongly recommend that you familiarize yourself with the filter system , because the implementation of filters in Lithium brings development as close as possible to aspect-oriented programming and can be the impetus for you to plunge into the world of AOP finally.

Aspect-oriented programming


So, we come to the most interesting - to use aspect-oriented programming to combat duplicate end-to-end code. On Habré there were already articles on AOP , including for PHP , so I will not repeat this material and give definitions of those terms and those techniques that AOP uses. If you are not familiar with the terms and concepts of AOP, then before further reading you can read the article about AOP on Wikipedia.

So, the filters in Lithium allow you to connect additional handlers almost anywhere, which makes it possible to put the caching code, logging, access rights checks into individual closures. It would seem, here it is, a silver bullet. But all is not so smooth. First, to use filters, we need to connect the entire framework, since there is no separate library for this, which is a pity. Secondly, filter closures (in terms of AOP - tips) are scattered everywhere and it is very difficult to follow them. Third, the code must be written in a certain way and implement special interfaces so that filters can be used. These three minuses significantly limit the ability to use filters as AOP in other applications and frameworks.

This is where I got the idea to write a library that would allow using AOP in any PHP application. Next was the battle with PHP, the study of code acceleration techniques, the fight against the opcode accelerator bugs, and many, many interesting things. As a result, the Go! Library was born. PHP AOP, which can be embedded in an existing application, intercepts the available methods in all classes and extract the cross-cutting functionality from them to several thousand lines of code in a couple of dozen lines of advice.

Go Library PHP AOP


The main differences from all existing analogues is a library that does not require any PHP extensions, does not call for help black magic runkit-a and php-aop. It does not use evals, is not tied to a DI container, does not need a separate aspect compiler in the final code. Aspects are ordinary classes that seamlessly use all the features of the PLO. The code generated by the library with interwoven aspects is very clean, it can be easily debugged with XDebug, both the classes themselves and the aspects.

The most valuable thing in this library is that theoretically it can be connected to any application, because to add new functionality with the help of AOP you do not need to change the application code at all, the aspects are woven dynamically. For example: using ten to twenty lines of code, you can intercept all public, protected and static methods in all classes when starting a standard ZF2 application and display the name of this method and its parameters when the method is called on the screen.

The issue of working with opcode-chesherem is worked out - in combat mode, the interweaving of aspects occurs only once, after which the code gets from the opcode-cacher. Doctrine annotations are used for aspect classes. In general, a lot of interesting things inside.

End-to-end refactoring using AOP


To spark interest in AOP more, I decided to choose an interesting topic about which you can find little information - refactoring code to aspects. Next, there will be two examples of how you can make your code cleaner and clearer using aspects.

We take out logging from code

So, let's imagine that we have logging of all executed public methods in 20 classes that are in the Acme namespace. It looks something like this:

 namespace Acme; class Controller { public function updateData($arg1, $arg2) { $this->logger->info("Executing method " . __METHOD__, func_get_args()); // ... } } 


Let's take and refactor this code using aspects! It is easy to see that logging is performed before the code of the method itself, so we immediately select the type of advice - Before. Next we need to determine the point of implementation - the implementation of all public methods inside the Acme namespace. This rule is given by the expression execution (public Acme \ * -> * ()). So, we write LoggingAspect:

 use Go\Aop\Aspect; use Go\Aop\Intercept\MethodInvocation; use Go\Lang\Annotation\Before; /** * Logging aspect */ class LoggingAspect implements Aspect { /** @var null|LoggerInterface */ protected $logger = null; /** ... */ public function __construct($logger) { $this->logger = $logger; } /** * Method that should be called before real method * * @param MethodInvocation $invocation Invocation * @Before("execution(public Acme\*->*())") */ public function beforeMethodExecution(MethodInvocation $invocation) { $obj = $invocation->getThis(); $class = is_object($obj) ? get_class($obj) : $obj; $type = $invocation->getMethod()->isStatic() ? '::' : '->'; $name = $invocation->getMethod()->getName(); $method = $class . $type . $name; $this->logger->info("Executing method " . $method, $invocation->getArguments()); } } 


Nothing complicated, normal class with a seemingly normal method. However, this is an aspect that determines the advice beforeMethodExecution, which will be called before calling the methods we need. As you have already noticed, go! uses annotations to store metadata, which has long been common practice, as it is visual and convenient. Now we can register our aspect in the Go kernel! and throw out of the heap of our classes all logging! By removing unnecessary dependency on the logger, we made our code of classes cleaner, he began to more respect the principle of common responsibility, because we learned from it what he should not do.

Moreover, now we can easily change the logging format, because now it is set in one place.

Transparent caching

I think everyone knows the sample code of the method using caching:

  /** ... */ public function cachedMethod() { $key = __METHOD__; $result = $this->cache->get($key, $success); if (!$success) { $result = // ... $this->cache->set($key, $result); } return $result; } 


Undoubtedly, everyone will recognize this template code, since there are always plenty of such places. If we have a large system, then there can be a lot of such methods, so that they can be cached. And what an idea! Let's annotate the methods that need to be cached, and in the point we set the condition - all methods marked with a specific annotation. Since caching wraps the method code, we also need the right type of advice — Around, the most powerful. This type of board itself decides whether to execute the source code of the method. And then everything is simple:

 use Go\Aop\Aspect; use Go\Aop\Intercept\MethodInvocation; use Go\Lang\Annotation\Around; class CachingAspect implements Aspect { /** * Cache logic * * @param MethodInvocation $invocation Invocation * @Around("@annotation(Annotation\Cacheable)") */ public function aroundCacheable(MethodInvocation $invocation) { static $memoryCache = array(); $obj = $invocation->getThis(); $class = is_object($obj) ? get_class($obj) : $obj; $key = $class . ':' . $invocation->getMethod()->name; if (!isset($memoryCache[$key])) { $memoryCache[$key] = $invocation->proceed(); } return $memoryCache[$key]; } } 


In this advice, the most interesting thing is to call the original method, which is done by calling proceed () on the MethodInvocation object containing information about the current method. It is easy to see that if we have data in the cache, then we do not make a call to the original method. At the same time, your code does not change in any way!
Having this aspect, we can annotate Annotation \ Cacheable before any method and this method will be cached thanks to AOP automatically. We go through all the methods and cut out the caching logic, replacing it with the annotation. Now the template code of the method using caching looks simple and elegant:

  /** * @Cacheable */ public function cachedMethod() { $result = // ... return $result; } 


This example can also be found inside the Go! Demos folder of the Go! Library. PHP AOP, and also look at the commit that implements the above in action.

Conclusion


Aspect-oriented programming is a fairly new paradigm for PHP, but it has a great future. The development of metaprogramming, the writing of Enterprise frameworks in PHP - all this follows the footsteps of Java, and AOP in Java has been living for a very long time, so you need to prepare for AOP now.

Go! PHP AOP is one of the few libraries that works with AOP and in some cases it compares favorably with its counterparts - the ability to intercept static methods, methods in final classes, access to object properties, the ability to debug source code and aspect code. Go! uses a lot of techniques to ensure high speed: compilation instead of interpretation, lack of slow techniques, optimized execution code, the ability to use opcode-cacher - all this contributes to the common cause. One of the amazing discoveries was that Go! in some similar conditions, PHP-AOP can work faster than C-extension. Yes, yes, this is true, which has a simple explanation - extension interferes with the work of all methods in PHP in runtime and makes small checks for matching the item, the more such checks, the slower the call to each method, whereas Go! does it once when compiling class code without affecting the speed of the methods in runtime.

If you have questions and suggestions about the library, I will be happy to discuss them with you. I hope, my first article on Habré was useful to you.

Links

  1. Source code https://github.com/lisachenko/go-aop-php
  2. SymfonyCampUA-2012 presentation http://www.slideshare.net/lisachenko/php-go-aop
  3. SymfonyCampUA-2012 Video http://www.youtube.com/watch?v=ZXbREKT5GWE
  4. An example of intercepting all methods in ZF2 (after cloning we install dependencies via composer) https://github.com/lisachenko/zf2-aspect
  5. An interesting article on the topic: Aspects, filters and signals - oh, my God! (en)

Source: https://habr.com/ru/post/165329/


All Articles