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.githubGuzzleHttp\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_mapsGuzzleHttp\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.