friendship ended with social-app. php is my new best friend
1---
2title: Custom Clients
3description: Learn how to create and configure custom clients for different API connections
4---
5
6# Custom Clients
7
8This guide explains how to create and configure custom clients for different API connections in the Fetch HTTP package.
9
10## Creating Custom Clients
11
12There are several ways to create custom client instances tailored to specific APIs or use cases.
13
14### Using Factory Methods
15
16The simplest way to create a custom client is using the factory methods:
17
18```php
19use Fetch\Http\ClientHandler;
20
21// Create a client with base URI
22$githubClient = ClientHandler::createWithBaseUri('https://api.github.com');
23
24// Create a client with a custom Guzzle client
25$guzzleClient = new \GuzzleHttp\Client([
26 'timeout' => 60,
27 'verify' => false // Disable SSL verification (not recommended for production)
28]);
29$customClient = ClientHandler::createWithClient($guzzleClient);
30
31// Create a basic client and customize it
32$basicClient = ClientHandler::create()
33 ->timeout(30)
34 ->withHeaders([
35 'User-Agent' => 'MyApp/1.0',
36 'Accept' => 'application/json'
37 ]);
38```
39
40### Cloning with Options
41
42You can create clones of existing clients with modified options:
43
44```php
45// Create a base client
46$baseClient = ClientHandler::createWithBaseUri('https://api.example.com')
47 ->withHeaders([
48 'User-Agent' => 'MyApp/1.0',
49 'Accept' => 'application/json'
50 ]);
51
52// Create a clone with authentication for protected endpoints
53$authClient = $baseClient->withClonedOptions([
54 'headers' => [
55 'Authorization' => 'Bearer ' . $token
56 ]
57]);
58
59// Create another clone with different timeout
60$longTimeoutClient = $baseClient->withClonedOptions([
61 'timeout' => 60
62]);
63```
64
65## Using Type-Safe Enums
66
67You can use the library's enums for type-safe client configuration:
68
69```php
70use Fetch\Enum\Method;
71use Fetch\Enum\ContentType;
72
73// Create a client with type-safe configuration
74$client = ClientHandler::create()
75 ->withBody($data, ContentType::JSON)
76 ->request(Method::POST, 'https://api.example.com/users');
77
78// Configure retries with enums
79use Fetch\Enum\Status;
80
81$client = ClientHandler::create()
82 ->retry(3, 100)
83 ->retryStatusCodes([
84 Status::TOO_MANY_REQUESTS->value,
85 Status::SERVICE_UNAVAILABLE->value,
86 Status::GATEWAY_TIMEOUT->value
87 ])
88 ->get('https://api.example.com/flaky-endpoint');
89```
90
91## Creating API Service Classes
92
93For more organized code, you can create service classes that encapsulate API functionality:
94
95```php
96class GitHubApiService
97{
98 private \Fetch\Http\ClientHandler $client;
99
100 public function __construct(string $token)
101 {
102 $this->client = \Fetch\Http\ClientHandler::createWithBaseUri('https://api.github.com')
103 ->withToken($token)
104 ->withHeaders([
105 'Accept' => 'application/vnd.github.v3+json',
106 'User-Agent' => 'MyApp/1.0'
107 ]);
108 }
109
110 public function getUser(string $username)
111 {
112 return $this->client->get("/users/{$username}")->json();
113 }
114
115 public function getRepositories(string $username)
116 {
117 return $this->client->get("/users/{$username}/repos")->json();
118 }
119
120 public function createIssue(string $owner, string $repo, array $issueData)
121 {
122 return $this->client->post("/repos/{$owner}/{$repo}/issues", $issueData)->json();
123 }
124}
125
126// Usage
127$github = new GitHubApiService('your-github-token');
128$user = $github->getUser('octocat');
129$repos = $github->getRepositories('octocat');
130```
131
132## Client Configuration for Different APIs
133
134Different APIs often have different requirements. Here are examples for popular APIs:
135
136### REST API Client
137
138```php
139$restClient = ClientHandler::createWithBaseUri('https://api.example.com')
140 ->withToken('your-api-token')
141 ->withHeaders([
142 'Accept' => 'application/json',
143 'Content-Type' => 'application/json'
144 ]);
145```
146
147### GraphQL API Client
148
149```php
150$graphqlClient = ClientHandler::createWithBaseUri('https://api.example.com/graphql')
151 ->withToken('your-api-token')
152 ->withHeaders([
153 'Content-Type' => 'application/json'
154 ]);
155
156// Example GraphQL query
157$response = $graphqlClient->post('', [
158 'query' => '
159 query GetUser($id: ID!) {
160 user(id: $id) {
161 id
162 name
163 email
164 }
165 }
166 ',
167 'variables' => [
168 'id' => '123'
169 ]
170]);
171```
172
173### OAuth 2.0 Client
174
175```php
176class OAuth2Client
177{
178 private \Fetch\Http\ClientHandler $client;
179 private string $tokenEndpoint;
180 private string $clientId;
181 private string $clientSecret;
182 private ?string $accessToken = null;
183 private ?int $expiresAt = null;
184
185 public function __construct(
186 string $baseUri,
187 string $tokenEndpoint,
188 string $clientId,
189 string $clientSecret
190 ) {
191 $this->client = \Fetch\Http\ClientHandler::createWithBaseUri($baseUri);
192 $this->tokenEndpoint = $tokenEndpoint;
193 $this->clientId = $clientId;
194 $this->clientSecret = $clientSecret;
195 }
196
197 private function ensureToken()
198 {
199 if ($this->accessToken === null || time() > $this->expiresAt) {
200 $this->refreshToken();
201 }
202
203 return $this->accessToken;
204 }
205
206 private function refreshToken()
207 {
208 $response = $this->client->post($this->tokenEndpoint, [
209 'grant_type' => 'client_credentials',
210 'client_id' => $this->clientId,
211 'client_secret' => $this->clientSecret
212 ]);
213
214 $tokenData = $response->json();
215 $this->accessToken = $tokenData['access_token'];
216 $this->expiresAt = time() + ($tokenData['expires_in'] - 60); // Buffer of 60 seconds
217 }
218
219 public function get(string $uri, array $query = [])
220 {
221 $token = $this->ensureToken();
222
223 return $this->client
224 ->withToken($token)
225 ->get($uri, $query)
226 ->json();
227 }
228
229 public function post(string $uri, array $data)
230 {
231 $token = $this->ensureToken();
232
233 return $this->client
234 ->withToken($token)
235 ->post($uri, $data)
236 ->json();
237 }
238
239 // Add other methods as needed
240}
241
242// Usage
243$oauth2Client = new OAuth2Client(
244 'https://api.example.com',
245 '/oauth/token',
246 'your-client-id',
247 'your-client-secret'
248);
249
250$resources = $oauth2Client->get('/resources', ['type' => 'active']);
251```
252
253## Asynchronous API Clients
254
255You can create asynchronous API clients using the async features:
256
257```php
258use function async;
259use function await;
260use function all;
261
262class AsyncApiClient
263{
264 private \Fetch\Http\ClientHandler $client;
265
266 public function __construct(string $baseUri, string $token)
267 {
268 $this->client = \Fetch\Http\ClientHandler::createWithBaseUri($baseUri)
269 ->withToken($token);
270 }
271
272 public function fetchUserAndPosts(int $userId)
273 {
274 return await(async(function() use ($userId) {
275 // Execute requests in parallel
276 $results = await(all([
277 'user' => async(fn() => $this->client->get("/users/{$userId}")),
278 'posts' => async(fn() => $this->client->get("/users/{$userId}/posts"))
279 ]));
280
281 // Process the results
282 return [
283 'user' => $results['user']->json(),
284 'posts' => $results['posts']->json()
285 ];
286 }));
287 }
288}
289
290// Usage
291$client = new AsyncApiClient('https://api.example.com', 'your-token');
292$data = $client->fetchUserAndPosts(123);
293echo "User: {$data['user']['name']}, Posts: " . count($data['posts']);
294```
295
296## Customizing Handlers with Middleware
297
298For advanced use cases, you can create a fully custom client with Guzzle middleware:
299
300```php
301use GuzzleHttp\Client;
302use GuzzleHttp\HandlerStack;
303use GuzzleHttp\Middleware;
304use GuzzleHttp\MessageFormatter;
305use Fetch\Http\ClientHandler;
306use Psr\Http\Message\RequestInterface;
307
308// Create a handler stack
309$stack = HandlerStack::create();
310
311// Add logging middleware
312$logger = new \Monolog\Logger('http');
313$logger->pushHandler(new \Monolog\Handler\StreamHandler('logs/http.log', \Monolog\Logger::DEBUG));
314
315$messageFormat = "{method} {uri} HTTP/{version} {req_body} -> {code} {res_body}";
316$stack->push(
317 Middleware::log($logger, new MessageFormatter($messageFormat))
318);
319
320// Add custom header middleware
321$stack->push(Middleware::mapRequest(function (RequestInterface $request) {
322 return $request->withHeader('X-Custom-Header', 'CustomValue');
323}));
324
325// Add timing middleware
326$stack->push(Middleware::mapRequest(function (RequestInterface $request) {
327 return $request->withHeader('X-Request-Time', (string) time());
328}));
329
330// Create a Guzzle client with the stack
331$guzzleClient = new Client([
332 'handler' => $stack,
333 'base_uri' => 'https://api.example.com'
334]);
335
336// Create a ClientHandler with the custom Guzzle client
337$client = ClientHandler::createWithClient($guzzleClient);
338
339// Use the client
340$response = $client->get('/resources');
341```
342
343## Global Default Client
344
345You can configure a global default client for all requests:
346
347```php
348// Configure the global client
349fetch_client([
350 'base_uri' => 'https://api.example.com',
351 'timeout' => 30,
352 'headers' => [
353 'User-Agent' => 'MyApp/1.0',
354 'Accept' => 'application/json'
355 ]
356]);
357
358// All requests will use this configuration
359$response = fetch('/users'); // Uses the base_uri
360```
361
362## Client Configuration for Testing
363
364For testing, you can configure a client that returns mock responses:
365
366```php
367use Fetch\Http\ClientHandler;
368use Fetch\Enum\Status;
369use GuzzleHttp\Handler\MockHandler;
370use GuzzleHttp\HandlerStack;
371use GuzzleHttp\Psr7\Response as GuzzleResponse;
372use GuzzleHttp\Client;
373
374// Create mock responses
375$mock = new MockHandler([
376 new GuzzleResponse(200, ['Content-Type' => 'application/json'], '{"id": 1, "name": "Test User"}'),
377 new GuzzleResponse(404, ['Content-Type' => 'application/json'], '{"error": "Not found"}'),
378 new GuzzleResponse(500, ['Content-Type' => 'application/json'], '{"error": "Server error"}')
379]);
380
381// Create a handler stack with the mock handler
382$stack = HandlerStack::create($mock);
383
384// Create a Guzzle client with the stack
385$guzzleClient = new Client(['handler' => $stack]);
386
387// Create a ClientHandler with the mock client
388$client = ClientHandler::createWithClient($guzzleClient);
389
390// First request - 200 OK
391$response1 = $client->get('/users/1');
392assert($response1->isOk());
393assert($response1->json()['name'] === 'Test User');
394
395// Second request - 404 Not Found
396$response2 = $client->get('/users/999');
397assert($response2->isNotFound());
398
399// Third request - 500 Server Error
400$response3 = $client->get('/error');
401assert($response3->isServerError());
402```
403
404Alternatively, you can use the built-in mock response utilities:
405
406```php
407// Mock a successful response
408$mockResponse = ClientHandler::createMockResponse(
409 200,
410 ['Content-Type' => 'application/json'],
411 '{"id": 1, "name": "Test User"}'
412);
413
414// Using Status enum
415$mockResponse = ClientHandler::createMockResponse(
416 Status::OK,
417 ['Content-Type' => 'application/json'],
418 '{"id": 1, "name": "Test User"}'
419);
420
421// Mock a JSON response directly
422$mockJsonResponse = ClientHandler::createJsonResponse(
423 ['id' => 2, 'name' => 'Another User'],
424 Status::OK
425);
426```
427
428## Clients with Logging
429
430You can create clients with logging enabled:
431
432```php
433use Monolog\Logger;
434use Monolog\Handler\StreamHandler;
435use Fetch\Http\ClientHandler;
436
437// Create a logger
438$logger = new Logger('api');
439$logger->pushHandler(new StreamHandler('logs/api.log', Logger::INFO));
440
441// Create a client with the logger
442$client = ClientHandler::create();
443$client->setLogger($logger);
444
445// Now all requests and responses will be logged
446$response = $client->get('https://api.example.com/users');
447
448// You can also set a logger on the global client
449$globalClient = fetch_client();
450$globalClient->setLogger($logger);
451```
452
453## Working with Multiple APIs
454
455For applications that interact with multiple APIs:
456
457```php
458class ApiManager
459{
460 private array $clients = [];
461
462 public function register(string $name, \Fetch\Http\ClientHandler $client): void
463 {
464 $this->clients[$name] = $client;
465 }
466
467 public function get(string $name): ?\Fetch\Http\ClientHandler
468 {
469 return $this->clients[$name] ?? null;
470 }
471
472 public function has(string $name): bool
473 {
474 return isset($this->clients[$name]);
475 }
476}
477
478// Usage
479$apiManager = new ApiManager();
480
481// Register clients for different APIs
482$apiManager->register('github', ClientHandler::createWithBaseUri('https://api.github.com')
483 ->withToken('github-token')
484 ->withHeaders(['Accept' => 'application/vnd.github.v3+json']));
485
486$apiManager->register('stripe', ClientHandler::createWithBaseUri('https://api.stripe.com/v1')
487 ->withAuth('sk_test_your_key', ''));
488
489$apiManager->register('custom', ClientHandler::createWithBaseUri('https://api.custom.com')
490 ->withToken('custom-token'));
491
492// Use the clients
493$githubUser = $apiManager->get('github')->get('/user')->json();
494$stripeCustomers = $apiManager->get('stripe')->get('/customers')->json();
495```
496
497## Dependency Injection with Clients
498
499For applications using dependency injection:
500
501```php
502// Service interface
503interface UserServiceInterface
504{
505 public function getUser(int $id): array;
506 public function createUser(array $userData): array;
507}
508
509// Implementation using Fetch
510class UserApiService implements UserServiceInterface
511{
512 private \Fetch\Http\ClientHandler $client;
513
514 public function __construct(\Fetch\Http\ClientHandler $client)
515 {
516 $this->client = $client;
517 }
518
519 public function getUser(int $id): array
520 {
521 return $this->client->get("/users/{$id}")->json();
522 }
523
524 public function createUser(array $userData): array
525 {
526 return $this->client->post('/users', $userData)->json();
527 }
528}
529
530// Usage with a DI container
531$container->singleton(\Fetch\Http\ClientHandler::class, function () {
532 return ClientHandler::createWithBaseUri('https://api.example.com')
533 ->withToken('api-token')
534 ->withHeaders([
535 'Accept' => 'application/json',
536 'User-Agent' => 'MyApp/1.0'
537 ]);
538});
539
540$container->singleton(UserServiceInterface::class, UserApiService::class);
541
542// Usage in a controller
543class UserController
544{
545 private UserServiceInterface $userService;
546
547 public function __construct(UserServiceInterface $userService)
548 {
549 $this->userService = $userService;
550 }
551
552 public function getUser(int $id)
553 {
554 return $this->userService->getUser($id);
555 }
556}
557```
558
559## Configuring Clients from Environment Variables
560
561For applications using environment variables for configuration:
562
563```php
564function createClientFromEnv(string $prefix): \Fetch\Http\ClientHandler
565{
566 $baseUri = getenv("{$prefix}_BASE_URI");
567 $token = getenv("{$prefix}_TOKEN");
568 $timeout = getenv("{$prefix}_TIMEOUT") ?: 30;
569
570 $client = ClientHandler::createWithBaseUri($baseUri)
571 ->timeout((int) $timeout);
572
573 if ($token) {
574 $client->withToken($token);
575 }
576
577 return $client;
578}
579
580// Usage
581$githubClient = createClientFromEnv('GITHUB_API');
582$stripeClient = createClientFromEnv('STRIPE_API');
583```
584
585## Custom Retry Logic
586
587You can create a client with custom retry logic:
588
589```php
590use Fetch\Enum\Status;
591
592$client = ClientHandler::create()
593 ->retry(3, 100) // Basic retry configuration: 3 attempts, 100ms initial delay
594 ->retryStatusCodes([
595 Status::TOO_MANY_REQUESTS->value,
596 Status::SERVICE_UNAVAILABLE->value,
597 Status::GATEWAY_TIMEOUT->value
598 ]) // Only retry these status codes
599 ->retryExceptions([\GuzzleHttp\Exception\ConnectException::class]);
600
601// Use the client
602$response = $client->get('https://api.example.com/unstable-endpoint');
603```
604
605## Extending ClientHandler
606
607For very specialized needs, you can extend the ClientHandler class:
608
609```php
610class GraphQLClientHandler extends \Fetch\Http\ClientHandler
611{
612 /**
613 * Execute a GraphQL query.
614 */
615 public function query(string $query, array $variables = []): array
616 {
617 $response = $this->post('', [
618 'query' => $query,
619 'variables' => $variables
620 ]);
621
622 $data = $response->json();
623
624 if (isset($data['errors'])) {
625 throw new \RuntimeException('GraphQL Error: ' . json_encode($data['errors']));
626 }
627
628 return $data['data'] ?? [];
629 }
630
631 /**
632 * Execute a GraphQL mutation.
633 */
634 public function mutation(string $mutation, array $variables = []): array
635 {
636 return $this->query($mutation, $variables);
637 }
638}
639
640// Usage
641$graphqlClient = new GraphQLClientHandler();
642$graphqlClient->baseUri('https://api.example.com/graphql')
643 ->withToken('your-token');
644
645$userData = $graphqlClient->query('
646 query GetUser($id: ID!) {
647 user(id: $id) {
648 id
649 name
650 email
651 }
652 }
653', ['id' => '123']);
654```
655
656## Best Practices
657
6581. **Use Type-Safe Enums**: Leverage the library's enums for type safety and better code readability.
659
6602. **Organize by API**: Create separate client instances for different APIs.
661
6623. **Configure Once**: Set up clients with all necessary options once, then reuse them.
663
6644. **Use Dependency Injection**: Inject client instances rather than creating them in methods.
665
6665. **Abstract APIs Behind Services**: Create service classes that use clients internally, exposing a domain-specific interface.
667
6686. **Handle Authentication Properly**: Implement token refresh logic for OAuth flows.
669
6707. **Use Timeouts Appropriately**: Configure timeouts based on the expected response time of each API.
671
6728. **Log Requests and Responses**: Add logging for debugging and monitoring API interactions.
673
6749. **Use Base URIs**: Always use base URIs to avoid repeating URL prefixes.
675
67610. **Set Common Headers**: Configure common headers (User-Agent, Accept, etc.) once.
677
67811. **Error Handling**: Implement consistent error handling for each client.
679
68012. **Create Async Clients When Needed**: Use async/await for operations that benefit from parallelism.
681
682## Next Steps
683
684- Learn about [Testing](/guide/testing) for testing with custom clients
685- Explore [Asynchronous Requests](/guide/async-requests) for working with async clients
686- See [Authentication](/guide/authentication) for handling different authentication schemes
687- Check out [Working with Responses](/guide/working-with-responses) for handling API responses