解决Symfony 5异常:Argument 1 passed to Symfony\Bridge\Doctrine\Form\ChoiceList\IdReader::getIdValue() must be an object or null, string given

2021年11月29日23:44:14 发表评论 1,047 次浏览

了解如何在处理表单时解决 Symfony 5 异常(Argument 1 passed to Symfony\Bridge\Doctrine\Form\ChoiceList\IdReader::getIdValue() must be an object or null, string given)。

解决Symfony 5异常:Argument 1 passed to Symfony\Bridge\Doctrine\Form\ChoiceList\IdReader::getIdValue() must be an object or null, string given
Argument 1 passed to IdReader::getIdValue()的解决办法

在过去的几天里,我正在研究一个搜索表单,其中的结果由表单中的一些参数过滤。我决定继续使用 FormType 创建的 Symfony 表单,这样我就可以使用 EntityType 字段等等。虽然我认为这比用纯 HTML 编写表单要容易得多,但结果却变得更加复杂,因为我最终遇到了这个奇怪的问题:

Argument 1 passed to Symfony\Bridge\Doctrine\Form\ChoiceList\IdReader::getIdValue() must be an object or null, string given

如何解决异常Argument 1 passed to IdReader::getIdValue()?在本文中,我将向你解释在你的 Symfony 5 项目中解决此问题的 2 种方法。

如何触发错误的示例

在讨论Argument 1 passed to IdReader::getIdValue()的解决办法之前,为了触发这个错误,我们有以下代码。首先,我们确实有一个没有实体的 FormType,它将用于向用户显示一个简单的表单,他应该能够在其中过滤一些带有字段的东西,在这种情况下,只有一个字段,即Categories字段。FormType 如下所示:

<?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,
        ]);
    }
}

此表单将从控制器呈现到 Twig 视图,如下所示:

<?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()
        ]);
    }
}

Twig 视图将是以下视图:

{# 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 %}

如果执行此操作,表单将如下所示,允许用户简单地检查要在表单中过滤的类别:

解决Symfony 5异常:Argument 1 passed to Symfony\Bridge\Doctrine\Form\ChoiceList\IdReader::getIdValue() must be an object or null, string given
表单应允许用户在表单中选择多个类别,以便稍后使用自定义逻辑进行过滤

理论上,它应该可以正常工作,但是,如果用户尝试提交表单,则会出现上述异常(🤯)。

有什么问题

问题的发生基本上是因为它没有处理对象(实体),在这种情况下是Categories对象,它接收一个数组,其中包含以下形式的所选类别的主键:

array:2 [▼
  "categories" => array:1 [▼
    0 => "1",
    1 => "2",
  ]
  "_token" => "ZrHIxYstXtq86hljZ8C_nRpMlQhcXIMoGSVSij3gGwY"
]

搞砸你的代码。在本文中,我将向你解释在你的 Symfony 项目中解决此问题的 2 种方法。

A. 用实体集合替换 id 数组

解决异常Argument 1 passed to IdReader::getIdValue()?解决这个问题的第一个也是最简单的解决方案就是提供已处理(之前createForm)数组中的对象集合,而不是数值(id)数组。

<?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()
        ]);
    }
}

通过这样做,在提交时接收 FormType 的集合将是以下带有对象而不是普通数字的数组:

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"
  }
]

因此,如果你尝试再次提交表单,至少选择一个类别,它将正常工作,并且在新页面上,它将显示为选中状态。

B. 使用数据转换器

Argument 1 passed to IdReader::getIdValue()的解决办法:如果你不同意猴子补丁解决方案,你可以选择 Data Transformer 解决方案。在这种情况下,在给定的表单逻辑下,数据转换器的结构将不会发生任何变化reverseTransform。相反,逻辑将始终是将数字数组转换为类别集合。像这样为表单创建DataTransformer(在/Form/DataTransformer目录下创建transformer ):

<?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 [];
    }
}

解决异常Argument 1 passed to IdReader::getIdValue()?现在 DataTransformer 存在,你需要将它附加到 FormType。通过构造函数方法注入它,检索有问题的字段,在本例中为类别,并添加创建的模型转换器:

<?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,
        ]);
    }
}

就是这样,就像第一个解决方案一样,如果用户不选择、选择一个或所有类别,表单将成功提交,并且在刷新时,它们仍将被选中。

快乐编码❤️!

木子山

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: