Weave Code
Code Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Guzzle Services Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. Install the package:
    composer require guzzlehttp/guzzle-services
    
  2. Define a service description (e.g., 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'],
                ],
            ],
        ],
    ];
    
  3. Create a service client (e.g., 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]);
        }
    }
    
  4. Use the service in a Laravel controller:
    use App\Services\UserService;
    
    class UserController extends Controller
    {
        public function show(UserService $userService, int $id)
        {
            $user = $userService->fetchUser($id);
            return response()->json($user);
        }
    }
    

First Use Case: API Wrapper for Third-Party Service

Leverage the package to create a type-safe wrapper for a third-party API (e.g., Stripe, Twilio) with:

  • Automatic request validation (e.g., ensure id is an integer).
  • Structured responses (e.g., map JSON to PHP objects).
  • CLI testing (e.g., debug endpoints via php artisan commands).

Implementation Patterns

1. Service Description Organization

  • Modularize descriptions by API domain (e.g., config/api-descriptions/stripe.php, config/api-descriptions/payment-gateway.php).
  • Reuse models across operations:
    'models' => [
        'paymentResponse' => [
            'type' => 'object',
            'properties' => [
                'status' => ['type' => 'string'],
                'amount' => ['type' => 'number'],
            ],
        ],
    ],
    'operations' => [
        'createPayment' => [
            'responseModel' => 'paymentResponse',
            // ...
        ],
        'refundPayment' => [
            'responseModel' => 'paymentResponse',
            // ...
        ],
    ],
    
  • Extend descriptions dynamically (e.g., load from a database or OpenAPI spec):
    $description = new Description([
        'baseUri' => env('API_BASE_URL'),
        'operations' => $this->loadOperationsFromDatabase(),
        'models' => $this->loadModelsFromOpenApi(),
    ]);
    

2. Laravel Integration Patterns

  • Bind services to the container:
    $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));
    });
    
  • Use middleware for cross-cutting concerns (e.g., auth, logging):
    $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;
            }),
        ],
    ]);
    
  • Leverage Laravel’s config caching:
    $description = new Description(config('api-descriptions'));
    cache(['api_descriptions' => $description], now()->addHours(1));
    

3. Response Handling

  • Transform responses to Laravel collections/resources:
    public function fetchUsers(UserService $userService)
    {
        $users = collect($userService->listUsers())->map(function ($user) {
            return new UserResource($user);
        });
        return $users;
    }
    
  • Handle errors gracefully:
    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');
    }
    

4. CLI and Artisan Commands

  • Create a command for API testing:
    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));
        }
    }
    
  • Register the command in Console/Kernel.php:
    protected $commands = [
        \App\Console\Commands\TestApiCommand::class,
    ];
    

5. Testing Strategies

  • Mock the Guzzle client in unit tests:
    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']);
    }
    
  • Use Laravel’s HTTP tests for integration testing:
    public function testUserEndpoint()
    {
        $response = $this->getJson("/users/1");
        $response->assertOk()->assertJson(['name' => 'John']);
    }
    

Gotchas and Tips

Pitfalls

  1. Parameter Validation Overhead:

    • The package validates parameters strictly by default. If an API is lenient (e.g., accepts null for optional fields), you may need to adjust the description:
      'parameters' => [
          'optionalField' => [
              'type' => 'string',
              'location' => 'query',
              'required' => false, // Explicitly mark as optional
          ],
      ],
      
    • Tip: Use additionalParameters for dynamic or non-validated fields:
      'parameters' => [
          'dynamic' => [
              'type' => 'object',
              'location' => 'query',
              'additionalParameters' => true,
          ],
      ],
      
  2. URI Template Confusion:

    • URI templates (e.g., /users/{id}) must match exactly. A typo in the template or parameter name will cause 404s.
    • Tip: Use the gimler/guzzle-description-loader plugin to load descriptions from OpenAPI/Swagger, which reduces manual errors.
  3. Response Model Mismatches:

    • If the API response structure changes (e.g., a field is renamed), the package will throw errors. Tip: Use additionalProperties to handle unexpected fields:
      'models' => [
          'userResponse' => [
              'type' => 'object',
              'properties' => [...],
              'additionalProperties' => true, // Allow extra fields
          ],
      ],
      
  4. Multipart Requests:

    • The package uses 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'
      
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
directorytree/privacy-filter-classifier
directorytree/privacy-filter
datacore/hub-sdk
develia/commons
cuci/prototurk-sdk
cuci/prototurk-sdk-symfony
develia/geo-bundle
dreamzy/livewire-charts
touchestate-sdk/php-sdk
22h/doctrine-garbage-collection-bundle
agtp/agtp-php
agtp/mod-php
splash/sonata-admin
splash/metadata
splash/openapi
splash/scopes
splash/toolkit
testo/output-teamcity
testo/bridge-symfony
spatie/flare-daemon-runtime