saloon-for-laravel

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Saloon 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
    app/Http/Integrations/{ServiceName}/
    (configurable in
    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-plugin
This installs both
saloonphp/saloon
(core) and the Laravel integration. Publish the config file:
bash
php artisan vendor:publish --tag=saloon-config
Optional 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/writing
bash
composer require saloonphp/laravel-plugin
这会同时安装
saloonphp/saloon
(核心包)和Laravel集成插件。发布配置文件:
bash
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.php
app/Http/Integrations/
└── Acme/
    ├── AcmeConnector.php
    ├── Requests/
    │   ├── GetUserRequest.php
    │   ├── ListProjectsRequest.php
    │   └── CreateProjectRequest.php
    ├── Responses/
    │   └── AcmeResponse.php
    └── Data/
        └── UserDTO.php

Creating Connectors

创建连接器

Generate with Artisan:
bash
php artisan saloon:connector Acme AcmeConnector
A 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 GetUserRequest
Define 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
HasBody
interface with a body trait on the request:
php
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
):
TraitUse Case
HasJsonBody
JSON payloads (
application/json
)
HasFormBody
Form data (
application/x-www-form-urlencoded
)
HasMultipartBody
File uploads (
multipart/form-data
)
HasXmlBody
XML payloads
HasStringBody
Raw string body
HasStreamBody
Stream/binary body
在请求中实现
HasBody
接口并使用请求体 trait:
php
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使用场景
HasJsonBody
JSON 负载(
application/json
HasFormBody
表单数据(
application/x-www-form-urlencoded
HasMultipartBody
文件上传(
multipart/form-data
HasXmlBody
XML 负载
HasStringBody
原始字符串请求体
HasStreamBody
流/二进制请求体

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
AlwaysThrowOnErrors
to throw exceptions automatically on failed responses:
php
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;
}
使用
AlwaysThrowOnErrors
在响应失败时自动抛出异常:
php
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
createDtoFromResponse
on a request to cast responses:
php
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();
在请求中实现
createDtoFromResponse
以转换响应:
php
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
boot
method for one-time setup:
php
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()]);
        });
    }
}
使用
boot
方法进行一次性设置:
php
public function boot(PendingRequest $pendingRequest): void
{
    $pendingRequest->config()->add('timeout', 60);
}

Testing with Saloon Facade

使用Saloon门面进行测试

The Laravel plugin provides a
Saloon
facade for fluent test mocking:
php
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插件提供
Saloon
门面用于流畅的测试模拟:
php
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-plugin
Implement the
HasPagination
interface on your connector:
php
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:
PagedPaginator
,
OffsetPaginator
,
CursorPaginator
.
安装分页插件:
bash
composer require saloonphp/pagination-plugin
在连接器上实现
HasPagination
接口:
php
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();
可用的分页器类型:
PagedPaginator
,
OffsetPaginator
,
CursorPaginator

Rate Limiting

速率限制

Install the rate limit plugin:
bash
composer require saloonphp/rate-limit-plugin
Add 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-plugin
Add 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 AcmeOAuthConnector
php
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 AcmeOAuthConnector
php
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
    HasBody
    interface when sending request body data.
  • Not using Artisan commands (
    saloon:connector
    ,
    saloon:request
    ) to generate classes.
  • Using the wrong HTTP method enum — always use
    Saloon\Enums\Method
    .
  • Forgetting to install
    saloonphp/pagination-plugin
    for pagination (required in v3).
  • Not setting
    HttpSender
    in config when Telescope or Pulse visibility is needed.
  • Not mocking all requests in tests — use
    Saloon::fake()
    to prevent stray requests.
  • 发送请求体数据时忘记实现
    HasBody
    接口。
  • 未使用Artisan命令(
    saloon:connector
    ,
    saloon:request
    )生成类。
  • 使用错误的HTTP方法枚举——务必使用
    Saloon\Enums\Method
  • 实现分页时忘记安装
    saloonphp/pagination-plugin
    (v3版本必填)。
  • 需要Telescope或Pulse可见性时未在配置中设置
    HttpSender
  • 测试中未模拟所有请求——使用
    Saloon::fake()
    阻止未模拟请求。