How to Extend a Fieldset in ZF2 to work with Doctrine’s Class Table Inheritance mapping strategy

StackOverflow https://stackoverflow.com/questions/23281724

Pregunta

I’m developing a project using Doctrine’s Class Table Inheritance mapping strategy which involves joining a parent table with one of a number of child tables depending on the value in a discriminator column in the parent table. I’ve got a working prototype in which the unique conglomerate fieldsets each contain duplicate copies of all of the code for the common elements from the parent entity. In order to ensure consistency and avoid excess code I want to change the fieldsets so that I have a single fieldset that’s related to the parent entity and all of the other fieldsets are simply extensions of the parent (a detailed explanation is included at How to Extend a Fieldset in ZF2). I run into problems when I separate the fieldsets and then try to get them to work with each other.

The first answer to the question How to Extend a Fieldset in ZF2 provides a clear explaination of how a single fieldset can extend another in ZF2. However, the answer uses init() to put together the fieldsets, and for the Doctrine strategy we need to use __construct. My first pass in developing the fieldsets was to build in the some of the code prescribed in the DoctrineModule/docs, which brings us to this:

class FieldsetParent extends Zend\Form\Fieldset
{
   public function __construct(ObjectManager $objectManager) {

        parent::__construct('parent-fieldset');

        $this->setHydrator(new DoctrineHydrator($objectManager, 'MyModule\Entity\Parent'))
             ->setObject(new Parent());

        $this->add(array('name' => 'fieldA'));
        $this->add(array('name' => 'fieldB'));
        $this->add(array('name' => 'fieldC'));
   }
}

and this:

class FieldsetFoo extends FieldsetParent
{
   public function __construct(ObjectManager $objectManager) {

        parent::__construct('foo-fieldset');

        $this->setHydrator(new DoctrineHydrator($objectManager, 'MyModule\Entity\Foo'))
             ->setObject(new Foo());

        $this->add(array('name' => 'fieldD'));
        $this->add(array('name' => 'fieldE'));
        $this->add(array('name' => 'fieldF'));
        $this->add(array('name' => 'fieldG'));
   }
}

This doesn’t work because it attempts to add a fieldset from a string, giving the error message:

Catchable fatal error: Argument 1 passed to MyModuule\Form\ParentFieldset::__construct() 
must implement interface Doctrine\Common\Persistence\ObjectManager, string given ...

The first answer to the question zend2 doctrine 2 form intgergration OneToOne explains how adding a fieldset from a string can be avoided in the case of a OneToOne strategy. However, I'm working with a different ORM strategy and I’m having difficulties working out how to solve the same problem.

EDIT

As requested, here is some more detailed information:

class FooController extends AbstractActionController
{
    /**
     * @var Doctrine\ORM\EntityManager
     */
    protected $em;

    public function setEntityManager(EntityManager $em)
    {
        $this->em = $em;
        return $this;
    }

    public function getEntityManager()
    {
        if (null === $this->em) {
            $this->em = $this->getServiceLocator()->get('Doctrine\ORM\EntityManager');
        }
        return $this->em;
    }

    // ... //

    public function editAction()
    {
        $fooID = (int)$this->getEvent()->getRouteMatch()->getParam('fooID');
        if (!$fooID) {
            return $this->redirect()->toRoute('foo', array('action'=>'add'));
        }

        $foo = $this->getEntityManager()->find('MyModule\Entity\Foo', $fooID);

        // Get your ObjectManager from the ServiceManager
        $objectManager = $this->getServiceLocator()->get('Doctrine\ORM\EntityManager');

        // Create the form and inject the ObjectManager
        $form = new EditFooForm($objectManager);
        $form->setBindOnValidate(false);
        $form->bind($foo);
        $form->get('submit')->setAttribute('label', 'Update');

        $request = $this->getRequest();
        if ($request->isPost()) {
            $form->setData($request->getPost());
            if ($form->isValid()) {
                $form->bindValues();
                $this->getEntityManager()->flush();

                return $this->redirect()->toRoute('foo');
            }
        }

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

}

and

class EditFooForm extends Form
{
    public function __construct(ObjectManager $objectManager)
    {
        parent::__construct('edit_foo_form');

        $this->setHydrator(new DoctrineHydrator($objectManager, 'MyModule\Entity\Foo'));

        $fooFieldset = new FooFieldset($objectManager);
        $fooFieldset->setUseAsBaseFieldset(true);
        $this->add($fooFieldset);

        // submit elements
    }
}

This fieldset works:

class FooFieldset extends Fieldset implements InputFilterProviderInterface
{
    public function __construct(ObjectManager $objectManager)
    {
        parent::__construct('foo-fieldset');

        $this->setHydrator(new DoctrineHydrator($objectManager, 'MyModule\Entity\Foo'))
             ->setObject(new Foo());

        $this->add(array('name' => 'fieldA'));
        $this->add(array('name' => 'fieldB'));
        $this->add(array('name' => 'fieldC'));
        $this->add(array('name' => 'fieldD'));
        $this->add(array('name' => 'fieldE'));
        $this->add(array('name' => 'fieldF'));
        $this->add(array('name' => 'fieldG'));

    }

