Custom validators registered through ValidatorProviderInterface not found #4219

Closed
sparrowek opened this Issue Apr 13, 2013 · 16 comments

Comments

Projects
None yet

I have implemented ValidatorProviderInterface in my module config

public function getValidatorConfig()
{
return array(
'invokables' => array(
'Pesel' => 'PolishExtensions\Validator\Pesel',
),
);
}

when I issue:
var_dump($this->serviceLocator->get('ValidatorManager')->get('Pesel'));
in my controller class I got a correct Pesel object but when I declare:
'validators' => array(
array(
'name' => 'Pesel',
),

I got:
Zend\ServiceManager\ServiceManager::get was unable to fetch or create an instance for Pesel

I found it also reported at http://stackoverflow.com/questions/13476164/zend-framework-2-custom-validators-for-forms

Contributor

samsonasik commented Apr 14, 2013

use

 'validators' => array(
    'invokables' => array(
        'Pesel' => 'PolishExtensions\Validator\Pesel',
    ),
),

add 'invokables' key.

As I have mentioned I have the 'invokables' key but I used the getValidatorConfig() in module config, but it still does not see validator when using its short name

public function getValidatorConfig()
{
return array(
'invokables' => array(
'Pesel' => 'PolishExtensions\Validator\Pesel',
),
);
}

Contributor

samsonasik commented Apr 15, 2013

you should call it by ValidatorManager :

$validator = $this->getServiceLocator()
                             ->get('ValidatorManager')
                             ->get('Pesel');

i tried it and it just work.

timdev commented Apr 21, 2013

I'm noticing something similar. I need a factory, so in my Module.php:

  public function getValidatorConfig(){
    return array(
      'factories' => array(
        'SomeValidator' => function(ValidatorPluginManager $pm){         
          $validator = new Validator\SomeValidator();
          return $validator;
        }
      )
    );
  }

And then I try to attach it in a fieldset:


public function getInputFilterSpecification(){
  return array(
    'myField' => array(
      'required' => true,
      'filters'=>array(
        array('name'=>'Zend\Filter\StringTrim')
      ),
      'validators' => array(
        array('name'=>'SomeValidator')
      )

    )
  );
}

But it fails, and I get

Zend\Validator\ValidatorPluginManager::get was unable to fetch or create an instance for SomeValidator

I'm currently stepping through a bunch of servicemanager-related stuff trying to figure out what's going wrong, but it's rough going.

timdev commented Apr 21, 2013

So far, I've haven't been able to figure out a sane way to make the above work. Ultimately, what happens, is that ValidatorChain::attachByName() gets called, and in turns called ValidatorChain::plugin(), which instantiates a new ValidatorPluginManager. There doesn't seem to be an obvious way to make sure that ServiceManager's ValidationPluginManager gets injected all the way down through Form > Fieldset > InputFilter > Input > ValidationChain (along with factories along the way).

My workaround for now is to make my fieldset ServiceLocatorAware, so I can shove a properly initialized validator into the input filter spec like so:

class MyFieldset extends Fieldset implements ServiceLocatorAware{
  use ServiceLocatorAwareTrait;

// ...

  public function getInputFilterSpecification(){
    $myValidator = $this
      ->getServiceLocator()  //FormElementManager
      ->getServiceLocator()  //ServiceManager
      ->get('ValidatorManager')
      ->get('SomeValidator');

    return array(
      'fooInput' => array(
        'validators' => array(
          $myValidator
        )
      )
    );
  }
}

It's not an ideal solution, at least because of the lack of lazy instantiation, but it's acceptable I guess.

I would love to see a real solution, though. If anyone has a hint about a fix, I'd be more than happy to take a swing. After a reading/stepping through a bunch of code, and a couple of experiments, I don't see how to share the application-level ValidatorManager all the way down the scope chain.

Contributor

grizzm0 commented Apr 25, 2013

Edit: Made the validator ServiceLocatorAware and a invokable instead of factory and pulled the mapper along with $options from there instead.

I also just ran into this. After a bit of debugging I came to the same conclusion as you did. I have no idea from where I need to inject the ValidatorPluginManger in order to create a PR.

I solved it in a bit different way than you did until someone can fix this. The only problem is that I have no idea how to access the options set in attachByName() in my factory. Right now I've hardcoded the key to email.

    public function getFormElementConfig()
    {
        return array(
            'factories' => array(
                'Newsletter\Form\Newsletter' => function ($fem) {
                    $sm = $fem->getServiceLocator();
                    return new Form\Newsletter($sm->get('ValidatorManager'));
                },
            ),
        );
    }

Form clas

namespace Newsletter\Form;

use Zend\Form\Form;
use Zend\InputFilter\InputFilterProviderInterface;
use Zend\Validator\ValidatorChain;
use Zend\Validator\ValidatorPluginManager;

class Newsletter extends Form implements InputFilterProviderInterface
{
    public $validatorChain;

    public function __construct(ValidatorPluginManager $validatorPluginManager)
    {
        parent::__construct();

        $this->validatorChain = new ValidatorChain();
        $this->validatorChain->setPluginManager($validatorPluginManager);
    }

    public function init()
    {
        $this->setAttribute('method', 'post');

        $this->add(array(
            'name' => 'id',
            'type' => 'Hidden',
        ));

        $this->add(array(
            'name' => 'email',
            'type' => 'Email',
            'attributes' => array(
                'placeholder' => 'Email',
            ),
        ));

        $this->add(array(
            'name' => 'submit',
            'type' => 'Submit',
            'attributes' => array(
                'value' => 'Submit',
            ),
        ));
    }

    public function getInputFilterSpecification()
    {
        return array(
            'email' => array(
                'validators' => array(
                    array('name' => 'EmailAddress'),
                    clone $this->validatorChain->attachByName('NoRecordExists'/*, array('key' => 'email')*/),
                ),
            ),
        );
    }
}

ValidatorConfig

    public function getValidatorConfig()
    {
        return array(
            'factories' => array(
                'NoRecordExists' => function ($vpm) {
                    return new NoRecordExists(array(
                        'mapper' => ...,
                        'key'    => 'email',
                    ));
                },
            ),
        );
    }
Contributor

bakura10 commented Apr 25, 2013

grizzm0 what is the problem ? For your first message, it should work, I've some filters and validators declared like this and it works. However you're right that when using forms, those things are not correctly injected. You can find in the code some parts where the framework do things like that:

if (null === $validatorPluginManager) {
    $validatorPluginManager = new ValidatorPluginManager();
}

So obviously any invokables/factories that you could add is not available here... The thing is that we realize quite quickly that basically, a ZF 2 application has nearly no "new Something()", but instead everything is pulled from SM. A lot of components still don't have this assumption and still create object using new. The framework itself would need more factories to create its own objects and correctly inject dependencies, but this is a big job. Be sure that for ZF 3 we'll pay attention to that and I'll try to spend some time to make this more uniform.

As it is now, yes some parts need to be "hacked".

Contributor

h3lpd3sk commented May 3, 2013

Find it pretty difficult to understand what is going on at all...

Could not manage to set ValidatorManager through a ServiceFactory, keeps creating a new InputFilter somewhere

            'my-form' => function(ServiceManager $serviceManager){
                    //attempt 1
                    $validatorManager = $serviceManager->get('ValidatorManager');

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

                    $form = new \User\Form\Authenticate;
                    $form->getInputFilter()->getFactory()->setDefaultValidatorChain($validatorChain);

                    return $form;

                    //attempt 2
                    $validatorManager = $serviceManager->get('ValidatorManager');

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

                    $factory = new \Zend\InputFilter\Factory;
                    $factory->setDefaultValidatorChain($validatorChain);    

                    $form->getInputFilter()->setFactory($factory);
                    return $form;

                    //attempt 3
                    $validatorManager = $serviceManager->get('ValidatorManager');

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

                    $factory = new \Zend\InputFilter\Factory;
                    $factory->setDefaultValidatorChain($validatorChain);    

                    $inputFilter = new \Zend\InputFilter\InputFilter;
                    $inputFilter->setFactory($factory);

                    $form = new \User\Form\Authenticate;
                    $form->setInputFilter($inputFilter);

                    return $form;
            }

Ended up overwriting the getInputFilter method to use ServiceLocator to arrange everything.
Far from ideal, but I'm not formiliar enough with the whole ServiceManager package to make something better at this moment. Need to pull through serviceManager to use it though, costly, but at least loads my custom validators from the config.

namespace Base\Form;

use Zend\Validator\ValidatorChain;
use Zend\InputFilter\Factory;
use Zend\InputFilter\InputFilter;
use Zend\ServiceManager\ServiceLocatorAwareInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use Zend\Form\Form as ZendForm;

/**
 * @see https://github.com/zendframework/zf2/issues/4219
 */
class Form extends ZendForm implements ServiceLocatorAwareInterface
{
    /**
     * @var Zend\ServiceManager\ServiceManager
     */
    protected $serviceLocator = null;

    /**
     * Overwrite method to utilize ServiceLocator to provide proper ValidatorManager
     * 
     * @access public
     * @return InputFilter
     */
    public function getInputFilter()
    {
        $validatorManager = $this->getServiceLocator()->get('ValidatorManager');
        $validatorChain = new ValidatorChain;
        $validatorChain->setPluginManager($validatorManager);

        $factory = new Factory;
        $factory->setDefaultValidatorChain($validatorChain);    

        $inputFilter = new InputFilter;
        $inputFilter->setFactory($factory);

        //@see Zend\Form\Form #672
        if(!$this->hasAddedInputFilterDefaults
            && $this->filter instanceof InputFilterInterface
            && $this->useInputFilterDefaults())
        {
            $this->attachInputFilterDefaults($this->filter, $this);
            $this->hasAddedInputFilterDefaults = true;
        }
        return $inputFilter;
    }
    /**
     * Store ServiceLocator
     * 
     * @access public
     * @param $serviceLocator
     * @return ServiceLocatorAwareInterface
     */
    public function setServiceLocator(ServiceLocatorInterface $serviceLocator)
    {
        $this->serviceLocator = $serviceLocator;
        return $this;
    }
    /**
     * Retrieve ServiceLocator 
     * 
     * @access public
     * @return ServiceLocatorInterface
     */
    public function getServiceLocator()
    {
        return $this->serviceLocator;
    }
}

Hope someone can make a decent fix for this in the near future!
Nice challenge there...

Owner

weierophinney commented May 3, 2013

@h3lpd3sk Please check out Zend\Form\FormAbstractServiceFactory on current master (and in 2.2.0rc1). For examples of how it works, check out the unit tests (tests/ZendTest/Form/FormAbstractServiceFactoryTest.php). Basically, this abstract factory takes care of injecting the new InputFilterManager with the filter and validator managers, and the form with an input filter pulled from the InputFilterManager.

This ensures that the filter and validator chains are getting injected with the plugin managers prior to creating any inputs -- which was the issue you were hitting above (when you pulled the input filter from the form, you were actually triggering creation of the input filter and its tree, so injecting the default filter and validator chains was happening too late).

Owner

weierophinney commented May 3, 2013

Closing this issue, as the FormAbstractServiceFactory resolves this. That said, @h3lpd3sk -- please request it be re-opened if you still observe issues.

Contributor

h3lpd3sk commented May 3, 2013

My previously posted code doesn't work in the end it seems...
Glad to head it's solved in RC, Will have a see later on

Saeven commented Oct 28, 2013

I'm having a similar problem, using ZF 2.2.4. I have registered a factory in a Module as:

    public function getValidatorConfig()
    {
        return array(
            'abstract_factories' => array(
                '\LDP\Factory\AbstractValidatorFactory',
            ),
        );
    }

And this factory looks like:

namespace LDP\Factory;

use Zend\ServiceManager\AbstractFactoryInterface,
    Zend\ServiceManager\ServiceLocatorInterface;

class AbstractValidatorFactory implements AbstractFactoryInterface
{

    private $invokables = array(
        'LDP_PinAvailable' => '\LDP\Form\Validator\PinAvailable'
    );


    /**
     * Determine if we can create a service with name
     *
     * @param ServiceLocatorInterface $serviceLocator
     * @param                         $name
     * @param                         $requestedName
     * @return bool
     */
    public function canCreateServiceWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName)
    {
        // baked in for measure here
        return stristr($requestedName, 'LDP_PinAvailable' ) !== false;
    }



    public function createServiceWithName(ServiceLocatorInterface $locator, $name, $requestedName)
    {
        $validator = new $this->invokables[$requestedName]();

        if( $validator instanceof DatabaseFormValidatorInterface )
            $validator->setDatabase( $locator->get('mysql_slave') );

        return $validator;
    }
}

