diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml new file mode 100644 index 00000000..e69de29b diff --git a/README.md b/README.md index 0dbc2c40..ecdf3272 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ - Background worker - [Symfony/Messenger](https://symfony.com/doc/current/messenger.html) integration - [Mercure](https://www.mercure.rocks) integration +- [API-Platform](https://api-platform.com/) support ## Installation @@ -98,6 +99,7 @@ When a task is configured, time to execute it, two approaches can be used: * [Usage](doc/usage.md) * [Configuration](doc/configuration.md) * [Best practices](doc/best_practices.md) +* [API-Platform](doc/api_platform.md) * [Tasks](doc/tasks.md) * [Transports](doc/transport.md) * [Lock](doc/lock.md) diff --git a/composer.json b/composer.json index 52d8752c..af693b94 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,8 @@ "psr-4": { "SchedulerBundle\\": "src/", "SchedulerBundle\\Bridge\\": "src/Bridge/", + "SchedulerBundle\\Bridge\\ApiPlatform\\": "src/Bridge/ApiPlatform/", + "SchedulerBundle\\Bridge\\ApiPlatform\\Filter\\": "src/Bridge/ApiPlatform/Filter/", "SchedulerBundle\\Bridge\\Doctrine\\": "src/Bridge/Doctrine/", "SchedulerBundle\\Bridge\\Doctrine\\SchemaListener\\": "src/Bridge/Doctrine/SchemaListener/", "SchedulerBundle\\Bridge\\Doctrine\\Transport\\": "src/Bridge/Doctrine/Transport/", @@ -56,6 +58,8 @@ "psr-4": { "Tests\\SchedulerBundle\\": "tests/", "Tests\\SchedulerBundle\\Bridge\\": "tests/Bridge/", + "Tests\\SchedulerBundle\\Bridge\\ApiPlatform\\": "tests/Bridge/ApiPlatform/", + "Tests\\SchedulerBundle\\Bridge\\ApiPlatform\\Filter\\": "tests/Bridge/ApiPlatform/Filter/", "Tests\\SchedulerBundle\\Bridge\\Doctrine\\": "tests/Bridge/Doctrine/", "Tests\\SchedulerBundle\\Bridge\\Doctrine\\SchemaListener\\": "tests/Bridge/Doctrine/SchemaListener/", "Tests\\SchedulerBundle\\Bridge\\Doctrine\\Transport\\": "tests/Bridge/Doctrine/Transport/", @@ -118,6 +122,7 @@ "ext-pcntl": "*", "ext-pdo": "*", "ext-redis": "*", + "api-platform/core": "^2.6", "doctrine/dbal": "^3.1.4", "doctrine/orm": "^2.8", "friendsofphp/php-cs-fixer": "^3.5", diff --git a/doc/api_platform.md b/doc/api_platform.md new file mode 100644 index 00000000..7696739c --- /dev/null +++ b/doc/api_platform.md @@ -0,0 +1,3 @@ +# API-Platform + +This bundle provides a bridge with API-Platform, diff --git a/src/Bridge/ApiPlatform/Filter/SearchFilter.php b/src/Bridge/ApiPlatform/Filter/SearchFilter.php new file mode 100644 index 00000000..93d847e4 --- /dev/null +++ b/src/Bridge/ApiPlatform/Filter/SearchFilter.php @@ -0,0 +1,115 @@ + + */ +final class SearchFilter implements FilterInterface +{ + /** + * {@inheritdoc} + */ + public function getDescription(string $resourceClass): array + { + if (TaskInterface::class !== $resourceClass) { + return []; + } + + return [ + 'expression' => [ + 'type' => 'string', + 'required' => false, + 'property' => 'expression', + 'swagger' => [ + 'description' => 'Filter tasks using the expression', + 'name' => 'expression', + 'type' => 'string', + ], + ], + 'queued' => [ + 'type' => 'bool', + 'required' => false, + 'property' => 'queued', + 'swagger' => [ + 'description' => 'Filter tasks that are queued', + 'name' => 'queued', + 'type' => 'bool', + ], + ], + 'state' => [ + 'type' => 'string', + 'required' => false, + 'property' => 'state', + 'swagger' => [ + 'description' => 'Filter tasks with a specific state', + 'name' => 'state', + 'type' => 'string', + ], + ], + 'timezone' => [ + 'type' => 'string', + 'required' => false, + 'property' => 'timezone', + 'swagger' => [ + 'description' => 'Filter tasks scheduled using a specific timezone', + 'name' => 'timezone', + 'type' => 'string', + ], + ], + 'type' => [ + 'type' => 'string', + 'required' => false, + 'property' => 'type', + 'swagger' => [ + 'description' => 'Filter tasks depending on internal type', + 'name' => 'timezone', + 'type' => 'string', + ], + ], + ]; + } + + public function filter(TaskListInterface $list, array $filters = []): TaskListInterface + { + if ([] === $filters) { + return $list; + } + + if (0 === $list->count()) { + return $list; + } + + foreach ($filters as $filter => $value) { + switch ($filter) { + case 'expression': + $list = $list->filter(static fn (TaskInterface $task): bool => $value === $task->getExpression()); + break; + case 'queued': + $list = $list->filter(static fn (TaskInterface $task): bool => $task->isQueued()); + break; + case 'state': + $list = $list->filter(static fn (TaskInterface $task): bool => $value === $task->getState()); + break; + case 'timezone': + $list = $list->filter(static function (TaskInterface $task) use ($value): bool { + $timezone = $task->getTimezone(); + + return null !== $timezone && $value === $timezone->getName(); + }); + break; + case 'type': + $list = $list->filter(static fn (TaskInterface $task): bool => $value === $task::class); + break; + } + } + + return $list; + } +} diff --git a/src/Bridge/ApiPlatform/Model/Task.php b/src/Bridge/ApiPlatform/Model/Task.php new file mode 100644 index 00000000..605c8176 --- /dev/null +++ b/src/Bridge/ApiPlatform/Model/Task.php @@ -0,0 +1,31 @@ +getName())); + } + + public function getWrappedTask(): TaskInterface + { + return $this->wrappedTask; + } +} diff --git a/src/Bridge/ApiPlatform/TaskDataProvider.php b/src/Bridge/ApiPlatform/TaskDataProvider.php new file mode 100644 index 00000000..04397a11 --- /dev/null +++ b/src/Bridge/ApiPlatform/TaskDataProvider.php @@ -0,0 +1,57 @@ + + */ +final class TaskDataProvider implements ItemDataProviderInterface, RestrictedDataProviderInterface +{ + private TransportInterface $transport; + private LoggerInterface $logger; + + public function __construct( + TransportInterface $transport, + ?LoggerInterface $logger = null + ) { + $this->transport = $transport; + $this->logger = $logger ?: new NullLogger(); + } + + /** + * {@inheritdoc} + */ + public function supports(string $resourceClass, string $operationName = null, array $context = []): bool + { + return $resourceClass === TaskInterface::class; + } + + /** + * {@inheritdoc} + */ + public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []): TaskInterface + { + try { + $task = $this->transport->get($id); + } catch (Throwable $throwable) { + $this->logger->critical(sprintf('The task "%s" cannot be found', $id), [ + 'error' => $throwable->getMessage(), + ]); + + throw $throwable; + } + + return $task; + } +} diff --git a/src/Bridge/ApiPlatform/TaskListDataProvider.php b/src/Bridge/ApiPlatform/TaskListDataProvider.php new file mode 100644 index 00000000..be241fe8 --- /dev/null +++ b/src/Bridge/ApiPlatform/TaskListDataProvider.php @@ -0,0 +1,66 @@ + + */ +final class TaskListDataProvider implements ContextAwareCollectionDataProviderInterface, RestrictedDataProviderInterface +{ + private SearchFilter $searchFilter; + private TransportInterface $transport; + private LoggerInterface $logger; + + public function __construct( + SearchFilter $searchFilter, + TransportInterface $transport, + ?LoggerInterface $logger = null + ) { + $this->searchFilter = $searchFilter; + $this->transport = $transport; + $this->logger = $logger ?: new NullLogger(); + } + + /** + * {@inheritdoc} + */ + public function supports(string $resourceClass, string $operationName = null, array $context = []): bool + { + return $resourceClass === TaskInterface::class; + } + + /** + * {@inheritdoc} + */ + public function getCollection(string $resourceClass, string $operationName = null, array $context = []): TaskListInterface + { + try { + $list = $this->transport->list(); + } catch (Throwable $throwable) { + $this->logger->critical('The list cannot be retrieved', [ + 'error' => $throwable->getMessage(), + ]); + + throw $throwable; + } + + if (array_key_exists('filters', $context) && [] !== $context['filters']) { + return $this->searchFilter->filter($list, $context['filters']); + } + + return $list; + } +} diff --git a/src/DependencyInjection/SchedulerBundleConfiguration.php b/src/DependencyInjection/SchedulerBundleConfiguration.php index 11eed084..5c32bf54 100644 --- a/src/DependencyInjection/SchedulerBundleConfiguration.php +++ b/src/DependencyInjection/SchedulerBundleConfiguration.php @@ -225,6 +225,10 @@ public function getConfigTreeBuilder(): TreeBuilder ->end() ->end() ->end() + ->scalarNode('api_platform') + ->info('Enable the API-Platform support') + ->defaultValue(false) + ->end() ->end() ->end() ; diff --git a/src/DependencyInjection/SchedulerBundleExtension.php b/src/DependencyInjection/SchedulerBundleExtension.php index 64ad914c..eca89278 100644 --- a/src/DependencyInjection/SchedulerBundleExtension.php +++ b/src/DependencyInjection/SchedulerBundleExtension.php @@ -7,6 +7,9 @@ use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Log\LoggerInterface; use Redis; +use SchedulerBundle\Bridge\ApiPlatform\TaskListDataProvider; +use SchedulerBundle\Bridge\ApiPlatform\Filter\SearchFilter; +use SchedulerBundle\Bridge\ApiPlatform\TaskDataProvider; use SchedulerBundle\Bridge\Doctrine\SchemaListener\SchedulerTransportDoctrineSchemaSubscriber; use SchedulerBundle\Bridge\Doctrine\Transport\DoctrineTransportFactory; use SchedulerBundle\Bridge\Redis\Transport\RedisTransportFactory; @@ -192,6 +195,7 @@ public function load(array $configs, ContainerBuilder $container): void $this->registerMiddlewareStacks($container, $config); $this->registerProbeContext($container, $config); $this->registerMercureSupport($container, $config); + $this->registerApiPlatformBridge($container, $config); $this->registerDataCollector($container); } @@ -1330,6 +1334,52 @@ private function registerMercureSupport(ContainerBuilder $container, array $conf ; } + private function registerApiPlatformBridge(ContainerBuilder $container, array $configuration): void + { + if (false === $configuration['api_platform']) { + return; + } + + $container->register(TaskDataProvider::class, TaskDataProvider::class) + ->setArguments([ + new Reference(TransportInterface::class, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE), + new Reference(LoggerInterface::class, ContainerInterface::NULL_ON_INVALID_REFERENCE), + ]) + ->setPublic(false) + ->addTag('api_platform.item_data_provider') + ->addTag('monolog.logger', [ + 'channel' => 'scheduler', + ]) + ->addTag('container.preload', [ + 'class' => TaskDataProvider::class, + ]) + ; + + $container->register(TaskListDataProvider::class, TaskListDataProvider::class) + ->setArguments([ + new Reference(SearchFilter::class, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE), + new Reference(TransportInterface::class, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE), + new Reference(LoggerInterface::class, ContainerInterface::NULL_ON_INVALID_REFERENCE), + ]) + ->setPublic(false) + ->addTag('api_platform.collection_data_provider') + ->addTag('monolog.logger', [ + 'channel' => 'scheduler', + ]) + ->addTag('container.preload', [ + 'class' => TaskListDataProvider::class, + ]) + ; + + $container->register(SearchFilter::class, SearchFilter::class) + ->setPublic(false) + ->addTag('api_platform.filter') + ->addTag('container.preload', [ + 'class' => SearchFilter::class, + ]) + ; + } + private function registerDataCollector(ContainerBuilder $container): void { $container->register(SchedulerDataCollector::class, SchedulerDataCollector::class) diff --git a/tests/Bridge/ApiPlatform/Filter/SearchFilterTest.php b/tests/Bridge/ApiPlatform/Filter/SearchFilterTest.php new file mode 100644 index 00000000..68a2457b --- /dev/null +++ b/tests/Bridge/ApiPlatform/Filter/SearchFilterTest.php @@ -0,0 +1,170 @@ + + */ +final class SearchFilterTest extends TestCase +{ + public function testFilterCanDefineDescription(): void + { + $filter = new SearchFilter(); + + self::assertEmpty($filter->getDescription(stdClass::class)); + self::assertNotEmpty($filter->getDescription(TaskInterface::class)); + } + + public function testFilterDescriptionIsDefined(): void + { + $filter = new SearchFilter(); + + $description = $filter->getDescription(TaskInterface::class); + + self::assertCount(5, $description); + self::assertArrayHasKey('expression', $description); + self::assertArrayHasKey('queued', $description); + self::assertArrayHasKey('state', $description); + self::assertArrayHasKey('timezone', $description); + self::assertArrayHasKey('type', $description); + self::assertCount(4, $description['expression']); + self::assertCount(4, $description['queued']); + self::assertCount(4, $description['state']); + self::assertCount(4, $description['timezone']); + self::assertCount(4, $description['type']); + self::assertSame('string', $description['expression']['type']); + self::assertFalse($description['expression']['required']); + self::assertSame('expression', $description['expression']['property']); + self::assertSame([ + 'description' => 'Filter tasks using the expression', + 'name' => 'expression', + 'type' => 'string', + ], $description['expression']['swagger']); + self::assertSame('bool', $description['queued']['type']); + self::assertFalse($description['queued']['required']); + self::assertSame('queued', $description['queued']['property']); + self::assertSame([ + 'description' => 'Filter tasks that are queued', + 'name' => 'queued', + 'type' => 'bool', + ], $description['queued']['swagger']); + self::assertSame('string', $description['state']['type']); + self::assertFalse($description['state']['required']); + self::assertSame('state', $description['state']['property']); + self::assertSame([ + 'description' => 'Filter tasks with a specific state', + 'name' => 'state', + 'type' => 'string', + ], $description['state']['swagger']); + self::assertSame('string', $description['timezone']['type']); + self::assertFalse($description['timezone']['required']); + self::assertSame('timezone', $description['timezone']['property']); + self::assertSame([ + 'description' => 'Filter tasks scheduled using a specific timezone', + 'name' => 'timezone', + 'type' => 'string', + ], $description['timezone']['swagger']); + self::assertSame('string', $description['type']['type']); + self::assertFalse($description['type']['required']); + self::assertSame('type', $description['type']['property']); + self::assertSame([ + 'description' => 'Filter tasks depending on internal type', + 'name' => 'timezone', + 'type' => 'string', + ], $description['type']['swagger']); + } + + public function testFilterCannotFilterWithoutFilters(): void + { + $list = $this->createMock(TaskListInterface::class); + $list->expects(self::never())->method('count'); + $list->expects(self::never())->method('filter'); + + $filter = new SearchFilter(); + $filter->filter($list); + } + + public function testFilterCannotFilterEmptyList(): void + { + $list = $this->createMock(TaskListInterface::class); + $list->expects(self::once())->method('count')->willReturn(0); + $list->expects(self::never())->method('filter'); + + $filter = new SearchFilter(); + $filter->filter($list, [ + 'expression' => '* * * * *', + ]); + } + + public function testFilterCanFilterOnExpression(): void + { + $filter = new SearchFilter(); + $list = $filter->filter(new TaskList([new NullTask('foo')]), [ + 'expression' => '* * * * *', + ]); + + self::assertNotEmpty($list); + self::assertCount(1, $list); + } + + public function testFilterCanFilterOnQueuedTask(): void + { + $filter = new SearchFilter(); + $list = $filter->filter(new TaskList([new NullTask('foo', [ + 'queued' => true, + ])]), [ + 'queued' => true, + ]); + + self::assertNotEmpty($list); + self::assertCount(1, $list); + } + + public function testFilterCanFilterOnTaskState(): void + { + $filter = new SearchFilter(); + $list = $filter->filter(new TaskList([new NullTask('foo', [ + 'execution_state' => TaskInterface::SUCCEED, + ])]), [ + 'execution_state' => TaskInterface::SUCCEED, + ]); + + self::assertNotEmpty($list); + self::assertCount(1, $list); + } + + public function testFilterCanFilterOnTaskTimezone(): void + { + $filter = new SearchFilter(); + $list = $filter->filter(new TaskList([new NullTask('foo', [ + 'timezone' => new DateTimeZone('UTC'), + ])]), [ + 'timezone' => 'UTC', + ]); + + self::assertNotEmpty($list); + self::assertCount(1, $list); + } + + public function testFilterCanFilterOnTaskType(): void + { + $filter = new SearchFilter(); + $list = $filter->filter(new TaskList([new NullTask('foo')]), [ + 'type' => NullTask::class, + ]); + + self::assertNotEmpty($list); + self::assertCount(1, $list); + } +} diff --git a/tests/Bridge/ApiPlatform/TaskDataProviderTest.php b/tests/Bridge/ApiPlatform/TaskDataProviderTest.php new file mode 100644 index 00000000..4fd27041 --- /dev/null +++ b/tests/Bridge/ApiPlatform/TaskDataProviderTest.php @@ -0,0 +1,63 @@ + + */ +final class TaskDataProviderTest extends TestCase +{ + public function testProviderSupport(): void + { + $provider = new TaskDataProvider(new InMemoryTransport([], new SchedulePolicyOrchestrator([]))); + + self::assertInstanceOf(RestrictedDataProviderInterface::class, $provider); + self::assertFalse($provider->supports(stdClass::class)); + self::assertTrue($provider->supports(TaskInterface::class)); + } + + public function testProviderCannotReturnUndefinedTask(): void + { + $logger = $this->createMock(LoggerInterface::class); + $logger->expects(self::once())->method('critical') + ->with(self::equalTo('The task "foo" cannot be found'), self::equalTo([ + 'error' => 'The task "foo" does not exist or is invalid', + ])) + ; + + $provider = new TaskDataProvider(new InMemoryTransport([], new SchedulePolicyOrchestrator([])), $logger); + + self::expectException(InvalidArgumentException::class); + self::expectExceptionMessage('The task "foo" does not exist or is invalid'); + self::expectExceptionCode(0); + $provider->getItem(TaskInterface::class, 'foo'); + } + + public function testProviderCanReturnTask(): void + { + $task = new NullTask('foo'); + + $transport = new InMemoryTransport([], new SchedulePolicyOrchestrator([])); + $transport->create($task); + + $logger = $this->createMock(LoggerInterface::class); + $logger->expects(self::never())->method('critical'); + + $provider = new TaskDataProvider($transport, $logger); + + self::assertSame($task, $provider->getItem(TaskInterface::class, 'foo')); + } +} diff --git a/tests/Bridge/ApiPlatform/TaskListDataProviderTest.php b/tests/Bridge/ApiPlatform/TaskListDataProviderTest.php new file mode 100644 index 00000000..b02dd604 --- /dev/null +++ b/tests/Bridge/ApiPlatform/TaskListDataProviderTest.php @@ -0,0 +1,100 @@ + + */ +final class TaskListDataProviderTest extends TestCase +{ + public function testProviderSupport(): void + { + $provider = new TaskListDataProvider(new SearchFilter(), new InMemoryTransport([], new SchedulePolicyOrchestrator([ + new FirstInFirstOutPolicy(), + ]))); + + self::assertInstanceOf(RestrictedDataProviderInterface::class, $provider); + self::assertFalse($provider->supports(stdClass::class)); + self::assertTrue($provider->supports(TaskInterface::class)); + } + + public function testProviderCannotReturnListWithError(): void + { + $transport = $this->createMock(TransportInterface::class); + $transport->expects(self::once())->method('list') + ->willThrowException(new RuntimeException('Random error')) + ; + + $logger = $this->createMock(LoggerInterface::class); + $logger->expects(self::once())->method('critical') + ->with( + self::equalTo('The list cannot be retrieved'), + self::equalTo([ + 'error' => 'Random error', + ]) + ) + ; + + $provider = new TaskListDataProvider(new SearchFilter(), $transport, $logger); + + self::expectException(RuntimeException::class); + self::expectExceptionMessage('Random error'); + self::expectExceptionCode(0); + $provider->getCollection(TaskInterface::class); + } + + public function testProviderCanReturnTaskList(): void + { + $logger = $this->createMock(LoggerInterface::class); + $logger->expects(self::never())->method('critical'); + + $provider = new TaskListDataProvider(new SearchFilter(), new InMemoryTransport([], new SchedulePolicyOrchestrator([ + new FirstInFirstOutPolicy(), + ])), $logger); + + self::assertCount(0, $provider->getCollection(TaskInterface::class)); + } + + public function testProviderCannotReturnFilteredTaskListWithoutFilters(): void + { + $logger = $this->createMock(LoggerInterface::class); + $logger->expects(self::never())->method('critical'); + + $provider = new TaskListDataProvider(new SearchFilter(), new InMemoryTransport([], new SchedulePolicyOrchestrator([ + new FirstInFirstOutPolicy(), + ])), $logger); + $provider->getCollection(TaskInterface::class, 'GET', [ + 'filters' => [], + ]); + } + + public function testProviderCanReturnFilteredTaskList(): void + { + $logger = $this->createMock(LoggerInterface::class); + $logger->expects(self::never())->method('critical'); + + $provider = new TaskListDataProvider(new SearchFilter(), new InMemoryTransport([], new SchedulePolicyOrchestrator([ + new FirstInFirstOutPolicy(), + ])), $logger); + $provider->getCollection(TaskInterface::class, 'GET', [ + 'filters' => [ + 'expression' => '* * * * *', + ], + ]); + } +} diff --git a/tests/DependencyInjection/SchedulerBundleExtensionTest.php b/tests/DependencyInjection/SchedulerBundleExtensionTest.php index dec89549..82efcd22 100644 --- a/tests/DependencyInjection/SchedulerBundleExtensionTest.php +++ b/tests/DependencyInjection/SchedulerBundleExtensionTest.php @@ -9,6 +9,9 @@ use PHPUnit\Framework\TestCase; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Log\LoggerInterface; +use SchedulerBundle\Bridge\ApiPlatform\TaskListDataProvider; +use SchedulerBundle\Bridge\ApiPlatform\Filter\SearchFilter; +use SchedulerBundle\Bridge\ApiPlatform\TaskDataProvider; use SchedulerBundle\Bridge\Doctrine\SchemaListener\SchedulerTransportDoctrineSchemaSubscriber; use SchedulerBundle\Bridge\Doctrine\Transport\DoctrineTransportFactory; use SchedulerBundle\Bridge\Redis\Transport\RedisTransportFactory; @@ -1631,6 +1634,45 @@ public function testProbeTasksCanBeConfigured(): void self::assertInstanceOf(Definition::class, $container->getDefinition(Scheduler::class)->getMethodCalls()[0][1][0]); } + public function testApiPlatformBridgeCanBeConfigured(): void + { + $container = $this->getContainer([ + 'path' => '/_foo', + 'timezone' => 'Europe/Paris', + 'transport' => [ + 'dsn' => 'memory://first_in_first_out', + ], + 'tasks' => [], + 'lock_store' => null, + 'api_platform' => true, + ]); + + self::assertTrue($container->hasDefinition(TaskDataProvider::class)); + self::assertFalse($container->getDefinition(TaskDataProvider::class)->isPublic()); + self::assertCount(2, $container->getDefinition(TaskDataProvider::class)->getArguments()); + self::assertInstanceOf(Reference::class, $container->getDefinition(TaskDataProvider::class)->getArgument(0)); + self::assertInstanceOf(Reference::class, $container->getDefinition(TaskDataProvider::class)->getArgument(1)); + self::assertTrue($container->getDefinition(TaskDataProvider::class)->hasTag('api_platform.item_data_provider')); + self::assertTrue($container->getDefinition(TaskDataProvider::class)->hasTag('container.preload')); + self::assertSame(TaskDataProvider::class, $container->getDefinition(TaskDataProvider::class)->getTag('container.preload')[0]['class']); + + self::assertTrue($container->hasDefinition(TaskListDataProvider::class)); + self::assertFalse($container->getDefinition(TaskListDataProvider::class)->isPublic()); + self::assertCount(3, $container->getDefinition(TaskListDataProvider::class)->getArguments()); + self::assertInstanceOf(Reference::class, $container->getDefinition(TaskListDataProvider::class)->getArgument(0)); + self::assertInstanceOf(Reference::class, $container->getDefinition(TaskListDataProvider::class)->getArgument(1)); + self::assertInstanceOf(Reference::class, $container->getDefinition(TaskListDataProvider::class)->getArgument(2)); + self::assertTrue($container->getDefinition(TaskListDataProvider::class)->hasTag('api_platform.collection_data_provider')); + self::assertTrue($container->getDefinition(TaskListDataProvider::class)->hasTag('container.preload')); + self::assertSame(TaskListDataProvider::class, $container->getDefinition(TaskListDataProvider::class)->getTag('container.preload')[0]['class']); + + self::assertTrue($container->hasDefinition(SearchFilter::class)); + self::assertFalse($container->getDefinition(SearchFilter::class)->isPublic()); + self::assertTrue($container->getDefinition(SearchFilter::class)->hasTag('api_platform.filter')); + self::assertTrue($container->getDefinition(SearchFilter::class)->hasTag('container.preload')); + self::assertSame(SearchFilter::class, $container->getDefinition(SearchFilter::class)->getTag('container.preload')[0]['class']); + } + public function testDataCollectorIsConfigured(): void { $container = $this->getContainer([ diff --git a/tests/SchedulerTest.php b/tests/SchedulerTest.php index 484bcd37..bf6aa06e 100644 --- a/tests/SchedulerTest.php +++ b/tests/SchedulerTest.php @@ -85,18 +85,14 @@ final class SchedulerTest extends TestCase */ public function testSchedulerCanScheduleTasks(): void { - $task = $this->createMock(TaskInterface::class); - $task->expects(self::once())->method('setScheduledAt'); - $task->expects(self::once())->method('setTimezone'); - $task->expects(self::never())->method('isQueued'); - $scheduler = new Scheduler('UTC', new InMemoryTransport([ 'execution_mode' => 'first_in_first_out', ], new SchedulePolicyOrchestrator([ new FirstInFirstOutPolicy(), ])), new SchedulerMiddlewareStack(), new EventDispatcher()); - $scheduler->schedule($task); + $scheduler->schedule(new NullTask('foo')); + self::assertCount(1, $scheduler->getTasks()); } /** @@ -105,11 +101,9 @@ public function testSchedulerCanScheduleTasks(): void */ public function testSchedulerCanScheduleTasksWithCustomTimezone(): void { - $task = $this->createMock(TaskInterface::class); - $task->expects(self::once())->method('setScheduledAt'); - $task->expects(self::once())->method('setTimezone')->with(new DateTimeZone('Europe/Paris')); - $task->expects(self::once())->method('getTimezone')->willReturn(new DateTimeZone('Europe/Paris')); - $task->expects(self::never())->method('isQueued'); + $task = new NullTask('foo', [ + 'timezone' => new DateTimeZone('Europe/Paris'), + ]); $scheduler = new Scheduler('UTC', new InMemoryTransport([ 'execution_mode' => 'first_in_first_out', @@ -118,6 +112,10 @@ public function testSchedulerCanScheduleTasksWithCustomTimezone(): void ])), new SchedulerMiddlewareStack(), new EventDispatcher()); $scheduler->schedule($task); + self::assertCount(1, $scheduler->getTasks()); + + $task = $scheduler->getTasks()->last(); + self::assertSame('Europe/Paris', $task->getTimezone()->getName()); } /** @@ -126,12 +124,6 @@ public function testSchedulerCanScheduleTasksWithCustomTimezone(): void */ public function testSchedulerCannotScheduleTasksWithErroredBeforeCallback(): void { - $task = $this->createMock(TaskInterface::class); - $task->expects(self::never())->method('setScheduledAt'); - $task->expects(self::never())->method('setTimezone'); - $task->expects(self::never())->method('isQueued'); - $task->expects(self::once())->method('getBeforeScheduling')->willReturn(fn (): bool => false); - $scheduler = new Scheduler('UTC', new InMemoryTransport([ 'execution_mode' => 'first_in_first_out', ], new SchedulePolicyOrchestrator([ @@ -143,7 +135,9 @@ public function testSchedulerCannotScheduleTasksWithErroredBeforeCallback(): voi self::expectException(RuntimeException::class); self::expectExceptionMessage('The task cannot be scheduled'); self::expectExceptionCode(0); - $scheduler->schedule($task); + $scheduler->schedule(new NullTask('foo', [ + 'before_scheduling' => static fn (): bool => false, + ])); } /** @@ -152,12 +146,6 @@ public function testSchedulerCannotScheduleTasksWithErroredBeforeCallback(): voi */ public function testSchedulerCanScheduleTasksWithBeforeCallback(): void { - $task = $this->createMock(TaskInterface::class); - $task->expects(self::once())->method('setScheduledAt'); - $task->expects(self::once())->method('setTimezone'); - $task->expects(self::never())->method('isQueued'); - $task->expects(self::once())->method('getBeforeScheduling')->willReturn(fn (): int => 1 + 1); - $scheduler = new Scheduler('UTC', new InMemoryTransport([ 'execution_mode' => 'first_in_first_out', ], new SchedulePolicyOrchestrator([ @@ -166,7 +154,10 @@ public function testSchedulerCanScheduleTasksWithBeforeCallback(): void new TaskCallbackMiddleware(), ]), new EventDispatcher()); - $scheduler->schedule($task); + $scheduler->schedule(new NullTask('foo', [ + 'before_scheduling' => static fn (): int => 1 + 1, + ])); + self::assertCount(1, $scheduler->getTasks()); } /** @@ -345,14 +336,6 @@ public function testSchedulerCannotScheduleTasksWithErroredAfterCallback(): void */ public function testSchedulerCanScheduleTasksWithAfterCallback(): void { - $task = $this->createMock(TaskInterface::class); - $task->expects(self::exactly(2))->method('getName')->willReturn('foo'); - $task->expects(self::once())->method('setScheduledAt'); - $task->expects(self::once())->method('setTimezone'); - $task->expects(self::never())->method('isQueued'); - $task->expects(self::once())->method('getBeforeScheduling')->willReturn(null); - $task->expects(self::once())->method('getAfterScheduling')->willReturn(fn (): bool => true); - $scheduler = new Scheduler('UTC', new InMemoryTransport([ 'execution_mode' => 'first_in_first_out', ], new SchedulePolicyOrchestrator([ @@ -361,19 +344,21 @@ public function testSchedulerCanScheduleTasksWithAfterCallback(): void new TaskCallbackMiddleware(), ]), new EventDispatcher()); - $scheduler->schedule($task); + $scheduler->schedule(new NullTask('foo', [ + 'after_scheduling' => static fn (): bool => true, + ])); + self::assertCount(1, $scheduler->getTasks()); } /** * @throws Throwable {@see Scheduler::__construct()} * @throws Throwable {@see Scheduler::getSynchronizedCurrentDate()} */ - public function testSchedulerCanScheduleTasksWithMessageBus(): void + public function testSchedulerCanScheduleQueuedTasksWithMessageBus(): void { - $task = $this->createMock(TaskInterface::class); - $task->expects(self::once())->method('setScheduledAt'); - $task->expects(self::once())->method('setTimezone'); - $task->expects(self::once())->method('isQueued')->willReturn(true); + $task = new NullTask('foo', [ + 'queued' => true, + ]); $bus = $this->createMock(MessageBusInterface::class); $bus->expects(self::once())->method('dispatch')->with(new TaskToExecuteMessage($task))->willReturn(new Envelope(new stdClass())); @@ -383,7 +368,9 @@ public function testSchedulerCanScheduleTasksWithMessageBus(): void ], new SchedulePolicyOrchestrator([ new FirstInFirstOutPolicy(), ])), new SchedulerMiddlewareStack(), new EventDispatcher(), $bus); + $scheduler->schedule($task); + self::assertCount(0, $scheduler->getTasks()); } /** @@ -1628,7 +1615,7 @@ public function testSchedulerCannotPreemptEmptyDueTasks(): void new FirstInFirstOutPolicy(), ])), new SchedulerMiddlewareStack(), new EventDispatcher()); - $scheduler->preempt('foo', fn (TaskInterface $task): bool => $task->getName() === 'bar'); + $scheduler->preempt('foo', static fn (TaskInterface $task): bool => $task->getName() === 'bar'); self::assertNotSame(TaskInterface::READY_TO_EXECUTE, $task->getState()); } @@ -1646,7 +1633,7 @@ public function testSchedulerCannotPreemptEmptyToPreemptTasks(): void ])), new SchedulerMiddlewareStack(), $eventDispatcher); $scheduler->schedule(new NullTask('foo')); - $scheduler->preempt('foo', fn (TaskInterface $task): bool => $task->getName() === 'bar'); + $scheduler->preempt('foo', static fn (TaskInterface $task): bool => $task->getName() === 'bar'); } /**