saloon-for-laravel
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSaloon for Laravel
Laravel 中的 Saloon
When to use this skill
何时使用该工具
Use this skill when working with SaloonPHP in a Laravel 11+ application:
- Integrating with third-party APIs (REST, JSON, XML)
- Building API connectors, requests, and handling responses
- Authenticating API requests (token, basic, OAuth2, custom)
- Sending request bodies (JSON, form, multipart, XML)
- Testing API integrations with mocking and faking
- Implementing pagination, rate limiting, caching, or retries
- Building reusable SDKs for APIs
- Working with DTOs from API responses
在Laravel 11+应用中使用SaloonPHP时适用本工具:
- 集成第三方API(REST、JSON、XML)
- 构建API连接器、请求并处理响应
- API请求认证(令牌、基础认证、OAuth2、自定义认证)
- 发送请求体(JSON、表单、多部分、XML)
- 通过模拟和伪造测试API集成
- 实现分页、速率限制、缓存或重试逻辑
- 构建可复用的API SDK
- 处理API响应中的DTO
Core Concepts
核心概念
- Connectors define the API base URL and shared configuration (headers, auth, middleware).
- Requests define individual endpoints, HTTP methods, and request-specific data.
- Responses provide a rich interface for reading status, headers, and parsed body data.
- Connectors send Requests and return Responses: .
$connector->send($request) - Use Artisan commands to generate classes — never create them manually from scratch.
- Store integration classes in (configurable in
app/Http/Integrations/{ServiceName}/).config/saloon.php
- Connectors(连接器) 定义API基础URL和共享配置(请求头、认证、中间件)。
- Requests(请求) 定义单个接口、HTTP方法和请求专属数据。
- Responses(响应) 提供丰富的接口用于读取状态码、请求头和解析后的响应体数据。
- 连接器发送请求并返回响应:。
$connector->send($request) - 使用Artisan命令生成类——切勿手动从零创建。
- 将集成类存储在(可在
app/Http/Integrations/{ServiceName}/中配置)。config/saloon.php
Installation
安装
bash
composer require saloonphp/laravel-pluginThis installs both (core) and the Laravel integration. Publish the config file:
saloonphp/saloonbash
php artisan vendor:publish --tag=saloon-configOptional plugins (install as needed):
bash
composer require saloonphp/pagination-plugin # Paginated API responses
composer require saloonphp/cache-plugin # Response caching
composer require saloonphp/rate-limit-plugin # Rate limit handling
composer require saloonphp/xml-wrangler # XML reading/writingbash
composer require saloonphp/laravel-plugin这会同时安装(核心包)和Laravel集成插件。发布配置文件:
saloonphp/saloonbash
php artisan vendor:publish --tag=saloon-config可选插件(按需安装):
bash
composer require saloonphp/pagination-plugin # 分页API响应
composer require saloonphp/cache-plugin # 响应缓存
composer require saloonphp/rate-limit-plugin # 速率限制处理
composer require saloonphp/xml-wrangler # XML读写File Structure
文件结构
app/Http/Integrations/
└── Acme/
├── AcmeConnector.php
├── Requests/
│ ├── GetUserRequest.php
│ ├── ListProjectsRequest.php
│ └── CreateProjectRequest.php
├── Responses/
│ └── AcmeResponse.php
└── Data/
└── UserDTO.phpapp/Http/Integrations/
└── Acme/
├── AcmeConnector.php
├── Requests/
│ ├── GetUserRequest.php
│ ├── ListProjectsRequest.php
│ └── CreateProjectRequest.php
├── Responses/
│ └── AcmeResponse.php
└── Data/
└── UserDTO.phpCreating Connectors
创建连接器
Generate with Artisan:
bash
php artisan saloon:connector Acme AcmeConnectorA connector defines the base URL and shared configuration:
php
namespace App\Http\Integrations\Acme;
use Saloon\Http\Connector;
use Saloon\Traits\Plugins\AcceptsJson;
use Saloon\Traits\Plugins\AlwaysThrowOnErrors;
class AcmeConnector extends Connector
{
use AcceptsJson;
use AlwaysThrowOnErrors;
public function __construct(
protected readonly string $token,
) {}
public function resolveBaseUrl(): string
{
return config('services.acme.base_url');
}
protected function defaultHeaders(): array
{
return [
'Authorization' => 'Bearer ' . $this->token,
];
}
}使用Artisan生成:
bash
php artisan saloon:connector Acme AcmeConnector连接器定义基础URL和共享配置:
php
namespace App\Http\Integrations\Acme;
use Saloon\Http\Connector;
use Saloon\Traits\Plugins\AcceptsJson;
use Saloon\Traits\Plugins\AlwaysThrowOnErrors;
class AcmeConnector extends Connector
{
use AcceptsJson;
use AlwaysThrowOnErrors;
public function __construct(
protected readonly string $token,
) {}
public function resolveBaseUrl(): string
{
return config('services.acme.base_url');
}
protected function defaultHeaders(): array
{
return [
'Authorization' => 'Bearer ' . $this->token,
];
}
}Creating Requests
创建请求
Generate with Artisan:
bash
php artisan saloon:request Acme GetUserRequestDefine the HTTP method and endpoint:
php
namespace App\Http\Integrations\Acme\Requests;
use Saloon\Enums\Method;
use Saloon\Http\Request;
class GetUserRequest extends Request
{
protected Method $method = Method::GET;
public function __construct(
protected readonly string $username,
) {}
public function resolveEndpoint(): string
{
return '/users/' . $this->username;
}
}使用Artisan生成:
bash
php artisan saloon:request Acme GetUserRequest定义HTTP方法和接口:
php
namespace App\Http\Integrations\Acme\Requests;
use Saloon\Enums\Method;
use Saloon\Http\Request;
class GetUserRequest extends Request
{
protected Method $method = Method::GET;
public function __construct(
protected readonly string $username,
) {}
public function resolveEndpoint(): string
{
return '/users/' . $this->username;
}
}Request Body Data
请求体数据
Implement interface with a body trait on the request:
HasBodyphp
use Saloon\Contracts\Body\HasBody;
use Saloon\Enums\Method;
use Saloon\Http\Request;
use Saloon\Traits\Body\HasJsonBody;
class CreateProjectRequest extends Request implements HasBody
{
use HasJsonBody;
protected Method $method = Method::POST;
public function __construct(
protected readonly string $name,
protected readonly bool $private = false,
) {}
public function resolveEndpoint(): string
{
return '/projects';
}
protected function defaultBody(): array
{
return [
'name' => $this->name,
'private' => $this->private,
];
}
}Available body traits (each requires implementing ):
HasBody| Trait | Use Case |
|---|---|
| JSON payloads ( |
| Form data ( |
| File uploads ( |
| XML payloads |
| Raw string body |
| Stream/binary body |
在请求中实现接口并使用请求体 trait:
HasBodyphp
use Saloon\Contracts\Body\HasBody;
use Saloon\Enums\Method;
use Saloon\Http\Request;
use Saloon\Traits\Body\HasJsonBody;
class CreateProjectRequest extends Request implements HasBody
{
use HasJsonBody;
protected Method $method = Method::POST;
public function __construct(
protected readonly string $name,
protected readonly bool $private = false,
) {}
public function resolveEndpoint(): string
{
return '/projects';
}
protected function defaultBody(): array
{
return [
'name' => $this->name,
'private' => $this->private,
];
}
}可用的请求体 trait(每个都需要实现):
HasBody| Trait | 使用场景 |
|---|---|
| JSON 负载( |
| 表单数据( |
| 文件上传( |
| XML 负载 |
| 原始字符串请求体 |
| 流/二进制请求体 |
Sending Requests
发送请求
php
$connector = new AcmeConnector(token: config('services.acme.token'));
$request = new GetUserRequest('johndoe');
$response = $connector->send($request);Using dependency injection in a controller:
php
class UserController extends Controller
{
public function show(string $username, AcmeConnector $connector)
{
$response = $connector->send(new GetUserRequest($username));
return $response->json();
}
}Register the connector in a service provider for DI:
php
// AppServiceProvider::register()
$this->app->bind(AcmeConnector::class, function () {
return new AcmeConnector(token: config('services.acme.token'));
});php
$connector = new AcmeConnector(token: config('services.acme.token'));
$request = new GetUserRequest('johndoe');
$response = $connector->send($request);在控制器中使用依赖注入:
php
class UserController extends Controller
{
public function show(string $username, AcmeConnector $connector)
{
$response = $connector->send(new GetUserRequest($username));
return $response->json();
}
}在服务提供者中注册连接器以支持依赖注入:
php
// AppServiceProvider::register()
$this->app->bind(AcmeConnector::class, function () {
return new AcmeConnector(token: config('services.acme.token'));
});Handling Responses
处理响应
php
$response = $connector->send($request);
// Status checks
$response->successful(); // 200-299
$response->ok(); // 200
$response->failed(); // 400+
$response->status(); // int
// Reading data
$response->json(); // array
$response->json('user.name'); // nested key access
$response->object(); // stdClass
$response->collect(); // Illuminate\Support\Collection
$response->body(); // raw string
// Headers
$response->headers()->all();
$response->header('Content-Type');
// Error handling
$response->throw(); // throw if failed
$response->onError(fn ($response) => logger()->error('API failed', [
'status' => $response->status(),
]));php
$response = $connector->send($request);
// 状态检查
$response->successful(); // 200-299
$response->ok(); // 200
$response->failed(); // 400+
$response->status(); // 整数
// 读取数据
$response->json(); // 数组
$response->json('user.name'); // 嵌套键访问
$response->object(); // stdClass
$response->collect(); // Illuminate\Support\Collection
$response->body(); // 原始字符串
// 请求头
$response->headers()->all();
$response->header('Content-Type');
// 错误处理
$response->throw(); // 失败时抛出异常
$response->onError(fn ($response) => logger()->error('API failed', [
'status' => $response->status(),
]));Authentication
认证
Built-in authenticators
内置认证器
php
use Saloon\Http\Auth\TokenAuthenticator;
use Saloon\Http\Auth\BasicAuthenticator;
use Saloon\Http\Auth\QueryAuthenticator;
use Saloon\Http\Auth\HeaderAuthenticator;
// Bearer token (most common)
$connector->authenticate(new TokenAuthenticator('my-api-token'));
// Basic auth
$connector->authenticate(new BasicAuthenticator('user', 'password'));
// Query parameter auth
$connector->authenticate(new QueryAuthenticator('api_key', 'my-key'));
// Custom header
$connector->authenticate(new HeaderAuthenticator('my-token', 'X-API-Key'));php
use Saloon\Http\Auth\TokenAuthenticator;
use Saloon\Http\Auth\BasicAuthenticator;
use Saloon\Http\Auth\QueryAuthenticator;
use Saloon\Http\Auth\HeaderAuthenticator;
// Bearer令牌(最常用)
$connector->authenticate(new TokenAuthenticator('my-api-token'));
// 基础认证
$connector->authenticate(new BasicAuthenticator('user', 'password'));
// 查询参数认证
$connector->authenticate(new QueryAuthenticator('api_key', 'my-key'));
// 自定义请求头
$connector->authenticate(new HeaderAuthenticator('my-token', 'X-API-Key'));Default authentication on a connector
连接器默认认证
php
class AcmeConnector extends Connector
{
public function __construct(protected readonly string $token) {}
protected function defaultAuth(): TokenAuthenticator
{
return new TokenAuthenticator($this->token);
}
}php
class AcmeConnector extends Connector
{
public function __construct(protected readonly string $token) {}
protected function defaultAuth(): TokenAuthenticator
{
return new TokenAuthenticator($this->token);
}
}Error Handling
错误处理
Use to throw exceptions automatically on failed responses:
AlwaysThrowOnErrorsphp
use Saloon\Traits\Plugins\AlwaysThrowOnErrors;
class AcmeConnector extends Connector
{
use AlwaysThrowOnErrors;
}Catch specific exceptions:
php
use Saloon\Exceptions\Request\RequestException;
use Saloon\Exceptions\Request\ClientException; // 4xx
use Saloon\Exceptions\Request\ServerException; // 5xx
use Saloon\Exceptions\Request\FatalRequestException; // network failure
try {
$response = $connector->send($request);
} catch (ClientException $e) {
// 4xx error — $e->getResponse() gives the Response object
} catch (ServerException $e) {
// 5xx error
} catch (FatalRequestException $e) {
// Connection failure, timeout, etc.
}Custom failure logic on a connector or request:
php
public function hasRequestFailed(Response $response): ?bool
{
return $response->json('success') === false;
}使用在响应失败时自动抛出异常:
AlwaysThrowOnErrorsphp
use Saloon\Traits\Plugins\AlwaysThrowOnErrors;
class AcmeConnector extends Connector
{
use AlwaysThrowOnErrors;
}捕获特定异常:
php
use Saloon\Exceptions\Request\RequestException;
use Saloon\Exceptions\Request\ClientException; // 4xx
use Saloon\Exceptions\Request\ServerException; // 5xx
use Saloon\Exceptions\Request\FatalRequestException; // 网络故障
try {
$response = $connector->send($request);
} catch (ClientException $e) {
// 4xx错误 — $e->getResponse() 返回响应对象
} catch (ServerException $e) {
// 5xx错误
} catch (FatalRequestException $e) {
// 连接失败、超时等
}在连接器或请求中自定义失败逻辑:
php
public function hasRequestFailed(Response $response): ?bool
{
return $response->json('success') === false;
}DTOs (Data Transfer Objects)
DTO(数据传输对象)
Implement on a request to cast responses:
createDtoFromResponsephp
use Saloon\Http\Request;
use Saloon\Http\Response;
class GetUserRequest extends Request
{
// ...
public function createDtoFromResponse(Response $response): UserDTO
{
return new UserDTO(
name: $response->json('name'),
email: $response->json('email'),
avatar: $response->json('avatar_url'),
);
}
}Then use it:
php
$user = $connector->send(new GetUserRequest('johndoe'))->dto();
$user = $connector->send(new GetUserRequest('johndoe'))->dtoOrFail();在请求中实现以转换响应:
createDtoFromResponsephp
use Saloon\Http\Request;
use Saloon\Http\Response;
class GetUserRequest extends Request
{
// ...
public function createDtoFromResponse(Response $response): UserDTO
{
return new UserDTO(
name: $response->json('name'),
email: $response->json('email'),
avatar: $response->json('avatar_url'),
);
}
}然后使用:
php
$user = $connector->send(new GetUserRequest('johndoe'))->dto();
$user = $connector->send(new GetUserRequest('johndoe'))->dtoOrFail();Middleware
中间件
Add middleware on connectors or requests to modify the request/response pipeline:
php
class AcmeConnector extends Connector
{
public function __construct()
{
// Request middleware
$this->middleware()->onRequest(function (PendingRequest $pendingRequest) {
$pendingRequest->headers()->add('X-Request-ID', Str::uuid()->toString());
});
// Response middleware
$this->middleware()->onResponse(function (Response $response) {
logger()->info('Acme API responded', ['status' => $response->status()]);
});
}
}Use the method for one-time setup:
bootphp
public function boot(PendingRequest $pendingRequest): void
{
$pendingRequest->config()->add('timeout', 60);
}在连接器或请求中添加中间件以修改请求/响应管道:
php
class AcmeConnector extends Connector
{
public function __construct()
{
// 请求中间件
$this->middleware()->onRequest(function (PendingRequest $pendingRequest) {
$pendingRequest->headers()->add('X-Request-ID', Str::uuid()->toString());
});
// 响应中间件
$this->middleware()->onResponse(function (Response $response) {
logger()->info('Acme API responded', ['status' => $response->status()]);
});
}
}使用方法进行一次性设置:
bootphp
public function boot(PendingRequest $pendingRequest): void
{
$pendingRequest->config()->add('timeout', 60);
}Testing with Saloon Facade
使用Saloon门面进行测试
The Laravel plugin provides a facade for fluent test mocking:
Saloonphp
use Saloon\Laravel\Facades\Saloon;
use Saloon\Http\Faking\MockResponse;
it('fetches a user from the API', function () {
Saloon::fake([
GetUserRequest::class => MockResponse::make(
body: ['name' => 'Sam', 'email' => 'sam@example.com'],
status: 200,
),
]);
$response = $this->get('/api/users/johndoe');
$response->assertOk();
Saloon::assertSent(GetUserRequest::class);
Saloon::assertSentCount(1);
});Laravel插件提供门面用于流畅的测试模拟:
Saloonphp
use Saloon\Laravel\Facades\Saloon;
use Saloon\Http\Faking\MockResponse;
it('fetches a user from the API', function () {
Saloon::fake([
GetUserRequest::class => MockResponse::make(
body: ['name' => 'Sam', 'email' => 'sam@example.com'],
status: 200,
),
]);
$response = $this->get('/api/users/johndoe');
$response->assertOk();
Saloon::assertSent(GetUserRequest::class);
Saloon::assertSentCount(1);
});Sequence responses
序列响应
php
Saloon::fake([
GetUserRequest::class => MockResponse::sequence([
MockResponse::make(['attempt' => 1], 500),
MockResponse::make(['attempt' => 2], 200),
]),
]);php
Saloon::fake([
GetUserRequest::class => MockResponse::sequence([
MockResponse::make(['attempt' => 1], 500),
MockResponse::make(['attempt' => 2], 200),
]),
]);Assertions
断言
php
Saloon::assertSent(GetUserRequest::class);
Saloon::assertSent(fn (Request $request) => $request->resolveEndpoint() === '/users/johndoe');
Saloon::assertNotSent(DeleteUserRequest::class);
Saloon::assertSentCount(2);
Saloon::assertNothingSent();php
Saloon::assertSent(GetUserRequest::class);
Saloon::assertSent(fn (Request $request) => $request->resolveEndpoint() === '/users/johndoe');
Saloon::assertNotSent(DeleteUserRequest::class);
Saloon::assertSentCount(2);
Saloon::assertNothingSent();Prevent stray requests
阻止未模拟请求
Ensure all API requests are mocked in tests:
php
Saloon::fake();
// or
Saloon::allowStrayRequests();确保测试中所有API请求都已模拟:
php
Saloon::fake();
// 或
Saloon::allowStrayRequests();Retry Logic
重试逻辑
Set retry properties on connectors or requests:
php
class AcmeConnector extends Connector
{
protected int $tries = 3;
protected int $retryInterval = 500; // milliseconds
protected bool $useExponentialBackoff = true;
protected bool $throwOnMaxTries = true;
public function handleRetry(FatalRequestException|RequestException $exception, Request $request): bool
{
// Return false to stop retrying
if ($exception instanceof RequestException && $exception->getResponse()->status() === 422) {
return false;
}
return true;
}
}在连接器或请求中设置重试属性:
php
class AcmeConnector extends Connector
{
protected int $tries = 3;
protected int $retryInterval = 500; // 毫秒
protected bool $useExponentialBackoff = true;
protected bool $throwOnMaxTries = true;
public function handleRetry(FatalRequestException|RequestException $exception, Request $request): bool
{
// 返回false停止重试
if ($exception instanceof RequestException && $exception->getResponse()->status() === 422) {
return false;
}
return true;
}
}Pagination
分页
Install the pagination plugin:
bash
composer require saloonphp/pagination-pluginImplement the interface on your connector:
HasPaginationphp
use Saloon\Http\Connector;
use Saloon\PaginationPlugin\PagedPaginator;
use Saloon\PaginationPlugin\Contracts\HasPagination;
class AcmeConnector extends Connector implements HasPagination
{
public function paginate(Request $request): PagedPaginator
{
return new class(connector: $this, request: $request) extends PagedPaginator
{
protected function isLastPage(Response $response): bool
{
return empty($response->json('next_page_url'));
}
protected function getPageItems(Response $response, Response $originalResponse): array
{
return $response->json('data');
}
};
}
}Use the paginator:
php
$paginator = $connector->paginate(new ListProjectsRequest);
foreach ($paginator as $response) {
$projects = $response->json('data');
}
// Or collect all items
$allProjects = $paginator->collect()->all();Available paginator types: , , .
PagedPaginatorOffsetPaginatorCursorPaginator安装分页插件:
bash
composer require saloonphp/pagination-plugin在连接器上实现接口:
HasPaginationphp
use Saloon\Http\Connector;
use Saloon\PaginationPlugin\PagedPaginator;
use Saloon\PaginationPlugin\Contracts\HasPagination;
class AcmeConnector extends Connector implements HasPagination
{
public function paginate(Request $request): PagedPaginator
{
return new class(connector: $this, request: $request) extends PagedPaginator
{
protected function isLastPage(Response $response): bool
{
return empty($response->json('next_page_url'));
}
protected function getPageItems(Response $response, Response $originalResponse): array
{
return $response->json('data');
}
};
}
}使用分页器:
php
$paginator = $connector->paginate(new ListProjectsRequest);
foreach ($paginator as $response) {
$projects = $response->json('data');
}
// 或收集所有项
$allProjects = $paginator->collect()->all();可用的分页器类型:, , 。
PagedPaginatorOffsetPaginatorCursorPaginatorRate Limiting
速率限制
Install the rate limit plugin:
bash
composer require saloonphp/rate-limit-pluginAdd rate limiting to your connector:
php
use Saloon\Http\Connector;
use Saloon\RateLimitPlugin\Contracts\RateLimitStore;
use Saloon\RateLimitPlugin\Limit;
use Saloon\RateLimitPlugin\Stores\LaravelCacheStore;
use Saloon\RateLimitPlugin\Traits\HasRateLimits;
class AcmeConnector extends Connector
{
use HasRateLimits;
protected function resolveLimits(): array
{
return [
Limit::allow(5000)->everyHour(),
];
}
protected function resolveRateLimitStore(): RateLimitStore
{
return new LaravelCacheStore(cache()->store());
}
}安装速率限制插件:
bash
composer require saloonphp/rate-limit-plugin为连接器添加速率限制:
php
use Saloon\Http\Connector;
use Saloon\RateLimitPlugin\Contracts\RateLimitStore;
use Saloon\RateLimitPlugin\Limit;
use Saloon\RateLimitPlugin\Stores\LaravelCacheStore;
use Saloon\RateLimitPlugin\Traits\HasRateLimits;
class AcmeConnector extends Connector
{
use HasRateLimits;
protected function resolveLimits(): array
{
return [
Limit::allow(5000)->everyHour(),
];
}
protected function resolveRateLimitStore(): RateLimitStore
{
return new LaravelCacheStore(cache()->store());
}
}Caching Responses
响应缓存
Install the cache plugin:
bash
composer require saloonphp/cache-pluginAdd caching to a request:
php
use Saloon\CachePlugin\Contracts\Cacheable;
use Saloon\CachePlugin\Contracts\Driver;
use Saloon\CachePlugin\Drivers\LaravelCacheDriver;
use Saloon\CachePlugin\Traits\HasCaching;
use Saloon\Http\Request;
class GetUserRequest extends Request implements Cacheable
{
use HasCaching;
public function resolveCacheDriver(): Driver
{
return new LaravelCacheDriver(cache()->store());
}
public function cacheExpiryInSeconds(): int
{
return 3600; // 1 hour
}
}安装缓存插件:
bash
composer require saloonphp/cache-plugin为请求添加缓存:
php
use Saloon\CachePlugin\Contracts\Cacheable;
use Saloon\CachePlugin\Contracts\Driver;
use Saloon\CachePlugin\Drivers\LaravelCacheDriver;
use Saloon\CachePlugin\Traits\HasCaching;
use Saloon\Http\Request;
class GetUserRequest extends Request implements Cacheable
{
use HasCaching;
public function resolveCacheDriver(): Driver
{
return new LaravelCacheDriver(cache()->store());
}
public function cacheExpiryInSeconds(): int
{
return 3600; // 1小时
}
}Concurrency & Pools
并发与请求池
Send multiple requests concurrently:
php
use Saloon\Http\Response;
$pool = $connector->pool(
requests: [
new GetUserRequest('alice'),
new GetUserRequest('bob'),
new GetUserRequest('charlie'),
],
concurrency: 5,
responseHandler: function (Response $response, int $key) {
logger()->info('User fetched', ['data' => $response->json('name')]);
},
exceptionHandler: function (Exception $exception, int $key) {
logger()->error('Request failed', ['key' => $key]);
},
);
$pool->send()->wait();并发发送多个请求:
php
use Saloon\Http\Response;
$pool = $connector->pool(
requests: [
new GetUserRequest('alice'),
new GetUserRequest('bob'),
new GetUserRequest('charlie'),
],
concurrency: 5,
responseHandler: function (Response $response, int $key) {
logger()->info('User fetched', ['data' => $response->json('name')]);
},
exceptionHandler: function (Exception $exception, int $key) {
logger()->error('Request failed', ['key' => $key]);
},
);
$pool->send()->wait();OAuth2 Authentication
OAuth2认证
Create an OAuth2 connector:
bash
php artisan saloon:connector Acme AcmeOAuthConnectorphp
use Saloon\Http\Connector;
use Saloon\Http\Auth\AccessTokenAuthenticator;
use Saloon\Traits\OAuth2\AuthorizationCodeGrant;
class AcmeOAuthConnector extends Connector
{
use AuthorizationCodeGrant;
public function resolveBaseUrl(): string
{
return config('services.acme.base_url');
}
protected function resolveAccessTokenUrl(): string
{
return config('services.acme.base_url') . '/oauth/token';
}
protected function resolveAuthorizationUrl(): string
{
return config('services.acme.base_url') . '/oauth/authorize';
}
}Usage in a controller:
php
// Redirect to OAuth provider
public function redirect(AcmeOAuthConnector $connector)
{
return $connector->getAuthorizationUrl(
scopes: ['read', 'write'],
state: session()->get('state'),
);
}
// Handle callback
public function callback(Request $request, AcmeOAuthConnector $connector)
{
$authenticator = $connector->getAccessToken(
code: $request->query('code'),
);
// Store for later — use encrypted cast on your model
$user->update(['acme_token' => $authenticator]);
}Store OAuth tokens with the provided Eloquent cast:
php
use Saloon\Laravel\Casts\EncryptedOAuthAuthenticatorCast;
class User extends Authenticatable
{
protected function casts(): array
{
return [
'acme_token' => EncryptedOAuthAuthenticatorCast::class,
];
}
}创建OAuth2连接器:
bash
php artisan saloon:connector Acme AcmeOAuthConnectorphp
use Saloon\Http\Connector;
use Saloon\Http\Auth\AccessTokenAuthenticator;
use Saloon\Traits\OAuth2\AuthorizationCodeGrant;
class AcmeOAuthConnector extends Connector
{
use AuthorizationCodeGrant;
public function resolveBaseUrl(): string
{
return config('services.acme.base_url');
}
protected function resolveAccessTokenUrl(): string
{
return config('services.acme.base_url') . '/oauth/token';
}
protected function resolveAuthorizationUrl(): string
{
return config('services.acme.base_url') . '/oauth/authorize';
}
}在控制器中使用:
php
// 重定向到OAuth提供商
public function redirect(AcmeOAuthConnector $connector)
{
return $connector->getAuthorizationUrl(
scopes: ['read', 'write'],
state: session()->get('state'),
);
}
// 处理回调
public function callback(Request $request, AcmeOAuthConnector $connector)
{
$authenticator = $connector->getAccessToken(
code: $request->query('code'),
);
// 存储以供后续使用 — 在模型上使用加密转换
$user->update(['acme_token' => $authenticator]);
}使用提供的Eloquent转换存储OAuth令牌:
php
use Saloon\Laravel\Casts\EncryptedOAuthAuthenticatorCast;
class User extends Authenticatable
{
protected function casts(): array
{
return [
'acme_token' => EncryptedOAuthAuthenticatorCast::class,
];
}
}Building SDKs
构建SDK
Organize connector methods as a clean SDK interface:
php
class AcmeConnector extends Connector
{
public function getUser(string $username): Response
{
return $this->send(new GetUserRequest($username));
}
public function listProjects(string $username): Response
{
return $this->send(new ListProjectsRequest($username));
}
public function createProject(string $name, bool $private = false): Response
{
return $this->send(new CreateProjectRequest($name, $private));
}
}Usage becomes expressive:
php
$acme = new AcmeConnector(token: config('services.acme.token'));
$user = $acme->getUser('johndoe')->dto();
$projects = $acme->listProjects('johndoe')->collect('data');将连接器方法组织为简洁的SDK接口:
php
class AcmeConnector extends Connector
{
public function getUser(string $username): Response
{
return $this->send(new GetUserRequest($username));
}
public function listProjects(string $username): Response
{
return $this->send(new ListProjectsRequest($username));
}
public function createProject(string $name, bool $private = false): Response
{
return $this->send(new CreateProjectRequest($name, $private));
}
}使用方式更具表达性:
php
$acme = new AcmeConnector(token: config('services.acme.token'));
$user = $acme->getUser('johndoe')->dto();
$projects = $acme->listProjects('johndoe')->collect('data');Telescope & Pulse Integration
Telescope & Pulse集成
Use the Laravel HTTP sender for Telescope and Pulse visibility:
php
// config/saloon.php
'default_sender' => \Saloon\Laravel\HttpSender::class,使用Laravel HTTP发送器以在Telescope和Pulse中可见:
php
// config/saloon.php
'default_sender' => \Saloon\Laravel\HttpSender::class,Common Pitfalls
常见陷阱
- Forgetting to implement interface when sending request body data.
HasBody - Not using Artisan commands (,
saloon:connector) to generate classes.saloon:request - Using the wrong HTTP method enum — always use .
Saloon\Enums\Method - Forgetting to install for pagination (required in v3).
saloonphp/pagination-plugin - Not setting in config when Telescope or Pulse visibility is needed.
HttpSender - Not mocking all requests in tests — use to prevent stray requests.
Saloon::fake()
- 发送请求体数据时忘记实现接口。
HasBody - 未使用Artisan命令(,
saloon:connector)生成类。saloon:request - 使用错误的HTTP方法枚举——务必使用。
Saloon\Enums\Method - 实现分页时忘记安装(v3版本必填)。
saloonphp/pagination-plugin - 需要Telescope或Pulse可见性时未在配置中设置。
HttpSender - 测试中未模拟所有请求——使用阻止未模拟请求。
Saloon::fake()