doctrine / DoctrineORMModule

Doctrine ORM Module for Laminas
https://www.doctrine-project.org/projects/doctrine-orm-module.html
MIT License
437 stars 229 forks source link

How do you migrate from doctrine-orm-module 1 to 4, wrt. annotations? #716

Open jarrettj opened 2 years ago

jarrettj commented 2 years ago

Hey,

Good day.

We currently having issues with the doctrine EntityBasedFormBuilder. This is our current setup:

<?php
namespace Application\Annotation;

use Doctrine\Laminas\Hydrator\DoctrineObject;
use Doctrine\ORM\EntityManager;
use DoctrineORMModule\Form\Annotation\EntityBasedFormBuilder;
use Laminas\Code\Annotation\Parser\DoctrineAnnotationParser;
use Laminas\Code\Annotation\AnnotationManager;

class Builder
{
    private $customAnnotations = array(
        'Guid',
        ...
    )
    protected $entityManager;

    ...
    public function createForm($entity)
    {
        $builder = new EntityBasedFormBuilder($this->entityManager);
        $form = $builder->createForm($entity);

        $hydrator = new DoctrineObject($this->entityManager, true);
        $form->setHydrator($hydrator);

        return $form;
    }

The bits I am not sure of is why my ‘id’ no longer works for an existing entity? I get the following message:

Additional information:
Laminas\Form\Exception\InvalidElementException
File:
/var/www/html/vendor/laminas/laminas-form/src/Fieldset.php                    :207
Message:
No element by the name of [id] found in form
Stack trace:
#0 /var/www/html/module/Litigation/view/litigation/matter/add.phtml(83): Laminas\Form\Fieldset->get('id')
#1 /var/www/html/vendor/laminas/laminas-view/src/Renderer/PhpRenderer.php(519): include('/var/www/html/m...')
#2 /var/www/html/vendor/laminas/laminas-view/src/View.php(194): Laminas\View\Renderer\PhpRenderer->render(NULL)
#3 /var/www/html/vendor/laminas/laminas-view/src/View.php(222): Laminas\View\View->render(Object(Laminas\View\Model\ViewModel))
#4 /var/www/html/vendor/laminas/laminas-view/src/View.php(187): Laminas\View\View->renderChildren(Object(Laminas\View\Model\ViewModel))
#5 /var/www/html/vendor/laminas/laminas-mvc/src/View/Http/DefaultRenderingStrategy.php(98): Laminas\View\View->render(Object(Laminas\View\Model\ViewModel))
#6 /var/www/html/vendor/laminas/laminas-eventmanager/src/EventManager.php(319): Laminas\Mvc\View\Http\DefaultRenderingStrategy->render(Object(Laminas\Mvc\MvcEvent))
#7 /var/www/html/vendor/laminas/laminas-eventmanager/src/EventManager.php(171): Laminas\EventManager\EventManager->triggerListeners(Object(Laminas\Mvc\MvcEvent))
#8 /var/www/html/vendor/laminas/laminas-mvc/src/Application.php(360): Laminas\EventManager\EventManager->triggerEvent(Object(Laminas\Mvc\MvcEvent))
#9 /var/www/html/vendor/laminas/laminas-mvc/src/Application.php(341): Laminas\Mvc\Application->completeRequest(Object(Laminas\Mvc\MvcEvent))
#10 /var/www/html/public/index.php(31): Laminas\Mvc\Application->run()
#11 {main}

composer.json

        ....
        "doctrine/annotations": "^1.13",
        "doctrine/doctrine-orm-module": "^4.1",
        "doctrine/migrations": "^3.3",
        "doctrine/orm": "^2.10",
        "gedmo/doctrine-extensions": "^3.4",
        ....

Any help would be much appreciated. Thanks.

Regards, Jarrett

driehle commented 2 years ago

@jarrettj Could you please post the entity with its annotation here? Otherwise, it will be hard to guess what could be the issue.

It looks like you are having some custom Guid annotation. That one probably is not correctly registered with the annotation builder. You should probably post the annotation class as well.

jarrettj commented 2 years ago

Hi,

Good day.

All our entities extends from a base class that simply has the following:

<?php

namespace Application\Entity;

use Doctrine\ORM\Mapping as ORM;
use Gedmo as Gedmo;
use Laminas\Form\Annotation;

/**
 * @ORM\MappedSuperclass
 */
class Entity
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     * @Annotation\Attributes({"type":"hidden"})
     */
    protected $id;

