Nukeface Nukeface - 14 days ago 4
PHP Question

ZF2 injecting InputFilter into Fieldset not working automatically

I'm building a small application with ZF2 and Doctrine2. Setting it up in such a way as to have a lot of reusable code and technique. However, getting stumped by the fact that my

InputFilter
is not automatically injected into the
Fieldset
that it should get associated to.

I've confirmed that the Form that uses the
Fieldset
works (without the
InputFilter
). The
InputFilter
is also visible as present during debugging.

The question then, what am I doing wrong and how to fix having a separate
InputFilter
, coupled to a
Fieldset
in ZF2?




Sidenotes:

1 - I am aware that by using the
InputFilterInterface
I could have the
InputFilter
inside of the
Fieldset
class with the
getInputFilterSpecification()
function. However, as I'm trying to keep it DRY and reusable, it wouldn't do to have to copy it if I were to create an API that needs to use the
Entity
and
InputFilter
, but can only have the latter coupled with a
Fieldset
.

2 - A lot of Abstract classes are used, where used I'll indicate in the snippets what they have that's relevant

3 - The problem line is in
CustomerFieldsetFactory.php


=========================================================================

Entity:
Customer.php


/**
* Class Customer
* @package Customer\Entity
*
* @ORM\Entity
* @ORM\Table(name="customers")
*/
class Customer extends AbstractEntity //Contains $id
{
/**
* @var string
* @ORM\Column(name="name", type="string", length=255, nullable=false)
*/
protected $name;
}


Form:
CustomerForm.php


class CustomerForm extends AbstractForm
{
public function __construct($name = null, array $options)
{
parent::__construct($name, $options); // Adds CSRF
}

public function init()
{
$this->add([
'name' => 'customer',
'type' => CustomerFieldset::class,
'options' => [
'use_as_base_fieldset' => true,
],
]);

//Call parent initializer. Check in parent what it does.
parent::init(); //Adds submit button if not in form
}
}


Fieldset:
CustomerFieldset.php


class CustomerFieldset extends AbstractFieldset //Contains EntityManager property and constructor requirement (as we're managing Doctrine Entities here)
{
public function init()
{
$this->add([ //For now id field is here, until InputFilter injection works
'name' => 'id',
'type' => Hidden::class,
'attributes' => [
'id' => 'entityId',
],
]);

$this->add([
'name' => 'name',
'type' => Text::class,
'options' => [
'label' => _('Name'),
],
]);
}
}


InputFilter:
CustomerInputFilter.php


class CustomerInputFilter 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,
'max' => 255,
],
],
],
]);
}
}


Above the classes. Below the Factories

FormFactory:
CustomerFormFactory.php


class CustomerFormFactory implements FactoryInterface, MutableCreationOptionsInterface
{
/**
* @var array
*/
protected $options;

/**
* @param array $options
*/
public function setCreationOptions(array $options)
{
//Arguments checking removed
$this->options = $options;
}

/**
* @param ServiceLocatorInterface|ControllerManager $serviceLocator
* @return CustomerForm
*/
public function createService(ServiceLocatorInterface $serviceLocator)
{
$serviceManager = $serviceLocator->getServiceLocator();

$form = new CustomerForm($this->options['name'], $this->options['options']);

$form->setTranslator($serviceManager->get('translator'));

return $form;
}
}


FieldsetFactory:
CustomerFieldsetFactory.php


class CustomerFieldsetFactory implements FactoryInterface, MutableCreationOptionsInterface
{
/**
* @var string
*/
protected $name;

public function setCreationOptions(array $options)
{
//Argument checking removed

$this->name = $options['name'];
}

public function createService(ServiceLocatorInterface $serviceLocator)
{
$serviceManager = $serviceLocator->getServiceLocator();

$fieldset = new CustomerFieldset($serviceManager->get('Doctrine\ORM\EntityManager'), $this->name);

$fieldset->setHydrator(new DoctrineObject($serviceManager->get('doctrine.entitymanager.orm_default'), false));
$fieldset->setObject(new Customer());
$fieldset->setInputFilter($serviceManager->get('InputFilterManager')->get(CustomerInputFilter::class));

//RIGHT HERE! THE LINE ABOVE IS THE ONE THAT DOES NOT WORK!!!

return $fieldset;
}
}


