Skip to content

Commit f705f31

Browse files
committed
Introduce a draft for a generic reusable HTTP client builder
1 parent 5b1fa94 commit f705f31

File tree

3 files changed

+265
-0
lines changed

3 files changed

+265
-0
lines changed

build.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
require __DIR__ . '/vendor/autoload.php';
3+
4+
use Http\Message\Authentication\BasicAuth;
5+
use Http\Message\Formatter;
6+
use Phpro\HttpTools\Client\ClientBuilder;
7+
use Phpro\HttpTools\Client\Factory\SymfonyClientFactory;
8+
use Phpro\HttpTools\Formatter\FormatterBuilder;
9+
use Phpro\HttpTools\Formatter\RemoveSensitiveHeadersFormatter;
10+
use Phpro\HttpTools\Request\Request;
11+
use Phpro\HttpTools\Transport\Presets\RawPreset;
12+
use Phpro\HttpTools\Uri\RawUriBuilder;
13+
use Symfony\Component\Console\Logger\ConsoleLogger;
14+
use Symfony\Component\Console\Output\ConsoleOutput;
15+
use Symfony\Component\Console\Output\OutputInterface;
16+
17+
$client = ClientBuilder::default(SymfonyClientFactory::create([]))
18+
->addBaseUri('https://www.google.com')
19+
->addHeaders([
20+
'x-Foo' => 'bar',
21+
])
22+
->addAuthentication(new BasicAuth('user', 'pass'))
23+
->addLogger(
24+
new ConsoleLogger(new ConsoleOutput(OutputInterface::VERBOSITY_DEBUG)),
25+
FormatterBuilder::default()
26+
->withDebug(true)
27+
->withMaxBodyLength(1000)
28+
->addDecorator(static fn (Formatter $formatter) => new RemoveSensitiveHeadersFormatter($formatter, [
29+
'X-SENSITIVE-HEADER',
30+
]))
31+
->build()
32+
)
33+
->addPluginWithCurrentlyConfiguredClient(
34+
static fn (\Psr\Http\Client\ClientInterface $client) => new class($client) implements \Http\Client\Common\Plugin
35+
{
36+
public function __construct(
37+
private \Psr\Http\Client\ClientInterface $client,
38+
) {
39+
}
40+
41+
public function handleRequest(\Psr\Http\Message\RequestInterface $request, callable $next, callable $first): \Http\Promise\Promise
42+
{
43+
// $token = $this->client->sendRequest('...;');
44+
45+
return $next($request->withHeader('X-token', '$token'));
46+
}
47+
48+
}
49+
)
50+
->build();
51+
52+
53+
$transport = RawPreset::create($client, RawUriBuilder::createWithAutodiscoveredPsrFactories());
54+
$request = new Request('GET', '/', [], '');
55+
$response = $transport($request);
56+
57+
echo $response;
58+
59+
60+

