Skip to content

Commit a2a74e5

Browse files
Merge pull request #6 from tomsommer/psr-http-client
Allow injecting a PSR-18 HTTP client and PSR-16 cache
2 parents 9e9bc33 + b4c320f commit a2a74e5

12 files changed

Lines changed: 284 additions & 13 deletions

‎README.md‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,28 @@ $response = $newRequest->getResponse();
109109

110110
See [Response/Definitions](src/Response/Definitions) for more details about code
111111

112+
### Using your own HTTP client and cache
113+
114+
By default, requests are made with `file_get_contents()` and the IANA bootstrap files are cached in the system temporary directory.
115+
116+
The client optionally accepts a [PSR-18](https://www.php-fig.org/psr/psr-18/) HTTP client, a [PSR-17](https://www.php-fig.org/psr/psr-17/) request factory and a [PSR-16](https://www.php-fig.org/psr/psr-16/) cache. The HTTP client is used for both the bootstrap files and the RDAP queries, so timeouts, proxies, retries and logging follow your application's configuration. The cache stores the bootstrap files for `AbstractRdapService::cacheExpirations()` seconds.
117+
118+
```php
119+
use ArrayAccess\RdapClient\Client;
120+
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
121+
use Symfony\Component\Cache\Psr16Cache;
122+
use Symfony\Component\HttpClient\HttpClient;
123+
use Symfony\Component\HttpClient\Psr18Client;
124+
125+
// Psr18Client is also a request factory, so the second argument can be omitted
126+
$httpClient = new Psr18Client(HttpClient::create(['timeout' => 5]));
127+
$cache = new Psr16Cache(new FilesystemAdapter());
128+
$client = new Client($httpClient, null, $cache);
129+
130+
// any PSR-18 client works, e.g. Guzzle with a PSR-17 factory
131+
$client = new Client(new \GuzzleHttp\Client(['timeout' => 5]), new \GuzzleHttp\Psr7\HttpFactory());
132+
```
133+
112134

113135
## Note
114136

‎composer.json‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@
2929
"require": {
3030
"php": ">= 8.1",
3131
"ext-intl": "*",
32-
"ext-json": "*"
32+
"ext-json": "*",
33+
"psr/http-client": "^1.0",
34+
"psr/http-factory": "^1.0",
35+
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
3336
},
3437
"require-dev": {
3538
"squizlabs/php_codesniffer": "^3.11.2",
@@ -46,4 +49,4 @@
4649
"ArrayAccess\\RdapClient\\" : "src/"
4750
}
4851
}
49-
}
52+
}

‎src/Client.php‎

Lines changed: 139 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,43 @@
55

66
use ArrayAccess\RdapClient\Exceptions\EmptyArgumentException;
77
use ArrayAccess\RdapClient\Exceptions\InvalidServiceDefinitionException;
8+
use ArrayAccess\RdapClient\Exceptions\RdapRemoteRequestException;
89
use ArrayAccess\RdapClient\Exceptions\UnsupportedProtocolException;
910
use ArrayAccess\RdapClient\Interfaces\RdapClientInterface;
11+
use ArrayAccess\RdapClient\Interfaces\RdapHttpClientAwareInterface;
1012
use ArrayAccess\RdapClient\Interfaces\RdapProtocolInterface;
13+
use ArrayAccess\RdapClient\Interfaces\RdapRequestInterface;
1114
use ArrayAccess\RdapClient\Protocols\AsnProtocol;
1215
use ArrayAccess\RdapClient\Protocols\DomainProtocol;
1316
use ArrayAccess\RdapClient\Protocols\IPv4Protocol;
1417
use ArrayAccess\RdapClient\Protocols\IPv6Protocol;
1518
use ArrayAccess\RdapClient\Protocols\NsProtocol;
1619
use ArrayAccess\RdapClient\Services\AsnService;
1720
use ArrayAccess\RdapClient\Util\CIDR;
21+
use InvalidArgumentException;
22+
use Psr\Http\Client\ClientExceptionInterface;
23+
use Psr\Http\Client\ClientInterface;
24+
use Psr\Http\Message\RequestFactoryInterface;
25+
use Psr\SimpleCache\CacheInterface;
1826
use function explode;
27+
use function file_get_contents;
1928
use function idn_to_ascii;
2029
use function is_a;
2130
use function is_array;
2231
use function is_int;
2332
use function is_object;
33+
use function is_string;
2434
use function preg_match;
35+
use function restore_error_handler;
36+
use function set_error_handler;
2537
use function sprintf;
2638
use function str_contains;
39+
use function stream_context_create;
2740
use function strlen;
2841
use function strtolower;
2942
use function trim;
3043

