Repository: DamienHarper/auditor (php)

Issue:
DoctrineSubscriber throw deprecation when Driver is wrapped with another class
<!--
- Please do not report an issue for a version of `auditor` that is no longer supported. Currently supported versions are listed here: https://github.com/DamienHarper/auditor#version-information
- Please fill in this template according to your issue.
- Please keep the table shown below at the top of your issue.
- You can retrieve `auditor` version by running `composer info | grep "auditor"`.
- You can retrieve PHP version by running `php -v`.
- Please post code as text (using proper markup). Do not post screenshots of code.
- Please remove this comment before submitting your issue.
-->

| Q                    | A
| ---------------------| -----------------------
| `auditor` version    | 2.4.7
| PHP version          | 8.1
| Database             | MySQL

#### Summary

I have both Auditor and Sentry packages.
Sentry also has a middleware that wraps connection with a special class.
Depending on priority of this middlewares `DoctrineSubscriber` may trigger a deprecation because $driver is not a `DHDriver`, but actually it's wrapped.


#### Current behavior

trigger deprecation when it's actually should not 

#### How to reproduce

Install both damienharper/auditor-bundle and sentry/sentry-symfony

#### Expected behavior

correct detection of `DHDriver`


Pull request description:
Detected wrapped driver
fixes #184 

Patch:
diff --git a/src/Provider/Doctrine/Auditing/Event/DoctrineSubscriber.php b/src/Provider/Doctrine/Auditing/Event/DoctrineSubscriber.php
index f43cc7a..46a7afe 100644
--- a/src/Provider/Doctrine/Auditing/Event/DoctrineSubscriber.php
+++ b/src/Provider/Doctrine/Auditing/Event/DoctrineSubscriber.php
@@ -4,6 +4,7 @@ declare(strict_types=1);
 
 namespace DH\Auditor\Provider\Doctrine\Auditing\Event;
 
+use Closure;
 use DH\Auditor\Provider\Doctrine\Auditing\Logger\Logger;
 use DH\Auditor\Provider\Doctrine\Auditing\Logger\LoggerChain;
 use DH\Auditor\Provider\Doctrine\Auditing\Logger\Middleware\DHDriver;
@@ -11,9 +12,12 @@ use DH\Auditor\Provider\Doctrine\Auditing\Transaction\TransactionManager;
 use DH\Auditor\Provider\Doctrine\Model\Transaction;
 use DH\Auditor\Provider\Doctrine\Persistence\Helper\DoctrineHelper;
 use Doctrine\Common\EventSubscriber;
+use Doctrine\DBAL\Driver;
+use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware;
 use Doctrine\DBAL\Logging\SQLLogger;
 use Doctrine\ORM\Event\OnFlushEventArgs;
 use Doctrine\ORM\Events;
