From 7f0e00cd2d838037f716e0b8588a6529ef9f158c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Rz=CC=87any?= Date: Thu, 14 Sep 2023 10:33:20 +0200 Subject: [PATCH 1/8] fix: add evaluateTopics() method --- .../PublishMercureUpdatesListener.php | 55 +++++++++++-------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/src/Doctrine/EventListener/PublishMercureUpdatesListener.php b/src/Doctrine/EventListener/PublishMercureUpdatesListener.php index e255a6ec42d..427caf8c737 100644 --- a/src/Doctrine/EventListener/PublishMercureUpdatesListener.php +++ b/src/Doctrine/EventListener/PublishMercureUpdatesListener.php @@ -190,35 +190,15 @@ private function storeObjectToPublish(object $object, string $property): void $options['enable_async_update'] ??= true; - if ($options['topics'] ?? false) { - $topics = []; - foreach ((array) $options['topics'] as $topic) { - if (!\is_string($topic)) { - $topics[] = $topic; - continue; - } - - if (!str_starts_with($topic, '@=')) { - $topics[] = $topic; - continue; - } - - if (null === $this->expressionLanguage) { - throw new \LogicException('The "@=" expression syntax cannot be used without the Expression Language component. Try running "composer require symfony/expression-language".'); - } - - $topics[] = $this->expressionLanguage->evaluate(substr($topic, 2), ['object' => $object]); - } - - $options['topics'] = $topics; - } - if ('deletedObjects' === $property) { $types = $operation instanceof HttpOperation ? $operation->getTypes() : null; if (null === $types) { $types = [$operation->getShortName()]; } + // We need to evaluate it here, because in publishUpdate() the resource would be already deleted + $this->evaluateTopics($options, $object); + $this->deletedObjects[(object) [ 'id' => $this->iriConverter->getIriFromResource($object), 'iri' => $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_URL), @@ -244,6 +224,9 @@ private function publishUpdate(object $object, array $options, string $type): vo $resourceClass = $this->getObjectClass($object); $context = $options['normalization_context'] ?? $this->resourceMetadataFactory->create($resourceClass)->getOperation()->getNormalizationContext() ?? []; + // We need to evaluate it here, because in storeObjectToPublish() the resource would not have been persisted yet + $this->evaluateTopics($options, $object); + $iri = $options['topics'] ?? $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_URL); $data = $options['data'] ?? $this->serializer->serialize($object, key($this->formats), $context); } @@ -260,6 +243,32 @@ private function publishUpdate(object $object, array $options, string $type): vo } } + private function evaluateTopics(array &$options, object $object): void + { + if ($options['topics'] ?? false) { + $topics = []; + foreach ((array) $options['topics'] as $topic) { + if (!\is_string($topic)) { + $topics[] = $topic; + continue; + } + + if (!str_starts_with($topic, '@=')) { + $topics[] = $topic; + continue; + } + + if (null === $this->expressionLanguage) { + throw new \LogicException('The "@=" expression syntax cannot be used without the Expression Language component. Try running "composer require symfony/expression-language".'); + } + + $topics[] = $this->expressionLanguage->evaluate(substr($topic, 2), ['object' => $object]); + } + + $options['topics'] = $topics; + } + } + /** * @return Update[] */ From 6a62a53f854ec93947d1c4a5a32007df09e55d06 Mon Sep 17 00:00:00 2001 From: Corentin FACKEURE Date: Mon, 25 Sep 2023 14:18:39 +0200 Subject: [PATCH 2/8] fix(hydra): add xxx[] hydra:search iexact --- src/Doctrine/Common/Filter/SearchFilterInterface.php | 5 +++++ src/Doctrine/Common/Filter/SearchFilterTrait.php | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Doctrine/Common/Filter/SearchFilterInterface.php b/src/Doctrine/Common/Filter/SearchFilterInterface.php index a96042cabc7..2979c8fd872 100644 --- a/src/Doctrine/Common/Filter/SearchFilterInterface.php +++ b/src/Doctrine/Common/Filter/SearchFilterInterface.php @@ -26,6 +26,11 @@ interface SearchFilterInterface */ public const STRATEGY_EXACT = 'exact'; + /** + * @var string Exact matching case-insensitive + */ + public const STRATEGY_IEXACT = 'iexact'; + /** * @var string The value must be contained in the field */ diff --git a/src/Doctrine/Common/Filter/SearchFilterTrait.php b/src/Doctrine/Common/Filter/SearchFilterTrait.php index e07a201046c..9bb610d4478 100644 --- a/src/Doctrine/Common/Filter/SearchFilterTrait.php +++ b/src/Doctrine/Common/Filter/SearchFilterTrait.php @@ -66,7 +66,7 @@ public function getDescription(string $resourceClass): array $strategy = $this->getProperties()[$property] ?? self::STRATEGY_EXACT; $filterParameterNames = [$propertyName]; - if (self::STRATEGY_EXACT === $strategy) { + if (self::STRATEGY_EXACT === $strategy || self::STRATEGY_IEXACT === $strategy) { $filterParameterNames[] = $propertyName.'[]'; } From 1fccb8413a902a1011f049d0f8ddcd8d5456d335 Mon Sep 17 00:00:00 2001 From: Vincent Chalamon <407859+vincentchalamon@users.noreply.github.com> Date: Thu, 28 Sep 2023 16:10:11 +0200 Subject: [PATCH 3/8] feat(doctrine): add SearchFilter case-insensitive strategies constants --- .../Common/Filter/SearchFilterInterface.php | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/Doctrine/Common/Filter/SearchFilterInterface.php b/src/Doctrine/Common/Filter/SearchFilterInterface.php index 2979c8fd872..198129dc4f5 100644 --- a/src/Doctrine/Common/Filter/SearchFilterInterface.php +++ b/src/Doctrine/Common/Filter/SearchFilterInterface.php @@ -36,18 +36,38 @@ interface SearchFilterInterface */ public const STRATEGY_PARTIAL = 'partial'; + /** + * @var string The value must be contained in the field case-insensitive + */ + public const STRATEGY_IPARTIAL = 'partial'; + /** * @var string Finds fields that are starting with the value */ public const STRATEGY_START = 'start'; + /** + * @var string Finds fields that are starting with the value case-insensitive + */ + public const STRATEGY_ISTART = 'start'; + /** * @var string Finds fields that are ending with the value */ public const STRATEGY_END = 'end'; + /** + * @var string Finds fields that are ending with the value case-insensitive + */ + public const STRATEGY_IEND = 'end'; + /** * @var string Finds fields that are starting with the word */ public const STRATEGY_WORD_START = 'word_start'; + + /** + * @var string Finds fields that are starting with the word case-insensitive + */ + public const STRATEGY_IWORD_START = 'word_start'; } From e8adb08b89d2ca2d340ace19bab4e97562c8dda6 Mon Sep 17 00:00:00 2001 From: Vincent Chalamon <407859+vincentchalamon@users.noreply.github.com> Date: Thu, 28 Sep 2023 16:26:30 +0200 Subject: [PATCH 4/8] test(doctrine): add xxx[] hydra:search iexact field non-regression tests --- features/doctrine/search_filter.feature | 14 +++++++++++++- src/Doctrine/Common/Filter/SearchFilterTrait.php | 2 +- tests/Fixtures/TestBundle/Document/DummyCar.php | 1 + tests/Fixtures/TestBundle/Entity/DummyCar.php | 1 + 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/features/doctrine/search_filter.feature b/features/doctrine/search_filter.feature index 119a64a8b61..50718f81963 100644 --- a/features/doctrine/search_filter.feature +++ b/features/doctrine/search_filter.feature @@ -76,7 +76,7 @@ Feature: Search filter on collections }, "hydra:search": { "@type": "hydra:IriTemplate", - "hydra:template": "/dummy_cars{?availableAt[before],availableAt[strictly_before],availableAt[after],availableAt[strictly_after],canSell,foobar[],foobargroups[],foobargroups_override[],colors.prop,colors,colors[],secondColors,secondColors[],thirdColors,thirdColors[],uuid,uuid[],name}", + "hydra:template": "/dummy_cars{?availableAt[before],availableAt[strictly_before],availableAt[after],availableAt[strictly_after],canSell,foobar[],foobargroups[],foobargroups_override[],colors.prop,colors,colors[],secondColors,secondColors[],thirdColors,thirdColors[],uuid,uuid[],name,brand,brand[]}", "hydra:variableRepresentation": "BasicRepresentation", "hydra:mapping": [ { @@ -186,6 +186,18 @@ Feature: Search filter on collections "variable": "name", "property": "name", "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "brand", + "property": "brand", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "brand[]", + "property": "brand", + "required": false } ] } diff --git a/src/Doctrine/Common/Filter/SearchFilterTrait.php b/src/Doctrine/Common/Filter/SearchFilterTrait.php index 9bb610d4478..d004add8e63 100644 --- a/src/Doctrine/Common/Filter/SearchFilterTrait.php +++ b/src/Doctrine/Common/Filter/SearchFilterTrait.php @@ -66,7 +66,7 @@ public function getDescription(string $resourceClass): array $strategy = $this->getProperties()[$property] ?? self::STRATEGY_EXACT; $filterParameterNames = [$propertyName]; - if (self::STRATEGY_EXACT === $strategy || self::STRATEGY_IEXACT === $strategy) { + if (\in_array($strategy, [self::STRATEGY_EXACT, self::STRATEGY_IEXACT], true)) { $filterParameterNames[] = $propertyName.'[]'; } diff --git a/tests/Fixtures/TestBundle/Document/DummyCar.php b/tests/Fixtures/TestBundle/Document/DummyCar.php index 16d482326c2..db3fe080125 100644 --- a/tests/Fixtures/TestBundle/Document/DummyCar.php +++ b/tests/Fixtures/TestBundle/Document/DummyCar.php @@ -65,6 +65,7 @@ class DummyCar private ?bool $canSell = null; #[ODM\Field(type: 'date')] private ?\DateTime $availableAt = null; + #[ApiFilter(SearchFilter::class, strategy: SearchFilter::STRATEGY_IEXACT)] #[Serializer\Groups(['colors'])] #[Serializer\SerializedName('carBrand')] #[ODM\Field] diff --git a/tests/Fixtures/TestBundle/Entity/DummyCar.php b/tests/Fixtures/TestBundle/Entity/DummyCar.php index 080abef640e..3991a1da6a8 100644 --- a/tests/Fixtures/TestBundle/Entity/DummyCar.php +++ b/tests/Fixtures/TestBundle/Entity/DummyCar.php @@ -73,6 +73,7 @@ class DummyCar private bool $canSell; #[ORM\Column(type: 'datetime')] private \DateTime $availableAt; + #[ApiFilter(SearchFilter::class, strategy: SearchFilter::STRATEGY_IEXACT)] #[Serializer\Groups(['colors'])] #[Serializer\SerializedName('carBrand')] #[ORM\Column] From 69383f473130e1e1c1602ecea06a39a184855844 Mon Sep 17 00:00:00 2001 From: Xavier Leune Date: Fri, 29 Sep 2023 09:44:37 +0200 Subject: [PATCH 5/8] docs: parser is losing the closing structure on new line (#5846) Co-authored-by: Xavier Leune --- docs/guides/subresource.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/guides/subresource.php b/docs/guides/subresource.php index 7ccee093e0d..0bf80024b6c 100644 --- a/docs/guides/subresource.php +++ b/docs/guides/subresource.php @@ -90,8 +90,7 @@ final class Migration extends AbstractMigration public function up(Schema $schema): void { $this->addSql('CREATE TABLE company (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(255) NOT NULL);'); - $this->addSql('CREATE TABLE employee (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, company_id INTEGER DEFAULT NULL, name VARCHAR(255) NOT NULL, CONSTRAINT FK_COMPANY FOREIGN KEY (company_id) REFERENCES company (id) NOT DEFERRABLE INITIALLY IMMEDIATE); -'); + $this->addSql('CREATE TABLE employee (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, company_id INTEGER DEFAULT NULL, name VARCHAR(255) NOT NULL, CONSTRAINT FK_COMPANY FOREIGN KEY (company_id) REFERENCES company (id) NOT DEFERRABLE INITIALLY IMMEDIATE);'); $this->addSql('CREATE INDEX FK_COMPANY ON employee (company_id)'); } } From dbadc6aa0cdebd9cab0a78fdfc9f8ffdfa362a35 Mon Sep 17 00:00:00 2001 From: Vincent Chalamon <407859+vincentchalamon@users.noreply.github.com> Date: Tue, 3 Oct 2023 10:27:06 +0200 Subject: [PATCH 6/8] tests: add TNR --- features/mercure/publish.feature | 33 ++++++++ tests/Behat/MercureContext.php | 77 +++++++++++++++++++ .../Document/Issue5074/MercureWithTopics.php | 36 +++++++++ .../Entity/Issue5074/MercureWithTopics.php | 38 +++++++++ 4 files changed, 184 insertions(+) create mode 100644 features/mercure/publish.feature create mode 100644 tests/Fixtures/TestBundle/Document/Issue5074/MercureWithTopics.php create mode 100644 tests/Fixtures/TestBundle/Entity/Issue5074/MercureWithTopics.php diff --git a/features/mercure/publish.feature b/features/mercure/publish.feature new file mode 100644 index 00000000000..ee7e3b7793f --- /dev/null +++ b/features/mercure/publish.feature @@ -0,0 +1,33 @@ +Feature: Mercure publish support + In order to publish an Update to the Mercure hub + As a developer + I need to specify which topics I want to send the Update on + + @createSchema + # see https://github.com/api-platform/core/issues/5074 + Scenario: Checks that Mercure Updates are dispatched properly + Given I add "Accept" header equal to "application/ld+json" + And I add "Content-Type" header equal to "application/ld+json" + When I send a "POST" request to "/issue5074/mercure_with_topics" with body: + """ + { + "name": "Hello World!", + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + } + """ + Then the response status code should be 201 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + Then 1 Mercure update should have been sent + And the Mercure update should have topics: + | http://example.com/issue5074/mercure_with_topics/1 | + And the Mercure update should have data: + """ + { + "@context": "/contexts/MercureWithTopics", + "@id": "/issue5074/mercure_with_topics/1", + "@type": "MercureWithTopics", + "id": 1, + "name": "Hello World!" + } + """ diff --git a/tests/Behat/MercureContext.php b/tests/Behat/MercureContext.php index c88a62fe4c3..ebcc90a2796 100644 --- a/tests/Behat/MercureContext.php +++ b/tests/Behat/MercureContext.php @@ -15,7 +15,10 @@ use Behat\Behat\Context\Context; use Behat\Gherkin\Node\PyStringNode; +use Behat\Gherkin\Node\TableNode; +use PHPUnit\Framework\Assert; use Psr\Container\ContainerInterface; +use Symfony\Component\Mercure\Update; /** * Context for Mercure. @@ -28,6 +31,80 @@ public function __construct(private readonly ContainerInterface $driverContainer { } + /** + * @Then :number Mercure updates should have been sent + * @Then :number Mercure update should have been sent + */ + public function mercureUpdatesShouldHaveBeenSent(int $number): void + { + $updateHandler = $this->driverContainer->get('mercure.hub.default.message_handler'); + $total = \count($updateHandler->getUpdates()); + + if (0 === $total) { + throw new \RuntimeException('No Mercure update has been sent.'); + } + + Assert::assertEquals($number, $total, sprintf('Expected %d Mercure updates to be sent, got %d.', $number, $total)); + } + + /** + * @Then the first Mercure update should have topics: + * @Then the Mercure update should have topics: + */ + public function firstMercureUpdateShouldHaveTopics(TableNode $table): void + { + $this->mercureUpdateShouldHaveTopics(1, $table); + } + + /** + * @Then the first Mercure update should have data: + * @Then the Mercure update should have data: + */ + public function firstMercureUpdateShouldHaveData(PyStringNode $data): void + { + $this->mercureUpdateShouldHaveData(1, $data); + } + + /** + * @Then the Mercure update number :index should have topics: + */ + public function mercureUpdateShouldHaveTopics(int $index, TableNode $table): void + { + $updateHandler = $this->driverContainer->get('mercure.hub.default.message_handler'); + $updates = $updateHandler->getUpdates(); + + if (0 === \count($updates)) { + throw new \RuntimeException('No Mercure update has been sent.'); + } + + if (!isset($updates[$index - 1])) { + throw new \RuntimeException(sprintf('Mercure update #%d does not exist.', $index)); + } + /** @var Update $update */ + $update = $updates[$index - 1]; + Assert::assertEquals(array_keys($table->getRowsHash()), array_values($update->getTopics())); + } + + /** + * @Then the Mercure update number :index should have data: + */ + public function mercureUpdateShouldHaveData(int $index, PyStringNode $data): void + { + $updateHandler = $this->driverContainer->get('mercure.hub.default.message_handler'); + $updates = $updateHandler->getUpdates(); + + if (0 === \count($updates)) { + throw new \RuntimeException('No Mercure update has been sent.'); + } + + if (!isset($updates[$index - 1])) { + throw new \RuntimeException(sprintf('Mercure update #%d does not exist.', $index)); + } + /** @var Update $update */ + $update = $updates[$index - 1]; + Assert::assertJsonStringEqualsJsonString($data->getRaw(), $update->getData()); + } + /** * @Then the following Mercure update with topics :topics should have been sent: */ diff --git a/tests/Fixtures/TestBundle/Document/Issue5074/MercureWithTopics.php b/tests/Fixtures/TestBundle/Document/Issue5074/MercureWithTopics.php new file mode 100644 index 00000000000..6688909b286 --- /dev/null +++ b/tests/Fixtures/TestBundle/Document/Issue5074/MercureWithTopics.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\Document\Issue5074; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Post; +use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; + +#[ApiResource( + operations: [ + new Get(uriTemplate: '/issue5074/mercure_with_topics/{id}{._format}'), + new Post(uriTemplate: '/issue5074/mercure_with_topics{._format}'), + ], + mercure: ['topics' => '@=iri(object)'], + extraProperties: ['standard_put' => false] +)] +#[ODM\Document] +class MercureWithTopics +{ + #[ODM\Id(strategy: 'INCREMENT', type: 'int')] + public $id; + #[ODM\Field(type: 'string')] + public $name; +} diff --git a/tests/Fixtures/TestBundle/Entity/Issue5074/MercureWithTopics.php b/tests/Fixtures/TestBundle/Entity/Issue5074/MercureWithTopics.php new file mode 100644 index 00000000000..acc22f05e95 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/Issue5074/MercureWithTopics.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5074; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Post; +use Doctrine\ORM\Mapping as ORM; + +#[ApiResource( + operations: [ + new Get(uriTemplate: '/issue5074/mercure_with_topics/{id}{._format}'), + new Post(uriTemplate: '/issue5074/mercure_with_topics{._format}'), + ], + mercure: ['topics' => '@=iri(object)'], + extraProperties: ['standard_put' => false] +)] +#[ORM\Entity] +class MercureWithTopics +{ + #[ORM\Id] + #[ORM\Column(type: 'integer')] + #[ORM\GeneratedValue(strategy: 'AUTO')] + public $id; + #[ORM\Column] + public $name; +} From 12822e34680b079818200229cd2ba38116a09912 Mon Sep 17 00:00:00 2001 From: clementtalleu Date: Mon, 2 Oct 2023 13:21:19 +0200 Subject: [PATCH 7/8] test(openapi): test OpenAPI security scheme --- .../TestBundle/Entity/Issue5625/Currency.php | 30 +++++++++++++++++++ .../Bundle/Command/OpenApiCommandTest.php | 10 ++++++- 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 tests/Fixtures/TestBundle/Entity/Issue5625/Currency.php diff --git a/tests/Fixtures/TestBundle/Entity/Issue5625/Currency.php b/tests/Fixtures/TestBundle/Entity/Issue5625/Currency.php new file mode 100644 index 00000000000..7086fdccff8 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/Issue5625/Currency.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5625; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\OpenApi\Model\Operation; + +/** + * Currency. + */ +#[ApiResource(operations: [ + new Get(uriTemplate: '/get_security_1', openapi: new Operation(security: [['JWT' => ['CURRENCY_READ']]])), +])] +class Currency +{ + public $id; + public $name; +} diff --git a/tests/Symfony/Bundle/Command/OpenApiCommandTest.php b/tests/Symfony/Bundle/Command/OpenApiCommandTest.php index 7b3b1b648d0..f012a8bcc52 100644 --- a/tests/Symfony/Bundle/Command/OpenApiCommandTest.php +++ b/tests/Symfony/Bundle/Command/OpenApiCommandTest.php @@ -41,7 +41,6 @@ protected function setUp(): void $application = new Application(static::$kernel); $application->setCatchExceptions(false); $application->setAutoExit(false); - $this->tester = new ApplicationTester($application); $this->handleDeprecations(); @@ -59,6 +58,7 @@ public function testExecuteWithYaml(): void $this->tester->run(['command' => 'api:openapi:export', '--yaml' => true]); $result = $this->tester->getDisplay(); + $this->assertYaml($result); $operationId = 'api_dummy_cars_get_collection'; @@ -96,6 +96,14 @@ public function testExecuteWithYaml(): void YAML; $this->assertStringContainsString(str_replace(\PHP_EOL, "\n", $expected), $result); + + $expected = <<assertStringContainsString(str_replace(\PHP_EOL, "\n", $expected), $result); } public function testWriteToFile(): void From 26e2bbe96b6aea1a31dfccbab23bd1f1b736592e Mon Sep 17 00:00:00 2001 From: Vincent <407859+vincentchalamon@users.noreply.github.com> Date: Thu, 5 Oct 2023 10:23:37 +0200 Subject: [PATCH 8/8] Update src/Doctrine/EventListener/PublishMercureUpdatesListener.php Co-authored-by: Antoine Bluchet --- .../PublishMercureUpdatesListener.php | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/src/Doctrine/EventListener/PublishMercureUpdatesListener.php b/src/Doctrine/EventListener/PublishMercureUpdatesListener.php index 427caf8c737..4bc59e18774 100644 --- a/src/Doctrine/EventListener/PublishMercureUpdatesListener.php +++ b/src/Doctrine/EventListener/PublishMercureUpdatesListener.php @@ -245,28 +245,30 @@ private function publishUpdate(object $object, array $options, string $type): vo private function evaluateTopics(array &$options, object $object): void { - if ($options['topics'] ?? false) { - $topics = []; - foreach ((array) $options['topics'] as $topic) { - if (!\is_string($topic)) { - $topics[] = $topic; - continue; - } - - if (!str_starts_with($topic, '@=')) { - $topics[] = $topic; - continue; - } - - if (null === $this->expressionLanguage) { - throw new \LogicException('The "@=" expression syntax cannot be used without the Expression Language component. Try running "composer require symfony/expression-language".'); - } - - $topics[] = $this->expressionLanguage->evaluate(substr($topic, 2), ['object' => $object]); + if (!($options['topics'] ?? false)) { + return; + } + + $topics = []; + foreach ((array) $options['topics'] as $topic) { + if (!\is_string($topic)) { + $topics[] = $topic; + continue; } - $options['topics'] = $topics; + if (!str_starts_with($topic, '@=')) { + $topics[] = $topic; + continue; + } + + if (null === $this->expressionLanguage) { + throw new \LogicException('The "@=" expression syntax cannot be used without the Expression Language component. Try running "composer require symfony/expression-language".'); + } + + $topics[] = $this->expressionLanguage->evaluate(substr($topic, 2), ['object' => $object]); } + + $options['topics'] = $topics; } /**