31-
class Client implements RdapClientInterface
44+
class Client implements RdapClientInterface, RdapHttpClientAwareInterface
3245
{
3346
/**
3447
* @var string Version
@@ -51,6 +64,131 @@ class Client implements RdapClientInterface
5164
*/
5265
protected array $protocols = self::PROTOCOLS;
5366

67+
/**
68+
* @var RequestFactoryInterface|null $requestFactory The PSR-17 request factory
69+
*/
70+
protected ?RequestFactoryInterface $requestFactory;
71+
72+
/**
73+
* Constructor
74+
*
75+
* Without arguments, requests are made with file_get_contents() and the
76+
* IANA bootstrap files are cached in the system temporary directory.
77+
*
78+
* @param ClientInterface|null $httpClient PSR-18 client used for bootstrap and RDAP requests
79+
* @param RequestFactoryInterface|null $requestFactory PSR-17 request factory,
80+
* optional when the HTTP client also implements RequestFactoryInterface
81+
* @param CacheInterface|null $cache PSR-16 cache for the IANA bootstrap files
82+
* @throws InvalidArgumentException if an HTTP client is given without a request factory
83+
*/
84+
public function __construct(
85+
protected ?ClientInterface $httpClient = null,
86+
?RequestFactoryInterface $requestFactory = null,
87+
protected ?CacheInterface $cache = null
88+
) {
89+
if ($requestFactory === null && $httpClient instanceof RequestFactoryInterface) {
90+
$requestFactory = $httpClient;
91+
}
92+
if ($httpClient !== null && $requestFactory === null) {
93+
throw new InvalidArgumentException(
94+
'A PSR-17 request factory is required when using a PSR-18 HTTP client'
95+
);
96+
}
97+
$this->requestFactory = $requestFactory;
98+
}
99+
100+
/**
101+
* @inheritDoc
102+
*/
103+
public function getHttpClient() : ?ClientInterface
104+
{
105+
return $this->httpClient;
106+
}
107+
108+
/**
109+
* Get the PSR-17 request factory
110+
*
111+
* @return RequestFactoryInterface|null
112+
*/
113+
public function getRequestFactory() : ?RequestFactoryInterface
114+
{
115+
return $this->requestFactory;
116+
}
117+
118+
/**
119+
* @inheritDoc
120+
*/
121+
public function getCache() : ?CacheInterface
122+
{
123+
return $this->cache;
124+
}
125+
126+
/**
127+
* @inheritDoc
128+
*/
129+
public function fetch(string $url) : string
130+
{
131+
if ($this->httpClient === null || $this->requestFactory === null) {
132+
return $this->fetchWithStream($url);
133+
}
134+
135+
$request = $this->requestFactory
136+
->createRequest('GET', $url)
137+
->withHeader('Accept', 'application/rdap+json,application/json;q=0.9,*/*;q=0.8')
138+
->withHeader('User-Agent', 'Rdap-Client/' . self::VERSION);
139+
try {
140+
$response = $this->httpClient->sendRequest($request);
141+
} catch (ClientExceptionInterface $e) {
142+
throw new RdapRemoteRequestException(
143+
$e->getMessage(),
144+
(int) $e->getCode(),
145+
$e
146+
);
147+
}
148+
149+
$content = (string) $response->getBody();
150+
if ($content === '') {
151+
throw new RdapRemoteRequestException(
152+
sprintf('Could not get RDAP response from %s', $url),
153+
$response->getStatusCode()
154+
);
155+
}
156+
return $content;
157+
}
158+
159+
/**
160+
* Fetch the body of the given URL with file_get_contents()
161+
*
162+
* @param string $url
163+
* @return string
164+
* @throws RdapRemoteRequestException
165+
*/
166+
protected function fetchWithStream(string $url) : string
167+
{
168+
$errorCode = 0;
169+
$errorMessage = null;
170+
set_error_handler(
171+
static function (int $code, string $message) use (&$errorCode, &$errorMessage) : bool {
172+
$errorCode = $code;
173+
$errorMessage = $message;
174+
return true;
175+
}
176+
);
177+
$content = file_get_contents(
178+
$url,
179+
false,
180+
stream_context_create(RdapRequestInterface::DEFAULT_STREAM_CONTEXT)
181+
);
182+
restore_error_handler();
183+
if (!is_string($content) || $content === '') {
184+
throw new RdapRemoteRequestException(
185+
$errorMessage ?? sprintf('Could not get RDAP response from %s', $url),
186+
$errorCode
187+
);
188+
}
189+
return $content;
190+
}
191+
54192
/**
55193
* Check if protocol is supported
56194
*
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace ArrayAccess\RdapClient\Interfaces;
5+
6+
use Psr\Http\Client\ClientInterface;
7+
use Psr\SimpleCache\CacheInterface;
8+
9+
interface RdapHttpClientAwareInterface
10+
{
11+
/**
12+
* Get the PSR-18 HTTP client
13+
*
14+
* @return ClientInterface|null null when the built-in stream wrapper is used
15+
*/
16+
public function getHttpClient() : ?ClientInterface;
17+
18+
/**
19+
* Get the PSR-16 cache used for the IANA bootstrap files
20+
*
21+
* @return CacheInterface|null null when the built-in temporary file cache is used
22+
*/
23+
public function getCache() : ?CacheInterface;
24+
25+
/**
26+
* Fetch the body of the given URL
27+
*
28+
* The body is returned regardless of the HTTP status code,
29+
* so RDAP error responses (RFC 9083 section 6) can be parsed.
30+
*
31+
* @param string $url
32+
* @return string
33+
* @throws \ArrayAccess\RdapClient\Exceptions\RdapRemoteRequestException
34+
*/
35+
public function fetch(string $url) : string;
36+
}

