Skip to content

[Webhook] decouple the Webhook component from the Serializer component #57881

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,7 @@ public function load(array $configs, ContainerBuilder $container): void
$this->registerProfilerConfiguration($config['profiler'], $container, $loader);

if ($this->readConfigEnabled('webhook', $container, $config['webhook'])) {
$this->registerWebhookConfiguration($config['webhook'], $container, $loader);
$this->registerWebhookConfiguration($config['webhook'], $container, $loader, $this->readConfigEnabled('serializer', $container, $config['serializer']));

// If Webhook is installed but the HttpClient or Serializer components are not available, we should throw an error
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

outdated comment

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (!$this->readConfigEnabled('http_client', $container, $config['http_client'])) {
Expand All @@ -559,14 +559,6 @@ public function load(array $configs, ContainerBuilder $container): void
)
->addTag('container.error');
}
if (!$this->readConfigEnabled('serializer', $container, $config['serializer'])) {
$container->getDefinition('webhook.body_configurator.json')
->setArguments([])
->addError('You cannot use the "webhook transport" service since the Serializer component is not '
.(class_exists(Serializer::class) ? 'enabled. Try setting "framework.serializer.enabled" to true.' : 'installed. Try running "composer require symfony/serializer-pack".')
)
->addTag('container.error');
}
}

if ($this->readConfigEnabled('remote-event', $container, $config['remote-event'])) {
Expand Down Expand Up @@ -2919,7 +2911,7 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $
}
}

private function registerWebhookConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader): void
private function registerWebhookConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader, bool $serializerEnabled): void
{
if (!class_exists(WebhookController::class)) {
throw new LogicException('Webhook support cannot be enabled as the component is not installed. Try running "composer require symfony/webhook".');
Expand All @@ -2938,6 +2930,9 @@ private function registerWebhookConfiguration(array $config, ContainerBuilder $c
$controller = $container->getDefinition('webhook.controller');
$controller->replaceArgument(0, $parsers);
$controller->replaceArgument(1, new Reference($config['message_bus']));

$jsonBodyConfigurator = $container->getDefinition('webhook.body_configurator.json');
$jsonBodyConfigurator->replaceArgument(0, new Reference($serializerEnabled ? 'webhook.payload_serializer.serializer' : 'webhook.payload_serializer.json'));
}

private function registerRemoteEventConfiguration(PhpFileLoader $loader): void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
use Symfony\Component\Webhook\Server\HeadersConfigurator;
use Symfony\Component\Webhook\Server\HeaderSignatureConfigurator;
use Symfony\Component\Webhook\Server\JsonBodyConfigurator;
use Symfony\Component\Webhook\Server\NativeJsonPayloadSerializer;
use Symfony\Component\Webhook\Server\SerializerPayloadSerializer;
use Symfony\Component\Webhook\Server\Transport;

return static function (ContainerConfigurator $container) {
Expand All @@ -32,6 +34,13 @@
->set('webhook.headers_configurator', HeadersConfigurator::class)

->set('webhook.body_configurator.json', JsonBodyConfigurator::class)
->args([
abstract_arg('payload serializer'),
])

->set('webhook.payload_serializer.json', NativeJsonPayloadSerializer::class)

->set('webhook.payload_serializer.serializer', SerializerPayloadSerializer::class)
->args([
service('serializer'),
])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Notifier\Notifier;
use Symfony\Component\RateLimiter\Policy\TokenBucketLimiter;
use Symfony\Component\RemoteEvent\RemoteEvent;
use Symfony\Component\Scheduler\Messenger\SchedulerTransportFactory;
use Symfony\Component\Serializer\Encoder\JsonDecode;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\Uid\Factory\UuidFactory;
use Symfony\Component\Webhook\Controller\WebhookController;

class ConfigurationTest extends TestCase
{
Expand Down Expand Up @@ -925,12 +927,12 @@ class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphor
],
'exceptions' => [],
'webhook' => [
'enabled' => false,
'enabled' => !class_exists(FullStack::class) && class_exists(WebhookController::class),
'routing' => [],
'message_bus' => 'messenger.default_bus',
],
'remote-event' => [
'enabled' => false,
'enabled' => !class_exists(FullStack::class) && class_exists(RemoteEvent::class),
],
];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2357,7 +2357,7 @@ public function testWebhook()
$this->assertSame(RequestParser::class, $container->getDefinition('webhook.request_parser')->getClass());

$this->assertFalse($container->getDefinition('webhook.transport')->hasErrors());
$this->assertFalse($container->getDefinition('webhook.body_configurator.json')->hasErrors());
$this->assertEquals('webhook.payload_serializer.serializer', $container->getDefinition('webhook.body_configurator.json')->getArgument(0));
}

public function testWebhookWithoutSerializer()
Expand All @@ -2369,11 +2369,7 @@ public function testWebhookWithoutSerializer()
$container = $this->createContainerFromFile('webhook_without_serializer');

$this->assertFalse($container->getDefinition('webhook.transport')->hasErrors());
$this->assertTrue($container->getDefinition('webhook.body_configurator.json')->hasErrors());
$this->assertSame(
['You cannot use the "webhook transport" service since the Serializer component is not enabled. Try setting "framework.serializer.enabled" to true.'],
$container->getDefinition('webhook.body_configurator.json')->getErrors()
);
$this->assertEquals('webhook.payload_serializer.json', $container->getDefinition('webhook.body_configurator.json')->getArgument(0));
}

