I think every programmer familiar principle sole responsibility it is no wonder it exists: keeping it, you can write better code, it will be more clear, it will be easier to refine.
The more each of us working with the code, the more comes the understanding that the present level of language - object-oriented - this can be done. And keeps us in compliance with the principle of sole responsibility of such a fact as crosscutting.
This article is about how to get rid of the duplication through the code, and how to make it a little better by using AOP.
With a probability of about 95% in any application, you can find pieces of crosscutting concerns , which are hidden in the code under the guise of caching, logging, exception handling, transaction control and allocation of access rights. As you can guess from the name, this functionality lives on all levels of the application (you do have layers?) And forces us to violate several important principles: DRY and KISS . Violating the principle of DRY, you automatically begin to use the principle of WET and the code becomes "wet", which is reflected in the increase of metrics Lines of Code (LOC), Weighted Method Count (WMC), Cyclomatic Complexity (CCN).
Let's see how it does in real life. Comes technical requirements, it is designed for system decomposition is conducted in classes and describes the necessary methods. At this point, the system is perfect, it is clear the purpose of each class and the service is simple and logical. And then crosscutting begins to dictate their own rules and forces the programmer to make changes to the code of all classes, as the PLO is not possible to carry out decomposition of the cross-cutting functionality. This process goes unnoticed, because all used to it, like a normal phenomenon, and no one is trying to fix something. The process follows the standard procedure, proven for years.
First written logic of the method that contains the necessary and sufficient implementation:
... Then add 3 more lines of code to check the access rights
... Then 2 more lines to log the start and end of the method
Know your code? Not yet? Then let's throw in another 5 lines of treatment with the possible exception of several different handlers. In the methods that return data, you can add another 5 lines to save the result in the cache. Thus, of the four lines of code that actually has value, it can turn about 20 lines of code. Than it threatens, I think it is clear - the method becomes more complicated, it is harder to read, longer have to deal with the fact that he really does, more difficult to test, because you have to slip Moki logger, cache, etc. Since the example was for one of the methods, it is logical to assume that the allegations are true about the size of a method to the class, and for the system as a whole. The older the system code, the more it accumulates like rubbish and getting tougher to watch him.
Let's look at the existing address crosscutting.
Title I recommend reading in the context of application development as follows: "Clean code - the guarantee of health applications! Health applications first! ". It would be nice to hang a sign in front of every designer is to always keep this in mind :)
So, we decided to include code in every way clean. What solutions we have and that we can use?
Decorators - the first thing that comes to mind. Decorator - structural design pattern, designed for dynamically attaching additional behavior to an object. Decorator pattern provides a flexible alternative to subclassing practice in order to enhance functionality.
When it comes to AOP , the first question usually asked OOP programmers - why not use a normal decorator? And rightly so! Because the designer can do almost everything that is done with AOP, but ... The counter-example is that if we make over LoggingDecorator CachingDecorator, and the latter, in turn, on top of the base class? How much the same type of code will be in these decorators? How many different classes of decorators will be in the whole system?
It is easy to figure out that if we have 100 classes that implement the interface 100, the addition of caching decorators we add in another 100 classes. Of course, this is not a problem in the world today (look in the cache folder of any large framework), but why do we need these 100 similar classes? It is not clear, you see?
However, the moderate use of decorators fully justified.
Proxy classes - the second thing that comes to mind. Proxy - a design pattern provides an object that controls access to another object, intercepting all calls (acts as a container).
Not a good solution from my point of view, but all the developers at the hearing caching proxy, so are so often found in applications. The main drawbacks: the fall speed (used __ call, __ get, __ callStatic, call_user_func_array), as well as breaks tayphinting, because instead of a real object comes proxy. If you try to wrap a proxy cache logs over, and that, in turn, on top of the base class, then the rate will drop considerably.
But there is a plus: in the case of the 100 classes, we can write a single caching proxy for all classes. But! Price of renouncing the tayphintinga 100 interface that is categorically unacceptable in the development of modern applications.
It's hard not to remember such a great pattern, as an observer. Observer (Observer) - a behavioral design pattern. Also known as "slave» (Dependents), «Publisher-Subscriber» (Publisher-Subscriber).
Many well-known frameworks developers face-through functionality and the need to eventually extend the logic of a particular method. Have tried a lot of ideas, and one of the most successful and clear was the model of events and subscriptions to these events. Adding or deleting subscribers to the event, we can extend the logic of the basic method, and change their order by priority - perform logic handlers in order. Not bad, almost AOP!
It should be noted that this is the most flexible pattern, since it is based, you can design a system that will grow very easily and is easy to understand. If it were not for AOP, it would be the best way to extend the logic of methods, without changing the source code. Not surprisingly, many frameworks use events to add functionality, such as ZF2, Symfony2. The site Symfony2 is an excellent article on how you can extend the logic of the method, without using inheritance.
However, despite all the advantages, there are some big drawbacks that sometimes outweigh the advantages. The first disadvantage is that you need to know how, what and where can expand your system. Unfortunately, this is often not known. The second disadvantage is that you need to write code in a special way, adding a wildcard string event generation and processing (example of Symfony2):
This pattern is , in essence, is the implementation of the Observer pattern, but reduces the number of repetitive code.
Of the most interesting implementations of this pattern, I would point out the core framework Lithium, the study of which can give a lot, even to advanced developers. In short, Lithium can hang the filter function callback to any important methods in the system and perform additional processing. Would you like to write queries to the log file in debug mode - could not be easier:
Urged to read the filter system , because the implementation of filters in the development of Lithium as close to the aspect-oriented programming and can be for you to push those who will enter the world of AOP final.
So we come to the most interesting part - the use of aspect-oriented programming to address through code duplication. Habré were articles on the ANC , including for PHP , so I will not repeat this material and give definitions of those terms and the techniques used by the AOP. If you are not familiar with the terminology and concepts of AOP, before further reading can be found with an article about AOP on Wikipedia.
Thus, the filters in Lithium can activate additional processors almost anywhere, making it possible to make the code caching, logging, access checks into separate circuits. It would seem that it's a silver bullet. But things are not so smooth. First, for the use of filters, we need to connect the whole framework as a separate library for this is not a pity. Second, filter circuit (in terms of AOP - councils) are scattered everywhere, and for them it is very difficult to follow. Third, the code must be written in a certain way and to implement specific interfaces that you can use filters. These three minus significantly limit the ability to use filters as the AOP in other applications and frameworks.
Here I had the idea - to write a library that would make it possible to use AOP in any application on PHP. Then there was the battle with PHP, learning techniques of acceleration code, fighting bugs uskoritieley opcode-and a lot of interesting things. As a result, library born Go! AOP PHP, which can integrate into an existing application, to intercept the methods available in all classes and learn from their cross-cutting a few thousand lines of code in a couple of dozen lines of advice.
The main differences from all existing analogue - a library that does not require any extensions of PHP, not to call on black magic runkit-a and php-aop. It does not use eval-s, not tied to the DI-container does not need a separate compiler considerations in the final code. Aspects are regular classes, organically using all possible PLO. The generated code library with interwoven aspects - very clean, it can be easy to debug with XDebug-a, and as the classes themselves, as well as aspects.
The most valuable thing in this library is that it is theoretically possible to connect to any application, because to add new functionality with AOP does not need to change the application code at all aspects of dynamically intertwined. For example: with ten to twenty lines of code, you can catch all the public, protected and static methods in all classes at startup ZF2-standard applications, and output in the call to the screen name of the method and its parameters.
Issues like dealing with opcodes-kesherem - in combat mode entanglement aspect occurs only once, and then pulled out of the code-keshera opcode. Doctrine-use annotations for classes aspects. In general, a lot of interesting things inside.
To rekindle interest in the ANC more, I decided to choose an interesting topic about which little information can be found - refactoring to aspects. Next will be two examples of how you can make your code cleaner and easier to use aspects.
So let's pretend that we have all carried out logging of public methods in 20 classes in neymspeyse Acme. It looks like this:
Let's take this code and refactor with issues! It is easy to see that logging is performed before the code of the method, so I choose the type of board - Before. Next we need to determine the point of introduction - that all public methods within neymspeysa Acme. It is usually given by execution (public Acme \ * -> * ()). So write LoggingAspect:
Nothing complicated, regular class with an ordinary-looking method. However, this is - an aspect that determines the board beforeMethodExecution, which will be called before the call that we need methods. As you can see, Go! uses annotations to store the metadata that has already become a common practice, as it is clear and safe. Now we can register our core aspect of Go! and throw heaps of logging all of our classes! Removing unnecessary dependency logger, we made our class code cleaner, it has become more common to observe the principle of responsibility, because we have learned from him what he should do.
Moreover, now we can easily change the format of logging, because now it is defined in one place.
I think we all know the boilerplate code method using caching:
Sure, everyone will know that template code, as such places always enough. If we have a great system, then these methods can be very much, I wish to make sure that they are cached. What an idea! Let's mark the abstract methods that should be cached, and pointkate define condition - all methods marked with a specific annotation. Since caching "wrap" method code, then we need a suitable type of advice - Around, most powerful. This type of board he makes a decision to run the source code of the method. And then it's simple:
This tip is most interesting - call the original method, which is carried by a call to proceed () the object MethodInvocation, containing information about the current method. It is easy to see that if we have the data in the cache, we do not make the call the original method. In this case, your code is not changed in any way!
With this aspect, we can deliver to any method of annotation Annotation \ Cacheable and this method will be cached by the AOP automatically. We pass on all methods and cut logic caching, replacing it with an annotation. Now boilerplate code method using caching looks simple and elegant:
This example can also be found inside the library folder demos Go! AOP PHP, and also look at the commit that implements the above in action.
Aspect-oriented programming - a relatively new paradigm for PHP, but it has a great future. Development of meta-programming, writing Enterprise-frameworks in PHP - all this comes in the wake of Java, and AOP in Java has been living for a long time, so you need to prepare for the ANC now.
Go! AOP PHP - one of the few libraries that works with the PLO and in some ways it apart from similar - to intercept static methods, methods in final classes, access properties of objects, the ability to debug the source code and code aspects. Go! uses an array of techniques to provide high performance: a compilation instead of interpretation, lack of slow techniques, optimized code performance, the use of opcode-kesher - give a contribution to the cause. One of the surprising discoveries was that Go! in some similar conditions can be faster of the extension C-PHP-AOP. Yes, yes, it is true, which has a simple explanation - an extension interfere in the work of all the methods in the PHP at runtime and makes small pointkatu compliance testing, the more of these checks, the slower each method invocation, while Go! making this one time you compile the class code without affecting the speed of the method at runtime.
If you have questions and suggestions to the library - I gladly will discuss them with you. I hope my first article on the Habré was useful to you.
The more each of us working with the code, the more comes the understanding that the present level of language - object-oriented - this can be done. And keeps us in compliance with the principle of sole responsibility of such a fact as crosscutting.
This article is about how to get rid of the duplication through the code, and how to make it a little better by using AOP.
Crosscutting or "wet" code
With a probability of about 95% in any application, you can find pieces of crosscutting concerns , which are hidden in the code under the guise of caching, logging, exception handling, transaction control and allocation of access rights. As you can guess from the name, this functionality lives on all levels of the application (you do have layers?) And forces us to violate several important principles: DRY and KISS . Violating the principle of DRY, you automatically begin to use the principle of WET and the code becomes "wet", which is reflected in the increase of metrics Lines of Code (LOC), Weighted Method Count (WMC), Cyclomatic Complexity (CCN).
Let's see how it does in real life. Comes technical requirements, it is designed for system decomposition is conducted in classes and describes the necessary methods. At this point, the system is perfect, it is clear the purpose of each class and the service is simple and logical. And then crosscutting begins to dictate their own rules and forces the programmer to make changes to the code of all classes, as the PLO is not possible to carry out decomposition of the cross-cutting functionality. This process goes unnoticed, because all used to it, like a normal phenomenon, and no one is trying to fix something. The process follows the standard procedure, proven for years.
First written logic of the method that 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(); }
... Then add 3 more lines of code to check the 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 2 more lines to log the start 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"); }
Know your code? Not yet? Then let's throw in another 5 lines of treatment with the possible exception of several different handlers. In the methods that return data, you can add another 5 lines to save the result in the cache. Thus, of the four lines of code that actually has value, it can turn about 20 lines of code. Than it threatens, I think it is clear - the method becomes more complicated, it is harder to read, longer have to deal with the fact that he really does, more difficult to test, because you have to slip Moki logger, cache, etc. Since the example was for one of the methods, it is logical to assume that the allegations are true about the size of a method to the class, and for the system as a whole. The older the system code, the more it accumulates like rubbish and getting tougher to watch him.
Let's look at the existing address crosscutting.
Clean - the guarantee of health! Health comes first!
Title I recommend reading in the context of application development as follows: "Clean code - the guarantee of health applications! Health applications first! ". It would be nice to hang a sign in front of every designer is to always keep this in mind :)
So, we decided to include code in every way clean. What solutions we have and that we can use?
Decorators
Decorators - the first thing that comes to mind. Decorator - structural design pattern, designed for dynamically attaching additional behavior to an object. Decorator pattern provides a flexible alternative to subclassing practice in order to enhance functionality.
When it comes to AOP , the first question usually asked OOP programmers - why not use a normal decorator? And rightly so! Because the designer can do almost everything that is done with AOP, but ... The counter-example is that if we make over LoggingDecorator CachingDecorator, and the latter, in turn, on top of the base class? How much the same type of code will be in these decorators? How many different classes of decorators will be in the whole system?
It is easy to figure out that if we have 100 classes that implement the interface 100, the addition of caching decorators we add in another 100 classes. Of course, this is not a problem in the world today (look in the cache folder of any large framework), but why do we need these 100 similar classes? It is not clear, you see?
However, the moderate use of decorators fully justified.
Proxy classes
Proxy classes - the second thing that comes to mind. Proxy - a design pattern provides an object that controls access to another object, intercepting all calls (acts as a container).
Not a good solution from my point of view, but all the developers at the hearing caching proxy, so are so often found in applications. The main drawbacks: the fall speed (used __ call, __ get, __ callStatic, call_user_func_array), as well as breaks tayphinting, because instead of a real object comes proxy. If you try to wrap a proxy cache logs over, and that, in turn, on top of the base class, then the rate will drop considerably.
But there is a plus: in the case of the 100 classes, we can write a single caching proxy for all classes. But! Price of renouncing the tayphintinga 100 interface that is categorically unacceptable in the development of modern applications.
Events and Observer pattern
It's hard not to remember such a great pattern, as an observer. Observer (Observer) - a behavioral design pattern. Also known as "slave» (Dependents), «Publisher-Subscriber» (Publisher-Subscriber).
Many well-known frameworks developers face-through functionality and the need to eventually extend the logic of a particular method. Have tried a lot of ideas, and one of the most successful and clear was the model of events and subscriptions to these events. Adding or deleting subscribers to the event, we can extend the logic of the basic method, and change their order by priority - perform logic handlers in order. Not bad, almost AOP!
It should be noted that this is the most flexible pattern, since it is based, you can design a system that will grow very easily and is easy to understand. If it were not for AOP, it would be the best way to extend the logic of methods, without changing the source code. Not surprisingly, many frameworks use events to add functionality, such as ZF2, Symfony2. The site Symfony2 is an excellent article on how you can extend the logic of the method, without using inheritance.
However, despite all the advantages, there are some big drawbacks that sometimes outweigh the advantages. The first disadvantage is that you need to know how, what and where can expand your system. Unfortunately, this is often not known. The second disadvantage is that you need to write code in a special way, adding a wildcard string event generation and processing (example of 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 is , in essence, is the implementation of the Observer pattern, but reduces the number of repetitive code.
Of the most interesting implementations of this pattern, I would point out the core framework Lithium, the study of which can give a lot, even to advanced developers. In short, Lithium can hang the filter function callback to any important methods in the system and perform additional processing. Would you like to write queries to the log file in debug mode - could not be 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); });
Urged to read the filter system , because the implementation of filters in the development of Lithium as close to the aspect-oriented programming and can be for you to push those who will enter the world of AOP final.
Aspect-Oriented Programming
So we come to the most interesting part - the use of aspect-oriented programming to address through code duplication. Habré were articles on the ANC , including for PHP , so I will not repeat this material and give definitions of those terms and the techniques used by the AOP. If you are not familiar with the terminology and concepts of AOP, before further reading can be found with an article about AOP on Wikipedia.
Thus, the filters in Lithium can activate additional processors almost anywhere, making it possible to make the code caching, logging, access checks into separate circuits. It would seem that it's a silver bullet. But things are not so smooth. First, for the use of filters, we need to connect the whole framework as a separate library for this is not a pity. Second, filter circuit (in terms of AOP - councils) are scattered everywhere, and for them it is very difficult to follow. Third, the code must be written in a certain way and to implement specific interfaces that you can use filters. These three minus significantly limit the ability to use filters as the AOP in other applications and frameworks.
Here I had the idea - to write a library that would make it possible to use AOP in any application on PHP. Then there was the battle with PHP, learning techniques of acceleration code, fighting bugs uskoritieley opcode-and a lot of interesting things. As a result, library born Go! AOP PHP, which can integrate into an existing application, to intercept the methods available in all classes and learn from their cross-cutting a few thousand lines of code in a couple of dozen lines of advice.
Library Go! AOP PHP
The main differences from all existing analogue - a library that does not require any extensions of PHP, not to call on black magic runkit-a and php-aop. It does not use eval-s, not tied to the DI-container does not need a separate compiler considerations in the final code. Aspects are regular classes, organically using all possible PLO. The generated code library with interwoven aspects - very clean, it can be easy to debug with XDebug-a, and as the classes themselves, as well as aspects.
The most valuable thing in this library is that it is theoretically possible to connect to any application, because to add new functionality with AOP does not need to change the application code at all aspects of dynamically intertwined. For example: with ten to twenty lines of code, you can catch all the public, protected and static methods in all classes at startup ZF2-standard applications, and output in the call to the screen name of the method and its parameters.
Issues like dealing with opcodes-kesherem - in combat mode entanglement aspect occurs only once, and then pulled out of the code-keshera opcode. Doctrine-use annotations for classes aspects. In general, a lot of interesting things inside.
Refactoring through code using AOP
To rekindle interest in the ANC more, I decided to choose an interesting topic about which little information can be found - refactoring to aspects. Next will be two examples of how you can make your code cleaner and easier to use aspects.
Take out of the logging code
So let's pretend that we have all carried out logging of public methods in 20 classes in neymspeyse Acme. It looks like this:
namespace Acme; class Controller { public function updateData($arg1, $arg2) { $this->logger->info("Executing method " . __METHOD__, func_get_args()); // ... } }
Let's take this code and refactor with issues! It is easy to see that logging is performed before the code of the method, so I choose the type of board - Before. Next we need to determine the point of introduction - that all public methods within neymspeysa Acme. It is usually given by execution (public Acme \ * -> * ()). So 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, regular class with an ordinary-looking method. However, this is - an aspect that determines the board beforeMethodExecution, which will be called before the call that we need methods. As you can see, Go! uses annotations to store the metadata that has already become a common practice, as it is clear and safe. Now we can register our core aspect of Go! and throw heaps of logging all of our classes! Removing unnecessary dependency logger, we made our class code cleaner, it has become more common to observe the principle of responsibility, because we have learned from him what he should do.
Moreover, now we can easily change the format of logging, because now it is defined in one place.
Transparent caching
I think we all know the boilerplate code method using caching:
/** ... */ public function cachedMethod() { $key = __METHOD__; $result = $this->cache->get($key, $success); if (!$success) { $result = // ... $this->cache->set($key, $result); } return $result; }
Sure, everyone will know that template code, as such places always enough. If we have a great system, then these methods can be very much, I wish to make sure that they are cached. What an idea! Let's mark the abstract methods that should be cached, and pointkate define condition - all methods marked with a specific annotation. Since caching "wrap" method code, then we need a suitable type of advice - Around, most powerful. This type of board he makes a decision to run the source code of the method. And then it's 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]; } }
This tip is most interesting - call the original method, which is carried by a call to proceed () the object MethodInvocation, containing information about the current method. It is easy to see that if we have the data in the cache, we do not make the call the original method. In this case, your code is not changed in any way!
With this aspect, we can deliver to any method of annotation Annotation \ Cacheable and this method will be cached by the AOP automatically. We pass on all methods and cut logic caching, replacing it with an annotation. Now boilerplate code method using caching looks simple and elegant:
/** * @Cacheable */ public function cachedMethod() { $result = // ... return $result; }
This example can also be found inside the library folder demos Go! AOP PHP, and also look at the commit that implements the above in action.
Conclusion
Aspect-oriented programming - a relatively new paradigm for PHP, but it has a great future. Development of meta-programming, writing Enterprise-frameworks in PHP - all this comes in the wake of Java, and AOP in Java has been living for a long time, so you need to prepare for the ANC now.
Go! AOP PHP - one of the few libraries that works with the PLO and in some ways it apart from similar - to intercept static methods, methods in final classes, access properties of objects, the ability to debug the source code and code aspects. Go! uses an array of techniques to provide high performance: a compilation instead of interpretation, lack of slow techniques, optimized code performance, the use of opcode-kesher - give a contribution to the cause. One of the surprising discoveries was that Go! in some similar conditions can be faster of the extension C-PHP-AOP. Yes, yes, it is true, which has a simple explanation - an extension interfere in the work of all the methods in the PHP at runtime and makes small pointkatu compliance testing, the more of these checks, the slower each method invocation, while Go! making this one time you compile the class code without affecting the speed of the method at runtime.
If you have questions and suggestions to the library - I gladly will discuss them with you. I hope my first article on the Habré was useful to you.
0 commentaires:
Enregistrer un commentaire