    /**
     * @Gedmo\Mapping\Annotation\Timestampable(on="create")
     * @ORM\Column(type="datetime")
     * @Annotation\Exclude()
     */
    protected $created;

    /**
     * @Gedmo\Mapping\Annotation\Timestampable(on="update")
     * @ORM\Column(type="datetime")
     * @Annotation\Exclude()
     */
    protected $modified;

    /**
     * Magic getter to expose protected properties.
     *
     * @param string $property
     *
     * @return mixed
     */
    public function __get($property)
    {
        return $this->{$property};
    }

    /**
     * Magic setter to save protected properties.
     *
     * @param string $property
     * @param mixed  $value
     */
    public function __set($property, $value)
    {
        $this->{$property} = $value;
    }

    /**
     * Convert the object to an array.
     *
     * @return array
     */
    public function getArrayCopy()
    {
        return get_object_vars($this);
    }

    /**
     * @param mixed $id
     */
    public function setId($id)
    {
        $this->id = $id;
    }

    /**
     * @return mixed
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * @return mixed
     */
    public function getCreated()
    {
        return $this->created;
    }

    /**
     * @param mixed $created
     */
    public function setCreated($created)
    {
        $this->created = $created;
    }

    /**
     * @return mixed
     */
    public function getModified()
    {
        return $this->modified;
    }

    /**
     * @param mixed $modified
     */
    public function setModified($modified)
    {
        $this->modified = $modified;
    }
}

Another thing I have noticed is that ManyToMany relationships are also not detected anymore. All other fields Work though. Example ManyToMany:

    /**
     * @var User[]
     *
     * @ORM\Cache("READ_WRITE")
     * @ORM\ManyToMany(targetEntity="Application\Entity\User", mappedBy="matters")
     * @ORM\JoinTable(name="UserMatterLinker",
     *      joinColumns={@ORM\JoinColumn(name="matter_id", referencedColumnName="id")},
     *      inverseJoinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id")}
     * )
     *
     * @Annotation\Validator("NotEmpty")
     * @Annotation\Type("DoctrineModule\Form\Element\ObjectSelect")
     * @Annotation\Attributes({
     *      "multiple": "multiple",
     * })
     * @Annotation\Options({
     *      "label": "External User Access",
     *      "target_class": "Application\Entity\User",
     *      "property": "displayName",
     *      "find_method"={"name": "findExternalUsers", "params"={"criteria"={}, "orderBy"={"displayName":"ASC"}}}
     * })
     */
    public $users;

Also, regarding the Guid annotation, I have not yet figured out how to add those as it was before. Since we can no onger extend Laminas\Form\Annotation\AnnotationBuilder. This means I can't do the following:

public function setAnnotationManager(AnnotationManager $annotationManager)
    {
        parent::setAnnotationManager($annotationManager);

        $parser = new DoctrineAnnotationParser();

        foreach ($this->customAnnotations as $module => $annotationName) {
            if (is_numeric($module)) {
                $class = __NAMESPACE__ . '\\' . $annotationName;
            }
            else {
                $class = $module;
            }

            $parser->registerAnnotation($class);
        }
        $annotationManager->attach($parser);

        return $this;
    }

First wanted to figure out why the 'id' and ManyToMany fields are not seen anymore. Thank you very much for the quick response and apologise for my delay.

Regards. Jarrett

driehle commented 2 years ago

Let's start with the annotations first. Earlier versions of DoctrineORMModule (<4.0) used an annotation parsing engine from laminas-code, which was removed with release 4.0.0 of laminas-code. Therefore, we switched to a different annotation parsing engine provided by doctrine/annotations.

In most cases, the annotations (i.e. the comment in the docblock section) can remain the same. However, the annotation class itself might need a little adjustment in regard to its contructor arguments, if the annotation is supposed to have arguments. That was why I asked for the code of your annotation class. If you have an annotation @Guid(), you need to have a class, e.g. Application\Annotation\Guid and import that class in the respective places, where the annotation is used.

Now with doctrine/annotations (in contrast to laminas-form!) you do not need to register each annotation class with the parser, so your $parser->registerAnnotation() is not needed anymore. You'll only need composer's autoloading to be in place.

