guzzle — правило для Cursor: что делает и как установить
Задаёт стандарты работы с Guzzle в PHP: фабрики клиентов с таймаутами, middleware для повторов и совместимость с PSR-18.
.cursor/rules/guzzle.mdcЧто делает guzzle
Правило инструктирует модель настраивать GuzzleHttp\Client через фабрику сервиса, а не плодить вызовы ad-hoc по всему коду. Клиент получает базовый base_uri и обязательные лимиты времени: общий timeout и connect_timeout для соединения. Без явных таймаутов сетевые запросы рискуют зависнуть навсегда.
Для сквозной логики правило предписывает использовать HandlerStack и стандартные middleware. Повторные попытки настраиваются через Middleware::retry с лимитом в три повтора и экспоненциальной задержкой от 100 мс при сбоях сети или ошибках 5xx. Токены авторизации добавляются централизованно через Middleware::mapRequest. Это избавляет методы сервисов от дублирования заголовков.
Если пишется SDK или пакет для других разработчиков, правило запрещает жёсткую привязку к Guzzle. Код должен зависеть от интерфейсов PSR-18 (ClientInterface) и PSR-17 (RequestFactoryInterface). При обработке ошибок правило требует ловить точечные исключения: RequestException для сбоев протокола и ConnectException при проблемах со связью.
Какие задачи решает
- настроить фабрику HTTP-клиента с едиными таймаутами и базовым URL
- внедрить middleware с экспоненциальным повтором запросов при ошибках 5xx
- подключить логирование запросов и ответов через HandlerStack
- переписать SDK на интерфейсы PSR-18 и PSR-17 вместо прямой зависимости от Guzzle
Примеры запросов агенту
- Сделай фабрику Guzzle-клиента с таймаутом на коннект 5 секунд и логами
- Добавь middleware повторных попыток для сетевых сбоев и кодов 5xx
- Переведи HTTP-клиент нашего SDK на интерфейс PSR-18
Как установить guzzle
Вручную: положить файлы
.cursor/rules/guzzle.mdcВо frontmatter: description, globs, alwaysApply
Вручную: положить файлы
.claude/rules/guzzle.mdПоле paths во frontmatter ограничивает файлы: globs из источника → paths
AGENTS.md
Codex читает AGENTS.md: вставьте текст правила туда
Вручную: положить файлы
.github/instructions/guzzle.instructions.mdПоле applyTo задаёт, к каким файлам применять: globs из источника → applyTo
Вручную: положить файлы
.devin/rules/guzzle.mdВручную: положить файлы
.clinerules/guzzle.mdПо шагам, с командами для каждой платформы: Cursor rules: файлы .mdc, четыре режима и замена .cursorrules
Вопросы о guzzle
Как установить guzzle в Cursor?
Сохраните файл правила по пути .cursor/rules/guzzle.mdc в корне проекта. После этого Cursor начнёт применять инструкции ко всем файлам **/*.php.
Что делает правило guzzle?
Оно задаёт правила конфигурирования HTTP-клиента: требует задавать таймауты, выносить повторы и логирование в middleware и корректно изолировать внешние зависимости.
Как правило предлагает проектировать публичные SDK?
Для библиотек правило запрещает напрямую завязываться на Guzzle. Модель будет использовать абстракции PSR-18 и PSR-17, чтобы вызывающий код сам передавал нужный HTTP-клиент.
Исходный текст
guzzle.mdc · CC0-1.0
---
description: Definitive guidelines for using Guzzle HTTP client in PHP, focusing on robust client configuration, effective middleware, PSR-18 interoperability, and testability.
globs: **/*.php
---
# Guzzle Best Practices
Guzzle is the de-facto PHP HTTP client. Use these rules to build robust, testable, and maintainable HTTP integrations.
## 1. Client Instantiation & Configuration
Always inject an immutable Guzzle client as a service. Configure it once with global defaults, especially timeouts.
### ✅ GOOD: Service-Oriented Client with Essential Defaults
```php
<?php
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use Psr\Log\LoggerInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use GuzzleHttp\Exception\ConnectException;
use Exception;
final class MyApiClientFactory
{
public static function create(LoggerInterface $logger): Client
{
$stack = HandlerStack::create();
// 1. Logging: Log all requests and responses
$stack->push(Middleware::log(
$logger,
new \GuzzleHttp\MessageFormatter(
'{method} {uri} HTTP/{version} {code} {res_header_Content-Length}'
)
));
// 2. Retry Logic: Robust handling for transient errors (5xx, connection issues)
$stack->push(Middleware::retry(
function (
$retries,
RequestInterface $request,
ResponseInterface $response = null,
Exception $exception = null
) {
// Limit to 3 retries
if ($retries >= 3) {
return false;
}
// Retry on connection exceptions
if ($exception instanceof ConnectException) {
return true;
}
// Retry on server errors (5xx)
if ($response && $response->getStatusCode() >= 500) {
return true;
}
return false;
},
// Exponential backoff: 100ms, 200ms, 400ms
function ($retries) {
return 100 * (2 ** ($retries - 1));
}
));
return new Client([
'base_uri' => 'https://api.example.com/v1/',
'timeout' => 10.0, // Total request timeout in seconds
'connect_timeout' => 5.0, // Connection timeout in seconds
'headers' => [
'Accept' => 'application/json',
'User-Agent' => 'MyApp/1.0 (PHP Guzzle)',
],
'handler' => $stack,
'http_errors' => true, // Throw exceptions for 4xx/5xx responses
]);
}
}
// Usage in a service container or dependency injection
// $client = MyApiClientFactory::create($myLogger);
// $response = $client->get('users');
```
### ❌ BAD: Global Singletons & Missing Timeouts
```php
<?php
use GuzzleHttp\Client;
// Client created ad-hoc, no shared configuration, no timeouts
// Hard to test, prone to hanging requests
class LegacyService
{
public function fetchUsers(): array
{
$client = new Client(); // ❌ BAD: No timeouts, no base_uri, no shared config
$response = $client->get('https://api.example.com/v1/users');
return json_decode($response->getBody()->getContents(), true);
}
}
```
## 2. Middleware for Cross-Cutting Concerns
Leverage `HandlerStack` and `GuzzleHttp\Middleware` for reusable logic like retries, logging, and authentication. This keeps your request code DRY.
### ✅ GOOD: Centralized Middleware for Auth & Retries
(See `MyApiClientFactory::create` example above for comprehensive middleware usage.)
```php
<?php
use GuzzleHttp\Middleware;
use Psr\Http\Message\RequestInterface;
// Example of adding an authentication header via middleware
$stack->push(Middleware::mapRequest(function (RequestInterface $request) {
$token = 'your_api_token_here'; // Get from secure config
return $request->withHeader('Authorization', 'Bearer ' . $token);
}));
```
### ❌ BAD: Duplicating Logic Per Request
```php
<?php
use GuzzleHttp\Client;
class UserService
{
private Client $client;
public function __construct(Client $client)
{
$this->client = $client;
}
public function getUser(string $id): array
{
// ❌ BAD: Authentication header added manually for every request type
$response = $this->client->get("users/{$id}", [
'headers' => [
'Authorization' => 'Bearer ' . 'your_api_token_here',
],
]);
return json_decode($response->getBody()->getContents(), true);
}
}
```
## 3. "Bring Your Own Client" (BYOC) for Libraries/SDKs
When building reusable libraries or SDKs, **never hard-depend on Guzzle**. Instead, type-hint `Psr\Http\Client\ClientInterface` (PSR-18) and `Psr\Http\Message\RequestFactoryInterface` (PSR-17). Let the consumer inject their preferred HTTP client.
### ✅ GOOD: PSR-18/17 Compliant SDK
```php
<?php
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\ResponseInterface;
final class MySdkClient
{
private const string BASE_URI = 'https://api.myservice.com';
public function __construct(
private ClientInterface $httpClient,
private RequestFactoryInterface $requestFactory,
private StreamFactoryInterface $streamFactory,
) {}
public function fetchData(string $endpoint, array $data = []): ResponseInterface
{
$body = $this->streamFactory->createStream(json_encode($data));
$request = $this->requestFactory->createRequest('POST', self::BASE_URI . $endpoint)
->withHeader('Content-Type', 'application/json')
->withBody($body);
return $this->httpClient->sendRequest($request);
}
}
// Consumer code (e.g., in a Symfony/Laravel app)
// use GuzzleHttp\Psr7\HttpFactory;
// use GuzzleHttp\Client as GuzzleClient;
// $guzzleClient = new GuzzleClient(/* ... config ... */);
// $httpFactory = new HttpFactory(); // PSR-17 implementation
// $sdk = new MySdkClient($guzzleClient, $httpFactory, $httpFactory);
```
### ❌ BAD: Hard-Dependence on Guzzle in SDKs
```php
<?php
use GuzzleHttp\Client; // ❌ BAD: Direct Guzzle dependency
final class MyBadSdkClient
{
private Client $httpClient;
public function __construct(Client $httpClient) // ❌ BAD: Forces Guzzle on consumers
{
$this->httpClient = $httpClient;
}
public function fetchData(string $endpoint): array
{
$response = $this->httpClient->get('https://api.myservice.com' . $endpoint);
return json_decode($response->getBody()->getContents(), true);
}
}
```
## 4. Error Handling
Always catch specific Guzzle exceptions, especially `RequestException` (for HTTP errors) and `ConnectException` (for network issues).
### ✅ GOOD: Specific Exception Handling
```php
<?php
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Exception\ConnectException;
final class DataFetcher
{
private Client $client;
public function __construct(Client $client)
{
$this->client = $client;
}
public function fetchImportantData(): ?array
{
try {
$response = $this->client->get('critical-endpoint');
return json_decode($response->getBody()->getContents(), true);
} catch (ConnectException $e) {
// Log network error, notify ops, potentially retry (if not handled by middleware)
error_log("Network error fetching data: " . $e->getMessage());
return null; // Or throw a custom application exception
} catch (RequestException $e) {
// Log HTTP error (4xx, 5xx), inspect response
error_log("HTTP error fetching data: " . $e->getMessage() . " Response: " . $e->getResponse()?->getBody());
if ($e->hasResponse() && $e->getResponse()->getStatusCode() === 404) {
return []; // Handle specific 404 case
}
return null; // Or throw a custom application exception
} catch (\Throwable $e) {
// Catch any other unexpected errors
error_log("Unexpected error: " . $e->getMessage());
throw $e; // Re-throw for higher-level handling
}
}
}
```
### ❌ BAD: Catching Generic Exceptions
```php
<?php
// ... (class definition)
public function fetchImportantData(): ?array
{
try {
$response = $this->client->get('critical-endpoint');
return json_decode($response->getBody()->getContents(), true);
} catch (\Exception $e) { // ❌ BAD: Catches too broadly, hides specific issues
error_log("Error fetching data: " . $e->getMessage());
return null;
}
}
```
## 5. Testing Guzzle Clients
Use `GuzzleHttp\Handler\MockHandler` for deterministic and fast unit tests. This avoids hitting real APIs and ensures consistent test results.
### ✅ GOOD: Mocking Responses with `MockHandler`
```php
<?php
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use PHPUnit\Framework\TestCase;
final class MyServiceTest extends TestCase
{
public function testFetchUsersSuccessfully(): void
{
// Create a mock handler with a successful response
$mock = new MockHandler([
new Response(200, ['Content-Type' => 'application/json'], json_encode(['users' => ['Alice', 'Bob']])),
]);
$handlerStack = HandlerStack::create($mock);
$client = new Client(['handler' => $handlerStack]);
// Inject the mocked client into your service
$service = new MyService($client);
$users = $service->fetchUsers();
$this->assertEquals(['Alice', 'Bob'], $users);
}
public function testFetchUsersNotFound(): void
{
// Mock a 404 response
$mock = new MockHandler([
new Response(404, [], 'Not Found'),
]);
$handlerStack = HandlerStack::create($mock);
$client = new Client(['handler' => $handlerStack]);
$service = new MyService($client);
// Expect an exception or specific return based on your error handling
$this->expectException(\GuzzleHttp\Exception\ClientException::class); // If http_errors is true
$service->fetchUsers();
}
}
// Example MyService class
class MyService
{
private Client $client;
public function __construct(Client $client)
{
$this->client = $client;
}
public function fetchUsers(): array
{
$response = $this->client->get('users');
return json_decode($response->getBody()->getContents(), true)['users'];
}
}
```
### ❌ BAD: Hitting Real APIs in Tests
```php
<?php
use GuzzleHttp\Client;
use PHPUnit\Framework\TestCase;
final class MyServiceIntegrationTest extends TestCase
{
public function testFetchUsersFromRealApi(): void
{
// ❌ BAD: Hits a real external API, making tests slow, flaky, and dependent on external services
$client = new Client(['base_uri' => 'https://api.example.com/v1/']);
$service = new MyService($client);
$users = $service->fetchUsers();
$this->assertIsArray($users);
$this->assertNotEmpty($users);
}
}
```
## 6. Laravel HTTP Client
If you are in a Laravel project, use Laravel's built-in HTTP client wrapper. It's powered by Guzzle but provides a more fluent API, built-in faking, and concurrent request helpers.
### ✅ GOOD: Laravel HTTP Client
```php
<?php
use Illuminate\Support\Facades\Http;
final class LaravelIntegrationService
{
public function getUserData(int $userId): array
{
$response = Http::withToken('your_api_token')
->timeout(10)
->retry(3, 100) // Retry 3 times, 100ms delay
->get("https://api.example.com/users/{$userId}");
$response->throw(); // Throws an exception for 4xx/5xx responses
return $response->json();
}
public function sendConcurrentRequests(): array
{
$responses = Http::pool(fn (Http\Client\Pool $pool) => [
$pool->get('https://api.example.com/data/1'),
$pool->get('https://api.example.com/data/2'),
]);
return [
$responses[0]->json(),
$responses[1]->json(),
];
}
}
```
© sanjeed5 · CC0-1.0 · sanjeed5/awesome-cursor-rules-mdc