friendship ended with social-app. php is my new best friend

title: Custom Clients description: Learn how to create and configure custom clients for different API connections#

Custom Clients#

This guide explains how to create and configure custom clients for different API connections in the Fetch HTTP package.

Creating Custom Clients#

There are several ways to create custom client instances tailored to specific APIs or use cases.

Using Factory Methods#

The simplest way to create a custom client is using the factory methods:

use Fetch\Http\ClientHandler;

// Create a client with base URI
$githubClient = ClientHandler::createWithBaseUri('https://api.github.com');

// Create a client with a custom Guzzle client
$guzzleClient = new \GuzzleHttp\Client([
    'timeout' => 60,
    'verify' => false  // Disable SSL verification (not recommended for production)
]);
$customClient = ClientHandler::createWithClient($guzzleClient);

// Create a basic client and customize it
$basicClient = ClientHandler::create()
    ->timeout(30)
    ->withHeaders([
        'User-Agent' => 'MyApp/1.0',
        'Accept' => 'application/json'
    ]);

Cloning with Options#

You can create clones of existing clients with modified options:

// Create a base client
$baseClient = ClientHandler::createWithBaseUri('https://api.example.com')
    ->withHeaders([
        'User-Agent' => 'MyApp/1.0',
        'Accept' => 'application/json'
    ]);

// Create a clone with authentication for protected endpoints
$authClient = $baseClient->withClonedOptions([
    'headers' => [
        'Authorization' => 'Bearer ' . $token
    ]
]);

// Create another clone with different timeout
$longTimeoutClient = $baseClient->withClonedOptions([
    'timeout' => 60
]);

Using Type-Safe Enums#

You can use the library's enums for type-safe client configuration:

use Fetch\Enum\Method;
use Fetch\Enum\ContentType;

// Create a client with type-safe configuration
$client = ClientHandler::create()
    ->withBody($data, ContentType::JSON)
    ->request(Method::POST, 'https://api.example.com/users');

// Configure retries with enums
use Fetch\Enum\Status;

$client = ClientHandler::create()
    ->retry(3, 100)
    ->retryStatusCodes([
        Status::TOO_MANY_REQUESTS->value,
        Status::SERVICE_UNAVAILABLE->value,
        Status::GATEWAY_TIMEOUT->value
    ])
    ->get('https://api.example.com/flaky-endpoint');

Creating API Service Classes#

For more organized code, you can create service classes that encapsulate API functionality:

class GitHubApiService
{
    private \Fetch\Http\ClientHandler $client;

    public function __construct(string $token)
    {
        $this->client = \Fetch\Http\ClientHandler::createWithBaseUri('https://api.github.com')
            ->withToken($token)
            ->withHeaders([
                'Accept' => 'application/vnd.github.v3+json',
                'User-Agent' => 'MyApp/1.0'
            ]);
    }

    public function getUser(string $username)
    {
        return $this->client->get("/users/{$username}")->json();
    }

    public function getRepositories(string $username)
    {
        return $this->client->get("/users/{$username}/repos")->json();
    }

    public function createIssue(string $owner, string $repo, array $issueData)
    {
        return $this->client->post("/repos/{$owner}/{$repo}/issues", $issueData)->json();
    }
}

// Usage
$github = new GitHubApiService('your-github-token');
$user = $github->getUser('octocat');
$repos = $github->getRepositories('octocat');

Client Configuration for Different APIs#

Different APIs often have different requirements. Here are examples for popular APIs:

REST API Client#

$restClient = ClientHandler::createWithBaseUri('https://api.example.com')
    ->withToken('your-api-token')
    ->withHeaders([
        'Accept' => 'application/json',
        'Content-Type' => 'application/json'
    ]);

GraphQL API Client#

$graphqlClient = ClientHandler::createWithBaseUri('https://api.example.com/graphql')
    ->withToken('your-api-token')
    ->withHeaders([
        'Content-Type' => 'application/json'
    ]);

// Example GraphQL query
$response = $graphqlClient->post('', [
    'query' => '
        query GetUser($id: ID!) {
            user(id: $id) {
                id
                name
                email
            }
        }
    ',
    'variables' => [
        'id' => '123'
    ]
]);

OAuth 2.0 Client#

class OAuth2Client
{
    private \Fetch\Http\ClientHandler $client;
    private string $tokenEndpoint;
    private string $clientId;
    private string $clientSecret;
    private ?string $accessToken = null;
    private ?int $expiresAt = null;

    public function __construct(
        string $baseUri,
        string $tokenEndpoint,
        string $clientId,
        string $clientSecret
    ) {
        $this->client = \Fetch\Http\ClientHandler::createWithBaseUri($baseUri);
        $this->tokenEndpoint = $tokenEndpoint;
        $this->clientId = $clientId;
        $this->clientSecret = $clientSecret;
    }

    private function ensureToken()
    {
        if ($this->accessToken === null || time() > $this->expiresAt) {
            $this->refreshToken();
        }

        return $this->accessToken;
    }

    private function refreshToken()
    {
        $response = $this->client->post($this->tokenEndpoint, [
            'grant_type' => 'client_credentials',
            'client_id' => $this->clientId,
            'client_secret' => $this->clientSecret
        ]);

        $tokenData = $response->json();
        $this->accessToken = $tokenData['access_token'];
        $this->expiresAt = time() + ($tokenData['expires_in'] - 60); // Buffer of 60 seconds
    }

    public function get(string $uri, array $query = [])
    {
        $token = $this->ensureToken();

        return $this->client
            ->withToken($token)
            ->get($uri, $query)
            ->json();
    }

    public function post(string $uri, array $data)
    {
        $token = $this->ensureToken();

        return $this->client
            ->withToken($token)
            ->post($uri, $data)
            ->json();
    }

    // Add other methods as needed
}

// Usage
$oauth2Client = new OAuth2Client(
    'https://api.example.com',
    '/oauth/token',
    'your-client-id',
    'your-client-secret'
);

$resources = $oauth2Client->get('/resources', ['type' => 'active']);

Asynchronous API Clients#

You can create asynchronous API clients using the async features:

use function async;
use function await;
use function all;

class AsyncApiClient
{
    private \Fetch\Http\ClientHandler $client;

    public function __construct(string $baseUri, string $token)
    {
        $this->client = \Fetch\Http\ClientHandler::createWithBaseUri($baseUri)
            ->withToken($token);
    }

    public function fetchUserAndPosts(int $userId)
    {
        return await(async(function() use ($userId) {
            // Execute requests in parallel
            $results = await(all([
                'user' => async(fn() => $this->client->get("/users/{$userId}")),
                'posts' => async(fn() => $this->client->get("/users/{$userId}/posts"))
            ]));

            // Process the results
            return [
                'user' => $results['user']->json(),
                'posts' => $results['posts']->json()
            ];
        }));
    }
}

// Usage
$client = new AsyncApiClient('https://api.example.com', 'your-token');
$data = $client->fetchUserAndPosts(123);
echo "User: {$data['user']['name']}, Posts: " . count($data['posts']);

Customizing Handlers with Middleware#

For advanced use cases, you can create a fully custom client with Guzzle middleware:

use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\MessageFormatter;
use Fetch\Http\ClientHandler;
use Psr\Http\Message\RequestInterface;

// Create a handler stack
$stack = HandlerStack::create();

// Add logging middleware
$logger = new \Monolog\Logger('http');
$logger->pushHandler(new \Monolog\Handler\StreamHandler('logs/http.log', \Monolog\Logger::DEBUG));

$messageFormat = "{method} {uri} HTTP/{version} {req_body} -> {code} {res_body}";
$stack->push(
    Middleware::log($logger, new MessageFormatter($messageFormat))
);

// Add custom header middleware
$stack->push(Middleware::mapRequest(function (RequestInterface $request) {
    return $request->withHeader('X-Custom-Header', 'CustomValue');
}));

// Add timing middleware
$stack->push(Middleware::mapRequest(function (RequestInterface $request) {
    return $request->withHeader('X-Request-Time', (string) time());
}));

// Create a Guzzle client with the stack
$guzzleClient = new Client([
    'handler' => $stack,
    'base_uri' => 'https://api.example.com'
]);

// Create a ClientHandler with the custom Guzzle client
$client = ClientHandler::createWithClient($guzzleClient);

// Use the client
$response = $client->get('/resources');

Global Default Client#

You can configure a global default client for all requests:

// Configure the global client
fetch_client([
    'base_uri' => 'https://api.example.com',
    'timeout' => 30,
    'headers' => [
        'User-Agent' => 'MyApp/1.0',
        'Accept' => 'application/json'
    ]
]);

// All requests will use this configuration
$response = fetch('/users');  // Uses the base_uri

Client Configuration for Testing#

For testing, you can configure a client that returns mock responses:

use Fetch\Http\ClientHandler;
use Fetch\Enum\Status;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response as GuzzleResponse;
use GuzzleHttp\Client;

// Create mock responses
$mock = new MockHandler([
    new GuzzleResponse(200, ['Content-Type' => 'application/json'], '{"id": 1, "name": "Test User"}'),
    new GuzzleResponse(404, ['Content-Type' => 'application/json'], '{"error": "Not found"}'),
    new GuzzleResponse(500, ['Content-Type' => 'application/json'], '{"error": "Server error"}')
]);

// Create a handler stack with the mock handler
$stack = HandlerStack::create($mock);

// Create a Guzzle client with the stack
$guzzleClient = new Client(['handler' => $stack]);

// Create a ClientHandler with the mock client
$client = ClientHandler::createWithClient($guzzleClient);

// First request - 200 OK
$response1 = $client->get('/users/1');
assert($response1->isOk());
assert($response1->json()['name'] === 'Test User');

// Second request - 404 Not Found
$response2 = $client->get('/users/999');
assert($response2->isNotFound());

// Third request - 500 Server Error
$response3 = $client->get('/error');
assert($response3->isServerError());

Alternatively, you can use the built-in mock response utilities:

// Mock a successful response
$mockResponse = ClientHandler::createMockResponse(
    200,
    ['Content-Type' => 'application/json'],
    '{"id": 1, "name": "Test User"}'
);

// Using Status enum
$mockResponse = ClientHandler::createMockResponse(
    Status::OK,
    ['Content-Type' => 'application/json'],
    '{"id": 1, "name": "Test User"}'
);

// Mock a JSON response directly
$mockJsonResponse = ClientHandler::createJsonResponse(
    ['id' => 2, 'name' => 'Another User'],
    Status::OK
);

Clients with Logging#

You can create clients with logging enabled:

use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Fetch\Http\ClientHandler;

// Create a logger
$logger = new Logger('api');
$logger->pushHandler(new StreamHandler('logs/api.log', Logger::INFO));

// Create a client with the logger
$client = ClientHandler::create();
$client->setLogger($logger);

// Now all requests and responses will be logged
$response = $client->get('https://api.example.com/users');

// You can also set a logger on the global client
$globalClient = fetch_client();
$globalClient->setLogger($logger);

Working with Multiple APIs#

For applications that interact with multiple APIs:

class ApiManager
{
    private array $clients = [];

    public function register(string $name, \Fetch\Http\ClientHandler $client): void
    {
        $this->clients[$name] = $client;
    }

    public function get(string $name): ?\Fetch\Http\ClientHandler
    {
        return $this->clients[$name] ?? null;
    }

    public function has(string $name): bool
    {
        return isset($this->clients[$name]);
    }
}

// Usage
$apiManager = new ApiManager();

// Register clients for different APIs
$apiManager->register('github', ClientHandler::createWithBaseUri('https://api.github.com')
    ->withToken('github-token')
    ->withHeaders(['Accept' => 'application/vnd.github.v3+json']));

$apiManager->register('stripe', ClientHandler::createWithBaseUri('https://api.stripe.com/v1')
    ->withAuth('sk_test_your_key', ''));

$apiManager->register('custom', ClientHandler::createWithBaseUri('https://api.custom.com')
    ->withToken('custom-token'));

// Use the clients
$githubUser = $apiManager->get('github')->get('/user')->json();
$stripeCustomers = $apiManager->get('stripe')->get('/customers')->json();

Dependency Injection with Clients#

For applications using dependency injection:

// Service interface
interface UserServiceInterface
{
    public function getUser(int $id): array;
    public function createUser(array $userData): array;
}

// Implementation using Fetch
class UserApiService implements UserServiceInterface
{
    private \Fetch\Http\ClientHandler $client;

    public function __construct(\Fetch\Http\ClientHandler $client)
    {
        $this->client = $client;
    }

