I’m trying to migrate Laminas\Db project to doctrine and I’ve reached a point where I want to join my forms to the entities. I followed the guide in ( A Complete Example using Laminas\Form - Doctrine Doctrine Laminas Hydrators ) but I have an issue with strictly typed entities.
Imaging a simple entity example (I’m simplifying stuff to keep it short) like the following:
class Post {
/**
* @var Attributes[]|Collection
*
* @ORM\OneToMany(targetEntity="Attribute", mappedBy="post", fetch="EXTRA_LAZY", cascade={"persist"})
*/
protected $attributes;
}
class Attribute {
...
/**
* @var string
* @ORM\Column(type="string", name="name", nullable=false)
*/
protected string $name;
public function getName(): string {
return $this->name;
}
}
One should go about and create an AttributeFieldset
under the Element\Collection
and add it to the main (Post) form as follows:
$this->add([
'type' => Element\Collection::class,
'name' => 'attributes',
'options' => [
'count' => 0,
'should_create_template' => true,
'allow_add' => true,
'allow_remove' => true,
'target_element' => [
'type' => AttributeFieldset::class,
]
],
]);
And the AttributeFieldset
might look like the following:
class AttributeFieldset extends Fieldset implements
InputFilterProviderInterface
{
/**
* @var ObjectManager
*/
protected ObjectManager $objectManager;
/**
* @param ObjectManager $objectManager
*
* @return $this
*/
public function setObjectManager(ObjectManager $objectManager): static
{
$this->objectManager = $objectManager;
return $this;
}
/**
* @return ObjectManager
*/
public function getObjectManager(): ObjectManager
{
return $this->objectManager;
}
public function init()
{
$this
->setHydrator(new DoctrineObject($this->getObjectManager()))
->setObject(new Attribute());
$this->setAllowedObjectBindingClass(Attribute::class);
$this->add([
'type' => Element\Text::class,
'name' => 'name',
'options' => [
'label' => _("Name"),
],
'attributes' => [
'class' => 'form-control'
]
]);
}
}
Imagine now that the following post data is being send to the form trying to update a Post object:
[
...
attributes => [
[
'name' => 'test attribute',
]
],
...
]
And the controller/ middleware whatever handles it like so:
$form = $container->get('FormElementManager')->get(PostForm::class);
$post = $em->getRepository(Post::class)->find(<postId>);
$form->bind($post);
if( $this->getRequest()->isPost() ) {
$form->setData($this->getRequest()->getPost());
if( $form->isValid() ) {
// $post is now updated
}
}
To my understanding, after few hours stepping into Laminas and Doctrine code, when the form is validated Fieldset::extract
is called, which in turns calls the hydrator’s (DoctrineObject) extract
method to get the key value pairs of the entity in an array format.
When the Fieldset gets to the AttributeFieldset
it will try to extract it and since object is set on it via the setObject(new Attribute())
mentioned above, it will try to call getName()
on the Attribute
entity and fail.
The problem however is that you must set an object to the Fieldset so as to get a valid object after the form post data, because the bindValues()
method on the Fieldset needs the object to be set to hydrate it (Line 578 in laminas-form/src/Fieldset.php):
if (! empty($hydratableData) && $this->object) {
$this->object = $hydrator->hydrate($hydratableData, $this->object);
}
To my understanding the entities should be a 1-1 representation of the database and since name cannot be null in the database it shouldn’t be null in the Entity as well.
My question is, how should one handle this? How can I get a valid entity with nested collections via the DoctrineObject after the form post?
I consider that the object at the point of instantiation in the Fieldset ($this->setObject(new Attribute()
) to not be completely valid yet, since Laminas\Form after the InputFilters will either have all the required data to fill it out, or fail in the validation step.