saloonphp/saloon
Saloon is a PHP HTTP client framework for building API integrations. Define connectors and requests, handle authentication, retries, and responses, and test easily with fakes and mocking. Works great in Laravel or any PHP app.
Installation:
composer require saloonphp/saloon
Add to config/app.php under providers:
Saloon\Laravel\SaloonServiceProvider::class,
First Request:
Define a request class (e.g., app/Http/Saloon/Requests/GetUser.php):
use Saloon\Contracts\SaloonRequest;
use Saloon\Enums\HttpMethod;
use Saloon\Traits\BodyParameters;
class GetUser extends SaloonRequest
{
public $endpoint = 'users/1';
public $method = HttpMethod::GET;
protected $baseUri = 'https://api.example.com';
}
Send the Request:
use Saloon\Saloon;
$response = Saloon::send(new GetUser());
$data = $response->json();
Key Files to Explore:
config/saloon.php (global config)app/Http/Saloon/ (request classes)tests/Feature/Saloon/ (test patterns)Pattern: Use dedicated request classes for each API endpoint.
class CreateOrder extends SaloonRequest
{
public $endpoint = 'orders';
public $method = HttpMethod::POST;
protected $baseUri = 'https://api.merchant.com';
public function resolveEndpoint(): string
{
return $this->endpoint . '?' . http_build_query($this->query);
}
public function defaultBody(): array
{
return [
'amount' => $this->amount,
'currency' => $this->currency,
];
}
}
Usage:
$order = Saloon::send(new CreateOrder(['amount' => 100, 'currency' => 'USD']));
OAuth2 Example:
use Saloon\Auth\OAuth2;
class AuthenticatedRequest extends SaloonRequest
{
protected $auth = OAuth2::class;
protected $authConfig = [
'client_id' => env('OAUTH_CLIENT_ID'),
'client_secret' => env('OAUTH_SECRET'),
'token_url' => 'https://oauth.example.com/token',
];
}
Bearer Token:
use Saloon\Auth\BearerToken;
class BearerRequest extends SaloonRequest
{
protected $auth = BearerToken::class;
protected $authConfig = ['token' => $this->token];
}
Global Middleware (in config/saloon.php):
'middleware' => [
\Saloon\Http\Middleware\AddDefaultHeaders::class,
\App\Http\Saloon\Middleware\LogRequests::class,
],
Request-Specific Middleware:
class PaginatedRequest extends SaloonRequest
{
protected $middleware = [
\Saloon\Http\Middleware\Pagination::class,
];
}
Type-Safe Parsing:
use Saloon\Decorators\ResponseDecorator;
class UserResponse extends ResponseDecorator
{
public function user(): array
{
return $this->json('data.user');
}
}
Usage:
$response = Saloon::send(new GetUser());
$user = $response->user(); // Auto-parsed
Fixture-Based Mocking:
// tests/Feature/Saloon/GetUserTest.php
public function test_get_user()
{
$this->mock(Saloon::class, GetUser::class)
->shouldReceive('send')
->once()
->andReturn(new GetUserResponse(['name' => 'John']));
$response = Saloon::send(new GetUser());
$this->assertEquals('John', $response->json('name'));
}
Custom Exceptions:
use Saloon\Exceptions\HttpException;
use Saloon\Exceptions\SaloonException;
class PaymentFailedException extends SaloonException {}
class PaymentRequest extends SaloonRequest
{
public function handleException(HttpException $exception): void
{
if ($exception->response->status == 402) {
throw new PaymentFailedException($exception->getMessage());
}
}
}
Auto-Pagination:
use Saloon\Traits\Pagination;
class ListUsers extends SaloonRequest
{
use Pagination;
public $endpoint = 'users';
public $method = HttpMethod::GET;
protected $paginateKey = 'data';
protected $perPage = 20;
}
Usage:
$users = Saloon::send(new ListUsers())->all();
Dynamic Request Modification:
GetUser::macro('withRole', function ($role) {
$this->query['role'] = $role;
return $this;
});
// Usage:
$response = Saloon::send((new GetUser())->withRole('admin'));
Base URI Overrides (v4.0+)
endpoint now override baseUri by default (security fix for SSRF).$request->withBaseUriOverride(true);
Fixture Path Traversal (v4.0+)
database/fixtures/).../ in fixture paths; use base_path('fixtures/...').Authenticator Injection (CVE-2026-33942)
AccessTokenAuthenticator instantiation with user input.NullAuthenticator for unauthenticated requests.Case-Sensitive Headers
Content-Type: json may fail; use lowercase (Content-Type: JSON).$request->withHeaders(['Content-Type' => 'application/json']);
Deprecated Methods
sendAndRetry() and auth() methods are deprecated.Saloon::send() with middleware or defaultAuth.Pretty-Print Responses (v3.6.3+)
$response->prettyPrint(); // Dumps formatted JSON/XML
Debug Middleware Add a debug middleware to inspect requests/responses:
class DebugMiddleware implements Middleware
{
public function handle(SaloonRequest $request): void
{
\Log::debug('Request:', [
'url' => $request->resolveEndpoint(),
'body' => $request->body(),
'headers' => $request->headers(),
]);
}
}
Assertion Failures
assertSent() to verify requests in tests:
$this->assertSent(GetUser::class, function (GetUser $request) {
return $request->userId === 1;
});
Retry Logic
config/saloon.php:
'retry' => [
'max_attempts' => 3,
'delay' => 1000,
'exceptions' => [
\Saloon\Exceptions\HttpException::class,
],
],
Custom Response Decorators
Extend ResponseDecorator to add domain-specific methods:
class StripeResponse extends ResponseDecorator
{
public function paymentIntent(): array
{
return $this->json('data.object');
}
}
PSR-18 Client Integration Replace Guzzle with a custom client:
use Saloon\Contracts\Client;
use Saloon\Http\Clients\Psr18Client;
class CustomClient implements Client
{
public function send(SaloonRequest $request): Response
{
$psrRequest = new \Psr\Http\Message\Request(
$request->method,
$request->resolveEndpoint(),
$request->headers(),
$request->body()
);
$response = $this->psrClient->send($psrRequest);
return new \Saloon\Http\Responses\Response($response);
}
}
Dynamic Endpoints
Use resolveEndpoint() for runtime URL generation:
public function resolveEndpoint(): string
{
return "users/{$this->userId}/orders";
}
Fixture Merging (v3.11+) Override mock responses dynamically
How can I help you explore Laravel packages today?