ZendFramework 2 - removing InputFilter causes misbehavior in custom filter

135 Views Asked by At

I have an registration form User\UserForm which contains a fieldset User\UserFieldset. In User\UserFieldset I put an field called "passwordVerify" which should be identical with another field in the fieldset called "password". This works fine.

However, if an admin wants to modify an user account within the Admin\UserForm, which also contains the User\UserFieldset, the field "passwordVerify" in the fieldset User\UserFieldset should be removed. Therefore I call the following within the Admin\UserForm:

$this->get('user')->remove('passwordVerify');
$this->getInputFilter()->get('user')->remove('passwordVerify')

As expected, the form lacks the field "passwordVerify" now.

If I save the form after editing some stuff, my custom filter "PasswordFilter" cannot retrieve the bound object of the fieldset anymore ($this->getOption('object'); returns an User-object) - but all properties of the bound object are nulled. If I use the Admin\UserForm without removing "passwordVerify"-field and "passwordVerify"-inputfilter everything works fine and the bound object is passed to "PasswordFilter" with populated properties (in respect of values, inserted by the user in the Admin\UserForm). The line which breaks everything is $this->getInputFilter()->get('user')->remove('passwordVerify'). So this leads to my assumption, that by removing an inputfilter, the hydrated object gets somehow nulled / emptied. Below are my some excerpts of my code, if needed I can provide more information about factories, etc.

Admin\UserForm:

class UserForm extends Form
{

    /**
     * @var EntityManager
     */
    protected $entityManager = null;

    /**
     * @var Translator
     */
    protected $translator = null;

    public function __construct(EntityManager $entityManager, Translator $translator)
    {
        $this->entityManager = $entityManager;
        $this->translator = $translator;

        parent::__construct("userForm");
        $this->setHydrator(new DoctrineHydrator($entityManager));
    }

    public function init()
    {
        // Adding UserFieldset
        $this->add(array(
            'name' => 'user',
            'type' => \User\Form\UserFieldset::class,
            'options' => array(
                'use_as_base_fieldset' => true,
            ),
        ));

        $this->get('user')->remove('passwordVerify');
        $this->getInputFilter()->get('user')->remove('passwordVerify');

        $this->add(array(
            'type' => 'Zend\Form\Element\Csrf',
            'name' => 'csrf',
        ));

        $this->add(array(
            'type' => 'submit',
            'name' => 'submit',
            'options' => array(
                'label' => $this->translator->translate('Btn.submit.user', 'Form')
            ),
        ));
    }
}

User\UserFieldset:

class UserFieldset extends Fieldset implements InputFilterProviderInterface
{

    /**
     * @var EntityManager
     */
    protected $entityManager = null;

    /**
     * @var Translator
     */
    protected $translator = null;

    public function __construct(EntityManager $entityManager, Translator $translator)
    {
        $this->entityManager = $entityManager;
        $this->translator = $translator;

        parent::__construct("userFieldset");
        $this->setHydrator(new DoctrineHydrator($entityManager))->setObject(new User());
    }

    public function init()
    {
        $this->add(array(
            'type' => 'text',
            'name' => 'firstName',
            'options' => array(
                'label' => $this->translator->translate('label.firstName', 'Form'),
                'label_attributes' => array(
                    'class' => 'col-sm-3',
                ),
                'column-size' => 'sm-5',
            ),
            'attributes' => array(
                'id' => 'firstName',
            ),
        ));
        /* ... */
        $this->add(array(
            'type' => 'text',
            'name' => 'password',
            'options' => array(
                'label' => $this->translator->translate('label.password', 'Form'),
                'label_attributes' => array(
                    'class' => 'col-sm-3',
                ),
                'column-size' => 'sm-5',
            ),
            'attributes' => array(
                'id' => 'password',
            ),
        ));

        $this->add(array(
            'type' => 'password',
            'name' => 'passwordVerify',
            'options' => array(
                'label_attributes' => array(
                    'class' => 'col-sm-3 control-label'
                ),
                'label' => $this->translator->translate('label.verifyPassword', 'Form'),
                'column-size' => 'sm-8',
            ),
            'attributes' => array(
                'class' => 'form-control',
                'id' => 'password'),
        ));

         /* ... */

        // Adding AddressFieldset
        $this->add(array(
            'name' => 'address',
            'type' => \User\Form\AddressFieldset::class,
        ));

         /* ... */

        $this->add(array(
            'type' => 'datetime',
            'name' => 'created',
            'options' => array(
                'label' => $this->translator->translate('label.created', 'Form'),
                'format' => 'd.m.Y H:i',
                'label_attributes' => array(
                    'class' => 'col-sm-3',
                ),
                'column-size' => 'sm-5',
            ),
            'attributes' => array(
                'id' => 'created',
            ),
        ));
    }

