With Guzzle v7, its class GuzzleHttp\Client
became annotated as @final
as it will be a real final
class in Guzzle
v8. Extending Guzzle clients to enrich them with custom functionality or to pass configuration (e.g. API credentials) is
now discouraged and static code analysis tools like PHPStan may report this as an error. Depending on how
GuzzleHttp\Client
is extended, migration may be cumbersome. I got your back, and I'll cover some common cases in this
blog post.
In a project, we heavily extended GuzzleHttp\Client
for all our cases as we wanted to make use of dependency injection
via the client's class names. Please see the example below how our clients were implemented.
We defined our class App\Client\GithubClient
that extends GuzzleHttp\Client
without any further logic:
// src/Client/GithubClient.php
namespace App\Client;
class GithubClient extends \GuzzleHttp\Client
{
}
The client App\Client\GithubClient
is injected into the service App\Service\GithubService
:
// src/Service/GithubService.php
namespace App\Service;
class GithubService
{
public function __construct(
private readonly GithubClient $client
) {
}
}
The client App\Client\GithubClient
is configured with the API key in the project's services.yaml
file:
# src/config/services.yaml
services:
App\Client\GithubClient
class: App\Client\GithubClient
arguments:
$config:
headers:
Authorization: 'token %app.github.api_key%'
This is the most simple way a Guzzle client may be configured. A client just takes some configuration (like the
Authorization
header from the example) above, and it's ready to use. The whole class can be replaced by a simple
GuzzleHttp\Client
instance, which is configured in our services.yaml
file:
services:
guzzle.client.github
class: GuzzleHttp\Client
arguments:
$config:
headers:
Authorization: 'token %app.github.api_key%'
This requires another change! Since we are using GuzzleHttp\Client
now, autowiring the correct Guzzle client via its class name does not work anymore.
Now, we have to configure our service to use the guzzle.client.github
service explicitly:
services:
App\Service\GithubService:
class: App\Service\GithubService
arguments:
$client: '@guzzle.client.github'
Finally, we have to adjust the constructor
of our service to expect an instance \GuzzleHttp\Client
as argument:
// src/Service/GithubService.php
namespace App\Service;
class GithubService
{
public function __construct(
private readonly \GuzzleHttp\Client $client
) {
}
}
This migration is pretty easy, the key changes are:
guzzle.client.github
GuzzleHttp\Client
, as our custom client App\Client\GithubClient
became obsoleteApp\Service\GithubService
is configured to use guzzle.client.github
explicitlyIn some cases, our clients are more complex and need custom logic besides the configuration. A common use-case is passing an API key via a query parameter in the request URL, for example as required by Google Maps.
See an example of how our GoogleMapsClient
was implemented before:
// src/Client/GoogleMapsClient.php
namespace App\Client;
class GoogleMapsClient extends \GuzzleHttp\Client
{
public function __construct(array $config, string $apiKey)
{
$handlerStack = \GuzzleHttp\HandlerStack::create($config['handler'] ?? null);
$config = array_merge($config, [
'base_uri' => rtrim($config['base_uri'] ?? '', '/') . '/',
'handler' => $handlerStack,
]);
$handlerStack->unshift(\GuzzleHttp\Middleware::mapRequest(static function (\Psr\Http\Message\RequestInterface $request) use ($apiKey) {
return $request->withUri(\GuzzleHttp\Psr7\Uri::withQueryValue($request->getUri(), 'key', $apiKey));
}));
parent::__construct($config);
}
}
In this example, we create a GuzzleHttp\HandlerStack
and add a middleware to it. The middleware is responsible for adding the key
query parameter to the request.
Reminder: We're extending \GuzzleHttp\Client
which will not work anymore in the future.
To be able to re-implement our custom logic, we use service factories to create our client. In this example, we're using an invokable factory:
// src/Factory/Guzzle/GoogleMapsClientFactory.php
namespace App\Factory\Guzzle;
class GoogleMapsClientFactory
{
public function __invoke(array $config, string $apiKey): \GuzzleHttp\ClientInterface
{
$handlerStack = \GuzzleHttp\HandlerStack::create($config['handler'] ?? null);
$config = array_merge($config, [
'base_uri' => rtrim($config['base_uri'] ?? '', '/') . '/',
'handler' => $handlerStack,
]);
$handlerStack->unshift(\GuzzleHttp\Middleware::mapRequest(static function (RequestInterface $request) use ($apiKey) {
return $request->withUri(\GuzzleHttp\Psr7\Uri::withQueryValue($request->getUri(), 'key', $apiKey));
}));
return new \GuzzleHttp\Client($config);
}
}
The factory now creates an instance of GuzzleHttp\Client
, containing GuzzleHttp\HandlerStack
with the middleware
that adds the key
query parameter to the request.
We can use the created factory with the factory
option in our service definition:
services:
App\Factory\Guzzle\GoogleMapsClientFactory: ~
guzzle.client.google_maps:
class: GuzzleHttp\Client
factory: '@App\Factory\Guzzle\GoogleMapsClientFactory'
arguments:
$config:
base_uri: '%env(GOOGLE_MAPS_BASE_URI)%'
$apiKey: '%env(GOOGLE_MAPS_API_KEY_PRIVATE)%'
The client can now get passed via dependency injection:
services:
App\Service\GoogleMapsService:
class: App\Service\GoogleMapsService
arguments:
$client: '@guzzle.client.google_maps'
The key changes are:
guzzle.client.google_maps
GuzzleHttp\Client
, as our custom client App\Client\GoogleMapsClient
became obsoleteApp\Service\GoogleMapsService
is configured to use guzzle.client.google_maps
explicitlyNow, you have only one Guzzle client to rule them all and there's one step less before Guzzle v8 hits the road.
Header photo by Taylor Vick on Unsplash.