Repository: doctrine-extensions/DoctrineExtensions (language: php)

Issue:
[Translatable] Deprecation notice: TranslationWalker should implement OutputWalker
### Environment

#### Package

<details><summary>show</summary>
<p>

```
name     : gedmo/doctrine-extensions
descrip. : Doctrine behavioral extensions
keywords : Blameable, behaviors, doctrine, extensions, gedmo, loggable, nestedset, odm, orm, sluggable, sortable, timestampable, translatable, tree, uploadable
versions : * v3.17.1
released : 2024-10-07, 1 month ago
latest   : v3.17.1 released 2024-10-07, 1 month ago
type     : library
license  : MIT License (MIT) (OSI approved) https://spdx.org/licenses/MIT.html#licenseText
homepage : http://gediminasm.org/
source   : [git] https://github.com/doctrine-extensions/DoctrineExtensions.git eabb45018c5a4362b46c5beae3881261da89f900
dist     : [zip] https://api.github.com/repos/doctrine-extensions/DoctrineExtensions/zipball/eabb45018c5a4362b46c5beae3881261da89f900 eabb45018c5a4362b46c5beae3881261da89f900
path     : /Users/jle/Documents/WebDev/services/vendor/gedmo/doctrine-extensions
names    : gedmo/doctrine-extensions

support
email : gediminas.morkevicius@gmail.com
issues : https://github.com/doctrine-extensions/DoctrineExtensions/issues
source : https://github.com/doctrine-extensions/DoctrineExtensions/tree/v3.17.1
wiki : https://github.com/Atlantic18/DoctrineExtensions/tree/main/doc

autoload
psr-4
Gedmo\ => src/

requires
behat/transliterator ^1.2
doctrine/collections ^1.2 || ^2.0
doctrine/common ^2.13 || ^3.0
doctrine/deprecations ^1.0
doctrine/event-manager ^1.2 || ^2.0
doctrine/persistence ^2.2 || ^3.0
php ^7.4 || ^8.0
psr/cache ^1 || ^2 || ^3
psr/clock ^1
symfony/cache ^5.4 || ^6.0 || ^7.0

requires (dev)
doctrine/annotations ^1.13 || ^2.0
doctrine/cache ^1.11 || ^2.0
doctrine/dbal ^3.7 || ^4.0
doctrine/doctrine-bundle ^2.3
doctrine/mongodb-odm ^2.3
doctrine/orm ^2.14.0 || ^3.0
friendsofphp/php-cs-fixer ^3.14.0
nesbot/carbon ^2.71 || ^3.0
phpstan/phpstan ^1.11
phpstan/phpstan-doctrine ^1.4
phpstan/phpstan-phpunit ^1.4
phpunit/phpunit ^9.6
rector/rector ^1.1
symfony/console ^5.4 || ^6.0 || ^7.0
symfony/doctrine-bridge ^5.4 || ^6.0 || ^7.0
symfony/phpunit-bridge ^6.0 || ^7.0
symfony/uid ^5.4 || ^6.0 || ^7.0
symfony/yaml ^5.4 || ^6.0 || ^7.0

suggests
doctrine/mongodb-odm to use the extensions with the MongoDB ODM
doctrine/orm to use the extensions with the ORM

conflicts
doctrine/annotations <1.13 || >=3.0
doctrine/dbal <3.7 || >=5.0
doctrine/mongodb-odm <2.3 || >=3.0
doctrine/orm <2.14.0 || 2.16.0 || 2.16.1 || >=4.0
```

</p>
</details>

#### Doctrine packages

<details><summary>show</summary>
<p>

