Search code examples
phpformszend-framework2

Zend Framework 2: Creating forms from configuration - how to fix ServiceNotCreatedException


I want to create a form in Zend Framework 2 from configuration, similar as described in the docs. So instead of doing this in the form class for every element:

// [...]

$this->add(array(
    'name' => 'fname',
    'type' => 'text',     
));

// [...]v

I want to do it like this:

// [...]

$form = $factory->createForm(array(
    'elements' => array(
        array(
            'spec' => array(
                'name' => 'fname',
                'type' => 'text',
            ),
        ),
    ),
));

// [...]

Since I am new to the framework, I am still a bit unsure as where things go & what is the correct way to do things, but I think I am on a good track – I just need to get things working to understand them more.

Currently my code results in an exception which leaves me a bit puzzled, although I understand it somehow:

An exception was raised while creating "MyForm"; no instance returned

Besides my confusion, I already have an idea where my code might go wrong.

The form class looks pretty simple because the actual elements won't be injected here:

namespace My\Form;

use Zend\Form\Form;

class MyForm extends Form
{
    public function __construct()
    {
        parent::__construct();

        $this->setAttribute('method', 'post');
        $this->setAttribute('action', '/some-action');
    }
}

The configuration for the form elements looks like this:

namespace My\Form;

use Zend\Form\Factory;

class MyFormFactory extends Factory
{
    public function __construct()
    {
        $factory = new Factory();

        $form = $factory->createForm(array(
            'elements' => array(
                array(
                    'spec' => array(
                        'name' => 'fname',
                        'type' => 'text',
                    ),
                ),
            ),
        ));

        return $form;
    }
}

To wire the two together, I have this service manager configuration in the module's module.config.php:

'service_manager' => array(
    'factories' => array(
        'MyForm' => function ($servicemanager) {
            $form = new \My\Form\MyForm();
            $factory = $servicemanager->get('MyFormFactory');
            $form->add($factory);
            return $form;
        },
        'MyFormFactory' => function ($servicemanager) {
            $factory = new \My\Form\MyFormFactory();
            return $factory;
        },
    ),
),

And last but not least, the controller:

 namespace My\Controller;


use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;

class MyController extends AbstractActionController
{
    public function indexAction()
    {
        $form = $this->getServiceLocator()->get('MyForm');

        return new ViewModel(array(
            'form' => $form,
        ));
    }
}

As already said, the only thing that happens is the exception which is actually pretty clear: The MyForm class does not return an instance. To fix this, I already tried to return something, but that did not do anything.

What needs to be changed to get the form created? Is my approach any good or am I on the wrong track?


Solution

  • In order to create a form via config, you will need to use the Zend\Form\Factory which will return the form element based on the configuration you provide it. Lets call this the "FormFactory".

    However to register this form with the FormElementManager as a service called 'MyForm' you will need to also create a service manager factory. This "factory" is a class implementing Zend\ServiceManager\FactoryInterface. Lets call this the "ServiceFactory".

    If I were to correct your code, I would do so in the following way.

    • Delete the class MyForm. There is no real need to actually extend the base form Zend\Form\Form; this is because you are only setting the form's attributes which can be done so via the FormFactory (you give it the config of these attributes and it will return a new form with them already set).

    • Delete MyFormFactory. In order to create our new form we do NOT need to extend the form factory or create a new instance of it, instead we use it's API in client code.

    • Remove the config under service_manager. We should register our forms with the 'FormElementManager' (using the `form_elements' config key). A 'plugin manager' configured to return form elements.

    With these changes, you are then able to create your form with just one "ServiceFactory". For example.

    namespace My\Factory;
    
    use Zend\ServiceManager\FactoryInterface;
    use Zend\ServiceManager\ServiceLocatorInterface;
    
    class MyFormFactory implements FactoryInterface
    {
        public function createService(ServiceLocatorInterface $formElementManager)
        {
            // Get the config of the new form
            $config = $this->getElementConfig($formElementManager);
    
            // Get the service manager
            $serviceManager = $formElementManager->getServiceLocator();
    
            // Get the form factory (service that creates the form)
            // from the service manager, FormFactory is the default
            $formFactory = new \Zend\Form\Factory($formElementManager); 
    
            // Create the form using the factory and config and then return it
            return $formFactory->create($config);
        }
    
        /** Return the config for the factory **/
        protected function getElementConfig(ServiceLocatorInterface $formElementManager)
        {
            // you could use the form element manager / service manager
            // here to load the config from elsewhere...
            return [
                'name' => 'myform',
                'type' => 'Zend\Form\Form',
                'attributes' => [
                    'method' => 'post',
                    'action' => '/foo/bar',
                ],
                'fieldsets' => [
                    /....
                ],
                'elements' => [
                   //...
                ]
            ];
        }
    
    }
    

    Then in your module.config.php you can register the factory as the new service

    'form_elements' => [
        'factories' => [
            'MyForm' => 'My\Factory\MyFormFactory',
        ],
    ],
    

    Notice above the config is under the 'form_elements' key. As we are registering the service with the form element manager, your controller code should also be updated.

    $serviceManager = $this->getServiceLocator();
    $form = $serviceManager->get('FormElementManager')->get('MyForm');