guzzlehttp/guzzle-services
Guzzle Services adds a command layer on top of Guzzle using service descriptions to define operations, serialize requests, and parse responses into convenient model structures. Build typed clients from descriptions, call operations as methods, and get structured results.
composer require guzzlehttp/guzzle-services
config/api-descriptions.php):
return [
'baseUri' => env('API_BASE_URL'),
'operations' => [
'getUser' => [
'httpMethod' => 'GET',
'uri' => '/users/{id}',
'parameters' => [
'id' => ['type' => 'integer', 'location' => 'uri'],
],
'responseModel' => 'userResponse',
],
],
'models' => [
'userResponse' => [
'type' => 'object',
'properties' => [
'id' => ['type' => 'integer'],
'name' => ['type' => 'string'],
],
],
],
];
app/Services/UserService.php):
use GuzzleHttp\Client;
use GuzzleHttp\Command\Guzzle\GuzzleClient;
use GuzzleHttp\Command\Guzzle\Description;
class UserService
{
public function __construct()
{
$client = new Client();
$description = new Description(config('api-descriptions'));
$this->guzzleClient = new GuzzleClient($client, $description);
}
public function fetchUser(int $id)
{
return $this->guzzleClient->getUser(['id' => $id]);
}
}
use App\Services\UserService;
class UserController extends Controller
{
public function show(UserService $userService, int $id)
{
$user = $userService->fetchUser($id);
return response()->json($user);
}
}
Leverage the package to create a type-safe wrapper for a third-party API (e.g., Stripe, Twilio) with:
id is an integer).php artisan commands).config/api-descriptions/stripe.php, config/api-descriptions/payment-gateway.php).'models' => [
'paymentResponse' => [
'type' => 'object',
'properties' => [
'status' => ['type' => 'string'],
'amount' => ['type' => 'number'],
],
],
],
'operations' => [
'createPayment' => [
'responseModel' => 'paymentResponse',
// ...
],
'refundPayment' => [
'responseModel' => 'paymentResponse',
// ...
],
],
$description = new Description([
'baseUri' => env('API_BASE_URL'),
'operations' => $this->loadOperationsFromDatabase(),
'models' => $this->loadModelsFromOpenApi(),
]);
$this->app->bind(UserService::class, function ($app) {
$client = new Client(['base_uri' => env('API_BASE_URL')]);
$description = new Description(config('api-descriptions.users'));
return new UserService(new GuzzleClient($client, $description));
});
$client = new Client([
'base_uri' => env('API_BASE_URL'),
'middleware' => [
new \GuzzleHttp\Middleware::mapRequest(function ($request) {
$request = $request->withHeader('Authorization', 'Bearer ' . auth()->token());
return $request;
}),
],
]);
$description = new Description(config('api-descriptions'));
cache(['api_descriptions' => $description], now()->addHours(1));
public function fetchUsers(UserService $userService)
{
$users = collect($userService->listUsers())->map(function ($user) {
return new UserResource($user);
});
return $users;
}
try {
$user = $userService->fetchUser($id);
} catch (\GuzzleHttp\Exception\RequestException $e) {
if ($e->hasResponse()) {
$response = json_decode($e->getResponse()->getBody(), true);
throw new \RuntimeException($response['error'] ?? 'API request failed');
}
throw new \RuntimeException('API unavailable');
}
use GuzzleHttp\Command\Guzzle\GuzzleClient;
use Illuminate\Console\Command;
class TestApiCommand extends Command
{
protected $signature = 'api:test {operation} {--params= : JSON-encoded parameters}';
protected $description = 'Test an API operation';
public function handle(GuzzleClient $client)
{
$params = json_decode($this->option('params'), true);
$result = $client->{$this->argument('operation')}($params);
$this->info('Response: ' . print_r($result, true));
}
}
Console/Kernel.php:
protected $commands = [
\App\Console\Commands\TestApiCommand::class,
];
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
public function testFetchUser()
{
$mock = new MockHandler([
new Response(200, [], json_encode(['id' => 1, 'name' => 'John'])),
]);
$handlerStack = HandlerStack::create($mock);
$client = new Client(['handler' => $handlerStack]);
$service = new UserService($client, $description);
$user = $service->fetchUser(1);
$this->assertEquals('John', $user['name']);
}
public function testUserEndpoint()
{
$response = $this->getJson("/users/1");
$response->assertOk()->assertJson(['name' => 'John']);
}
Parameter Validation Overhead:
null for optional fields), you may need to adjust the description:
'parameters' => [
'optionalField' => [
'type' => 'string',
'location' => 'query',
'required' => false, // Explicitly mark as optional
],
],
additionalParameters for dynamic or non-validated fields:
'parameters' => [
'dynamic' => [
'type' => 'object',
'location' => 'query',
'additionalParameters' => true,
],
],
URI Template Confusion:
/users/{id}) must match exactly. A typo in the template or parameter name will cause 404s.gimler/guzzle-description-loader plugin to load descriptions from OpenAPI/Swagger, which reduces manual errors.Response Model Mismatches:
additionalProperties to handle unexpected fields:
'models' => [
'userResponse' => [
'type' => 'object',
'properties' => [...],
'additionalProperties' => true, // Allow extra fields
],
],
Multipart Requests:
multipart for file uploads, but some APIs expect form-data. Ensure your description matches the API’s expectations:
'parameters' => [
'file' => [
'type' => 'file',
'location' => 'multipart', // Not 'formParam'
How can I help you explore Laravel packages today?