    public function getInputFilterSpecification()
    {
        return array('fieldA' => array('required' => false),);
        return array('fieldB' => array('required' => false),);
        return array('fieldC' => array('required' => false),);
        return array('fieldD' => array('required' => false),);
        return array('fieldE' => array('required' => false),);
        return array('fieldF' => array('required' => false),);
        return array('fieldG' => array('required' => false),);

    }

}

These fieldsets give an error message:

class FoobarFieldset extends Fieldset implements InputFilterProviderInterface
{
    public function __construct(ObjectManager $objectManager)
    {
        parent::__construct('foobar-fieldset');

        $this->setHydrator(new DoctrineHydrator($objectManager, 'MyModule\Entity\Foobar'))
             ->setObject(new Foobar());

        $this->add(array('name' => 'fieldA'));
        $this->add(array('name' => 'fieldB'));
        $this->add(array('name' => 'fieldC'));

    }

    public function getInputFilterSpecification()
    {
        return array('fieldA' => array('required' => false),);
        return array('fieldB' => array('required' => false),);
        return array('fieldC' => array('required' => false),);

    }

}

and

use MyModule\Form\FoobarFieldset;

class FooFieldset extends FoobarFieldset implements InputFilterProviderInterface
{
    public function __construct(ObjectManager $objectManager)
    {
        parent::__construct('foo-fieldset');

        $this->setHydrator(new DoctrineHydrator($objectManager, 'MyModule\Entity\Foo'))
             ->setObject(new Foo());

        $this->add(array('name' => 'fieldD'));
        $this->add(array('name' => 'fieldE'));
        $this->add(array('name' => 'fieldF'));
        $this->add(array('name' => 'fieldG'));

    }

    public function getInputFilterSpecification()
    {
        return array('fieldD' => array('required' => false),);
        return array('fieldE' => array('required' => false),);
        return array('fieldF' => array('required' => false),);
        return array('fieldG' => array('required' => false),);

    }

}

The resulting error message is:

Catchable fatal error: Argument 1 passed to MyModule\Form\FoobarFieldset::__construct() 
must implement interface Doctrine\Common\Persistence\ObjectManager, string given, called
in C:\xampp\htdocs\GetOut\module\MyModule\src\MyModule\Form\FooFieldset.php on line 17 
and defined in C:\xampp\htdocs\GetOut\module\MyModule\src\MyModule\Form\FoobarFieldset.php
on line 14

I have tried to avoid adding a fieldset from a string by making these changes:

use MyModule\Form\FoobarFieldset;

class FooFieldset extends Fieldset implements InputFilterProviderInterface
{
    public function __construct(ObjectManager $objectManager)
    {
        parent::__construct('foo-fieldset');

        $this->setHydrator(new DoctrineHydrator($objectManager, 'MyModule\Entity\Foo'))
             ->setObject(new Foo());

        $this->add(array('name' => 'fieldD'));
        $this->add(array('name' => 'fieldE'));
        $this->add(array('name' => 'fieldF'));
        $this->add(array('name' => 'fieldG'));

        $fieldset = new FoobarFieldset($objectManager);
        $this->add($fieldset);

    }

but this gives a Zend\Form\Exception\InvalidElementException with the message, No element by the name of [fieldA] found in form, indicating that the filedset is not getting added.

AN ALTERNATIVE

When it’s all said and done, all I’m really trying to do is keep all of the common statements in one place and draw them into the various unique fieldsets that need to include them. I can solve this without ZF2 by using include statements as shown below:

// FoobarFieldset_fields.php

$this->add(array('name' => 'fieldA'));
$this->add(array('name' => 'fieldB'));
$this->add(array('name' => 'fieldC'));

and

// FoobarFieldset_filters.php

return array('fieldA' => array('required' => false),);
return array('fieldB' => array('required' => false),);
return array('fieldC' => array('required' => false),);

and

class FooFieldset extends Fieldset implements InputFilterProviderInterface
{
    public function __construct(ObjectManager $objectManager)
    {
        parent::__construct('foo-fieldset');

        $this->setHydrator(new DoctrineHydrator($objectManager, 'MyModule\Entity\Foo'))
             ->setObject(new Foo());

        include 'FoobarFieldset_fields.php';
        $this->add(array('name' => 'fieldD'));
        $this->add(array('name' => 'fieldE'));
        $this->add(array('name' => 'fieldF'));
        $this->add(array('name' => 'fieldG'));

    }