    public function getUser(int $id): array
    {
        return $this->client->get("/users/{$id}")->json();
    }

    public function createUser(array $userData): array
    {
        return $this->client->post('/users', $userData)->json();
    }
}

// Usage with a DI container
$container->singleton(\Fetch\Http\ClientHandler::class, function () {
    return ClientHandler::createWithBaseUri('https://api.example.com')
        ->withToken('api-token')
        ->withHeaders([
            'Accept' => 'application/json',
            'User-Agent' => 'MyApp/1.0'
        ]);
});

$container->singleton(UserServiceInterface::class, UserApiService::class);

// Usage in a controller
class UserController
{
    private UserServiceInterface $userService;

    public function __construct(UserServiceInterface $userService)
    {
        $this->userService = $userService;
    }

    public function getUser(int $id)
    {
        return $this->userService->getUser($id);
    }
}

Configuring Clients from Environment Variables#

For applications using environment variables for configuration:

function createClientFromEnv(string $prefix): \Fetch\Http\ClientHandler
{
    $baseUri = getenv("{$prefix}_BASE_URI");
    $token = getenv("{$prefix}_TOKEN");
    $timeout = getenv("{$prefix}_TIMEOUT") ?: 30;

    $client = ClientHandler::createWithBaseUri($baseUri)
        ->timeout((int) $timeout);

    if ($token) {
        $client->withToken($token);
    }

    return $client;
}

// Usage
$githubClient = createClientFromEnv('GITHUB_API');
$stripeClient = createClientFromEnv('STRIPE_API');

Custom Retry Logic#

You can create a client with custom retry logic:

use Fetch\Enum\Status;

$client = ClientHandler::create()
    ->retry(3, 100)  // Basic retry configuration: 3 attempts, 100ms initial delay
    ->retryStatusCodes([
        Status::TOO_MANY_REQUESTS->value,
        Status::SERVICE_UNAVAILABLE->value,
        Status::GATEWAY_TIMEOUT->value
    ])  // Only retry these status codes
    ->retryExceptions([\GuzzleHttp\Exception\ConnectException::class]);

// Use the client
$response = $client->get('https://api.example.com/unstable-endpoint');

Extending ClientHandler#

For very specialized needs, you can extend the ClientHandler class:

class GraphQLClientHandler extends \Fetch\Http\ClientHandler
{
    /**
     * Execute a GraphQL query.
     */
    public function query(string $query, array $variables = []): array
    {
        $response = $this->post('', [
            'query' => $query,
            'variables' => $variables
        ]);

        $data = $response->json();

        if (isset($data['errors'])) {
            throw new \RuntimeException('GraphQL Error: ' . json_encode($data['errors']));
        }

        return $data['data'] ?? [];
    }

    /**
     * Execute a GraphQL mutation.
     */
    public function mutation(string $mutation, array $variables = []): array
    {
        return $this->query($mutation, $variables);
    }
}

// Usage
$graphqlClient = new GraphQLClientHandler();
$graphqlClient->baseUri('https://api.example.com/graphql')
    ->withToken('your-token');

$userData = $graphqlClient->query('
    query GetUser($id: ID!) {
        user(id: $id) {
            id
            name
            email
        }
    }
', ['id' => '123']);

Best Practices#

  1. Use Type-Safe Enums: Leverage the library's enums for type safety and better code readability.

  2. Organize by API: Create separate client instances for different APIs.

  3. Configure Once: Set up clients with all necessary options once, then reuse them.

  4. Use Dependency Injection: Inject client instances rather than creating them in methods.

  5. Abstract APIs Behind Services: Create service classes that use clients internally, exposing a domain-specific interface.

  6. Handle Authentication Properly: Implement token refresh logic for OAuth flows.

  7. Use Timeouts Appropriately: Configure timeouts based on the expected response time of each API.

  8. Log Requests and Responses: Add logging for debugging and monitoring API interactions.

  9. Use Base URIs: Always use base URIs to avoid repeating URL prefixes.

  10. Set Common Headers: Configure common headers (User-Agent, Accept, etc.) once.

  11. Error Handling: Implement consistent error handling for each client.

  12. Create Async Clients When Needed: Use async/await for operations that benefit from parallelism.

Next Steps#