ZF2 + Doctrine2 - Fieldset 中集合的 Fieldset 未正确验证

ZF2 + Doctrine2 - Fieldset in Fieldset of a Collection in Fieldset does not validate properly

我刚才问了一个 ,这归结为表单、字段集和输入过滤器的结构。

我一直在彻底应用关注点分离原则将 Fieldsets 与 InputFilters 分开,因为创建它们的模块也将在 API(基于 Apigility)中使用,所以我会只需要实体和输入过滤器。

但是,我现在有一个问题,当我有一个由 Fieldset 使用的 Fieldset,用于 Fieldset 中的 Collection 时,最里面的 Fieldset 没有验证。

让我用例子和代码来详细说明!

情况是我希望能够创建一个LocationLocation 由 属性 nameOneToMany ArrayCollection|Address[] 关联组成。这是因为 Location 可以有多个地址(例如访客地址和送货地址)。

Address 由几个属性(街道、号码、城市、Country 等)和一个 OneToOne Coordinates 关联组成。

现在,Address 具有以下字段集:

class AddressFieldset extends AbstractFieldset
{
    public function init()
    {
        parent::init();

        // More properties, but you get the idea

        $this->add([
            'name' => 'street',
            'required' => false,
            'type' => Text::class,
            'options' => [
                'label' => _('Street'),
            ],
        ]);

        $this->add([
            'name' => 'country',
            'required' => false,
            'type' => ObjectSelect::class,
            'options' => [
                'object_manager' => $this->getEntityManager(),
                'target_class'   => Country::class,
                'property'       => 'id',
                'is_method'      => true,
                'find_method'    => [
                    'name' => 'getEnabledCountries',
                ],
                'display_empty_item' => true,
                'empty_item_label'   => '---',
                'label' => _('Country'),
                'label_generator' => function ($targetEntity) {
                    return $targetEntity->getName();
                },
            ],
        ]);

        $this->add([
            'type' => CoordinatesFieldset::class,
            'required' => false,
            'name' => 'coordinates',
            'options' => [
                'use_as_base_fieldset' => false,
            ],
        ]);
    }
}

如您所见,必须输入 Address 实体的详细信息,必须选择 Country 并且可以(不需要)提供 Coordinates

以上是使用下面的 InputFilter 验证的。

class AddressFieldsetInputFilter extends AbstractFieldsetInputFilter
{
    /** @var CoordinatesFieldsetInputFilter $coordinatesFieldsetInputFilter */
    protected $coordinatesFieldsetInputFilter;

    public function __construct(
        CoordinatesFieldsetInputFilter $filter,
        EntityManager $objectManager,
        Translator $translator
    ) {
        $this->coordinatesFieldsetInputFilter = $filter;

        parent::__construct([
            'object_manager' => $objectManager,
            'object_repository' => $objectManager->getRepository(Address::class),
            'translator' => $translator,
        ]);
    }

    /**
     * Sets AddressFieldset Element validation
     */
    public function init()
    {
        parent::init();

        $this->add($this->coordinatesFieldsetInputFilter, 'coordinates');

        $this->add([
            'name' => 'street',
            'required' => false,
            'filters' => [
                ['name' => StringTrim::class],
                ['name' => StripTags::class],
            ],
            'validators' => [
                [
                    'name' => StringLength::class,
                    'options' => [
                        'min' => 3,
                        'max' => 255,
                    ],
                ],
            ],
        ]);

        $this->add([
            'name' => 'country',
            'required' => false,
        ]);
    }
}

如您所见,AddressFieldsetInputFilter 需要一些东西,其中之一就是 CoordinatesFieldsetInputFilter。然后在 init() 函数中添加与 Fieldset 中给定的名称相对应的名称。

现在,以上都可以,没问题。到处都是带坐标的地址。太棒了。

当我们更进一步并得到 LocationFieldset 时,问题就出现了,如下所示,它是 LocationFieldsetInputFilter

