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