public function testAssetMapperWithoutAssets()
Expand Down
2 changes: 2 additions & 0 deletions src/Symfony/Bundle/FrameworkBundle/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"symfony/property-info": "^6.4|^7.0",
"symfony/uid": "^6.4|^7.0",
"symfony/web-link": "^6.4|^7.0",
"symfony/webhook": "^7.2",
"phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0",
"twig/twig": "^3.0.4"
},
Expand Down Expand Up @@ -102,6 +103,7 @@
"symfony/twig-bundle": "<6.4",
"symfony/validator": "<6.4",
"symfony/web-profiler-bundle": "<6.4",
"symfony/webhook": "<7.2",
"symfony/workflow": "<6.4"
},
"autoload": {
Expand Down
5 changes: 5 additions & 0 deletions src/Symfony/Component/Webhook/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
CHANGELOG
=========

7.2
---

* Add `PayloadSerializerInterface` with implementations to decouple the remote event handling from the Serializer component

6.4
---

Expand Down
10 changes: 6 additions & 4 deletions src/Symfony/Component/Webhook/Server/JsonBodyConfigurator.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,16 @@
*/
final class JsonBodyConfigurator implements RequestConfiguratorInterface
{
public function __construct(
private readonly SerializerInterface $serializer,
) {
private PayloadSerializerInterface $payloadSerializer;

public function __construct(SerializerInterface|PayloadSerializerInterface $payloadSerializer)
{
$this->payloadSerializer = $payloadSerializer instanceof SerializerInterface ? new SerializerPayloadSerializer($payloadSerializer) : $payloadSerializer;
}

public function configure(RemoteEvent $event, #[\SensitiveParameter] string $secret, HttpOptions $options): void
{
$body = $this->serializer->serialize($event->getPayload(), 'json');
$body = $this->payloadSerializer->serialize($event->getPayload());
$options->setBody($body);
$headers = $options->toArray()['headers'];
$headers['Content-Type'] = 'application/json';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Webhook\Server;

final class NativeJsonPayloadSerializer implements PayloadSerializerInterface
{
public function serialize(array $payload): string
{
return json_encode($payload, \JSON_THROW_ON_ERROR);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Webhook\Server;

interface PayloadSerializerInterface
{
public function serialize(array $payload): string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Webhook\Server;

use Symfony\Component\Serializer\SerializerInterface;

final class SerializerPayloadSerializer implements PayloadSerializerInterface
{
public function __construct(
private SerializerInterface $serializer,
) {
}

public function serialize(array $payload): string
{
return $this->serializer->serialize($payload, 'json');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Webhook\Tests\Server;

use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpClient\HttpOptions;
use Symfony\Component\RemoteEvent\RemoteEvent;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Webhook\Server\JsonBodyConfigurator;
use Symfony\Component\Webhook\Server\PayloadSerializerInterface;

class JsonBodyConfiguratorTest extends TestCase
{
public function testPayloadWithPayloadSerializer()
{
$payload = ['foo' => 'bar'];

$payloadSerializer = $this->createMock(PayloadSerializerInterface::class);
$payloadSerializer
->expects($this->once())
->method('serialize')
->with($payload)
;

$httpOptions = new HttpOptions();
$httpOptions->setHeaders([
'Webhook-Event' => 'event-name',
'Webhook-Id' => 'event-id',
]);

$configurator = new JsonBodyConfigurator($payloadSerializer);
$configurator->configure(new RemoteEvent('event-name', 'event-id', $payload), 's3cr3t', $httpOptions);
}

public function testPayloadWithSerializer()
{
$payload = ['foo' => 'bar'];

$payloadEncoder = $this->createMock(SerializerInterface::class);
$payloadEncoder
->expects($this->once())
->method('serialize')
->with($payload, 'json')
->willReturn('{"foo": "bar"}')
;

$httpOptions = new HttpOptions();
$httpOptions->setHeaders([
'Webhook-Event' => 'event-name',
'Webhook-Id' => 'event-id',
]);

$configurator = new JsonBodyConfigurator($payloadEncoder);
$configurator->configure(new RemoteEvent('event-name', 'event-id', $payload), 's3cr3t', $httpOptions);

$this->assertJsonStringEqualsJsonString('{"foo": "bar"}', $httpOptions->toArray()['body']);
}
}
4 changes: 4 additions & 0 deletions src/Symfony/Component/Webhook/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
"symfony/messenger": "^6.4|^7.0",
"symfony/remote-event": "^6.4|^7.0"
},
"require-dev": {
"symfony/http-client": "^6.4|^7.0",
"symfony/serializer": "^6.4|^7.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Webhook\\": "" },
"exclude-from-classmap": [
Expand Down