2

I have a form (Zend\Form\Form) with some nested fieldsets (Zend\Form\Fieldset) in it. The construction is pretty similar to that in the Form Collections tutorial.

Storage\Form\MyForm
|_'Storage\Form\Fieldset\FooFieldset'
  |_'Storage\Form\Fieldset\BarFieldset'
|_'Storage\Form\Fieldset\BazFieldset'
...

MyForm

class MyForm {
    public function __construct()
    {
        ...
        $this->add(
            [
                'type' => 'Storage\Form\Fieldset\FooFieldset',
                'options' => [
                    'use_as_base_fieldset' => true
                ]
            ]
        );
    }
}

FooFieldset

class FooFieldset extends Fieldset implements InputFilterProviderInterface
{
    public function __construct()
    {
        parent::__construct('foo');
        $this->setHydrator(new ClassMethodsHydrator())->setObject(new Foo()); // dependencies!
    }
}

It works, but the fieldset class has two dependencies in it. I want to inject them. In order to do it, I created a FooFieldsetFactory and extended the /module/MyModule/config/module.config.php by:

'service_manager' => [
    'factories' => [
        'Storage\Form\Fieldset\FooFieldset' => 'Storage\Form\Fieldset\Factory\FooFieldsetFactory',
    ],
],

The factory is simply being ignored. I guess, the service locator first tries to find the class by namespace and only if nothing is found, takes a look in the invokables and factories. OK. Then I created an alias:

'service_manager' => [
    'factories' => [
        'Storage\Form\Fieldset\FooFieldset' => 'Storage\Form\Fieldset\Factory\FooFieldsetFactory',
    ],
],
'aliases' => [
    'Storage\Form\Fieldset\Foo' => 'Storage\Form\Fieldset\FooFieldset',
],

... and tried to use it instead of Storage\Form\Fieldset\FooFieldset in my form class. But now I get an exception:

Zend\Form\FormElementManager::get was unable to fetch or create an instance for Storage\Form\Fieldset\Foo

I've also tried this directly:

'service_manager' => [
    'factories' => [
        'Storage\Form\Fieldset\Foo' => 'Storage\Form\Fieldset\Factory\FooFieldsetFactory',
    ],
],

No effect, the same error.

And this also didn't work (the same error):

'form_elements' => [
    'factories' => [
        'Storage\Form\Fieldset\Foo' => 'Storage\Form\Fieldset\Factory\FooFieldsetFactory',
    ],
],

So the referencing a service for a fieldset seems not to work. Or am I doing something wrong?

How to use services for form fieldsets?


UPDATE

With some debuggin I found out, that my Foo fieldset factory cannot be found, because it has not be added to the factories list of the Zend\Form\FormElementManager. Here is the place in the Zend\Form\Factory:

enter image description here

So my config

'form_elements' => [
    'factories' => [
        'Storage\Form\Fieldset\Foo' => 'Storage\Form\Fieldset\Factory\FooFieldsetFactory',
    ],
],

is ignored. How to fix this?


UPDATE Additional information, how I'm creating my Form object.

/module/Foo/config/module.config.php

return [
    'controllers' => [
        'factories' => [
            'Foo\Controller\My' => 'Foo\Controller\Factory\MyControllerFactory'
        ]
    ],
    'service_manager' => [
        'factories' => [
            'Foo\Form\MyForm' => 'Foo\Form\Factory\MyFormFactory',
        ],
    ],
];

/module/Foo/src/Foo/Form/Factory/MyFormFactory.php

namespace Foo\Form\Factory;

use ...;

class MyFormFactory implements FactoryInterface
{

    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        $form = new MyForm();
        $form->setAttribute('method', 'post')
            ->setHydrator(new ClassMethods())
            ->setInputFilter(new InputFilter());
        return $form;
    }
}

/module/Foo/src/Foo/Controller/Factory/MyControllerFactory.php

namespace Foo\Controller\Factory;

use ...;

class MyControllerFactory implements FactoryInterface
{

    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        $fooPrototype = new Foo();
        $realServiceLocator = $serviceLocator->getServiceLocator();
        // $myForm = $realServiceLocator->get('Foo\Form\MyForm'); <-- This doesn't work correctly for this case. The FormElementManager should be used instead.
        $formElementManager = $realServiceLocator->get('FormElementManager');                        
        $myForm = $formElementManager->get('Foo\Form\MyForm');
        return new MyController($myForm, $fooPrototype);
    }
}
automatix
  • 14,018
  • 26
  • 105
  • 230

