Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Doctrine ORM 2.9 use both AnnotationDriver and AttributeDriver to parse entity metadata

Recently we upgraded our applications to PHP8.

Since PHP8 introduced attributes and doctrine/orm supports them as of version 2.9 it seemed like a good idea to utilize this feature to incrementally (ie. not all entities at once) update entity metadata to the attributes' format.

In order to do so I need to somehow register both Doctrine\ORM\Mapping\Driver\AnnotationDriver and Doctrine\ORM\Mapping\Driver\AttributeDriver to parse the metadata.

The tricky part is to register both parsers for a set of entities decorated either using annotations or attributes. From the point of Doctrine\ORM\Configuration it seems what I need is not possible.

Am I correct (in assumption this cannot be reasonably achieved) or could this be done in some not-very-hackish way?

like image 493
helvete Avatar asked Feb 07 '26 19:02

helvete


1 Answers

Doctrine by itself doesn't offer this possibility. But we can implement a custom mapping driver to make this happen.

The actual implementation could look like this:

<?php                                                                           
                                                                                
namespace Utils\Doctrine;                                                    
                                                                                
use Doctrine\ORM\Mapping\Driver\AnnotationDriver;                               
use Doctrine\ORM\Mapping\Driver\AttributeDriver;                                
use Doctrine\ORM\Mapping\MappingException;                                      
use Doctrine\Persistence\Mapping\ClassMetadata;                                 
use Doctrine\Persistence\Mapping\Driver\AnnotationDriver as AbstractAnnotationDriver;
                                                                                
class HybridMappingDriver extends AbstractAnnotationDriver                      
{                                                                               
    public function __construct(                                                
        private AnnotationDriver $annotationDriver,                                
        private AttributeDriver $attributeDriver,                                  
    ) {                                                                            
    }                                                                              
                                                                                
    public function loadMetadataForClass($className, ClassMetadata $metadata): void
    {                                                                           
        try {                                                                      
            $this->attributeDriver->loadMetadataForClass($className, $metadata);
            return;                                                             
        } catch (MappingException $me) {                                        
            // Class X is not a valid entity, so try the other driver            
            if (!preg_match('/^Class(.)*$/', $me->getMessage())) {// meh           
                throw $me;                                                         
            }                                                                      
        }                                                                       
        $this->annotationDriver->loadMetadataForClass($className, $metadata);   
    }                                                                            
                                                                                 
    public function isTransient($className): bool                                     
    {                                                                           
        return $this->attributeDriver->isTransient($className)                     
            || $this->annotationDriver->isTransient($className);                   
    }                                                                              
}

In a nutshell:

  • the driver tries to use AttributeDriver first, then fallbacks to the AnnotationDriver in case the class under inspection is not evaluated as a valid entity
  • in order to comply with Doctrine\Persistence\Mapping\Driver\MappingDriver interface after extending Doctrine\Persistence\Mapping\Driver\AnnotationDriver class only 2 methods have to be implemented
  • as it can be seen in the example implementation both methods regard both metadata mapping drivers
  • distinguishing between various kinds of MappingExceptions by parsing the message is not elegant at all, but there is no better attribute to distinguish by; having different exception subtypes or some unique code per mapping error case would help a lot to differentiate between individual causes of mapping errors

The HybridMappingDriver can be hooked up in an EntityManagerFactory like this:

<?php

namespace App\Services\Doctrine;

use Doctrine\ORM\Tools\Setup;
use Doctrine\ORM\EntityManager;
use Doctrine\Common\Annotations\AnnotationRegistry;
use Doctrine\Common\Proxy\AbstractProxyFactory as APF;
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
use Utils\Doctrine\NullCache;

class EntityManagerFactory
{
    public static function create(
        array $params,
        MappingDriver $mappingDriver,
        bool $devMode,
    ): EntityManager {
        AnnotationRegistry::registerLoader('class_exists');
        $config = Setup::createConfiguration(
            $devMode,
            $params['proxy_dir'],
            new NullCache(), // must be an instance of Doctrine\Common\Cache\Cache
        );
        $config->setMetadataDriverImpl($mappingDriver); // <= this is the actual hook-up
        if (!$devMode) {
            $config->setAutoGenerateProxyClasses(APF::AUTOGENERATE_FILE_NOT_EXISTS);
        }

        return EntityManager::create($params['database'], $config);
    }
}
like image 104
helvete Avatar answered Feb 09 '26 10:02

helvete



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!