+use ReflectionClass;
 
 class DoctrineSubscriber implements EventSubscriber
 {
@@ -47,6 +51,11 @@ class DoctrineSubscriber implements EventSubscriber
         $this->transactionManager->populate($transaction);
 
         $driver = $entityManager->getConnection()->getDriver();
+
+        if (!$driver instanceof DHDriver) {
+            $driver = $this->getWrappedDriver($driver);
+        }
+
         if ($driver instanceof DHDriver) {
             $driver->addDHFlusher(function () use ($transaction): void {
                 $this->transactionManager->process($transaction);
@@ -87,4 +96,41 @@ class DoctrineSubscriber implements EventSubscriber
     {
         return [Events::onFlush];
     }
+
+    /**
+     * @internal this method is used to retrieve the wrapped driver from the given driver
+     *
+     * @return Closure|Driver
+     */
+    public function getWrappedDriver(Driver $driver)
+    {
+        $that = $this;
+
+        // if the driver is already a DHDriver, return it
+        if ($driver instanceof DHDriver) {
+            return $driver;
+        }
+
+        // if the driver is an instance of AbstractDriverMiddleware, return the wrapped driver
+        if ($driver instanceof AbstractDriverMiddleware) {
+            return Closure::bind(function () use ($that) {
+                // @var AbstractDriverMiddleware $this
+                return $that->getWrappedDriver($this->wrappedDriver);
+            }, $driver, AbstractDriverMiddleware::class)();
+        }
+
+        return Closure::bind(function () use ($that) {
+            /** @var Driver $this */
+            $properties = (new ReflectionClass($this))->getProperties();
+            foreach ($properties as $property) {
+                $property->setAccessible(true);
+                $value = $property->getValue($this);
+                if ($value instanceof Driver) {
+                    return $that->getWrappedDriver($value);
+                }
+            }
+
+            return null;
+        }, $driver, Driver::class)() ?? $driver;
+    }
 }
diff --git a/src/Provider/Doctrine/Auditing/Logger/Middleware/DHDriver.php b/src/Provider/Doctrine/Auditing/Logger/Middleware/DHDriver.php
index 1e74141..33162c0 100644
--- a/src/Provider/Doctrine/Auditing/Logger/Middleware/DHDriver.php
+++ b/src/Provider/Doctrine/Auditing/Logger/Middleware/DHDriver.php
@@ -4,7 +4,6 @@ declare(strict_types=1);
 
 namespace DH\Auditor\Provider\Doctrine\Auditing\Logger\Middleware;
 
-use Doctrine\DBAL\Driver as DriverInterface;
 use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware;
 
 /**
@@ -12,23 +11,12 @@ use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware;
  */
 final class DHDriver extends AbstractDriverMiddleware
 {
-    private DriverInterface $driver;
-
     /** @var array<callable> */
     private array $flusherList = [];
 
-    public function __construct(DriverInterface $driver)
-    {
-        parent::__construct($driver);
-        $this->driver = $driver;
-    }
-
     public function connect(array $params): DHConnection
     {
-        return new DHConnection(
-            $this->driver->connect($params),
-            $this
-        );
+        return new DHConnection(parent::connect($params), $this);
     }
 
     public function addDHFlusher(callable $flusher): void


Test patch:
diff --git a/tests/Provider/Doctrine/Event/DoctrineSubscriberTest.php b/tests/Provider/Doctrine/Event/DoctrineSubscriberTest.php
index f036c36..13fa6e4 100644
--- a/tests/Provider/Doctrine/Event/DoctrineSubscriberTest.php
+++ b/tests/Provider/Doctrine/Event/DoctrineSubscriberTest.php
@@ -5,11 +5,17 @@ declare(strict_types=1);
 namespace DH\Auditor\Tests\Provider\Doctrine\Event;
 
 use DH\Auditor\Provider\Doctrine\Auditing\Event\DoctrineSubscriber;
+use DH\Auditor\Provider\Doctrine\Auditing\Logger\LoggerChain;
+use DH\Auditor\Provider\Doctrine\Auditing\Logger\Middleware\DHDriver;
 use DH\Auditor\Provider\Doctrine\Auditing\Transaction\TransactionManager;
 use Doctrine\DBAL\Configuration;
-use Doctrine\DBAL\Connection;
+use Doctrine\DBAL\Connection as ConnectionDbal;
 use Doctrine\DBAL\Driver;
+use Doctrine\DBAL\Driver\API\MySQL\ExceptionConverter;
+use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware;
 use Doctrine\DBAL\Logging\SQLLogger;
+use Doctrine\DBAL\Platforms\AbstractPlatform;
+use Doctrine\DBAL\VersionAwarePlatformDriver;
 use Doctrine\ORM\EntityManagerInterface;
 use Doctrine\ORM\Event\OnFlushEventArgs;
 use PHPUnit\Framework\TestCase;
@@ -30,7 +36,7 @@ final class DoctrineSubscriberTest extends TestCase
 
         $objectManager
             ->method('getConnection')
-            ->willReturn($connection = $this->createMock(Connection::class))
+            ->willReturn($connection = $this->createMock(ConnectionDbal::class))
         ;
 
         $connection
@@ -59,4 +65,144 @@ final class DoctrineSubscriberTest extends TestCase
         $result = $configuration->getSQLLogger();
         self::assertCount(2, $result->getLoggers());
     }
+
+    public function testIssue184IfAbstractDriverMiddleware(): void
+    {
+        $transactionManager = $this->createMock(TransactionManager::class);
+        $objectManager = $this->createMock(EntityManagerInterface::class);
+
+        $args = new OnFlushEventArgs($objectManager);
+
+        $nativeDriver = $this->createMock(Driver::class);
+        $dhDriver = new DHDriver($nativeDriver);
+        $driver = new class($dhDriver) extends AbstractDriverMiddleware {};
+
+        $objectManager
+            ->method('getConnection')
+            ->willReturn($connection = $this->createMock(ConnectionDbal::class))
+        ;
+
+        $connection
+            ->method('getDriver')
+            ->willReturn($driver)
+        ;
+
+        $target = new DoctrineSubscriber($transactionManager);
+        $target->onFlush($args);
+
+        $transactionManager
+            ->expects(self::once())
+            ->method('process')
+        ;
+
+        foreach ($dhDriver->getFlusherList() as $item) {
+            ($item)();
+        }
+    }
+
+    public function testIssue184IfNotAbstractDriverMiddleware(): void
+    {
+        $transactionManager = $this->createMock(TransactionManager::class);
+        $objectManager = $this->createMock(EntityManagerInterface::class);
+
+        $args = new OnFlushEventArgs($objectManager);
+
+        $nativeDriver = $this->createMock(Driver::class);
+        $dhDriver = new DHDriver($nativeDriver);
+        $driver = new class($dhDriver) implements VersionAwarePlatformDriver {
+            /** @noinspection PhpPropertyOnlyWrittenInspection */
+            private Driver $wrappedDriver;
+
+            public function __construct(Driver $wrappedDriver)
+            {
+                $this->wrappedDriver = $wrappedDriver;
+            }
+
+            public function connect(array $params): void {}
+
+            public function getDatabasePlatform(): void {}
+
+            public function getSchemaManager(ConnectionDbal $conn, AbstractPlatform $platform): void {}
+
+            public function getExceptionConverter(): Driver\API\ExceptionConverter
+            {
+                return new ExceptionConverter();
+            }
+
+            public function createDatabasePlatformForVersion($version): void {}
+        };
+
+        $objectManager
+            ->method('getConnection')
+            ->willReturn($connection = $this->createMock(ConnectionDbal::class))
+        ;
+
+        $connection
+            ->method('getDriver')
+            ->willReturn($driver)
+        ;
+
+        $target = new DoctrineSubscriber($transactionManager);
+        $target->onFlush($args);
+
+        $transactionManager
+            ->expects(self::once())
+            ->method('process')
+        ;
+
+        foreach ($dhDriver->getFlusherList() as $item) {
+            ($item)();
+        }
+    }
+
+    public function testIssue184Unexpected(): void
+    {
+        $transactionManager = $this->createMock(TransactionManager::class);
+        $objectManager = $this->createMock(EntityManagerInterface::class);
+
+        $args = new OnFlushEventArgs($objectManager);
+
+        $driver = new class() implements VersionAwarePlatformDriver {
+            public function connect(array $params): void {}
+
+            public function getDatabasePlatform(): void {}
+
+            public function getSchemaManager(ConnectionDbal $conn, AbstractPlatform $platform): void {}
+
+            public function getExceptionConverter(): Driver\API\ExceptionConverter
+            {
+                return new ExceptionConverter();
+            }
+
+            public function createDatabasePlatformForVersion($version): void {}
+        };
+
+        $objectManager
+            ->method('getConnection')
+            ->willReturn($connection = $this->createMock(ConnectionDbal::class))
+        ;
+
+        $connection
+            ->method('getDriver')
+            ->willReturn($driver)
+        ;
+
+        $connection
+            ->method('getConfiguration')
+            ->willReturn($configuration = $this->createMock(Configuration::class))
+        ;
+
+        $transactionManager
+            ->expects(self::never())
+            ->method('process')
+        ;
+
+        $configuration->expects(self::once())
+            ->method('setSQLLogger')
+            ->with(self::isInstanceOf(LoggerChain::class))
+        ;
+
+        $target = new DoctrineSubscriber($transactionManager);
+        $target->onFlush($args);
+    }
 }