1 Answers1

6

This issue is because you are adding your form elements in the forms __construct() method rather than init() as suggested in the documentation.

You can use a factory instead of an invokable in order to handle dependencies in your elements/fieldsets/forms.

And now comes the first catch.

If you are creating your form class by extending Zend\Form\Form, you must not add the custom element in the __construct-or (as we have done in the previous example where we used the custom element’s FQCN), but rather in the init() method:

The reason is that the new form's factory (which is used to create new elements using add()) must have the application's form element manager injected after the form's constructor has been called This form element manager instance contains all the references to your custom forms elements which are registered under the form_elements configuration key.

By calling add() in the form __construct the form factory will lazy load a new instance of the form element manager; which will be able to create all default form elements but will not have any knowledge of your custom form element.

Community
  • 1
  • 1
AlexP
  • 9,906
  • 1
  • 24
  • 43
  • Thank you very much for your detailed answer! I moved the initialization logic from the constructor into the `init()`. But it's not working. For the **`Form`** the elements are just not found, e.g.: `No element by the name of [foo] found in form`. For **`FieldSet`s**: The same error, as before: `Zend\Form\FormElementManager::get was unable to fetch or create an instance for Order\Form\Fieldset\Foo`. – automatix Mar 11 '16 at 09:48
  • I also analyzed the state of the `FormElementManager` in the `init()`. Since the `init()` is called after the constructor, I expected to find my custom fieldset factories in `MyFieldset->factory->formElementManager->factories`. But the property only contains the standard Zend factories. – automatix Mar 11 '16 at 10:00
  • @automatix Are you creating the *form* using `$formElementManager->get('Order\Form\Fieldset\Foo`);` with `Order\Form\Fieldset\Foo` registered in `form_elements`? You question only shows `Storage\Form\Fieldset\Foo`. – AlexP Mar 11 '16 at 15:21
  • Just updated the question. I've been creating the `Form` directly from the `ServiceManager`. Just changed this to cretaing via `FormElementManager`. Now I can create the fields in the `Form#init()` and my `FooFieldsetFactory` is finally getting retrieved! Thank you very much for your help! – automatix Mar 12 '16 at 10:36
  • @automatix how did you fixed the problem with the "No element by the name.." ? I do `$form->get('employee')` which gives me the exception. My EmployeeFieldSet does in the constructor: `parent::__construct('employee');` In the init function I do all those `add` calls. In my form's constructor I do this: `parent::__construct('create_employee');` and within the init() method I add the `EmployeeFieldSet`. in the config.module.php I have the `form_elements` but it won't get called – rogaa Aug 26 '16 at 10:33
  • @rogaa I do nothing with the constructor and only use the `form_elements` config key (e.g. `'form_elements'.'factories'.'My\Fieldset\Bar' => 'My\Fieldset\Factory\BarFactory'`) and add the `Fieldset` within the `init()` (e.g. `$this->add(['name' => 'bar', 'type' => 'My\Fieldset\Bar', ...])`). In the views script I call then the `Fieldset` like this: `$fooFieldset->get('bar')`. Oh, now whhen writing this comment, I think I see your mistake. It's the `name` parameter -- you should pass this to your parent `Fieldset`. Otherwise the `FooFieldset`'s `get(...)` of doesn't know, what to retrieve. – automatix Aug 26 '16 at 11:10
  • @automatix is your updated question still correct with your FormFactory? Sorry for bothering you again - I removed my construct() methods from my `EmployeeForm` and my `EmployeeFieldset` but I'm still getting the same error output. What do you mean with the `name` parameter? In my `EmployeeForm->init()` I do the following: `$this->add([ 'name' => 'employee', 'type' => EmployeeFieldset::class, 'options' => [ 'use_as_base_fieldset' => true, ], ]);` – rogaa Aug 29 '16 at 07:03
  • Ok I got it .. my oh my.. I added a DI to my `IndexController` to retrieve the form via the `FormElementManager` (in the `IndexControllerFactory`). But well I forgot to change the form call in my action - I still created the form object via `new EmployeeForm();` instead of calling `this->form` due to the DI... Anyway thanks @automatix ! – rogaa Aug 29 '16 at 07:59
  • @rogaa I don't see any differences between your `add(...)` call and mine. But I've just updated my question and corrected the way I'm retrieving `MyForm` object from the `ServiceManager`. Oh, you were faster with your comment... :) OK, is working now? – automatix Aug 29 '16 at 08:14