diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bbe5be7..4db2c16 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,11 +24,18 @@ jobs: - '8.3' - '8.4' - '8.5' - dependencies: [highest, lowest] + dependencies: [highest] allowed-to-fail: [false] symfony-require: [''] variant: [normal] include: + # Lowest versions allowed by composer.json, on the lowest supported PHP version (with a newer PHP, they + # trigger PHP deprecations, e.g. implicitly nullable parameters with PHP 8.4, which fail the tests) + - php-version: '8.2' + dependencies: lowest + allowed-to-fail: false + symfony-require: '' + variant: normal - php-version: '8.2' dependencies: lowest allowed-to-fail: false diff --git a/CHANGELOG.md b/CHANGELOG.md index ab7998f..d9e31df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ Latest ------ ### Changes +* [#58](https://github.com/cleverage/doctrine-process-bundle/issues/58) `DoctrineReaderTask` (`order_by` option): pass the `\SortDirection` enum to `QueryBuilder::addOrderBy()` when doctrine/orm accepts it (3.7 or later, where a string direction is deprecated), the string otherwise (same query). Require `psr/log` `^2 || ^3`: process-bundle does not work with psr/log 1. The tests fail on deprecations and boot a kernel with process-bundle and DoctrineBundle. * [#57](https://github.com/cleverage/doctrine-process-bundle/issues/57) Define the public API (Symfony BC promise, as [cleverage/process-bundle#279](https://github.com/cleverage/process-bundle/issues/279)): the concrete tasks (`DatabaseReaderTask`, `DatabaseUpdaterTask`, `DoctrineReaderTask`, `DoctrineWriterTask`, `DoctrineBatchWriterTask`, `DoctrineCleanerTask`, `DoctrineDetacherTask`, `DoctrineRefresherTask`, `DoctrineRemoverTask`, `ClearEntityManagerTask`) and `CleverAgeDoctrineProcessBundle` are marked `@final since 3.3`, they will be `final` in 4.0: extending them triggers a deprecation with the Symfony `DebugClassLoader`. `AbstractDoctrineTask` and `AbstractDoctrineQueryTask` stay open. `CleverAgeDoctrineProcessExtension` is marked `@internal`. * [#56](https://github.com/cleverage/doctrine-process-bundle/issues/56) `CleverAgeDoctrineProcessExtension::findServices()` loads the service definition files with a glob pattern instead of a `Finder` (same files, as [cleverage/process-bundle#271](https://github.com/cleverage/process-bundle/issues/271)), `symfony/finder` is no longer required. Add tests. * [#54](https://github.com/cleverage/doctrine-process-bundle/issues/54) `CleverAgeDoctrineProcessExtension` extends `Symfony\Component\DependencyInjection\Extension\Extension` instead of `Symfony\Component\HttpKernel\DependencyInjection\Extension` (internal since Symfony 7.1, to be deprecated in 8.1), as the other bundles. Same behaviour: the removed `addAnnotatedClassesToCompile()` / `getAnnotatedClassesToCompile()` methods (deprecated since Symfony 7.1, removed in 8.0) were not used. diff --git a/composer.json b/composer.json index 88157b7..ae605d3 100644 --- a/composer.json +++ b/composer.json @@ -56,7 +56,7 @@ "doctrine/doctrine-bundle": "^2.18 || ^3.1", "doctrine/orm": "^2.20 || ^3.5", "doctrine/persistence": "^3.1 || ^4", - "psr/log": "^1.1.4 || ^2 || ^3", + "psr/log": "^2 || ^3", "symfony/config": "^6.4 || ^7.4 || ^8", "symfony/dependency-injection": "^6.4 || ^7.4 || ^8", "symfony/http-kernel": "^6.4 || ^7.4 || ^8", @@ -74,6 +74,7 @@ "phpunit/phpunit": "^10.5 || ^11 || ^12 || ^13", "rector/rector": "*", "roave/security-advisories": "dev-latest", + "symfony/framework-bundle": "^6.4 || ^7.4 || ^8", "symfony/test-pack": "^1.1" }, "suggest": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 6bdb884..9e8b688 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,20 +1,22 @@ + failOnWarning="true" + failOnDeprecation="true" + displayDetailsOnTestsThatTriggerDeprecations="true"> tests - + src diff --git a/rector.php b/rector.php index df0114e..c77c80d 100644 --- a/rector.php +++ b/rector.php @@ -3,6 +3,7 @@ declare(strict_types=1); use Rector\Config\RectorConfig; +use Rector\DeadCode\Rector\If_\RemoveAlwaysTrueIfConditionRector; use Rector\Symfony\Set\SymfonySetList; use Rector\ValueObject\PhpVersion; @@ -20,4 +21,10 @@ ->withSets([ SymfonySetList::SYMFONY_CONSTRUCTOR_INJECTION, ]) + ->withSkip([ + // Symfony version checks of the test kernel, always true with the installed version only + RemoveAlwaysTrueIfConditionRector::class => [ + __DIR__.'/tests/App/TestKernel.php', + ], + ]) ; diff --git a/src/Task/EntityManager/AbstractDoctrineQueryTask.php b/src/Task/EntityManager/AbstractDoctrineQueryTask.php index 06bd713..e433c35 100644 --- a/src/Task/EntityManager/AbstractDoctrineQueryTask.php +++ b/src/Task/EntityManager/AbstractDoctrineQueryTask.php @@ -98,7 +98,7 @@ protected function getQueryBuilder( } } foreach ($orderBy as $field => $order) { - $qb->addOrderBy("e.{$field}", $order); + $qb->addOrderBy("e.{$field}", $this->getSortDirection($order)); } if (null !== $limit) { $qb->setMaxResults($limit); @@ -109,4 +109,24 @@ protected function getQueryBuilder( return $qb; } + + /** + * doctrine/orm 3.7 deprecates the string (and null) sort directions in favor of the \SortDirection enum (PHP 8.6, + * polyfilled), which the previous versions do not accept (the enum may exist with them, e.g. on PHP 8.6). + */ + private function getSortDirection(?string $order): \SortDirection|string|null + { + static $acceptsSortDirection = null; + $acceptsSortDirection ??= str_contains((string) (new \ReflectionMethod(QueryBuilder::class, 'addOrderBy'))->getParameters()[1]->getType(), 'SortDirection'); + if (!$acceptsSortDirection) { + return $order; + } + + return match (strtolower($order ?? 'asc')) { + 'asc' => \SortDirection::Ascending, + 'desc' => \SortDirection::Descending, + // Invalid direction: rejected by doctrine/orm, as before + default => $order, + }; + } } diff --git a/tests/App/TestKernel.php b/tests/App/TestKernel.php new file mode 100644 index 0000000..eeccd25 --- /dev/null +++ b/tests/App/TestKernel.php @@ -0,0 +1,122 @@ + task class, one task of the "test.doctrine" process per code. + */ + public const TASKS = [ + 'doctrine_database_reader' => DatabaseReaderTask::class, + 'doctrine_database_updater' => DatabaseUpdaterTask::class, + 'doctrine_clear_entity_manager' => ClearEntityManagerTask::class, + 'doctrine_batch_writer' => DoctrineBatchWriterTask::class, + 'doctrine_cleaner' => DoctrineCleanerTask::class, + 'doctrine_detacher' => DoctrineDetacherTask::class, + 'doctrine_reader' => DoctrineReaderTask::class, + 'doctrine_refresher' => DoctrineRefresherTask::class, + 'doctrine_remover' => DoctrineRemoverTask::class, + 'doctrine_writer' => DoctrineWriterTask::class, + ]; + + public function registerBundles(): iterable + { + yield new FrameworkBundle(); + yield new DoctrineBundle(); + yield new CleverAgeProcessBundle(); + yield new CleverAgeDoctrineProcessBundle(); + } + + public function getProjectDir(): string + { + return __DIR__; + } + + public function getCacheDir(): string + { + return $this->getRunDir().'/cache/'.$this->environment; + } + + public function getLogDir(): string + { + return $this->getRunDir().'/logs'; + } + + protected function configureContainer(ContainerConfigurator $container): void + { + $container->extension('framework', [ + 'secret' => 'test', + 'test' => true, + 'http_method_override' => false, + 'handle_all_throwables' => true, + 'php_errors' => ['log' => true], + 'validation' => ['email_validation_mode' => 'html5'], + ]); + if (Kernel::VERSION_ID >= 70300) { + // Not setting it is deprecated since Symfony 7.3: its default value changes in 8.0 + $container->extension('framework', ['property_info' => ['with_constructor_extractor' => true]]); + } + $container->extension('doctrine', [ + 'dbal' => ['url' => 'sqlite:///:memory:'], + ]); + + $services = $container->services(); + // Tasks referenced by code (task: ): process-bundle 5.4 or later + if (class_exists(TaskRegistry::class)) { + $tasks = []; + foreach (array_keys(self::TASKS) as $code) { + $tasks[$code] = ['task' => $code]; + } + $container->extension('clever_age_process', [ + 'configurations' => [ + 'test.doctrine' => ['tasks' => $tasks], + ], + ]); + $services->alias('test.task_registry', TaskRegistry::class)->public(); + $services->alias('test.process_configuration_registry', ProcessConfigurationRegistry::class)->public(); + } + } + + private function getRunDir(): string + { + return sys_get_temp_dir().'/doctrine_process_bundle_tests_'.getmypid(); + } +} diff --git a/tests/Functional/KernelTest.php b/tests/Functional/KernelTest.php new file mode 100644 index 0000000..2ca14f6 --- /dev/null +++ b/tests/Functional/KernelTest.php @@ -0,0 +1,79 @@ +exceptionHandler = $this->getExceptionHandler(); + } + + protected function tearDown(): void + { + parent::tearDown(); + // The ErrorHandler registered when the kernel boots is not removed by the first Symfony 6.4 versions + if ($this->getExceptionHandler() !== $this->exceptionHandler) { + restore_exception_handler(); + } + } + + public function testDoctrineIsResolved(): void + { + self::assertInstanceOf(ManagerRegistry::class, self::getContainer()->get('doctrine')); + } + + public function testTasksAreResolvedByCode(): void + { + if (!class_exists(TaskRegistry::class)) { + self::markTestSkipped('The tasks are referenced by code since cleverage/process-bundle 5.4.'); + } + $processConfigurationRegistry = self::getContainer()->get('test.process_configuration_registry'); + self::assertInstanceOf(ProcessConfigurationRegistry::class, $processConfigurationRegistry); + $taskRegistry = self::getContainer()->get('test.task_registry'); + self::assertInstanceOf(TaskRegistry::class, $taskRegistry); + + $processConfiguration = $processConfigurationRegistry->getProcessConfiguration('test.doctrine'); + foreach (TestKernel::TASKS as $code => $class) { + self::assertInstanceOf($class, $taskRegistry->getTask($processConfiguration->getTaskConfiguration($code)), $code); + } + } + + private function getExceptionHandler(): mixed + { + $handler = set_exception_handler(null); + restore_exception_handler(); + + return $handler; + } + + protected static function getKernelClass(): string + { + return TestKernel::class; + } +} diff --git a/tests/Task/EntityManager/AbstractDoctrineQueryTaskTest.php b/tests/Task/EntityManager/AbstractDoctrineQueryTaskTest.php index ef7c26a..5a7075f 100644 --- a/tests/Task/EntityManager/AbstractDoctrineQueryTaskTest.php +++ b/tests/Task/EntityManager/AbstractDoctrineQueryTaskTest.php @@ -37,6 +37,13 @@ public function testGetQueryBuilderWithInvalidField(): void $method->invoke($task, $repository, ['e.field; DROP TABLE dummy;' => 'value'], []); } + public function testGetQueryBuilderWithDescendingOrder(): void + { + $qb = $this->getQueryBuilder([], ['title' => 'DESC']); + + self::assertSame('SELECT e FROM App\\Entity\\Book e ORDER BY e.title DESC', $qb->getDQL()); + } + public function testGetQueryBuilder(): void { $qb = $this->getQueryBuilder( diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..9c71347 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,19 @@ +