Antenne
Antenne

Reputation: 61

ZF2: Form with nested Fieldsets - Doctrine Hydrator trying to add new Entities for all objects

I have the following setup:

I have a Product Doctrine object with some related Doctrine entities.

/**
* Product
*
* @ORM\Table(name="products")
* @ORM\Entity(repositoryClass="\Entities\ProductRepository")
*/

class Product
{
/**
 * @var integer
 *
 * @ORM\Column(name="id", type="integer", precision=0, scale=0, nullable=false, unique=false)
 * @ORM\Id
 * @ORM\GeneratedValue(strategy="IDENTITY")
 */
private $id;

/**
 * @var string
 *
 * @ORM\Column(name="artist", type="string", length=100, precision=0, scale=0, nullable=false, unique=false)
 */
private $artist;

/**
 * @var string
 *
 * @ORM\Column(name="title", type="string", length=100, precision=0, scale=0, nullable=false, unique=false)
 */
private $title;

/**
 * @var string
 *
 * @ORM\Column(name="subtitle", type="string", length=100, precision=0, scale=0, nullable=true, unique=false)
 */
private $subtitle;

/**
 * @var string
 *
 * @ORM\Column(name="labelcode", type="string", length=100, precision=0, scale=0, nullable=false, unique=false)
 */
private $labelcode;

/**
 * @var string
 *
 * @ORM\Column(name="description", type="string", length=100, precision=0, scale=0, nullable=true, unique=false)
 */
private $description;

/**
 * @var string
 *
 * @ORM\Column(name="hints", type="text", precision=0, scale=0, nullable=true, unique=false)
 */
private $hints;

/**
 * @var string
 *
 * @ORM\Column(name="price", type="decimal", precision=0, scale=2, nullable=false, unique=false)
 */
private $price;

/**
 * @var integer
 *
 * @ORM\Column(name="amount", type="integer", precision=0, scale=0, nullable=true, options={"default" = 0}, unique=false)
 */
private $amount;

/**
 * @var string
 *
 * @ORM\Column(name="image", type="string", length=100, precision=0, scale=0, nullable=true, unique=false)
 */
private $image;

/**
 * @var \DateTime
 *
 * @ORM\Column(name="instockdate", type="date", precision=0, scale=0, nullable=true, unique=false)
 */
private $instockdate;

/**
 * @var boolean
 *
 * @ORM\Column(name="ownrelease", type="boolean", precision=0, scale=0, nullable=true, columnDefinition="BOOLEAN DEFAULT FALSE", unique=false)
 */
private $ownrelease;

/**
 * @var \Entities\Label
 *
 * @ORM\ManyToOne(targetEntity="Entities\Label", inversedBy="products")
 * @ORM\JoinColumns({
 *   @ORM\JoinColumn(name="label_id", referencedColumnName="id", nullable=false)
 * })
 */
private $label;

/**
 * @var \Entities\Genre
 *
 * @ORM\ManyToOne(targetEntity="Entities\Genre", inversedBy="products"))
 * @ORM\JoinColumns({
 *   @ORM\JoinColumn(name="genre_id", referencedColumnName="id", nullable=false)
 * })
 */
private $genre;

/**
 * @var \Entities\Type
 *
 * @ORM\ManyToOne(targetEntity="Entities\Type")
 * @ORM\JoinColumns({
 *   @ORM\JoinColumn(name="type_id", referencedColumnName="id", nullable=false)
 * })
 */
private $type;

/**
 * @var \Entities\Webshop
 *
 * @ORM\ManyToOne(targetEntity="Entities\Webshop", inversedBy="products")
 * @ORM\JoinColumns({
 *   @ORM\JoinColumn(name="webshop_id", referencedColumnName="id", nullable=true)
 * })
 */
private $webshop;

/**
 * @var \Entities\Supplier
 *
 * @ORM\ManyToOne(targetEntity="Entities\Supplier")
 * @ORM\JoinColumns({
 *   @ORM\JoinColumn(name="supplier_id", referencedColumnName="id", nullable=true)
 * })
 */
private $supplier;

/**  Getters and Setters **/
}

