Skip to content

Commit 52ccb5d

Browse files
committed
feat: 🔧 Allow adapter configuration via config file.
1 parent 2da629d commit 52ccb5d

6 files changed

Lines changed: 146 additions & 12 deletions

File tree

‎.php-codesniffer/MikeBronner/ruleset.xml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@
370370
<rule ref="SlevomatCodingStandard.Files.TypeNameMatchesFileName">
371371
<properties>
372372
<property name="rootNamespaces" type="array">
373-
<element key="app" value="App" />
373+
<element key="src" value="Geocoder\Laravel" />
374374
<element key="database/factories" value="Database\Factories" />
375375
<element key="database/seeders" value="Database\Seeders" />
376376
<element key="tests" value="Tests" />

‎README.md‎

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -190,8 +190,18 @@ return [
190190
| swap in a different adapter (e.g., `Http\Client\Curl\Client` from
191191
| `php-http/curl-client`, which you would need to install separately).
192192
|
193+
| To pass constructor arguments (timeouts, proxies, client options, etc.)
194+
| use the array form `[Class => [args]]`. Arguments are forwarded to the
195+
| adapter's constructor — it's on you to match its signature. Named args
196+
| are supported: `[Class => ['timeout' => 10]]`.
197+
|
193198
| Default: LaravelHttpClient::class
194199
|
200+
| Examples:
201+
| 'adapter' => LaravelHttpClient::class,
202+
| 'adapter' => [LaravelHttpClient::class => ['timeout' => 10, 'retry' => [3, 100]]],
203+
| 'adapter' => [Http\Client\Curl\Client::class => [null, null, [CURLOPT_PROXY => '...']]],
204+
|
195205
*/
196206
'adapter' => LaravelHttpClient::class,
197207

@@ -221,14 +231,37 @@ By default we ship `Geocoder\Laravel\Http\LaravelHttpClient`, a thin PSR-18
221231
client that delegates every request to Laravel's `Http` facade. This means:
222232

223233
- `Http::fake()` and `Http::assertSent()` work in your tests with no extra setup
224-
- `Http::timeout()`, `Http::retry()`, `Http::withMiddleware()`, and any other
225-
Laravel HTTP client configuration applies to geocoder requests
226234
- One less third-party HTTP client to manage
227235

228-
If you need a different transport, set `'adapter'` in `config/geocoder.php` to
229-
any class that implements `Psr\Http\Client\ClientInterface`. To go back to the
230-
previous CURL adapter, install `php-http/curl-client` and set
231-
`'adapter' => Http\Client\Curl\Client::class`.
236+
#### Configuring Adapter Options
237+
You can pass constructor arguments to the adapter via the array form
238+
`[Class => [args]]` in `config/geocoder.php`. Arguments are forwarded directly
239+
to the adapter's constructor — it's on you to match its signature. Named
240+
arguments are supported.
241+
242+
The default `LaravelHttpClient` accepts `timeout`, `connectTimeout`, `retry`,
243+
and `options` (Guzzle transport options). These are forwarded to the
244+
underlying Laravel `PendingRequest`:
245+
```php
246+
'adapter' => [LaravelHttpClient::class => [
247+
'timeout' => 10,
248+
'connectTimeout' => 3,
249+
'retry' => [3, 100], // [times, sleepMilliseconds]
250+
'options' => ['verify' => false], // Guzzle transport options
251+
]],
252+
```
253+
254+
#### Using a Different Transport
255+
Set `'adapter'` to any class that implements `Psr\Http\Client\ClientInterface`.
256+
To go back to the previous CURL adapter, install `php-http/curl-client` and
257+
pass your CURL options the same way:
258+
```php
259+
'adapter' => [Http\Client\Curl\Client::class => [
260+
null,
261+
null,
262+
[CURLOPT_PROXY => env('CURL_PROXY'), CURLOPT_PROXYUSERPWD => env('CURL_PROXYUSERPWD')],
263+
]],
264+
```
232265

233266
### Customization
234267
If you would like to make changes to the default configuration, publish and

‎config/geocoder.php‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<?php
22

3+
declare(strict_types=1);
4+
35
use Geocoder\Laravel\Http\LaravelHttpClient;
46
use Geocoder\Provider\Chain\Chain;
57
use Geocoder\Provider\GeoPlugin\GeoPlugin;
@@ -79,8 +81,18 @@
7981
| swap in a different adapter (e.g., `Http\Client\Curl\Client` from
8082
| `php-http/curl-client`, which you would need to install separately).
8183
|
84+
| To pass constructor arguments (timeouts, proxies, client options, etc.)
85+
| use the array form `[Class => [args]]`. Arguments are forwarded to the
86+
| adapter's constructor — it's on you to match its signature. Named args
87+
| are supported: `[Class => ['timeout' => 10]]`.
88+
|
8289
| Default: LaravelHttpClient::class
8390
|
91+
| Examples:
92+
| 'adapter' => LaravelHttpClient::class,
93+
| 'adapter' => [LaravelHttpClient::class => ['timeout' => 10, 'retry' => [3, 100]]],
94+
| 'adapter' => [Http\Client\Curl\Client::class => [null, null, [CURLOPT_PROXY => '...']]],
95+
|
8496
*/
8597
'adapter' => LaravelHttpClient::class,
8698

‎src/Http/LaravelHttpClient.php‎

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<?php
22

3+
declare(strict_types=1);
4+
35
namespace Geocoder\Laravel\Http;
46

57
use Illuminate\Support\Facades\Http;
@@ -9,6 +11,14 @@
911

1012
class LaravelHttpClient implements ClientInterface
1113
{
14+
public function __construct(
15+
public ?int $timeout = null,
16+
public ?int $connectTimeout = null,
17+
public ?array $retry = null,
18+
public array $options = [],
19+
) {}
20+
21+
// phpcs:ignore SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh,SlevomatCodingStandard.Functions.FunctionLength.FunctionLength
1222
public function sendRequest(RequestInterface $request): ResponseInterface
1323
{
1424
$headers = [];
@@ -20,8 +30,27 @@ public function sendRequest(RequestInterface $request): ResponseInterface
2030
$body = (string) $request->getBody();
2131
$pending = Http::withHeaders($headers);
2232

33+
if ($this->timeout !== null) {
34+
$pending = $pending->timeout($this->timeout);
35+
}
36+
37+
if ($this->connectTimeout !== null) {
38+
$pending = $pending->connectTimeout($this->connectTimeout);
39+
}
40+
41+
if ($this->retry !== null) {
42+
$pending = $pending->retry(...$this->retry);
43+
}
44+
45+
if ($this->options !== []) {
46+
$pending = $pending->withOptions($this->options);
47+
}
48+
2349
if ($body !== '') {
24-
$pending = $pending->withBody($body, $request->getHeaderLine('Content-Type') ?: 'application/octet-stream');
50+
$pending = $pending->withBody(
51+
$body,
52+
$request->getHeaderLine('Content-Type') ?: 'application/octet-stream',
53+
);
2554
}
2655

2756
return $pending

‎src/ProviderAndDumperAggregator.php‎

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,26 @@ protected function getAdapterClass(string $provider) : string
233233
return $specificAdapters->get($provider);
234234
}
235235

236-
return config('geocoder.adapter');
236+
$adapter = config('geocoder.adapter');
237+
238+
if (is_array($adapter)) {
239+
return array_key_first($adapter);
240+
}
241+
242+
return $adapter;
243+
}
244+
245+
protected function buildAdapter(string $class)
246+
{
247+
$adapterConfig = config('geocoder.adapter');
248+
249+
if (is_array($adapterConfig) && isset($adapterConfig[$class])) {
250+
$reflection = new ReflectionClass($class);
251+
252+
return $reflection->newInstanceArgs($adapterConfig[$class]);
253+
}
254+
255+
return app($class);
237256
}
238257

239258
protected function getReader()
@@ -264,7 +283,7 @@ protected function getArguments(array $arguments, string $provider) : array
264283
if ($this->requiresReader($provider)) {
265284
$adapter = new $adapter($this->getReader());
266285
} else {
267-
$adapter = app($adapter);
286+
$adapter = $this->buildAdapter($adapter);
268287
}
269288

270289
array_unshift($arguments, $adapter);

‎tests/Feature/Providers/GeocoderServiceTest.php‎

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,14 +194,17 @@
194194
});
195195