‎src/Protocols/AbstractRdapProtocol.php‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,17 @@
33

44
namespace ArrayAccess\RdapClient\Protocols;
55

6+
use ArrayAccess\RdapClient\Exceptions\InvalidServiceDefinitionException;
67
use ArrayAccess\RdapClient\Exceptions\RdapServerNotFoundException;
78
use ArrayAccess\RdapClient\Interfaces\RdapClientInterface;
9+
use ArrayAccess\RdapClient\Interfaces\RdapHttpClientAwareInterface;
810
use ArrayAccess\RdapClient\Interfaces\RdapProtocolInterface;
911
use ArrayAccess\RdapClient\Interfaces\RdapRequestInterface;
1012
use ArrayAccess\RdapClient\Interfaces\RdapServiceInterface;
13+
use ArrayAccess\RdapClient\Services\AbstractRdapService;
1114
use function explode;
15+
use function is_string;
16+
use function md5;
1217
use function sprintf;
1318
use function str_contains;
1419

@@ -39,6 +44,45 @@ public function getClient(): RdapClientInterface
3944
return $this->client;
4045
}
4146

47+
/**
48+
* Load the service from an IANA bootstrap URL
49+
*
50+
* Uses the PSR-18 HTTP client and PSR-16 cache of the client when configured,
51+
* otherwise falls back to {@see AbstractRdapService::fromURL()}.
52+
*
53+
* @template T of AbstractRdapService
54+
* @param class-string<T> $serviceClass
55+
* @param string $url
56+
* @return T
57+
* @throws \Exception
58+
* @noinspection PhpFullyQualifiedNameUsageInspection
59+
*/
60+
protected function loadService(string $serviceClass, string $url): AbstractRdapService
61+
{
62+
$client = $this->getClient();
63+
if (!$client instanceof RdapHttpClientAwareInterface
64+
|| ($client->getHttpClient() === null && $client->getCache() === null)
65+
) {
66+
return $serviceClass::fromURL($url);
67+
}
68+
69+
$cache = $client->getCache();
70+
$cacheKey = 'rdap-client.bootstrap.' . md5($url);
71+
$content = $cache?->get($cacheKey);
72+
if (is_string($content)) {
73+
try {
74+
return $serviceClass::fromJson($content, $url);
75+
} catch (InvalidServiceDefinitionException) {
76+
$cache?->delete($cacheKey);
77+
}
78+
}
79+
80+
$content = $client->fetch($url);
81+
$service = $serviceClass::fromJson($content, $url);
82+
$cache?->set($cacheKey, $content, $serviceClass::cacheExpirations());
83+
return $service;
84+
}
85+
4286
/**
4387
* @inheritDoc
4488
*/

‎src/Protocols/AsnProtocol.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public function __construct(RdapClientInterface $client, ?AsnService $service =
3636
public function getService(): AsnService
3737
{
3838
if (!isset($this->services) || !$this->services instanceof AsnService) {
39-
$this->services = AsnService::fromURL(self::ASN_URI);
39+
$this->services = $this->loadService(AsnService::class, self::ASN_URI);
4040
}
4141
return $this->services;
4242
}

‎src/Protocols/DomainProtocol.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public function setService(DomainService $service): void
4545
public function getService(): DomainService
4646
{
4747
if (!isset($this->services) || !($this->services instanceof DomainService)) {
48-
$this->services = DomainService::fromURL(self::DOMAIN_URI);
48+
$this->services = $this->loadService(DomainService::class, self::DOMAIN_URI);
4949
}
5050
return $this->services;
5151
}

‎src/Protocols/IPv4Protocol.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public function setService(Ipv4Service $service): void
4747
public function getService(): Ipv4Service
4848
{
4949
if (!isset($this->services) || !($this->services instanceof Ipv4Service)) {
50-
$this->services = Ipv4Service::fromURL(self::IPV4_URI);
50+
$this->services = $this->loadService(Ipv4Service::class, self::IPV4_URI);
5151
}
5252
return $this->services;
5353
}

‎src/Protocols/IPv6Protocol.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public function setService(Ipv6Service $service): void
4747
public function getService(): Ipv6Service
4848
{
4949
if (!isset($this->services) || !($this->services instanceof Ipv6Service)) {
50-
$this->services = Ipv6Service::fromURL(self::IPV6_URI);
50+
$this->services = $this->loadService(Ipv6Service::class, self::IPV6_URI);
5151
}
5252
return $this->services;
5353
}

‎src/Protocols/NsProtocol.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public function setService(NsService $service): void
4646
public function getService(): NsService
4747
{
4848
if (!isset($this->services) || !($this->services instanceof NsService)) {
49-
$this->services = NsService::fromURL(self::NS_URI);
49+
$this->services = $this->loadService(NsService::class, self::NS_URI);
5050
}
5151
return $this->services;
5252
}

0 commit comments

Comments
 (0)