Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
451 views
in Technique[技术] by (71.8m points)

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

I've set up a structure using Abstract classes for Forms, Fieldsets and InputFilters. Forms and Fieldsets have Factories while InputFilters are created and set on the Fieldsets by the FieldsetFactory (uses the MutableCreationOptionsInterface to pass along options)

The problem I have is that InputFilters are loaded for the Form, but are not used to validate the data. All input is accepted as valid.

E.g. I have a Country Entity with a name property. The name of the Country must be at least 3 chars and max 255. When the name is "ab", it's found to be valid.

Before someone asks: no error is thrown, the data is just accepted as valid.

I've been breaking my head over this for the passed few days trying to find where I've made a mistake, but cannot find it.

Also, there's quite a bit of code. I've limited it to what I think is relevant, although: if you need more, there will be more. I've removed a lot of type checking, docblocks and type hints to limit code lines/reading time ;)

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
     * @ORMColumn(name="name", type="string", length=255, nullable=false)
     */
    protected $name;

    // Other properties
    // Getters/setters
}

CountryController.php - Extends the 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 - (goes wrong in here) - Used by 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('DoctrineORMEntityManager');

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

        return $form;
    }
}

AbstractFormFactory.php - Uses MutableCreationOptionsInterface to receive options from the Controller function call: $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 - Used above by CountryForm.php as the base fieldset

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 - IN HERE THE INPUTFILTER IS SET ONTO THE 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('DoctrineORMEntityManager');

        /** @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 - Last one! :)

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
}

Any help would very much be appreciated. Hopefully you're not too overloaded with the code above. But I've been mashing this issue back'n'forth for about 3-4 days and haven't stumbled onto what's going wrong.

To sum up:

In the above a CountryForm is created. It uses the CountryFieldset which gets preloaded (in CountryFieldsetFactory) with the CountryInputFilter.

When it comes to validating the data, everything is accepted as valid. E.g. - Country name "ab" is valid, though StringLength validator has 'min' => 3, defined as option.

See Question&Answers more d

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

Since you've got all the classes set up already there is another approach (from @AlexP), by constructing and adding the InputFilters of the Fieldsets to the Forms InputFilter. Instead of using the InputFilterSpecifications.

So add the input filters to your input_filters config key:

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

Factory classes:

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

        $inputFilter = new CountryInputFilter(
            $serviceLocator->get(CountryFieldsetInputFilter::class),
            $serviceManager()->get('DoctrineORMEntityManager'),
            $serviceManager()->get('translator')
        );

        return $inputFilter;
    }
}

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

        return new CountryFieldsetInputFilter(
            $serviceManager()->get('DoctrineORMEntityManager'),
            $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('DoctrineORMEntityManager'));
        $form->setTranslator($serviceManager->get('translator'));

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

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

InputFilters:

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
        ]);

    }
}

Note that I injected the dependencies to the InputFilters per argument by instance instead of an array holding the instances.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...