    public function getInputFilterSpecification()
    {
        include 'FoobarFieldset_filters.php';
        return array('fieldD' => array('required' => false),);
        return array('fieldE' => array('required' => false),);
        return array('fieldF' => array('required' => false),);
        return array('fieldG' => array('required' => false),);

    }

}
¿Fue útil?

Solución

The issue you are having relates to the fieldset's __construct

The 'parent'

class FoobarFieldset extends Fieldset {
    public function __construct(ObjectManager $objectManager) {}

In the 'child' however you are calling the parent __construct passing a string (which should be the $objectManager)

class FooFieldset extends FoobarFieldset implements InputFilterProviderInterface
{
  public function __construct(ObjectManager $objectManager)
  {
     parent::__construct('foo-fieldset'); // This should be $objectManager, not string

There are some additional things that could improve your code.

Currently you are creating the form using the new keyword.

$form = new EditFooForm($objectManager);

This is fine (it will work) however you should really be loading it via the service manager to allow you to attach a factory to it.

$form = $this->getServiceLocator()->get('MyModule\Form\EditFoForm');

Then you would have a factory registered to create the new form and fieldset (keeping all the construction code in one place)

Module.php

public function getFormElementConfig()
{
  return array(
    'factories' => array(
       'MyModule\Form\EditFooForm' => function($fem) {
          // inject form stuff
          $sm = $fem->getServiceLocator();
          $om = $sm->get('object_manager');

          return new EditFooForm($om);
       },
       'MyModule\Form\EditFooFieldset' => function($fem) {
          // inject fieldset stuff
          $sm = $fem->getServiceLocator();
          $om = $sm->get('object_manager');

          $fieldset = new EditFooFieldset($om);

          $hydrator = $sm->get('HydratorManager');
          // you can also create the hydrator via a factory
          // and inject it outside the form, meaning you don't need to do so
          // within the form
          $hydrator = $hydrator->get('MyFooHydrator'); 
          $fieldset->setHydrator($hydrator);

          return $fieldset;
       },

    ),
  );
}

Lastly; allow the Zend\Form\Factory to create the fieldset by adding it via $this->add() (rather than creating the fieldset within the form using new)

    class FooFieldset extends Fieldset implements InputFilterProviderInterface
{
    public function init()
    {
        //.....
        // Allow the fieldset to be loaded via the FormElementManager 
        // and use the new factory
        $this->add(array(
            'name' => 'foo_fieldset',
            'type' => 'MyModule\Form\EditFooFieldset',
        ));

        // ....

    }
}

Notice in the last example the form elements are added in init() this is because the FormElementManager will call init() when you request it from the service manager. This is an important separation of concerns as it will allow you to provide the dependencies via __construct injection (when the form is created) and then separately add the form elements after all the forms properties have been set.

This is explained within the documentation:

[...] you must not directly instantiate your form class, but rather get an instance of it through the Zend\Form\FormElementManager:

and also:

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:

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top