The Doge Prince
The Doge Prince

Reputation: 468

Zend Framework 2 Custom Validators for Forms

I'm trying to make a user registration form which checks for the complexity of the password field. I've written a custom validator to do this according to the documentation. This file lives in my 'User' module at User\src\User\Validator.

<?php

namespace User\Validator;

use Zend\Validator\AbstractValidator;

class PasswordStrength extends AbstractValidator {

const LENGTH = 'length';
const UPPER  = 'upper';
const LOWER  = 'lower';
const DIGIT  = 'digit';

protected $messageTemplates = array(
    self::LENGTH => "'%value%' must be at least 6 characters long",
    self::UPPER => "'%value% must contain at least one uppercase letter",
    self::LOWER => "'%value% must contain at least one lowercase letter",
    self::DIGIT => "'%value% must contain at least one digit letter"
);

public function isValid($value) {
    ... validation code ...
}
}

My problem arises in trying to use this validator in my user registration form. I tried adding the validator to the ServiceManager by configuring it in Module.php.

public function getServiceConfig() {
    return array(
        'invokables' => array(
            'PasswordStrengthValidator' => 'User\Validator\PasswordStrength'
        ),
    );
}

Then I added it to the input filter in User.php.

public function getInputFilter() {
    if (!$this->inputFilter) {
        $inputFilter = new InputFilter();
        $factory     = new InputFactory();

        $inputFilter->add($factory->createInput(array(
            'name'     => 'username',
            'required' => true,
            'validators' => array(
                array(
                    'name'    => 'StringLength',
                    'options' => array(
                        'encoding' => 'UTF-8',
                        'min'      => 1,
                        'max'      => 100,
                    ),
                ),
            ),
        )));

        $inputFilter->add($factory->createInput(array(
            'name'     => 'password',
            'required' => true,
            'validators' => array(
                array(
                    'name'    => 'PasswordStrengthValidator',
                ),
            ),
        )));

        $this->inputFilter = $inputFilter;
    }

    return $this->inputFilter;
}

However, when I access the form and hit the submit button, I get a ServiceNotFoundException.

Zend\ServiceManager\ServiceManager::get was unable to fetch or create an instance for PasswordStrengthValidator

Is there a problem with my ServiceManager configuration? I'm not even sure if this is the appropriate way to use a custom validator in the first place. I've found plenty of examples using ZF1, but the documentation and examples for ZF2 that I've found never extend beyond the writing of the validator to address its integration with forms, etc. Any advice would be greatly appreciated.

Upvotes: 10

Views: 19420

Answers (5)

Aleksandar Stojilkovic
Aleksandar Stojilkovic

Reputation: 497

Yes, it can be invokable as stated by Conti or as factory in case you need to inject Service manager for example, or Doctrine entity manager in following example. It is very elegant solution this way:

 'validators' => array(
        'invokables' => array(
            //'emailExist' => 'Application\MyValidation\EmailExistValidator',
        ),
        'factories' => array(
            'emailExist' => function ($vm) {
                $serviceLocator = $vm->getServiceLocator();
                $emailExistValidator = new \Application\MyValidation\EmailExistValidator();
                $doctrineEntityManager = $serviceLocator->get('Doctrine\ORM\EntityManager');
                $emailExistValidator->setObjectManager($doctrineEntityManager);
                return $emailExistValidator;
            },
        ),
    ),

Upvotes: 0

Conti
Conti

Reputation: 1273

Rufinus solution works like charm. The steps I followed to use a custom validator were (in my case an URL validator to check if the web actually exists):

1) In module.config.php

'validators' => array(
     'invokables' => array(
         'UrlValidator' => 'Application\Validators\UrlValidator'
     ),
 )

2) In path Application/src/Application/Validators/UrlValidator.php

namespace Application\Validators; 
use Zend\Validator\AbstractValidator;                     
class UrlValidator extends AbstractValidator{
    const NOTURL = 'NOTURL';

    protected $messageTemplates = array(
        self::NOTURL => 'Value should be a valid URL',
    );

    public function __construct(array $options = array()){
        parent::__construct($options);
    }