    public function getInputFilterSpecification()
    {
        return array(
            'firstName' => array(
                'required' => true,
                'filters' => array(
                    array('name' => 'StringTrim'),
                    array('name' => 'StripTags'),
                ),
                'validators' => array(),
            ),
            /* ... */
            'password' => array(
                'required' => true,
                'filters' => array(
                    array('name' => 'StringTrim'),
                    array('name' => 'StripTags'),
                    [
                        'name' => PasswordFilter::class,
                        'options' => [
                            'object' => $this->getObject(),
                            'field' => 'password'
                        ]
                    ]
                ),
                'validators' => array(),
            ),
            'passwordVerify' => array(
                'required' => true,
                'filters' => [
                    [
                        'name' => PasswordFilter::class,
                        'options' => [
                            'object' => $this->getObject(),
                            'field' => 'password'
                        ]
                    ]
                ],
                'validators' => array(
                    array(
                        'name' => 'StringLength',
                        'options' => array(
                            'min' => 6
                        )
                    ),
                    array(
                        'name' => 'Identical',
                        'options' => array(
                            'token' => 'password'
                        )
                    )
                )
            ),
           /* ... */
            'created' => array(
                'required' => false,
                'filters' => array(
                    array('name' => 'StringTrim'),
                ),
                'validators' => array(
                    array(
                        'name' => 'Date',
                        'options' => array('format' => 'd.m.Y H:i')
                    ),
                ),
            )
        );
    }
}

PasswordFilter:

class PasswordFilter extends AbstractFilter
{
    /** @var  EntityManager */
    protected $entityManager;

    /** @var  PasswordInterface */
    protected $passwordManager;

    /**
     * PasswordFilter constructor.
     * @param EntityManager $entityManager
     * @param PasswordInterface $passwordManager
     * @param array $options
     */
    public function __construct(EntityManager $entityManager, PasswordInterface $passwordManager, $options = [])
    {
        $this->entityManager = $entityManager;
        $this->passwordManager = $passwordManager;

        $this->options = $options;
    }

    public function filter($value)
    {
        $object = $this->getOption('object');
        $field = $this->getOption('field');
        $getter = 'get'.ucfirst($field);

        if (!$object || !$field) {
            throw new \Exception('Options "object" and "field" are required.');
        }

        if ($object->getId()) {
            $dbObject = $this->entityManager->getRepository(get_class($object))->find($object->getId());

            if ($value === $dbObject->{$getter}()) {
                return $value;
            }
        }

        // hash password here...
        return $this->passwordManager->create($value);
    }

    private function getOption($option)
    {
        if (array_key_exists($option, $this->options)) {
            return $this->options[$option];
        }
        return false;
    }
}

Any clues? Do I call remove inputfilter of "passwordVerify" to early in the process of instantiation? I also tested to remove the inputFilter and field after "$this->form->bind($user)" in my controller, which also works. Why it does not work then if I remove it in Admin\UserForm, which is in my opinion the cleaner way of managing the "passwordVerify"-stuff?

1

There are 1 best solutions below

1
On

If you call $this->getInputFilter(), the InputProviderInterface::getInputSpecification method in your UserForm is being called.

If you did not attached the object already, it cannot retrieve it. But I dont get why you would even need that. You are hashing the password if the value does not fit to the database value, but obviously the database value seems to be plain text as the input or why would you compare it?

IMHO you just should hash the password, no matter what the current value in your database is.

If you are using doctrine and the password wont change, it wont execute an UPDATE query anyway.