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?
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:
AttributeDriver
first, then fallbacks to the AnnotationDriver
in case the class under inspection is not evaluated as a valid entityDoctrine\Persistence\Mapping\Driver\MappingDriver
interface after extending Doctrine\Persistence\Mapping\Driver\AnnotationDriver
class only 2 methods have to be implementedMappingException
s 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 errorsThe 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);
}
}