ZF2/Doctrine2 - 字段集未验证,数据始终有效

ZF2/Doctrine2 - Fieldsets not validated, data is always valid

我已经使用 Abstract 类 为表单、字段集和 InputFilters 设置了一个结构。表单和字段集有工厂,而 InputFilters 由 FieldsetFactory 在字段集上创建和设置(使用 MutableCreationOptionsInterface 传递选项)

我遇到的问题是 InputFilters 为表单加载的,但不用于验证数据。所有输入都被视为有效。

例如 我有一个 Country 实体和 name 属性。 Country 的名称必须至少 3 个字符,最多 255 个字符。当名称为 "ab" 时,它被认为是有效的。

之前有人问没有抛出错误,数据只是被接受为有效。

在过去的几天里,我一直在为这个问题绞尽脑汁,试图找到我犯错的地方,但找不到。

此外,还有相当多的代码。我将它限制在我认为相关的范围内,尽管如此:如果您需要更多,将会有更多。我删除了很多类型检查、文档块和类型提示以限制代码 lines/reading 时间 ;)

module.config.php

'form_elements' => [
    'factories' => [
        CountryForm::class => CountryFormFactory::class,
        CountryFieldset::class => CountryFieldsetFactory::class,
    ],
],

Country.php

class Country extends AbstractEntity // Creates $id + getter/setter
{
    /**
     * @var string
     * @ORM\Column(name="name", type="string", length=255, nullable=false)
     */
    protected $name;

    // Other properties
    // Getters/setters
}

CountryController.php - 扩展 AbstractActionController

public function editAction() // Here to show the params used to call function
{
    return parent::editAction(
        Country::class,
        CountryForm::class,
        [
            'name' => 'country',
            'options' => []
        ],
        'id',
        'admin/countries/view',
        'admin/countries',
        ['id']
    );
}

AbstractActionController.php - (此处出错) - 由 CountryController#editAction()

使用
public function editAction (
    $emEntity,
    $formName,
    $formOptions,
    $idProperty,
    $route,
    $errorRoute, array
    $routeParams = []
) {
    //Check if form is set

    $id = $this->params()->fromRoute($idProperty, null);

    /** @var AbstractEntity $entity */
    $entity = $this->getEntityManager()->getRepository($emEntity)->find($id);

    /** @var AbstractForm $form */
    $form = $this->getFormElementManager()->get($formName, (is_null($formOptions) ? [] : $formOptions));
    $form->bind($entity);

    /** @var Request $request */
    $request = $this->getRequest();
    if ($request->isPost()) {
        $form->setData($request->getPost());

        if ($form->isValid()) { // HERE IS WHERE IT GOES WRONG -> ALL IS TRUE
            try {
                $this->getEntityManager()->flush();
            } catch (\Exception $e) {
                //Print errors & return (removed, unnecessary)
            }

            return $this->redirectToRoute($route, $this->getRouteParams($entity, $routeParams));
        }
    }

    return [
        'form' => $form,
        'validationMessages' => $form->getMessages() ?: '',
    ];
}

CountryForm.php

class CountryForm extends AbstractForm
{
    // This one added for SO, does nothing but call parent#__construct, which would happen anyway
    public function __construct($name = null, array $options)
    {
        parent::__construct($name, $options);
    }

    public function init()
    {
        //Call parent initializer.
        parent::init();

        $this->add([
            'name' => 'country',
            'type' => CountryFieldset::class,
            'options' => [
                'use_as_base_fieldset' => true,
            ],
        ]);
    }
}

CountryFormFactory.php

class CountryFormFactory extends AbstractFormFactory
{
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        $serviceManager = $serviceLocator->getServiceLocator();

        /** @var EntityManager $entityManager */
        $entityManager = $serviceManager->get('Doctrine\ORM\EntityManager');

        $form = new CountryForm($this->name, $this->options);
        $form->setObjectManager($entityManager);
        $form->setTranslator($serviceManager->get('translator'));

        return $form;
    }
}

AbstractFormFactory.php - 使用 MutableCreationOptionsInterface 从控制器函数调用接收选项:$form = $this->getFormElementManager()->get($formName, (is_null($formOptions) ? [] : $formOptions))

abstract class AbstractFormFactory implements FactoryInterface, MutableCreationOptionsInterface
{
    protected $name;
    protected $options;

