Best way to hydrate doctrine entity data into laminas form

Long story short …

I use a very complex form with a hand full of fieldsets and form collections. This form uses the ClassMethodsHydrator to hydrate the complex form data into a value object. This value object is given to a doctrine manager class, which processes the value object and stores the complex data via doctrine into different postgresql data tables.

This way works fine. The data is stored in the database. My issue is the other way around. Read the stored data from the database (works pretty well and fast) and hydrate the doctrine entities into the needed form structure.

A shortened code example of the doctrine entity. As you can see the given Partner entity acts as a base class for further inheritances for other entites.

<?php
declare(strict_types=1);
namespace Application\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="\Application\Repository\PartnerRepository")
 * @ORM\InheritanceType("JOINED")
 * @ORM\DiscriminatorColumn(name="discriminator", type="string")
 * @ORM\DiscriminatorMap({
 *     "partner" = "\Application\Entity\Partner", 
 *     "person" = "\Application\Entity\Person", 
 *     "juristische_person" = "\Application\Entity\JuristischePerson"
 * })
 */
class Partner
{
    /**
     * @ORM\Id
     * @ORM\Column(name="id", type="integer")
     * @ORM\GeneratedValue
     */
    protected int $id;

    /**
     * @ORM\ManyToMany(targetEntity="Partner\Entity\Partneranschrift")
     * @ORM\JoinTable(
     *     name="partner_partneranschrift",
     *     joinColumns={@ORM\JoinColumn(name="partner_id", referencedColumnName="id")},
     *     inverseJoinColumns={@ORM\JoinColumn(name="partneranschrift_id", referencedColumnName="id")}
     * )
     */
    protected Collection $anschrift;

    public function __construct()
    {
        $this->anschrift = new ArrayCollection();
    }

    public function getAnschrift(): Collection
    {
        return $this->anschrift;
    }
    
    public function setAnschrift(Collection $anschrift): void
    {
        $this->anschrift = $anschrift;
    }
    
    public function addAnschrift(Adresse $anschrift): void
    {
        $this->anschrift->add($anschrift);
    }
}

As mentioned before, doctrine entities work perfectly in my application. The question is how to hydrate a collection into a form collection? I 've tried the following …

<?php
declare(strict_types=1);
namespace Application\Form;

use Laminas\Form\Fieldset;
use Laminas\Form\Element\Collection;
use Laminas\Form\Element\Hidden;
use Laminas\InputFilter\InputFilterProviderInterface;

class AnschriftCollectionFieldset extends Fieldset implements InputFilterProviderInterface
{
    public function init()
    {
        $this->add([
            'name' => 'count',
            'type' => Hidden::class,
            'attributes' => [
                'id' => 'anschrift-count',
            ],
        ]);
        
        $this->add([
            'name' => 'collection',
            'type' => Collection::class,
            'options' => [
                'count' => 0,
                'should_create_template' => true,
                'template_placeholder' => '__index__',
                'allow_add' => true,
                'allow_remove' => true,
                'target_element' => [
                    'type' => AnschriftCollection::class,
                ],
            ]
        ]);
    }

    public function getInputFilterSpecification(): array
    {
        /* did some validation and filtering here */
    }
}

The target element is a simple fieldset with elements like street, housenumber, zipcode, etc. This results into a simple structure like this …

[
    'anschrift' => [
        'count' => 2,
        'collection' => [
            [
                'strasse' => 'Musterstraße',
                'hausnummer' => '1a',
               ...
            ],
            [
                'strasse' => 'Musterweg',
                'hausnummer' => '17b',
            ],
      ],
]

How do I get the entity data structure into the array data structure needed by the form instance?
I 've tried the following hydrator stuff, to extract the data from the entity into the given array structure above.

<?php
declare(strict_types=1);
namespace Application\Hydrator\Factory;

class PersonReflectionHydratorFactory implements FactoryInterface
{
    /**
     * {@inheritDoc}
     * @see \Laminas\ServiceManager\Factory\FactoryInterface::__invoke()
     */
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        $hydrator = new PartnerReflectionHydrator();
        $hydrator->setNamingStrategy(new UnderscoreNamingStrategy());
        $hydrator->addStrategy('anschrift', new AnschriftHydratorStrategy());
        
        return $hydrator;
    }
}

… and the AnschriftHydratorStrategy looks as follows …

declare(strict_types=1);
namespace Partner\Hydrator\Strategy;

use Doctrine\Common\Collections\Collection;
use Laminas\Hydrator\ClassMethodsHydrator;
use Laminas\Hydrator\Strategy\ClosureStrategy;
use Laminas\Hydrator\Strategy\CollectionStrategy;
use Laminas\Hydrator\Strategy\StrategyInterface;
use Application\Entity\Anschrift;
use BadMethodCallException;

class AnschriftHydratorStrategy implements StrategyInterface
{
    public function extract($value, $object = null)
    {
        if ($value instanceof Collection) {
            $hydrator = new ClassMethodsHydrator();
            $hydrator->addStrategy('geoposition', new ClosureStrategy(function($data) {
                $cmh = new ClassMethodsHydrator();
                $extracted = $cmh->extract($data);
                
                return $extracted;
            }));
                
            $strategy = new CollectionStrategy($hydrator, Partneranschrift::class);
            
            $value = [
                'count' => $value->count(),
                'collection' => $strategy->extract($value->toArray()),
            ];
        }
        
        return $value;
    }

    public function hydrate($value, ?array $data)
    {
        throw new BadMethodCallException('This method is not implemented yet as it is not needed at the time this hydrator was implemented.');
    }
}

This works fine for now. When I read the data from the database, I get the Anschrift entity with all the collections and sub-entities. In my controller I call the hydrator to extract the data from the doctrine entity into the given array structure above.

$partner = $entityManager->getRepository(Partner::class)->find($id);
$array = $hydrator->extract($partner);

Is there a shorter way or do I have to write this kind of hydrator logic to extract all the data from doctrine into the array structure the form is working with?

Have you tried the doctrine-laminas-hydrator?

Schöner Name! :grin:

Oh man! I could have guessed that such a hydrator already exists. Sometimes you can’t see the forest for the trees. Thank you for this really valuable tip!

Noch einen schönen Abend. :wink::beer: