diff --git a/composer.json b/composer.json index 70c4ed68..8f3fce70 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,8 @@ "symfony/yaml": "^5.0", "psy/psysh": "^0.10.8", "oleg-andreyev/mink-phpwebdriver": "^1.2", - "oleg-andreyev/mink-phpwebdriver-extension": "^1.0" + "oleg-andreyev/mink-phpwebdriver-extension": "^1.0", + "symfony/form": "^5.4" }, "require-dev": { "ibexa/code-style": "^1.0", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 7f25e2e2..e8187021 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -130,6 +130,36 @@ parameters: count: 1 path: src/bundle/DependencyInjection/IbexaBehatExtension.php + - + message: "#^Method Ibexa\\\\Bundle\\\\Behat\\\\Form\\\\RetryChoiceListFactory\\:\\:createListFromChoices\\(\\) has parameter \\$choices with no value type specified in iterable type iterable\\.$#" + count: 1 + path: src/bundle/Form/RetryChoiceListFactory.php + + - + message: "#^Method Ibexa\\\\Bundle\\\\Behat\\\\Form\\\\RetryChoiceListFactory\\:\\:createView\\(\\) has parameter \\$attr with no value type specified in iterable type array\\.$#" + count: 1 + path: src/bundle/Form/RetryChoiceListFactory.php + + - + message: "#^Method Ibexa\\\\Bundle\\\\Behat\\\\Form\\\\RetryChoiceListFactory\\:\\:createView\\(\\) has parameter \\$preferredChoices with no value type specified in iterable type array\\.$#" + count: 1 + path: src/bundle/Form/RetryChoiceListFactory.php + + - + message: "#^Method Symfony\\\\Component\\\\Form\\\\ChoiceList\\\\Factory\\\\ChoiceListFactoryInterface\\:\\:createListFromChoices\\(\\) invoked with 3 parameters, 1\\-2 required\\.$#" + count: 1 + path: src/bundle/Form/RetryChoiceListFactory.php + + - + message: "#^Method Symfony\\\\Component\\\\Form\\\\ChoiceList\\\\Factory\\\\ChoiceListFactoryInterface\\:\\:createListFromLoader\\(\\) invoked with 3 parameters, 1\\-2 required\\.$#" + count: 1 + path: src/bundle/Form/RetryChoiceListFactory.php + + - + message: "#^Method Symfony\\\\Component\\\\Form\\\\ChoiceList\\\\Factory\\\\ChoiceListFactoryInterface\\:\\:createView\\(\\) invoked with 7 parameters, 1\\-6 required\\.$#" + count: 1 + path: src/bundle/Form/RetryChoiceListFactory.php + - message: "#^Method Ibexa\\\\Bundle\\\\Behat\\\\IbexaBehatBundle\\:\\:build\\(\\) has no return type specified\\.$#" count: 1 @@ -3025,6 +3055,21 @@ parameters: count: 1 path: tests/Browser/Element/Assert/CollectionAssertTest.php + - + message: "#^Method Ibexa\\\\Tests\\\\Bundle\\\\Behat\\\\Form\\\\Stub\\\\UnstableChoiceListFactory\\:\\:createListFromChoices\\(\\) has parameter \\$choices with no value type specified in iterable type iterable\\.$#" + count: 1 + path: tests/bundle/Form/Stub/UnstableChoiceListFactory.php + + - + message: "#^Method Ibexa\\\\Tests\\\\Bundle\\\\Behat\\\\Form\\\\Stub\\\\UnstableChoiceListFactory\\:\\:createView\\(\\) has parameter \\$attr with no value type specified in iterable type array\\.$#" + count: 1 + path: tests/bundle/Form/Stub/UnstableChoiceListFactory.php + + - + message: "#^Method Ibexa\\\\Tests\\\\Bundle\\\\Behat\\\\Form\\\\Stub\\\\UnstableChoiceListFactory\\:\\:createView\\(\\) has parameter \\$preferredChoices with no value type specified in iterable type array\\.$#" + count: 1 + path: tests/bundle/Form/Stub/UnstableChoiceListFactory.php + - message: "#^Method Ibexa\\\\Tests\\\\Behat\\\\Browser\\\\Element\\\\BaseTestCase\\:\\:createCollection\\(\\) has parameter \\$elementTexts with no type specified\\.$#" count: 1 diff --git a/src/bundle/Form/RetryChoiceListFactory.php b/src/bundle/Form/RetryChoiceListFactory.php new file mode 100644 index 00000000..936334d8 --- /dev/null +++ b/src/bundle/Form/RetryChoiceListFactory.php @@ -0,0 +1,93 @@ +choiceListFactory = $choiceListFactory; + } + + /** {@inheritDoc} */ + public function createListFromChoices(iterable $choices, callable $value = null): ChoiceListInterface + { + $filter = \func_num_args() > 2 ? func_get_arg(2) : null; + + return $this->executeWithRetry(function () use ($choices, $value, $filter) { + return $this->choiceListFactory->createListFromChoices($choices, $value, $filter); + }); + } + + /** {@inheritDoc} */ + public function createListFromLoader(ChoiceLoaderInterface $loader, callable $value = null): ChoiceListInterface + { + $filter = \func_num_args() > 2 ? func_get_arg(2) : null; + + return $this->executeWithRetry(function () use ($loader, $value, $filter) { + return $this->choiceListFactory->createListFromLoader($loader, $value, $filter); + }); + } + + /** {@inheritDoc} */ + public function createView( + ChoiceListInterface $list, + $preferredChoices = null, + $label = null, + callable $index = null, + callable $groupBy = null, + $attr = null + ): ChoiceListView { + $labelTranslationParameters = \func_num_args() > 6 ? func_get_arg(6) : []; + + return $this->executeWithRetry(function () use ($list, $preferredChoices, $label, $index, $groupBy, $attr, $labelTranslationParameters) { + return $this->choiceListFactory->createView( + $list, + $preferredChoices, + $label, + $index, + $groupBy, + $attr, + $labelTranslationParameters + ); + }); + } + + /** + * @template T + * + * @param callable(mixed ...$args): T $fn + * + * @return T + */ + private function executeWithRetry(callable $fn) + { + $counter = 0; + while (true) { + try { + return $fn(); + } catch (ErrorException $e) { + if ($counter > self::RETRY_LIMIT) { + throw $e; + } + ++$counter; + } + } + } +} diff --git a/src/bundle/Resources/config/services.yaml b/src/bundle/Resources/config/services.yaml index 75b8db1d..f6da7fea 100644 --- a/src/bundle/Resources/config/services.yaml +++ b/src/bundle/Resources/config/services.yaml @@ -2,6 +2,7 @@ imports: - { resource: services/contexts.yaml } - { resource: services/controllers.yaml } - { resource: services/factory.yaml } + - { resource: services/form.yaml } - { resource: services/fieldtype_data_providers.yaml } - { resource: services/known_issues.yaml } - { resource: services/limitation_parsers.yaml } diff --git a/src/bundle/Resources/config/services/form.yaml b/src/bundle/Resources/config/services/form.yaml new file mode 100644 index 00000000..be4d6306 --- /dev/null +++ b/src/bundle/Resources/config/services/form.yaml @@ -0,0 +1,8 @@ +services: + _defaults: + autowire: true + autoconfigure: true + public: false + + Ibexa\Bundle\Behat\Form\RetryChoiceListFactory: + decorates: 'form.choice_list_factory' diff --git a/tests/bundle/Form/RetryChoiceListFactoryTest.php b/tests/bundle/Form/RetryChoiceListFactoryTest.php new file mode 100644 index 00000000..8fab3fcb --- /dev/null +++ b/tests/bundle/Form/RetryChoiceListFactoryTest.php @@ -0,0 +1,78 @@ +createListFromChoices([], null)->getChoices()); + } + + /** + * @dataProvider provider + */ + public function testCreateListFromLoaderSuccess(int $numberOfFails): void + { + $retryChoiceListFactory = new RetryChoiceListFactory(new UnstableChoiceListFactory($numberOfFails)); + self::assertEquals([], $retryChoiceListFactory->createListFromLoader($this->createMock(ChoiceLoaderInterface::class))->getChoices()); + } + + /** + * @dataProvider provider + */ + public function testCreateViewSuccess(int $numberOfFails): void + { + $retryChoiceListFactory = new RetryChoiceListFactory(new UnstableChoiceListFactory($numberOfFails)); + self::assertEquals([], $retryChoiceListFactory->createView($this->createMock(ChoiceListInterface::class), null)->choices); + } + + public function testCreateListFromChoicesFail(): void + { + $retryChoiceListFactory = new RetryChoiceListFactory(new UnstableChoiceListFactory(4)); + $this->expectException(ErrorException::class); + $retryChoiceListFactory->createListFromChoices([], null); + } + + public function testCreateListFromLoaderFail(): void + { + $retryChoiceListFactory = new RetryChoiceListFactory(new UnstableChoiceListFactory(4)); + $this->expectException(ErrorException::class); + $retryChoiceListFactory->createListFromLoader($this->createMock(ChoiceLoaderInterface::class)); + } + + public function testCreateViewFail(): void + { + $retryChoiceListFactory = new RetryChoiceListFactory(new UnstableChoiceListFactory(4)); + $this->expectException(ErrorException::class); + $retryChoiceListFactory->createView($this->createMock(ChoiceListInterface::class), null); + } + + /** + * @return iterable> + */ + public static function provider(): iterable + { + yield ['No failures' => 0]; + yield ['One failure' => 1]; + yield ['Two failures' => 2]; + yield ['Three failures' => 3]; + } +} diff --git a/tests/bundle/Form/Stub/UnstableChoiceListFactory.php b/tests/bundle/Form/Stub/UnstableChoiceListFactory.php new file mode 100644 index 00000000..1a266fbb --- /dev/null +++ b/tests/bundle/Form/Stub/UnstableChoiceListFactory.php @@ -0,0 +1,63 @@ +successfulCallAfterNthTry = $successfulCallAfterNthTry; + } + + public function createListFromChoices(iterable $choices, callable $value = null) + { + ++$this->createListFromChoicesCounter; + $this->failIfNeeded($this->createListFromChoicesCounter); + + return new ArrayChoiceList([]); + } + + public function createListFromLoader(ChoiceLoaderInterface $loader, callable $value = null) + { + ++$this->createListFromLoaderCounter; + $this->failIfNeeded($this->createListFromLoaderCounter); + + return new ArrayChoiceList([]); + } + + public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, callable $index = null, callable $groupBy = null, $attr = null) + { + ++$this->createViewCounter; + $this->failIfNeeded($this->createViewCounter); + + return new ChoiceListView([]); + } + + private function failIfNeeded(int $callNumber): void + { + if ($callNumber <= $this->successfulCallAfterNthTry) { + throw new ErrorException(sprintf('Failing call: %d', $callNumber)); + } + } +}