I've got a Company
that has many Employee
s. 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:
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);
}
}
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...
}
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...
}
}
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,
]);
}
}
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 TextType
s 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.
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:
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,
]);
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,
]);
}
}
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!