Skip to content

Commit 7afd399

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

3 files changed

Lines changed: 224 additions & 0 deletions

File tree

build.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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\OutputInterface;
15+
16+
$client = ClientBuilder::default(SymfonyClientFactory::create([]))
17+
->addBaseUri('https://www.google.com')
18+
->addHeaders([
19+
'x-Foo' => 'bar',
20+
])
21+
->addAuthentication(new BasicAuth('user', 'pass'))
22+
->addLogger(
23+
new ConsoleLogger(new Symfony\Component\Console\Output\ConsoleOutput(OutputInterface::VERBOSITY_DEBUG)),
24+
FormatterBuilder::default()
25+
->withDebug(true)
26+
->withMaxBodyLength(1000)
27+
->addDecorator(static fn (Formatter $formatter) => new RemoveSensitiveHeadersFormatter($formatter, [
28+
'X-SENSITIVE-HEADER',
29+
]))
30+
->build()
31+
)
32+
33+
->build();
34+
35+
36+
$transport = RawPreset::create($client, RawUriBuilder::createWithAutodiscoveredPsrFactories());
37+
$request = new Request('GET', '/', [], '');
38+
$response = $transport($request);
39+
40+
echo $response;
41+
42+
43+

src/Client/ClientBuilder.php

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
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 readonly 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+
public function addPlugin(
58+
Plugin $plugin,
59+
int $priority = self::PRIORITY_LEVEL_DEFAULT,
60+
): self {
61+
$this->plugins->insert($plugin, $priority);
62+
63+
return $this;
64+
}
65+
66+
public function addAuthentication(
67+
Authentication $authentication,
68+
int $priority = self::PRIORITY_LEVEL_SECURITY,
69+
): self {
70+
return $this->addPlugin(new Plugin\AuthenticationPlugin($authentication), $priority);
71+
}
72+
73+
public function addLogger(
74+
LoggerInterface $logger,
75+
?Formatter $formatter = null,
76+
int $priority = self::PRIORITY_LEVEL_LOGGING,
77+
): self {
78+
return $this->addPlugin(new Plugin\LoggerPlugin($logger, $formatter), $priority);
79+
}
80+
81+
/**
82+
* @param array<string, string | string[]> $headers
83+
*
84+
* @return $this
85+
*/
86+
public function addHeaders(
87+
array $headers,
88+
int $priority = self::PRIORITY_LEVEL_DEFAULT,
89+
): self {
90+
return $this->addPlugin(new Plugin\HeaderSetPlugin($headers), $priority);
91+
}
92+
93+
public function addBaseUri(
94+
UriInterface|string $baseUri,
95+
bool $replaceHost = true,
96+
int $priority = self::PRIORITY_LEVEL_DEFAULT,
97+
): self {
98+
$baseUri = match (true) {
99+
is_string($baseUri) => Psr17FactoryDiscovery::findUriFactory()->createUri($baseUri),
100+
default => $baseUri,
101+
};
102+
103+
return $this->addPlugin(new Plugin\BaseUriPlugin($baseUri, ['replace' => $replaceHost]), $priority);
104+
}
105+
106+
public function addRecording(
107+
NamingStrategyInterface $namingStrategy,
108+
RecorderInterface&PlayerInterface $recorder,
109+
int $priority = self::PRIORITY_LEVEL_LOGGING,
110+
): self {
111+
return $this
112+
->addPlugin(new RecordPlugin($namingStrategy, $recorder), $priority)
113+
->addPlugin(new ReplayPlugin($namingStrategy, $recorder, false), $priority);
114+
}
115+
116+
public function build(): PluginClient
117+
{
118+
return PluginsConfigurator::configure($this->client, [...$this->plugins]);
119+
}
120+
}

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)