    /**
     * @param array $options
     */
    public function setCreationOptions(array $options)
    {
        // Check presence of required "name" (string) parameter in $options
        $this->name = $options['name'];

        // Check presence of required "options" (array) parameter in $options
        $this->options = $options['options'];
    }
}

CountryFieldset.php - 在上面被 CountryForm.php 用作基础字段集

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

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

AbstractFieldset.php

abstract class AbstractFieldset extends Fieldset
{
    use InputFilterAwareTrait;
    use TranslatorAwareTrait;

    protected $entityManager;

    public function __construct(EntityManager $entityManager, $name)
    {
        parent::__construct($name);

        $this->setEntityManager($entityManager);
    }

    public function init()
    {
        $this->add([
            'name' => 'id',
            'type' => Hidden::class,
        ]);
    }

    // Getters/setters for $entityManager
}

CountryFieldsetFactory.php - 此处将输入过滤器设置到 FIELDSET

class CountryFieldsetFactory extends AbstractFieldsetFactory
{
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        parent::createService($serviceLocator);

        /** @var CountryRepository $entityRepository */
        $entityRepository = $this->getEntityManager()->getRepository(Country::class);

        $fieldset = new CountryFieldset($this->getEntityManager(), $this->name);
        $fieldset->setHydrator(new DoctrineObject($this->getServiceManager()->get('doctrine.entitymanager.orm_default'), false));
        $fieldset->setObject(new Country());
        $fieldset->setTranslator($this->getTranslator());

        // HERE THE INPUTFILTER IS SET ONTO THE FIELDSET THAT WAS JUST CREATED
        $fieldset->setInputFilter(
            $this->getServiceManager()->get('InputFilterManager')->get(
                CountryInputFilter::class,
                [ // These are the options read by the MutableCreationOptionsInterface
                    'object_manager' => $this->getEntityManager(),
                    'object_repository' => $entityRepository,
                    'translator' => $this->getTranslator(),
                ]
            )
        );

        return $fieldset;
    }
}

AbstractFieldsetFactory.php

abstract class AbstractFieldsetFactory implements FactoryInterface, MutableCreationOptionsInterface
{

    protected $serviceManager;
    protected $entityManager;
    protected $translator;
    protected $name;


    public function setCreationOptions(array $options)
    {
        $this->name = $options['name'];
    }

    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        /** @var ServiceLocator $serviceManager */
        $this->serviceManager = $serviceLocator->getServiceLocator();

        /** @var EntityManager $entityManager */
        $this->entityManager = $this->getServiceManager()->get('Doctrine\ORM\EntityManager');

        /** @var Translator $translator */
        $this->translator = $this->getServiceManager()->get('translator');
    }

    // Getters/setters for properties
}

CountryFieldsetInputFilter.php

class CountryInputFilter extends AbstractInputFilter
{
    public function init()
    {
        parent::init();

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

        // More adding
    }
}

AbstractFieldsetInputFilter.php - 最后一个! :)

abstract class AbstractInputFilter extends InputFilter
{
    use TranslatorAwareTrait;

    protected $repository;
    protected $objectManager;

    public function __construct(array $options)
    {
        // Check if ObjectManager|EntityManager for InputFilter is set
        $this->setObjectManager($options['object_manager']);

        // Check if EntityRepository instance for InputFilter is set
        $this->setRepository($options['object_repository']);

        // Check for presence of translator so as to translate return messages
        $this->setTranslator($options['translator']);
    }

    public function init()
    {
        $this->add([
            'name' => 'id',
            'required' => false,
            'filters' => [
                ['name' => ToInt::class],
            ],
            'validators' => [
                ['name' => IsInt::class],
            ],
        ]);
    }

    //Getters/setters for properties
}

非常感谢任何帮助。希望上面的代码不会让您负担过重。但是我已经反复研究这个问题大约 3-4 天,并没有发现问题所在。

总结一下:

在上面创建了一个 CountryForm。它使用预加载(在 CountryFieldsetFactory 中)的 CountryFieldsetCountryInputFilter

在验证数据时,一切都被认为是有效的。例如。 - 国家名称 "ab" 是有效的,尽管 StringLength 验证器将 'min' => 3, 定义为选项。

$fieldset->setInputFilter($inputFilter); 

假设 $fieldsetZend\Form\Fieldset 的实例,该方法不存在。这是因为您需要在 表单 上设置输入过滤器 (Zend\Form\Form).

