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#
-
Use Type-Safe Enums: Leverage the library's enums for type safety and better code readability.
-
Organize by API: Create separate client instances for different APIs.
-
Configure Once: Set up clients with all necessary options once, then reuse them.
-
Use Dependency Injection: Inject client instances rather than creating them in methods.
-
Abstract APIs Behind Services: Create service classes that use clients internally, exposing a domain-specific interface.
-
Handle Authentication Properly: Implement token refresh logic for OAuth flows.
-
Use Timeouts Appropriately: Configure timeouts based on the expected response time of each API.
-
Log Requests and Responses: Add logging for debugging and monitoring API interactions.
-
Use Base URIs: Always use base URIs to avoid repeating URL prefixes.
-
Set Common Headers: Configure common headers (User-Agent, Accept, etc.) once.
-
Error Handling: Implement consistent error handling for each client.
-
Create Async Clients When Needed: Use async/await for operations that benefit from parallelism.
Next Steps#
- Learn about Testing for testing with custom clients
- Explore Asynchronous Requests for working with async clients
- See Authentication for handling different authentication schemes
- Check out Working with Responses for handling API responses