Search code examples
symfonysymfony4

Use query_builder on CollectionType in symfony4 forms?


In a symfony 4 form, I need to use something like a query_builder option that is available on EntityType but from a CollectionType. There is a similar question here with no good answers.

In my project, each Site entity has many Goal. Each Goal has a numeric goal and a specific date. I'd like to edit the goals of a site for a specific date only. The problem is that a CollectionType form pulls all goals to show in the form, but I only want to pull the goals for a given date. How? There is no query_builder on a CollectionType like there is on an EntityType. I could change the getter in my Site entity, but I don't know how to pass the needed date to my getter.

For now my work-around is to render the entire form (with ALL associated goals for a given site), and then use some javascript to hide all goals except those with the date to edit. This works, but it's a terrible solution for sites with lots of goals spanning a range of dates.

My Site entity (only relevant code is shown):

class Site
{
    public function __construct()
    {
        $this->goals = new ArrayCollection();
    }

    /** @ORM\OneToMany(targetEntity="App\Entity\Goal", mappedBy="site") */
    private $goals;


    public function getGoals()
    {
        return $this->goals;
    }
}

and my related Goal entity:

class Goal
{
    /** @ORM\Column(type="date") */
    private $goalDate;

    /** @ORM\Column(type="integer") */
    private $goal;

    /** @ORM\ManyToOne(targetEntity="App\Entity\Site", inversedBy="goals") */
    private $site;

    // ...
}

My forms:

class SiteGoalsAdminForm extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('goals', CollectionType::class, [
                'entry_type' => GoalsEmbeddedForm::class,
            ]);
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => Site::class
        ]);
    }
}

and the individual goal form:

class GoalsEmbeddedForm extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('goal', IntegerType::class)
            ->add('goalDate', DateType::class);
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => Goal::class,
        ]);
    }
}

Solution

  • Using Form Events, while avoiding the allow_add and allow_delete options for the CollectionType form might land you in the right neighbourhood:

    First - let's assume we're filtering by year, for ease of example, and that the year is being scooped up from a ?y=2018 style of querystring. We'll pass that info down to the form builder:

    <?php
    // Inside a *Action method of a controller
    
    public function index(Request $request): Response
    {
        // ...
        $filteredYear = $request->get('y');
        $form         = $this->createForm(SiteGoalsAdminForm::class, $site, ['year_filter' => $filteredYear]);
        // ...
    }
    

    This implies we should be updating the default options for the SiteGoalsAdminForm class:

    <?php
    
    // SiteGoalsAdminForm.php
    
    // ...
        public function configureOptions(OptionsResolver $resolver)
        {
            $resolver->setDefaults([
              'data_class' => Site::class,
              'year_filter' => 2018
            ]);
         }
    // ...
    

    Then, in the buildForm method of that same class, we could access the Site object and remove Goals from it where the year of the goalDate did not fall inside the form's

    <?php
    
    // SiteGoalsAdminForm.php
    
    namespace App\Form;
    
    // ... other `use` statements, plus:
    use Symfony\Component\Form\FormEvent;
    use Symfony\Component\Form\FormEvents;
    
    class SiteGoalsAdminForm extends AbstractType
    {
        public function buildForm(FormBuilderInterface $builder, array $options)
        {
            $builder->addEventListener(
                FormEvents::PRE_SET_DATA,
                function (FormEvent $event) use ($options) {
                    $form = $event->getForm();
                    /** @var Site */
                    $site  = $event->getData();
                    $goals = $site->getGoals();
    
                    foreach ($goals as $g) {
                        if ($g->getGoalDate()->format('Y') !== (string) $options['year_filter']) {
                            $site->removeGoal($g);
                        }
                    }
    
                    $form->add('goals', CollectionType::class, [
                        'entry_type' => GoalsEmbeddedForm::class,
                    ]);
                }
            );
        }
    
        // ...
    }
    

    Not a query_builder exactly, but functionally similar.