diff --git a/CHANGELOG.md b/CHANGELOG.md
index bee883f90b..0c1d417956 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -43,6 +43,7 @@ a release.
### Added
- IP address provider for use with extensions with IP address references (#2928)
+- Mapping Driver: Added option `forceUseAttributeReader`, force the use of AttributeReader for Gedmo attributes ( ignore default XML driver for all namespaces) (#2613)
## [3.19.0] - 2025-02-24
### Added
diff --git a/src/Mapping/ExtensionMetadataFactory.php b/src/Mapping/ExtensionMetadataFactory.php
index 16d321cc3f..5dc67737ca 100644
--- a/src/Mapping/ExtensionMetadataFactory.php
+++ b/src/Mapping/ExtensionMetadataFactory.php
@@ -15,6 +15,7 @@
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata as DocumentClassMetadata;
use Doctrine\ORM\Mapping\ClassMetadata as EntityClassMetadata;
use Doctrine\ORM\Mapping\ClassMetadataInfo as LegacyEntityClassMetadata;
+use Doctrine\ORM\Mapping\Driver\AttributeDriver;
use Doctrine\Persistence\Mapping\ClassMetadata;
use Doctrine\Persistence\Mapping\Driver\DefaultFileLocator;
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
@@ -70,12 +71,18 @@ class ExtensionMetadataFactory
private ?CacheItemPoolInterface $cacheItemPool = null;
+ /**
+ * Ignore doctrine driver class and force use attribute reader for gedmo properties
+ * @var bool
+ */
+ private $forceUseAttributeReader;
+
/**
* @param Reader|AttributeReader|object|null $annotationReader
*
* @note Providing any object as the third argument is deprecated, as of 4.0 an {@see AttributeReader} will be required
*/
- public function __construct(ObjectManager $objectManager, string $extensionNamespace, ?object $annotationReader = null, ?CacheItemPoolInterface $cacheItemPool = null)
+ public function __construct(ObjectManager $objectManager, string $extensionNamespace, ?object $annotationReader = null, ?CacheItemPoolInterface $cacheItemPool = null, bool $forceUseAttributeReader = false)
{
if (null !== $annotationReader) {
if ($annotationReader instanceof Reader) {
@@ -101,6 +108,7 @@ public function __construct(ObjectManager $objectManager, string $extensionNames
$this->objectManager = $objectManager;
$this->annotationReader = $annotationReader;
$this->extensionNamespace = $extensionNamespace;
+ $this->forceUseAttributeReader = $forceUseAttributeReader;
$omDriver = $objectManager->getConfiguration()->getMetadataDriverImpl();
$this->driver = $this->getDriver($omDriver);
$this->cacheItemPool = $cacheItemPool;
@@ -205,11 +213,19 @@ protected function getDriver($omDriver)
$driverName = substr($className, strrpos($className, '\\') + 1);
if ($omDriver instanceof MappingDriverChain || 'DriverChain' === $driverName) {
$driver = new Chain();
+ $attributeDriver = $this->forceUseAttributeReader ? new AttributeDriver([], true) : null;
foreach ($omDriver->getDrivers() as $namespace => $nestedOmDriver) {
+ if (!$nestedOmDriver instanceof AttributeDriver && $attributeDriver) {
+ $nestedOmDriver = $attributeDriver;
+ }
$driver->addDriver($this->getDriver($nestedOmDriver), $namespace);
}
- if (null !== $omDriver->getDefaultDriver()) {
- $driver->setDefaultDriver($this->getDriver($omDriver->getDefaultDriver()));
+ if ($attributeDriver || null !== $omDriver->getDefaultDriver()) {
+ $defDriver = $omDriver->getDefaultDriver();
+ if (!$defDriver instanceof AttributeDriver && $attributeDriver) {
+ $defDriver = $attributeDriver;
+ }
+ $driver->setDefaultDriver($this->getDriver($defDriver));
}
} else {
$driverName = substr($driverName, 0, strpos($driverName, 'Driver'));
diff --git a/src/Mapping/MappedEventSubscriber.php b/src/Mapping/MappedEventSubscriber.php
index 640e5cb984..ad1c91898f 100644
--- a/src/Mapping/MappedEventSubscriber.php
+++ b/src/Mapping/MappedEventSubscriber.php
@@ -99,12 +99,22 @@ abstract class MappedEventSubscriber implements EventSubscriber
private ?ClockInterface $clock = null;
+ /**
+ * Ignore doctrine driver class and force use attribute reader for gedmo properties
+ * @var bool
+ */
+ private $forceUseAttributeReader = false;
+
public function __construct()
{
$parts = explode('\\', $this->getNamespace());
$this->name = end($parts);
}
+ public function setForceUseAttributeReader(bool $forceUseAttributeReader) {
+ $this->forceUseAttributeReader = $forceUseAttributeReader;
+ }
+
/**
* Get the configuration for specific object class
* if cache driver is present it scans it also
@@ -166,7 +176,8 @@ public function getExtensionMetadataFactory(ObjectManager $objectManager)
$objectManager,
$this->getNamespace(),
$this->annotationReader,
- $this->getCacheItemPool($objectManager)
+ $this->getCacheItemPool($objectManager),
+ $this->forceUseAttributeReader
);
}
diff --git a/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.SeparateDoctrineMapping.dcm.xml b/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.SeparateDoctrineMapping.dcm.xml
new file mode 100644
index 0000000000..d72d9e0bdb
--- /dev/null
+++ b/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.SeparateDoctrineMapping.dcm.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Gedmo/Mapping/Fixture/Xml/SeparateDoctrineMapping.php b/tests/Gedmo/Mapping/Fixture/Xml/SeparateDoctrineMapping.php
new file mode 100644
index 0000000000..9f46358490
--- /dev/null
+++ b/tests/Gedmo/Mapping/Fixture/Xml/SeparateDoctrineMapping.php
@@ -0,0 +1,34 @@
+ http://www.gediminasm.org
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Gedmo\Tests\Mapping\Fixture\Xml;
+use Gedmo\Mapping\Annotation as Gedmo;
+
+class SeparateDoctrineMapping
+{
+ /**
+ * @var int
+ */
+ private $id;
+
+ private $title;
+
+ /**
+ * @var \DateTime
+ */
+ #[Gedmo\Timestampable(on: 'create')]
+ private $created;
+ /**
+ * @var \DateTime
+ */
+ #[Gedmo\Timestampable(on: 'update')]
+ private $updated;
+}
diff --git a/tests/Gedmo/Mapping/SeparateWithDoctrineDriverMappingTest.php b/tests/Gedmo/Mapping/SeparateWithDoctrineDriverMappingTest.php
new file mode 100644
index 0000000000..9de2ffa345
--- /dev/null
+++ b/tests/Gedmo/Mapping/SeparateWithDoctrineDriverMappingTest.php
@@ -0,0 +1,135 @@
+ http://www.gediminasm.org
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Gedmo\Mapping;
+
+use Doctrine\ORM\EntityManager;
+use Doctrine\ORM\Mapping\Driver\AnnotationDriver;
+use Doctrine\ORM\Mapping\Driver\YamlDriver;
+use Gedmo\Tests\Mapping\Fixture\Category;
+use Gedmo\Tests\Mapping\Fixture\Xml\SeparateDoctrineMapping;
+use Gedmo\Tests\Mapping\Fixture\Xml\Timestampable;
+use Gedmo\Tests\Mapping\ORMMappingTestCase;
+use Gedmo\Timestampable\TimestampableListener;
+use Psr\Cache\CacheItemPoolInterface;
+use Symfony\Component\Cache\Adapter\ArrayAdapter;
+
+/**
+ * These are mapping tests for timestampable extension
+ *
+ * @author Gediminas Morkevicius
+ */
+final class SeparateWithDoctrineDriverMappingTest extends ORMMappingTestCase
+{
+ private EntityManager $em;
+ private EntityManager $emWithForceAttr;
+ /**
+ * @var CacheItemPoolInterface
+ */
+ protected $cacheWithForceAttr;
+
+ protected function setUp(): void
+ {
+ if (PHP_VERSION_ID < 80000) {
+ self::markTestSkipped('Test requires PHP 8.');
+ }
+
+ parent::setUp();
+
+ // Base Configuration without forceUseAttributeReader option
+ $listener = new TimestampableListener();
+ $listener->setCacheItemPool($this->cache);
+
+ $this->em = $this->getBasicEntityManager();
+ $this->em->getEventManager()->addEventSubscriber($listener);
+
+
+ // Configuration with forceUseAttributeReader option
+ $this->cacheWithForceAttr = new ArrayAdapter();
+ $listener = new TimestampableListener();
+ $listener->setCacheItemPool($this->cacheWithForceAttr);
+ $listener->setForceUseAttributeReader(true);
+ $this->emWithForceAttr = $this->getBasicEntityManager();
+ $this->emWithForceAttr->getEventManager()->addEventSubscriber($listener);
+
+
+ }
+
+
+ /**
+ * @return \Generator
+ *
+ * @note the XML fixture has a different mapping from the other configs, so it is tested separately
+ */
+ public static function dataTimestampableObject(): \Generator
+ {
+ yield 'Model with separated doctrine(XML Driver), gedmo(Attribute Driver)' => [SeparateDoctrineMapping::class, false, true];
+ yield 'Model without attributes (XML Driver only)' => [Timestampable::class, false, false];
+ yield 'Model with attributes (Attribute Driver only)' => [Category::class, true, true];
+
+ }
+
+ /**
+ * @param class-string $className
+ *
+ * @dataProvider dataTimestampableObject
+ */
+ public function testForceGedmoInAttributeDriverMapping(string $className, bool $doctrineMappingInAttributes, bool $gedmoMappingInAttributes): void
+ {
+ // This entityManager configured to enforce the use of attributes for gedmo.
+ $em = $this->emWithForceAttr;
+ $cache = $this->cacheWithForceAttr;
+ $em->getClassMetadata($className);
+
+ $this->em->getClassMetadata($className);
+ $cacheId = ExtensionMetadataFactory::getCacheId($className, 'Gedmo\Timestampable');
+ $config = $cache->getItem($cacheId)->get();
+
+ if($gedmoMappingInAttributes) {
+ static::assertArrayHasKey('create', $config);
+ static::assertSame('created', $config['create'][0]);
+ static::assertArrayHasKey('update', $config);
+ static::assertSame('updated', $config['update'][0]);
+ }else{
+ static::assertArrayNotHasKey('create', $config);
+ static::assertArrayNotHasKey('update', $config);
+ }
+ }
+ /**
+ * @param class-string $className
+ *
+ * @dataProvider dataTimestampableObject
+ */
+ public function testStandardWayMapping(string $className, bool $doctrineMappingInAttributes, bool $gedmoMappingInAttributes): void
+ {
+ $em = $this->em;
+ $cache = $this->cache;
+ $em->getClassMetadata($className);
+
+ $this->em->getClassMetadata($className);
+ $cacheId = ExtensionMetadataFactory::getCacheId($className, 'Gedmo\Timestampable');
+ $config = $cache->getItem($cacheId)->get();
+
+ if($gedmoMappingInAttributes && !$doctrineMappingInAttributes) {
+ // example: doctrine use XmlDriver, Gedmo - use AttributeDriver,
+ // Gedmo attributes - unreadable
+ static::assertArrayNotHasKey('create', $config);
+ static::assertArrayNotHasKey('update', $config);
+ }else{
+ static::assertArrayHasKey('create', $config);
+ static::assertSame('created', $config['create'][0]);
+ static::assertArrayHasKey('update', $config);
+ static::assertSame('updated', $config['update'][0]);
+ }
+
+ }
+
+}