InputFilterFactory:
CustomerInputFilterFactory.php


class CustomerInputFilterFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceLocator)
{
$repository = $serviceLocator->getServiceLocator()
->get('Doctrine\ORM\EntityManager')
->getRepository(Customer::class);

return new CustomerInputFilter($repository);
}
}


Config:
module.config.php


'controllers' => [
'factories' => [
CustomerController::class => CustomerControllerFactory::class,
],
],
'form_elements' => [
'factories' => [
CustomerForm::class => CustomerFormFactory::class,
CustomerFieldset::class => CustomerFieldsetFactory::class,
],
],
'input_filters' => [
'factories' => [
CustomerInputFilter::class => CustomerInputFilterFactory::class,
],
],
'service_manager' => [
'invokables' => [
CustomerControllerService::class => CustomerControllerService::class,
],
],





I am hoping one of you can help me out here.




EDIT: Update with actual error

The following line in the
CustomerFieldset.php
(above) triggers the error.

$fieldset->setInputFilter($serviceManager->get('InputFilterManager')->get(CustomerInputFilter::class));


The error:

Fatal error: Call to undefined method Customer\Fieldset\CustomerFieldset::setInputFilter() in D:\htdocs\server-manager\module\Customer\src\Customer\Factory\CustomerFieldsetFactory.php on line 57


Debug snippet

As seen in the above snippet, the InputFilter (and it's Factory) are known the the
InputFilterManager
.

The error states it does not know the
getInputFilter()
function on the Fieldset. Which is correct in a way, it doesn't exist. The question then is, how to have the function exist so that injecting the InputFilter will work, or how to bind this InputFilter to the Fieldset?




EDIT 2: Update based on Wilt's answer
Added
use InputFilterAwareTrait
to Abstract class
AbstractInputFilter
to create following (from answer):

use Zend\InputFilter\InputFilterAwareTrait;

abstract class AbstractFieldset extends Fieldset
{
use InputFilterAwareTrait;
// ... Other code
}


Turns out that I had another mistake in the (original) code above as well:
In file
module.config.php
the
input_filters
should've been
input_filter_specs
. This was (after using the Trait) a
Invalid Factory registered
error (titled
ServiceNotCreatedException
).

The following might be of use to someone, the Factory to create a Fieldset with Hydrator, Object and Inputfilter has the following
createService()
function:

public function createService(ServiceLocatorInterface $serviceLocator)
{
/** @var ServiceLocator $serviceManager */
$serviceManager = $serviceLocator->getServiceLocator();
/** @var CustomerRepository $customerRepository */
$customerRepository = $serviceManager->get('Doctrine\ORM\EntityManager')->getRepository(Customer::class);

$fieldset = new CustomerFieldset($serviceManager->get('Doctrine\ORM\EntityManager'), $this->name);
$fieldset->setHydrator(new DoctrineObject($serviceManager->get('doctrine.entitymanager.orm_default'), false));
$fieldset->setObject(new Customer());
$fieldset->setInputFilter($serviceManager->get('InputFilterManager')->get(CustomerInputFilter::class, $customerRepository));

return $fieldset;
}

Answer

There is lots of information added to your question. I'd suggest you try to narrow down your question in the future. Read more on the guidelines for a good question here: How to create a Minimal, Complete, and Verifiable example.

Zend framework provides a InputFilterAwareTrait with both the setInputFilter and getInputFilter methods. You can easily implement/use this trait inside your CustomerFieldset class:

use Zend\InputFilter\InputFilterAwareTrait;

class CustomerFieldset extends AbstractFieldset
{
    use InputFilterAwareTrait;

    //...remaining code

}

In case you want the inputfilter in all classes that extend your abstract AbstractFieldset class you could also decide to add the trait there:

use Zend\InputFilter\InputFilterAwareTrait;

class AbstractFieldset
{
    use InputFilterAwareTrait;

    //...remaining code

}