Как применить валидаторы InputFilter к элементам набора полей в ZF3

У меня была форма с двумя полями. К нему был применен InputFilter с валидаторами. Работало нормально. Затем я переместил поля в набор полей и добавил набор полей в форму. Теперь валидаторы назначения к полям нет. Объекты валидатора isValid метод не срабатывает вообще. Итак, как применить валидаторы InputFilter к полям в наборе полей? Вот такие у вас занятия:

Текстовый класс Validator

namespace Application\Validator;

use Zend\Validator\StringLength;
use Zend\Validator\ValidatorInterface;

class Text implements ValidatorInterface
{
    protected $stringLength;
    protected $messages = [];

    public function __construct()
    {
        $this->stringLengthValidator = new StringLength();
    }

    public function isValid($value, $context = null)
    {       
        if (empty($context['url'])) {
            if (empty($value)) return false;
            $this->stringLengthValidator->setMin(3);
            $this->stringLengthValidator->setMax(5000);

            if ($this->stringLengthValidator->isValid($value)) {
                return true;
            }
            $this->messages = $this->stringLengthValidator->getMessages();

            return false;
        }
        if (!empty($value)) return false;
        return true;
    }

    public function getMessages()
    {
        return $this->messages;
    }
}

Тестовый класс InputFilter

namespace Application\Filter;

use Application\Fieldset\Test as Fieldset;
use Application\Validator\Text;
use Application\Validator\Url;
use Zend\InputFilter\InputFilter;

class Test extends InputFilter
{
    public function init()
    {
        $this->add([
            'name' => Fieldset::TEXT,
            'required' => false,
            'allow_empty' => true,
            'continue_if_empty' => true,
            'validators' => [
                ['name' => Text::class],
            ],
        ]);
        $this->add([
            'name' => Fieldset::URL,
            'required' => false,
            'allow_empty' => true,
            'continue_if_empty' => true,
            'validators' => [
                ['name' => Url::class],
            ],
        ]);
    }
}

Тестовый класс Fieldset

namespace Application\Fieldset;

use Zend\Form\Fieldset;

class Test extends Fieldset
{
    const TEXT = 'text';
    const URL = 'url';
    public function init()
    {
        $this->add([
            'name' => self::TEXT,
            'type' => 'textarea',
            'attributes' => [
                'id' => 'text',
                'class' => 'form-control',
                'placeholder' => 'Type text here',
                'rows' => '6',
            ],
            'options' => [
                'label' => self::TEXT,

            ],
        ]);
        $this->add([
            'name' => self::URL,
            'type' => 'text',
            'attributes' => [
                'id' => 'url',
                'class' => 'form-control',
                'placeholder' => 'Type url here',
            ],
            'options' => [
                'label' => self::URL,

            ],
        ]);
    }
}

Форма теста

namespace Application\Form;

use Application\Fieldset\Test as TestFieldset;
use Zend\Form\Form;

class Test extends Form
{
    public function init()
    {
        $this->add([
            'name' => 'test',
            'type' => TestFieldset::class,
            'options' => [
                'use_as_base_fieldset' => true,
            ],
        ]);
        $this->add([
            'name' => 'submit',
            'attributes' => [
                'type' => 'submit',
                'value' => 'Send',
            ],
        ]);
    }
}

Класс TestController

namespace Application\Controller;

use Application\Form\Test as Form;
use Zend\Debug\Debug;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;

class TestController extends AbstractActionController
{
    private $form;

    public function __construct(Form $form)
    {
        $this->form = $form;
    }

    public function indexAction()
    {
        if ($this->getRequest()->isPost()) {
            $this->form->setData($this->getRequest()->getPost());
            Debug::dump($this->getRequest()->getPost());
            if ($this->form->isValid()) {
                Debug::dump($this->form->getData());
                die();
            }
        }
        return new ViewModel(['form' => $this->form]);
    }
}

Класс TestControllerFactory

namespace Application\Factory;

use Application\Controller\TestController;
use Application\Form\Test;
use Interop\Container\ContainerInterface;
use Zend\ServiceManager\Factory\FactoryInterface;

class TestControllerFactory implements FactoryInterface
{
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        $form = $container->get('FormElementManager')->get(Test::class);

        return new TestController($form);
    }
}

Тестовый класс

namespace Application\Factory;

use Application\Filter\Test as Filter;
use Application\Entity\Form as Entity;
use Application\Form\Test as Form;
use Interop\Container\ContainerInterface;
use Zend\Hydrator\ClassMethods;
use Zend\ServiceManager\Factory\FactoryInterface;

