zendframework / zend-inputfilter

InputFilter component from Zend Framework
BSD 3-Clause "New" or "Revised" License
64 stars 50 forks source link

Input Filter Factory uses a different FilterPluginManager instance #79

Closed ffraenz closed 8 years ago

ffraenz commented 8 years ago

I created a custom filter which needs dependency injection by a factory. I added the filter to the config array like so:

return  [
    'filters' => [
        'factories' => [
            'ToFile' => 'Module\Factory\Filter\ToFile',
        ],
    ],
];

I am able to get a filter instance from the FilterPluginManager inside a controller:

$filter = $this->getServiceLocator()->get('FilterManager')->get('ToFile');
var_dump($filter);

Inside the init() method of a class extending Zend\Form\Form I configured an input filter using the custom filter. The form also uses custom form elements from the FormElementManager.

$this->getInputFilter()->add([
    'name' => 'file',
    'required' => false,
    'filters' => [
        [ 'name' => 'ToFile' ],
    ],
]);

Custom form elements are working fine but the custom filter always raises a ServiceNotFoundException:

Zend\Filter\FilterPluginManager::get was unable to fetch or create an instance for ToFile

I found out that the Input Filter Factory is using a different FilterPluginManager instance which has never seen the config array. The custom filter with its factory is not included in the plugin manager.

This is unexpected behaviour? Or am I wrong?

weierophinney commented 8 years ago

This is expected behavior.

To get the input filter as configured at the application level, you need to do the following:

Alternately, you can set all this up semi-manually.

In your controller, you will do something like the following:

$services = $this->getServiceLocator();
$inputFilters = $services->get('InputFilterManager');
$inputFilter = new YourCustomInputFilter();
$inputFilterFactory = $inputFilter->getFactory()->setInputFilterManager($inputFilters);
$inputFilter->init();

A simpler approach would be to register your input filter with the InputFilterManager. Assuming you have no constructor dependencies, it could be registered as an invokable:

return [
    'input_filters' => [
        'invokables' => [
            'YourCustomInputFilter' => 'FullyQualfied\Namespace\For\YourCustomInputFilter',
        ],
    ],
];

From there, you could do the following in your controller:

$services = $this->getServiceLocator();
$inputFilters = $services->get('InputFilterManager');
$inputFilter = $inputFilters->get('YourCustomInputFilter');

Even better would be to inject it directly into your controller via the constructor, and to create a factory for your controller. The tutorial demonstrates such factories.

What the above approaches do is ensure that any input filters or inputs you've defined in the InputFilterManager are injected in your custom input filter's internal factory — and this simultaneously pulls the configured FilterManager and ValidatorManager and sets them in the default filter and validator chains, ensuring that you have access to those filters and validators you defined in modules and/or the application level.

The reason it works this way is because ZF2 does not define any globally static plugin managers; you must inject them where you want them. This prevents a whole category of problems we encountered in ZF1, and makes your code ultimately more testable (particularly if you inject the input filter into the controller instead of pulling it from the service manager within your controller).

ffraenz commented 8 years ago

Thank you very much for this detailed answer!

I want to define the input filters next to their related form elements inside a class extending Zend\Form\Form, not as different services. I retrieve the form instance through the FormElementManager which does inject the form factory with its dependencies. That's why the custom form elements are available in my form.

When the object bound to the form does not implement InputFilterAwareInterface, the form creates its InputFilter instance itself when getInputFilter gets called (as described here):

$this->filter = new InputFilter();

In this case, the Zend\InputFilter\Factory dependency of this newly created input filter instance does not get satisfied which results in creating a different factory instance with separate ServiceManagers.

The form has a pointer to a Zend\InputFilter\Factory instance with properly injected ServiceManagers. It uses it here.

Adding following line right after the statement shown above makes custom filters and validators available inside the form as expected:

$this->filter->setFactory($this->getFormFactory()->getInputFilterFactory());

I wanted to continue the discussion here before opening a pull request in the zend-form repository. Let me know your thoughts about this!