diff --git a/src/Debug/TraceableHub.php b/src/Debug/TraceableHub.php index bfb0037..7193cbb 100644 --- a/src/Debug/TraceableHub.php +++ b/src/Debug/TraceableHub.php @@ -18,6 +18,7 @@ use Symfony\Component\Mercure\Jwt\TokenProviderInterface; use Symfony\Component\Mercure\Update; use Symfony\Component\Stopwatch\Stopwatch; +use Symfony\Contracts\HttpClient\ResponseInterface; use Symfony\Contracts\Service\ResetInterface; /** @@ -74,6 +75,36 @@ public function publish(Update $update): string return $content; } + public function publishFast(Update $update, ?string $token = null): ResponseInterface + { + $this->stopwatch->start(__CLASS__); + $content = $this->hub->publishFast($update, $token); + + $e = $this->stopwatch->stop(__CLASS__); + $this->messages[] = [ + 'object' => $update, + 'duration' => $e->getDuration(), + 'memory' => $e->getMemory(), + ]; + + return $content; + } + + public function publishBatch($updates, bool $fireAndForget = false): array + { + $this->stopwatch->start(__CLASS__); + $content = $this->hub->publishBatch($updates); + + $e = $this->stopwatch->stop(__CLASS__); + $this->messages[] = [ + 'object' => $updates, + 'duration' => $e->getDuration(), + 'memory' => $e->getMemory(), + ]; + + return $content; + } + public function reset(): void { $this->messages = []; diff --git a/src/Hub.php b/src/Hub.php index 19935fe..056e2f8 100644 --- a/src/Hub.php +++ b/src/Hub.php @@ -18,6 +18,7 @@ use Symfony\Component\Mercure\Jwt\TokenProviderInterface; use Symfony\Contracts\HttpClient\Exception\ExceptionInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; /** * @author Saif Eddin Gmati @@ -82,6 +83,21 @@ public function getFactory(): ?TokenFactoryInterface * {@inheritDoc} */ public function publish(Update $update): string + { + $jwt = $this->getProvider()->getJwt(); + $this->validateJwt($jwt); + + try { + return $this->publishFast($update, $jwt)->getContent(); + } catch (ExceptionInterface $exception) { + throw new Exception\RuntimeException('Failed to send an update.', 0, $exception); + } + } + + /** + * {@inheritDoc} + */ + public function publishFast(Update $update, ?string $token = null): ResponseInterface { $postData = [ 'topic' => $update->getTopics(), @@ -92,14 +108,37 @@ public function publish(Update $update): string 'retry' => $update->getRetry(), ]; + if (!$token) { + $token = $this->getProvider()->getJwt(); + $this->validateJwt($token); + } + + return $this->httpClient->request('POST', $this->getUrl(), [ + 'auth_bearer' => $token, + 'body' => Internal\QueryBuilder::build($postData), + ]); + } + + /** + * {@inheritDoc} + */ + public function publishBatch($updates, bool $fireAndForget = false): array + { $jwt = $this->getProvider()->getJwt(); $this->validateJwt($jwt); try { - return $this->httpClient->request('POST', $this->getUrl(), [ - 'auth_bearer' => $jwt, - 'body' => Internal\QueryBuilder::build($postData), - ])->getContent(); + $requests = []; + foreach ($updates as $update) { + $requests[] = $this->publishFast($update, $jwt); + } + if ($fireAndForget) { + return []; + } + + return array_map(function ($val) { + return $val->getContent(); + }, $requests); } catch (ExceptionInterface $exception) { throw new Exception\RuntimeException('Failed to send an update.', 0, $exception); } @@ -118,7 +157,7 @@ public function publish(Update $update): string private function validateJwt(string $jwt): void { if (!preg_match('/^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$/', $jwt)) { - throw new Exception\InvalidArgumentException('The provided JWT is not valid'); + throw new Exception\InvalidArgumentException('The provided JWT is not valid.'); } } } diff --git a/src/HubInterface.php b/src/HubInterface.php index 46d24e3..1423129 100644 --- a/src/HubInterface.php +++ b/src/HubInterface.php @@ -15,6 +15,7 @@ use Symfony\Component\Mercure\Jwt\TokenFactoryInterface; use Symfony\Component\Mercure\Jwt\TokenProviderInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; /** * @author Saif Eddin Gmati @@ -50,4 +51,16 @@ public function getFactory(): ?TokenFactoryInterface; * Publish an update to this Hub. */ public function publish(Update $update): string; + + /** + * Publish an update to this Hub. + */ + public function publishFast(Update $update, ?string $token = null): ResponseInterface; + + /** + * Publish updates to this Hub. + * + * @param iterable $updates + */ + public function publishBatch($updates, bool $fireAndForget = false): array; } diff --git a/src/MockHub.php b/src/MockHub.php index 093db32..c2857ab 100644 --- a/src/MockHub.php +++ b/src/MockHub.php @@ -15,6 +15,7 @@ use Symfony\Component\Mercure\Jwt\TokenFactoryInterface; use Symfony\Component\Mercure\Jwt\TokenProviderInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; final class MockHub implements HubInterface { @@ -25,7 +26,7 @@ final class MockHub implements HubInterface private $publicUrl; /** - * @param (callable(Update): string) $publisher + * @param (callable(Update): ResponseInterface) $publisher */ public function __construct( string $url, @@ -62,7 +63,28 @@ public function getFactory(): ?TokenFactoryInterface } public function publish(Update $update): string + { + return ($this->publisher)($update)->getContent(); + } + + public function publishFast(Update $update, ?string $token = null): ResponseInterface { return ($this->publisher)($update); } + + public function publishBatch($updates, bool $fireAndForget = false): array + { + $requests = []; + $token = null; + foreach ($updates as $update) { + $requests[] = $this->publishFast($update, $token); + } + if ($fireAndForget) { + return []; + } + + return array_map(function ($val) { + return $val->getContent(); + }, $requests); + } } diff --git a/tests/AuthorizationTest.php b/tests/AuthorizationTest.php index ca174ce..6416ee8 100644 --- a/tests/AuthorizationTest.php +++ b/tests/AuthorizationTest.php @@ -15,6 +15,7 @@ use Lcobucci\JWT\Signer\Key\InMemory; use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpClient\Response\MockResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Mercure\Authorization; use Symfony\Component\Mercure\Exception\RuntimeException; @@ -24,6 +25,7 @@ use Symfony\Component\Mercure\Jwt\TokenFactoryInterface; use Symfony\Component\Mercure\MockHub; use Symfony\Component\Mercure\Update; +use Symfony\Contracts\HttpClient\ResponseInterface; /** * @author Kévin Dunglas @@ -39,7 +41,9 @@ public function testJwtLifetime(): void $registry = new HubRegistry(new MockHub( 'https://example.com/.well-known/mercure', new StaticTokenProvider('foo.bar.baz'), - function (Update $u): string { return 'dummy'; }, + function (Update $u): ResponseInterface { + return new MockResponse('dummy'); + }, new LcobucciFactory('secret', 'hmac.sha256', 3600) )); @@ -57,13 +61,14 @@ public function testSetCookie(): void $tokenFactory ->expects($this->once()) ->method('create') - ->with($this->equalTo(['foo']), $this->equalTo(['bar']), $this->arrayHasKey('x-foo')) - ; + ->with($this->equalTo(['foo']), $this->equalTo(['bar']), $this->arrayHasKey('x-foo')); $registry = new HubRegistry(new MockHub( 'https://example.com/.well-known/mercure', new StaticTokenProvider('foo.bar.baz'), - function (Update $u): string { return 'dummy'; }, + function (Update $u): ResponseInterface { + return new MockResponse('dummy'); + }, $tokenFactory )); @@ -81,7 +86,9 @@ public function testClearCookie(): void $registry = new HubRegistry(new MockHub( 'https://example.com/.well-known/mercure', new StaticTokenProvider('foo.bar.baz'), - function (Update $u): string { return 'dummy'; }, + function (Update $u): ResponseInterface { + return new MockResponse('dummy'); + }, new class() implements TokenFactoryInterface { public function create(array $subscribe = [], array $publish = [], array $additionalClaims = []): string { @@ -111,7 +118,9 @@ public function testApplicableCookieDomains(?string $expected, string $hubUrl, s $registry = new HubRegistry(new MockHub( $hubUrl, new StaticTokenProvider('foo.bar.baz'), - function (Update $u): string { return 'dummy'; }, + function (Update $u): ResponseInterface { + return new MockResponse('dummy'); + }, new LcobucciFactory('secret', 'hmac.sha256', 3600) )); @@ -143,7 +152,9 @@ public function testNonApplicableCookieDomains(string $hubUrl, string $requestUr $registry = new HubRegistry(new MockHub( $hubUrl, new StaticTokenProvider('foo.bar.baz'), - function (Update $u): string { return 'dummy'; }, + function (Update $u): ResponseInterface { + return new MockResponse('dummy'); + }, new LcobucciFactory('secret', 'hmac.sha256', 3600) )); @@ -168,7 +179,9 @@ public function testSetMultipleCookies(): void $registry = new HubRegistry(new MockHub( 'https://example.com/.well-known/mercure', new StaticTokenProvider('foo.bar.baz'), - function (Update $u): string { return 'dummy'; }, + function (Update $u): ResponseInterface { + return new MockResponse('dummy'); + }, new class() implements TokenFactoryInterface { public function create(array $subscribe = [], array $publish = [], array $additionalClaims = []): string { diff --git a/tests/HubRegistryTest.php b/tests/HubRegistryTest.php index 013b7c4..151c2b5 100644 --- a/tests/HubRegistryTest.php +++ b/tests/HubRegistryTest.php @@ -14,17 +14,23 @@ namespace Symfony\Component\Mercure\Tests; use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpClient\Response\MockResponse; use Symfony\Component\Mercure\Exception\InvalidArgumentException; use Symfony\Component\Mercure\HubRegistry; use Symfony\Component\Mercure\Jwt\StaticTokenProvider; use Symfony\Component\Mercure\MockHub; +use Symfony\Contracts\HttpClient\ResponseInterface; class HubRegistryTest extends TestCase { public function testGetHubByName(): void { - $fooHub = new MockHub('fooUrl', new StaticTokenProvider('fooToken'), static function (): string { return 'foo'; }); - $barHub = new MockHub('barUrl', new StaticTokenProvider('barToken'), static function (): string { return 'bar'; }); + $fooHub = new MockHub('fooUrl', new StaticTokenProvider('fooToken'), static function (): ResponseInterface { + return new MockResponse('foo'); + }); + $barHub = new MockHub('barUrl', new StaticTokenProvider('barToken'), static function (): ResponseInterface { + return new MockResponse('bar'); + }); $registry = new HubRegistry($fooHub, ['foo' => $fooHub, 'bar' => $barHub]); $this->assertSame($fooHub, $registry->getHub('foo')); @@ -32,8 +38,12 @@ public function testGetHubByName(): void public function testGetDefaultHub(): void { - $fooHub = new MockHub('fooUrl', new StaticTokenProvider('fooToken'), static function (): string { return 'foo'; }); - $barHub = new MockHub('barUrl', new StaticTokenProvider('barToken'), static function (): string { return 'bar'; }); + $fooHub = new MockHub('fooUrl', new StaticTokenProvider('fooToken'), static function (): ResponseInterface { + return new MockResponse('foo'); + }); + $barHub = new MockHub('barUrl', new StaticTokenProvider('barToken'), static function (): ResponseInterface { + return new MockResponse('bar'); + }); $registry = new HubRegistry($fooHub, ['foo' => $fooHub, 'bar' => $barHub]); $this->assertSame($fooHub, $registry->getHub()); @@ -41,7 +51,9 @@ public function testGetDefaultHub(): void public function testGetMissingHubThrows(): void { - $fooHub = new MockHub('fooUrl', new StaticTokenProvider('fooToken'), static function (): string { return 'foo'; }); + $fooHub = new MockHub('fooUrl', new StaticTokenProvider('fooToken'), static function (): ResponseInterface { + return new MockResponse('foo'); + }); $registry = new HubRegistry($fooHub, ['foo' => $fooHub]); $this->expectException(InvalidArgumentException::class); @@ -50,8 +62,12 @@ public function testGetMissingHubThrows(): void public function testGetAllHubs(): void { - $fooHub = new MockHub('fooUrl', new StaticTokenProvider('fooToken'), static function (): string { return 'foo'; }); - $barHub = new MockHub('barUrl', new StaticTokenProvider('barToken'), static function (): string { return 'bar'; }); + $fooHub = new MockHub('fooUrl', new StaticTokenProvider('fooToken'), static function (): ResponseInterface { + return new MockResponse('foo'); + }); + $barHub = new MockHub('barUrl', new StaticTokenProvider('barToken'), static function (): ResponseInterface { + return new MockResponse('bar'); + }); $registry = new HubRegistry($fooHub, ['foo' => $fooHub, 'bar' => $barHub]); $this->assertSame(['foo' => $fooHub, 'bar' => $barHub], $registry->all()); diff --git a/tests/Twig/MercureExtensionTest.php b/tests/Twig/MercureExtensionTest.php index ffe8c86..ded1b8f 100644 --- a/tests/Twig/MercureExtensionTest.php +++ b/tests/Twig/MercureExtensionTest.php @@ -14,6 +14,7 @@ namespace Symfony\Component\Mercure\Tests\Twig; use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpClient\Response\MockResponse; use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; @@ -24,6 +25,7 @@ use Symfony\Component\Mercure\MockHub; use Symfony\Component\Mercure\Twig\MercureExtension; use Symfony\Component\Mercure\Update; +use Symfony\Contracts\HttpClient\ResponseInterface; /** * @author Kévin Dunglas @@ -35,7 +37,9 @@ public function testMercure(): void $registry = new HubRegistry(new MockHub( 'https://example.com/.well-known/mercure', new StaticTokenProvider('foo.bar.baz'), - function (Update $u): string { return 'dummy'; }, + function (Update $u): ResponseInterface { + return new MockResponse('dummy'); + }, $this->createMock(TokenFactoryInterface::class) )); @@ -56,8 +60,8 @@ public function testMercureLastEventId(): void $registry = new HubRegistry(new MockHub( 'https://example.com/.well-known/mercure', new StaticTokenProvider('foo.bar.baz'), - function (Update $u): string { - return 'dummy'; + function (Update $u): ResponseInterface { + return new MockResponse('dummy'); }, $this->createMock(TokenFactoryInterface::class) ));