src/Client/ClientBuilder.php

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Phpro\HttpTools\Client;
6+
7+
use Http\Client\Common\Plugin;
8+
use Http\Client\Common\PluginClient;
9+
use Http\Client\Plugin\Vcr\NamingStrategy\NamingStrategyInterface;
10+
use Http\Client\Plugin\Vcr\Recorder\PlayerInterface;
11+
use Http\Client\Plugin\Vcr\Recorder\RecorderInterface;
12+
use Http\Client\Plugin\Vcr\RecordPlugin;
13+
use Http\Client\Plugin\Vcr\ReplayPlugin;
14+
use Http\Discovery\Psr17FactoryDiscovery;
15+
use Http\Discovery\Psr18ClientDiscovery;
16+
use Http\Message\Authentication;
17+
use Http\Message\Formatter;
18+
use Phpro\HttpTools\Client\Configurator\PluginsConfigurator;
19+
use Psr\Http\Client\ClientInterface;
20+
use Psr\Http\Message\UriInterface;
21+
use Psr\Log\LoggerInterface;
22+
use SplPriorityQueue;
23+
24+
final class ClientBuilder
25+
{
26+
public const int PRIORITY_LEVEL_DEFAULT = 0;
27+
public const int PRIORITY_LEVEL_SECURITY = 1000;
28+
public const int PRIORITY_LEVEL_LOGGING = 2000;
29+
30+
private ClientInterface $client;
31+
32+
/**
33+
* @var SplPriorityQueue<Plugin>
34+
*/
35+
private SplPriorityQueue $plugins;
36+
37+
public function __construct(
38+
?ClientInterface $client = null,
39+
iterable $middlewares = [],
40+
) {
41+
$this->client = $client ?? Psr18ClientDiscovery::find();
42+
$this->plugins = new SplPriorityQueue();
43+
44+
foreach ($middlewares as $middleware) {
45+
$this->plugins->insert($middleware, self::PRIORITY_LEVEL_DEFAULT);
46+
}
47+
}
48+
49+
public static function default(
50+
?ClientInterface $client = null,
51+
): self {
52+
return new self($client, [
53+
new Plugin\ErrorPlugin(),
54+
]);
55+
}
56+
57+
/**
58+
* @param \Closure(ClientInterface): ClientInterface $decorator
59+
*/
60+
public function addDecorator(\Closure $decorator): self
61+
{
62+
$this->client = $decorator($this->client);
63+
64+
return $this;
65+
}
66+
67+
public function addPlugin(
68+
Plugin $plugin,
69+
int $priority = self::PRIORITY_LEVEL_DEFAULT,
70+
): self {
71+
$this->plugins->insert($plugin, $priority);
72+
73+
return $this;
74+
}
75+
76+
/**
77+
* @param \Closure(ClientInterface): Plugin $lazyPlugin
78+
* @return self
79+
*/
80+
public function addPluginWithCurrentlyConfiguredClient(
81+
\Closure $pluginBuilder,
82+
int $priority = self::PRIORITY_LEVEL_DEFAULT,
83+
): self {
84+
return $this->addPlugin(
85+
$pluginBuilder($this->build()),
86+
$priority,
87+
);
88+
}
89+
90+
public function addAuthentication(
91+
Authentication $authentication,
92+
int $priority = self::PRIORITY_LEVEL_SECURITY,
93+
): self {
94+
return $this->addPlugin(new Plugin\AuthenticationPlugin($authentication), $priority);
95+
}
96+
97+
public function addLogger(
98+
LoggerInterface $logger,
99+
?Formatter $formatter = null,
100+
int $priority = self::PRIORITY_LEVEL_LOGGING,
101+
): self {
102+
return $this->addPlugin(new Plugin\LoggerPlugin($logger, $formatter), $priority);
103+
}
104+
105+
/**
106+
* @param array<string, string | string[]> $headers
107+
*
108+
* @return $this
109+
*/
110+
public function addHeaders(
111+
array $headers,
112+
int $priority = self::PRIORITY_LEVEL_DEFAULT,
113+
): self {
114+
return $this->addPlugin(new Plugin\HeaderSetPlugin($headers), $priority);
115+
}
116+
117+
public function addBaseUri(
118+
UriInterface|string $baseUri,
119+
bool $replaceHost = true,
120+
int $priority = self::PRIORITY_LEVEL_DEFAULT,
121+
): self {
122+
$baseUri = match (true) {
123+
is_string($baseUri) => Psr17FactoryDiscovery::findUriFactory()->createUri($baseUri),
124+
default => $baseUri,
125+
};
126+
127+
return $this->addPlugin(new Plugin\BaseUriPlugin($baseUri, ['replace' => $replaceHost]), $priority);
128+
}
129+
130+
public function addRecording(
131+
NamingStrategyInterface $namingStrategy,
132+
RecorderInterface&PlayerInterface $recorder,
133+
int $priority = self::PRIORITY_LEVEL_LOGGING,
134+
): self {
135+
return $this
136+
->addPlugin(new RecordPlugin($namingStrategy, $recorder), $priority)
137+
->addPlugin(new ReplayPlugin($namingStrategy, $recorder, false), $priority);
138+
}
139+
140+
public function build(): PluginClient
141+
{
142+
return PluginsConfigurator::configure($this->client, [...clone $this->plugins]);
143+
}
144+
}

src/Formatter/FormatterBuilder.php

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Phpro\HttpTools\Formatter;
6+
7+
use Closure;
8+
use Http\Message\Formatter;
9+
use Phpro\HttpTools\Formatter\Factory\BasicFormatterFactory;
10+
11+
use function Psl\Fun\pipe;
12+
13+
/**
14+
* @psalm-type Decorator = \Closure(Formatter): Formatter
15+
*/
16+
final class FormatterBuilder
17+
{
18+
private bool $debug = false;
19+
private int $maxBodyLength = 1000;
20+
21+
/**
22+
* @var list<Decorator>
23+
*/
24+
private array $decorators = [];
25+
26+
public static function default(
27+
): FormatterBuilder {
28+
return new self();
29+
}
30+
31+
public function withDebug(bool $debug = true): self
32+
{
33+
$this->debug = $debug;
34+
35+
return $this;
36+
}
37+
38+
public function withMaxBodyLength(int $maxBodyLength): self
39+
{
40+
$this->maxBodyLength = $maxBodyLength;
41+
42+
return $this;
43+
}
44+
45+
/**
46+
* @param Decorator $decorator
47+
*/
48+
public function addDecorator(Closure $decorator): self
49+
{
50+
$this->decorators[] = $decorator;
51+
52+
return $this;
53+
}
54+
55+
public function build(): Formatter
56+
{
57+
return pipe(...$this->decorators)(
58+
BasicFormatterFactory::create($this->debug, $this->maxBodyLength)
59+
);
60+
}
61+
}

0 commit comments

Comments
 (0)