The form whose config I am using, that needs this validator, looks like:

'pin' => array(
                    'required' => true,
                    'filters'  => array(
                        array('name' => 'alnum'),
                        array('name' => 'stringtrim'),
                    ),
                    'validators' => array(
                        array( 'name' => 'LDP_PinAvailable' )
                    ),
                ),

However, the form explodes when comes to validation time. It just can't find the class. Testing the SM from within the controller that creates the class, creates it properly:

        $mycustomvalidator = $this->getServiceLocator()
            ->get('ValidatorManager')
            ->get('LDP_PinAvailable');

(this last part works well)

Sorry for my remedial explanation, have aged a bit over this today!

Owner

weierophinney commented Oct 30, 2013

@Saeven Please ask support questions on the mailing list or stack overflow.

I am encountering a similar issue with a validator factory that it is never used... Instead using factory AbstractPluginController invokes my validator (ignoring my defined validator factory in module.config.php):
AbstractPluginManager->createFromInvokable( ) instead of AbstractPluginManager->createFromFactory( ).
You can find (a lot) more detail here: http://stackoverflow.com/questions/35975653/zf2-custom-validator-via-factory.

faoliva commented Jan 20, 2017

if you want to use validators via annotation like:

@Validator({"name": "customValidator",  "options": {}})

