Search code examples
phpformssymfonycheckboxtransformer-model

symfony : how to render edit a class json_array attribute with a list of checkboxes?


In my symfony2 application, I have an attribute of type json_array :

/**
 * @ORM\Column(name="rights", type="json_array", nullable=true)
 */
protected $rights = array();

The data for this attributeis an associative array as follows :

    $allRights = array(
        Associate::READ_PROFILE => array('all' => false),
        Associate::UPDATE_PROFILE => array('all' => false),
        Associate::READ_CONTACT => array('all', 'created' => false),
);

I want to be able to edit this attribute with a collection of checkboxes collections ie. I want one line per key of the first level and then one checkbox per key of the second level.

I have starter a form type which calls a custom type :

<?php

namespace AppBundle\Form\User;

use AppBundle\Entity\User\Associate;
use AppBundle\Form\DataTransformer\RightsToArrayTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Validator\Constraints as Assert;

class AssociateRightsType extends AbstractType
{
    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('rights', 'fmu_rights', array(
                'label' => false,
            ));
    }

    /**
     * @param OptionsResolverInterface $resolver
     */
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'AppBundle\Entity\User\Associate',
            'validation_groups' => array('Default', 'rights'),
            'cascade_validation' => true,
        ));
    }

    /**
     * @return string
     */
    public function getName()
    {
        return 'appbundle_user_associate_rights';
    }
}

In this custom type I have added a dataTransformer :

<?php

namespace AppBundle\Form\Type;

use AppBundle\Application\User\AssociateManager;
use AppBundle\Entity\User\Associate;
use AppBundle\Form\DataTransformer\RightsToArrayTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class RightsType extends AbstractType
{
    /**
     * @var AssociateManager
     */
    private $associateManager;

    public function __construct(AssociateManager $associateManager)
    {
        $this->associateManager = $associateManager;
    }

    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $transformer = new RightsToArrayTransformer();
        $builder->addModelTransformer($transformer);


    }

    /**
     * @return string
     */
    public function getName()
    {
        return 'fmu_rights';
    }

}

In this data transformer I make sure the data is ouputed in the right format :

<?php

namespace AppBundle\Form\DataTransformer;

use AppBundle\Entity\User\Associate;
use Symfony\Component\Form\DataTransformerInterface;

class RightsToArrayTransformer implements DataTransformerInterface
{
    /**
     * @param mixed $data
     * @return array|mixed
     */
    public function transform($data)
    {
        return is_array($data) ? $data + $this->getRights() : $this->getRights();
    }

    /**
     * @param mixed $data
     * @return array
     */
    public function reverseTransform($data)
    {
        return $data;
    }

    private function getRights()
    {
        $allRights = array(
            Associate::READ_PROFILE => array('all'),
            Associate::UPDATE_PROFILE => array('all'),
            Associate::READ_CONTACT => array('all', 'created'),
            Associate::UPDATE_CONTACT => array('all', 'created'),
            Associate::DELETE_CONTACT => array('all', 'created'),
            Associate::IS_BOSS => array('all'),
        );

        foreach ($allRights as $right => $parameters) {
            $allRights[$right] = array();
            foreach ($parameters as $parameter) {
                $allRights[$right][$parameter] = false;
            }

        }

        return $allRights;
    }
}

and I have a custom view that makes the right visual ouput :

{% block fmu_rights_widget %}

    {% if attr.class is defined %}
        {% set attr = attr|merge({'class': attr.class ~ ' input-sm form-control fmu_rights'}) %}
    {% endif %}

    {% set rights = form.vars.data %}

    {% for right, parameters in rights %}
        <div class="row padding-v">
            <div class="col-md-2">
                {{ right }}
            </div>
            <div class="col-md-10">
                {% for parameter, value in parameters %}
                    {% set name = id ~ '[' ~ loop.index ~ ']' ~ right ~ '[' ~ parameter ~ ']' %}
                        <label for="{{ name }}">{{ parameter }}</label>
                        <input type="checkbox" id="{{ name }}" name="{{ name }}" {% if value %}checked{% endif %}>
                {% endfor %}
            </div>
        </div>
    {% endfor %}

{% endblock %}

However, the data returned when I send my form is the original data output, not the modified data. I guess I've done it wrong in the view. How can I edit this data correctly and be able to manipulate it the reverse transform function of my data transformer ?


Solution

  • Your problem is that you are creating a number of input fields (the checkboxes) of which symfony knows nothing about. In fact when you submit the form, it is probably going to throw an exception saying that it received some fields that it did not expect. You need to transform these fields into something that symfony can understand. You can do it in two ways:

    • You can intercept the submit event in JavaScript and use these checkboxes to construct the data which should be sent back to symfony in the data for the original field (and unset the checkboxes so that they are not sent to the backend)
    • You can test for the existence of these fields when the form is submitted in your controller and use them to construct the data you need (also unset them so that symfony does not complain when validating the form)