Oliverde8's blue Website

Playing with trait's to make autoconstructs in PHP

I have recently for the first time in a while being playing with a bit of Java. One of the library on the project was lombok.

What this library allows is to generate getters & setters during compilations thanks to annotations. This is very often done in php with the magic methods (http://php.net/manual/en/language.oop5.magic.php).

What lombok also allows is to create constructors automatically, again with a simple annoation. This is great and is something I have never seen in PHP. 

Why is it practical to have constructors genetated automatically ?

When extending an existing class you will need to have in your constructor both parameters of the original class and the ones you have added. Exemple

<?php

class A {
  protect $var1;
  public function __construct($var1) {
    $this->var1 = $var1;
  }
}

 

So now if B extends A, 

<?php
class B extends A {
  protect $var2;
  public function __construct($var1, $var2) {
    parent::__construct($var1)
    $this->var2 = $var2;
  }
}

 

Now A is modified to have another `propertie` : 

<?php
class A {
  protect $var1;
  proctected $newVar;
  public function __construct($var1, $newVar) {
    $this->var1 = $var1;
    $this->newVar = $newVar;
  }
}

The class `B` won't work anymore. That is why and cahnge to constructors is condisered a `BC`. The compatibility is lost. Usually this is not much of an issue as everything using it needs to change as well, but when our classes are services this becomes an issue. 

Basically class `B` replaces class `A` for the service. With modern DI systems(Symfony3, Laravel ....) that `autowire` arguments you might not even need to change any configurations for the service. As `var2` will be automatically injected to the new constructor. This also happens if the constructor changes and `newVar` is added to it. 

So if the constructor could be generated automatically adding a new `propertie` that can be `autowired` to any `parent` class would have no impact on the children. 

All of this sounds great, but can we actually implement such a logic in PHP ?

Here is a quick implementation I did to test this out, using Doctrine Annotations. 

<?php

require_once "vendor/autoload.php";

/**
 * First class that has a propertie that should bein the constructor
 */
class A {
    /**
     * @var string
     * @AutoConstructAnnotation()
     */
    protected $var1;
}

/**
 * Second class extending first that has 1 propertie that should be in the constructor and one that shouldn't be.
 */
class B extends A{
    use AutoConstructTrait;
    
    /**
     * @var string
     * @AutoConstructAnnotation()
     */
    protected $var2;

    /** @var string  */
    protected $var3 = "";
}

/**
 * We are using a trait to add the "generic" constructor.
 */
trait AutoConstructTrait {
    public function __construct($args)
    {
        $class = get_class($this);
        $properties = AutoConstructAnnotation::getClassProperties($class);

        foreach ($properties as $field => $constrains) {
            if (isset($args[$field])) {
                // TODO validate value with constraints? using the type in @var. 
                $this->$field = $args[$field];
            } else {
                throw new \Exception("$class is expecting parameter $field");
                // TODO nice exception.
            }
        }
    }
}
/**
 * Class AutoConstructAnnotation
 *
 * @Annotation
 */
final class AutoConstructAnnotation
{
    static $classProperties = [];

    /**
     * Get properties that needs to be in the constructor of the class.
     * 
     * @return string[]
     */
    public static function getClassProperties($class)
    {
        if (!isset(self::$classProperties[$class])) {
            $properties = [];
            $reader = new \Doctrine\Common\Annotations\AnnotationReader();
            $reflection = new ReflectionClass($class);
            foreach ($reflection->getProperties() as $property) {
                $annotation = $reader->getPropertyAnnotation($property, AutoConstructAnnotation::class);
                if ($annotation) {
                    $properties[$property->name] = null;
                }
            }
            self::$classProperties[$class] = $properties;
        }
        return self::$classProperties[$class];
    }
}

new B(['var1' => "hop", "var2" => "yep"]);

This as it is works as expected.

But as we have annotated the properties that can be injected the arguments can be checked. Sot the fallowing code will throw an exception as `var2` is missing.

<?php
new B(['var1' => "hop", "var3" => "yep"]);

Having var3 in the array will have not change the actual value of the propertie, 

<?php
new B(['var1' => "hop", "var2" => "yep", "var3" => "yep"]);

So this works quite well; nevertheless there is a few issues due to the constructor taking an array in parameter.

  • we can't use the languages native type checks, we will have todo it ourself and that will impact performance ....
  • If we ever need to create an instance ourself our IDE, will not be able to help us, it will be clueless on what to expect.  

We could use the `...$args` but that does not fix our issues, and make it harder to map the attributes. If the order of the properties changes all usages of the constructor will need to change.

Nevertheless I think there is an idea here that needs to be thought on, specially for services where the constructor is anyway used by the framework and doesn't require to be "good looking".

I will certainly think a bit more on how the idea can be improved. On CMS's such as Akeneo or Magento this is a common problem and having a simplification would be great.