In your Application onBoostrap method you can add:

$formConstructor		= $serviceManager->get('doctrine.formannotationbuilder.orm_default');
//Para poder utilizar nuestros propios validadores por medio de anotaciones hacemos lo siguiente:
//Obtenemos la factory de formularios
$formFactory			= $formConstructor->getFormFactory();
//Obtenemos la factory de input filter
$inputFilterFactory		= $formFactory->getInputFilterFactory();
//Obtenemos nuestro plugin manager
$customValidatorPluginManager	= $serviceManager->get('CustomValidatorManager');
//Seteamos nuestro propio ValidatorPluginManager.
$inputFilterFactory->getDefaultValidatorChain()->setPluginManager($customValidatorPluginManager);

CustomValidatorPluginManager class is like:

use Zend\Validator\ValidatorPluginManager as ZendValidatorPluginManager;

/**
 * Extensión de ValidatorPlugin manager para poder agregar validadores propios a
 * través de anotaciones.
 */
class CustomValidatorPluginManager extends ZendValidatorPluginManager {

	protected $aliasesExtension = [
		'CustomValidator'   => CustomValidator::class,
		'customvalidator'   => CustomValidator::class
	];

	public function __construct($configOrContainerInstance = null, array $v3config = array()) {
		$this->agregarValidadores();
		parent::__construct($configOrContainerInstance, $v3config);
		$this->addInitializer([$this, 'injectTranslator']);
        	$this->addInitializer([$this, 'injectValidatorPluginManager']);
	}

