Laminas Fieldset Objet / DoctrineObject Hydator issue

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.

I do not use the Doctrine Laminas Hydrators and I find the example in the documentation with the Tag class useless because it allows to create an empty class without data.

My suggestion is to create a custom hydrator where the $object parameter is null

public function hydrate(array $data, object|null $object = null);

And then create your Attribute class there and handle the case without data also in this custom hydrator.

Maybe this help.