Symfony Entities and Traits

I’m currently working on a Symfony-based project whose data model is far from being stable and all its properties known; for this reason, three Doctrine entities have a common private property called “attributes”, declared as json_array: by mapping and converting array data based on PHP’s JSON encoding functions, I’m sure that any other additional entity properties can be added to the array, without altering database structure or application logic.

The following piece of code shows one of the entity.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
namespace AppBundle\Entity;

use Doctrine\ORM\Mapping AS ORM;

/**
 * @ORM\Entity
 * @ORM\Table()
 */
class Archetype {

  ...
      
    /**
     * @var array
     * @ORM\Column(type="json_array", nullable=true)
     */
    private $attributes;
  
  ...
}  

Of course this feature comes with a little of logic to be implemented, so that controllers and repositories can use them:

1
2
3
4
5
6
7
8
9
public function setAttribute($name, $value, $exposed = true) { ... }

public function setAttributes($attributes) { ... }

public function getAttributes() { ... }

public function getAttribute($key) { ... }

public function hasAttribute($key) { ... }

Instead of duplicating all these methods in each entity class, I found that PHP’s traits offers a good and nice tradeoff.

Traits

As of PHP 5.4.0, PHP implements a method of code reuse called Traits. Traits are a mechanism for code reuse in single inheritance languages such as PHP. A Trait is intended to reduce some limitations of single inheritance by enabling a developer to reuse sets of methods freely in several independent classes living in different class hierarchies. The semantics of the combination of Traits and classes is defined in a way which reduces complexity, and avoids the typical problems associated with multiple inheritance and Mixins.

I created a trait called “AttributeTrait” in the same namespace of my entities and implemented in it the methods listed before.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
namespace AppBundle\Entity;

trait AttributeTrait {
  
    /**
     * @var array
     * @ORM\Column(type="json_array", nullable=true)
     */
    private $attributes;

    public function setAttribute($name, $value, $exposed = true) {
        $attribute = compact('value', 'exposed');
        $this->attributes[$name] = $attribute;
        return $this;
    }
  
  ...

The nice aspect is that, the traits and the class that uses it, are merged togheter, as they were a whole class: this means that Doctrine will create a table with all the properties declared in the entity class and those belonging to the traits. The $this reference points to the container class, the entity instance in this case, so no changes to the code should be made.

Accessing the property from Twig

To complete the picture and make it more flexible even inside a Twig templates, I also implemented the special method __call, to make the code more readable.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<?php

namespace AppBundle\Entity;

trait AttributeTrait {

    /**
     * @var array
     * @ORM\Column(type="json_array", nullable=true)
     */
    private $attributes;

    public function __call($name, $arguments) {
        $attributeName =  lcfirst(str_replace('get', '', trim($name)));
        if (array_key_exists($attributeName, $this->attributes)) {
            return $this->attributes[$attributeName]['value'];
        }

        throw new \BadMethodCallException(sprintf('Product has no "%s" attribute.', $attributeName));
    }

    /**
     * Add a new attribute
     * 
     * @param string $name
     * @param mixed $value
     * @param bool $exposed
     * @return container class
     */
    public function setAttribute($name, $value, $exposed = true) {
        $attribute = compact('value', 'exposed');
        $this->attributes[$name] = $attribute;
        return $this;
    }

    /**
     * Add a set of attributes
     * 
     * @param array $attributes
     * @return container class
     */
    public function setAttributes($attributes) {
        $this->attributes = $attributes;
        return $this;
    }

    /**
     * Get all the attributes
     * 
     * @return array|null
     */
    public function getAttributes() {
        return $this->attributes;
    }

    /**
     * Get a single attribute corresponding to the given key
     * 
     * @see __call
     * @param string $key
     * @return mixed
     */
    public function getAttribute($key) {
        return $this->attributes[$key]['value'];
    }

    /**
     * Returns true if instance has the given attribute set
     * 
     * @param string $key
     * @return boolean
     */
    public function hasAttribute($key) {
        return array_key_exists($key, $this->attributes);
    }

}

From within a Twig template, I could use the typical syntax instance.property.

For example:

1
2
3
4
5
<?php

$archetype = new Archetype();
$archetype->setAttribute('width', 25.4);
echo $archetype->getWidth(); // this will invoke __call

From within a Twig template:

1
<p>Width: {{ archetype.width }}</p>

The width property doesn’t exist in class Archetype, but was added as dynamic property with trait support; Twig will call the corresponding method getWidth which is intercepted from special method __call and converted in an attribute access.

Comments