guzzlehttp/command
Build higher-level web service clients on top of Guzzle by modeling operations as Commands and responses as Results. Includes a generic ServiceClient plus command middleware to map commands to PSR-7 requests and responses to structured results.
Installation:
composer require guzzlehttp/command
Add to composer.json if using Laravel’s require-dev or require section.
Basic Client Initialization:
use GuzzleHttp\Client;
use GuzzleHttp\Command\ServiceClient;
$httpClient = new Client(['base_uri' => 'https://api.example.com']);
$serviceClient = new ServiceClient(
$httpClient,
fn($command) => new Request('POST', '/', [], json_encode($command->toArray())),
fn($response) => new Result(json_decode($response->getBody(), true))
);
First Use Case: Execute a command with magic methods:
$result = $serviceClient->getUser(['id' => 1]);
dd($result['data']); // Access parsed response data
app/Providers/AppServiceProvider.php: Bind the ServiceClient to Laravel’s container.app/Http/Clients/: Create a dedicated directory for service clients (e.g., UserClient.php).config/services.php: Store API base URIs and default configs.Pattern: Group related API endpoints into dedicated clients. Example:
// app/Http/Clients/StripeClient.php
namespace App\Http\Clients;
use GuzzleHttp\Command\ServiceClient;
class StripeClient extends ServiceClient
{
public function __construct()
{
parent::__construct(
new Client(['base_uri' => config('services.stripe.base_uri')]),
// Command-to-Request transformer...
// Response-to-Result transformer...
);
}
// Magic methods for Stripe endpoints
public function createCustomer(array $args) { /* ... */ }
}
Laravel Integration:
// app/Providers/AppServiceProvider.php
public function register()
{
$this->app->singleton(StripeClient::class, fn($app) => new StripeClient());
}
Pattern: Use middleware for auth, logging, or request/response transformations. Example:
// Add auth middleware
$client->getHandlerStack()->push(function ($handler) {
return function ($command) use ($handler) {
$command['@http']['headers']['Authorization'] = 'Bearer ' . auth()->token();
return $handler($command);
};
});
Laravel-Specific:
// app/Http/Middleware/ApiAuthMiddleware.php
public function handle($request, Closure $next)
{
$client = app(StripeClient::class);
$client->getHandlerStack()->push(fn($handler) => function($command) use ($handler) {
$command['@http']['headers']['Authorization'] = 'Bearer ' . $request->bearerToken();
return $handler($command);
});
return $next($request);
}
Pattern: Use executeAsync() for non-blocking calls (e.g., background jobs).
Example:
// Dispatch a job with async command
CreateUserJob::dispatch($serviceClient->createUserAsync(['name' => 'John']));
Laravel Job:
// app/Jobs/CreateUserJob.php
public function handle()
{
$promise = $this->serviceClient->createUserAsync($this->data);
$result = $promise->wait(); // Block until resolved
// Process $result
}
Pattern: Batch API calls with executeAll() (e.g., fetching multiple users).
Example:
$commands = [
'user1' => $client->getUser(['id' => 1]),
'user2' => $client->getUser(['id' => 2]),
];
$results = $client->executeAll($commands, ['concurrency' => 5]);
foreach ($results as $key => $result) {
if ($result instanceof Result) {
// Success
} else {
// Failure: $result is the exception
}
}
Pattern: Catch CommandException for API errors.
Example:
try {
$result = $client->deleteUser(['id' => 1]);
} catch (\GuzzleHttp\Command\Exception\CommandException $e) {
if ($e->getResponse()->getStatusCode() === 404) {
// Handle not found
}
}
Laravel Exception Handler:
// app/Exceptions/Handler.php
public function render($request, Throwable $exception)
{
if ($exception instanceof \GuzzleHttp\Command\Exception\CommandException) {
return response()->json(['error' => 'API Error'], 500);
}
return parent::render($request, $exception);
}
@http Injection:
@http. Validate/reserve this key explicitly:
if (array_key_exists('@http', $input)) {
throw new \InvalidArgumentException('Forbidden key: @http');
}
Middleware Order:
Async Deadlocks:
wait() with Laravel’s sync queue workers. Use then() for callbacks:
$promise->then(fn($result) => User::create($result['data']));
Concurrency Limits:
concurrency wisely. Default (25) may overload APIs. Monitor with:
$client->executeAllAsync($commands, ['concurrency' => 3]);
Log Raw Requests/Responses:
$client->getHandlerStack()->push(
Middleware::tap(fn($request) => Log::debug('Request:', $request->toPsr()))
);
Inspect Commands:
$command = $client->getCommand('foo', ['bar' => 'baz']);
dd($command->toArray()); // Debug payload
Handle Deprecated Guzzle:
guzzlehttp/command is ^1.2.0+ for PSR-7 v2 support.Custom Results:
Result to add metadata:
class ApiResult extends Result {
public function getRateLimit() { /* ... */ }
}
Dynamic URIs:
UriTemplate for dynamic paths:
$request = new Request(
'GET',
UriTemplate::expand('/users/{id}', ['id' => $command['id']])
);
Laravel Facades:
// app/Facades/Api.php
public static function getUser($id) {
return app(StripeClient::class)->getUser(['id' => $id]);
}
Testing:
ServiceClient with Mockery:
$mock = Mockery::mock(ServiceClient::class);
$mock->shouldReceive('getUser')->andReturn(new Result(['id' => 1]));
Base URI Overrides:
@http:
$client->getUser(['id' => 1, '@http' => ['base_uri' => 'https://staging-api.com']]);
JSON vs. Form Data:
@http:
$command['@http'] = ['headers' => ['Content-Type' => 'application/x-www-form-urlencoded']];
HandlerStack vs. Guzzle Middleware:
ServiceClient’s HandlerStack.Client stack.How can I help you explore Laravel packages today?