However, the annotation itself just provides the data, there should be some code in your application which actually handles the annotation. In 1.x, 2.x and 3.x this was done with the class ElementAnnotationsListener, in >4.1 and 5.x that class is named DoctrineAnnotationsListener. In essence, you will need an event listener to handle your custom annotations.

Register your custom listener as follows:

$formBuilder = new EntityBasedFormBuilder($this->entityManager);
$customEventListener = new CustomEventListener(...);
$formBuilder->getBuilder()->attach($customEventListener);

Afterwards, you can call $formBuilder->createForm($entity). However, for debugging purposes, I recommend to first call $formBuilder->getFormSpecification($entity). This will provide you with an array, i.e. an array-based laminas form specification that is later passed to the usual laminas form factory. So make sure that this array actually matches your expectations!

Regarding your issue with the id field and many-to-many associations not being identified correctly, I suggest that you provide a full example (i.e. entity class, mapped superclass, all custom annotations, custom event listener) together with the output of $formBuilder->getFormSpecification().

jarrettj commented 2 years ago

Builder.php https://pastebin.com/znRySF40 Listener.php https://pastebin.com/MTED6Ne1 Guid.php https://pastebin.com/PN25sWZP

Added the one custom annotation as the others would follow the same setup.

Previously we retrieved the Builder using the serviceLocator, but that's been deprecated:

$builder = $this->serviceLocator->get('AppAnnotationBuilder');

My assumption is that I should instead inject the builder into my Controller class via its Factory class:

<?php
namespace Litigation\Factory;

use Litigation\Controller\MatterController;

class Matter implements \Laminas\ServiceManager\Factory\FactoryInterface
{
    public function __invoke(\Interop\Container\ContainerInterface $container, $requestedName, array $options = null) {
        return new MatterController(
                $container->get('AppAnnotationBuilder'),
                $container->get('doctrine.entitymanager.orm_default'),
                ...
                );
    }
}

The creation of AppAnnotationBuilder is in module/Application/config/module.config.php:

'AppAnnotationBuilder' => function ($serviceManager) {
                return new Application\Annotation\Builder(
                    $serviceManager->get('doctrine.entitymanager.orm_default'),
                    $serviceManager->get('AppListener')
                );
            },

Not using AppListener, as you detailed, the registration of custom annotations have changed. I tried to use CustomEventListener, but that class does not exist in my project. How do you install that? My current composer.json:

        "doctrine/annotations": "^1.13",
        "doctrine/doctrine-orm-module": "^4.1"
        "doctrine/orm": "^2.10",
        "laminas/laminas-mvc": "^3.3",

I might be missing a doctrine module I guess?

My mapped superclass would be the one specified in the response above 'class Entity'. In which the 'id' of all extending Entities should derive from. An example would be Phase:

<?php
namespace Litigation\Entity;

use Application\Annotation as AppAnnotation;
use Doctrine\ORM\Mapping as ORM;
use Laminas\Form\Annotation;

use Doctrine\Common\Collections\ArrayCollection;

use Application\Entity\MatterSubType;

use Application\Entity\Entity;

/**
 * @package Application
 *
 * @ORM\Cache("READ_WRITE")
 * @ORM\Entity(repositoryClass="Litigation\Repository\Phase")
 * @ORM\Table(name="Phase")
 *
 * @Annotation\Name("Phase")
 * @Annotation\Hydrator("Laminas\Hydrator\ObjectPropertyHydrator")
 *
 */
class Phase extends Entity
{
/**
     * @var string
     *
     * @ORM\Column(type="string", unique=true)
     *
     * @Annotation\Filter("StringTrim")
     * @Annotation\Filter("StripTags")
     * @Annotation\Validator("StringLength", {"min":1, "max":255})
     * @Annotation\Attributes({
     *      "type":"text",
     *      "class": "form-control",
     *      "placeholder": "Name",
     *      "autofocus": "autofocus"
     * })
     * @Annotation\Options({
     *      "label":"Name",
     *      "column-size": "sm-10",
     *      "label_attributes": {"class": "col-sm-2" }
     * })
     * @AppAnnotation\NoObjectExists({"entity": "Litigation\Entity\Phase", "fields": "name"})
     */
    protected $name;

}

Thanks for the help thus far.

Regards. Jarrett