Add a special listener to a Content Attribute

Context

When working with the Content and ContentType, you might have the need to add a specific listener.

In some case, the field can store a collection which needs to be initialized by a listener to set all the data.

The main issues is that all the contentAttribute are stored as a raw field in mongoDB. By doing this, the data you will save will be typed but the data you get back will only be an array.

Here is an example of how you can take profit of the Content architecture to store some TranslatedValue field inside.

Usage

Prerequisites

In the Open Orchestra model abstraction, the Translated Value are stored as a collection of TranslatedValueInterface implementation. Each object will store the language code and the translated value.

When you use it in a simple form, you will need to add a listener on the form :

$builder->addEventListener(FormEvents::PRE_SET_DATA, array($this->translateValueInitializerListener, 'preSetData'));

Then you can simply add the form type :

$builder->add('descriptions', 'translated_value_collection');

The entity should also implement the TranslatedValueContainerInterface.

Issue description

As you use a dynamic Field Type to describe your content, the Content document cannot implement the TranslatedValueContainerInterface to give the name of the translated fields.

Moreover, the listener has to be configured in the root form. As the configuration is fully dynamic, you cannot add it.

Solution

A simple solution is to create a specific form type, add an initializer, and take care of the data transformation.

First create the specific form type :

class TranslatedValueContainerAttributeType extends AbstractType
{
    public function getName()
    {
        return 'translated_value_container_attribute';
    }
}

Then inject the default value initializer :

protected $translatedValueDefaultValueInitializer;

/**
 * @param TranslatedValueDefaultValueInitializer $translatedValueDefaultValueInitializer
 */
public function __construct(TranslatedValueDefaultValueInitializer $translatedValueDefaultValueInitializer)
{
    $this->translatedValueDefaultValueInitializer = $translatedValueDefaultValueInitializer;
}

Finally, create the listener to initialize the data :

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $translatedValueDefaultValueInitializer = $this->translatedValueDefaultValueInitializer;
    $builder->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event) use ($translatedValueDefaultValueInitializer) {
        $data = $event->getData();
        if (is_null($data)) {
            $values = new ArrayCollection();
            $translatedValueDefaultValueInitializer->generate($values);
            $data['value'] = $values;
        } elseif (is_array($data) && array_key_exists('value', $data)) {
            foreach ($data['value'] as $key => $element) {
                $newElement = new TranslatedValue();
                $newElement->setLanguage($element['language']);
                $newElement->setValue($element['value']);
                $data['value'][$key] = $newElement;
            }
        }
        $event->setData($data);
    });

    $builder->add('value', 'translated_value_collection');
}

When you create the contentAttribute, there are no data stored, so the $data is null. You need to create an ArrayCollection which will be later saved and use the initializer to generate all the data you need.

This is the same workflow as the other translated values.

As you can see, there is no event listener bound on Submit events. The raw field type automatically accepts array and ArrayCollection.

When you want to display the data stored, you need to transform it from an array to a TranslatedValue document.

Note : This should be done in a DataTransformer.

Configuration

In order to make this form type available in the field type, you need to declare it :

translated:
    label: translated
    type: translated_value_container_attribute
    options:
        required:
            default_value: false