    public function isValid($value){
        $this->setValue($value);       
        if (!$this->validateurl($value)) {
            $this->error(self::NOTURL);
            return false;
        } 
        return true;
    }
    private function validateurl($url) {
        $ch = curl_init(); // initialize curl handle
        curl_setopt($ch, CURLOPT_URL, $url); // set url to post to
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HEADER, 1);
        $result = curl_exec($ch);

        $info = curl_getinfo($ch);
        $code = $info['http_code'];
        curl_close($ch);

        if ($code == substr($code, 0, 1) || substr($code, 0, 1) == '4' || substr($code, 0, 1) == '5') {
            return false;
        } else {
            return true;
        }
   }
}

3) In the form

  $inputFilter->add($factory->createInput(array(
                    'name' => 'url',                       
                    'validators' => array(
                        array(
                            'name' => 'NotEmpty',
                            'options' => array(
                                'messages' => array(
                                    'isEmpty' => 'URL is required'
                                )
                            )
                        ),
                        array(
                            'name'=>'Application\Validators\UrlValidator'
                        )
                    )
        )));

Extra) In case you want to use it anywhere in the app (i.e. inside a controller action), you can retrieve from the ValidatorManager plugin:

$urlValidator = $this->getServiceLocator()->get('ValidatorManager')->get('UrlValidator');

Upvotes: 1

Mathieu
Mathieu

Reputation: 96

Remove your invokable configuration.

And modify your validator settings to:

  $inputFilter->add($factory->createInput(array(
            'name'     => 'password',
            'required' => true,
            'validators' => array(
                array(
                    'name'    => 'User\Validator\PasswordStrength',
                ),
            ),
        )));

Work for me.

Upvotes: 3

Martin Vlach
Martin Vlach

Reputation: 121

you can try this workaround... registrer your validator in Module.php but with function getValidatorConfig or in module.config.php under key 'validators'.

public function getValidatorConfig() {
  return array(
    'invokables' => array(
        'PasswordStrengthValidator' => 'User\Validator\PasswordStrength'
    ),
  );
}

Then in your User.php, try this: (but you must have access to service locator, you can inject it from UserFactory etc.)

$validatorManager = $this->getServiceLocator()->get('ValidatorManager');
// here you can test $validatorManager->get('PasswordStrengthValidator');

$validatorChain = new ValidatorChain();
$validatorChain->setPluginManager($validatorManager);

$inputFilter = new InputFilter();   
$inputFilter->getFactory()->setDefaultValidatorChain($validatorChain);

This works for me.

Martin

Upvotes: 6

Adam Lundrigan
Adam Lundrigan

Reputation: 598

The "short name" validator loading you are attempting to use in your example only works if you register that short name / alias with the validator plugin manager (Zend\Validator\ValidatorPluginManager) first.

One alternative to this (and the way I do it) is to inject instances of necessary custom validators when creating the form filter object. This is the way ZfcUser does it:

// Service factory definition from Module::getServiceConfig
'zfcuser_register_form' => function ($sm) {
     $options = $sm->get('zfcuser_module_options');
     $form = new Form\Register(null, $options);
     $form->setInputFilter(new Form\RegisterFilter(
         new Validator\NoRecordExists(array(
             'mapper' => $sm->get('zfcuser_user_mapper'),
             'key'    => 'email'
         )),
         new Validator\NoRecordExists(array(
            'mapper' => $sm->get('zfcuser_user_mapper'),
            'key'    => 'username'
         )),
         $options
     ));
     return $form;
},

Source: https://github.com/ZF-Commons/ZfcUser/blob/master/Module.php#L100

Here, the two ZfcUser\Validator\NoRecordExists validator instances (one for email and one for username) are injected into the constructor of the input filter object for the registration form (ZfcUser\Form\RegisterFilter).

Then, inside the ZfcUser\Form\RegisterFilter class, the validators are added to the element definitions:

$this->add(array(
    'name'       => 'email',
    'required'   => true,
    'validators' => array(
        array(
            'name' => 'EmailAddress'
        ),
        // Constructor argument containing instance of the validator
        $emailValidator
    ),
));

Source: https://github.com/ZF-Commons/ZfcUser/blob/master/src/ZfcUser/Form/RegisterFilter.php#L37

I believe another alternative is to use the fully-qualified class name as the validator name (ie: "User\Validator\PasswordStrength" instead of just "PasswordStrengthValidator"), though i've never attempted this myself.

Upvotes: 5

Related Questions