Skip to content

Commit 99e7c30

Browse files
authored
Merge pull request #3 from MacPaw/feat/http-client-decorator
http client decorator
2 parents 18306b6 + e8d95b4 commit 99e7c30

6 files changed

Lines changed: 149 additions & 4 deletions

File tree

README.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ The **SchemaContextBundle** provides a lightweight way to manage dynamic schema
66

77
## Features
88

9-
- Extracts tenant schema from request headers.
9+
- Extracts tenant schema param from baggage request header.
1010
- Stores schema context in a global `SchemaResolver`.
1111
- Injects schema info into Messenger messages via a middleware.
1212
- Rehydrates schema on message consumption via a middleware.
13+
- Provide decorator for Http clients to propagate schema header
1314

1415
---
1516

@@ -57,6 +58,19 @@ public function index(SchemaResolver $schemaResolver)
5758
}
5859
```
5960

61+
## Schema-Aware HTTP Client
62+
Decorate your http client in your service configuration:
63+
```yaml
64+
services:
65+
schema_aware_payment_http_client:
66+
class: Macpaw\SchemaContextBundle\HttpClient\SchemaAwareHttpClient
67+
decorates: payment_http_client #http client to decorate
68+
arguments:
69+
- '@schema_aware_payment_http_client.inner'
70+
- '@Macpaw\SchemaContextBundle\Service\SchemaResolver'
71+
- '%schema_context.header_name%'
72+
```
73+
6074
## Messenger Integration
6175
The bundle provides a middleware that automatically:
6276

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
"symfony/messenger": "^6.4 || ^7.0",
1818
"symfony/http-kernel": "^6.4 || ^7.0",
1919
"symfony/dependency-injection": "^6.4 || ^7.0",
20-
"symfony/config": "^6.4 || ^7.0"
20+
"symfony/config": "^6.4 || ^7.0",
21+
"symfony/http-client": "^7.3"
2122
},
2223
"require-dev": {
2324
"phpstan/phpstan": "^1.10",

src/EventListener/SchemaRequestListener.php

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,24 @@ public function onKernelRequest(RequestEvent $event): void
3232
return;
3333
}
3434

35-
$schema = $event->getRequest()->headers->get($this->schemaRequestHeader, $this->defaultSchema);
35+
$request = $event->getRequest();
36+
$baggage = $request->headers->get('baggage');
37+
38+
if ($baggage) {
39+
foreach (explode(',', $baggage) as $part) {
40+
[$key, $value] = array_map(
41+
static fn(?string $v): ?string => $v !== null ? trim($v) : null,
42+
explode('=', $part, 2) + [null, null]
43+
);
44+
45+
if ($key === $this->schemaRequestHeader && $value !== null) {
46+
$schema = $value;
47+
break;
48+
}
49+
}
50+
}
51+
52+
$schema ??= $this->defaultSchema;
3653

3754
if ($schema !== null && $schema !== '') {
3855
$this->schemaResolver->setSchema($schema);
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Macpaw\SchemaContextBundle\HttpClient;
6+
7+
use Symfony\Contracts\HttpClient\HttpClientInterface;
8+
use Symfony\Contracts\HttpClient\ResponseInterface;
9+
use Macpaw\SchemaContextBundle\Service\SchemaResolver;
10+
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
11+
12+
class SchemaAwareHttpClient implements HttpClientInterface
13+
{
14+
public function __construct(
15+
private HttpClientInterface $inner,
16+
private SchemaResolver $schemaResolver,
17+
private string $schemaRequestHeader
18+
) {
19+
}
20+
21+
/**
22+
* @param array<string, mixed> $options
23+
*/
24+
public function request(string $method, string $url, array $options = []): ResponseInterface
25+
{
26+
$schema = $this->schemaResolver->getSchema();
27+
$baggageHeader = $this->schemaRequestHeader . '=' . $schema;
28+
$headers = isset($options['headers']) && is_array($options['headers'])
29+
? $options['headers']
30+
: [];
31+
32+
if (isset($headers['baggage'])) {
33+
$headers['baggage'] .= ',' . $baggageHeader;
34+
} else {
35+
$headers['baggage'] = $baggageHeader;
36+
}
37+
38+
$options['headers'] = $headers;
39+
40+
return $this->inner->request($method, $url, $options);
41+
}
42+
43+
public function stream(iterable|ResponseInterface $responses, ?float $timeout = null): ResponseStreamInterface
44+
{
45+
return $this->inner->stream($responses, $timeout);
46+
}
47+
48+
/**
49+
* @param array<string, mixed> $options
50+
*/
51+
public function withOptions(array $options): static
52+
{
53+
$wrapped = $this->inner->withOptions($options);
54+
55+
/** @phpstan-ignore-next-line */
56+
return new self($wrapped, $this->schemaResolver, $this->schemaRequestHeader);
57+
}
58+
}

tests/EventListener/SchemaRequestListenerTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public function testSchemaFromHeaderIsSet(): void
2424
['test-app'],
2525
);
2626

27-
$request = new Request([], [], [], [], [], ['HTTP_X_SCHEMA' => 'tenant1']);
27+
$request = new Request([], [], [], [], [], ['HTTP_BAGGAGE' => 'X-Schema=tenant1']);
2828
$kernel = $this->createMock(HttpKernelInterface::class);
2929
$event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST);
3030

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Macpaw\SchemaContextBundle\Tests\HttpClient;
6+
7+
use Macpaw\SchemaContextBundle\HttpClient\SchemaAwareHttpClient;
8+
use Macpaw\SchemaContextBundle\Service\SchemaResolver;
9+
use PHPUnit\Framework\TestCase;
10+
use Symfony\Component\HttpClient\Response\MockResponse;
11+
use Symfony\Contracts\HttpClient\HttpClientInterface;
12+
use Symfony\Contracts\HttpClient\ResponseInterface;
13+
14+
class SchemaAwareHttpClientTest extends TestCase
15+
{
16+
public function testItInjectsSchemaIntoBaggageHeader(): void
17+
{
18+
$expectedSchema = 'tenant_42';
19+
$mockResponse = new MockResponse('OK');
20+
$schemaRequestHeader = 'X-Schema';
21+
22+
$mockClient = $this->createMock(HttpClientInterface::class);
23+
$mockClient
24+
->expects($this->once())
25+
->method('request')
26+
->with(
27+
'GET',
28+
'https://api.example.com/test',
29+
$this->callback(function (array $options) use ($expectedSchema, $schemaRequestHeader) {
30+
$headers = $options['headers'] ?? [];
31+
$baggage = $headers['baggage'] ?? null;
32+
33+
if (is_array($baggage)) {
34+
$baggage = implode(',', $baggage);
35+
}
36+
37+
return is_string($baggage) && str_contains($baggage, $schemaRequestHeader . '=' . $expectedSchema);
38+
})
39+
)
40+
->willReturn($mockResponse);
41+
42+
$schemaResolver = $this->createMock(SchemaResolver::class);
43+
$schemaResolver->method('getSchema')->willReturn($expectedSchema);
44+
45+
$client = new SchemaAwareHttpClient(
46+
$mockClient,
47+
$schemaResolver,
48+
$schemaRequestHeader,
49+
);
50+
51+
$response = $client->request('GET', 'https://api.example.com/test');
52+
53+
self::assertInstanceOf(ResponseInterface::class, $response);
54+
}
55+
}

0 commit comments

Comments
 (0)