```
Direct dependencies required in composer.json:
doctrine/dbal                       3.9.3  4.2.1  Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.
doctrine/doctrine-bundle            2.13.0 2.13.0 Symfony DoctrineBundle
doctrine/doctrine-fixtures-bundle   3.6.1  3.6.1  Symfony DoctrineFixturesBundle
doctrine/doctrine-migrations-bundle 3.3.1  3.3.1  Symfony DoctrineMigrationsBundle
doctrine/orm                        2.20.0 3.3.0  Object-Relational-Mapper for PHP

Transitive dependencies not required in composer.json:
doctrine/cache                      2.2.0  2.2.0  PHP Doctrine Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and others.
doctrine/collections                2.2.2  2.2.2  PHP Doctrine Collections library that adds additional functionality on top of PHP arrays.
doctrine/common                     3.4.5  3.4.5  PHP Doctrine Common project is a library that provides additional functionality that other Doctrine projects depend on such as better reflection sup...
doctrine/data-fixtures              1.8.0  2.0.0  Data Fixtures for all Doctrine Object Managers
doctrine/deprecations               1.1.3  1.1.3  A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.
doctrine/event-manager              2.0.1  2.0.1  The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.
doctrine/inflector                  2.0.10 2.0.10 PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.
doctrine/instantiator               2.0.0  2.0.0  A small, lightweight utility to instantiate objects in PHP without invoking their constructors
doctrine/lexer                      3.0.1  3.0.1  PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.
doctrine/migrations                 3.8.2  3.8.2  PHP Doctrine Migrations project offer additional functionality on top of the database abstraction layer (DBAL) for versioning your database schema a...
doctrine/persistence                3.4.0  4.0.0  The Doctrine Persistence project is a set of shared interfaces and functionality that the different Doctrine object mappers share.
doctrine/sql-formatter              1.5.1  1.5.1  a PHP SQL highlighting library
```

</p>
</details>

#### PHP version

```
PHP 8.1.30 (cli) (built: Oct 28 2024 22:10:41) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.1.30, Copyright (c) Zend Technologies
    with Zend OPcache v8.1.30, Copyright (c), by Zend Technologies
    with Xdebug v3.3.2, Copyright (c) 2002-2024, by Derick Rethans
```

## Subject

Deprecation notice:

> Your output walker class Gedmo\Translatable\Query\TreeWalker\TranslationWalker should implement Doctrine\ORM\Query\OutputWalker in order to provide a Doctrine\ORM\Query\Exec\SqlFinalizer. This also means the output walker should not use the query firstResult/maxResult values, which should be read from the query by the SqlFinalizer only. (Query.php:833 called by Query.php:264, https://github.com/doctrine/orm/pull/11188/, package doctrine/orm)


Discussion / hints:
If I'm following the ORM's deprecation correctly, there's two changes needed in this package.

The first one is to conditionally extend from the new `SqlOutputWalker` when it exists, which provides a default implementation of that new `OutputWalker` interface:

```php
namespace Gedmo\Tool\ORM\Walker;

use Doctrine\ORM\Query\SqlOutputWalker;
use Doctrine\ORM\Query\SqlWalker;

if (class_exists(SqlOutputWalker::class)) {
    abstract class CompatSqlOutputWalker extends SqlOutputWalker {}
} else {
    abstract class CompatSqlOutputWalker extends SqlWalker {}
}
```

The second change, which seems more involved, is refactoring the way SELECT statements are provided (this part only affects the translatable extension's walker as the soft-deleteable extension's walker only handles DELETE statements), and doing that in a B/C manner.

Fix patch:
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
index 83d8455f9..fb5a69aa3 100644
--- a/phpstan-baseline.neon
+++ b/phpstan-baseline.neon
@@ -440,6 +440,11 @@ parameters:
 			count: 1
 			path: src/Timestampable/Mapping/Driver/Yaml.php
 
+		-
+			message: "#^Call to an undefined static method Doctrine\\\\ORM\\\\Query\\\\SqlWalker\\:\\:getFinalizer\\(\\)\\.$#"
+			count: 2
+			path: src/Tool/ORM/Walker/CompatSqlOutputWalker.php
+
 		-
 			message: "#^Call to an undefined method Doctrine\\\\Persistence\\\\ObjectManager\\:\\:getUnitOfWork\\(\\)\\.$#"
 			count: 1
diff --git a/src/SoftDeleteable/Query/TreeWalker/SoftDeleteableWalker.php b/src/SoftDeleteable/Query/TreeWalker/SoftDeleteableWalker.php
index 6f8de3ce7..5bbdc6fab 100644
--- a/src/SoftDeleteable/Query/TreeWalker/SoftDeleteableWalker.php
+++ b/src/SoftDeleteable/Query/TreeWalker/SoftDeleteableWalker.php
@@ -18,12 +18,14 @@
 use Doctrine\ORM\Query\AST\SelectStatement;
 use Doctrine\ORM\Query\AST\UpdateStatement;
 use Doctrine\ORM\Query\Exec\AbstractSqlExecutor;
+use Doctrine\ORM\Query\Exec\PreparedExecutorFinalizer;
 use Doctrine\ORM\Query\Exec\SingleTableDeleteUpdateExecutor;
-use Doctrine\ORM\Query\SqlWalker;
+use Doctrine\ORM\Query\Exec\SqlFinalizer;
 use Gedmo\Exception\RuntimeException;
 use Gedmo\Exception\UnexpectedValueException;
 use Gedmo\SoftDeleteable\Query\TreeWalker\Exec\MultiTableDeleteExecutor;
 use Gedmo\SoftDeleteable\SoftDeleteableListener;
+use Gedmo\Tool\ORM\Walker\CompatSqlOutputWalker;
 use Gedmo\Tool\ORM\Walker\SqlWalkerCompat;
 
 /**
@@ -36,7 +38,7 @@
  *
  * @final since gedmo/doctrine-extensions 3.11
  */
-class SoftDeleteableWalker extends SqlWalker
+class SoftDeleteableWalker extends CompatSqlOutputWalker
 {
     use SqlWalkerCompat;
 
@@ -99,21 +101,43 @@ public function __construct($query, $parserResult, array $queryComponents)
      * @param SelectStatement|UpdateStatement|DeleteStatement $statement
      *
      * @throws UnexpectedValueException when an unsupported AST statement is given
+     *
+     * @phpstan-assert DeleteStatement $statement
      */
     protected function doGetExecutorWithCompat($statement): AbstractSqlExecutor
     {
-        switch (true) {
-            case $statement instanceof DeleteStatement:
-                assert(class_exists($statement->deleteClause->abstractSchemaName));
+        if (!$statement instanceof DeleteStatement) {
+            throw new UnexpectedValueException('SoftDeleteable walker should be used only on delete statement');
+        }
 
-                $primaryClass = $this->getEntityManager()->getClassMetadata($statement->deleteClause->abstractSchemaName);
+        return $this->createDeleteStatementExecutor($statement);
+    }
 
-                return $primaryClass->isInheritanceTypeJoined()
-                    ? new MultiTableDeleteExecutor($statement, $this, $this->meta, $this->getConnection()->getDatabasePlatform(), $this->configuration)
-                    : new SingleTableDeleteUpdateExecutor($statement, $this);
-            default:
-                throw new UnexpectedValueException('SoftDeleteable walker should be used only on delete statement');
+    /**
+     * @param DeleteStatement|UpdateStatement|SelectStatement $AST
+     *
+     * @throws UnexpectedValueException when an unsupported AST statement is given
+     *
+     * @phpstan-assert DeleteStatement $AST
+     */
+    protected function doGetFinalizerWithCompat($AST): SqlFinalizer
+    {
+        if (!$AST instanceof DeleteStatement) {
+            throw new UnexpectedValueException('SoftDeleteable walker should be used only on delete statement');
         }
+
+        return new PreparedExecutorFinalizer($this->createDeleteStatementExecutor($AST));
+    }
+
+    protected function createDeleteStatementExecutor(DeleteStatement $AST): AbstractSqlExecutor
+    {
+        assert(class_exists($AST->deleteClause->abstractSchemaName));
+
+        $primaryClass = $this->getEntityManager()->getClassMetadata($AST->deleteClause->abstractSchemaName);
+
+        return $primaryClass->isInheritanceTypeJoined()
+            ? new MultiTableDeleteExecutor($AST, $this, $this->meta, $this->getConnection()->getDatabasePlatform(), $this->configuration)
+            : new SingleTableDeleteUpdateExecutor($AST, $this);
     }
 
     /**
diff --git a/src/Tool/ORM/Walker/CompatSqlOutputWalker.php b/src/Tool/ORM/Walker/CompatSqlOutputWalker.php
new file mode 100644
index 000000000..726dadea2
--- /dev/null
+++ b/src/Tool/ORM/Walker/CompatSqlOutputWalker.php
@@ -0,0 +1,59 @@
+<?php
+
+/*
+ * This file is part of the Doctrine Behavioral Extensions package.
+ * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> 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\Tool\ORM\Walker;
+
+use Doctrine\ORM\Query\SqlOutputWalker;
+use Doctrine\ORM\Query\SqlWalker;
+
+if (class_exists(SqlOutputWalker::class)) {
+    if ((new \ReflectionClass(SqlWalker::class))->getMethod('getExecutor')->hasReturnType()) {
+        /**
+         * Helper trait to address compatibility issues between ORM 2.x and 3.x.
+         *
+         * @internal
+         */
+        abstract class CompatSqlOutputWalker extends SqlOutputWalker
+        {
+            use CompatSqlOutputWalkerForOrm3;
+        }
+    } else {
+        /**
+         * Helper trait to address compatibility issues between ORM 2.x and 3.x.
+         *
+         * @internal
+         */
+        abstract class CompatSqlOutputWalker extends SqlOutputWalker
+        {
+            use CompatSqlOutputWalkerForOrm2;
+        }
+    }
+} else {
+    if ((new \ReflectionClass(SqlWalker::class))->getMethod('getExecutor')->hasReturnType()) {
+        /**
+         * Helper trait to address compatibility issues between ORM 2.x and 3.x.
+         *
+         * @internal
+         */
+        abstract class CompatSqlOutputWalker extends SqlWalker
+        {
+            use CompatSqlOutputWalkerForOrm3;
+        }
+    } else {
+        /**
+         * Helper trait to address compatibility issues between ORM 2.x and 3.x.
+         *
+         * @internal
+         */
+        abstract class CompatSqlOutputWalker extends SqlWalker
+        {
+            use CompatSqlOutputWalkerForOrm2;
+        }
+    }
+}
diff --git a/src/Tool/ORM/Walker/CompatSqlOutputWalkerForOrm2.php b/src/Tool/ORM/Walker/CompatSqlOutputWalkerForOrm2.php
new file mode 100644
index 000000000..9639aac52
--- /dev/null
+++ b/src/Tool/ORM/Walker/CompatSqlOutputWalkerForOrm2.php
@@ -0,0 +1,42 @@
+<?php
+
+/*
+ * This file is part of the Doctrine Behavioral Extensions package.
+ * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> 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\Tool\ORM\Walker;
+
+use Doctrine\ORM\Query\AST\DeleteStatement;
+use Doctrine\ORM\Query\AST\SelectStatement;
+use Doctrine\ORM\Query\AST\UpdateStatement;
+use Doctrine\ORM\Query\Exec\SqlFinalizer;
+use Doctrine\ORM\Query\SqlOutputWalker;
+
+/**
+ * Helper trait to address compatibility issues between ORM 2.x and 3.x.
+ *
+ * @mixin SqlOutputWalker
+ *
+ * @internal
+ */
+trait CompatSqlOutputWalkerForOrm2
+{
+    /**
+     * @param DeleteStatement|UpdateStatement|SelectStatement $AST
+     */
+    public function getFinalizer($AST): SqlFinalizer
+    {
+        return $this->doGetFinalizerWithCompat($AST);
+    }
+
+    /**
+     * @param DeleteStatement|UpdateStatement|SelectStatement $AST
+     */
+    protected function doGetFinalizerWithCompat($AST): SqlFinalizer
+    {
+        return parent::getFinalizer($AST);
+    }
+}
diff --git a/src/Tool/ORM/Walker/CompatSqlOutputWalkerForOrm3.php b/src/Tool/ORM/Walker/CompatSqlOutputWalkerForOrm3.php
new file mode 100644
index 000000000..52427fa90
--- /dev/null
+++ b/src/Tool/ORM/Walker/CompatSqlOutputWalkerForOrm3.php
@@ -0,0 +1,39 @@
+<?php
+
+/*
+ * This file is part of the Doctrine Behavioral Extensions package.
+ * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> 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\Tool\ORM\Walker;
+
+use Doctrine\ORM\Query\AST\DeleteStatement;
+use Doctrine\ORM\Query\AST\SelectStatement;
+use Doctrine\ORM\Query\AST\UpdateStatement;
+use Doctrine\ORM\Query\Exec\SqlFinalizer;
+use Doctrine\ORM\Query\SqlOutputWalker;
+
+/**
+ * Helper trait to address compatibility issues between ORM 2.x and 3.x.
+ *
+ * @mixin SqlOutputWalker
+ *
+ * @internal
+ */
+trait CompatSqlOutputWalkerForOrm3
+{
+    public function getFinalizer(DeleteStatement|UpdateStatement|SelectStatement $AST): SqlFinalizer
+    {
+        return $this->doGetFinalizerWithCompat($AST);
+    }
+
+    /**
+     * @param DeleteStatement|UpdateStatement|SelectStatement $AST
+     */
+    protected function doGetFinalizerWithCompat($AST): SqlFinalizer
+    {
+        return parent::getFinalizer($AST);
+    }
+}
diff --git a/src/Translatable/Query/TreeWalker/TranslationWalker.php b/src/Translatable/Query/TreeWalker/TranslationWalker.php
index 7fad4792b..2e1f50405 100644
--- a/src/Translatable/Query/TreeWalker/TranslationWalker.php
+++ b/src/Translatable/Query/TreeWalker/TranslationWalker.php
@@ -32,8 +32,10 @@
 use Doctrine\ORM\Query\AST\WhereClause;
 use Doctrine\ORM\Query\Exec\AbstractSqlExecutor;
 use Doctrine\ORM\Query\Exec\SingleSelectExecutor;
-use Doctrine\ORM\Query\SqlWalker;
+use Doctrine\ORM\Query\Exec\SingleSelectSqlFinalizer;
+use Doctrine\ORM\Query\Exec\SqlFinalizer;
 use Gedmo\Exception\RuntimeException;
+use Gedmo\Tool\ORM\Walker\CompatSqlOutputWalker;
 use Gedmo\Tool\ORM\Walker\SqlWalkerCompat;
 use Gedmo\Translatable\Hydrator\ORM\ObjectHydrator;
 use Gedmo\Translatable\Hydrator\ORM\SimpleObjectHydrator;
@@ -54,7 +56,7 @@
  *
  * @final since gedmo/doctrine-extensions 3.11
  */
-class TranslationWalker extends SqlWalker
+class TranslationWalker extends CompatSqlOutputWalker
 {
     use SqlWalkerCompat;
 
@@ -141,6 +143,21 @@ protected function doGetExecutorWithCompat($statement): AbstractSqlExecutor
         return new SingleSelectExecutor($statement, $this);
     }
 
+    /**
+     * @param DeleteStatement|UpdateStatement|SelectStatement $AST
+     */
+    protected function doGetFinalizerWithCompat($AST): SqlFinalizer
+    {
+        // If it's not a Select, the TreeWalker ought to skip it, and just return the parent.
+        // @see https://github.com/doctrine-extensions/DoctrineExtensions/issues/2013
+        if (!$AST instanceof SelectStatement) {
+            return parent::getFinalizer($AST);
+        }
+        $this->prepareTranslatedComponents();
+
+        return new SingleSelectSqlFinalizer($this->walkSelectStatement($AST));
+    }
+
     protected function doWalkSelectStatementWithCompat(SelectStatement $selectStatement): string
     {
         $result = parent::walkSelectStatement($selectStatement);


Test patch: