2

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?

helvete
  • 2,455
  • 13
  • 33
  • 37

2 Answers2

8

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);
    }
}
helvete
  • 2,455
  • 13
  • 33
  • 37
  • 1
    Repository that contains a small library based on this ^ snippets. https://github.com/helvete/hybrid_mapping_driver – helvete Nov 29 '21 at 16:26
1

I'm not sure if it can be done but you could take a look at Rector to automatically upgrade all your entities at once. There already seems to be a config for this.

https://github.com/rectorphp/rector

https://github.com/rectorphp/rector-doctrine/blob/4bbeb676e9ec8c146a81617f6362be4cafbdf3b3/config/sets/doctrine-orm-29.php

  • Thanks for your answer. I thought about it, but bulk upgrade of all entities and follow-up regression tests of all applications is unfortunately out of scope. Hence looking for 'per partes' solution. So far, I just decorated the new entities using both approaches so the switch is less painful once it comes. – helvete Jul 23 '21 at 08:56
  • I ended up using a custom solution. But thanks for the answer anyway. – helvete Sep 22 '21 at 12:16