如果我要修改您的代码,我会按以下方式进行。修改 CountryForm 以通过表单提供输入过滤器。这可以在不需要为每个表单定义自定义输入过滤器的情况下完成,方法是使用 Zend\InputFilter\InputFilterProviderInterface 并在 type 键下引用您的自定义输入过滤器。

当表单元素管理器创建表单时,它还会注入能够找到自定义 CountryInputFilter.

的输入过滤器管理器

例如:

use Zend\InputFilter\InputFilterProviderInterface;

class CountryForm extends AbstractForm implements InputFilterProviderInterface
{

    public function init() 
    { 
        //.. add the country fieldset here
    }

    /**
     * getInputFilterSpecification
     *
     * Return the form's input filter specification array.
     *
     * @return array
     */
    public function getInputFilterSpecification()
    {
        return [
            // Refers to the country element (happens to be a fieldset)
            'country' => [

                // Tell the input filter factory what type of input filter.
                'type' => 'Country\InputFilter\CountryInputFilter',

                // You could even overload/change the default defined in the input filter
                // here. This is useful as sometimes the requirements of the FORM are 
                // different from what the FIELDSET expects.
                // [
                //     'name'        => 'name',
                //     'required'    => false,
                //     'allow_empty' => true,
                // ],
            ],
        ];
    }

}

由于您已经设置了所有 类 ,因此还有另一种方法(来自@AlexP),即构建 Fieldset 的 InputFilters 并将其添加到 Forms InputFilter。而不是使用 InputFilterSpecifications。

因此,将输入过滤器添加到您的 input_filters 配置键中:

'form_elements' => [
    'factories' => [
        CountryForm::class => CountryFormFactory::class,
        CountryFieldset::class => CountryFieldsetFactory::class,
    ],
],
'input_filters' => [
    'factories' => [
        CountryInputFilter::class => CountryInputFilterFactory::class,
        CountryFieldsetInputFilter::class => CountryFieldsetInputFilterFactory::class,
    ],
],

工厂类:

class CountryInputFilterFactory implements FactoryInterface
{
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        $serviceManager = $serviceLocator->getServiceLocator();

        $inputFilter = new CountryInputFilter(
            $serviceLocator->get(CountryFieldsetInputFilter::class),
            $serviceManager()->get('Doctrine\ORM\EntityManager'),
            $serviceManager()->get('translator')
        );

        return $inputFilter;
    }
}

class CountryFieldsetInputFilterFactory implements FactoryInterface
{
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        $serviceManager = $serviceLocator->getServiceLocator();

        return new CountryFieldsetInputFilter(
            $serviceManager()->get('Doctrine\ORM\EntityManager'),
            $serviceManager()->get('translator')
        );
    }
}

class CountryFormFactory implements AbstractFormFactory
{
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        $serviceManager = $serviceLocator->getServiceLocator();

        $form = new CountryForm($this->name, $this->options);
        $form->setObjectManager($serviceManager->get('Doctrine\ORM\EntityManager'));
        $form->setTranslator($serviceManager->get('translator'));

        $form->setInputFilter($serviceManager->get('InputFilterManager')->get(CountryInputFilterFactory::class));
        return $form;
    }
}

表格:

class CountryForm extends AbstractForm
{
    const COUNTRY_FIELDSET_NAME = 'country';

    // This one added for SO, does nothing but call parent#__construct, which would happen anyway
    public function __construct($name = null, array $options)
    {
        parent::__construct($name, $options);
    }

    public function init()
    {
        //Call parent initializer.
        parent::init();

        $this->add([
            'name' => self::COUNTRY_FIELDSET_NAME,
            'type' => CountryFieldset::class,
            'options' => [
                'use_as_base_fieldset' => true,
            ],
        ]);
    }
}

输入过滤器:

class CountryInputFilter extends AbstractInputFilter
{
    /** @var CountryFieldsetInputFilter  */
    protected $countryFieldsetInputFilter;

    public function __construct(CountryFieldsetInputFilter $filter, $objectManager, $translator)
    {
        $this->countryFieldsetInputFilter = $filter;
        // other dependencies
    }

    public function init()
    {
        $this->add($this->countryFieldsetInputFilter, CountryForm::COUNTRY_FIELDSET_NAME);
    }
}

class CountryFieldsetInputFilter extends AbstractInputFilter
{
    public function __construct($objectManager, $translator)
    {
        // dependencies
    }

    public function init()
    {
        $this->add([
            // configuration
        ]);
    }
}

请注意,我将依赖项按实例而不是包含实例的数组注入到 InputFilters 中。