	public function agregarValidadores() {
		foreach ($this->aliasesExtension as $alias => $clase) {
			//Agrego par alias|clase
			$this->aliases[$alias] = $clase ;
			//Agrego par clase|factory
			$this->factories[$clase] = \Zend\ServiceManager\Factory\InvokableFactory::class;
			$canonical = strtolower(str_replace("\\", "", $clase));
			$this->factories[$canonical] = \Zend\ServiceManager\Factory\InvokableFactory::class;
		}
	}

}

For anyone interested, this code resolves all problems. Just call it as replacement for bugged service doctrine.formannotationbuilder.orm_default

use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use DoctrineORMModule\Form\Annotation\AnnotationBuilder;

class DoctrineFormAnnotationBuilderFactory implements FactoryInterface
{
    /**
     * Create service
     *
     * @param ServiceLocatorInterface $serviceLocator
     * @return mixed
     */
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        $factory = new \Zend\Form\Factory();
        $inputFilterFactory = new \Zend\InputFilter\Factory();
        $filterChain = new \Zend\Filter\FilterChain();
        $validatorChain = new \Zend\Validator\ValidatorChain();

        $filterChain->setPluginManager($serviceLocator->get('FilterManager'));
        $validatorChain->setPluginManager($serviceLocator->get('ValidatorManager'));

        $inputFilterFactory->setDefaultFilterChain($filterChain);
        $inputFilterFactory->setDefaultValidatorChain($validatorChain);

        $factory->setFormElementManager($serviceLocator->get('FormElementManager'));
        $factory->setInputFilterFactory($inputFilterFactory);

        $builder = new AnnotationBuilder($serviceLocator->get('doctrine.entitymanager.orm_default'));
        $builder->setFormFactory($factory);

        return $builder;
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment