📜 ⬆️ ⬇️

Dynamic Impurities in PHP

Starting from version 5.4.0, a new language design will appear in PHP - traits , which allows the use of impurities ( mix in ). The impurity mechanism is another code reuse mechanism and is present in one form or another in other languages, such as Ruby, Python, Common Lisp, etc.

Impurities allow you to use existing code when implementing the behavior of classes, while avoiding the many problems of multiple inheritance. The impurities interacting with each other, combined into separate software libraries, are a very powerful tool for implementing complex logic.

It should be noted that implementations of impurities in PHP exist at least from version 4.0.1, and are currently present, most often under the name of behavior, in a number of popular frameworks, for example, in Yii, Symfony, Doctrine, CakePhp, Propel.
')
The purpose of the article is to demonstrate and compare several basic approaches to the implementation of impurities in PHP up to version 5.4.0, based only on the functions of the language itself and not using third-party extensions, such as, for example, the runkit_method_copy function from the PECL runkit.

When comparing, the following criteria will be used:

Method One: Magic methods


The method is based on the idea of ​​using magic methods __call , __get , and others: a collection of impurities is added to the result of mixing, and the implementation of magic methods selects the necessary impurity. Each impurity can be parametrized by reference to the result, thus supporting the interaction of impurities with each other.

Example of implementation:

abstract class Mixin {     protected $mixedObject = null;     public function setObject( MixedObject $object )     {         $this->mixedObject = $object;     }     abstract public function getName(); } class MixedObject {     private $mixins = array();     public function addMixin( Mixin $mixin )     {         $mixin->setObject( $this );         $this->mixins[$mixin->getName()] = $mixin;     }     public function hasMixin( $mixinName )     {         return array_key_exists( $mixinName, $this->mixins );     }     public function __call( $name, $arguments )     {         foreach ($this->mixins as $mixin) {            if (is_callable( array( $mixin, $name ) )) {                return call_user_func_array( array( $mixin, $name ), $arguments );            }        }        throw new \Exception('Unknown method call.');     } } 

Usage example:

 class Foo extends MixedObject {    public function objectFunc()    {        return 'FooName';    } } class Debuggable extends Mixin {    public function getName()    {        return 'Debug';    }    public function getDebug()    {        return sprintf( "%s", $this->mixedObject->objectFunc() );    } } class Loggable extends Mixin {    public function getName()    {        return 'Log';    }    public function getLog( $level )    {        return $this->mixedObject->hasMixin( 'Debug' )            ? sprintf( "%s %s", $level, $this->mixedObject->getDebug() )            : sprintf( "%s", $level );    } } $foo = new Foo(); $foo->addMixin( new Debuggable() ); $foo->addMixin( new Loggable() ); print $foo->getDebug(); print $foo->getLog( 'info' ); 

Obviously, the result is of the same type as the object itself. This approach also leaves it possible for impurities to communicate with both the object itself and with each other using the $ this-> mixedObject link and the unique name system.

Advantages and disadvantages:

Method two: Object context


This method is based on some feature of the $ this variable. Namely:
This is a reference to the secondary object .

Highlighted words enable the following implementation:

 class Foo {    public function objectFunc()    {        return 'FooName';    } } class Debuggable {    public function getDebug()    {        return sprintf( "%s", $this->objectFunc() );    } } class Loggable {    public function getLog( $level )    {        return is_callable( array( $this, 'getDebug' ) )            ? sprintf( "%s %s", $level, $this->getDebug() )            : sprintf( "%s", $level );    } } 

... and use:

 class MixedFoo extends Foo {    public function getDebug()    {        return Debuggable::getDebug();    }    public function getLog()    {        return Loggable::getLog( func_get_arg( 0 ) );    } } $foo = new MixedFoo(); $foo->getDebug(); $foo->getLog( 'info' ); 

Next, it is easy to automate the generation of the code for the MixedFoo class, the subsequent eval , the creation of the object of the generated class and its return, resulting in something like the following:

 $foo = Mixer::Construct( 'Foo', array( 'Debuggable', 'Loggable' ) ); $foo->getDebug(); $foo->getLog( 'info' ); 

You can also make a separate interface for each impurity and add it to the list of implements for the generated class.

 interface IMixinDebuggable {    public function getDebug(); } ... $foo = Mixer::Construct( 'Foo', array( 'IMixinDebuggable' => 'Debuggable', 'Loggable' ) ); 

This is possible, since the result of mixing will implement these interfaces, and checking for the existence of an impurity will then be reduced to the instanceof native call:

 class Loggable {    public function getLog( $level )    {        return $this instanceof IMixinDebuggable            ? sprintf( "%s %s", $level, $this->getDebug() )            : sprintf( "%s", $level );    } } 

Advantages and disadvantages:

Conclusion


Both methods allow you to dynamically refine the behavior of classes, complementing them with the existing implementation, and have the right to use.

If we put the results in a separate table:
Magic methodsObject context
whether the mix result is of the same type as the object itselfYesYes
can one impurity interact with anotherYesYes
is it possible to check that the result of mixing has some impurityYesYes
is it possible to add impurity to an arbitrary classNotYes
is it possible to add an admixture to an already created object “on the fly”YesNot
how simple is the implementationSimple and obviousAssociated with code generation

It is easy to see that the shortcomings of the first method are solved by the second, as well as vice versa, and also to draw a conclusion regarding the scope of one or another method.

The first is part of the project design, and therefore its scope is the task of designing complex business objects.

The second allows you to select impurities in separate independent libraries and apply them in any projects, so its scope is lightweight tasks specific to a number of your projects.

Thank.

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


All Articles