In the last days, I was working on a search form where the results are filtered by some parameters in a form. I decided to proceed with a Symfony Form created with a FormType, so I could use EntityType fields and so on. Although I thought it would be way easier than writing the form with plain HTML, it ended up being way more complicated as I ended up with this weird issue:
Argument 1 passed to Symfony\Bridge\Doctrine\Form\ChoiceList\IdReader::getIdValue() must be an object or null, string given
In this article, I'm going to explain to you 2 ways to solve this issue in your Symfony 5 project.
Example of how to trigger the error
In order to trigger this error, we have the following code. As first, we do have a FormType without an Entity that will be used to display a simple form to the user, where he should be able to filter some stuff with the fields, in this case, just a single field, the Categories
field. The FormType is the following one:
<?php
// src/Form/FilterFormType.php
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
// 1. Import the Entity Class of Categories
use App\Entity\Categories;
class FilterFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
// 2. Create the Form with your fields, in this casem just a single field
$builder
->add('categories', EntityType::class, [
// Look for choices from the Categories entity
'class' => Categories::class,
// Display as checkboxes
'expanded' => true,
'multiple' => true,
// The property of the Categories entity that will show up on the select (or checkboxes)
'choice_label' => 'name'
]);
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => null,
]);
}
}
This form will be rendered from a controller to a Twig view, like this:
<?php
// src/Controller/SomeController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use App\Repository\CategoriesRepository;
// 1. Import the FormType
use App\Form\FilterFormType;
class SomeController extends AbstractController
{
public function index(Request $request): Response
{
// 2. Obtain possibly submitted data of the form (we will use a GET form)
// so, the filter_form parameter should contain the information of the form
$data = $request->query->all("filter_form");
// 3. Instantiate the Form with the class with the data that it may be submitted
$form = $this->createForm(FilterFormType::class, $data);
return $this->render('pages/search.html.twig', [
'form' => $form->createView()
]);
}
}
And the Twig view will be the following one:
{# templates/pages/index.html #}
{% extends 'base.html.twig' %}
{% block body %}
{# Use the GET method for this Form #}
{{ form_start(form, {'method': 'GET'}) }}
<div class="row">
<div class="col-md-12 col-lg-12">
<div class="card">
<div class="card-header">
<div class="card-title">Filter by Category</div>
<div class="card-options">
<a href="#" class="card-options-collapse" data-toggle="card-collapse">
<i class="fe fe-chevron-up"></i>
</a>
</div>
</div>
<div class="card-body">
<div class="custom-controls-stacked">
{# We customized every option in the expanded entity field #}
{% for CategoryField in form.categories %}
<label class="custom-control custom-checkbox">
{{ form_widget(CategoryField, {"attr": {"class": "custom-control-input"}}) }}
<span class="custom-control-label">
{{ form_label(CategoryField) }}
</span>
</label>
{% endfor %}
</div>
</div>
</div>
<button type="submit" class="btn btn-success btn-block">
Filter
</button>
</div>
</div>
{{ form_end(form) }}
{% endblock %}
If this action is executed, the form will look like this, allowing the user to simply check the Categories to filter in the form:
Theoretically, it should work without an issue, however, if the user tries to submit the form, the mentioned exception will appear (🤯).
What's the issue
The problem happens basically because instead of handling Objects (entities), in this case, Categories
objects, it receives an array with the primary keys of the selected categories in the form:
array:2 [▼
"categories" => array:1 [▼
0 => "1",
1 => "2",
]
"_token" => "ZrHIxYstXtq86hljZ8C_nRpMlQhcXIMoGSVSij3gGwY"
]
screwing up your code. In this article, I'm going to explain to you 2 ways to fix this problem in your Symfony project.
A. Replacing the array of ids with an entity collection
The first and easiest solution to solve this problem is just to provide the collection of objects in the processed (before createForm
) array instead of the array of numeric values (ids).
<?php
// src/Controller/SomeController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use App\Repository\CategoriesRepository;
// 1. Import the FormType
use App\Form\FilterFormType;
use App\Entity\Categories;
class SomeController extends AbstractController
{
public function index(Request $request): Response
{
// 2. Obtain possibly submitted data of the form (we will use a GET form)
// so, the filter_form parameter should contain the information of the form
$data = $request->query->all("filter_form");
// !MONKEYPATCH FIX!
// If the user did select at least a single option in the categories filter
if(isset($data["categories"])){
// Retrieve the Categories repository
$em = $this->getDoctrine()->getManager();
$repoCategories = $em->getRepository(Categories::class);
// Replace the categories array (1,2) with the result of a findBy of the Categories Repository
$data["categories"] = $repoCategories->findBy(['id' => $data["categories"]]);
}
// 3. Instantiate the Form with the class with the data that it may be submitted
$form = $this->createForm(FilterFormType::class, $data);
return $this->render('pages/search.html.twig', [
'form' => $form->createView()
]);
}
}
By doing this, the collection that will receive the FormType when it's submitted will be the following array with objects instead of plain numbers:
array:1 [▼
0 => App\Entity\Categories {#822 ▼
-id: "1"
-nombre: "Prestadores de Servicios de Salud"
-slug: "prestadores-de-servicios-de-salud"
},
1 => App\Entity\Categories {#823 ▼
-id: "2"
-nombre: "Bancos de sangre"
-slug: "bancos-de-sangre"
}
]
So if you try to submit the form once again selecting at least a single category, it will work properly and on the new page, it will appear as selected.
B. Using a Data Transformer
If you don't agree with the monkey patch solution, you may opt for the Data Transformer solution. In this case, with the given logic of the form, the structure of the Data Transformer wouldn't be the same as there won't be any reverseTransform
that happens. Instead, the logic will always be to convert the numeric array to a collection of Categories. Create the DataTransformer for the form like this (create the transformer under the /Form/DataTransformer
directory):
<?php
// src/Form/DataTransformer/CategoriesToNumbersTransformer.php
namespace App\Form\DataTransformer;
use App\Entity\Categories;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
class CategoriesToNumbersTransformer implements DataTransformerInterface
{
private $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
/**
* Transforms the numeric array (1,2,3,4) to a collection of Categories (Categories[])
*
* @param Array|null $categories
* @return array
*/
public function transform($categoriesNumber): array
{
$result = [];
if (null === $categoriesNumber) {
return $result;
}
return $this->entityManager
->getRepository(Categories::class)
->findBy(["id" => $categoriesNumber])
;
}
/**
* In this case, the reverseTransform can be empty.
*
* @param type $value
* @return array
*/
public function reverseTransform($value): array
{
return [];
}
}
Now that the DataTransformer exists, you need to attach it to the FormType. Inject it through the constructor method, retrieve the field with the problem, in this case, Categories, and add the created model transformer:
<?php
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use App\Entity\Categories;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
// 1. Use the CategoriesToNumbersTransformer
use App\Form\DataTransformer\CategoriesToNumbersTransformer;
class RegistrosFilterType extends AbstractType
{
// 2. Define the transformer
private $transformer;
// 3. Assign the injected transformer to the class accessible variable
public function __construct(CategoriesToNumbersTransformer $transformer) {
$this->transformer = $transformer;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
// Build the form as usual
$builder
->add('categories', EntityType::class, [
// looks for choices from this entity
'class' => Categories::class,
'expanded' => true,
'multiple' => true,
'choice_label' => 'name'
]);
;
// 4. Add the Data Transformer
$builder->get('categories')->addModelTransformer($this->transformer);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => null,
]);
}
}
And that's it, just as the first solution if the user selects none, one, or all of the categories, the form will be submitted successfully and on refresh, they will still be selected.
Happy coding ❤️!