class LocationFieldset extends AbstractFieldset
{
    public function init()
    {
        parent::init();

        $this->add([
            'name' => 'name',
            'required' => true,
            'type' => Text::class,
            'options' => [
                'label' => _('Name'),
            ],
        ]);

        $this->add([
            'type' => Collection::class,
            'name' => 'addresses',
            'options' => [
                'label' => _('Addresses'),
                'count' => 1,
                'allow_add' => true,
                'allow_remove' => true,
                'should_create_template' => true,
                'target_element' => $this->getFormFactory()->getFormElementManager()->get(AddressFieldset::class),
            ],
        ]);
    }
}

在下面的 class 中,您可能会注意到一堆注释掉的行,这些是修改 InputFilter 的 DI and/or 设置以使其工作的不同尝试。

class LocationFieldsetInputFilter extends AbstractFieldsetInputFilter
{
    /** @var AddressFieldsetInputFilter $addressFieldsetInputFilter */
    protected $addressFieldsetInputFilter;

//    /** @var CoordinatesFieldsetInputFilter $coordinatesFieldsetInputFilter */
//    protected $coordinatesFieldsetInputFilter;

    public function __construct(
        AddressFieldsetInputFilter $filter,
//        CoordinatesFieldsetInputFilter $coordinatesFieldsetInputFilter,
        EntityManager $objectManager,
        Translator $translator
    ) {
        $this->addressFieldsetInputFilter = $filter;
//        $this->coordinatesFieldsetInputFilter = $coordinatesFieldsetInputFilter;

        parent::__construct([
            'object_manager' => $objectManager,
            'object_repository' => $objectManager->getRepository(Location::class),
            'translator' => $translator,
        ]);
    }

    /**
     * Sets LocationFieldset Element validation
     */
    public function init()
    {
        parent::init();

        $this->add($this->addressFieldsetInputFilter, 'addresses');
//        $this->get('addresses')->add($this->coordinatesFieldsetInputFilter, 'coordinates');

        $this->add([
            'name' => 'name',
            'required' => true,
            'filters' => [
                ['name' => StringTrim::class],
                ['name' => StripTags::class],
            ],
            'validators' => [
                [
                    'name' => StringLength::class,
                    'options' => [
                        'min' => 3,
                        'max' => 255,
                    ],
                ],
            ],
        ]);
    }
}