I have a CreateProductForm:

class CreateProductForm extends Form implements InputFilterProviderInterface
{

protected $objectManager;

public function __construct(ObjectManager $objectManager)
{
    parent::__construct('create-product-form');
    $this->setObjectManager($objectManager);

    $this->setHydrator(new DoctrineHydrator($this->getObjectManager()));

    $this->addElements();
}

public function addElements()
{
    // Add the product fieldset, and set it as the base fieldset
    $productFieldset = new ProductFieldSet($this->getObjectManager());
    $productFieldset->setUseAsBaseFieldset(true);
    $this->add($productFieldset);

    // Add a checkbox allowing creation of another \Entities\Product
    $this->add(array(
        'type' => 'Zend\Form\Element\Checkbox',
        'name' => 'create_another_product',
        'attributes' => array(
            'required' => FALSE,
            'allow_empty' => TRUE,
        ),
        'options' => array(
            'label' => 'Create another',
            'use_hidden_element' => TRUE,
            'checked_value' => TRUE,
            'unchecked_value' => FALSE,
            'label_attributes' => array(
                'class' => 'control-label'
            ),
        ),
    ));

    // … add CSRF and submit elements …
    $this->add(array(
        'type' => 'Zend\Form\Element\Csrf',
        'name' => 'create_product_csrf',
        'options' => array(
            'csrf_options' => array(
                'timeout' => 600
            )
        )
    ));

    $this->add(array(
        'name' => 'submit',
        'type' => 'Submit',
        'attributes' => array(
            'value' => 'Go',
            'id' => 'submitbutton',
            'class' => 'btn btn-primary'
        ),
    ));
}

/**
 * Get objectManager
 * 
 * @return type 
 */
public function getObjectManager()
{
    return $this->objectManager;
}

/**
 * Set objectManager
 * 
 * @param ObjectManager $objectManager
 * @return \Cms\Form\ProductForm
 */
public function setObjectManager(ObjectManager $objectManager)
{
    $this->objectManager = $objectManager;

    return $this;
}

/**
 * Should return an array specification compatible with
 * {@link Zend\InputFilter\Factory::createInputFilter()}.
 *
 * @return array
 */
public function getInputFilterSpecification()
{
    return array(
        'create_another_product' => array(
            'required' => FALSE,
            'allow_empty' => TRUE,
    ));
}
}

I will show you two FieldSets one for product and one for a related entity:

class ProductFieldSet extends Fieldset implements InputFilterProviderInterface
{

/**
 * @var ObjectManager
 *
 */
protected $objectManager;

/**
 * @var InputFilter
 *
 */
protected $inputFilter;

public function __construct(ObjectManager $objectManager)
{
    parent::__construct('product');

    $this->setObjectManager($objectManager);


    $this->setHydrator(new DoctrineEntity($this->getObjectManager()))->setObject(new Product());

    $this->addElements();
}

public function addElements()
{
    $this->add(array(
        'name' => 'id',
        'type' => 'Hidden',
    ));

    $this->addLabelCode();
    $this->addArtist();
    $this->addTitle();
    $this->addDescription();
    $this->addPrice();
    $this->addHints();

    $this->add(array(
        'name' => 'subtitle',
        'type' => 'Text',
        'options' => array(
            'label' => 'Subtitle',
        ),
        'attributes' => array(
            'class' => 'form-control',
        ),
    ));


    $this->add(array(
        'name' => 'amount',
        'type' => 'Text',
        'options' => array(
            'label' => 'Amount',
        ),
    ));

    $date = new Date('instockdate');
    $date
            ->setLabel('In Stock Date')
            ->setAttributes(array(
                'min' => '2012-01-01',
                'max' => '2200-01-01',
                'step' => '1', // days; default step interval is 1 day
    ));



    $this->addGenre();
    $this->addLabel();
    $this->addType();
}
// A lot of add methods

