One evening, in order to implement the behavior patterns in the ORM in my
bike framework, I needed something that behaves like a mixin in Ruby or an extension method in C # (or as a treit / graft in future versions of PHP) I decided for interest to see how I can implement the impurities in PHP. If you do not know what an impurity is, it does not matter, now I'll tell you everything.
I invite you to follow me in the arguments about the implementation of impurities in PHP and programming a small library that allows them to be implemented. The article is focused on PHP developers, beginner and intermediate level (the main thing is that you are well oriented in OOP). In the process, I will also make a small mistake regarding the intricacies of working with PHP 5.3 with classes, I will point out to it over time and offer to correct it. And also give my solution to your criticism. Enjoy reading.
')
What is an impurity?
Impurity is a class that simply provides its methods and properties to other classes. We can assume that adding other classes to the class is simply a variant of emulation of multiple inheritance, which is not implemented in PHP. I will give a small example in pseudocode, syntax-like in PHP for clarity:
<? php
mixin Timeable {
private $ timeStarted;
private $ timeStopped;
public function start () {$ timeStarted = time (); }
public function stop () {$ timeStopped = time (); }
public function getElapsed () {return $ timeStopped - $ timeStarted}
}
mixin Dumpable {
public function dump () {var_dump ($ this); }
}
class MyClass mixing Timeable, Dumpable {
public function hello () {}
}
$ a = new MyClass ();
$ a-> start ();
sleep (250);
$ a-> stop ();
echo $ a-> getElapsed ();
$ a-> dump ();
?>
Is the idea clear? Impurities simply add their functionality to the class, as if the class is inherited immediately from them all. In doing so, they can manipulate the members of the class in which they are mixed. Here we will implement such functionality in PHP.
Let's set ourselves a task.
- We need to implement the ability to add a functional from the indicated classes of impurities to instances of given classes.
- Impurity classes should not be loaded before the class in which they are mixed. The example above uses pseudo-syntax, which allowed us to define impurity classes directly in the class declaration. But this method has its drawbacks. What if, in the course of the program, we need to add plugins that will act as impurities to the classes of our system? In this case, we could declare all impurities somewhere in the initialization script and it is important for us that such an announcement does not lead to the loading of classes.
- If an admixture is mixed into a class, this means that its functionality must be available in the descendant class of this class. Nevertheless, we use object-oriented language and it will be logical.
- When implementing it is desirable to take into account that the use of members of the classes of impurities should not be too inhibitory, especially if the system will use a lot of impurities.
- Modification of existing classes for the use of impurities should not require redesign of the existing system. As a consequence, this means that there must be another possibility, in addition to inheriting from an abstract class, in order to teach the class to add functionality from other classes.
- Public properties and methods of impurities should be available through an instance of the host class (hereinafter I will call it “aggregator”, since it can aggregate several impurities in itself). And private and protected should be visible only to the impurity itself.
- An impurity should be able to address even the hidden and protected fields of its class-aggregator (when setting such a demand, I was guided by Ruby, in which there are no hidden and protected properties in the sense that they are in C ++, PHP or C #. There are addresses from everywhere It is possible to any class fields. But, since an impurity can add new behavior, it may need protected information from the aggregator class).
Design the registry.
Let's think about it. We may want to add different impurities to different classes of the system. That is, somewhere we must store information about which classes which impurities are mixed into. Such information for the project is global and should be accessible from everywhere. Therefore, to implement such a repository, I chose a static class (In PHP, there are no static classes as they exist in C #. By static class, I mean a class that does not need to be instantiated. All its functionality will be implemented by static methods accessible through the name class). As a small task, I suggest (if you are interested, after you finish reading the article to the end) to redesign the registry so that the use of a singleton is not required.
It follows from the above that the registry should be able to register impurities for aggregator classes. And a little higher we said that if we register an impurity for some class, then the functional of this impurity should be mixed into all descendant classes. We cannot get a list of ancestor classes right at the time of registration (after all, we need to avoid loading classes, and the inspection of the class hierarchy will require this). From this it follows that we will build the list of correspondences (class => list of impurities) when it is really needed. In addition, such a list will need to be cached so that when creating new instances of aggregator classes, it will not be rebuilt.
class Registry { private static $registeredMixins = array(); public static function register($className, $mixinClassName) { $mixinClassNames = func_get_args(); unset($mixinClassNames[0]); foreach ($mixinClassNames as $mixinClassName) { self::$registeredMixins[$className][] = $mixinClassName; } self::$classNameToMixinCache = array(); } }
The registration function turned out pretty simple. We give it the name of the aggregator class and a list of impurities for it. The list of impurities for convenience can be specified through a comma. Func_get_args () will take care of this (add elegant support for specifying the list of impurities with an array if you're interested). Then we simply add each impurity to the list of impurities for this class. And the last call at the end of the function clears the cache, since registering an impurity for a given class will add it also to all its descendants, which will require rebuilding the cache.
Now let's write the caching function. It should go through the list of classes and impurities registered for them and add to it all descendant classes given with the same list of impurities. The result is a cache.
For the caching function, we need a function that retrieves the list of ancestors of this class:
private static $classNameToMixinCache = array(); private static function getAncestors($className) { $classes = array($className); while (($className = get_parent_class($className)) !== false) { $classes[] = $className; } return $classes; } private static function precacheMixinListForClass($className) { if (isset(self::$classNameToMixinCache[$className])) { return; } $ancestors = self::getAncestors($className); $result = array(); foreach ($ancestors as $ancestor) { if (isset(self::$registeredMixins[$ancestor])) { $result = array_merge($result, self::$registeredMixins[$ancestor]); } } self::$classNameToMixinCache[$className] = array_unique($result); }
Please note that the function adds to the cache a list of impurities only for the specified class. We will not immediately build the entire cache, since most of its contents may never be needed. Before building a cache, we checked if we had already done it before.
Now, if we need to get a list of impurities for a given class, we can use this function:
public static function getMixinsFor($className) { self::precacheMixinListForClass($className); return self::$classNameToMixinCache[$className]; }
Go to the next step. Imagine that we are calling a method of an aggregator class that is defined not in it, but in one of the impurities. What do we need to do? We need to get a list of impurities of this class, then go over them and see if there is a method that we need is not defined in any of them.
Since the impurity is the essence of the class, we do something like this:
private static $methodLookupCache = array(); public static function getMixinNameByMethodName($className, $methodName) { if (isset(self::$methodLookupCache[$className][$methodName])) { return self::$methodLookupCache[$className][$methodName]; } self::precacheMixinListForClass($className); foreach (self::$classNameToMixinCache[$className] as $mixin) { if (method_exists($mixin, $methodName)) { self::$methodLookupCache[$className][$methodName] = $mixin; return $mixin; } } throw new MemberNotFoundException("$className has no mixed method $methodName()!"); }
That is, if there is already an entry in the cache for this class and the name of the method, we simply return them. If not, we get a list of impurities for this class from our cache, go around them and check if any method we need implements. If yes, add it to the cache and return the name of the impurity. If nothing was found, we throw an exception.
Exactly the same option is obtained for properties. I suggest you write it yourself.
That's all. The registry we have implemented. We proceed to programming the impurity class.
We program an impurity.
So, admixture. And what admixture? Impurity is the usual class. He just knows how to work with fields of another class. And it will be logical to pass an instance of this other class to it in the constructor.
class Base { protected $_owningClassInstance; protected $_owningClassName; public function __construct($owningClassInstance) { $this->_owningClassInstance = $owningClassInstance; $this->_owningClassName = get_class($owningClassInstance); } }
I called the base impurity class Base simply because in my project it belongs to the Mixins namespace and it is not specifically required to call it. But you can name it as you wish.
We can work with public fields and methods directly through the variable owningClassInstance. But with hidden and protected will have to work through the reflection. Nothing complicated. I give all the definitions of functions:
protected $_owningPropertyReflectionCache; protected $_owningMethodReflectionCache; protected function getProtected($name) { if (! isset($this->_owningPropertyReflectionCache[$name])) { $property = new \ReflectionProperty($this->_owningClassName, $name); $property->setAccessible(true); $this->_owningPropertyReflectionCache[$name] = $property; } return $this->_owningPropertyReflectionCache[$name]->getValue($this->_owningClassInstance); } protected function setProtected($name, $value) { if (! isset($this->_owningPropertyReflectionCache[$name])) { $property = new \ReflectionProperty($this->_owningClassName, $name); $property->setAccessible(true); $this->_owningPropertyReflectionCache[$name] = $property; } $this->_owningPropertyReflectionCache[$name]->setValue($this->_owningClassInstance, $value); } protected function invokeProtected($name, $parameters) { $method = new \ReflectionMethod($this->_owningClassName, $name); $method->setAccessible(true); $parameters = func_get_args(); unset($parameters[0]); $method->invokeArgs($this->_owningClassInstance, $parameters); }
Please note that here I again activated caching in order not to constantly create and not configure instances of the system classes for the reflection operation. To reduce memory consumption, you can refuse caching, if necessary.
Someone may have already noticed that the method_exists () and property_exists () functions that we used in the registry class check for the presence of implicit and hidden and protected functions with the given name, along with the public ones. This leads to the fact that the aggregator class will “try” to call a function with that name if it is defined as hidden or protected. As a result, we still get an error, but I prefer to do it explicitly:
public function __call($name, array $arguments) { throw new MemberNotFoundException( "Method $name is not defined or is not accessible in mixin \"" . get_class() . "\""); } public function __get($name) { throw new MemberNotFoundException( "Property $name is not defined or is not accessible in mixin \"" . get_class() . "\""); } public function __set($name, $value) { throw new MemberNotFoundException( "Property $name is not defined or is not accessible in mixin \"" . get_class() . "\""); }
As a small assignment, try to correct such unworthy behavior of our registry class. Especially since it will lead to the inability to call the public impurity method with a name that has already come across as hidden or protected in another.
Um That's all. Impurity is ready to use. The last step remains - the implementation of a platform for mixing in impurities - aggregator classes. This is what we will do now.
We write class aggregator.
What we can class aggregator? He can keep copies of classes of impurities in himself and call their methods. Well, to apply to the properties. We will implement this behavior using the "magic" methods of PHP.
class Aggregator { protected $_mixins; protected $_className; public function __construct($aggregatorClassInstance = false) { $this->_className = $aggregatorClassInstance ? get_class($aggregatorClassInstance) : get_class($this); $mixinNames = Registry::getMixinsFor($this->_className); foreach ($mixinNames as $mixinName) { $this->_mixins[$mixinName] = new $mixinName($aggregatorClassInstance ? $aggregatorClassInstance : $this); } } }
In the constructor code, we simply get a list of impurities for the class, then we loop through them and create their instances.
The $ aggregatorClassInstance variable serves to make it unnecessary for us to inherit our class from the Aggregator class. We can include the Aggregator class in another class and call its constructor with the $ aggregatorClassInstance parameter equal to an instance of this other class. Accordingly, in this case, we obtain a list of impurities for this class-owner and pass the corresponding instance of the aggregator class to the impurities.
If the explanation above seemed too complicated to you - it doesn't matter. Slide just below, there are examples. See how the Inheritance example differs from the Composition example and how they work.
We implement the "magic methods".
public function __call($name, array $arguments) { return call_user_func_array(array($this->_mixins[Registry::getMixinNameByMethodName($this->_className, $name)], $name), $arguments); } public function __get($name) { return $this->_mixins[Registry::getMixinNameByPropertyName($this->_className, $name)]->$name; } public function __set($name, $value) { $this->_mixins[Registry::getMixinNameByPropertyName($this->_className, $name)]->$name = $value; } public function __isset($name) { return isset($this->_mixins[Registry::getMixinNameByPropertyName($this->_className, $name)]->$name); }
Each of the magical methods refers to the registry for information. It's simple.
The exception class that we used looks like this:
class MemberNotFoundException extends \Exception {}
Let's look at a few examples.
First, the traditional scheme with inheritance:
class MixinAggregatorSample extends Mixins\Aggregator { } class MixinHello extends Mixins\Base { protected $inaccessible; public $text = "I am a text!\r\n"; public function hello() { echo ("Hello from mixin!\r\n"); } } Mixins\Registry::register("MixinAggregatorSample", "MixinHello"); $a = new MixinAggregatorSample(); $a->hello();
And now take a look at the inclusion scheme:
class MixinAggregatorSample { protected $_aggregator; public function __construct() { $this->_aggregator = new Mixins\Aggregator($this); } public function __call($name, $arguments) { return $this->_aggregator->__call($name, $arguments); } } class MixinHello extends Mixins\Base { public function hello() { echo ("Hellp from mixin!"); } } Mixins\Registry::register("MixinAggregatorSample", "MixinHello"); $a = new MixinAggregatorSample(); $a->hello();
See the difference? In the case of inclusion, we are free to inherit our class aggregator from any other without losing functionality. Of course, for its normal use, you will have to implement all the magic methods, and not just __call ().
Speed ​​performance
I made some measurements of the speed of the resulting library. The measurements are very approximate, carried out on a home computer with an open IDE, Winamp and everything that is necessary.
Time native: 0.57831501960754
Time byname: 1.5227220058441
Time mixed: 7.5425450801849
Time reflection: 12.221807956696
- Native - the time to directly call a class method in PHP
- Byname - the time to call a class method through the name $ myClass -> $ methodName
- Mixed - time to call the mixed method
- Reflection - the time of invoking a mixed method that changes a property of a class through Reflection. Those. = mixed + reflection.
- The time is given in seconds for 800,000 calls.
I think the figures cited are quite acceptable so that such an approach can be used in a large project. As a rule, impurity methods are not called thousands of times in a script and 10 microseconds to call a method versus 0.7 microseconds for native methods is an acceptable option. Especially if you take into account that the time spent on htmlspecialchars (), for example, on a large amount of text or on a query to the database is much higher.
Since we use almost everywhere caching based on PHP hashed arrays, as the number of impurities and aggregator classes grows, the speed should not drop much. However, if someone does the necessary tests, I will be very happy.
Epilogue
I am pleased to hear your criticism regarding this article. I am especially interested in whether I was able to make the material understandable to all readers.
Of course, this article does not claim to be complete, accurate or free from errors. I would be very grateful if you correct me. I spread the code of the library
here . Of course, the project is an educational project and before using it in a real project it is worthwhile to think well and test everything. Some problem points like several impurities with the same names of public methods in the same class are present.
If the topic of impurities in PHP you are interested in, I suggest also to go through Google.