您可能已经注意到 LocationFieldsetLocationFieldsetInputFilter 使用了现有的 AddressFieldset 和 `AddressFieldsetInputFilter。

看看它们是如何工作的,我不明白为什么会出错。

但是哪里出了问题?

好吧,要创建 Location,似乎总是需要输入 Coordinates。如果您查看 AddressFieldset(在顶部),您会注意到一个 'required' => false,,所以这没有意义。

但是,当我在输入中输入值时,它们没有得到验证。调试时,我进入 \Zend\InputFilter\BaseInputFilter,第 262 行,它专门验证输入,我注意到它在验证过程中丢失了数据。

我已经确认数据在开始时存在,在验证过程中,直到它尝试验证 Coordinates 实体,它似乎丢失了它(还没有找到原因).

如果有人能指出正确的方向来清理它,我将不胜感激。已经在这个问题上讨论了太多个小时了。

编辑

在视图部分代码中添加以显示打印方法,以防should/could帮助:

地址-form.phtml

<?php
/** @var \Address\Form\AddressForm $form */

$form->prepare();
echo $this->form()->openTag($form);
echo $this->formRow($form->get('csrf'));

echo $this->formRow($form->get('address')->get('id'));
echo $this->formRow($form->get('address')->get('street'));
echo $this->formRow($form->get('address')->get('city'));
echo $this->formRow($form->get('address')->get('country'));

echo $this->formCollection($form->get('address')->get('coordinates'));

echo $this->formRow($form->get('submit'));
echo $this->form()->closeTag($form);

位置-form.phtml

<?php
/** @var \Location\Form\LocationForm $form */

$form->prepare();
echo $this->form()->openTag($form);
echo $this->formRow($form->get('csrf'));

echo $this->formRow($form->get('location')->get('id'));
echo $this->formRow($form->get('location')->get('name'));
//echo $this->formCollection($form->get('location')->get('addresses'));

$addresses = $form->get('location')->get('addresses');
foreach ($addresses as $address) {
    echo $this->formCollection($address);
}

echo $this->formRow($form->get('submit'));
echo $this->form()->closeTag($form);

为了以防万一它使一切变得更加清晰:一张调试图片来帮忙

经过又一天的调试(和发誓),我找到了答案!

将我指向 Zend CollectionInputFilter,从而帮助了我。

因为 AddressFieldset 添加到 Collection 中的 LocationFieldset,所以必须使用具有特定 InputFilterCollectionInputFilter 进行验证Fieldset 指定。

为了修复我的应用程序,我必须同时修改 LocationFieldsetInputFilterLocationFieldsetInputFilterFactory。在更新的代码下方,旧代码在注释中。

LocationFieldsetInputFilterFactory.php

class LocationFieldsetInputFilterFactory extends AbstractFieldsetInputFilterFactory
{
    /**
     * @param ServiceLocatorInterface|ControllerManager $serviceLocator
     * @return InputFilter
     */
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        parent::setupRequirements($serviceLocator, Location::class);

        /** @var AddressFieldsetInputFilter $addressFieldsetInputFilter */
        $addressFieldsetInputFilter = $this->getServiceManager()->get('InputFilterManager')
            ->get(AddressFieldsetInputFilter::class);

        $collectionInputFilter = new CollectionInputFilter();
        $collectionInputFilter->setInputFilter($addressFieldsetInputFilter); // Make sure to add the FieldsetInputFilter that is to be used for the Entities!

        return new LocationFieldsetInputFilter(
            $collectionInputFilter,         // New
            // $addressFieldsetInputFilter, // Removed
            $this->getEntityManager(),
            $this->getTranslator()
        );
    }
}

LocationFieldsetInputFilter.php

class LocationFieldsetInputFilter extends AbstractFieldsetInputFilter
{
    // Removed
    // /** @var AddressFieldsetInputFilter $addressFieldsetInputFilter */
    // protected $addressFieldsetInputFilter ;

    // New
    /** @var CollectionInputFilter $addressCollectionInputFilter */
    protected $addressCollectionInputFilter;

    public function __construct(
        CollectionInputFilter $addressCollectionInputFilter, // New
        // AddressFieldsetInputFilter $filter, // Removed
        EntityManager $objectManager,
        Translator $translator
    ) {
        // $this->addressFieldsetInputFilter = $filter; // Removed
        $this->addressCollectionInputFilter = $addressCollectionInputFilter; // New

        parent::__construct([
            'object_manager' => $objectManager,
            'object_repository' => $objectManager->getRepository(Location::class),
            'translator' => $translator,
        ]);
    }

    /**
     * Sets LocationFieldset Element validation
     */
    public function init()
    {
        parent::init();

        // $this->add($this->addressFieldsetInputFilter, 'addresses'); // Removed
        $this->add($this->addressCollectionInputFilter, 'addresses'); // New

        $this->add([
            'name' => 'name',
            'required' => true,
            'filters' => [
                ['name' => StringTrim::class],
                ['name' => StripTags::class],
            ],
            'validators' => [
                [
                    'name' => StringLength::class,
                    'options' => [
                        'min' => 3,
                        'max' => 255,
                    ],
                ],
            ],
        ]);
    }
}

它的工作方式是,在数据验证期间,它将对从客户端收到的每个“element”应用单数 AddressFieldsetInputFilter。因为来自客户端的 Collection 可能是这些元素中的 0 个或多个(因为 adding/removing 它们是使用 JavaScript 完成的)。

现在我已经弄清楚了,它确实非常有道理。