Search code examples
symfonysymfony-forms

empty_data not working for compound forms, or entity is not being instantiated (ArgumentCountError: too few arguments to function)


I've got a Company that has many Employees. In my form, I want the user to be able to dynamically add employees (easy enough). EmployeeType (an AbstractType) is compound, containing a first and last name. On form submission, Symfony doesn't seem to carry over the data from the form into the constructor for the "new" Employee. I get an erro

ArgumentCountError: Too few arguments to function Employee::__construct() ... 0 passed in ... and exactly 3 expected

Showing and editing existing Employees works, so I'm confident my relationships, etc., are all correct.

Abbreviated code:

Company

class Company
{
    protected $employees;

    public function __construct()
    {
        $this->employees = new ArrayCollection();
    }

    public function addEmployee(Employee $employee)
    {
        if ($this->employees->contains($employee)) {
            return;
        }
        $this->employees->add($employee);
    }

    public function removeEmployee(Employee $employee)
    {
        if (!$this->employees->contains($employee)) {
            return;
        }
        $this->employees->removeElement($employee);
    }
}

Employee

class Employee
{
    // ... firstName and lastName properties...

    public function __construct(Company $company, $firstName, $lastName)
    {
        $this->company = $company;
        $this->company->addEmployee($this);
    }

    // ...getter and setter for firstName / lastName...
}

CompanyType

class CompanyType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('employees', CollectionType::class, [
            'entry_type' => EmployeeType::class,
            'allow_add' => true,
            'allow_delete' => false,
            'required' => false,
        ]);
        // ...other fields, some are CollectionType of TextTypes that work correctly...
    }
}

EmployeeType

class EmployeeType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('firstName')
            ->add('lastName');
    }

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

CompanyController

class CompanyController
{
    // Never mind that this is a show and not edit, etc.
    public function showAction()
    {
        // Assume $this->company is a new or existing Company
        $form = $this->createForm(CompanyType::class, $this->company);

        $form->handleRequest($this->request);

        if ($form->isSubmitted() && $form->isValid()) {
            $company = $form->getData();

            $entityManager = $this->getDoctrine()->getManager();
            $entityManager->persist($company);
            $entityManager->flush();
        }

        // set flash message, redirect, etc.
    }

    // ...render view...
}

The above will work when modifying existing Employees, just not when creating new ones. Debugging from within the Symfony code, I can see that no data exists for the new employees, so it's trying to find a closure or definition for empty_data in CompanyType. I've tried this every which way (via configureOptions, and empty_data option when building the CompanyType::buildForm form), e.g. https://symfony.com/doc/current/form/use_empty_data.html. My gut tells me I don't even need to do this, because the form data should not be empty (I explicitly filled out the fields).

I tried using a model transformer as well. In that case, the transformation from the form (second function argument passed to new CallbackTransformer) isn't even hit.

The view properly sets name attributes when adding new employee fields, e.g. form[employees][1][firstName], etc. That isn't the problem. It also sends the right data to the controller. I confirmed this by inspecting the form submission data via CompanyType::onPreSubmit (using an event listener).

I also have a CollectionType of TextTypes for other things in CompanyType, those work fine. So the issue seems to be related to the fact that EmployeeType is compound (containing multiple fields).

Hopefully the above is enough to illustrate the problem. Any ideas?

UPDATE:

It seems the issue is there isn't an instantiation of Employee for Symfony to work with. Internally, each field gets passed to Symfony\Component\Form\Form::submit(). For existing employees, there is also an Employee passed in. For the new one, it's null. That explains why it's looking for empty_data, but I don't know why I can't get empty_data to work.


Solution

  • The solution was to define empty_data in the compound form, and not the CollectionType form.

    My situation is a little weird, because I also need the instance of Company in my EmployeeType, as it must be passed to the constructor for Employee. I accomplished this by passing in the Company as form option into configureOptions (supplied by the controller), and then into entry_options. I don't know if this is best practice, but it works:

    CompanyController

    Make sure we pass in the Company instance, so it can be used in EmployeeType when building a new Employee:

    $form = $this->createForm(CompanyType::class, $this->company, [
        // This is a form option, valid because it's in CompanyType::configureOptions()
        'company' => $this->company,
    ]);
    

    CompanyType

    class CompanyType extends AbstractType
    {
        public function buildForm(FormBuilderInterface $builder, array $options)
        {
            $builder->add('employees', CollectionType::class, [
                // ...
    
                // Pass the Company instance to the EmployeeType.
                'entry_options' => [ 'company' => $options['company'] ],
    
                // This was also needed, apparently.
                'by_reference' => false,
            ]);
        }
    
        public function configureOptions(OptionsResolver $resolver)
        {
            $resolver->setDefaults([
                // Allows the controller to pass in a Company instance.
                'company' => null,
            ]);
        }
    }
    

    EmployeeType

    Here we make sure empty_data properly builds an Employee from the form data.

    class EmployeeType extends AbstractType
    {
        private $company;
    
        public function buildForm(FormBuilderInterface $builder, array $options)
        {
            $builder->add('firstName')
                ->add('lastName');
    
            // A little weird to set a class property here, but we need the Company
            // instance in the 'empty_data' definition in configureOptions(),
            // at which point we otherwise wouldn't have access to the Company.
            $this->company = $options['company'];
        }
    
        public function configureOptions(OptionsResolver $resolver)
        {
            $resolver->setDefaults([
                'data_class' => Employee::class,
                'empty_data' => function (FormInterface $form) use ($resolver) {
                    return new Employee(
                        $this->company,
                        $form->get('firstName')->getData(),
                        $form->get('lastName')->getData(),
                    );
                },
            ]);
        }
    }
    

    Viola! I can now add new employees.

    Hope this helps other people!