Skip to content

Commit

Permalink
Merge pull request #5871 from soyuka/merge
Browse files Browse the repository at this point in the history
Merge 3.1 into main
  • Loading branch information
soyuka authored Oct 6, 2023
2 parents 1d38e9c + 98f818d commit ce0bb9e
Show file tree
Hide file tree
Showing 13 changed files with 299 additions and 28 deletions.
3 changes: 1 addition & 2 deletions docs/guides/subresource.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)');
}
}
Expand Down
14 changes: 13 additions & 1 deletion features/doctrine/search_filter.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
{
Expand Down Expand Up @@ -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
}
]
}
Expand Down
33 changes: 33 additions & 0 deletions features/mercure/publish.feature
Original file line number Diff line number Diff line change
@@ -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!"
}
"""
25 changes: 25 additions & 0 deletions src/Doctrine/Common/Filter/SearchFilterInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,48 @@ 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
*/
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';
}
2 changes: 1 addition & 1 deletion src/Doctrine/Common/Filter/SearchFilterTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public function getDescription(string $resourceClass): array
$strategy = $this->getProperties()[$property] ?? self::STRATEGY_EXACT;
$filterParameterNames = [$propertyName];

if (self::STRATEGY_EXACT === $strategy) {
if (\in_array($strategy, [self::STRATEGY_EXACT, self::STRATEGY_IEXACT], true)) {
$filterParameterNames[] = $propertyName.'[]';
}

Expand Down
57 changes: 34 additions & 23 deletions src/Doctrine/EventListener/PublishMercureUpdatesListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -192,35 +192,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),
Expand All @@ -246,6 +226,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);
}
Expand All @@ -262,6 +245,34 @@ private function publishUpdate(object $object, array $options, string $type): vo
}
}

private function evaluateTopics(array &$options, object $object): void
{
if (!($options['topics'] ?? false)) {
return;
}

$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[]
*/
Expand Down
77 changes: 77 additions & 0 deletions tests/Behat/MercureContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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:
*/
Expand Down
1 change: 1 addition & 0 deletions tests/Fixtures/TestBundle/Document/DummyCar.php
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
36 changes: 36 additions & 0 deletions tests/Fixtures/TestBundle/Document/Issue5074/MercureWithTopics.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* 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;
}
1 change: 1 addition & 0 deletions tests/Fixtures/TestBundle/Entity/DummyCar.php
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
38 changes: 38 additions & 0 deletions tests/Fixtures/TestBundle/Entity/Issue5074/MercureWithTopics.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* 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;
}
Loading

0 comments on commit ce0bb9e

Please sign in to comment.