This morning me and another developer have spent 3.5 hours figuring out how on earth to solve our problem. We are writing a Zend Framework 2 application that uses Doctrine entities. The trouble was figuring out how to use the $form->bind(..)
method with our Doctrine entities. Turns out, a combination of RTFM and sifting through the code for Doctrine, what we were trying to do is possible without nasty hacks. In the examples below, the AdVariant
entity is the entity we are creating the edit form for, and in our application's vernacular, a "module" is a semantic "grouping" of pages - it does not mean a ZF2 "Module"!
Step 1 - Hydration
First, we need to use Doctrine's built in Hydrator (what is a hydrator?) in our form:
<?php
namespace Ed\Advertisers\Form;
// .. more use
use DoctrineModule\Stdlib\Hydrator\DoctrineObject as DoctrineHydrator;
class AdVariantForm extends Form implements InputFilterProviderInterface
{
protected $entityManager;
protected $moduleService;
protected $pageService;
public function __construct(EntityManager $entityManager, ModuleService $moduleService, PageService $pageService, $name = null)
{
parent::__construct($name);
$hydrator = new DoctrineHydrator($entityManager, '\Ed\Advertisers\Entity\AdVariant');
$this->setHydrator($hydrator);
// ... set up elements here
}
}
The DoctrineHydrator
basically tells Zend\Form
:
- ... how to populate the form when given an entity (extract)
- ... how to populate an entity when given a form (hydrate)
So in the code snippet, we create the DoctrineHydrator
, give it the object manager and tell it what Entity we are using for hydration.
Step 2 - Bind Entity to Form
This is pretty much as per the example given in the Doctrine Hydrator documentation above:
public function editAction()
{
// .. create the form
// .. load $adVariant from query string params
$form->bind($adVariant);
if ($this->getRequest()->isPost())
{
$form->setData($this->getRequest()->getPost());
if ($form->isValid())
{
$this->adVariantService->getEntityManager()->persist($adVariant);
$this->adVariantService->getEntityManager()->flush();
// .. redirect or whatever else
}
}
// .. other stuff
}
Previously, we were populating the form using:
$form->setData($entity->getArrayCopy())
and then populating the entity by fetching values from the validated form and using the entity's setters, which was pretty ugly:
$adGroupId = $form->get('adGroupId')->getValue();
$adGroup = $this->adGroupService->fetchById($adGroupId);
// .. fetch other entities in this awful way
$name = $form->get('name')->getValue();
// .. other entitites
// .. using lots of setters to populate!!
$adVariant->setName($name);
$adVariant->setAdGroup($adGroup);
$adVariant->setWebsite($website);
$adVariant->setProduct($product);
$adVariant->setBusiness($business);
$adVariant->setLandingPage($landingPage);
$adVariant->setTargetedLandingPage($targetedLandingPage);
$adVariant->setTrackingCode($trackingCode);
$adVariant->setCostType($costType);
$adVariant->setUnitCost($unitCost);
$adVariant->setStatus($status);
$adVariant->setDateUpdated(new \DateTime());
Step 3 - Using Doctrine ObjectSelect
Instead of using ZF2's Select element, we should use Doctrine's ObjectSelect
from its form element collection, which works with a "Proxy" object. The ObjectSelect itself is pretty straightforward. Here's how to use it:
$adGroup = new ObjectSelect('adGroup');
$adGroup->setEmptyOption('Select..');
$adGroup->setOptions(array(
'object_manager' => $this->entityManager,
'target_class' => '\Ed\Advertisers\Entity\AdGroup',
'property' => 'name',
'is_method' => true,
'find_method' => array(
'name' => 'findBy',
'params' => array(
'criteria' => array('status' => 'ACTIVE'),
),
),
));
This uses the repository's findBy()
method to find all Ad Groups with a status of "ACTIVE".
Step 4 - Custom options
After examining the ObjectSelect
class, it seems we can provide our own options (for example, we wanted to have <optgroup>
groups on one of our dropdowns):
// .. build $optionValues, e.g.:
// $optionValues = array(
// 0 => array(
// 'label' => 'MyModule1',
// 'options' => array(
// 1 => 'page1',
// 2 => 'page2',
// 3 => 'page3',
// ),
// ),
// 1 => array(
// 'label' => 'MyModule2',
// 'options' => array(
// 4 => 'page1',
// 5 => 'page2',
// 6 => 'page3',
// ),
// ),
// );
$landingPage = new ObjectSelect('landingPage');
$landingPage->setEmptyOption('Select..');
$landingPage->setOptions(array(
'object_manager' => $this->entityManager,
'target_class' => '\Ed\Advertisers\Entity\AdGroup',
));
$landingPage->setValueOptions($optionValues);
This works because in the ObjectSelect->getValueOptions()
method only calls the proxy method if the $this->valueOptions
is NOT empty:
public function getValueOptions()
{
if (empty($this->valueOptions)) { // this won't be empty because we have called setValueOptions in our form
$this->setValueOptions($this->getProxy()->getValueOptions());
}
return $this->valueOptions;
}
Huzzah! Things now work :)