class Test implements FactoryInterface
{
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        return (new Form())
            ->setHydrator($container
                ->get('HydratorManager')
                ->get(ClassMethods::class))
            ->setObject(new Entity())
            ->setInputFilter($container->get('InputFilterManager')->get(Filter::class));
    }
}

Test Fieldset

namespace Application\Factory;

use Application\Entity\Fieldset as Entity;
use Application\Fieldset\Test as Fieldset;
use Interop\Container\ContainerInterface;
use Zend\Hydrator\ClassMethods;
use Zend\ServiceManager\Factory\FactoryInterface;

class TestFieldset implements FactoryInterface
{
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        return (new Fieldset())
            ->setHydrator($container->get('HydratorManager')->get(ClassMethods::class))
            ->setObject(new Entity());
    }
}

ОБНОВИТЬ

Я обновил класс fieldset в соответствии с рекомендациями @Nukeface, добавив setInputFilter(), Но это не сработало. Он даже не выполнил класс InpuFilter init метод. Возможно, я поступил неправильно:

<?php

namespace Application\Fieldset;

use Application\Filter\Test as Filter;
use Zend\Form\Fieldset;
use Zend\InputFilter\InputFilterAwareTrait;

class Test extends Fieldset
{
    use InputFilterAwareTrait;

    const TEXT = 'text';
    const URL = 'url';

    public function init()
    {
        $this->add([
            'name' => self::TEXT,
            'type' => 'textarea',
            'attributes' => [
                'id' => 'text',
                'class' => 'form-control',
                'placeholder' => 'Type text here',
                'rows' => '6',
            ],
            'options' => [
                'label' => self::TEXT,

            ],
        ]);
        $this->add([
            'name' => self::URL,
            'type' => 'text',
            'attributes' => [
                'id' => 'url',
                'class' => 'form-control',
                'placeholder' => 'Type url here',
            ],
            'options' => [
                'label' => self::URL,

            ],
        ]);
        $this->setInputFilter(new Filter());
    }
}

2 ответа

Попробовал ответ раньше и у него закончились символы (ограничение 30 тыс.), Поэтому вместо этого создал репо. Репозиторий содержит абстракцию ответа ниже, который является рабочим примером.

Ваш вопрос показывает, что у вас есть правильная идея, но пока нет реализации. Он также содержит несколько ошибок, таких как установка FQCN для имени Fieldset. Надеюсь, что нижеприведенное может помочь вам.

В качестве варианта использования у нас будет базовая форма адреса. Отношения для Страны, Часовые пояса и другие вещи, которые я оставлю вне рамок. Для более подробной информации и вложенности полей (также с коллекциями) я отошлю вас к моему репо.


Общая настройка

Сначала создайте базовую настройку. Создайте сущность и конфигурацию.

Основной объект

namespace Demo\Entity;

class Address
{
    protected $id;     // int - primary key - unique - auto increment
    protected $street; // string - max length 255 - not null
    protected $number; // int - max length 11 - not null
    protected $city;   // string - max length 255 - null

    // getters/setters/annotation/et cetera
}

Для обработки этого универсального и многократного использования нам понадобится:

  • AddressForm (общий контейнер)
  • AddressFormFieldset (форма должна быть проверена)
  • AddressFieldset (содержит входы объекта)
  • AddressFieldsetInputFilter (необходимо проверить введенные данные)
  • AddressController (для обработки действий CRUD)
  • Фабричные классы для всего вышеперечисленного
  • частичная форма

конфигурация

Чтобы связать их вместе в Zend Framework, они должны быть зарегистрированы в конфигурации. С четкими именами, вы уже можете добавить их. Если вы используете что-то вроде PhpStorm в качестве вашей IDE, вы можете оставить это до последнего, как use Заявления могут быть сгенерированы для вас.

Поскольку это объяснение, я покажу вам сейчас. Добавьте это в конфигурацию вашего модуля:

