This forum is in READ-ONLY mode.
You can look around, but if you want to ask a new question, please use Stack Overflow.

Why the element of ClassMetadata::members is an array?

New topics about Symfony 2 should go here

Why the element of ClassMetadata::members is an array?

by zerustech » Mon Jun 08, 2015 9:57 am

I am reviewing the code base of symfony/validator component and I have a question about the class of Symfony\Component\Validator\Mapping\ClassMetadata.

According to the code base, each element of its $member property is an array:
Code: Select all
class ClassMetadata extends ElementMetadata implements ClassMetadataInterface
{
    // ...
    private function addPropertyMetadata(PropertyMetadataInterface $metadata)
    {
         $property = $metadata->getPropertyName();
         $this->members[$property][] = $metadata;
    }
    // ...
}


But in fact, at anytime, there could be only one PropertyMetadata instance at most for each class property, so what is the point to make each element of the $members property an array?

If a property has multiple constraints defined both in the subclass and its parent class(es), the constraints of the top-most parent class will be processed first, thus only one PropertyMetadata instance will be inserted into its $members property ($members[<name>][]=<the instance>) . And at the time when merging the constraints into subclass(es), the subclass's $members property are still empty, thus the PropertyMetadata instance from the parent class will be inserted into subclass's $members property ($members[<name>][]=<the instance>) as well, and again, there is still only one element for each property.

Let's go through this by an example. Assume we have two classes: Person and Author as follows:

Code: Select all
use Symfony\Component\Validator\Constraints as Assert;

class Person
{
    /**
     * @Assert\NotBlank()
    */
    public $name;
}



Code: Select all
use Symfony\Component\Validator\Constraints as Assert;

class Author extends Person
{
     /**
       * @Assert\Length(
       * min = 3,
       * max = 50,
       * )
     */
     public $name;
}


When validating an Author object, its $members property shall be initialized as follows:
    1. The ClassMetadata instance of its parent class - Person, will be created first.

    2. The data of Person's $member property will be as follows:
    Code: Select all
    array (
       'name' => array (
            <the PropertyMetadata> // only the NotBlank constraint is included
       )
    )


    3. At the time when merging Person's $member property, the $member property of Author class is still empty, because the meta data of Author class has not been loaded:
    Code: Select all
    array()


    4. After merging the Person's $member property, the $member property of Author class will be as follows:
    Code: Select all
    array (
       'name' => array (
            <the PropertyMetadata> // only the NotBlank constraint is included.
       )
    )


    5. Now load the meta data of Author class, but it will not create another element in $member['name']. Instead, the constraints will be added to the existing PropertyMetadata instance:
    Code: Select all
    array(
       'name' => array(
           <the PropertyMetadata> // now it includes two constraints: the NotBlank and the Length constraints.
       )
    )



So as we can see, it's impossible to have multiple PropertyMetadata instances under one property name, e.g, 'name', in this case. So why implementing the $member's element as an array?

The only scenario that may produce multiple objects in $members[<property name>] is when a class extends a parent class, which defines constraint(s) of a property, and also implements an interface, which defines getter function(s) for the same property name:

Code: Select all
use Symfony\Component\Validator\Constraints as Assert;

interface Foo
{
    /**
      * @Assert\True(message="This is not a valid name.")
    */
    public function isName($name); // this is a getter function for property name 'name'
}


Code: Select all
use Symfony\Component\Validator\Constraints as Assert;

class Person
{
    /**
     * @Assert\NotBlank()
    */
    public $name;
}



Code: Select all
use Symfony\Component\Validator\Constraints as Assert;

class Author extends Person implements Foo
{
     function isName($name)
     {
         // ...
     }

     /**
       * @Assert\Length(
       * min = 3,
       * max = 50,
       * )
     */
     public $name;
}


When validating an Author instance, it's $members property will eventually be initialized as follows:
Code: Select all
array(
    'name' => array(
        [0] => <PropertyMetadata> // includes NotBlank and Length constraints
        [1] => <GetterMetadata> // corresponds to the 'isName()' getter function
    )
)


But when naming a getter function for validating purpose, we don't use getter prefix + propery name, for example, isName(), getName() or hasName(). Instead, generally, we will also use an appendix, for example, isNameLegal(). And if this is the case, the getter is actually using a different property name, which will produce a separate element in the $members property.

Anyways, I believe the implementation of ClassMetadata::members is a bit confusing. It's not a bug though, but I think it will be great if it can be improved in the future.
Michael Lee / Managing Director / ZerusTech Ltd / www.zerustech.com
zerustech
Junior Member
 
Posts: 1
Joined: Mon Jun 08, 2015 8:35 am