196196
it('does not cache empty results', function () {
197+
Http::swap(new \Illuminate\Http\Client\Factory());
197198
Http::fake([
198199
'nominatim.openstreetmap.org/search*' => Http::response([]),
199200
]);
200-
$cacheKey = md5(Str::slug(strtolower(urlencode('_'))));
201+
$providerName = app('geocoder')->getProvider()->getName();
202+
$hashedCacheKey = sha1("{$providerName}-" . Str::slug(strtolower(urlencode('_'))));
201203

202204
Geocoder::geocode('_')->get();
203205

204-
expect(app('cache')->has("geocoder-{$cacheKey}"))->toBeFalse();
206+
$store = app('cache')->store(config('geocoder.cache.store'));
207+
expect($store->has($hashedCacheKey))->toBeFalse();
205208
});
206209

207210
it('can disable caching', function () {
@@ -231,6 +234,44 @@
231234
expect($provider->getName())->toBe('nominatim');
232235
});
233236

237+
it('accepts configuration options on the default adapter', function () {
238+
$client = new LaravelHttpClient(
239+
timeout: 7,
240+
connectTimeout: 3,
241+
retry: [2, 50],
242+
options: ['verify' => false],
243+
);
244+
245+
expect($client->timeout)->toBe(7);
246+
expect($client->connectTimeout)->toBe(3);
247+
expect($client->retry)->toBe([2, 50]);
248+
expect($client->options)->toBe(['verify' => false]);
249+
});
250+
251+
it('defaults to null adapter options when none are configured', function () {
252+
$client = new LaravelHttpClient();
253+
254+
expect($client->timeout)->toBeNull();
255+
expect($client->connectTimeout)->toBeNull();
256+
expect($client->retry)->toBeNull();
257+
expect($client->options)->toBe([]);
258+
});
259+
260+
it('resolves array-form adapter config with constructor arguments', function () {
261+
config()->set('geocoder.adapter', [
262+
LaravelHttpClient::class => ['timeout' => 15, 'retry' => [2, 100]],
263+
]);
264+
265+
$result = app('geocoder')
266+
->using('nominatim')
267+
->geocode('1600 Pennsylvania Ave NW, Washington, DC 20500, USA')
268+
->get()
269+
->first();
270+
271+
expect($result)->not->toBeNull();
272+
expect($result->getStreetNumber())->toBe('1600');
273+
});
274+
234275
it('does not collide reverse cache keys across coordinate signs', function () {
235276
$providerName = app('geocoder')->getProvider()->getName();
236277
$negativeKey = sha1("{$providerName}-" . strtolower(urlencode('-45.473282--73.834721')));

0 commit comments

Comments
 (0)