// use statements here
return [
    'controllers' => [
        'factories' => [
            AddressController::class => AddressControllerFactory::class,
        ],
    ],
    'form_elements' => [ // <-- note: both Form and Fieldset classes count as Form elements
        'factories' => [
            AddressForm::class => AddressFormFactory::class,
            AddressFieldset::class => AddressFieldsetFactory::class,
        ],
    ],
    'input_filters' => [ // <-- note: input filter classes only!
        'factories' => [
            AddressFormInputFilter::class => AddressFormInputFilterFactory::class,
            AddressFieldsetInputFilter::class => AddressFieldsetInputFilterFactory::class,
        ],
    ],
    'view_manager' => [
        'template_map' => [
            'addressFormPartial'   => __DIR__ . '/../view/partials/address-form.phtml',
    ],
];

Fieldset

Сначала мы создаем класс Fieldset (и Factory). Это потому, что он содержит фактический объект, который мы собираемся обработать.

AddressFieldset

// other use statements for Elements
use Zend\Form\Fieldset;

class AddressFieldset extends Fieldset
{
    public function init()
    {
        parent::init(); // called due to inheritance

        $this->add([
            'name' => 'id',
            'type' => Hidden::class,
        ]);

        $this->add([
            'name' => 'street',
            'required' => true,
            'type' => Text::class,
            'options' => [
                'label' => 'Name',
            ],
            'attributes' => [
                'minlength' => 1,
                'maxlength' => 255,
            ],
        ]);

        $this->add([
            'name' => 'number',
            'required' => true,
            'type' => Number::class,
            'options' => [
                'label' => 'Number',
            ],
            'attributes' => [
                'step' => 1,
                'min' => 0,
            ],
        ]);

        $this->add([
            'name' => 'city',
            'required' => false,
            'type' => Text::class,
            'options' => [
                'label' => 'Name',
            ],
            'attributes' => [
                'minlength' => 1,
                'maxlength' => 255,
            ],
        ]);
    }
}

AddressFieldsetFactory

// other use statements
use Zend\ServiceManager\Factory\FactoryInterface;

class AddressFieldsetFactory implements FactoryInterface
{
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        $this->setEntityManager($container->get(EntityManager::class));

        /** @var AddressFieldset $fieldset */
        $fieldset = new AddressFieldset($this->getEntityManager(), 'address');
        $fieldset->setHydrator(
            new DoctrineObject($this->getEntityManager())
        );
        $fieldset->setObject(new Address());

        return $fieldset;
    }
}

InputFilter

Выше мы создали Fieldset. Это позволяет генерировать Fieldset для формы. В то же время Zend Framework также имеет значения по умолчанию, уже установленные для каждого типа ввода (например, 'type' => Text::class). Однако, если мы хотим проверить его в соответствии со своими собственными, более строгими стандартами, нам необходимо переопределить значения по умолчанию. Для этого нам нужен класс InputFilter.

AddressFieldsetInputFilter

// other use statements
use Zend\InputFilter\InputFilter;

class AddressFieldsetInputFilter extends InputFilter
{
    public function init()
    {
        parent::init(); // called due to inheritance

        $this->add([
            'name' => 'id',
            'required' => true,
            'filters' => [
                ['name' => ToInt::class],
            ],
            'validators' => [
                ['name' => IsInt::class],
            ],
        ]);

        $this->add([
            'name' => 'street',
            'required' => true,
            'filters' => [
                ['name' => StringTrim::class], // remove whitespace before & after string
                ['name' => StripTags::class],  // remove unwanted tags 
                [                              // if received is empty string, set to 'null'
                    'name' => ToNull::class,
                    'options' => [
                        'type' => ToNull::TYPE_STRING, // also supports other types
                    ],
                ],
            ],
            'validators' => [
                [
                    'name' => StringLength::class, // set min/max string length
                    'options' => [
                        'min' => 1,
                        'max' => 255,
                    ],
                ],
            ],
        ]);

        $this->add([
            'name' => 'number',
            'required' => true,
            'filters' => [
                ['name' => ToInt::class],    // received from HTML form always string, have it cast to integer
                [
                    'name' => ToNull::class, // if received is empty string, set to 'null'
                    'options' => [
                        'type' => ToNull::TYPE_INTEGER,
                    ],
                ],
            ],
            'validators' => [
                ['name' => IsInt::class], // check if actually integer
            ],
        ]);

        $this->add([
            'name' => 'city',
            'required' => false, // <-- not required
            'filters' => [
                ['name' => StringTrim::class], // remove whitespace before & after string
                ['name' => StripTags::class],  // remove unwanted tags 
                [                              // if received is empty string, set to 'null'
                    'name' => ToNull::class,
                    'options' => [
                        'type' => ToNull::TYPE_STRING, // also supports other types
                    ],
                ],
            ],
            'validators' => [
                [
                    'name' => StringLength::class, // set min/max string length
                    'options' => [
                        'min' => 1,
                        'max' => 255,
                    ],
                ],
            ],
        ]);
    }
}

AddressFieldsetInputFilterFactory