 /**
 * Add Type to the Cms\Form\ProductFieldSet
 */
private function addType()
{
    $typeFieldset = new TypeFieldSet($this->getObjectManager());
    $typeFieldset->setName('type');
   //$labelFieldset->setAttribute('required', TRUE);
   //$labelFieldset->setAttribute('class', 'form-control');

    $typeFieldset->setLabel('Type');
   //$labelFieldset->setLabelAttributes(array('class' => 'control-label'));

    $this->add($typeFieldset);
}
}

And now the TypeFieldSet:

class TypeFieldSet extends Fieldset implements InputFilterProviderInterface
{

/**
 * @var ObjectManager
 *
 */
protected $objectManager;

/**
 * @var InputFilter
 *
 */
protected $inputFilter;

/**
 * Construct Cms\Form\GenreFieldSet.
 * 
 * @param ObjectManager $objectManager
 */
public function __construct(ObjectManager $objectManager)
{
    parent::__construct('type');

    $this->setObjectManager($objectManager);

    $this->setHydrator(new DoctrineEntity($this->getObjectManager()))->setObject(new Type());

    $this->addElements();
}

/**
 * Method responsible for adding elements to \Cms\Form\Fieldset.
 */
public function addElements()
{
    $this->add(array(
        'name' => 'name',
        'type' => 'Zend\Form\Element\Text',
        'attributes' => array(
            'required' => true,
            'id' => 'name',
            'class' => 'form-control',
            'placeholder' => 'Enter type name'
        ),
        'options' => array(
            'label' => 'Name',
            'label_attributes' => array(
                'class' => 'control-label'
            ),

        ),
    ));
}
// Additional methods
}

Somehow Doctrine tries to add all the related entities as new entity and not find the existing entity to relate to based on the name I enter.

Could anyone help me out?

-- Edit

So indeed I don't have an ID present of the related Entities. Controller code look like this:

Hi, so the ID is missing, because I use the name to identify the related object. My controller looks like this:

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

    // Create the form and inject the ObjectManager
    $form = new CreateProductForm($objectManager);

    // Create a new, empty entity and bind it to the form
    $product = new Product();
    $form->bind($product);

    if ($this->request->isPost())
    {
        $form->setData($this->request->getPost());

        if ($form->isValid())
        {

            $this->getProductService()->saveProduct($product);

            $this->flashMessenger()->addSuccessMessage('Product ' . $product->getName() . ' saved');

                return $this->redirect()->toRoute('product');
        }
        else
        {
            $this->flashMessenger()->addErrorMessage($form->getMessages());
        }
    }

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

Should I obtain related IDs in the controller or is there a more subtle way to do this? In the official example https://github.com/doctrine/DoctrineModule/blob/master/docs/hydrator.md#a-complete-example-using-zendform no ids are obtained in the controller code.

Upvotes: 0

Views: 1325

Answers (1)

Wilt
Wilt

Reputation: 44336

I cannot totally figure out what fields/data you are passing from the huge amount of code you posted here, but to make the DoctrineObject hydrator find and use existing entities from the database and subsequently use these for hydration of your new object you need to pass the field(s) that correspond with the identifiers of the associating objects.

So let's say you want to create a new Product with an existing Genre it would mean passing the genre_id in the data. So something like this:

$hydrator = new DoctrineObject($objectManager);

$data = array(
    "genre" => array(
        "genre_id" => 1
    )
);

$product = new Product();

$hydrator->hydrate($data, $product);

In this very basic code example the hydrator will recognize the identifier field from the Genre entity and use it to collect it before hydrating your new Product. So if you pass identifier fields in your data sets it should work.

DoctrineObject won't collect any entities using other than identifier fields. So if you would pass any but identifier fields it assumes you want to make a new association with a new entity and it will use the data you pass to hydrate the new relating entity.

If passing identifier fields is what you are doing and it doesn't work, then please explain in detail what data you are exactly passing...

Upvotes: 1

Related Questions