Search code examples
phpsymfonydesign-patternssilexformbuilder

PHP silex Formbuilder and object with array of object


I am using PHP with the Silex framework and after some hours trying to find a satisfying solution I am still blocked with the following problem regarding forms and objects containing array of Objects. The hours spent permitted me to find a working solution but I hope there is a better way to do that with Silex.

In my application I have a User class that is defined like :

class User implements UserInterface
{
    private $id;
    private $username;
    private $displayname;
    private $capacities;
    //...
}

$capacities variable contains an array of objects from another class (Capacity). A Capacity is a specific role in my app with various information (label, place of the capacity ...) and I have added, in that Capacity class a boolean telling if the Capacity is active for a specific user, when attached to a user via the $capacities array.

At the moment I am able to create the form that looks as I want with the following code :

use Planning\Domain\User;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints as Assert;


class UserType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('username', TextType::class, array(
                    'required'    => true,
                    'label' => "Login (pour Jean Durant → jdurant)"
                    ))
            ->add('displayname', TextType::class, array(
                    'label' => "Nom à afficher"
                ));

        $choices = array();
        $choicesActive = array();
        foreach ($builder->getData()->getCapacities() as $id => $capacity) {
            $choices[$capacity->getLabel()] = $capacity->getId();
            if ($capacity->getActive()) {
                $choicesActive[] = $capacity->getId();
            }
        }

        $builder->add('capacities', ChoiceType::class, array(
            'choices' => $choices,
            'data' => $choicesActive,
            'label' => "Groupes",
            'multiple' => True,
            'expanded' => True
        ));
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => User::class,

        ));
    }

    public function getName()
    {
        return 'capacity';
    }
}

but the User object I am getting after the form is validated contains for capacities :

[capacities:Planning\Domain\User:private] => Array
    (
        [0] => 2
        [1] => 4
        [2] => 1
    )

capacities variable contains the list of values for checkboxes that have been checked in the form. The issue is that my User object is not consistant with its definition which says that the capacities property should be an array of Capacity objects. What I am doing at the moment is that I have added the following code to my controller when $userForm->isSubmitted() and $userForm->isValid() :

// Getting the array from the returned user, should contain Capacity object but just contains the IDs.
$capacitiesChecked = $userForm->getData()->getCapacities();
// We regenerate the full array of capacities for this user
$user->setCapacities($app["dao.capacity"]->findAll());

// Then we will activate capacities that have been checked, one by one
foreach ($capacitiesChecked as $capacityChecked) {
    $user->getCapacityById($capacityChecked)->setActive(True);
}

This is working and I am happy with it but being new to Silex and the framework world, I am surprized that I have not been able to find an easier way to answer my problem which I believe should be quite common.

I might be missing something from the Silex/Symfony philosophy and I hope someone there will be able to point me to the correct place to get more information or find a solution!

Edit following @dbrumann answer

As it might not be clear how my data is organized, here are the tables in my database :

  • user
    • id
    • username
    • displayname
  • capacity
    • id
    • label
    • place
  • user_capacity
    • id_user
    • id_capacity

There might be an issue with the modeling of my project but I have a Capacity class and a User class and User has an attribute with an array containing all the Capacity available in the database and each one of this Capacity object has an active attribute that is set to True if there is an entry in the table user_capacity that links user and capacity. What I would like is ONE form that allows me to properly update data into tables user and user_capacity.


Solution

  • I have found a solution that I think is acceptable and might be useful for someone landing on this page. So what I have done is that I have changed my User class so that the $capacities attribute now contains only Capacity objects that are related to the user. But to get all the capacities available on the form, I am passing them as an option (allCapacities) and iterating over them to find which one are present in User->capacities to check them in the form.

    The updated class used to build the form is as following:

    class UserType extends AbstractType
    {
        public function buildForm(FormBuilderInterface $builder, array $options)
        {
            $builder
                ->add('username', TextType::class, array(
                        'required'    => true,
                        'label' => "Login (pour Jean Durant → jdurant)"
                        ))
                ->add('displayname', TextType::class, array(
                        'label' => "Nom à afficher"
                    ));
    
            $choices = array();
            $choicesActive = array();
            foreach ($options["allCapacities"] as $id => $capacity) {
                $choices[] = $capacity;
                if ($builder->getData()->hasCapacity($capacity)) {
                    $choicesActive[] = $capacity;
    
                }
            }
    
            $builder->add('capacities', ChoiceType::class, array(
                'choices' => $choices,
                'data' => $choicesActive,
                'choice_label' => function($category, $key, $index) {
                    return $category->getLabel();
                },
                'label' => "Groupes",
                'multiple' => True,
                'expanded' => True,
            ));
        }
    
        public function configureOptions(OptionsResolver $resolver)
        {
            $resolver->setDefaults(array(
                'data_class' => User::class,
            ));
    
            $resolver->setRequired(array(
                'allCapacities',
            ));
    
    
        }
    
        public function getName()
        {
            return 'capacity';
        }
    }
    

    This is working as expected and does seem to be overcomplicated but there might be easier solution by changing the design, any comment on this would then be welcome!