// other use statements
use Zend\ServiceManager\Factory\FactoryInterface;

class AddressFieldsetInputFilterFactory implements FactoryInterface
{
   public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        // Nothing else required in this example. So it's as plain as can be.
        return new AddressFieldsetInputFilter();
    }
}

Форма и проверка

Так. Выше мы создали Fieldset, его InputFilter и 2 обязательных класса Factory. Это уже позволяет нам многое сделать, например:

  • использовать InputFilter в автономном режиме для динамической проверки объекта
  • повторно использовать комбинацию Fieldset + InputFilter в других классах Fieldset и InputFilter для вложения

форма

use Zend\Form\Form;
use Zend\InputFilter\InputFilterAwareInterface;
// other use statements

class AddressForm extends Form implements InputFilterAwareInterface
{
    public function init()
    {
        //Call parent initializer. Check in parent what it does.
        parent::init();

        $this->add([
            'type'    => Csrf::class,
            'name'    => 'csrf',
            'options' => [
                'csrf_options' => [
                    'timeout' => 86400, // day
                ],
            ],
        ]);

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

        $this->add([
            'name'       => 'submit',
            'type'       => Submit::class,
            'attributes' => [
                'value' => 'Save',
            ],
        ]);
    }
}

Фабрика форм

use Zend\ServiceManager\Factory\FactoryInterface;
// other use statements

class AddressFormFactory implements FactoryInterface
{
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        /** @var AbstractForm $form */
        $form = new AddressForm('address', $this->options);
        $form->setInputFilter(
            $container->get('InputFilterManager')->get(ContactFormInputFilter::class);
        );

        return $form;
    }
}

Делать все это вместе

Я покажу только AddressController#addAction

AddressController

use Zend\Mvc\Controller\AbstractActionController;
// other use statements

class AddressController extends AbstractActionController
{
    protected $addressForm;   // + getter/setter
    protected $entityManager; // + getter/setter

    public function __construct(
        EntityManager $entityManager, 
        AddressForm $form
    ) {
        $this->entityManager = $entityManager;
        $this->addressForm = $form;
    }

    // Add your own: index, view, edit and delete functions

    public function addAction () {
        /** @var AddressForm $form */
        $form = $this->getAddressForm();

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

            if ($form->isValid()) {
                $entity = $form->getObject();
                $this->getEntityManager()->persist($entity);

                try {
                    $this->getEntityManager()->flush();
                } catch (\Exception $e) {
                    $this->flashMessenger()->addErrorMessage($message);

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

                $this->flashMessenger()->addSuccessMessage(
                    'Successfully created object.'
                );

                return $this->redirect()->route($route, ['param' => 'routeParamValue']);
            }

            $this->flashMessenger()->addWarningMessage(
                'Your form contains errors. Please correct them and try again.'
            );
        }

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

AddressControllerFactory

class AddressControllerFactory implements FactoryInterface
{

    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        /** @var AddressController $controller */
        $controller = new AddressController(
            $container->get(EntityManager::class), 
            $container->get('FormElementManager')->get(AddressForm::class);
        );

        return $controller;
    }
}

Показать в addressFormPartial

$this->headTitle('Add address');

$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('number'));
echo $this->formRow($form->get('address')->get('city'));

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

Чтобы использовать это частичное, скажем, в add.phtml просматривать, использовать:

<?= $this->partial('addressFormPartial', ['form' => $form]) ?>

Этот бит кода будет работать с продемонстрированным addAction в коде контроллера выше.


Надеюсь, вы нашли это полезным;-) Если у вас есть какие-либо вопросы, не стесняйтесь спрашивать.

Просто используйте InputFilterProviderInterface класс для вашего fieldset. Это реализует getInputFilterSpecification метод для вашего fieldset, который выполняет входные фильтры, упомянутые в этом методе.

class MyFieldset extends Fieldset implements InputFilterProviderInterface
{
    public function init()
    {
        $this->add([
            'name' => 'textfield',
            'type' => Text::class,
            'attributes' => [
                ...
            ],
            'options' => [
                ...
            ]
        ]);
    }

    public function getInputFilterSpecification()
    {
        return [
            'textfield' => [
                'required' => true,
                'filters' => [
                    ...
                ],
                'validators' => [
                    [
                        'name' => YourTextValidator::class,
                        'options' => [
                            ...
                        ],
                    ],
                ],
            ],
        ];
    }
}

Пока вы добавляете этот набор полей в форму, связанные фильтры и валидаторы будут выполняться на isValid вызов метода вашей формы.

Другие вопросы по тегам