Files
alfa-bank-sdk/docs/xml/saloon_docs.xml
Anatoliy Poloz 6a9243d969 update docs
2026-06-08 11:43:18 +03:00

7747 lines
273 KiB
XML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?xml version="1.0" encoding="UTF-8"?>
<laravel_api_calls_saloon_lib_docs>
<conclusion>
<credits_md>
<![CDATA[
# 🤗 Credits
### Special Thanks
* [Juse Less](https://github.com/juse-less) for contributing to many core features of Saloon v2 but mainly pagination and debugging.
* [Tom Witkowski](https://github.com/Gummibeer) for contributing to Saloon's fixture recording and helping decide many core parts of version two like data transfer objects and the error handler.
* [Caneco](https://github.com/caneco) for such a wonderful logo and branding.
* [Ashley Allen](https://github.com/ash-jc-allen) for working closely with me and writing an amazing book that features Saloon ([Consuming APIs in Laravel](https://consuming-apis-in-laravel.com/)). Ashley made countless contributions to the documentation and helped make decisions for v3.
### But also... Thank _you_.
This is my chance to thank you - the amazing PHP community for installing, using and contributing to Saloon's success. I absolutely love working on open source and working within the PHP community as it feels so rewarding. It's an absolute honour to build something that people enjoy and actively use. I hope it will be useful for many years ahead..
[**Sam Carré**](https://github.com/sammyjo20)
]]>
</credits_md>
<how_to_guides>
<improving_speed_with_laravel_md>
<![CDATA[
# Improving Speed With Laravel
If you are using Laravel, then you are probably aware of its powerful container that keeps objects open in the background while you make requests or process jobs. Saloon uses Guzzle under the hood, which keeps connections open for as long as it can - which is why subsequent requests using the same connector is usually faster. Typically though, the Guzzle HTTP client will be destructed and connections closed between requests/jobs. This results in increased request times if you are doing lots of API calls across jobs or requests because it is establishing a new connection each time.
If you are using Laravel, you can bind an instance of Saloon's `GuzzleSender` inside of its container and it will keep connections open between queued jobs and requests (if you are using Octane). This can provide a nice speed boost if you are using the same API service, especially if you are frequently accessing an API, like migrating data from an external API into your application.
First, you should create a new singleton in one of your service provider's register methods, and return a new instance of `GuzzleSender`. The `AppServiceProvider` is a good place to put this, or you could create a new `HttpServiceProvider` for this.
```php
<?php
use Saloon\Http\Senders\GuzzleSender;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
$this->app->singleton(GuzzleSender::class, fn () => new GuzzleSender);
}
}
```
Now, inside your Saloon connectors, you should overwrite the `defaultSender` method and resolve the sender which you have bound to the container.
```php
<?php
use Saloon\Http\Connector;
use Saloon\Contracts\Sender;
use Saloon\Http\Senders\GuzzleSender;
class SpotifyConnector extends Connector
{
protected function defaultSender(): Sender
{
return resolve(GuzzleSender::class);
}
}
```
Now when you make requests with this connector, it will re-use the same Guzzle sender and the same Guzzle client under the hood!
]]>
</improving_speed_with_laravel_md>
<per_request_authentication_md>
<![CDATA[
# Per-request Authentication
Some API integrations require you to make a request to create an access token that a subsequent request will use. When building this, it can add a level of complexity to your application because you have to make two requests for every single resource. Let's say you have two requests for an API. You have:
* GetAccessTokenRequest
* GetSongsByArtistRequest
But in order to make an API call `GetSongsByArtistRequest` you must obtain an access token by calling `GetAccessTokenRequest` first. Let's also assume that the `GetAccessTokenRequest` requires a username and password. Let's see how this may be implemented currently in your application.
```php
<?php
$api = new MusicApi();
$authResponse = $api->send(new GetAccessTokenRequest(username: 'Sam', password: 'yee-haw'));
$token = $authResponse->json()['token'];
$songsRequest = new GetSongsByArtistRequest('Luke Combs');
$songsRequest->withTokenAuth($token);
$response = $api->send($songsRequest);
```
This is pretty cumbersome. Every time you have to make a request, you have to write all these lines of code. One way you can get around this is by utilizing the `boot` method on your connector. Let's try refactoring this.
In our connector, we'll extend the `boot()` method which is a method that runs before every request that is sent through that connector, and you get access to the special `PendingRequest` class which is a class that is used to store all the information of a request to be sent in a temporary way. We'll move our logic into here.
```php
<?php
use Saloon\Http\PendingRequest;
use Saloon\Http\Auth\TokenAuthenticator;
class MusicApi extends Connector
{
// You should move any authentication requirements into the constructor
// of the connector so your users have to provide the authentication
// requirements before instantiating the connector.
public function __construct(
protected string $username,
protected string $password,
)
{
//
}
public function boot(PendingRequest $pendingRequest): void
{
// Let's start by returning early if the request being sent is the
// GetAccessTokenRequest. We don't want to create an infinite loop
if ($pendingRequest->getRequest() instanceof GetAccessTokenRequest) {
return;
}
// Now let's make our authentication request. Since we are in the
// context of the connector, we can just simply call $this and
// make another request!
$authResponse = $this->send(new GetAccessTokenRequest($this->username, $this->password));
// Now we'll take the token from the auth response and then pass it
// into the $pendingRequest which is the original GetSongsByArtistRequest.
$pendingRequest->authenticate(new TokenAuthenticator($authResponse->json()['token']));
}
}
```
Now when we send any request using this connector, the authentication will be handled every time! We do have to make sure to pass in our authentication requirements (username, password) into the connector, but this is way better than repeating the same code. Saloon is all about standardisation!
```php
<?php
$api = new MusicApi(
username: 'Sam',
password: 'yee-haw',
);
$response = $api->send(new GetSongsByArtistRequest('Luke Combs'));
// 200 body: [{name: 'When it rains, it pours'}]
```
You may also write additional logic inside of the boot method, like caching access tokens and reusing them.
]]>
</per_request_authentication_md>
<README_md>
<![CDATA[
---
description: >-
This page contains a collection of useful start-to-finish guides for doing
specific things in Saloon.
---
# 📔 How-to Guides
]]>
</README_md>
</how_to_guides>
<known_issues_md>
<![CDATA[
# 💥 Known Issues
### Usage of anonymous functions with long-running processes like Laravel Queues
There appears to be an issue with PHP which means any object that has an anonymous function inside of one of its properties will result in it not properly being destructed or running the `__destruct()` method. With Saloon, take care when using anonymous functions inside your connector or solo request because we have found that if you have an anonymous function/closure inside of the connector and when running it from within a long-running process like Laravel Queues, it won't close connections.
This is because as mentioned above, the connector isn't able to be appropriately destructed which results in the sender and the sender's HTTP client like Guzzle not being able to be destructed and finally resulting in a "leak" of connections. This can cause further issues like the operating system running out of file handles and causing errors like "Too many files open".&#x20;
You can workaround this by changing the way you define your anonymous closure. You can:
* Move the logic into an invokable class
* Make the closure _static_
```php
<?php
class SDK extends Connector
{
protected Closure $callable;
public function __construct()
{
// ❌ This will cause connection leaks
$this->middleware()->onRequest(function () {
//
});
// ✅ Using an invokable class will work
$this->middleware()->onRequest(new MyRequestMiddleware);
// ✅ Using static will work
$this->middleware()->onRequest(static function () {
//
});
}
}
```
### Minimum TLS Version
From version three, Saloon uses Guzzle `^7.6` which introduced a new option to configure a minimum TLS version. With Saloon v3, Saloon has set this default to **TLS 1.2.** If any of your API integrations require a lower level of TLS security, you can change the Saloon config while your application is loading.
```php
use Saloon\Config;
Config::$defaultTlsMethod = STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
```
You can [visit this page on the PHP documentation](https://www.php.net/manual/en/function.stream-socket-enable-crypto.php) to see the different TLS options available.&#x20;
{% hint style="info" %}
**TLS 1.1** has been deprecated since 2021 so it is unlikely that APIs are still using it, but older systems may have not upgraded yet.
{% endhint %}
]]>
</known_issues_md>
<official_book_md>
<![CDATA[
# 📘 Official Book!
<figure><img src="../.gitbook/assets/horizontal-creative.png" alt=""><figcaption></figcaption></figure>
We're super excited to partner with [Ash Allen](https://twitter.com/AshAllenDesign) with his latest book - _Consuming APIs in Laravel**.**_ This book with over 440 pages contains everything you need to build awesome API integrations in Laravel. The book uses Saloon as the tool to build the integrations, so it's a great way to learn Saloon inside out too!
You'll also learn about the most common types of APIs, How OAuth2 works and how to handle webhooks securely.
{% hint style="info" %}
Use code **SALOON** at checkout to get **20% off** the "Consuming APIs in Laravel" book or the book bundle with "Battle Ready Laravel"!
{% endhint %}
[Pick up your copy today at consuming-apis-in-laravel.com!](https://consuming-apis-in-laravel.com/?aff=DGMrX)
]]>
</official_book_md>
<showcase_md>
<![CDATA[
# 🎖 Showcase
{% hint style="info" %}
Want to add your application or SDK to Saloon's showcase page? Edit this page on Github! 🤠
{% endhint %}
<table data-card-size="large" data-view="cards">
<thead>
<tr>
<th></th>
<th></th>
<th data-hidden></th>
<th data-hidden></th>
<th data-hidden data-card-cover data-type="files"></th>
<th data-hidden data-card-target data-type="content-ref"></th>
</tr>
</thead>
<tbody>
<tr>
<td>🚨</td>
<td>ohdearapp/ohdear-php-sdk</td>
<td>1</td>
<td></td>
<td></td>
<td><a href="https://github.com/ohdearapp/ohdear-php-sdk">https://github.com/ohdearapp/ohdear-php-sdk</a></td>
</tr>
<tr>
<td>🌲</td>
<td>astrotomic/ecologi-sdk</td>
<td>1</td>
<td></td>
<td></td>
<td><a href="https://github.com/Astrotomic/ecologi-sdk">https://github.com/Astrotomic/ecologi-sdk</a></td>
</tr>
<tr>
<td>🪐</td>
<td>astrotomic/tmdb-sdk</td>
<td>2</td>
<td></td>
<td></td>
<td><a href="https://github.com/Astrotomic/tmdb-sdk">https://github.com/Astrotomic/tmdb-sdk</a></td>
</tr>
<tr>
<td>💰</td>
<td>myoutdeskllc/salesforce-php</td>
<td>3</td>
<td></td>
<td></td>
<td><a href="https://github.com/MyOutDeskLLC/SalesforcePhp">https://github.com/MyOutDeskLLC/SalesforcePhp</a></td>
</tr>
<tr>
<td>🤑</td>
<td>sawirricardo/midtrans-api</td>
<td></td>
<td></td>
<td></td>
<td><a href="https://github.com/sawirricardo/midtrans-api">https://github.com/sawirricardo/midtrans-api</a></td>
</tr>
<tr>
<td>📃</td>
<td>carboneio/carbone-sdk-php</td>
<td></td>
<td></td>
<td></td>
<td><a href="https://github.com/carboneio/carbone-sdk-php">https://github.com/carboneio/carbone-sdk-php</a></td>
</tr>
<tr>
<td>📞</td>
<td>samuelmwangiw/africastalking-laravel</td>
<td></td>
<td></td>
<td></td>
<td><a href="https://github.com/SamuelMwangiW/africastalking-laravel">https://github.com/SamuelMwangiW/africastalking-laravel</a></td>
</tr>
<tr>
<td>💻</td>
<td>samuelmwangiw/linode</td>
<td></td>
<td></td>
<td></td>
<td><a href="https://github.com/SamuelMwangiW/linode">https://github.com/SamuelMwangiW/linode</a></td>
</tr>
<tr>
<td>🌱</td>
<td>alphaolomi/json-placeholder</td>
<td></td>
<td></td>
<td></td>
<td><a href="https://github.com/alphaolomi/json-placeholder">https://github.com/alphaolomi/json-placeholder</a></td>
</tr>
<tr>
<td>💬</td>
<td>flourishlabs/saloon-slack</td>
<td></td>
<td></td>
<td></td>
<td><a href="https://github.com/flourishlabs/saloon-slack">https://github.com/flourishlabs/saloon-slack</a></td>
</tr>
<tr>
<td>📦</td>
<td>alphaolomi/laravel-repman</td>
<td></td>
<td></td>
<td></td>
<td><a href="https://github.com/alphaolomi/laravel-repman">https://github.com/alphaolomi/laravel-repman</a></td>
</tr>
<tr>
<td>📊</td>
<td>njoguamos/laravel-plausible</td>
<td></td>
<td></td>
<td></td>
<td><a href="https://github.com/njoguamos/laravel-plausible">https://github.com/njoguamos/laravel-plausible</a></td>
</tr>
<tr>
<td>🧠</td>
<td>probots-io/pinecone-php</td>
<td></td>
<td></td>
<td></td>
<td><a href="https://github.com/probots-io/pinecone-php">https://github.com/probots-io/pinecone-php</a></td>
</tr>
<tr>
<td>🇨🇴</td>
<td>felipeva/api-colombia-php</td>
<td></td>
<td></td>
<td></td>
<td><a href="https://github.com/felipeva/api-colombia-php">https://github.com/felipeva/api-colombia-php</a></td>
</tr>
<tr>
<td>🤝</td>
<td>deschutesdesigngroupllc/perscom-php-sdk</td>
<td></td>
<td></td>
<td></td>
<td><a href="https://github.com/DeschutesDesignGroupLLC/perscom-php-sdk">https://github.com/DeschutesDesignGroupLLC/perscom-php-sdk</a></td>
</tr>
<tr>
<td>✈️</td>
<td>noahnxt/laravel-open-holiday-api</td>
<td></td>
<td></td>
<td></td>
<td><a href="https://github.com/NoahNxT/laravel-open-holidays-api">https://github.com/NoahNxT/laravel-open-holidays-api</a></td>
</tr>
<tr>
<td>💰</td>
<td>njoguamos/laravel-pesapal</td>
<td></td>
<td></td>
<td></td>
<td><a href="https://github.com/njoguamos/laravel-pesapal">https://github.com/njoguamos/laravel-pesapal</a></td>
</tr>
<tr>
<td>🔖</td>
<td>oneduo/github-sdk-php</td>
<td></td>
<td></td>
<td></td>
<td><a href="https://github.com/oneduo/github-sdk-php">https://github.com/oneduo/github-sdk-php</a></td>
</tr>
<tr>
<td>💳</td>
<td>lloricode/paymaya-sdk-php</td>
<td></td>
<td></td>
<td></td>
<td><a href="https://github.com/lloricode/paymaya-sdk-php">https://github.com/lloricode/paymaya-sdk-php</a></td>
</tr>
</tbody>
</table>
]]>
</showcase_md>
<tutorials_md>
<![CDATA[
# 📖 Tutorials & Blog Posts
### Video Tutorials
#### Wrangling APIs in the Wild West using Saloon
{% embed url="https://www.youtube.com/watch?v=ZV00ggpWfBU" %}
Created by Sam Carré
{% endembed %}
#### Saloon - A PHP package to simplify API integrations
{% embed url="https://www.youtube.com/watch?v=r3AU6uYQGt4" %}
Created by Andrew Schmelyun
{% endembed %}
#### Demystifying Saloon: A Step-by-Step Guide
{% embed url="https://www.youtube.com/watch?v=Pl51BkVTn8M" %}
Created by Ruslan Steiger
{% endembed %}
#### Integrate an API with Laravel and Saloon (FR)
{% embed url="https://www.youtube.com/watch?v=8rPXqmBG99c" %}
Created by Nord Coders
{% endembed %}
## API Integrations using Saloon in Laravel
Steve McDougall wrote a super in-depth tutorial for building a GitHub integration in Laravel using Saloon that enabled you to see a status of repository workflows. It's a great example of using Saloon and is good to follow if you would like to get started
[https://laravel-news.com/api-integrations-using-saloon-in-laravel](https://laravel-news.com/api-integrations-using-saloon-in-laravel)
### Podcast
Want to \*hear\* more about Saloon? The creator of Saloon, Sam Carré and Jake Bennet recorded an episode all about how Saloon works on the North Meets South podcast. Click on the link below to listen to the podcast episode.
#### [Listen on their website](https://www.northmeetssouth.audio/112)
[**Listen on Apple Podcasts**](https://podcasts.apple.com/us/podcast/north-meets-south-web-podcast/id1123508667?i=1000551046555)
[**Listen on Spotify**](https://open.spotify.com/episode/1oT4LC4bQSx2PNNMQDAkIc?si=b695129aa0d34327\&nd=1)
]]>
</tutorials_md>
</conclusion>
<digging_deeper>
<concurrency_and_pools_md>
<![CDATA[
# 🏎 Concurrency & Pools
Saloon supports request concurrency and requests pools out of the box. This allows you to make multiple API calls to the same service while keeping the CURL connection open. Under the hood, it uses PHP's cURL multi-handler to keep the connection open, this results in huge speed benefits when making lots of API calls. Saloon's concurrency is powered by [Guzzle's implementation](https://docs.guzzlephp.org/en/stable/quickstart.html?highlight=pool#concurrent-requests) behind the scenes.
One of Laravel's core members, Nuno Maduro [wrote a great blog post](https://nunomaduro.com/speed\_up\_your\_php\_http\_guzzle\_requests\_with\_concurrency) about request concurrency and its performance with Guzzle directly. The same performance can be shared with Saloon's implementation as Saloon uses Guzzle behind the scenes.
{% hint style="info" %}
Concurrency is only supported with the `GuzzleSender` and `HttpSender` senders for Saloon. The default sender out of the box with Saloon is the GuzzleSender.
{% endhint %}
We conducted our own benchmark by making 1,000 API calls to our internal testing API, running on a virtual server with 2 vCPUs and 2GB of RAM. The results were impressive, as all 1,000 API calls were completed in just **2.5 seconds**, a significant improvement compared to 60 seconds when using synchronous requests.
```php
<?php
$connector = new TestConnector;
// ⬇️ Takes 60 seconds... 😪
$requests = function () {
for ($i = 0; $i < 1000; $i++) {
$connector->send(new GetUserRequest);
}
};
// ⬇️ Takes 2.5 seconds! 🔥
$requests = function () {
for ($i = 0; $i < 1000; $i++) {
yield new GetUserRequest;
}
};
$connector->pool($requests, concurrency: 10)->send()->wait();
```
### Getting Started
Saloon's pooling has been designed specifically to be easy to use. Simply instantiate your connector class and use the `pool` method to create a pool. This method doesn't require any arguments, but you may provide requests, concurrency and handlers right from this method.
```php
<?php
$forge = new ForgeConnector;
// Pool has various optional parameters if you want to set them here...
$pool = $forge->pool(
requests: [],
concurrency: 5,
responseHandler: function () { ... },
exceptionHandler: function () { ... },
);
// Initiate the transfer of requests
$promise = $pool->send();
// Force all the requests to be fulfilled
$promise->wait();
```
### Available Methods
* `setRequests(callable|iterable $requests)`
* `getRequests()`
* `withResponseHandler(callable $callable)`
* `withExceptionHandler(callable $callable)`
* `send()`
### Providing Requests
The pool class accepts many types of requests:
* Array of requests
* PHP generator providing requests
* Closure or Invokable class returning an array of requests
* Closure or Invokable class returning a generator
You may provide these requests into the pool either as the first argument of the `pool` method or using the `setRequests` method. The requests can be instances of `Saloon\Http\Request` or `Saloon\Http\PendingRequest`.
#### Array of requests
The simplest way to provide requests to the pool is an array.
```php
<?php
$forge = new ForgeConnector;
$forge->pool([
new GetServersRequest,
new GetSitesRequest,
new GetUserRequest,
]);
// Or
$pool = $forge->pool();
$pool->setRequests([
new GetServersRequest,
new GetSitesRequest,
new GetUserRequest,
]);
```
#### Using a PHP generator
If you are going to send lots of requests you may wish to provide a generator into the pool. This allows you to keep memory consumption low and potentially send hundreds of requests.
```php
<?php
$generatorCallback = function (): Generator {
for ($i = 0; $i < 3; $i++) {
yield $i => new UserRequest;
}
};
$forge = new ForgeConnector;
$pool = $forge->pool($generatorCallback);
// or
$pool->setRequests($generatorCallback);
```
#### Using a callable or invokable class
You may wish to use a callable method to return an array of requests or a generator, this is useful if you have any additional logic that needs to execute just before the request pool begins.
{% tabs %}
{% tab title="Callable" %}
```php
<?php
$callback = function () {
return [
new GetServersRequest,
new GetSitesRequest,
new GetUserRequest,
];
};
$forge = new Forge;
$pool = $forge->pool($callback);
// or
$pool->setRequests($callback);
```
{% endtab %}
{% tab title="Invokable Class" %}
```php
<?php
class PoolClass {
public function __invoke() {
return [
new GetServersRequest,
new GetSitesRequest,
new GetUserRequest,
];
}
}
$forge = new ForgeConnector;
$pool = $forge->pool(new PoolClass);
```
{% endtab %}
{% endtabs %}
### Concurrency
By default, Saloon will send up to 5 requests concurrently. You can customise the concurrency that is used by pools with the `setConcurrency` method. It accepts an integer or a callable like a method or an invokable class in case you want to write advanced logic to determine the concurrency.
```php
<?php
$forge = new ForgeConnector;
$pool = $forge->pool(...);
$pool->setConcurrency(10);
// or
$pool->setConcurrency(function () {
return 10;
});
```
### Response Handlers
When you send requests with pooling, each request is asynchronous, so you cannot guarantee when they are returned. In order to handle the response, Saloon has provided you with two handlers to handle successful requests and failed requests.
#### Handling Successful Requests
Any responses that are successful will be handled by the response handler. You may add this with the constructor of your pool or add it once it is created. You can only have one handler. You will get a response instance.
```php
<?php
use Saloon\Http\Response;
$pool = $forge->pool(
requests: [],
concurrency: 5,
responseHandler: function (Response $response) {
// Handle Response
},
);
// Or
$pool->withResponseHandler(function (Response $response) {
// Handle Response
});
```
#### Handling Failed Requests
When requests fail, they will always be caught with the error handler, even if you don't throw on requests. When a failed request happens you can handle the exception with the `withExceptionHandler` method.
```php
<?php
use Saloon\Http\Response;
use Saloon\Exceptions\Request\RequestException;
use Saloon\Exceptions\Request\FatalRequestException;
$pool = $forge->pool(
requests: [],
concurrency: 5,
exceptionHandler: function (FatalRequestException|RequestException $exception) {
// Handle Exception
},
);
// Or
$pool->withExceptionHandler(function (FatalRequestException|RequestException $exception) {
// Handle Exception
});
```
### Named/Keyed Requests
Saloon also supports keyed responses to help you easily track exact requests that have been sent. This is especially useful if you are sending requests to different endpoints. You may use keying with arrays or even with a generator.
```php
<?php
$pool = $forge->pool([
'servers' => new GetServersRequest,
'sites' => new GetSitesRequest,
'user' => new GetUserRequest,
]);
// You may access the key in the response and error handlers
$pool->withResponseHandler(function (Response $response, string $key) {
match($key) {
'servers' => $this->updateServersList($response),
'sites' => $this->updateSitesList($response),
'user' => $this->updateUser($response),
}
});
```
### Sending
Once you have provided the pool with requests, you are ready to send them. Just use the `send` method on the pool. This will return an instance of `PromiseInterface` . Requests will be handled asynchronously but you can force them to complete with the `wait` method.
```php
<?php
$forge = new ForgeConnector;
$pool = $forge->pool([
new GetServersRequest,
new GetSitesRequest,
new GetUserRequest,
]);
$pool->withResponseHandler(function (Response $response) {
// Handle Response
});
$pool->withExceptionHandler(function (FatalRequestException|RequestException $exception) {
// Handle Exception
});
// Initiate the transfer of requests
$promise = $pool->send();
// Force all the requests to be fulfilled
$promise->wait();
```
]]>
</concurrency_and_pools_md>
<delay_md>
<![CDATA[
# ⏸ Delaying Requests
Sometimes in your application, you may want to introduce a delay in your requests. This may be to avoid hitting rate limits or to avoid overloading a development environment. With Saloon, you may use the `defaultDelay` and `delay` methods on either your connector or request to define a delay in milliseconds.
### Default Delay
You may also wish to define a default delay on your connector or request. You can do this by extending the `defaultDelay` method.&#x20;
{% tabs %}
{% tab title="Connector" %}
```php
<?php
class ForgeConnector extends Connector
{
// Every request sent through this connector will have a 500ms delay.
protected function defaultDelay(): ?int
{
return 500;
}
}
```
{% endtab %}
{% tab title="Request" %}
```php
<?php
class GetServersRequest extends Request
{
// Every time this request is sent, a 500ms delay is added
protected function defaultDelay(): ?int
{
return 500;
}
}
```
{% endtab %}
{% endtabs %}
### Using the delay methods
You can also apply a delay to a connector or a request on the fly with the `delay()` method.
{% tabs %}
{% tab title="Connector" %}
```php
<?php
$forge = new ForgeConnector;
// Delay every request sent with the ForgeConnector by 500ms
$forge->delay()->set(500);
```
{% endtab %}
{% tab title="Request" %}
```php
<?php
$request = new GetServersRequest;
// Delay just this request instance by 500ms
$request->delay()->set(500);
```
{% endtab %}
{% endtabs %}
{% hint style="warning" %}
If you have defined the delay on both the connector and the request, the request delay will take priority.&#x20;
{% endhint %}
]]>
</delay_md>
<middleware_md>
<![CDATA[
# 💫 Middleware
Saloon has a powerful middleware system that allows you to tap into the request and response lifecycle and make any changes you need before the request is sent or the response is given back to the user. This is useful if you want to build your own advanced Saloon integrations or write more advanced logic like generating a unique reference for every request.
### The Boot Method
Before we get into Saloon's middleware, there is a useful built-in method on every connector and request that you can utilize. This is the `boot` method. It gets executed every time you send a request. You will get access to the underlying `PendingRequest` instance that the sender will provide to the HTTP client. The boot method is a great way to quickly tap into a pending request and change something like add a header, modify the request body or even trigger events.
You may extend the public boot method on either your connector or your request. Remember if you use the connector's boot method, every request with that connector will use that boot method.
You may register middleware inside of the boot method too, which will be used in the rest of the `PendingRequest` lifecycle.
{% tabs %}
{% tab title="Connector" %}
```php
<?php
use Saloon\Http\Connector;
use Saloon\Http\PendingRequest;
class ForgeConnector extends Connector
{
// ...
public function boot(PendingRequest $pendingRequest): void
{
$pendingRequest->headers()->add('X-Current-Time', new DateTime);
}
}
```
{% endtab %}
{% tab title="Request" %}
```php
<?php
use Saloon\Http\Request;
use Saloon\Http\PendingRequest;
class GetServersRequest extends Request
{
// ...
public function boot(PendingRequest $pendingRequest): void
{
$pendingRequest->headers()->add('X-Current-Time', new DateTime);
}
}
```
{% endtab %}
{% endtabs %}
### The PendingRequest
The `PendingRequest` class is an intermediary class that Saloon uses to populate everything when you send a request. Every time you send a request, a new `PendingRequest` instance is created which prevents your connector or request from being mutated every time a request is sent. The `PendingRequest` class has many of the methods that you are used to seeing on the request/connector for managing headers, query parameters, config, and request body.
The `PendingRequest` class is used for boot methods, middleware and [plugins](../installable-plugins/traits.md).
### Request Middleware
You may also at any point tap into the request lifecycle by using request middleware. Request middleware is useful for changing something on the `PendingRequest` instance before the request is sent like making another request to get an authentication token for the original request.
On your connector or request, call the `middleware()` method and use the `onRequest()` method. You should provide a callable, like a closure or invokable class. This callable will be given access to the `PendingRequest` instance which can be mutated.
{% hint style="info" %}
Return values are not required, but you may either return an instance of `PendingRequest` or a `FakeResponse` class for an early fake response.
{% endhint %}
#### Anonymous Functions
You can use a regular closure/anonymous function to create a middleware on the fly.
{% tabs %}
{% tab title="Connector" %}
```php
<?php
use Saloon\Http\PendingRequest;
$forge = new ForgeConnector;
$forge->middleware()->onRequest(function (PendingRequest $pendingRequest) {
$pendingRequest->headers()->add('Authorization', 'Bearer ' . $token);
});
```
{% endtab %}
{% tab title="Request" %}
```php
<?php
use Saloon\Http\PendingRequest;
$request = new GetServersRequest;
$request->middleware()->onRequest(function (PendingRequest $pendingRequest) {
$pendingRequest->headers()->add('Authorization', 'Bearer ' . $token);
});
```
{% endtab %}
{% endtabs %}
#### Invokable Classes
You may also use invokable classes to keep your middleware classes tidy. If you are using invokable middleware classes, you should implement the `RequestMiddleware` interface.
{% tabs %}
{% tab title="Definition" %}
```php
<?php
use Saloon\Http\PendingRequest;
use Saloon\Contracts\RequestMiddleware;
class AuthenticateRequest implements RequestMiddleware
{
public function __invoke(PendingRequest $pendingRequest): void
{
$pendingRequest->headers()->add('Authorization', 'Bearer ' . $token);
}
}
```
{% endtab %}
{% tab title="Usage (Connector)" %}
```php
<?php
use Saloon\Http\PendingRequest;
$forge = new ForgeConnector;
$forge->middleware()->onRequest(new AuthenticateRequest);
```
{% endtab %}
{% tab title="Usage (Request)" %}
```php
<?php
use Saloon\Http\PendingRequest;
$request = new GetServersRequest;
$request->middleware()->onRequest(new AuthenticateRequest);
```
{% endtab %}
{% endtabs %}
### Early Fake Responses
You may also choose to tap into Saloon's mock functionality by creating your own fake responses. When you return a `FakeResponse`, the rest of the request middleware will still be processed but the fake response will be stored on the `PendingRequest`.
If this fake response is present before Saloon sends the request, it won't send the request to the sender, instead, Saloon will use the `FakeResponse`. This is super handy if you want to build your own middleware that stops Saloon from sending real requests, like for caching.
```php
<?php
use Saloon\Http\PendingRequest;
use Saloon\Http\Faking\FakeResponse;
$request = new GetServersRequest;
$request->middleware()
->onRequest(function (PendingRequest $pendingRequest) {
return new FakeResponse(
body: ['data' => 'Fake Data!'],
status: 200,
headers: []
);
});
```
{% hint style="warning" %}
Even though you are returning a `FakeResponse` class, the next middleware will still receive the PendingRequest instance. Additionally, if another middleware also returns a fake response, the latest one will be used.
{% endhint %}
### Response Middleware
Once you have sent your request, even if it's a fake response, Saloon will send the response down the response middleware pipeline. You may add your own response middleware to change the response class or do something like log responses.
On your connector or request, you can call the `middleware()` method and use the `onResponse()` method. You should provide a callable, like a closure or invokable class. This callable will be given access to the `Response` instance.
{% hint style="info" %}
Return values are not required, but you may return an instance of `Saloon\Http\Response` to overwrite the response class in the middleware.
{% endhint %}
#### Anonymous Functions
You can use a regular closure/anonymous function to create a middleware on the fly.
{% tabs %}
{% tab title="Connector" %}
```php
<?php
use Saloon\Http\Response;
$forge = new ForgeConnector;
$forge->middleware()->onResponse(function (Response $response) {
Logger::recordResponse($response);
});
```
{% endtab %}
{% tab title="Request" %}
```php
<?php
use Saloon\Http\Response;
$request = new GetServersRequest;
$request->middleware()->onResponse(function (Response $response) {
Logger::recordResponse($response);
});
```
{% endtab %}
{% endtabs %}
#### Invokable classes
Similar to request middleware, you can also create invokable middleware classes for response middleware. You should use the `ResponseMiddleware` contract to ensure your invokable class has the correct signature.
{% tabs %}
{% tab title="Definition" %}
```php
<?php
use Saloon\Http\Response;
use Saloon\Contracts\ResponseMiddleware;
class LogResponse implements ResponseMiddleware
{
public function __invoke(Response $response): void
{
Logger::recordResponse($response);
}
}
```
{% endtab %}
{% tab title="Usage (Connector)" %}
```php
<?php
use Saloon\Http\PendingRequest;
$forge = new ForgeConnector;
$forge->middleware()->onResponse(new LogResponse);
```
{% endtab %}
{% tab title="Usage (Request)" %}
```php
<?php
use Saloon\Http\PendingRequest;
$request = new GetServersRequest;
$request->middleware()->onResponse(new LogResponse);
```
{% endtab %}
{% endtabs %}
### Using Constructors
While registering middleware on the fly is really useful, it often leads to repeated code. If you would like your connector to always have a specific request or response middleware you should use the `boot` method described above, or use the constructor of your connector or request.
```php
<?php
class ForgeConnector extends Connector
{
// {...}
public function __construct()
{
$this->middleware()->onRequest(new AuthenticateRequest);
$this->middleware()->onResponse(new LogResponse);
}
}
```
{% hint style="danger" %}
Be cautious using anonymous non-static closures inside the constructor/boot method. This may cause issues like Saloon not being able to close connections properly. [Click here to read more.](../conclusion/known-issues.md#usage-of-anonymous-functions-with-long-running-processes-like-laravel-queues)
{% endhint %}
### Using Plugins
Plugins are another useful feature for Saloon that allows you to extend Saloon and tap into the middleware from traits. This is especially useful if you intend to use the trait on multiple requests or connectors.
[Read through the plugins page for more information.](../installable-plugins/traits.md)
### Middleware Naming
You can choose to name your middleware. Each name must be unique to the given pipeline. For example, you cannot have two middleware with the same name on the request pipeline, but you could have the same name used once for the request pipeline and once for the response pipeline. Use the second argument to specify a name.
```php
<?php
$request = new GetServersRequest;
$request->middleware()->onResponse(new LogResponse, 'logResponse');
```
### Global Middleware
Saloon also supports adding global middleware. You most likely won't need this level of granularity but if you do, you may use the `Config` class. When using global middleware, you should make sure to name your middleware so it isn't accidentally registered twice.
```php
<?php
use Saloon\Config;
Config::globalMiddleware()->onResponse(new LogResponse, 'logResponse');
```
{% hint style="danger" %}
Be cautious with global middleware. Since it uses a static property behind the scenes, the value is kept between tests when running a full test suite. You can use the `Config::clearGlobalMiddleware()` method to get around this issue.
{% endhint %}
### Middleware Execution Order
The following is the order in which middleware is executed.
1. Global middleware (Like the Laravel plugin)
2. Mock client finds a fake response (if present)
3. Plugin middleware
4. User-added middleware
5. Debugging middleware is run (for the final object)
### Choosing When Your Middleware Is Executed
You may choose to change when your middleware is executed. For example, you might want to run something at the very end of all the other middleware that has been run. This is especially useful if you are building a plugin for people to install in Saloon and require the final request object before it is sent. You can use the third argument to specify an order. This expects a `PipeOrder` enum and allows you to choose either `FIRST` or `LAST`.&#x20;
When the middleware is executed, any middleware marked as "first" will run first, and any that have been marked as "last" will be executed last.
<pre class="language-php"><code class="lang-php">&#x3C;?php
use Saloon\Http\Request;
use Saloon\Http\PendingRequest;
use Saloon\Enums\PipeOrder;
class GetServersRequest extends Request
{
// { ... }
public function boot(PendingRequest $pendingRequest): void
{
<strong> $request->middleware()->onRequest(new RecordResponse, order: PipeOrder::LAST);
</strong> }
}
</code></pre>
### Middleware Caveats
Here are some known caveats that you should know about when using Saloon's middleware.
* You cannot add request middleware from inside of another request middleware**,** but you can add response middleware inside of the onRequest() middleware method.
* You cannot add response middleware from inside of another response middleware.
* You may return a fake response in request middleware but you will always get a PendingRequest back
### Guzzle Handlers / Middleware
With previous versions of Saloon, you could add Guzzle middleware or "handlers" directly to the connector or request. From version two, Saloon is now sender-agnostic, so the `addHandler` method has been removed but you may still add Guzzle middleware if you are using the `GuzzleSender` (the default sender with Saloon)
### Adding Guzzle Middleware
You can add middleware to the Guzzle client by using the `sender` method on the connector. You must only add Guzzle middleware directly on your connector with your constructor method. This is because Saloon only instantiates the sender once, so in order to prevent middleware from being registered multiple times, it should be placed in the constructor.
```php
<?php
class ForgeConnector extends Connector
{
// {...}
public function __construct()
{
$this->sender()->addMiddleware(function (callable $handler) {
return function (RequestInterface $request, array $options) use ($handler) {
$request->withHeader('X-Custom-Header', 'Hello');
return $handler($request, $options);
};
})
}
}
```
{% hint style="info" %}
To read more about Guzzle's middleware and handlers [click here](https://docs.guzzlephp.org/en/stable/handlers-and-middleware.html).
{% endhint %}
### Accessing The Underlying Guzzle Instance
You may need to modify the Guzzle client or the handler stack. If you need to do this, you can use the `getGuzzleClient` or `getHandlerStack` methods.
```php
<?php
class ForgeConnector extends Connector
{
// {...}
public function __construct()
{
$guzzleClient = $this->sender()->getGuzzleClient();
$handlerStack = $this->sender()->getHandlerStack();
}
}
```
]]>
</middleware_md>
<oauth2_authentication>
<client_credentials_grant_md>
<![CDATA[
# Client Credentials Grant
Some API providers implement the OAuth2 _Client Credentials Grant_ for authentication. Implementing this grant type every time you create a new API integration can be tedious and time-consuming. Saloon offers a simple, extendable OAuth2 trait to help you get up and running quickly.
### Prerequisites
This section of the documentation assumes that you are familiar with OAuth2 and specifically the _Client Credentials Grant_. If you are not familiar with how this grant type works, [Auth0 has a great explanation on its website.](https://auth0.com/docs/get-started/authentication-and-authorization-flow/client-credentials-flow)
### Flow Example
Saloon has provided methods for the full client credentials grant.
```php
$connector = new WarehouseConnector;
// 1. Create an access token authenticator
$authenticator = $connector->getAccessToken($scopes);
// 2. Authenticate the connector
$connector->authenticate($authenticator);
// 3. Send your requests
$connector->send(new GetInventoryRequest);
```
### Getting Started
Let's start with preparing our connector to support the client credentials grant. All we have to do is add the `ClientCredentialsGrant` trait to our connector.
```php
<?php
use Saloon\Http\Connector;
use Saloon\Traits\OAuth2\ClientCredentialsGrant;
class WarehouseConnector extends Connector
{
use ClientCredentialsGrant;
}
```
After you have added the trait, you will need to tell Saloon how to authenticate with your API. First, extend the `defaultOauthConfig` method and use the methods to define your client ID and secret. Saloon also has sensible defaults set for the token endpoint, but you may customize it if you need to. For example, some APIs have a different base URL than the connector's base URL. You can also provide default scopes and even provide a callback to modify the OAuth2 requests being sent.
```php
<?php
use Saloon\Http\Connector;
use Saloon\Traits\OAuth2\ClientCredentialsGrant;
use Saloon\Helpers\OAuth2\OAuthConfig;
class WarehouseConnector extends Connector
{
use ClientCredentialsGrant;
public function resolveBaseUrl(): string
{
return 'https://local-warehouse.app';
}
protected function defaultOauthConfig(): OAuthConfig
{
return OAuthConfig::make()
->setClientId('my-client-id')
->setClientSecret('my-client-secret')
->setDefaultScopes(['inventory.read'])
->setTokenEndpoint('/oauth/token')
->setRequestModifier(function (Request $request) {
// Optional: Modify the requests being sent.
});
}
}
```
{% hint style="info" %}
The endpoint method, like `setTokenEndpoint` on the OAuthConfig class supports full URLs if you need to overwrite the base URL on the connector however you may just use the endpoint if the base URL is the same.
{% endhint %}
#### Overwriting the OAuth2 config
Sometimes, you may have a different OAuth2 client ID and secret for each user of your application. If your OAuth2 config is dependent on a per-user/tenant basis, it's recommended that you pass in the credentials as constructor arguments of your connector and then set the `oauthConfig` inside the constructor.&#x20;
In the following example, I will pass in the `$clientId` and the `$clientSecret` as constructor arguments and overwrite the OAuth2 config.
```php
<?php
use Saloon\Http\Connector;
use Saloon\Traits\OAuth2\ClientCredentialsGrant;
class WarehouseConnector extends Connector
{
use ClientCredentialsGrant;
public function __construct(string $clientId, string $clientSecret)
{
$this->oauthConfig()->setClientId($clientId);
$this->oauthConfig()->setClientSecret($clientSecret);
}
// ...
}
```
### Creating Access Tokens
You are now ready to create access tokens. You should use the `getAccessToken` method on your connector. If successful, the method will return an `AccessTokenAuthenticator`. The access token and expiry (if provided) are wrapped up in a [Saloon Authenticator](../../the-basics/authentication.md#custom-authenticators) class that can be used to authenticate your connector/requests. It acts like a DTO that can be easily serialized and transported around your application.&#x20;
```php
<?php
$connector = new WarehouseConnector;
$authenticator = $connector->getAccessToken();
// Use authenticator to authenticate your connector instance
$connector->authenticate($authenticator);
// Any request sent through this connector will now have authentication applied
$connector->send(new GetInventoryRequest);
```
{% hint style="info" %}
Once you have received the authenticator instance, you should cache it securely in your application for future use. Read further to see how you can do this.
{% endhint %}
#### Custom Scopes
Sometimes you may need to provide an additional "scope" to declare the level of access that your token needs. You may provide default scopes in the OauthConfig class on your connector, but you can also provide additional scopes when creating access tokens. Saloon will separate scopes with spaces but if your API integration requires scopes to be separated any other way, you can specify this with the `scopeSeparator` argument.
```php
<?php
$connector = new WarehouseConnector;
$authenticator = $connector->getAccessToken(
scopes: ['inventory.update', 'inventory.delete'],
scopeSeparator: '+',
);
```
#### Returning Responses
If you prefer, you may request Saloon to return a `Saloon\Http\Resonse` instance instead of a `AccessTokenAuthenticator` when creating access tokens. To use responses, just provide the `returnResponse` argument when creating access tokens.
```php
<?php
$connector = new WarehouseConnector;
$response = $connector->getAccessToken(
returnResponse: true,
);
```
### Authenticator Methods
The authenticator returned by Saloon when using the `getAccessToken` method will contain the Access Token, and optionally an expiry date that was returned by the OAuth2 server. You can access these properties with the following methods. You can also check if the authenticator has expired, which will come in handy when refreshing access tokens.
```php
<?php
$authenticator->getAccessToken();
$authenticator->getExpiresAt();
$authenticator->hasExpired();
$authenticator->hasNotExpired();
```
### Customising The Authenticator
Sometimes the API provider you are authenticating with may require additional information to be used in the authenticator. You can customise how the authenticator will be created by extending the `createAccessTokenAuthenticator` method on your connector.
```php
<?php
protected function createOAuthAuthenticator(string $accessToken, ?DateTimeImmutable $expiresAt = null): OAuthAuthenticatorInterface
{
return new WarehouseAuthenticator($accessToken, $expiresAt);
}
```
### Customising How The Authenticator Is Created
Sometimes the API provider you are authenticating with may have a different way that they respond with their tokens. If you need to customise the way Saloon creates the authenticator you can extend the `createOAuthAuthenticatorFromResponse` method.
```php
<?php
protected function createOAuthAuthenticatorFromResponse(SaloonResponse $response): OAuthAuthenticatorInterface
{
$responseData = $response->object();
$accessToken = $responseData->access_token;
$expiresAt = new DateTimeImmutable('+' . $responseData->expires_in . ' seconds');
return $this->createOAuthAuthenticator($accessToken, $expiresAt);
}
```
### Customising The Requests
Sometimes you might integrate with an API that requires additional query parameters or headers to be sent with the OAuth2 flow. You may use the `requestModifier` method on the `getAccessToken` method or use the `setRequestModifier` method within the `OAuthConfig` to add a callable that is invoked before a request is sent.
{% tabs %}
{% tab title="Per Request" %}
```php
<?php
$connector->getAccessToken(requestModifier: function (Request $request) {
$request->query()->add('access_type', 'offline');
});
```
{% endtab %}
{% tab title="All Requests" %}
<pre class="language-php"><code class="lang-php">&#x3C;?php
use Saloon\Http\Request;
use Saloon\Http\OAuth2\GetClientCredentialsTokenRequest;
protected function defaultOauthConfig(): OAuthConfig
{
return OAuthConfig::make()
->setClientId('my-client-id')
->setClientSecret('my-client-secret')
<strong> ->setRequestModifier(function (GetClientCredentialsTokenRequest $request) {
</strong><strong> //
</strong><strong> }),
</strong>}
</code></pre>
{% endtab %}
{% endtabs %}
### Using your own request classes
There are situations where Saloon's own request classes for getting the access token might not suit the API you are integrating with. For example, if an API uses JSON encoding instead of form encoding. You may use the following method on your connector to overwrite the instantiation process of the request class.
```php
<?php
class SpotifyConnector extends Connector
{
// ...
protected function resolveAccessTokenRequest(OAuthConfig $oauthConfig, array $scopes = [], string $scopeSeparator = ' '): Request
{
return new GetClientCredentialsTokenRequest($oauthConfig, $scopes, $scopeSeparator);
}
}
```
]]>
</client_credentials_grant_md>
<oauth2_authentication_md>
<![CDATA[
# Authorization Code Grant
Some API providers implement the OAuth 2 _Authorization Code Flow_ for authentication. Implementing this grant type every time you create a new API integration can be tedious and time-consuming. Saloon offers a simple, extendable OAuth2 trait to help you get up and running quickly.
### Prerequisites
This section of the documentation assumes that you are familiar with OAuth2 and specifically the _Authorization Code Grant_. If you are not familiar with how this grant type works, [Auth0 has a great explanation on its website.](https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow)
### Flow Example
Saloon has provided methods for the full Authorization Code grant.
```php
$connector = new SpotifyConnector;
// 1. Redirect the user to the authorization URL...
$authorizationUrl = $connector->getAuthorizationUrl($scopes, $state);
// 2. Handle the callback from the API provider and create an access token...
$authenticator = $connector->getAccessToken($code, $state);
// 3. Authenticate the connector
$connector->authenticate($authenticator);
// 4. Send your requests
$connector->send(new GetTracksRequest);
// 5. Refresh your access tokens...
$newAuthenticator = $connector->refreshAccessToken($authenticator);
```
### Getting Started
Let’s start with preparing our connector to support the Authorization Code Flow. All we have to do is add the `AuthorizationCodeGrant` trait to our connector.
```php
<?php
use Saloon\Http\Connector;
use Saloon\Traits\OAuth2\AuthorizationCodeGrant;
class SpotifyConnector extends Connector;
{
use AuthorizationCodeGrant;
}
```
After you have added the trait, you will need to tell Saloon how to authenticate with your API. First, extend the `defaultOauthConfig` method and use the methods to define your client ID, secret and redirect URI. Saloon also has sensible defaults set for the authorization and token endpoints, but you may customize them if you need to. For example, Spotify has a different base URL than the connector's base URL, so we have overwritten it in this example.
You can also provide default scopes and even provide a callback to modify the OAuth2 requests being sent.
```php
<?php
use Saloon\Http\Connector;
use Saloon\Http\Request;
use Saloon\Helpers\OAuth2\OAuthConfig;
use Saloon\Traits\OAuth2\AuthorizationCodeGrant;
class SpotifyConnector extends Connector
{
use AuthorizationCodeGrant;
public function resolveBaseUrl(): string
{
// Spotify's API has a different base URL for OAuth2 auth.
return 'https://api.spotify.com/v1';
}
protected function defaultOauthConfig(): OAuthConfig
{
return OAuthConfig::make()
->setClientId('my-client-id')
->setClientSecret('my-client-secret')
->setDefaultScopes(['user-read-currently-playing'])
->setRedirectUri('https://my-app.saloon.dev/auth/callback')
->setAuthorizeEndpoint('https://accounts.spotify.com/authorize')
->setTokenEndpoint('https://accounts.spotify.com/api/token')
->setUserEndpoint('/me')
->setRequestModifier(function (Request $request) {
// Optional: Modify the requests being sent.
});
}
}
```
{% hint style="info" %}
Each of the endpoint methods, like `setAuthorizeEndpoint`on the OAuthConfig class support full URLs if you need to overwrite the base URL on the connector however you may just use the endpoint if the base URL is the same.
{% endhint %}
#### Overwriting the OAuth2 config
Sometimes, you may have a different OAuth2 client ID and secret for each user of your application. If your OAuth2 config is dependent on a per-user/tenant basis, it's recommended that you pass in the credentials as constructor arguments of your connector and then set the `oauthConfig` inside the constructor.
In the following example, I will pass in the `$clientId` and the `$clientSecret` as constructor arguments and overwrite the OAuth2 config.
<pre class="language-php"><code class="lang-php">&#x3C;?php
use Saloon\Http\Connector;
use Saloon\Traits\OAuth2\AuthorizationCodeGrant;
class SpotifyConnector extends Connector
{
use AuthorizationCodeGrant;
<strong> public function __construct(string $clientId, string $clientSecret)
</strong> {
$this->oauthConfig()->setClientId($clientId);
$this->oauthConfig()->setClientSecret($clientSecret);
}
// ...
}
</code></pre>
### Creating an Authorization URL
Now we have setup our connector to support the authorization code grant, we are ready to start the OAuth2 process. Usually, the first stage is to generate a URL to redirect our application users to. To generate an authorization URL, you can use the `getAuthorizationUrl` method on the connector.
```php
<?php
$connector = new SpotifyConnector;
$authorizationUrl = $connector->getAuthorizationUrl();
```
You can also pass in scopes which will be merged with the default scopes if you provided them in the OAuth config. Saloon will separate scopes with spaces but if your API integration requires scopes to be separated any other way, you can specify this with the `scopeSeparator` argument.
```php
$authorizationUrl = $connector->getAuthorizationUrl(
scopes: ['user-library-read'],
scopeSeparator: '+',
);
```
You can also provide additional query parameters if you need to with the `additionalQueryParameters` argument. This should be a key-value array where the key is the query parameter name and the value is the value of the query parameter.
```php
$authorizationUrl = $authConnector->getAuthorizationUrl(
additionalQueryParameters: [
'username' => 'JohnWayne'
],
);
```
#### State
To help prevent CSRF attacks or send additional data during authentication, you can pass an additional unique string in your authorization URL that the API's OAuth2 server will send back to you after your user has approved or denied access to your OAuth2 app.
```php
$authorizationUrl = $authConnector->getAuthorizationUrl(
state: 'application-user-id',
);
```
If you do not provide your own state, Saloon will automatically generate a unique, 32-character string. Once you have generated the authorization URL, you can then use the `getState` method on your connector to get the state back. You should store this string in your application's session or cache to be verified later.
<pre class="language-php"><code class="lang-php">&#x3C;?php
$connector = new SpotifyConnector;
$authorizationUrl = $connector->getAuthorizationUrl();
<strong>$state = $authConnector->getState(); // '8484b43fdjfdnfdj3llls...'
</strong></code></pre>
### Creating Access Tokens
After the user has approved your application, the API provider will redirect you back to your application with an authorization code and state. This data usually sent in the form of query parameters should be passed into your `getAccessToken` method on your connector. If successful, the method will return an `AccessTokenAuthenticator`. The access token, refresh token and expiry are wrapped up in a [Saloon Authenticator](../../the-basics/authentication.md#custom-authenticators) class that can be used to authenticate your connector/requests. It acts like a DTO that can be easily serialized and transported around your application.
<pre class="language-php"><code class="lang-php">&#x3C;?php
$connector = new SpotifyConnector;
<strong>$authenticator = $authConnector->getAccessToken($code);
</strong>
// Use authenticator to authenticate your connector instance
$connector->authenticate($authenticator);
// Any request sent through this connector will now have authentication applied
$connector->send(new GetTracksRequest);
</code></pre>
{% hint style="info" %}
Once you have received the authenticator instance, you should cache it securely in your application for future use. Read further to see how you can do this.
{% endhint %}
#### Verifying State
If you stored the state that was generated during creating an authorization URL, you should pass this expected state alongside the state sent back by the API provider's OAuth2 server. This will be used to verify the state provided back by the application is valid. If the state does not match the expected state, Saloon will throw an exception.
```php
<?php
$authConnector = new SpotifyConnector;
// It will throw an exception if the state and expected state don't match
// however both must be present
$authenticator = $authConnector->getAccessToken($code, $state, $expectedState);
```
### Storing Authentication For Later
You will likely need to store the authenticator securely so you can use it for future requests. You may serialize and unserialize the authenticator class using the helper methods below, then you can store the string wherever you like, usually encrypted in the database if it's against a user. Then, you can retrieve this authenticator and use it to authenticate your connector.
```php
<?php
$connector = new SpotifyConnector;
$authenticator = $connector->getAccessToken($code);
// Securely store this against your user.
$serialized = $authenticator->serialize();
// Unserialize the authenticator when retrieving it
$authenticator = AccessTokenAuthenticator::unserialize($serialized);
```
### Authenticator Methods
The authenticator returned by Saloon when using the `getAccessToken` or `refreshAccessToken` methods will contain the Access Token, Refresh Token and Expiry Date that was returned by the OAuth2 server. You can access these properties with the following methods. You can also check if the authenticator has expired, which will come in handy when refreshing access tokens.
```php
<?php
$authenticator->getAccessToken();
$authenticator->getRefreshToken();
$authenticator->getExpiresAt();
$authenticator->hasExpired();
$authenticator->hasNotExpired();
```
### Refreshing Access Tokens
When retrieving your authenticator out of storage, you should always check if the access token has expired and if it needs refreshing. If the authenticator's access token has expired, you can call the `refreshAccessToken` method which will create a fresh authenticator.
In this example, `$user` is the user of my application and I have written methods to get and store the authenticators.
```php
<?php
// $user in this example is my application's user
$authenticator = $user->getCachedAuthenticator();
$connector = new SpotifyConnector;
if ($authenticator->hasExpired()) {
// We'll refresh the access token which will return a new authenticator
// which we can store against our user in our application.
$authenticator = $connector->refreshAccessToken($authenticator);
$user->updateAuthenticator($authenticator);
}
// Authenticate our connector and send the request
$connector->authenticate($authenticator);
$response = $connector->send(new GetTracksRequest);
```
{% hint style="info" %}
If you are using Laravel and the Saloon Laravel library, you can use built-in `EncryptedOAuthAuthenticatorCast` **/** `OAuthAuthenticatorCast` Eloquent casts to automatically cast the authenticator for storing in your database.
{% endhint %}
### Customising The Authenticator
Sometimes the API provider you are authenticating with may require additional information to be used in the authenticator. You can customise how the authenticator will be created by extending the `createAccessTokenAuthenticator` method on your connector.
```php
<?php
protected function createOAuthAuthenticator(string $accessToken, string $refreshToken, DateTimeImmutable $expiresAt): OAuthAuthenticatorInterface
{
return new SpotifyAuthenticator($accessToken, $refreshToken, $expiresAt);
}
```
### Customising How The Authenticator Is Created
Sometimes the API provider you are authenticating with may have a different way that they respond with their tokens. If you need to customise the way Saloon creates the authenticator you can extend the `createOAuthAuthenticatorFromResponse` method.
```php
<?php
protected function createOAuthAuthenticatorFromResponse(SaloonResponse $response, string $fallbackRefreshToken = null): OAuthAuthenticatorInterface
{
$responseData = $response->object();
$accessToken = $responseData->access_token;
$refreshToken = $responseData->refresh_token ?? $fallbackRefreshToken;
$expiresAt = new DateTimeImmutable('+' . $responseData->expires_in . ' seconds');
return $this->createOAuthAuthenticator($accessToken, $refreshToken, $expiresAt);
}
```
### Customising The Requests
Sometimes you might integrate with an API that requires additional query parameters or headers to be sent with the OAuth2 flow. You may use the `requestModifier` property on the methods or use the `setRequestModifier` method within the `OAuthConfig` to add a callable that is invoked before a request is sent.
{% tabs %}
{% tab title="Per Request" %}
```php
<?php
$connector->getAccessToken($code, requestModifier: function (Request $request) {
$request->query()->add('access_type', 'offline');
});
$connector->getRefreshToken($code, requestModifier: function (Request $request) {
$request->headers()->add('X-App-Key', $appKey);
});
$connector->getUser($code, requestModifier: function (Request $request) {
$request->headers('Accept', 'text/plain');
});
```
{% endtab %}
{% tab title="All Requests" %}
```php
<?php
use Saloon\Http\Request;
use Saloon\Http\OAuth2\GetUserRequest;
use Saloon\Http\OAuth2\GetAccessTokenRequest;
use Saloon\Http\OAuth2\GetRefreshTokenRequest;
protected function defaultOauthConfig(): OAuthConfig
{
return OAuthConfig::make()
->setClientId('my-client-id')
->setClientSecret('my-client-secret')
->setRedirectUri('https://my-app.saloon.dev/auth/callback')
->setRequestModifier(function (Request $request) {
// This callback is invoked on every request, so you
// may want to use if-statements or a match statement
// to apply conditions based on request.
if ($request instanceof GetAccessTokenRequest) {
$request->query()->add('access_type', 'offline');
}
if ($request instanceof GetRefreshTokenRequest) {
$request->headers()->add('X-App-Key', $appKey);
}
if ($request instanceof GetUserRequest) {
$request->headers('Accept', 'text/plain');
}
}),
}
```
{% endtab %}
{% endtabs %}
### Using your own request classes
There are situations where Saloon's own request classes for getting the access token, refreshing the access token or getting the user might not suit the API you are integrating with. For example, if an API uses JSON encoding instead of form encoding. You may use the following methods on your connector to overwrite the instantiation process of the request classes.
```php
<?php
class SpotifyConnector extends Connector
{
// ...
protected function resolveAccessTokenRequest(string $code, OAuthConfig $oauthConfig): Request
{
return new CustomGetAccessTokenRequest($code, $oauthConfig);
}
protected function resolveRefreshTokenRequest(OAuthConfig $oauthConfig, string $refreshToken): Request
{
return new CustomGetRefreshTokenRequest($oauthConfig, $refreshToken);
}
protected function resolveUserRequest(OAuthConfig $oauthConfig): Request
{
return new CustomGetUserRequest($oauthConfig);
}
}
```
### Returning Responses
If you prefer, you may request Saloon to return a `Saloon\Http\Resonse` instance instead of a `AccessTokenAuthenticator` when creating or refreshing access tokens. To use responses, just provide the `returnResponse` argument when creating or refreshing access tokens.
```php
<?php
$connector = new SpotifyConnector;
$response = $connector->getAccessToken(
returnResponse: true,
);
$response = $connector->refreshAccessToken(
returnResponse: true,
);
```
### Real-world example
If you would like to see an example integration using the OAuth2 methods mentioned above, you can see the following Laravel application.
[https://github.com/Sammyjo20/saloon-v2-spotify-example](https://github.com/Sammyjo20/saloon-v2-spotify-example)
]]>
</oauth2_authentication_md>
<README_md>
<![CDATA[
# 🔑 OAuth2 Authentication
Saloon supports some OAuth2 authentication grants out of the box which you can use in your application. They come in the form of traits that can be easily added to your connector and then configured to your needs. Read more below by finding the grant type you would like to use
### Authorization Code Grant
This flow is the typical process where your application redirects your users to a third party to approve your authentication request. After they have approved the authentication request, they will be redirected back to your application where you can generate access tokens.
{% content-ref url="oauth2-authentication.md" %}
[oauth2-authentication.md](oauth2-authentication.md)
{% endcontent-ref %}
### Client Credentials Grant
This flow is often used for internal server-to-server authentication. Like a shopping website communicating with an internal warehouse API. You are often given a single access token which can be used to generate access tokens.
{% content-ref url="client-credentials-grant.md" %}
[client-credentials-grant.md](client-credentials-grant.md)
{% endcontent-ref %}
]]>
</README_md>
</oauth2_authentication>
<pagination>
<cursor_pagination_md>
<![CDATA[
# Cursor Pagination
First, you will want to import the `CursorPaginator` class and return a new anonymous class that extends Saloon's `CursorPaginator`. This class expects the connector and request to be passed in via the constructor arguments.
<pre class="language-php"><code class="lang-php">&#x3C;?php
use Saloon\Http\Response;
use Saloon\Http\Connector;
<strong>use Saloon\PaginationPlugin\CursorPaginator;
</strong>use Saloon\PaginationPlugin\Contracts\HasPagination;
class SpotifyConnector extends Connector implements HasPagination
{
// ...
public function paginate(Request $request): CursorPaginator
{
<strong> return new class(connector: $this, request: $request) extends CursorPaginator
</strong> {
//
};
}
}
</code></pre>
{% hint style="info" %}
You don't need to use an anonymous class if it doesn't fit your code style. It is recommended to reduce the number of classes, but you can create your own pagination class that extends the base paginator if you prefer.
{% endhint %}
After you have defined your paginator class, you will be required to define three protected methods which are used to power the paginator. These methods are:
* **getNextCursor** - This method is used to find the next cursor from the response. This method must return either an integer or a string. The next cursor is used in the next request to get a different page.
* **isLastPage** - This method is used to tell the paginator when to stop processing. Here you can use the response class provided to determine if you are on the last page. Some APIs may provide metadata like remaining results or next page URLs which you can use to check if you are on the last page. Additionally, Saloon has a few properties that can be used to determine if you are on the last page.
* **getPageItems** - This method is used to return the array of results inside of each page. This is used when using the `items` or `collect` method on your paginator class.
Let's implement these three methods on our paginator and dive into how it works.
<pre class="language-php"><code class="lang-php">&#x3C;?php
use Saloon\Http\Response;
use Saloon\Http\Connector;
use Saloon\PaginationPlugin\CursorPaginator;
use Saloon\PaginationPlugin\Contracts\HasPagination;
class SpotifyConnector extends Connector implements HasPagination
{
// ...
public function paginate(Request $request): CursorPaginator
{
return new class(connector: $this, request: $request) extends CursorPaginator
{
<strong> protected function getNextCursor(Response $response): int|string
</strong> {
return $response->json('next_cursor');
}
<strong> protected function isLastPage(Response $response): bool
</strong> {
return is_null($response->json('next_cursor'));
}
<strong> protected function getPageItems(Response $response, Request $request): array
</strong> {
return $response->json('items');
}
};
}
}
</code></pre>
Let's assume in this hypothetical example that the API provides a `next_cursor` property in the JSON response. We'll return the value of this property in the `getNextCursor` method. After that, we need to tell Saloon when to stop paginating. We'll use the same `next_cursor` property again. If it's null, we know that there isn't another page to look up. There are various other ways of knowing if you are on the last page, so it's best to fully understand your API's pagination.
To get the page items, we'll use the `json` method on the response to access the `items` array from the body.
### Specifying a default per-page on the paginator
The third-party API you are integrating with may require you to define a page size (per page) on your requests, or you may want to set a default page size used for every request. You can set this default page size as a property on the paginator class.&#x20;
<pre class="language-php"><code class="lang-php">public function paginate(Request $request): CursorPaginator
{
return new class(connector: $this, request: $request) extends CursorPaginator
{
<strong> protected ?int $perPageLimit = 100;
</strong>
// ...
}
}
</code></pre>
You may also use the `setPerPageLimit` method on an instantiated paginator if you want to configure it on the fly. **You must set this before iterating over the paginator.**&#x20;
```php
$spotifyConnector = new SpotifyConnector;
$paginator = $spotifyConnector->paginate($request);
$paginator->setPerPageLimit(250);
// foreach($paginator as $response) { ... }
```
### **Assumptions made with the CursorPaginator**
The CursorPaginator will apply pagination by sending two **query parameters**:
* cursor
* per\_page
This assumption might not be the way your third-party API works. You can really easily change this by extending the `applyPagination` method. For example, let's say our API expects a "pageCursor" and a "pageSize" instead. We can simply extend the `applyPagination` method and change the keys. You can apply the pagination in whichever the API requires.
<pre class="language-php"><code class="lang-php">public function paginate(Request $request): CursorPaginator
{
return new class(connector: $this, request: $request) extends CursorPaginator
{
protected function getNextCursor(Response $response): int|string
{
return $response->json('next_cursor');
}
protected function isLastPage(Response $response): bool
{
return is_null($response->json('next_cursor'));
}
protected function getPageItems(Response $response, Request $request): array
{
return $response->json('items');
}
protected function applyPagination(Request $request): Request
{
if ($this->currentResponse instanceof Response) {
<strong> $request->query()->add('pageCursor', $this->getNextCursor($this->currentResponse));
</strong> }
if (isset($this->perPageLimit)) {
<strong> $request->query()->add('pageSize', $this->perPageLimit);
</strong> }
return $request;
}
};
}
</code></pre>
### **Useful Properties On The CursorPaginator**
We previously mentioned that the CursorPaginator class provides a few methods that can be used to help with last-page calculation. These methods are:
* **totalResults** - An integer which will return the total number of items returned. This can be used to check if it's equal to the number of total results in the list.
* **page** - An integer which will return the current page that the paginator is currently on.
### Next Steps
After configuring your paginator, head back to the [Using The Paginator](./#using-the-paginator) section of the documentation.
]]>
</cursor_pagination_md>
<custom_pagination_md>
<![CDATA[
# Custom Pagination
If you are building an integration with a third-party API that does not use paged, limit/offset or cursor pagination, you can build your own paginator. Saloon's base paginator class has been designed to be flexible and easy to build against.
### Getting Started
First, you will want to import the `Paginator` abstract class and return a new anonymous class that extends Saloon's `Paginator`. This class expects the connector and request to be passed in via the constructor arguments.
<pre class="language-php"><code class="lang-php">&#x3C;?php
use Saloon\Http\Response;
use Saloon\Http\Connector;
<strong>use Saloon\PaginationPlugin\Paginator;
</strong>use Saloon\PaginationPlugin\Contracts\HasPagination;
class SpotifyConnector extends Connector implements HasPagination
{
// ...
public function paginate(Request $request): Paginator
{
return new class(connector: $this, request: $request) extends Paginator
{
//
};
}
}
</code></pre>
{% hint style="info" %}
You don't need to use an anonymous class if it doesn't fit your code style. It is recommended to reduce the number of classes, but you can create your own pagination class that extends the base paginator if you prefer.
{% endhint %}
After you have defined your paginator class, you will be required to define three protected methods which are used to power the paginator. These methods are:
* **isLastPage** - This method is used to tell the paginator when to stop processing. Here you can use the response class provided to determine if you are on the last page. Some APIs may provide metadata like remaining results or next page URLs which you can use to check if you are on the last page. Additionally, Saloon has a few properties that can be used to determine if you are on the last page.
* **getPageItems** - This method is used to return the array of results inside of each page. This is used when using the `items` or `collect` method on your paginator class.
* **applyPagination** - This method is used to change each request to apply the pagination for the next page.
Let's implement these three methods on our paginator and dive into how it works.
<pre class="language-php"><code class="lang-php">&#x3C;?php
use Saloon\Http\Response;
use Saloon\Http\Connector;
<strong>use Saloon\PaginationPlugin\Paginator;
</strong>use Saloon\PaginationPlugin\Contracts\HasPagination;
class SpotifyConnector extends Connector implements HasPagination
{
// ...
public function paginate(Request $request): Paginator
{
return new class(connector: $this, request: $request) extends Paginator
{
protected function isLastPage(Response $response): bool
{
<strong> return is_null($response->json('next_page_url'));
</strong> }
protected function getPageItems(Response $response, Request $request): array
{
<strong> return $response->json('items');
</strong> }
protected function applyPagination(Request $request): Request
{
<strong> $request->headers()->add('X-Page', $this->currentPage);
</strong>
<strong> $request->headers()->add('X-Per-Page', $this->perPageLimit);
</strong>
<strong> return $request;
</strong> }
};
}
}
</code></pre>
Let's assume in this hypothetical example that the API provides some useful information to see if we're on the last page. In this example, we know we're on the last page if the `next_page_url` JSON property on the body is null. There are various other ways of knowing if you are on the last page, so it's best to fully understand your API's pagination.
To get the page items, we'll use the `json` method on the response to access the `items` array from the body.
After that, to apply the pagination, our API expects us to send a `X-Page` header with the current page on the paginator. We'll use the `$this->currentPage` property on the paginator as it acts like an index for the current iteration of the paginator. We will also make sure to send the `perPageLimit` to our API with the `X-Per-Page` header. This header is optional depending on the third-party API you are integrating with.
### Specifying a default per-page on the paginator
The third-party API you are integrating with may require you to define a page size (per page) on your requests, or you may want to set a default page size used for every request. You can set this default page size as a property on the paginator class.
<pre class="language-php"><code class="lang-php">public function paginate(Request $request): Paginator
{
return new class(connector: $this, request: $request) extends Paginator
{
<strong> protected ?int $perPageLimit = 100;
</strong>
// ...
};
}
</code></pre>
You may also use the `setPerPageLimit` method on an instantiated paginator if you want to configure it on the fly. **You must set this before iterating over the paginator.**
```php
$spotifyConnector = new SpotifyConnector;
$paginator = $spotifyConnector->paginate($request);
$paginator->setPerPageLimit(250);
// foreach($paginator as $response) { ... }
```
{% hint style="warning" %}
For this feature to work, you must implement sending the `perPageLimit` inside of your `applyPagination` method in whichever way your API may expect.
{% endhint %}
### Next Steps
After configuring your paginator, head back to the [Using The Paginator](./#using-the-paginator) section of the documentation.
]]>
</custom_pagination_md>
<limit_offset_pagination_md>
<![CDATA[
# Limit/Offset Pagination
First, you will want to import the `OffsetPaginator` class and return a new anonymous class that extends Saloon's `OffsetPaginator`. This class expects the connector and request to be passed in via the constructor arguments.
<pre class="language-php"><code class="lang-php">&#x3C;?php
use Saloon\Http\Response;
use Saloon\Http\Connector;
<strong>use Saloon\PaginationPlugin\OffsetPaginator;
</strong>use Saloon\PaginationPlugin\Contracts\HasPagination;
class SpotifyConnector extends Connector implements HasPagination
{
// ...
public function paginate(Request $request): OffsetPaginator
{
<strong> return new class(connector: $this, request: $request) extends OffsetPaginator
</strong> {
//
};
}
}
</code></pre>
{% hint style="info" %}
You don't need to use an anonymous class if it doesn't fit your code style. It is recommended to reduce the number of classes, but you can create your own pagination class that extends the base paginator if you prefer.
{% endhint %}
After you have defined your paginator class, you will be required to define a single property and two protected methods which are used to power the paginator. These properties/methods are:
* **perPageLimit** - This property is required as this is the default limit of the paginator. Since this paginator requires limit/offset - the limit is always required. If you need to change the limit on a per-request basis you can change it on the paginator instance.
* **isLastPage** - This method is used to tell the paginator when to stop processing. Here, you can use the response class provided to determine if you are on the last page. Some APIs may provide metadata like remaining results or next-page URLs, which you can use to check if you are on the last page. Additionally, Saloon has a few properties that can be used to determine if you are on the last page.
* **getPageItems** - This method is used to return the array of results inside of each page. This is used when using the `items` or `collect` method on your paginator class.
Let's implement the property and the two methods on our paginator and dive into how it works.
<pre class="language-php"><code class="lang-php">&#x3C;?php
use Saloon\Http\Response;
use Saloon\Http\Connector;
use Saloon\PaginationPlugin\OffsetPaginator;
use Saloon\PaginationPlugin\Contracts\HasPagination;
class SpotifyConnector extends Connector implements HasPagination
{
// ...
public function paginate(Request $request): OffsetPaginator
{
return new class(connector: $this, request: $request) extends OffsetPaginator
{
<strong> protected ?int $perPageLimit = 100;
</strong>
<strong> protected function isLastPage(Response $response): bool
</strong> {
return $this->getOffset() >= (int)$response->json('total');
}
<strong> protected function getPageItems(Response $response, Request $request): array
</strong> {
return $response->json('items');
}
};
}
}
</code></pre>
First, we'll define a `perPageLimit` of 100. This means that every request will attempt to use a "limit" of 100.&#x20;
Next, let's assume in this hypothetical example that the API provides some useful information that helps us calculate if we're on the last page. For example, our API returns a `total` integer with every page - this number is simply the total results across all pages. We can use this to compare it with the current offset, and if the offset is greater than or equal to the total, we will stop. There are other ways of knowing if you are on the last page, so it's best to understand your API's pagination fully.
To get the page items, we'll use the `json` method on the response to access the `items` array from the body.
### Changing the per-page limit on the paginator
As mentioned previously, you must specify a "per-page limit" on your offset paginator. You may also use the `setPerPageLimit` method on an instantiated paginator if you want to change it on the fly. **You must set this before iterating over the paginator.**&#x20;
```php
$spotifyConnector = new SpotifyConnector;
$paginator = $spotifyConnector->paginate($request);
$paginator->setPerPageLimit(250);
// foreach($paginator as $response) { ... }
```
### **Assumptions made with the OffsetPaginator**
The OffsetPaginator will apply pagination by sending two **query parameters**:
* limit
* offset
This assumption might not be the way your third-party API works. You can really easily change this by extending the `applyPagination` method. For example, let's say our API expects the "top" and a "skip" query parameters instead. We can simply extend the `applyPagination` method and change the keys. You can apply the pagination in whichever the API requires.
<pre class="language-php"><code class="lang-php">public function paginate(Request $request): OffsetPaginator
{
return new class(connector: $this, request: $request) extends OffsetPaginator
{
protected ?int $perPageLimit = 100;
protected function isLastPage(Response $response): bool
{
return $this->getOffset() >= (int)$response->json('total');
}
protected function getPageItems(Response $response, Request $request): array
{
return $response->json('items');
}
<strong> protected function applyPagination(Request $request): Request
</strong> {
$request->query()->merge([
<strong> 'top' => $this->perPageLimit,
</strong><strong> 'skip' => $this->getOffset(),
</strong> ]);
return $request;
}
};
}
</code></pre>
{% hint style="info" %}
The `getOffset()` method is a useful helper that calculates the current offset for you based on current interation and the per-page limit.
{% endhint %}
### **Useful Methods/Properties On The OffsetPaginator**
We previously mentioned that the `OffsetPaginator` class provides a few methods that can be used to help with last-page calculation. These methods are:
* **totalResults** - An integer which will return the total number of items returned. This can be used to check if it's equal to the number of total results in the list.
* **page** - An integer which will return the current page that the paginator is currently on.
* **getOffset()** - A helpful method that calculates the current offset for you based on current interation and the per-page limit.
### Next Steps
After configuring your paginator, head back to the [Using The Paginator](./#using-the-paginator) section of the documentation.
]]>
</limit_offset_pagination_md>
<paged_pagination_md>
<![CDATA[
# Paged Pagination
First, you will want to import the `PagedPaginator` class and return a new anonymous class that extends Saloon's `PagedPaginator`. This class expects the connector and request to be passed in via the constructor arguments.
<pre class="language-php"><code class="lang-php">&#x3C;?php
use Saloon\Http\Response;
use Saloon\Http\Connector;
<strong>use Saloon\PaginationPlugin\PagedPaginator;
</strong>use Saloon\PaginationPlugin\Contracts\HasPagination;
class SpotifyConnector extends Connector implements HasPagination
{
// ...
public function paginate(Request $request): PagedPaginator
{
<strong> return new class(connector: $this, request: $request) extends PagedPaginator
</strong> {
//
};
}
}
</code></pre>
{% hint style="info" %}
You don't need to use an anonymous class if it doesn't fit your code style. It is recommended to reduce the number of classes, but you can create your own pagination class that extends the base paginator if you prefer.
{% endhint %}
After you have defined your paginator class, you will be required to define two protected methods which are used to power the paginator. These methods are:
* **isLastPage** - This method is used to tell the paginator when to stop processing. Here you can use the response class provided to determine if you are on the last page. Some APIs may provide metadata like remaining results or next page URLs which you can use to check if you are on the last page. Additionally, Saloon has a few properties that can be used to determine if you are on the last page.
* **getPageItems** - This method is used to return the array of results inside of each page. This is used when using the `items` or `collect` method on your paginator class.
Let's implement these two methods on our paginator and dive into how it works.
```php
<?php
use Saloon\Http\Response;
use Saloon\Http\Connector;
use Saloon\PaginationPlugin\PagedPaginator;
use Saloon\PaginationPlugin\Contracts\HasPagination;
class SpotifyConnector 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 is_null($response->json('next_page_url'));
}
protected function getPageItems(Response $response, Request $request): array
{
return $response->json('items');
}
};
}
}
```
Let's assume in this hypothetical example that the API provides some useful information to see if we're on the last page. In this example, we know we're on the last page if the `next_page_url` JSON property on the body is null. There are various other ways of knowing if you are on the last page, so it's best to fully understand your API's pagination.&#x20;
To get the page items, we'll use the `json` method on the response to access the `items` array from the body.
### Specifying a default per-page on the paginator
The third-party API you are integrating with may require you to define a page size (per page) on your requests, or you may want to set a default page size used for every request. You can set this default page size as a property on the paginator class.&#x20;
<pre class="language-php"><code class="lang-php">public function paginate(Request $request): PagedPaginator
{
return new class(connector: $this, request: $request) extends PagedPaginator
{
<strong> protected ?int $perPageLimit = 100;
</strong>
// ...
}
}
</code></pre>
You may also use the `setPerPageLimit` method on an instantiated paginator if you want to configure it on the fly. **You must set this before iterating over the paginator.**&#x20;
```php
$spotifyConnector = new SpotifyConnector;
$paginator = $spotifyConnector->paginate($request);
$paginator->setPerPageLimit(250);
// foreach($paginator as $response) { ... }
```
### **Assumptions made with the PagedPaginator**
The PagedPaginator will apply pagination by sending two **query parameters**:
* page
* per\_page
This assumption might not be the way your third-party API works. You can really easily change this by extending the `applyPagination` method. For example, let's say our API expects a "currentPage" and a "pageSize" instead. We can simply extend the `applyPagination` method and change the keys. You can apply the pagination in whichever the API requires.
<pre class="language-php"><code class="lang-php">public function paginate(Request $request): PagedPaginator
{
return new class(connector: $this, request: $request) extends PagedPaginator
{
protected function isLastPage(Response $response): bool
{
return is_null($response->json('next_page_url'));
}
protected function getPageItems(Response $response, Request $request): array
{
return $response->json('items');
}
protected function applyPagination(Request $request): Request
{
<strong> $request->query()->add('currentPage', $this->currentPage);
</strong>
if (isset($this->perPageLimit)) {
<strong> $request->query()->add('pageSize', $this->perPageLimit);
</strong> }
return $request;
}
};
}
</code></pre>
### **Useful Properties On The PagedPaginator**
We previously mentioned that the PagedPaginator class provides a few methods that can be used to help with last-page calculation. These methods are:
* **totalResults** - An integer which will return the total number of items returned. This can be used to check if it's equal to the number of total results in the list.
* **currentPage** - An integer which will return the current page that the paginator is currently on.
### Next Steps
After configuring your paginator, head back to the [Using The Paginator](./#using-the-paginator) section of the documentation.
]]>
</paged_pagination_md>
<README_md>
<![CDATA[
# 📚 Pagination
When building API integrations, you may encounter a scenario where the server does not provide all the results in a single list. Instead, it divides the results into several pages. This strategy is called pagination, and integrating it into your application can be tedious and repetitive. With Saloon, you can install this pagination plugin to reduce the boilerplate code and iterate through every result across _every_ page in one loop. **It's like magic!** Saloon has three paginators out of the box to support the most common types of pagination:
* **Paged pagination** - where you have to specify the next page as a query parameter (?page=1, ?page=2 and so on)
* **Limit/Offset pagination** - where you must specify two query parameters - a limit and an offset.
* **Cursor pagination** - where each page will return a "cursor" (a string) that you use to query the next page.
Saloon's paginators are custom [PHP iterators](https://www.php.net/manual/en/class.iterator.php)_,_ meaning they can be used in for-loops. They are also memory efficient, so they only keep one page in memory at a time, this means you can use Saloon's paginators to iterate through **thousands of pages and millions of results without running out of memory**.
Additionally, Saloon's paginators have a few really useful methods to traverse through the results, like mapping them into Laravel's famous "Collection" class or simply an array of results. Here's how the pagination looks in your application:
```php
<?php
$spotifyConnector = new SpotifyConnector;
$likedSongs = $spotifyConnector->paginate(new GetLikedSongsRequest);
foreach($likedSongs->items() as $likedSong) {
// e.g. Luke Combs - When It Rains It Pours 🎵
}
```
{% hint style="info" %}
Saloon's paginators are versatile and can work with any API format, including JSON, XML, and even multipart responses.
{% endhint %}
### Getting Started
Saloon's paginators are provided through a plugin. First, install the following package with Composer.
```sh
composer require saloonphp/pagination-plugin "^2.0"
```
### Making your requests pageable
Once the pagination plugin is installed, you must implement an interface on the requests that use pagination. This interface enables the paginator to decide whether or not to try pagination. It's important to do this as it avoids developer mistakes and potentially causing infinite loops.
All you need to do is add the `Paginatable` interface to your requests.
<pre class="language-php"><code class="lang-php">&#x3C;?php
use Saloon\Http\Request;
<strong>use Saloon\PaginationPlugin\Contracts\Paginatable;
</strong>
<strong>class GetLikedSongsRequest extends Request implements Paginatable
</strong>{
// ...
}
</code></pre>
After that, you need to add the `HasPagination` interface to your connector. This will require you to define a `paginate` method which we will implement next.
<pre class="language-php"><code class="lang-php">&#x3C;?php
use Saloon\Http\Connector;
use Saloon\PaginationPlugin\Paginator;
use Saloon\PaginationPlugin\Contracts\HasPagination;
class SpotifyConnector extends Connector implements HasPagination
{
// ...
<strong> public function paginate(Request $request): Paginator
</strong> {
//
}
}
</code></pre>
Now you need to choose a paginator that corresponds to the pagination type used by the third-party API. This documentation is divided into three sections based on Saloon's three pre-installed paginators.
{% content-ref url="paged-pagination.md" %}
[paged-pagination.md](paged-pagination.md)
{% endcontent-ref %}
{% content-ref url="limit-offset-pagination.md" %}
[limit-offset-pagination.md](limit-offset-pagination.md)
{% endcontent-ref %}
{% content-ref url="cursor-pagination.md" %}
[cursor-pagination.md](cursor-pagination.md)
{% endcontent-ref %}
### Building Your Own Paginators
Sometimes an API may implement their own kind of pagination and Saloon might not have a pagination supported out of the box for it. You can create your own paginators really easily. Follow the link below for building your own paginators.
{% content-ref url="custom-pagination.md" %}
[custom-pagination.md](custom-pagination.md)
{% endcontent-ref %}
### Using The Paginator
After you have configured your paginator, it's time to start using it! There are various ways to use the paginator, so we'll cover them in this part of the guide. First, let's take a look at how we instantiate our paginator. When we instantiate the paginator - Saloon won't make any requests. The requests are only made when we start iterating over the paginator.
```php
<?php
$spotifyConnector = new SpotifyConnector;
$paginator = $spotifyConnector->paginate(new GetLikedSongsRequest);
```
As you can see, instantiating the paginator is as simple as running the method and passing in the paginatable request. We are now ready to start using the paginator.
#### Iterating Over Responses
The simplest way to start using the paginator is to use it inside of a `foreach` loop. This will return each page as a Saloon `Response` for you to access. Saloon will only keep one response in memory at a time so you won't have to worry about running out of memory.
```php
<?php
$spotifyConnector = new SpotifyConnector;
$paginator = $spotifyConnector->paginate(new GetLikedSongsRequest);
foreach($paginator as $response) {
$status = $response->status();
$data = $response->body();
}
```
#### Iterating Over Items
One of the more exciting methods Saloon provides is the `items` method. This cool method will give you each item across multiple pages. With this, you can handle the individual items. This uses the `getPageItems` method which you defined earlier in your paginator.
```php
<?php
$spotifyConnector = new SpotifyConnector;
$paginator = $spotifyConnector->paginate(new GetLikedSongsRequest);
foreach($paginator->items() as $song) {
$songName = $song['name'];
$songArtists = $song['artists'];
}
```
#### Using Laravel Collections (LazyCollection)
Another really cool method Saloon provides is the `collect` method. If you are using Laravel with Saloon, this method will work out of the box, but if you aren't using Laravel - you can add the support for this method by installing the "collections" library via Composer.
```bash
composer require illuminate/collections
```
This method will return a `LazyCollection` class. This class extends Laravel's base `Collection` class but works with custom iterators and keeps memory consumption low. The `LazyCollection` is really powerful when it comes to processing results from a paginated API. You can use the collection to filter, map, sort and much more all within just one iteration!
```php
<?php
$spotifyConnector = new SpotifyConnector;
$paginator = $spotifyConnector->paginate(new GetLikedSongsRequest);
$collection = $paginator->collect();
$results = $collection
->filter(function (array $song) {
return count($song['artists']) > 1;
})
->map(function (array $song) {
return $song['name'];
})
->sort();
foreach($results as $song) {
// $song: "When It Rains It Pours"
}
```
Even though in this example, we are doing multiple operations (filter, map, sort) the transformation will only happen when we iterate through the `LazyCollection` making it very performant and easy on system memory.
By default, Saloon will iterate through each result within each API page, but if you would rather have a collection that iterates through `Response` classes, you may do this too. Just set the `throughItems` argument to false.
<pre class="language-php"><code class="lang-php">&#x3C;?php
$spotifyConnector = new SpotifyConnector;
$paginator = $spotifyConnector->paginate(new GetLikedSongsRequest);
<strong>$collection = $paginator->collect(throughItems: false);
</strong></code></pre>
{% hint style="info" %}
To see a full list of available methods on the `LazyCollection` you can [click here to read](https://laravel.com/docs/collections) the Laravel Documentation on Collections.
{% endhint %}
### Advanced Features
Saloon's paginators are a fantastic way of iterating through many page-separated results quickly and easily without writing boilerplate code in your application. So far we've covered the basics of pagination, but there's still plenty you can do. Within this section of the guide, we'll go through some more advanced features.
#### Casting Items Into DTOs
Saloon has a great way of casting responses into DTOs, but what if instead of getting the raw array or string item for each item in the paginator you wanted to resolve your DTO? You can simply use your `dto` or `dtoOrFail` method inside of the `getPageItems` method of your paginator. By using this method, you already have configured your DTO building logic on your connector. If you're not sure how to do this, [click here](../../the-basics/data-transfer-objects.md).
{% tabs %}
{% tab title="Paginator Config" %}
<pre class="language-php"><code class="lang-php">class SpotifyConnector extends Connector implements HasPagination
{
public function paginate(Request $request): PagedPaginator
{
return new class(connector: $this, request: $request) extends PagedPaginator
{
// ...
protected function getPageItems(Response $response, Request $request): array
{
<strong> return $response->dto();
</strong> }
}
}
}
</code></pre>
{% endtab %}
{% tab title="Request" %}
<pre class="language-php"><code class="lang-php">&#x3C;?php
class GetLikedSongsRequest extends Request
{
// {...}
public function createDtoFromResponse(Response $response): array
{
<strong> return array_map(function (array $song) {
</strong><strong> return Song::fromArray($song);
</strong><strong> }, $response->json('items'));
</strong> }
}
</code></pre>
{% endtab %}
{% endtabs %}
{% hint style="info" %}
The `createDtoFromResponse` method on your request/connector must return an array.
{% endhint %}
You can also implement the DTO conversion logic inside of the `getPageItems` method like below.
```php
protected function getPageItems(Response $response, Request $request): array
{
return array_map(function (array $song) {
return Song::fromArray($song);
}, $response->json('items'));
}
```
#### Asynchronous Pagination / Pooling
You may prefer to send your requests asynchronously or use a request pool for better performance. To get started with this, you must implement a new method on your paginator called `getTotalPages`. This method is invoked after the first request and is used to determine how many additional requests Saloon needs to send.
Let's start with defining our `getTotalPages` method. Some APIs may not return this information, and unfortunately, if they do not return this information then you may not be able to do asynchronous pagination.
<pre class="language-php"><code class="lang-php">&#x3C;?php
class SpotifyConnector extends Connector implements HasPagination
{
public function paginate(Request $request): PagedPaginator
{
return new class(connector: $this, request: $request) extends PagedPaginator
{
// ...
<strong> protected function getTotalPages(Response $response): int
</strong> {
<strong> return $response->json('total_results');
</strong> }
}
}
}
</code></pre>
Next, instantiate your paginator and use the `async` method to enable asynchronous pagination. Now when you iterate through results, you will get a `Promise` instance instead of a Saloon Response.
<pre class="language-php"><code class="lang-php">&#x3C;?php
$spotifyConnector = new SpotifyConnector;
$paginator = $spotifyConnector->paginate(new GetLikedSongsRequest);
<strong>$paginator->async();
</strong>
<strong>foreach($paginator as $promise) {
</strong> // $promise->then()
}
</code></pre>
You can also use a paginator pool. If you are unfamiliar with how asynchronous request pools work, [check out this section of the documentation](../concurrency-and-pools.md) first.
<pre class="language-php"><code class="lang-php">&#x3C;?php
$spotifyConnector = new SpotifyConnector;
$paginator = $spotifyConnector->paginate(new GetLikedSongsRequest);
<strong>$pool = $paginator->pool(concurrency: 10);
</strong>
$pool->withResponseHandler(function (Response $response) {
// Handle each response
$status = $response->status();
$data = $response->body();
});
<strong>$promise = $pool->send();
</strong>
<strong>$promise->wait();
</strong></code></pre>
#### Custom Per-Request Pagination
Sometimes you may wish to use a different paginator on your request than your connector. For example, an API might not use the same pagination across all its endpoints. To get started with this - first, implement the `HasRequestPagination` interface on your request. This interface will expect you to define a `paginate` method on your request.
```php
<?php
use Saloon\PaginationPlugin\Contracts\HasRequestPagination;
class GetLikedSongsRequest extends Request implements HasRequestPagination
{
// {...}
public function paginate(Connector $connector): Paginator;
{
//
}
}
```
This method works just like the `paginate` method on your connector, so follow the steps above to configure your paginator. The main difference is that this method expects a connector to be passed into the constructor and not a request.
```php
<?php
use Saloon\PaginationPlugin\Contracts\HasRequestPagination;
class GetLikedSongsRequest extends Request implements HasRequestPagination
{
// {...}
public function paginate(Connector $connector): Paginator
{
return new class(connector: $connector, request: $this) extends PagedPaginator
{
protected function isLastPage(Response $response): bool
{
return is_null($response->json('next_page_url'));
}
protected function getPageItems(Response $response, Request $request): array
{
return $response->json('items');
}
};
}
}
```
Next, you can pass the connector into your request to instantiate the paginator.
<pre class="language-php"><code class="lang-php">&#x3C;?php
$spotifyConnector = new SpotifyConnector;
$request = new GetLikedSongsRequest;
<strong>$paginator = $request->paginate($spotifyConnector);
</strong></code></pre>
#### Counting results
You may wish to count the pages of the paginator. You can use the `count()` PHP method to get the total number of pages from a paginator.
<pre class="language-php"><code class="lang-php">&#x3C;?php
$spotifyConnector = new SpotifyConnector;
$paginator = $spotifyConnector->paginate(new GetLikedSongsRequest);
<strong>$pages = count($paginator);
</strong></code></pre>
{% hint style="warning" %}
You should use the `count()` method and not `iterator_count()` on the paginator because `iterator_count()` is not supported and you may get inaccurate results.
{% endhint %}
#### Handling Infinite Loops
While building your paginators, sometimes, you might run into a situation when the paginator is attempting to get the same page over and over again, causing an infinite loop. This is usually caused by mis-configuring the `isLastPage` method. To try to mitigate infinite loops, the paginator has a built-in safety feature that will throw an exception if the last five responses have exactly the same body.
Saloon will throw a `PaginationException` with a message like:
> Potential infinite loop detected! The last 5 requests have had exactly the same body.
If you would like to disable this check, you can set the `detectInfiniteLoop` property on your paginator to false.
<pre class="language-php"><code class="lang-php">public function paginate(Connector $connector): Paginator;
{
return new class(connector: $connector, request: $this) extends PagedPaginator
{
<strong> protected bool $detectInfiniteLoop = false;
</strong> }
}
</code></pre>
{% hint style="warning" %}
Detecting infinite loops with asynchronous pagination is not supported.
{% endhint %}
]]>
</README_md>
</pagination>
<psr_support_md>
<![CDATA[
# ⛩️ PSR Support
When you send a request, Saloon will construct a [PSR-7](https://www.php-fig.org/psr/psr-7/) request behind the scenes and pass it to the HTTP client. Saloon also uses [PSR-17](https://www.php-fig.org/psr/psr-17/) factories defined by the sender to create the URI (the full URL) and streams for the request body. When you make a request to Saloon, the response will contain a PSR-7 request and response pair.&#x20;
```php
<?php
$response = $forgeConnector->send(new GetServersRequest);
$psrResponse = $response->getPsrResponse();
$psrRequest = $response->getPsrRequest();
```
This design decision was made because the PSR-7 and 17 standards are very well-known and have been implemented by many popular HTTP clients. While Saloon uses Guzzle as the default way to send requests, if Guzzle suddenly becomes abandoned - Saloon can use one of many other HTTP clients without needing an entire rebuild from scratch. While Guzzle is amazing, and we'd never want it to go away - making Saloon independent of the HTTP client was a big step toward future-proofing the library.
Another significant benefit to having PSR support is a better experience for you, the developer. With previous versions, Saloon heavily relied on Guzzle's config options to build the PSR-7 request internally, which meant that debugging was difficult because you never really knew what was sent to the third party. With version three, you can see the PSR request right before it is sent. If you want to learn more about debugging the PSR request, [click here](../the-basics/debugging.md).
### Modifying the PSR-7 Request
There may be a situation where you need to modify the PSR-7 request before it's sent to the HTTP client. You can use a special hook on either the connector or the request. This hook provides you with the built PSR-7 request that you can modify and return. To use this hook, extend the `handlePsrRequest` method on either your connector or request.
If you use the method on the connector - the method will be run for every request sent through the connector. The method will give you access to both the PSR request and the Saloon `PendingRequest` if you need to use it for additional information, like the Saloon request class.&#x20;
```php
<?php
use Saloon\Http\PendingRequest;
use Psr\Http\Message\RequestInterface;
class ForgeConnector extends Connector
{
public function handlePsrRequest(RequestInterface $request, PendingRequest $pendingRequest): RequestInterface
{
$request->withHeader('X-API-Key', 'Howdy!');
return $request;
}
}
```
{% hint style="warning" %}
You may find it easier to modify the `Connector`, `Request`, or `PendingRequest` classes instead of the underlying PSR-7 request. You should be cautious when using this hook as there are many nicer, higher-level APIs in Saloon to modify the request.
{% endhint %}
]]>
</psr_support_md>
<retrying_requests_md>
<![CDATA[
# 🎯 Retrying Requests
Sometimes you may use APIs that are unreliable or require you to retry multiple times before a request is successful. Saloon has a global retry system that can be configured to automatically retry failed requests.
### Configuring our connector
Let's say that we have an API which isn't reliable. We can define a public `$tries` property on our connector which defines how many times a request will be sent if it fails.
<pre class="language-php"><code class="lang-php">class ForgeConnector extends Connector
{
<strong> public ?int $tries = 3;
</strong>}
</code></pre>
Now when we send any request through this connector, if it fails it will be retried.
```php
$forgeConnector = new ForgeConnector;
$response = $forgeConnector->send(new UnreliableRequest);
```
If we get to attempt three and it still fails, Saloon will throw a `FatalRequestException` (for connection errors) or a `RequestException` for typical HTTP errors.
{% hint style="warning" %}
The retry functionality will only work when sending requests synchronously. The retry logic will not work when using `sendAsync` or pools because Saloon needs to wait for a response to come back to determine if it needs to be retried.
{% endhint %}
#### What does Saloon consider a failed request?
By default, if the API couldn't be connected to or if the response status code is either a 4xx or 5xx, then Saloon will consider it a failed request. You can change this by extending the `hasRequestFailed` method on your connector. [Click here to learn more.](../the-basics/handling-failures.md#customising-when-saloon-thinks-a-request-has-failed)
### Intervals
You can also configure an interval in **milliseconds** that Saloon should wait between retries. You can configure an interval with the `$retryInterval` property.
<pre class="language-php"><code class="lang-php">class ForgeConnector extends Connector
{
public ?int $tries = 3;
<strong> public ?int $retryInterval = 1000;
</strong>}
</code></pre>
#### Exponential Backoff
You can also use exponential backoff when retrying which will double the retry interval after each unsuccessful attempt. This is useful as it can be less strenuous on the API if it is experiencing issues.
<pre class="language-php"><code class="lang-php">class ForgeConnector extends Connector
{
public ?int $tries = 3;
public ?int $retryInterval = 500;
<strong> public ?bool $useExponentialBackoff = true;
</strong>}
</code></pre>
### Exceptions
By default, when all attempts have been exceeded, Saloon will either throw a `FatalRequestException` or a `RequestException`. You can choose to disable this behaviour and return the last response that Saloon received instead.
If the exception was `FatalRequestException` then the exception will still be thrown as we wouldn't have a response to return.&#x20;
<pre class="language-php"><code class="lang-php">class ForgeConnector extends Connector
{
public ?int $tries = 3;
<strong> public ?bool $throwOnMaxTries = false;
</strong>}
</code></pre>
### Customising the request before the next retry
In some situations, you might want to change the next request to improve the chances of the next retry being successful. You can overwrite the `handleRetry` method on the connector or request and use it to customise the `Request` class. You must also return a boolean in this method to tell Saloon if the retry should commence.
<pre class="language-php"><code class="lang-php">&#x3C;?php
use Saloon\Exceptions\RequestException;
use Saloon\Http\Auth\TokenAuthenticator;
use Saloon\Exceptions\FatalRequestException;
use Saloon\Http\Request;
class ForgeConnector extends Connector
{
public ?int $tries = 3;
public function handleRetry(FatalRequestException|RequestException $exception, Request $request): bool
{
if ($exception instanceof RequestException &#x26;&#x26; $exception->getResponse()->status() === 401) {
<strong> $request->authenticate(new TokenAuthenticator($this->getNewToken()));
</strong> }
return true;
}
}
</code></pre>
### Using the sendAndRetry method
{% hint style="warning" %}
This method has been deprecated since Saloon v3.6.4
{% endhint %}
Saloon also has a `sendAndRetry` method which can be used on the fly on any connector or request without needing to configure anything. This method has the same functionality in the format of a method.
```php
<?php
$forgeConnector = new ForgeConnector;
$response = $forgeConnector->sendAndRetry(
request: new UnreliableRequest,
tries: 3,
interval: 500,
handleRetry: function (FatalRequestException|RequestException $exception, Request $request) {
//
},
throw: true,
useExponentialBackoff: true,
);
```
]]>
</retrying_requests_md>
<solo_requests_md>
<![CDATA[
# ☝ Solo Requests
While Saloon's typical setup of a connector and requests is great, sometimes all you need is to make a single request to a service. For scenarios like these, you may create a "SoloRequest" instead of making a connector and a single request. This saves you from having to create additional classes.
### Setup
Create a class, but instead of extending `Saloon\Http\Request`, you should extend `Saloon\Http\SoloRequest.` Next, just define everything else like you would a normal request. Make sure to include the full URL of the service you are integrating with.
```php
<?php
use Saloon\Http\SoloRequest;
use Saloon\Enums\Method;
class GetAllPokemonRequest extends SoloRequest
{
protected Method $method = Method::GET;
public function resolveEndpoint(): string
{
return 'https://pokeapi.co/api/v2/pokemon';
}
}
```
### Defaults
Saloon Requests allow you to define all your default headers, config, query parameters and define request body just like you would traditionally with a connector.
### Sending Solo Requests
As you don't have a connector for this request, you can use the `send` or `sendAsync` methods directly on the request instance. This method works exactly the same as it would on the connector.
```php
<?php
$request = new GetAllPokemonRequest;
$response = $request->send();
```
]]>
</solo_requests_md>
</digging_deeper>
<installable_plugins>
<caching_responses_md>
<![CDATA[
# 🔁 Caching Responses
There are scenarios where you may want to cache a response from an API, like retrieving a static list or retrieving data that you know won't change for a specified amount of time. Caching can be incredibly powerful and can speed up an application by relying less on a third-party integration. Saloon has a [first-party plugin](https://github.com/Sammyjo20/saloon-cache-plugin) that you can install to enable caching support.
### Installation
To install the caching functionality into Saloon, install the plugin using Composer.
```bash
composer require saloonphp/cache-plugin "^3.0"
```
### Configuration
Next, add the `Cacheable` interface and `HasCaching` trait to your request or connector. You must define the two methods.
* resolveCacheDriver
* cacheExpiryInSeconds
```php
<?php
use Saloon\CachePlugin\Contracts\Driver;
use Saloon\CachePlugin\Traits\HasCaching;
use Saloon\CachePlugin\Contracts\Cacheable;
class GetServersRequest extends Request implements Cacheable
{
use HasCaching;
// ...
public function resolveCacheDriver(): Driver
{
//
}
public function cacheExpiryInSeconds(): int
{
//
}
}
```
{% hint style="warning" %}
When you add the `HasCaching` trait onto your connector, every request through the connector will be cached.
{% endhint %}
### Cache Drivers
Saloon's caching plugin works with different caching drivers that you can use on your request or connector. The plugin comes with the following cache drivers:
* PsrCacheDriver (Supports PSR-16 Cache Implementations)
* FlysystemDriver (Requires `league/flysystem` version 3)
* LaravelCacheDriver (Supports any of Laravel's cache disks, requires Laravel)
{% tabs %}
{% tab title="PsrCacheDriver" %}
```php
use Saloon\CachePlugin\Drivers\PsrCacheDriver;
public function resolveCacheDriver(): Driver
{
// This example uses the PhpArrayAdapter from
// the symfony/cache library.
return new PsrCacheDriver(new PhpArrayAdapter);
}
```
{% endtab %}
{% tab title="FlysystemDriver" %}
```php
use Saloon\CachePlugin\Drivers\FlysystemDriver;
public function resolveCacheDriver(): Driver
{
// This example uses the "AwsS3V3Adapter" driver
// that is provided by Flysystem.
return new FlysystemDriver(
new Filesystem(new AwsS3V3Adapter($s3Client, 'bucket-name'))
);
}
```
{% endtab %}
{% tab title="LaravelCacheDriver" %}
```php
use Illuminate\Support\Facades\Cache;
use Saloon\CachePlugin\Drivers\LaravelCacheDriver;
public function resolveCacheDriver(): Driver
{
// This example uses Redis cache store that
// Laravel provides.
return new LaravelCacheDriver(Cache::store('redis'));
}
```
{% endtab %}
{% endtabs %}
### Expiry
When Saloon caches a request, it will last a specified amount of time in seconds. You should specify this time in the `cacheExpiryInSeconds` method.
```php
public function cacheExpiryInSeconds(): int
{
return 3600; // One Hour
}
```
### Caching Requests
Once you have configured the cache driver and expiry, Saloon will automatically cache requests and store them in your requested cache-store. The next time you send a request, the cached response will be automatically swapped out and no real request will be sent.
#### Checking if a response is cached
You may use the `isCached()` method to determine if a response is cached.
```php
<?php
$forge = new ForgeConnector;
$response = $forge->send(new GetServersRequest);
$response->isCached(); // false
// Next time it is sent...
$response = $forge->send(new GetServersRequest);
$response->isCached(); // true
```
#### When will Saloon cache a request?
Saloon will only cache a **successful** request when the method is either GET or OPTIONS. You can customise this by extending the `getCacheableMethods` method where you added the `HasCaching` trait.
```php
<?php
use Saloon\Enums\Method;
class GetServersRequest extends Request implements Cacheable
{
use HasCaching;
protected function getCacheableMethods(): array
{
return [Method::GET, Method::OPTIONS, Method::POST];
}
}
```
You may customise when Saloon considers a request as successful. To read more about this, [click here.](../the-basics/handling-failures.md#customising-when-saloon-thinks-a-request-has-failed)
### Customising the cache key
By default, the cache key is created from the full request URL, the headers that are sent and the query parameters that are used. You may choose to define your own custom cache key. Just extend the protected `cacheKey` method where you added the `HasCaching` trait. You will get access to the `PendingRequest` instance that contains all the request properties.
```php
<?php
use Saloon\Http\PendingRequest;
class GetServersRequest extends Request implements Cacheable
{
use HasCaching;
protected function cacheKey(PendingRequest $pendingRequest): ?string
{
return 'custom-cache-key';
}
}
```
### Invalidating the current cache
You may want to make a request and purge the existing cache before making the request. You can use the `invalidateCache` method on the request before sending the request and Saloon will delete any existing cache for that request.
```php
<?php
$forge = new ForgeConnector;
$request = new GetServersRequest;
$request->invalidateCache();
$response = $forge->send($request);
```
### Temporarily Disabling Caching
Sometimes you may wish to disable the caching on a per-request basis for debugging or to bypass caching. You can do this by using the `disableCaching` method on the request.
```php
<?php
$forge = new ForgeConnector;
$request = new GetServersRequest;
$request->disableCaching();
$response = $forge->send($request);
```
### Source Code
To view the source code of this plugin, [click here](https://github.com/Sammyjo20/saloon-cache-plugin).
]]>
</caching_responses_md>
<handling_rate_limits_md>
<![CDATA[
# ⛔ Rate Limit Handler
Handling rate limits with API integrations can be hard. Saloon has a first-party plugin that provides you with the tools you need to prevent rate limits and handle what happens if a rate limit is exceeded.
With this plugin, you are able to define various limits on a per-connector or request basis. You can also control if Saloon should throw an exception or sleep if a limit is reached. Saloon will keep track of how many requests are made and when a rate limit is hit, Saloon will prevent further requests on the connector/request until the rate limit has been lifted.
Saloon will even listen out for "429 Too Many Requests" responses and will automatically throw exceptions before your future requests are sent and the limit will be lifted based on the `Retry-After` header.
This plugin also comes with a Laravel Job middleware which you can use inside of your jobs to automatically release them back onto the queue if a rate limit has been reached.
```php
<?php
use Saloon\Http\Connector;
use Saloon\RateLimitPlugin\Limit;
use Saloon\Traits\Plugins\AcceptsJson;
use Saloon\RateLimitPlugin\Stores\RedisStore;
use Saloon\RateLimitPlugin\Traits\HasRateLimits;
use Saloon\RateLimitPlugin\Contracts\RateLimitStore;
class SpotifyConnector extends Connector
{
use AcceptsJson;
use HasRateLimits;
public function resolveBaseUrl(): string
{
return 'https://api.spotify.com/v1';
}
protected function resolveLimits(): array
{
return [
Limit::allow(100)->everyMinute(),
Limit::allow(1000)->everyDay(),
Limit::allow(5000)->everyMonth(),
];
}
protected function resolveRateLimitStore(): RateLimitStore
{
$redis = new Redis;
$redis->connect('127.0.0.1');
return new RedisStore($redis);
}
}
```
### Installation
You can install this plugin via Composer.
```
composer require saloonphp/rate-limit-plugin "^2.0"
```
### Getting Started
To use the plugin, add the `HasRateLimit` trait to your connector or request. If you have a connector, you should put it on the connector, but it can be put on an individual request if a specific endpoint has a different rate limit or if you are using solo requests. If you are using the trait on both your connector and request, the rate limits are combined together.
```php
use Saloon\Http\Connector;
use Saloon\RateLimitPlugin\Traits\HasRateLimits;
class SpotifyConnector extends Connector
{
use HasRateLimits;
}
```
Next, you will be required to implement two methods: `resolveLimits` and `resolveRateLimitStore`. These methods allow you to define the limits that Saloon will keep track of and the store where the limit "hits" will be kept.
```php
use Saloon\Http\Connector;
use Saloon\RateLimitPlugin\Contracts\RateLimitStore;
use Saloon\RateLimitPlugin\Stores\MemoryStore;
use Saloon\RateLimitPlugin\Traits\HasRateLimits;
class SpotifyConnector extends Connector
{
use HasRateLimits;
protected function resolveLimits(): array
{
return [];
}
protected function resolveRateLimitStore(): RateLimitStore
{
return new MemoryStore;
}
}
```
### Stores
Here are the various stores that the rate-limiting plugin supports. You may also [create your own stores](https://github.com/Sammyjo20/saloon-docs/blob/v2/digging-deeper/broken-reference/README.md) if this library does not come with the one you need. Stores are used to keep track of how many requests have been sent through a given connector/request.
#### Available Stores
* In-Memory (Array)
* File
* Redis
* Predis
* PSR Cache Store
* Laravel Cache Store
#### Memory Store
The simplest store. This store is persisted on the current instance of the connector/request and all information is lost when the connector is destructed.
```php
use Saloon\RateLimitPlugin\Contracts\RateLimitStore;
use Saloon\RateLimitPlugin\Stores\MemoryStore;
protected function resolveRateLimitStore(): RateLimitStore
{
return new MemoryStore;
}
```
#### File Store
This store will use the local filesystem to store the limits. The only requirement for this store is for you to define the absolute path to a directory where you would like the limits to be stored.
```php
use Saloon\RateLimitPlugin\Contracts\RateLimitStore;
use Saloon\RateLimitPlugin\Stores\FileStore;
protected function resolveRateLimitStore(): RateLimitStore
{
return new FileStore('some/application/directory');
}
```
#### Redis Store
This store will use PHP's `Redis` extension to store the limits on a Redis database. You should pass the Redis configuration into this store.
```php
use Redis;
use Saloon\RateLimitPlugin\Contracts\RateLimitStore;
use Saloon\RateLimitPlugin\Stores\RedisStore;
protected function resolveRateLimitStore(): RateLimitStore
{
$client = new Redis;
$client->connect('127.0.0.1');
return new RedisStore($client);
}
```
#### Predis Store
Similar to the `RedisStore`, the `PredisStore` allows you to connect to Redis through the `predis/predis` PHP library.
```php
use Saloon\RateLimitPlugin\Contracts\RateLimitStore;
use Saloon\RateLimitPlugin\Stores\PredisStore;
protected function resolveRateLimitStore(): RateLimitStore
{
$client = new Predis\Client([
'scheme' => 'tcp',
'host' => '10.0.0.1',
'port' => 6379,
]);
return new PredisStore($client);
}
```
#### PSR Cache Store
This store supports any PSR-16 cache store provided by the `psr/simple-cache` library.
```php
use Saloon\RateLimitPlugin\Contracts\RateLimitStore;
use Saloon\RateLimitPlugin\Stores\PsrStore;
protected function resolveRateLimitStore(): RateLimitStore
{
return new PsrStore(new SomePsr16Store);
}
```
#### Laravel Cache Store
This store can only be used in a Laravel environment but allows you to use any of Laravel's cache disks.
```php
use Illuminate\Support\Facades\Cache;
use Saloon\RateLimitPlugin\Contracts\RateLimitStore;
use Saloon\RateLimitPlugin\Stores\LaravelCacheStore;
protected function resolveRateLimitStore(): RateLimitStore
{
return new LaravelCacheStore(Cache::store('redis'));
}
```
### Limits
While this plugin can detect if a 429 status occurs from a response, it's better to prevent your application from hitting rate limits than let them happen. This plugin provides an expressive `Limit` class which can be used to define different limits. You can define as many limits as you like, with various intervals.
#### What limit type should I be using?
The rate limit plugin supports two types of limits, time-based limiters, for example 100 requests every 60 seconds and a "Leaky Bucket" limiter. Please continue reading if you need to use time-based limiters or you can [click here for leaky bucket limiters](handling-rate-limits.md#leaky-bucket-limiter).
#### Configuring Limits
Here is a simple example of a limit for an API which only allows 60 requests per minute, but has a daily limit of 1,000 API calls. There are many different limit intervals, as well as different ways you can instruct Saloon to handle the limit. There is no restriction on the number of limits you can have.
```php
use Saloon\RateLimitPlugin\Limit;
protected function resolveLimits(): array
{
return [
Limit::allow(60)->everyMinute(),
Limit::allow(1000)->everyDay(),
];
}
```
#### Limit Intervals
There are various limit intervals which you can use on your limiter, ranging for seconds to up to the end of the month.
```php
use Saloon\RateLimitPlugin\Limit;
Limit::allow(60)->everySeconds(seconds: 5);
Limit::allow(60)->everyMinute();
Limit::allow(60)->untilEndOfMinute();
Limit::allow(60)->everyFiveMinutes();
Limit::allow(60)->everyThirtyMinutes();
Limit::allow(60)->everyHour();
Limit::allow(60)->untilEndOfHour();
Limit::allow(60)->everySixHours();
Limit::allow(60)->everyTwelveHours();
Limit::allow(60)->everyDay();
Limit::allow(60)->everyDayUntil('8pm');
Limit::allow(60)->untilMidnightTonight();
Limit::allow(60)->untilEndOfMonth();
```
#### Custom Names
Sometimes you may need to add a name to your limiter. A good example of this is if you have separate API keys per user and therefore require a different API rate limit per user. You can add the `name()` method to your limit to specify a custom name for your limiter. Each limiter's name must be unique.
```php
protected function resolveLimits(): array
{
return [
Limit::allow(60)->everyMinute()->name('spotify-limit-user-' . $this->userId),
];
}
```
#### Custom Prefixes
Saloon will append the class name of the connector or a request as the prefix to the limiter name. For example SpotifyConnector:30\_every\_60. You may customise the prefix by extending the `getLimiterPrefix` method on your connector or request.
```php
protected function getLimiterPrefix(): ?string
{
return 'spotify-user-' . $this->userId;
}
```
#### Custom Thresholds
You may want to specify the percentage threshold that Saloon should accept as the number of "hits" on a given limit. This is useful if you want to stay just under the real API limit while still defining the limit in the connector/request.
The threshold must be a number between 0 and 1 (e.g 0.8 = 80%)
```php
use Saloon\RateLimitPlugin\Limit;
protected function resolveLimits(): array
{
return [
Limit::allow(60, threshold: 0.8)->everyMinute(), // Will fail when at 80% capacity
];
}
```
#### Sleep
If would rather Saloon didn't throw an exception, you can use the `sleep` method when defining a limit. When using the sleep method, an exception won't be thrown. Instead, Saloon will wait the remaining number of seconds before a request is attempted again.
```php
use Saloon\RateLimitPlugin\Limit;
protected function resolveLimits(): array
{
return [
Limit::allow(60)->sleep(),
];
}
```
#### "429: Too Many Attempts" Detection
While it's recommended that you should define your limits above, Saloon will try to catch 429 "Too Many Attempts" errors from an API and will automatically mark a limit as "exceeded" if it sees this status. By default, Saloon will attempt to parse the `Retry-After` header to work out when a limit has been exceeded. If Saloon cannot calculate this, the limit will be released after 60 seconds.
You can customise this behaviour by overwriting the `handleTooManyAttempts` method.
```php
protected function handleTooManyAttempts(Response $response, Limit $limit): void
{
if ($response->status() !== 429) {
return;
}
$limit->exceeded(
releaseInSeconds: RetryAfterHelper::parse($response->header('Retry-After')),
);
}
```
Alternatively, you may choose to disable this functionality. You can do this by setting the `detectTooManyAttempts` property to `false` in your connector/request's constructor.
<pre class="language-php"><code class="lang-php">use Saloon\Http\Connector;
use Saloon\RateLimitPlugin\Stores\MemoryStore;
use Saloon\RateLimitPlugin\Traits\HasRateLimits;
class SpotifyConnector extends Connector
{
use HasRateLimits;
public function __construct()
{
<strong> $this->detectTooManyAttempts = false;
</strong> }
}
</code></pre>
When Saloon detects a rate limit has been exceeded, Saloon will throw a `RateLimitReachedException` . If you would like your application to sleep instead of throwing an exception you can extend the `getTooManyAttemptsLimiter` method and apply the `sleep` modifier
<pre class="language-php"><code class="lang-php">use Saloon\Http\Connector;
use Saloon\RateLimitPlugin\Stores\MemoryStore;
use Saloon\RateLimitPlugin\Traits\HasRateLimits;
class SpotifyConnector extends Connector
{
use HasRateLimits;
protected function getTooManyAttemptsLimiter(): ?Limit
{
<strong> return Limit::custom($this->handleTooManyAttempts(...))->sleep();
</strong> }
}
</code></pre>
{% hint style="warning" %}
When using the `sleep` modifier, Saloon will automatically attempt to make another request after detecting a rate limit being reached in the response. This does not affect the retry definition you may have defined on your connector or request.
{% endhint %}
### Leaky Bucket Limiter
Some applications may implement a "Leaky Bucket" algorithm for their rate limits, essentially this means that while you are making requests, within a given time frame (usually every second) you "gain" additional requests you can make. For example if an API integration allowed 60 requests per minute but had a leak rate of 1 per second, you could in theory be constantly communicating with the API as long as you stay within the 60 request limit.
The plugin implementation is really straight forward. In order to implement the above example you would write something like this.
```php
use Saloon\RateLimitPlugin\Bucket;
protected function resolveLimits(): array
{
return [
Bucket::capacity(60)
->leak(1)
->everySeconds(1)
->sleep()
];
}
```
{% hint style="info" %}
The `Bucket` class extends the `Limit` class so you may use all of the limit intervals as defined above.
{% endhint %}
You'll notice that it does an initial burst of 60 requests, then the bucket fills back up and the next second does 1 request, then the next second does 1 request, which adheres to our "leak 1 every 1 second" specification.
### Handling Rate Limits Being Exceeded
When a rate limit has been reached, Saloon will throw a `RateLimitReachedException`. This exception contains a `getLimit` method which may be used to see the limit that has thrown the exception and see the number of seconds to wait before a request can be sent again. This can be done with a simple try-catch approach or if you are using the provided Laravel job middleware, then you can instruct your jobs to wait until the limit has been lifted.
#### Try/Catch
As mentioned above, Saloon will throw an exception if a rate limit is reached or if the API returns a 429: Too Many Requests response. You could use a try/catch block to catch the exception and do something with the limit. For example, you may return an error to your users to let them know how long they need to wait - or you might retry the request later. If you are using Saloon in a context of a queued process, then you may want to retry the queued job in the future, from the remaining seconds.
```php
use Saloon\RateLimitPlugin\Exceptions\RateLimitReachedException;
$spotify = new SpotifyConnector;
try {
$response = $spotify->send(new GetPlaylistRequest);
} catch (RateLimitReachedException $exception) {
$seconds = $exception->getLimit()->getRemainingSeconds();
// Return our users back to our application with a custom response that could be
// shown on the front end.
return response("Too many requests to Spotify's API. Please try again in ${$seconds} seconds.");
}
```
#### Sleep
If would rather Saloon didn't throw an exception, you can use the `sleep` method when defining a limit. When using the sleep method, an exception won't be thrown. Instead, Saloon will wait the remaining number of seconds before a request is attempted again.
```php
use Saloon\RateLimitPlugin\Limit;
protected function resolveLimits(): array
{
return [
Limit::allow(60)->sleep(),
];
}
```
#### Laravel Job Middleware
If you are using Laravel, then this library comes with a [job middleware](https://laravel.com/docs/queues#job-middleware) that you can use. This job middleware will catch the `RateLimitReachedException` and automatically release your job back onto the queue with the remaining seconds added. Add this to the `middleware` method on your Laravel Job.
```php
use Saloon\RateLimitPlugin\Helpers\ApiRateLimited;
public function middleware(): array
{
return [new ApiRateLimited];
}
```
{% hint style="info" %}
You may also wish to increase your job's tries when using this middleware in case the job needs to be retried multiple times.
{% endhint %}
### Creating your own store
You may create your own rate limit store by implementing the `RateLimitStore` interface.
```php
use Saloon\RateLimitPlugin\Contracts\RateLimitStore;
class CustomStore implements RateLimitStore
{
/**
* Get a rate limit from the store
*/
public function get(string $key): ?string
{
//
}
/**
* Set the rate limit in the store
*/
public function set(string $key, string $value, int $ttl): bool
{
//
}
}
```
### Disabling Rate Limiting
Sometimes you might want to disable rate limiting by default or even disable it on a per-connector basis. You can either use a property to disable the rate-limiting functionality by default, or you can use the `useRateLimitPlugin()` method to disable it on a per-instance basis.
<pre class="language-php"><code class="lang-php">use Saloon\Http\Connector;
use Saloon\RateLimitPlugin\Traits\HasRateLimits;
class SpotifyConnector extends Connector
{
use HasRateLimits;
public function __construct()
{
<strong> $this->rateLimitingEnabled = false;
</strong> }
}
// Or...
$connector = new SpotifyConnector;
$connector->useRateLimitPlugin(false);
</code></pre>
]]>
</handling_rate_limits_md>
<laravel_integration_md>
<![CDATA[
# ⛵ Laravel Plugin
Saloon has been built to integrate beautifully with [Laravel](https://laravel.com). The separate Laravel plugin comes with a facade that helps with mocking and recording requests, Artisan console commands to easily build your API integrations, and events which you can listen to and build further functionality from.
### Installation
You can install the separate package with Composer. You must already have`saloonphp/saloon` as a required dependency in your `composer.json` file.
```bash
composer require saloonphp/laravel-plugin "^3.0"
```
Next, publish the configuration file with the following Artisan command
```bash
php artisan vendor:publish --tag=saloon-config
```
### Artisan Console Commands
The Laravel plugin provides a few useful Artisan commands that can help you build your API integrations.
<table><thead><tr><th width="364">Command</th><th>Description</th></tr></thead><tbody><tr><td>php artisan saloon:connector</td><td>Creates a new connector - You can provide an optional <code>--oauth</code> option if you would like to create an OAuth2 connector.</td></tr><tr><td>php artisan saloon:request</td><td>Creates a new request</td></tr><tr><td>php artisan saloon:response</td><td>Creates a custom response</td></tr><tr><td>php artisan saloon:plugin</td><td>Creates a plugin</td></tr><tr><td>php artisan saloon:auth</td><td>Creates a custom authenticator</td></tr><tr><td>php artisan saloon:list</td><td>List all your API integrations</td></tr></tbody></table>
### Saloon Facade
The Laravel plugin also provides a facade which can be used to feel more unified with the rest of Laravel. You can use the Facade in tests instead of the `MockClient::global()` method.
```php
use Saloon\Laravel\Facades\Saloon;
Saloon::fake([
GetServersRequest::class => MockResponse::make(body: '', status: 200),
]);
```
To learn more about testing API integrations, [click here](../the-basics/testing/).
### Laravel Nightwatch Support
From v3.6.0 the Saloon Laravel plugin natively supports Laravel Nightwatch. The plugin will automatically register a Nightwatch middleware on all of your connectors.
### Laravel Telescope Support
From v3.8.0 the Saloon Laravel plugin natively supports Laravel Telescope. The plugin will automatically register a Telescope middleware on all of your connectors.
### Events
With the Laravel plugin installed, Saloon will start sending events when requests are being sent. These events are:
* SendingSaloonRequest
* SentSaloonRequest
These events can be added to your `EventServiceProvider` and you can create listeners to handle when these happen.
### Laravel Zero
If you are using [Laravel Zero](https://github.com/laravel-zero/laravel-zero), the `SaloonServiceProvider` that registers the `Saloon` facade as well as some default middleware might not be registered. You can register Saloon's service provider in your `AppServiceProvider.php`'s `register()` method definition.
```php
<?php
use Saloon\Laravel\SaloonServiceProvider;
public function register()
{
$this->app->register(SaloonServiceProvider::class);
}
```
]]>
</laravel_integration_md>
<lawman_md>
<![CDATA[
# 🤠 Lawman
Lawman is a [PestPHP](https://pestphp.com/) Plugin for SaloonPHP which allows you to easily write architecture tests for your API integrations with a focus on them being easy to write and easy to read. After all, if SaloonPHP makes our API integrations beautiful, the tests for them should be beautiful too, right?
Lawman is a third-party plugin written and maintained by [Jon Purvis](https://twitter.com/JonPurvis\_) and gives you access to Expectations for your Connectors, Requests, Plugins and more, to ensure they stick to whichever architecture rules your application has.
One of the great things about Lawman is because it's a Pest plugin, it will work with regular PestPHP Expectations, just chain them up and you're good to go!
### Installation
You can install the plugin using Composer. If you run the following command, it will install it as a dev dependency:
```bash
composer require jonpurvis/lawman --dev
```
Lawman makes use of autoloading with Composer, so once you've installed the package, the Expectations should appear as you type!
### Examples
Let's take a look at how you can use Lawman to easily write Arch tests for your API Integrations.
Let's say we have a Connector class that we want to test, with PestPHP we could do the following:
```php
test('connector')
->expect('App\Http\Integrations\Integration\Connector')
->toExtend('Saloon\Http\Connector')
->toUse('Saloon\Traits\Plugins\AcceptsJson')
->toUse('Saloon\Traits\Plugins\AlwaysThrowOnErrors');
```
So that test is ensuring our class extends the base Connector and uses the `AcceptJson` and `AlwaysThrowOnErrors` traits. Whilst that test works, we could perhaps make it quicker to write and easier to read, so with Lawman, you can do:
```php
test('connector')
->expect('App\Http\Integrations\Integration\Connector')
->toBeSaloonConnector()
->toUseAcceptsJsonTrait()
->toUseAlwaysThrowOnErrorsTrait();
```
Next up, let's take a Request test that we have:
```php
test('request')
->expect('App\Http\Integrations\Integration\Requests\Request')
->toExtend('\Saloon\Http\Request')
->toImplement('Saloon\Contracts\Body\HasBody')
->toUse('Saloon\Traits\Body\HasFormBody')
->toUse('Saloon\Traits\Plugins\AcceptsJson');
```
Lawman makes this test much nicer to read:
```php
test('request')
->expect('App\Http\Integrations\Integration\Requests\Request')
->toBeSaloonRequest()
->toSendPostRequest()
->toHaveFormBody()
->toUseAcceptsJsonTrait();
```
What about if we want to test our Connector has an Authentication method? Lawman makes this easy to do, it even works with multi auth:
```php
test('connector')
->expect('App\Http\Integrations\Integration\Connector')
->toBeSaloonConnector()
->toUseCertificateAuthentication()
->toUseTokenAuthentication();
```
Lawman also has Expectations for the Pagination, Cache and Rate Limit Plugins:
```php
test('request')
->expect('App\Http\Integrations\Integration\Requests\Request')
->toBeSaloonRequest()
->toSendPostRequest()
->toUsePagedPagination()
->toHaveCaching()
->toHaveRateLimits()
```
Maybe our Connector has some Retry instructions that we want to test. Again, with Lawman, it's as simple as:
```php
test('connector')
->expect('App\Http\Integrations\Integration\Connector')
->toBeSaloonConnector()
->toBeTriedAgainOnFailure()
->toHaveRetryInterval()
->toUseExponentialBackoff()
```
### Available Expectations
#### Authentication
* `toUseTokenAuthentication`
* `toUseBasicAuthentication`
* `toUseCertificateAuthentication`
* `toUseHeaderAuthentication`
#### Cache
* `toHaveCaching`
#### Connector
* `toBeSaloonConnector`
#### Pagination
* `toUsePagedPagination`
* `toUseOffsetPagination`
* `toUseCursorPagination`
* `toUseCustomPagination`
#### Properties
* `toSetConnectTimeout`
* `toSetRequestTimeout`
* `toBeTriedAgainOnFailure`
* `toHaveRetryInterval`
* `toUseExponentialBackoff`
* `toThrowOnMaxTries`
#### Rate Limit
* `toHaveRateLimits`
#### Request
* `toBeSaloonRequest`
* `toHaveRequestMethod`
* `toSendGetRequest`
* `toSendPostRequest`
* `toSendHeadRequest`
* `toSendPutRequest`
* `toSendPatchRequest`
* `toSendDeleteRequest`
* `toSendOptionsRequest`
* `toSendConnectRequest`
* `toSendTraceRequest`
* `toHaveJsonBody`
* `toHaveMultipartBody`
* `toHaveXmlBody`
* `toHaveFormBody`
* `toHaveStringBody`
* `toHaveStreamBody`
#### Response
* `toBeSaloonResponse`
#### Trait
* `toUseAcceptsJsonTrait`
* `toUseAlwaysThrowOnErrorsTrait`
* `toUseTimeoutTrait`
* `toUseAuthorisationCodeGrantTrait`
* `toUseClientCredentialsGrantTrait`
### Contributing
Contributions to Lawman are welcome! If you can think of an Expectation that would be useful for others to utilise in their tests, please submit a PR on GitHub. The only real rules are:
* All Expectations must have a test to cover it
* A new Fixture class should be added for every new Expectation
If you just follow how previous Expectations are done, then you should be good.
### Reporting an Issue
If you do spot an issue with Lawman, please [open an issue](https://github.com/JonPurvis/lawman/issues) on GitHub.
]]>
</lawman_md>
<sdk_generator_md>
<![CDATA[
# 🏭 SDK Generator
The Saloon SDK Generator is a third-party Saloon plugin written and maintained by [Crescat](https://crescat.io/) ([Helge Sverre](https://twitter.com/HelgeSverre)) and allows people to easily generate Saloon SDKs from an OpenAPI file or Postman collection. The generator can automatically create connectors, requests and responses for you to help save you time when starting a new SDK.&#x20;
{% hint style="info" %}
Note: This tool helps you set up the foundation for your SDK, but it might not create a complete, ready-to-use solution.
{% endhint %}
<figure><img src="../.gitbook/assets/header (1).png" alt=""><figcaption></figcaption></figure>
### Installation
You can install the Saloon SDK Generator through Composer. You can install it globally on your machine and have a simple CLI to generate SDKs.
```sh
composer global require crescat-io/saloon-sdk-generator
```
{% hint style="info" %}
The library requires PHP 8.1 and above
{% endhint %}
### Basic Usage
To generate the PHP SDK from an API specification file, run the following command:
```sh
sdkgenerator generate:sdk API_SPEC_FILE.{json|yaml|yml}
--type={postman|openapi}
[--name=SDK_NAME]
[--output=OUTPUT_PATH]
[--namespace=Company\\Integration]
[--force]
[--dry]
[--zip]
```
Replace the placeholders with the appropriate values:
* `API_SPEC_FILE`: Path to the API specification file (JSON or YAML format).
* `--type`: Specify the type of API specification (`postman` or `openapi`).
* `--name`: (Optional) Specify the name of the generated SDK (default: Unnamed).
* `--namespace`: (Optional) Specify the root namespace for the SDK (default: `App\\Sdk`).
* `--output`: (Optional) Specify the output path where the generated code will be created (default: ./Generated).
* `--force`: (Optional) Force overwriting existing files.
* `--dry`: (Optional) Perform a dry run. It will not save generated files, only show a list of them.
* `--zip`: (Optional) Use this flag to generate a zip archive containing all the generated files.
### Full Documentation
The full documentation can be found by visiting the Github repository here:
{% embed url="https://github.com/crescat-io/saloon-sdk-generator" %}
### Issue Tracking
As this is not directly affiliated with Saloon, please report any issues you have directly on the repository linked above.
]]>
</sdk_generator_md>
<traits_md>
<![CDATA[
# 🏗 Building Your Own Plugins
Saloon has plugins which make it easy for you to add logic to your connector or requests in a reusable and elegant way. Plugins are traits that can be added to either a request or a connector and have a special "boot" method which is invoked during the request lifecycle.
### Getting Started
It's easy to create your own plugin for Saloon, just create a trait and add it to either your connector or request. In this example, we will create a logging plugin that when added will log the request being sent to [Ray](https://myray.app/).
Our plugin will be called `HasLogging` . In order for Saloon to execute this plugin, we must create a public method that starts with `boot` followed by the name of the trait. For example: `bootHasLogging`. The method is given an instance of `PendingRequest`.
{% tabs %}
{% tab title="Definition (Trait)" %}
<pre class="language-php"><code class="lang-php">&#x3C;?php
trait HasLogging
{
<strong> public function bootHasLogging(PendingRequest $pendingRequest): void
</strong> {
ray($pendingRequest);
}
}
</code></pre>
{% hint style="warning" %}
You must not use `$this` inside of the trait, as mutating the original request or connector is discouraged. The PendingRequest is the source of truth while a request is being sent.
{% endhint %}
{% endtab %}
{% tab title="Usage (Connector)" %}
<pre class="language-php"><code class="lang-php">&#x3C;?php
class ForgeConnector extends Connector
{
<strong> use HasLogging;
</strong>}
</code></pre>
{% endtab %}
{% endtabs %}
When you send your request, plugins are the first things that are invoked, even before the `boot` method. This is to allow maximum compatibility with [middleware ](../digging-deeper/middleware.md)and [authenticators](../the-basics/authentication.md). This also means that because it's the first process in the chain, other steps like middleware and the boot method will be able to overwrite anything added by a plugin.
<figure><img src="../.gitbook/assets/Saloon v2 (1).png" alt=""><figcaption></figcaption></figure>
Because we added the trait to our connector, every request will use the `HasLogging` plugin. If you would like the plugin to be applied to just one request, you can add the plugin to the request only.&#x20;
{% hint style="warning" %}
Take caution when adding the plugin to both the connector and the request, because the plugin will be executed twice.
{% endhint %}
]]>
</traits_md>
<xml_wrangler_md>
<![CDATA[
# 🏇 XML Wrangler
XML Wrangler is a first-party library written by the maintainer of Saloon and is designed to modernise reading and writing XML in PHP. XML Wrangler can detect errors in XML, read gigabytes of XML files and convert XML results into Laravel Collections. Its powerful reader supports dot notation and XPath queries.
### Key Features
* Convert XML into an array
* Write XML from an array
* Memory-safe reader for large XML documents
* Dot notation and XPath support for reading
{% tabs %}
{% tab title="Reader" %}
```php
use Saloon\XmlWrangler\XmlReader;
$reader = XmlReader::fromString($xml);
// Convert XML to an array
$reader->values(); // ['breakfast_menu' => [['name' => '...'], ['name' => '...'], ['name' => '...']]
// Query the XML
$reader->value('food.0')->sole(); // ['name' => 'Belgian Waffles', 'price' => '$5.95', ...]
$reader->xpathValue('//food[@bestSeller="true"]/name')->get(); // ['Belgian Waffles', 'Berry-Berry Belgian Waffles']
$reader->element('food.0')->sole()->getAttributes(); // ['soldOut' => false, 'bestSeller' => true]
```
{% endtab %}
{% tab title="XML" %}
```xml
<breakfast_menu>
<food soldOut="false" bestSeller="true">
<name>Belgian Waffles</name>
<price>$5.95</price>
<description>Two of our famous Belgian Waffles with plenty of real maple syrup</description>
<calories>650</calories>
</food>
<food soldOut="false" bestSeller="false">
<name>Strawberry Belgian Waffles</name>
<price>$7.95</price>
<description>Light Belgian waffles covered with strawberries and whipped cream</description>
<calories>900</calories>
</food>
<food soldOut="false" bestSeller="true">
<name>Berry-Berry Belgian Waffles</name>
<price>$8.95</price>
<description>Light Belgian waffles covered with an assortment of fresh berries and whipped cream</description>
<calories>900</calories>
</food>
</breakfast_menu>
```
{% endtab %}
{% endtabs %}
### Documentation
Click the card below to read the documentation
{% embed url="https://github.com/saloonphp/xml-wrangler" %}
### Saloon Integration
XML Wrangler is also integrated with Saloon! Once installed, you'll be able to use the `xmlReader()` method on your responses to instantiate an XML Wrangler reader.
```php
$response = $connector->send($request);
$reader = $response->xmlReader();
// $reader->values();
// etc...
```
#### Request Body
You may also choose to use XML Wrangler to create the request body when sending XML requests in Saloon.
```php
<?php
use Saloon\Http\Request;
use Saloon\XmlWrangler\XmlWriter;
use Saloon\Contracts\Body\HasBody;
use Saloon\Traits\Body\HasXmlBody;
use Saloon\XmlWrangler\Data\Element;
class CreateServerRequest extends Request implements HasBody
{
use HasXmlBody;
protected Method $method = Method::POST;
public function __construct(
protected readonly string $ubuntuVersion,
protected readonly string $type,
protected readonly string $provider
){}
protected function defaultBody(): string
{
return XmlWriter::make()->write('root', [
'ubuntu-version' => $this->ubuntuVersion,
'type' => $this->type,
'provider' => $this->provider,
]);
// Same as...
// <?xml version="1.0" encoding="utf-8"?>
// <root>
// <ubuntu-version>ubuntuVersion</ubuntu-version>
// <type>type</type>
// <provider>provider</provider>
// </root>
}
}
```
]]>
</xml_wrangler_md>
</installable_plugins>
<README_md>
<![CDATA[
---
description: Build beautiful API integrations and SDKs with Saloon
---
# 🤠 Saloon
Saloon is a PHP library that helps you rapidly build third party API integrations or SDKs. It's configured to be production-ready by default, offers a simple, standardised development flow for your whole team and has utilities to help test your API integrations before you ship to production. Saloon is loved by thousands of developers and downloaded over 2 million times.
[![Build Status](https://github.com/saloonphp/saloon/actions/workflows/tests.yml/badge.svg)](https://img.shields.io/github/actions/workflow/status/saloonphp/saloon/tests.yml?label=tests)![](https://img.shields.io/packagist/dm/saloonphp/saloon)![](https://img.shields.io/github/stars/saloonphp/saloon)
<figure><img src=".gitbook/assets/Introducing Saloon v3.png" alt=""><figcaption><p>Introducing Saloon v3</p></figcaption></figure>
### Key Features
* Lightweight, only depends on one library - Guzzle.
* Offers "Connector" and "Request" classes to encapsulate API integrations into files
* Allows you to record real requests in your tests so you have accurate testing data for your assertions.
* Works in any PHP framework, or just regular PHP applications (with Composer)
* Comes with powerful plugins to handle caching, rate limits, data transfer objects, XML, and more
{% content-ref url="the-basics/installation.md" %}
[installation.md](the-basics/installation.md)
{% endcontent-ref %}
]]>
</README_md>
<SUMMARY_md>
<![CDATA[
# Table of contents
* [🤠 Saloon](README.md)
## Upgrade
* [🪄 What's new in v3](upgrade/whats-new-in-v3.md)
* [🌳 Upgrading from v2](upgrade/upgrading-from-v2.md)
## The Basics
* [⬇️ Installation](the-basics/installation.md)
* [🔌 Connectors](the-basics/connectors.md)
* [✉️ Requests](the-basics/requests.md)
* [🔐 Authentication](the-basics/authentication.md)
* [🎁 Sending Body/Data](the-basics/request-body-data/README.md)
* [JSON Body](the-basics/request-body-data/json-body.md)
* [Multipart Form Body](the-basics/request-body-data/multipart-form-body.md)
* [XML Body](the-basics/request-body-data/xml-body.md)
* [URL Encoded Body (Form)](the-basics/request-body-data/form-body-url-encoded.md)
* [String / Plain Text Body](the-basics/request-body-data/string-plain-text-body.md)
* [Stream Body](the-basics/request-body-data/stream-body.md)
* [🚀 Sending Requests](the-basics/sending-requests.md)
* [📡 Responses](the-basics/responses.md)
* [🔥 Handling Failures](the-basics/handling-failures.md)
* [🔎 Debugging](the-basics/debugging.md)
* [✅ Testing](the-basics/testing/README.md)
## Digging Deeper
* [🛸 Data Transfer Objects](the-basics/data-transfer-objects.md)
* [✨ Building SDKs](the-basics/building-sdks.md)
* [☝️ Solo Requests](digging-deeper/solo-requests.md)
* [🎯 Retrying Requests](digging-deeper/retrying-requests.md)
* [⏸️ Delaying Requests](digging-deeper/delay.md)
* [🏎️ Concurrency & Pools](digging-deeper/concurrency-and-pools.md)
* [🔑 OAuth2](digging-deeper/oauth2-authentication/README.md)
* [Authorization Code Grant](digging-deeper/oauth2-authentication/oauth2-authentication.md)
* [Client Credentials Grant](digging-deeper/oauth2-authentication/client-credentials-grant.md)
* [💫 Middleware](digging-deeper/middleware.md)
* [⛩️ PSR Support](digging-deeper/psr-support.md)
## Installable Plugins
* [📚 Pagination](digging-deeper/pagination/README.md)
* [Paged Pagination](digging-deeper/pagination/paged-pagination.md)
* [Limit/Offset Pagination](digging-deeper/pagination/limit-offset-pagination.md)
* [Cursor Pagination](digging-deeper/pagination/cursor-pagination.md)
* [Custom Pagination](digging-deeper/pagination/custom-pagination.md)
* [⛵ Laravel Plugin](installable-plugins/laravel-integration.md)
* [🔁 Caching](installable-plugins/caching-responses.md)
* [⛔ Rate Limit Handler](installable-plugins/handling-rate-limits.md)
* [🏭 Auto SDK Generator](installable-plugins/sdk-generator.md)
* [🤠 Lawman](installable-plugins/lawman.md)
* [🏇 XML Wrangler](installable-plugins/xml-wrangler.md)
* [🏗️ Building Your Own Plugins](installable-plugins/traits.md)
## Conclusion
* [📘 Official Book!](conclusion/official-book.md)
* [📔 How-to Guides](conclusion/how-to-guides/README.md)
* [Improving Speed With Laravel](conclusion/how-to-guides/improving-speed-with-laravel.md)
* [Per-request Authentication](conclusion/how-to-guides/per-request-authentication.md)
* [📖 Tutorials & Blog Posts](conclusion/tutorials.md)
* [🎖️ Showcase](conclusion/showcase.md)
* [💥 Known Issues](conclusion/known-issues.md)
* [🤗 Credits](conclusion/credits.md)
* [❤️ Support Saloon](https://github.com/sponsors/Sammyjo20)
]]>
</SUMMARY_md>
<the_basics>
<authentication_md>
<![CDATA[
# 🔐 Authentication
Saloon has optional authenticator classes to help you with the most common types of authentication. These are classes that can be used on your connector or request.
[You can view all of the built-in authenticators on GitHub.](https://github.com/saloonphp/saloon/tree/v3/src/Http/Auth)
### Authorization "Bearer" Tokens
The `TokenAuthenticator` class can be used to add a `Authorization: Bearer` header to the request. Just extend the `defaultAuth` method and return the authenticator class.
```php
<?php
use Saloon\Http\Auth\TokenAuthenticator;
class ForgeConnector extends Connector
{
public function __construct(public readonly string $token) {}
protected function defaultAuth(): TokenAuthenticator
{
return new TokenAuthenticator($this->token);
}
}
```
### Basic Auth (Base64 Encoded)
The `BasicAuthenticator` class can be used to add a `Authorization: Basic` header to the request. Just extend the `defaultAuth` method and return the authenticator class.
```php
<?php
use Saloon\Http\Auth\BasicAuthenticator;
class ForgeConnector extends Connector
{
public function __construct(
public readonly string $username,
public readonly string $password
){}
protected function defaultAuth(): BasicAuthenticator
{
return new BasicAuthenticator($this->username, $this->password);
}
}
```
### Query Parameter
The `QueryAuthenticator` class can be used to add a query parameter to requests. Just extend the `defaultAuth` method and return the authenticator class.
```php
<?php
use Saloon\Http\Auth\QueryAuthenticator;
class ForgeConnector extends Connector
{
public function __construct(public readonly string $token) {}
protected function defaultAuth(): QueryAuthenticator
{
return new QueryAuthenticator('api-key', $this->token);
}
}
```
### Certificate Auth
The `CertificateAuthenticator` class can be used to authenticate with a custom client-side certificate. An optional password can be provided. Just extend the `defaultAuth` method and return the authenticator class.
```php
<?php
use Saloon\Http\Auth\CertificateAuthenticator;
class ForgeConnector extends Connector
{
public function __construct(
public readonly string $path,
public readonly string $password
){}
protected function defaultAuth(): CertificateAuthenticator
{
return new CertificateAuthenticator($this->path, $this->password);
}
}
```
### Header Auth
The `HeaderAuthenticator` class can be used to authenticate with a custom header. Just extend the `defaultAuth` method and return the authenticator class.
```php
<?php
use Saloon\Http\Auth\HeaderAuthenticator;
class ForgeConnector extends Connector
{
public function __construct(public readonly string $token) {}
protected function defaultAuth(): HeaderAuthenticator
{
return new HeaderAuthenticator($this->token, 'X-API-KEY');
}
}
```
### Multiple Authenticators
You may need to use multiple authenticator classes together, like a certificate and an authorization token. You can use the `MultiAuthenticator` class for this scenario.
```php
<?php
use Saloon\Http\Auth\MultiAuthenticator;
use Saloon\Http\Auth\TokenAuthenticator;
use Saloon\Http\Auth\CertificateAuthenticator;
class ForgeConnector extends Connector
{
public function __construct(
public readonly string $certificate,
public readonly string $token
){}
protected function defaultAuth(): MultiAuthenticator
{
return new MultiAuthenticator(
new CertificateAuthenticator($this->certificate),
new TokenAuthenticator($this->token),
);
}
}
```
### Custom Authentication
If your API integration requires a more complicated authentication process, you can create your own authenticator classes which can be used on your connector. This helps abstract any complicated logic away from the connector keeping it tidy.
```php
<?php
use Saloon\Http\PendingRequest;
use Saloon\Contracts\Authenticator;
class ForgeAuthenticator implements Authenticator
{
public function __construct(public readonly string $token) {}
public function set(PendingRequest $pendingRequest): void
{
// $pendingRequest->headers()->add(...);
// $pendingRequest->config()->add(...);
}
}
```
```php
<?php
class ForgeConnector extends Connector
{
protected function defaultAuth(): ForgeAuthenticator
{
return new ForgeAuthenticator($this->token);
}
}
```
### The authenticate method
You may also use the `authenticate` method on your connector or request if you would like to use or overwrite an authenticator at runtime.
```php
$forge = new ForgeConnector;
$forge->authenticate(new TokenAuthenticator($user->forge_token));
// $forge->send(...)
```
{% hint style="info" %}
Only one authenticator can be used at the same time.
{% endhint %}
]]>
</authentication_md>
<building_sdks_md>
<![CDATA[
# ✨ Building SDKs
Saloon provides everything you need to build a great SDK or library for an API. It already offers the ability to mock responses, provide authentication, implement OAuth2 boilerplate and even record your API requests in your tests. With Saloon you won't need to write the same boilerplate code over and over again. Saloon comes with just three dependencies making your library or SDK lightweight.
{% hint style="info" %}
You can see some Saloon SDKs built by the PHP community by [visiting the showcase page!](../conclusion/showcase.md)
{% endhint %}
### Getting Started
This guide will be using PokéAPI as an example API. [Click here to see a full SDK example repository](https://github.com/Sammyjo20/pokeapi-sdk/tree/v3). This guide assumes you know the basics of connectors, requests and sending requests.
To start building an SDK with Saloon, we recommend that you create a connector as your SDK class. This class allows you to configure the base URL and any defaults you may need like default headers or authentication.
#### Example SDK Connector
This is an example SDK for the [PokéAPI](https://pokeapi.co/). As you can see, I have defined the API base URL, as well as used the constructor to provide an authentication token. I have also defined some default headers.
```php
<?php
namespace Pokeapi;
use Saloon\Http\Request;
use Saloon\Http\Connector;
use Saloon\Http\Auth\TokenAuthenticator;
class Pokeapi extends Connector
{
public function __construct(protected readonly string $token) {}
public function resolveBaseUrl(): string
{
return 'https://pokeapi.co/api/v2';
}
protected function defaultHeaders(): array
{
return [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
];
}
protected function defaultAuth(): TokenAuthenticator
{
return new TokenAuthenticator($this->token);
}
}
```
Now that we have created the SDK class, all we need to do is instantiate it and provide the API token. We are now ready to send requests through our SDK. You may add your methods to this class as it is the root class of your SDK.
```php
<?php
use Pokeapi\Pokeapi;
$pokeapi = new Pokeapi('my-api-token');
// Ready to make requests!
```
### Sending Requests
One of the ways that you can build SDKs in Saloon is by creating request classes and then calling them from the SDK connector. This is the simplest way and you will get up and running really quickly with Saloon. Make sure you are familiar with how requests work first.
#### Getting started
Firstly, you will need to [create a request](requests.md), this is the same as making a normal request described in the documentation.
#### Use your request
When you have created the request, all that developers would need to do is instantiate and send the request on the connector. This means you only need to have two classes as a minimum for a fully working SDK!
```php
<?php
use Pokeapi\Pokeapi;
$pokeapi = new Pokeapi('my-api-token');
$request = new GetAllPokemon(page: 1);
// Developers would just send the request.
$response = $pokeapi->send($request);
```
With this method, it’s really simple to build your SDK. All you would need to do is create all the requests and then document them in your README. Developers using your SDK can just instantiate your SDK and then use the `send` methods. This also gives the developer freedom to customise the request's headers, config and query parameters if they choose to.
### Sending Requests With Methods
Sometimes you may want to make it easy for the developer to find all the methods that they need to call the API through your SDK. You can create methods on your connector which send an API request or you could write a "resource" class that contains lots of requests
{% tabs %}
{% tab title="Definition" %}
```php
class Pokeapi extends Connector
{
// ...
public function allPokemon(int $page): Response
{
return $this->send(new GetAllPokemon($page));
}
}
```
{% endtab %}
{% tab title="Usage" %}
```php
<?php
$pokeapi = new Pokeapi('my-api-token');
$response = $pokeapi->allPokemon(page: 1);
```
{% endtab %}
{% endtabs %}
### Request Resources/Groups
The resource pattern can help you combine your SDK requests into simple groups that are easy for the developer to find and consume. First, we'll create a class that extends Saloon's `BaseResource` class.
For this API, I will create a Pokémon resource which will group all the Pokémon requests together. Imagine a resource like a controller in an MVC framework like Laravel. You should pass any arguments the request needs through the method arguments.
<pre class="language-php"><code class="lang-php">&#x3C;?php
use Saloon\Http\BaseResource;
use Saloon\Http\Response;
<strong>class PokemonResource extends BaseResource
</strong>{
public function all(int $page): Response
{
return $this->connector->send(new GetAllPokemon($page));
}
public function get(int $id): Response
{
return $this->connector->send(new GetSinglePokemon($id));
}
}
</code></pre>
#### Defining a resource on your connector
Now we'll define a method on the connector which returns this resource class. Don't forget to pass the connector's instance (`$this`) into the resource.
```php
class Pokeapi extends Connector
{
// ...
public function pokemon(): PokemonResource
{
return new PokemonResource($this);
}
}
```
Now all our users have to do is access the `pokemon()` method on the SDK class to get access to all the various requests that our SDK has to offer.
```php
<?php
$pokeapi = new Pokeapi('my-api-token');
$allPokemon = $pokeapi->pokemon()->all(page: 1);
$giratina = $pokeapi->pokemon()->get(id: 'giratina');
```
### Custom Responses
You may wish to customise the `Response` class that is returned by Saloon from your SDK connector. [Click here to read more about custom responses.](responses.md#custom-responses)
### Additional Features
Please read through the other sections of Saloon's documentation to see the other features that you can offer for your SDK. Every other feature like testing, authentication, failure handling is all supported for SDKs.
### Testing
When building an SDK, it's important to write tests that ensure the SDK calls the correct requests from an API and returns the right response, especially if you're converting the response into a data-transfer-object. [Read through the testing section of the documentation](building-sdks.md#testing) to get familiar with mocking and recording requests.
]]>
</building_sdks_md>
<connectors_md>
<![CDATA[
# 🔌 Connectors
Connectors are classes which define an API integration's properties like its URL and headers. Any behaviour that should be used on every request like authentication should be defined in a connector. You should have a separate connector for each API integration.
### Getting Started
You should establish a standard place to keep your API connectors. For example in Laravel, a sensible place would be to place them inside the `App/Http/Integrations`folder.&#x20;
Create a new class and extend the abstract `Connector` class. You will then need to define a method `resolveBaseUrl`. This is the URL that points to the API.
```php
<?php
use Saloon\Http\Connector;
class ForgeConnector extends Connector
{
public function resolveBaseUrl(): string
{
return 'https://forge.laravel.com/api/v1';
}
}
```
> If you have installed the Laravel plugin, you can use the **php artisan saloon:connector** command to create a connector.
### Headers
Most API integrations will have common headers used by all of its requests. You can extend the `defaultHeaders` method to define the headers.
<pre class="language-php"><code class="lang-php">class ForgeConnector extends Connector
{
// ...
<strong> protected function defaultHeaders(): array
</strong> {
return [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
];
}
}
</code></pre>
You can also use the `headers` method on a connector instance.
```php
$forge = new ForgeConnector;
$forge->headers()->add('Content-Type', 'text/plain');
```
### Timeout
By default, Saloon will have a connection timeout of 10 seconds and a request timeout of 30 seconds. You can customise this by using the `HasTimeout` trait and specifying a `connectTimeout` and `requestTimeout` property.
```php
use Saloon\Traits\Plugins\HasTimeout;
class ForgeConnector extends Connector
{
use HasTimeout;
protected int $connectTimeout = 60;
protected int $requestTimeout = 120;
}
```
### Constructor Arguments
Since connectors are just classes, you can define a constructor to populate its default properties. For example, if the URL changes per user of your application you can define this as a constructor argument.
```php
class ForgeConnector extends Connector
{
public function __construct(protected readonly string $baseUrl) {
//
}
public function resolveBaseUrl(): string
{
return $this->baseUrl;
}
}
```
```php
$connector = new ForgeConnector('https://forge.laravel.com/api/v1');
```
### HTTP Client Config
The connector uses a HTTP client to send the request. By default, this client is [Guzzle](https://github.com/guzzle/guzzle). If you would like to define Guzzle config options then you can extend the `defaultConfig` method.
[Click here to see a list of the available options Guzzle provide.](https://docs.guzzlephp.org/en/stable/request-options.html)
```php
class ForgeConnector extends Connector
{
// ...
public function defaultConfig(): array
{
return [
'stream' => true,
];
}
}
```
You can also use the `config` method on a connector instance.
```php
$forge = new ForgeConnector;
$forge->config()->add('stream', true);
```
]]>
</connectors_md>
<data_transfer_objects_md>
<![CDATA[
# 🛸 Data Transfer Objects
When building API integrations, sometimes dealing with a raw response or a JSON response can be tedious and unpredictable. Data transfer objects are a good solution as they allow you to define a structure for a request and response. Saloon supports casting a response from an API request into a DTO.
### Defining DTOs from responses
In your request, extend the `createDtoFromResponse` method. Within this method, you get access to the `Response` object containing the response to cast into a data transfer object.
```php
<?php
use Saloon\Http\Request;
use Saloon\Http\Response;
class GetServerRequest extends Request
{
// {...}
public function createDtoFromResponse(Response $response): mixed
{
$data = $response->json();
return new Server(
id: $data['id'],
name: $data['name'],
ipAddress: $data['ip'],
);
}
}
```
This is what the `Server` DTO looks like:
```php
<?php
class Server
{
public function __construct(
public readonly int $id,
public readonly string $name,
public readonly string $ipAddress,
){}
}
```
### Retrieving your DTO from responses
Now we have defined our DTO on our request or connector, we can use the build in `dto` or `dtoOrFail` methods on our response class.
```php
<?php
$connector = new ForgeConnector;
$response = $connector->send(new GetServerRequest(id: 12345));
// Create a DTO even if the response was a failure
$server = $response->dto();
// Create a DTO or throw an exception if the response is not successful
$server = $response->dtoOrFail();
```
{% hint style="info" %}
Saloon will attempt to create a DTO from your response no matter the status of the response. This allows you to create "error" data transfer objects. If you don't want to use this functionality, you can use the `dtoOrFail` method which will throw a LogicException if the response is a failure. **You can customise what is considered a failed response** [**here**](handling-failures.md#customising-when-saloon-thinks-a-request-has-failed)**.**
{% endhint %}
### PHPStan / TypeHinting DTOs
Due to the way DTOs are created in Saloon, you can lose the type of DTO. This can reduce the developer experience. There are two ways to get around this issue.
You can use an inline-doc block above the `dto`/`dtoOrFail` method call.
```php
/** @var Server $server */
$server = $response->dto();
```
Or you can use the `createDtoFromResponse` method directly on your connector or request - which will use the return type specified.
```php
$server = $request->createDtoFromResponse($response);
```
### Using a DTO to populate request data
You may also allow your data transfer objects to go both ways, back into requests. You can do this easily with Saloon, just accept your DTO as an argument in the constructor of your request and then use the DTO to set default properties inside of the request.
{% tabs %}
{% tab title="Definition" %}
<pre class="language-php"><code class="lang-php">&#x3C;?php
use Saloon\Http\Request;
use Saloon\Enums\Method;
use Saloon\Http\Response;
use Saloon\Contracts\Body\HasBody;
use Saloon\Traits\Body\HasJsonBody;
class UpdateServerRequest extends Request implements HasBody
{
use HasJsonBody;
protected Method $method = Method::PUT;
public function __construct(readonly protected Server $server)
{
//
}
public function resolveEndpoint(): string
{
<strong> return '/servers/' . $this->server->id;
</strong> }
protected function defaultBody(): array
{
return [
<strong> 'name' => $this->server->name,
</strong> ];
}
public function createDtoFromResponse(Response $response): mixed
{
$data = $response->json();
return new Server(
id: $data['id'],
name: $data['name'],
ipAddress: $data['ip'],
);
}
}
</code></pre>
{% endtab %}
{% tab title="Usage" %}
```php
<?php
$connector = new ForgeConnector;
$response = $connector->send(new GetServerRequest(id: 12345));
$server = $response->dto();
// Modify the server
$server->name = 'YEE-HAW-2';
// Update the server request
$response = $connector->send(new UpdateServerRequest($server));
```
{% endtab %}
{% endtabs %}
### Accessing the response from your DTO
Sometimes debugging a DTO can be difficult, especially if you have passed the data object through your application and no longer have access to the original `Response` that you created the DTO from. Saloon can inject the response into your data transfer object for you if you use the `HasResponse` trait and the `WithResponse` interface. Let's add it to our existing `Server` DTO.
<pre class="language-php"><code class="lang-php">&#x3C;?php
use Saloon\Http\Response;
use Saloon\Traits\Responses\HasResponse;
use Saloon\Contracts\DataObjects\WithResponse;
<strong>class Server implements WithResponse
</strong>{
<strong> use HasResponse;
</strong>
public function __construct(
public readonly int $id,
public readonly string $name,
public readonly string $ipAddress,
){}
}
</code></pre>
Now whenever we retrieve an instance of our data transfer object, you will be able to access the underlying response that was created with it!
```php
<?php
$server = $response->dto();
$response = $server->getResponse();
```
]]>
</data_transfer_objects_md>
<debugging_md>
<![CDATA[
# 🔎 Debugging
While building API integrations, sometimes you will send a request and the API will return an error. This could be due to sending the request badly or incorrect data. Sometimes debugging the request can be difficult - so Saloon has some helpers to resolve this.
### Prerequisites
Saloon's default debugging driver uses [Symfony's Var Dumper](https://github.com/symfony/var-dumper) library. If you're not using Laravel and do not have it installed, you can install it below.
```
composer require symfony/var-dumper
```
{% hint style="info" %}
Laravel already includes this library.
{% endhint %}
### Debugging Request & Response
The simplest way to debug a request and response is to use the `debug()` method on your connector before sending a request. This will output an easy-to-understand array of the request and the response.
{% tabs %}
{% tab title="Connector" %}
```php
$connector = new ForgeConnector;
$connector->debug()->send($request);
```
{% endtab %}
{% tab title="Request" %}
```php
$connector = new ForgeConnector;
$connector->send($request->debug());
```
{% endtab %}
{% endtabs %}
You can provide the `die` argument if you would like to terminate the application after receiving the response.
```php
$connector->debug(die: true)->send($request);
```
This will provide an output in your terminal/browser like this:
```
Saloon Request (UserRequest) -> array:6 [
"connector" => "Saloon\Tests\Fixtures\Connectors\TestConnector"
"request" => "Saloon\Tests\Fixtures\Requests\UserRequest"
"method" => "GET"
"uri" => "https://tests.saloon.dev/api/user"
"headers" => array:2 [
"Host" => "tests.saloon.dev"
"Accept" => "application/json"
]
"body" => ""
]
Saloon Response (UserRequest) -> array:3 [
"status" => 200
"headers" => []
"body" => "{"name":"Sam"}"
]
```
{% hint style="warning" %}
This output will show the request just before it is sent to the sender. If the sender (like Guzzle) adds any additional headers or changes the request, these changes will not be displayed.
{% endhint %}
### Separate Debuggers
You may also use `debugRequest` and `debugResponse` independently if you would like to debug just the request or response respectively.
```php
<?php
$connector->debugRequest(); // $connector->debugRequest(die: true);
$connector->debugResponse(); // $connector->debugResponse(die: true);
```
### Custom Debugger Handlers
You may also provide a closure to the `debugRequest` and `debugResponse` methods if you would like to write your own debugging implementations.
```php
<?php
use Saloon\Http\Response;
use Saloon\Http\PendingRequest;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
// ...
$connector->debugRequest(function (PendingRequest $pendingRequest, RequestInterface $psrRequest) {
ray($psrRequest);
});
$connector->debugResponse(function (Response $response, ResponseInterface $psrResponse) {
ray($psrResponse);
});
```
]]>
</debugging_md>
<handling_failures_md>
<![CDATA[
# 🔥 Handling Failures
Saloon has a powerful exception handler with many descriptive exceptions you can use in your application.
When you send a request, Saloon won't do anything if the request fails, but by default, it will use the status code to determine if a request is successful or not. The only exception to this is if Saloon cannot connect to an API, which will throw a `FatalRequestException`.
```php
<?php
$forge = new ForgeConnector;
$response = $forge->send(new ErrorRequest);
$response->failed(); // true
$response->status(); // 500
$response->body(); // {"message": "Server Error"}
```
### Always throw exceptions on failed requests
You may wish to throw an exception whenever a request fails (4xx or 5xx response). You can add the `AlwaysThrowOnErrors` trait on your connector, and then every request that fails will throw an exception.
```php
<?php
use Saloon\Traits\Plugins\AlwaysThrowOnErrors;
class ForgeConnector extends Connector
{
use AlwaysThrowOnErrors;
// {...}
}
```
### Using the throw method
On a per-response basis, you may use the `throw` method after sending your response. This method will throw an exception if the response has a "failed" HTTP status code like 4xx or 5xx.
```php
<?php
$forge = new ForgeConnector;
$response = $forge->send(new ErrorRequest);
// throws InternalServerErrorException (extends ServerException)
$response->throw();
```
### Response Exceptions
Saloon's default exception handler contains the following exceptions based on the status code and severity of the exception. These are thrown depending on the method you use below.
```
SaloonException
├── FatalRequestException (Connection Errors)
└── RequestException (Request Errors)
├── ServerException (5xx)
│ ├── InternalServerErrorException (500)
│ ├── ServiceUnavailableException (503)
│ └── GatewayTimeoutException (504)
└── ClientException (4xx)
├── UnauthorizedException (401)
├── PaymentRequiredException (402)
├── ForbiddenException (403)
├── NotFoundException (404)
├── MethodNotAllowedException (405)
├── RequestTimeOutException (408)
├── UnprocessableEntityException (422)
└── TooManyRequestsException (429)
```
### Handling failures with promises
When sending requests using `sendAsync` or using request pooling, you will receive a `PromiseInterface` instance. Since this class catches exceptions, Saloon will automatically pass the request exception in the `otherwise` block, and you do not have to use the `throw` method.
```php
<?php
use Saloon\Http\Response;
$forge = new ForgeConnector('api-token');
$promise = $forge->sendAsync(new GetServersRequest);
$promise
->then(function (Response $response) {
// Handle successful response
})
->otherwise(function (RequestException $exception) {
// Handle failed request
});
```
### Customising when Saloon thinks a request has failed
You may integrate with an API which returns a 200 response status but with an error message in the response body. To handle this, you can extend the `hasRequestFailed` method on your connector or request.
{% tabs %}
{% tab title="Connector" %}
<pre class="language-php"><code class="lang-php">&#x3C;?php
use Saloon\Http\Connector;
use Saloon\Http\Response;
class ForgeConnector extends Connector
{
// {...}
<strong> public function hasRequestFailed(Response $response): ?bool
</strong> {
<strong> return str_contains($response->body(), 'Server Error');
</strong> }
}
</code></pre>
{% endtab %}
{% tab title="Request" %}
<pre class="language-php"><code class="lang-php">&#x3C;?php
use Saloon\Http\Request;
use Saloon\Http\Response;
class ErrorRequest extends Request
{
// {...}
<strong> public function hasRequestFailed(Response $response): ?bool
</strong> {
<strong> return str_contains($response->body(), 'Server Error');
</strong> }
}
</code></pre>
{% endtab %}
{% endtabs %}
### Customising the request exception
By default, Saloon will use the exceptions [listed above](handling-failures.md#response-exceptions), but you may choose to return your own exception if a request has failed. Just extend the `getRequestException` method on either your connector or request. You will receive an instance of the response and a sender exception, which may be nullable.
{% tabs %}
{% tab title="Connector" %}
```php
<?php
use Saloon\Http\Connector;
use Saloon\Http\Response;
use \Throwable;
class ForgeConnector extends Connector
{
// {...}
public function getRequestException(Response $response, ?Throwable $senderException): ?Throwable
{
return new CustomException('Oh yee-naw!', $response, $senderException);
}
}
```
{% endtab %}
{% tab title="Request" %}
```php
<?php
use Saloon\Http\Request;
use Saloon\Http\Response;
use \Throwable;
class ErrorRequest extends Request
{
// {...}
public function getRequestException(Response $response, ?Throwable $senderException): ?Throwable
{
return new CustomException('Oh yee-naw!', $response, $senderException);
}
}
```
{% endtab %}
{% endtabs %}
{% hint style="info" %}
Priority is given to the request when you extend the `getRequestException` method on both your connector and request.
{% endhint %}
]]>
</handling_failures_md>
<installation_md>
<![CDATA[
# ⬇️ Installation
To get started with Saloon, you will need to install it through [Composer](https://getcomposer.org/).&#x20;
```bash
composer require saloonphp/saloon "^3.0"
```
### Using Laravel?
You can install an additional Laravel Plugin for Saloon which provides Artisan console commands, a facade and useful testing helpers. To read more about the Laravel plugin [click here](../installable-plugins/laravel-integration.md).
]]>
</installation_md>
<request_body_data>
<form_body_url_encoded_md>
<![CDATA[
# Form Body (URL Encoded)
To get started, change your method to **POST, PUT or PATCH** depending on the requirements of the API. After that, you will need to add the `HasBody` interface to your request. Without this interface, Saloon will not send any request body to the HTTP client.
<pre class="language-php"><code class="lang-php">&#x3C;?php
use Saloon\Http\Request;
use Saloon\Contracts\Body\HasBody;
<strong>class CreateServerRequest extends Request implements HasBody
</strong>{
protected Method $method = Method::POST;
}
</code></pre>
Next, you will need to add the `HasFormBody` trait to your request. This trait will implement the `body()` method that the `HasBody` interface requires. It also provides a method `defaultBody()` which you can extend to provide a default body on your request.
<pre class="language-php"><code class="lang-php">&#x3C;?php
use Saloon\Http\Request;
use Saloon\Contracts\Body\HasBody;
<strong>use Saloon\Traits\Body\HasFormBody;
</strong>
class CreateServerRequest extends Request implements HasBody
{
<strong> use HasFormBody;
</strong>
protected Method $method = Method::POST;
}
</code></pre>
{% hint style="info" %}
Saloon will automatically send the `Content-Type: application/x-www-form-urlencoded` header for you when using the `HasFormBody` trait, however, if you would like to overwrite this behaviour then you can use the `defaultHeaders` method on the request or modify the headers before the request is sent.
{% endhint %}
### Default Body
There are a couple of ways to interact with the request body to prepare it to be sent. You can either use the methods mentioned below to add to the body on any given instance or you can use the `defaultBody` method on your request. This is recommended because you could then define any requirements as constructor arguments in your request and then standardise your request even more.&#x20;
```php
<?php
use Saloon\Http\Request;
use Saloon\Contracts\Body\HasBody;
use Saloon\Traits\Body\HasFormBody;
class CreateServerRequest extends Request implements HasBody
{
use HasFormBody;
protected Method $method = Method::POST;
public function __construct(
protected string $ubuntuVersion,
protected string $type,
protected string $provider
){}
protected function defaultBody(): array
{
return [
'ubuntu_version' => $this->ubuntuVersion,
'type' => $this->type,
'provider' => $this->provider,
];
}
}
```
Sometimes there can be too many items to define as constructor arguments. If this is the case for you, you can require the user to provide an array, or even a DTO to populate the default body of your request.
{% tabs %}
{% tab title="Using an array" %}
```php
<?php
use Saloon\Http\Request;
use Saloon\Contracts\Body\HasBody;
use Saloon\Traits\Body\HasFormBody;
class CreateServerRequest extends Request implements HasBody
{
use HasFormBody;
protected Method $method = Method::POST;
public function __construct(
protected array $payload
){}
protected function defaultBody(): array
{
return $this->payload;
}
}
```
{% endtab %}
{% tab title="Using a DTO (Data Transfer Object)" %}
<pre class="language-php"><code class="lang-php">&#x3C;?php
use Saloon\Http\Request;
use Saloon\Contracts\Body\HasBody;
use Saloon\Traits\Body\HasFormBody;
class CreateServerRequest extends Request implements HasBody
{
use HasFormBody;
protected Method $method = Method::POST;
public function __construct(
<strong> protected Server $server
</strong> ){}
protected function defaultBody(): array
{
return [
'ubuntu_version' => $this->server->ubuntuVersion,
'type' => $this->server->type,
'provider' => $this->server->provider,
];
}
}
</code></pre>
{% endtab %}
{% endtabs %}
### Interacting with the body() method
While you can define the default body on your request, it might be useful to add or modify the body at runtime on a per-request basis. Saloon has the following methods to allow you to modify the form request body:
* add(string $key, mixed $value) -> **Add items to the form body**
* remove(string $key) -> **Remove items from the form body**
* merge(…$values) -> **Merge another array of items into the form body**
* set(array $value) -> **Overwrite the form body entirely**
* all(): array -> **Get all the values of the form body**
* isEmpty(): bool -> **Check if the body is empty**
* isNotEmpty(): bool -> **Check if the body is not empty**
```php
<?php
$request = new CreateServerRequest;
$request->body()->add('ubuntu_version', '22.04');
$request->body()->merge([
'type' => 'app',
'provider' => 'ocean2',
]);
$body = $request->body()->all();
// array: [
// 'ubuntu_version' => '22.04',
// 'type' => 'app',
// 'provider' => 'ocean2',
// ]
```
### Connector Body
If you would like to also have form body on your connector, you can add the same interface and trait to your connector. If you have the trait on both the connector and the request, the properties will be merged. This is useful if you want to have a shared form body across every request, like an authentication token.
```php
<?php
use Saloon\Http\Connector;
use Saloon\Contracts\Body\HasBody;
use Saloon\Traits\Body\HasFormBody;
class ForgeConnector extends Connector implements HasBody
{
use HasFormBody;
protected function defaultBody(): array
{
return [
'name' => 'Sam',
];
}
}
```
]]>
</form_body_url_encoded_md>
<json_body_md>
<![CDATA[
# JSON Body
To get started, change your method to **POST, PUT or PATCH** depending on the requirements of the API. After that, you will need to add the `HasBody` interface to your request. Without this interface, Saloon will not send any request body to the HTTP client.
<pre class="language-php"><code class="lang-php">&#x3C;?php
use Saloon\Http\Request;
use Saloon\Contracts\Body\HasBody;
use Saloon\Enums\Method;
<strong>class CreateServerRequest extends Request implements HasBody
</strong>{
protected Method $method = Method::POST;
}
</code></pre>
Next, you will need to add the `HasJsonBody` trait to your request. This trait will implement the `body()` method that the `HasBody` interface requires. It also provides a method `defaultBody()` which you can extend to provide a default body on your request.
<pre class="language-php"><code class="lang-php">&#x3C;?php
use Saloon\Http\Request;
use Saloon\Contracts\Body\HasBody;
<strong>use Saloon\Traits\Body\HasJsonBody;
</strong>
class CreateServerRequest extends Request implements HasBody
{
<strong> use HasJsonBody;
</strong>
protected Method $method = Method::POST;
}
</code></pre>
{% hint style="info" %}
Saloon will automatically send the `Content-Type: application/json` header for you when using the `HasJsonBody` trait, however, if you would like to overwrite this behaviour then you can use the `defaultHeaders` method on the request or modify the headers before the request is sent.
{% endhint %}
### Default Body
There are a couple of ways to interact with the request body to prepare it to be sent. You can either use the methods mentioned below to add to the body on any given instance or you can use the `defaultBody` method on your request. This is recommended because you could then define any requirements as constructor arguments in your request and then standardise your request even more.
```php
<?php
use Saloon\Http\Request;
use Saloon\Contracts\Body\HasBody;
use Saloon\Traits\Body\HasJsonBody;
class CreateServerRequest extends Request implements HasBody
{
use HasJsonBody;
protected Method $method = Method::POST;
public function __construct(
protected string $ubuntuVersion,
protected string $type,
protected string $provider
){}
protected function defaultBody(): array
{
return [
'ubuntu_version' => $this->ubuntuVersion,
'type' => $this->type,
'provider' => $this->provider,
];
}
}
```
Sometimes there can be too many items to define as constructor arguments. If this is the case for you, you can require the user to provide an array, or even a DTO to populate the default body of your request.
{% tabs %}
{% tab title="Using an array" %}
```php
<?php
use Saloon\Http\Request;
use Saloon\Contracts\Body\HasBody;
use Saloon\Traits\Body\HasJsonBody;
class CreateServerRequest extends Request implements HasBody
{
use HasJsonBody;
protected Method $method = Method::POST;
public function __construct(
protected array $payload
){}
protected function defaultBody(): array
{
return $this->payload;
}
}
```
{% endtab %}
{% tab title="Using a DTO (Data Transfer Object)" %}
<pre class="language-php"><code class="lang-php">&#x3C;?php
use Saloon\Http\Request;
use Saloon\Contracts\Body\HasBody;
use Saloon\Traits\Body\HasJsonBody;
class CreateServerRequest extends Request implements HasBody
{
use HasJsonBody;
protected Method $method = Method::POST;
public function __construct(
<strong> protected Server $server
</strong> ){}
protected function defaultBody(): array
{
return [
'ubuntu_version' => $this->server->ubuntuVersion,
'type' => $this->server->type,
'provider' => $this->server->provider,
];
}
}
</code></pre>
{% endtab %}
{% endtabs %}
### Interacting with the body() method
While you can define the default body on your request, it might be useful to add or modify the body at runtime on a per-request basis. Saloon has the following methods to allow you to modify the JSON request body:
* add(string $key, mixed $value) -> **Add items to the JSON body**
* remove(string $key) -> **Remove items from the JSON body**
* merge(…$values) -> **Merge another array of items into the JSON body**
* set(array $value) -> **Overwrite the JSON body entirely**
* all(): array -> **Get all the values of the JSON body**
* isEmpty(): bool -> **Check if the body is empty**
* isNotEmpty(): bool -> **Check if the body is not empty**
```php
<?php
$request = new CreateServerRequest;
$request->body()->add('ubuntu_version', '22.04');
$request->body()->merge([
'type' => 'app',
'provider' => 'ocean2',
]);
$body = $request->body()->all();
// array: [
// 'ubuntu_version' => '22.04',
// 'type' => 'app',
// 'provider' => 'ocean2',
// ]
```
### Connector Body
If you would like to also have JSON body on your connector, you can add the same interface and trait to your connector. If you have the trait on both the connector and the request, the properties will be merged. This is useful if you want to have a shared JSON body across every request, like an authentication token.
```php
<?php
use Saloon\Http\Connector;
use Saloon\Contracts\Body\HasBody;
use Saloon\Traits\Body\HasJsonBody;
class ForgeConnector extends Connector implements HasBody
{
use HasJsonBody;
protected function defaultBody(): array
{
return [
'name' => 'Sam',
];
}
}
```
### JSON Flags
When using the `HasJsonBody` trait, you may want to customise the flags used when encoding the body to be sent to the API you are integrating with. You may do this with the `setJsonFlags` method. It's recommended that you set this within the constructor of your request or connector to ensure that the flags are always used.
For example, if I know that my JSON will contain URLs, but I don't want the URL slashes to be escaped, I can use the `JSON_UNESCAPED_SLASHES` flag. You can also specify multiple flags at once by using the `|` pipe to separate the flags.
```php
<?php
use Saloon\Http\Request;
use Saloon\Contracts\Body\HasBody;
use Saloon\Traits\Body\HasJsonBody;
class CreateServerRequest extends Request implements HasBody
{
use HasJsonBody;
public function __construct()
{
$this->body()->setJsonFlags(JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
}
}
```
]]>
</json_body_md>
<multipart_form_body_md>
<![CDATA[
# Multipart Form Body
Multipart body (multipart/form-data) is often used in modern APIs as a way to upload a mixture of files and data. Saloon makes handling multipart bodies easy by providing you with a standardised way of interacting with the values to be sent. You don't have to worry about calculating boundaries or properly encoding the multipart values, you can just use simple arrays and a value object to define the values.
To get started, change your method to **POST, PUT or PATCH** depending on the requirements of the API. After that, you will need to add the `HasBody` interface to your request. Without this interface, Saloon will not send any request body to the HTTP client.
<pre class="language-php"><code class="lang-php">&#x3C;?php
use Saloon\Http\Request;
use Saloon\Contracts\Body\HasBody;
<strong>class UploadProfilePictureRequest extends Request implements HasBody
</strong>{
protected Method $method = Method::POST;
}
</code></pre>
Next, you will need to add the `HasMultipartBody` trait to your request. This trait will implement the `body()` method that the `HasBody` interface requires. It also provides a method `defaultBody()` which you can extend to provide a default body on your request.
<pre class="language-php"><code class="lang-php">&#x3C;?php
use Saloon\Http\Request;
use Saloon\Contracts\Body\HasBody;
<strong>use Saloon\Traits\Body\HasMultipartBody;
</strong>
class UploadProfilePictureRequest extends Request implements HasBody
{
<strong> use HasMultipartBody;
</strong>
protected Method $method = Method::POST;
}
</code></pre>
{% hint style="info" %}
Saloon will automatically send the `Content-Type: multipart/form-data` header for you when using the `HasMultipartBody` trait, however, if you would like to overwrite this behaviour then you can use the `defaultHeaders` method on the request or modify the headers before the request is sent.
{% endhint %}
### The MultipartValue
To build up multipart data, Saloon uses a class `Saloon\Data\MultipartValue`. This class is used as a value object and when your request is sent, it will automatically be converted into a stream to be sent to the API. To see how to use this class, continue reading below.
```php
<?php
use Saloon\Data\MultipartValue;
new MultipartValue(
name: 'picture', // Required: the name of the multipart value
value: 'file-path-or-stream', // Required: Absolute path or file stream
filename: 'profile.png', // Optional: File name
headers: [], // Optional: Headers to be sent with the individual value
)
```
### Default Body
There are a couple of ways to interact with the request body to prepare it to be sent. You can either use the methods mentioned below to add to the body on any given instance or you can use the `defaultBody` method on your request. This is recommended because you could then define any requirements as constructor arguments in your request and then standardise your request even more. In this example, we are uploading a profile picture to a server so we will expect a file path through our constructor - however, Saloon can support file paths or streams.
```php
<?php
use Saloon\Http\Request;
use Saloon\Data\MultipartValue;
use Saloon\Contracts\Body\HasBody;
use Saloon\Traits\Body\HasMultipartBody;
class UploadProfilePictureRequest extends Request implements HasBody
{
use HasMultipartBody;
protected Method $method = Method::POST;
public function __construct(
protected string $filePath,
){}
protected function defaultBody(): array
{
return [
new MultipartValue(name: 'picture', value: $this->filePath)
];
}
}
```
### Interacting with the body() method
While you can define the default body on your request, it might be useful to add or modify the body at runtime on a per-request basis. Saloon has the following methods to allow you to modify the multipart request body:
* add(string $name, mixed $value, string $filename = null, array $headers = \[]) -> **Add a multipart value to the multipart body**
* attach(MultipartValue $value) -> **Attach a multipart directly to the multipart body**
* remove(string $key) -> **Remove an item from the multipart body**
* merge(... $arrays) -> **Merge another array of multipart values into the multipart body**
* set(array $value) -> **Overwrite the entire multipart body with a different set of values**
* all() -> **Get the array of multipart values**
* get(string $key) **-> Get an individual multipart value**
* isEmpty() **-> Check if the multipart body is empty**
* isNotEmpty() **-> Check if the multipart body is not empty**
```php
<?php
$request = new UploadProfilePictureRequest;
$request->body()->add(
name: 'picture',
contents: 'your-file-contents-or-stream',
filename: 'picture.png', // Optional file name
headers: [
// Optional custom headers
]
);
// You may also get a multipart value by name.
$request->body()->get('picture'); // MultipartValue class
```
{% hint style="info" %}
The `contents` of the `MultipartValue` class can be either the raw-text contents of the file or a PHP stream/resource.
{% endhint %}
### Connector Body
If you would like to also have a multipart body on your connector, you can add the same interface and trait to your connector. If you have the trait on both the connector and the request, the properties will be merged. This is useful if you want to have a shared value across every request.
```php
<?php
use Saloon\Http\Connector;
use Saloon\Data\MultipartValue;
use Saloon\Contracts\Body\HasBody;
use Saloon\Traits\Body\HasMultipartBody;
class ForgeConnector extends Connector implements HasBody
{
use HasMultipartBody;
protected function defaultBody(): array
{
return [
new MultipartValue(name: 'logo', value: 'image-contents'),
new MultipartValue(name: 'logo', value: StreamObject),
new MultipartValue(name: 'logo', value: 'image-contents', filename: 'logo.png', headers: [...]),
];
}
}
```
]]>
</multipart_form_body_md>
<README_md>
<![CDATA[
# 🎁 Attaching Body/Data
When sending HTTP requests, a common requirement is to send data to the server with POST, PUT, or PATCH requests, like JSON, XML or multipart data. Saloon makes this easy for you with built-in body traits.
### Getting Started
To get started, you will need to add the `HasBody` interface to your request. This interface is required as it tells Saloon to look for a `body()` method supplied by one of the body traits. Without this interface, Saloon will not send any request body to the HTTP client. Also make sure to change your method to POST, PUT or PATCH depending on the requirements of the API.
<pre class="language-php"><code class="lang-php">&#x3C;?php
use Saloon\Http\Request;
use Saloon\Contracts\Body\HasBody;
<strong>class CreateServerRequest extends Request implements HasBody
</strong>{
protected Method $method = Method::POST;
}
</code></pre>
Next, you will need to add a trait to provide an implementation for the missing `body()` method. Saloon has a trait for all the common types of request bodies.
Continue reading below to understand more about the specific body type that you need.
{% content-ref url="json-body.md" %}
[json-body.md](json-body.md)
{% endcontent-ref %}
{% content-ref url="multipart-form-body.md" %}
[multipart-form-body.md](multipart-form-body.md)
{% endcontent-ref %}
{% content-ref url="stream-body.md" %}
[stream-body.md](stream-body.md)
{% endcontent-ref %}
{% content-ref url="form-body-url-encoded.md" %}
[form-body-url-encoded.md](form-body-url-encoded.md)
{% endcontent-ref %}
{% content-ref url="xml-body.md" %}
[xml-body.md](xml-body.md)
{% endcontent-ref %}
{% content-ref url="string-plain-text-body.md" %}
[string-plain-text-body.md](string-plain-text-body.md)
{% endcontent-ref %}
]]>
</README_md>
<stream_body_md>
<![CDATA[
# Stream Body
Similar to the string / plain text body, the stream body allows you to send a stream or a file resource directly to the server. This is useful if the API accepts a plain-text style input but large files in the form of streams.
To get started, change your method to **POST, PUT or PATCH** depending on the requirements of the API. After that, you will need to add the `HasBody` interface to your request. Without this interface, Saloon will not send any request body to the HTTP client.&#x20;
<pre class="language-php"><code class="lang-php">&#x3C;?php
use Saloon\Http\Request;
use Saloon\Contracts\Body\HasBody;
<strong>class CreateServerRequest extends Request implements HasBody
</strong>{
protected Method $method = Method::POST;
}
</code></pre>
Next, you will need to add the `HasBody` trait to your request. This trait will implement the `body()` method that the `HasBody` interface requires. It also provides a method `defaultBody()` which you can extend to provide a default body on your request.
<pre class="language-php"><code class="lang-php">&#x3C;?php
use Saloon\Http\Request;
use Saloon\Contracts\Body\HasBody;
use Saloon\Traits\Body\HasStreamBody;
class CreateServerRequest extends Request implements HasBody
{
<strong> use HasStreamBody;
</strong>
protected Method $method = Method::POST;
// Define our own Content-Type...
protected function defaultHeaders(): array
{
return [
'Content-Type' => 'text/plain',
];
}
}
</code></pre>
{% hint style="info" %}
Saloon won't add a Content-Type header for you for plain stream bodies so you must define the Content-Type or the API might not accept the data.
{% endhint %}
### Default Body
There are a couple of ways to interact with the request body to prepare it to be sent. You can either use the methods mentioned below to add to the body on any given instance or you can use the `defaultBody` method on your request. This is recommended because you could then define any requirements as constructor arguments in your request and then standardise your request even more.&#x20;
```php
<?php
use Saloon\Http\Request;
use Saloon\Contracts\Body\HasBody;
use Saloon\Traits\Body\HasStreamBody;
class CreateServerRequest extends Request implements HasBody
{
use HasStreamBody;
protected Method $method = Method::POST;
public function __construct(
protected mixed $stream,
){}
protected function defaultBody(): mixed
{
return $this->stream;
}
}
```
{% hint style="info" %}
Saloon can accept a PHP resource or a PSR `StreamInterface` Stream implementation as the stream.
{% endhint %}
### Interacting with the body() method
While you can define the default body on your request, it might be useful to add or modify the body at runtime on a per-request basis. Saloon has the following methods to allow you to modify the stream request body:
* set(resource|StreamInterface $value) -> **Set the stream**
* all(): StreamInterface -> **Get the stream**
* isEmpty(): bool -> **Check if the body is empty**
* isNotEmpty(): bool -> **Check if the body is not empty**
```php
<?php
$request = new CreateServerRequest;
$request->body()->set(fopen('cowboy-hat.png', 'r'));
$body = $request->body()->all(); // Returns PSR StreamInterface
```
### Connector Body
You can add the same interface and trait to your connector. If you have the trait on both the connector and the request, the body on the request will take priority. If you have the body on just the connector but not on the request, the request will inherit the body on the connector.
```php
<?php
use Saloon\Http\Request;
use Saloon\Contracts\Body\HasBody;
use Saloon\Traits\Body\HasStreamBody;
class ForgeConnector extends Connector implements HasBody
{
use HasStreamBody;
protected function defaultBody(): mixed
{
return fopen('cowboy-hat.png', 'r');
}
}
```
]]>
</stream_body_md>
<string_plain_text_body_md>
<![CDATA[
# String / Plain Text Body
This body type has been created if you need to send a plain string/text to the server. This body type does not come with a default Content-Type, so you must provide this.
To get started, change your method to **POST, PUT or PATCH** depending on the requirements of the API. After that, you will need to add the `HasBody` interface to your request. Without this interface, Saloon will not send any request body to the HTTP client.
<pre class="language-php"><code class="lang-php">&#x3C;?php
use Saloon\Http\Request;
use Saloon\Contracts\Body\HasBody;
<strong>class CreateServerRequest extends Request implements HasBody
</strong>{
protected Method $method = Method::POST;
}
</code></pre>
Next, you will need to add the `HasStringBody` trait to your request. This trait will implement the `body()` method that the `HasBody` interface requires. It also provides a method `defaultBody()` which you can extend to provide a default body on your request.
<pre class="language-php"><code class="lang-php">&#x3C;?php
use Saloon\Http\Request;
use Saloon\Contracts\Body\HasBody;
<strong>use Saloon\Traits\Body\HasStringBody;
</strong>
class CreateServerRequest extends Request implements HasBody
{
<strong> use HasStringBody;
</strong>
protected Method $method = Method::POST;
// Define our own Content-Type...
protected function defaultHeaders(): array
{
return [
'Content-Type' => 'text/plain',
];
}
}
</code></pre>
{% hint style="info" %}
Saloon won't add a Content-Type header for you for plain string bodies so you must provide your own Content-Type header.
{% endhint %}
### Default Body
There are a couple of ways to interact with the request body to prepare it to be sent. You can either use the methods mentioned below to add to the body on any given instance or you can use the `defaultBody` method on your request. This is recommended because you could then define any requirements as constructor arguments in your request and then standardise your request even more.&#x20;
<pre class="language-php"><code class="lang-php">&#x3C;?php
use Saloon\Http\Request;
use Saloon\Contracts\Body\HasBody;
<strong>use Saloon\Traits\Body\HasStringBody;
</strong>
class CreateServerRequest extends Request implements HasBody
{
<strong> use HasStringBody;
</strong>
protected Method $method = Method::POST;
public function __construct(
protected string $ubuntuVersion,
protected string $provider
){}
protected function defaultBody(): string
{
return 'Howdy, Partner. I want a ' . $this->ubuntuVersion . ' server through ' . $this->provider . ' provider!';
}
}
</code></pre>
### Interacting with the body() method
While you can define the default body on your request, it might be useful to add or modify the body at runtime on a per-request basis. Saloon has the following methods to allow you to modify the string request body:
* set(string $value) -> **Overwrite the string body entirely**
* all(): array -> **Get the string body**&#x20;
* isEmpty(): bool -> **Check if the body is empty**
* isNotEmpty(): bool -> **Check if the body is not empty**
```php
<?php
$request = new CreateServerRequest;
$request->body()->set('Howdy, Partner');
$body = $request->body()->all();
// string: 'plain-text-response-body'
```
### Connector Body
You can add the same interface and trait to your connector. If you have the trait on both the connector and the request, the body on the request will take priority. If you have the body on just the connector but not on the request, the request will inherit the body on the connector.
```php
<?php
use Saloon\Http\Request;
use Saloon\Contracts\Body\HasBody;
use Saloon\Traits\Body\HasStringBody;
class ForgeConnector extends Connector implements HasBody
{
use HasStringBody;
protected function defaultBody(): string
{
return 'Howdy, Partner';
}
}
```
]]>
</string_plain_text_body_md>
<xml_body_md>
<![CDATA[
# XML Body
To get started, change your method to **POST, PUT or PATCH** depending on the requirements of the API. After that, you will need to add the `HasBody` interface to your request. Without this interface, Saloon will not send any request body to the HTTP client.
<pre class="language-php"><code class="lang-php">&#x3C;?php
use Saloon\Http\Request;
use Saloon\Contracts\Body\HasBody;
<strong>class CreateServerRequest extends Request implements HasBody
</strong>{
protected Method $method = Method::POST;
}
</code></pre>
Next, you will need to add the `HasXmlBody` trait to your request. This trait will implement the `body()` method that the `HasBody` interface requires. It also provides a method `defaultBody()` which you can extend to provide a default body on your request.
<pre class="language-php"><code class="lang-php">&#x3C;?php
use Saloon\Http\Request;
use Saloon\Contracts\Body\HasBody;
<strong>use Saloon\Traits\Body\HasXmlBody;
</strong>
class CreateServerRequest extends Request implements HasBody
{
<strong> use HasXmlBody;
</strong>
protected Method $method = Method::POST;
}
</code></pre>
{% hint style="info" %}
Saloon will automatically send the `Content-Type: application/xml` header for you when using the `HasXmlBody` trait, however, if you would like to overwrite this behaviour then you can use the `defaultHeaders` method on the request or modify the headers before the request is sent.
{% endhint %}
### Default Body
There are a couple of ways to interact with the request body to prepare it to be sent. You can either use the methods mentioned below to add to the body on any given instance or you can use the `defaultBody` method on your request. This is recommended because you could then define any requirements as constructor arguments in your request and then standardise your request even more.&#x20;
```php
<?php
use Saloon\Http\Request;
use Saloon\Contracts\Body\HasBody;
use Saloon\Traits\Body\HasXmlBody;
class CreateServerRequest extends Request implements HasBody
{
use HasXmlBody;
protected Method $method = Method::POST;
public function __construct(
protected readonly string $ubuntuVersion,
protected readonly string $type,
protected readonly string $provider
){}
protected function defaultBody(): string
{
return <<<XML
<?xml version="1.0" ?>
<root>
<ubuntu-version>' . $this->ubuntuVersion . '</ubuntu-version>
<type>' . $this->type . '</type>
<provider>' . $this->provider . '</provider>
</root>
XML;
}
}
```
{% hint style="info" %}
While you are expected to return a raw string for the XML body, it can be tedious to write XML as text. We have built a library, [XML Wrangler](https://github.com/saloonphp/xml-wrangler) which allows you to read and write XML in a more developer-friendly way.
{% endhint %}
### Interacting with the body() method
While you can define the default body on your request, it might be useful to add or modify the body at runtime on a per-request basis. Saloon has the following methods to allow you to modify the XML request body:
* set(string $value) -> **Overwrite the XML body entirely**
* all(): array -> **Get the XML body**&#x20;
* isEmpty(): bool -> **Check if the body is empty**
* isNotEmpty(): bool -> **Check if the body is not empty**
```php
<?php
$request = new CreateServerRequest;
$request->body()->set('<?xml version="1.0"?><root></root>');
$body = $request->body()->all();
// string: 'plain-text-response-body'
```
### Connector Body
You can add the same interface and trait to your connector. If you have the trait on both the connector and the request, the body on the request will take priority. If you have the body on just the connector but not on the request, the request will inherit the body on the connector.
```php
<?php
use Saloon\Http\Connector;
use Saloon\Contracts\Body\HasBody;
use Saloon\Traits\Body\HasXmlBody;
class ForgeConnector extends Connector implements HasBody
{
use HasXmlBody;
protected function defaultBody(): string
{
return '<?xml version="1.0"?><root></root>';
}
}
```
### XML Wrangler
You may be interested in a third-party library written by the maintainer of Saloon - XML Wrangler. It provides a modern syntax for reading and writing XML in PHP.
{% content-ref url="../../installable-plugins/xml-wrangler.md" %}
[xml-wrangler.md](../../installable-plugins/xml-wrangler.md)
{% endcontent-ref %}
]]>
</xml_body_md>
</request_body_data>
<requests_md>
<![CDATA[
# ✉ Requests
API requests in Saloon each have a class. Within a request class, you define the endpoint, the HTTP method, and properties like headers, query parameters and body.
### Getting Started
Create a class that is in a similar place to your connector. The class should extend the `Request` abstract class. Inside the class, define a `$method` property and a `resolveEndpoint` method. The `resolveEndpoint` method will be combined with the connector's base URL.
```php
<?php
use Saloon\Enums\Method;
use Saloon\Http\Request;
class GetServersRequest extends Request
{
protected Method $method = Method::GET;
public function resolveEndpoint(): string
{
return '/servers';
}
}
```
> If you have installed the Laravel plugin, you can use the **php artisan saloon:request** command to create a request.&#x20;
### Headers
Some API requests require headers to be sent. To define default headers on your request, you can extend the `defaultHeaders` method. These will be merged with the connector's headers.
```php
class GetServersRequest extends Request
{
// ...
protected function defaultHeaders(): array
{
return [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
];
}
}
```
You can also use the `headers` method on a request instance.
```php
$request = new GetServersRequest;
$request->headers()->add('Content-Type', 'text/plain');
```
### Query Parameters
Some API requests require query parameters to be sent. You can extend the `defaultQuery` method to provide these query parameters.
```php
class GetServersRequest extends Request
{
// ...
protected function defaultQuery(): array
{
return [
'sort' => 'name', // ?sort=name
'filter[active]' => 'true', // ?filter[active]=true
];
}
}
```
You can also use the `query` method on a request instance.
```php
$request = new GetServersRequest;
$request->query()->add('sort', 'provider');
```
{% hint style="info" %}
Different APIs sometimes require different query string formats. Saloon uses PHP's built-in [`http_build_query`](https://www.php.net/manual/en/function.http-build-query.php) function to create query strings, but if you need something different, you can reformat the query string in the `Connector::handlePsrRequest()` method (see [Modifying the PSR-7 Request](../digging-deeper/psr-support.md#modifying-the-psr-7-request).
{% endhint %}
### Timeout
By default, Saloon will have a connection timeout of 10 seconds and a request timeout of 30 seconds. You can customise this by using the `HasTimeout` trait and specifying a `connectTimeout` and `requestTimeout` property.
```php
use Saloon\Traits\Plugins\HasTimeout;
class GetServersRequest extends Request
{
use HasTimeout;
protected int $connectTimeout = 60;
protected int $requestTimeout = 120;
}
```
### Constructor Arguments
Since requests are just classes, you can define a constructor to populate the request's properties. For example, if you are getting a specific resource you would need to pass its ID into the request class.
```php
class GetServerRequest extends Request
{
protected Method $method = Method::GET;
public function __construct(protected readonly string $id) {
//
}
public function resolveEndpoint(): string
{
return '/servers/' . $this->id;
}
}
```
```php
$request = new GetServerRequest('5664b05b-8b32-4523-a0d5-837f5080417a');
```
]]>
</requests_md>
<responses_md>
<![CDATA[
# 📡 Responses
After sending a request, Saloon will return a `Response` class. This response class contains many helpful methods for interacting with your HTTP response like seeing the HTTP status code and retrieving the body.
```php
<?php
$forge = new ForgeConnector;
$request = new GetServersRequest;
$response = $forge->send($request);
$status = $response->status();
$body = $response->body();
```
{% hint style="info" %}
By default, Saloon will not throw an exception if a synchronous request fails. [Refer to the handling failures section for handling errors.](handling-failures.md)
{% endhint %}
### Useful Methods
Below are some of the key methods provided by Saloon's `Response` class.
<table><thead><tr><th width="342">Method</th><th>Description</th></tr></thead><tbody><tr><td><code>status</code></td><td>Returns the response status code.</td></tr><tr><td><code>headers</code></td><td>Returns all response headers</td></tr><tr><td><code>header</code></td><td>Returns a given header</td></tr><tr><td><code>body</code></td><td>Returns the raw response body as a string</td></tr><tr><td><code>json</code></td><td>Retrieves a JSON response body and json_decodes it into an array.</td></tr><tr><td><code>array</code></td><td>Alias of <code>json</code></td></tr><tr><td><code>collect</code></td><td>Retrieves a JSON response body and json_decodes it into a Laravel Collection. Requires <code>illuminate/collections</code>.</td></tr><tr><td><code>object</code></td><td>Retrieves a JSON response body and json_decodes it into an object.</td></tr><tr><td><code>xmlReader</code></td><td>Used for XML responses - returns a <a href="../installable-plugins/xml-wrangler.md">XML Wrangler</a> reader. Requires <code>saloonphp/xml-wrangler</code>.</td></tr><tr><td><code>dom</code></td><td>Used for HTML responses - returns a <a href="https://symfony.com/doc/current/components/dom_crawler.html">Symfony DOM Crawler</a> instance. Requires <code>symfony/dom-crawler</code>.</td></tr><tr><td><code>stream</code></td><td>Returns the response body as an instance of <code>StreamInterface</code></td></tr><tr><td><code>saveBodyToFile</code></td><td>Allows you to save the raw body to a file or open file resource.</td></tr><tr><td><code>dto</code></td><td>Converts the response into a data-transfer object. You must define your DTO first, <a href="data-transfer-objects.md">click here to read more.</a></td></tr><tr><td><code>dtoOrFail</code></td><td>Will work just like <code>dto</code> but will throw an exception if the response is considered "failed".</td></tr><tr><td><code>ok</code>, <code>successful</code>, <code>redirect</code>, <code>failed</code>, <code>clientError</code>, <code>serverError</code></td><td>Methods used to determine if a request was successful or not based on status code. The <code>failed</code> method <a href="handling-failures.md#customising-when-saloon-thinks-a-request-has-failed">can be customised</a>.</td></tr><tr><td><code>throw</code></td><td>Will throw an exception if the response is considered "failed".</td></tr><tr><td><code>getPendingRequest</code></td><td>Returns the <code>PendingRequest</code> class that was built up for the request.</td></tr><tr><td><code>getPsrRequest</code></td><td>Returns the PSR-7 request that was built up by Saloon</td></tr><tr><td><code>getPsrResponse</code></td><td>Return the PSR-7 response that was built up by the HTTP client/sender.</td></tr></tbody></table>
### Asynchronous Responses
When `sendAsync` or concurrent requests, Saloon will respond with a `GuzzleHttp\Promise\PromiseInterface.` The promise will contain a `Response` a class described above. When the request fails, Saloon will not use the `then` method but return an instance of `RequestException`in the `otherwise` block.
```php
<?php
use Saloon\Http\Response;
$forge = new ForgeConnector;
$promise = $forge->sendAsync(new GetServersRequest);
$promise
->then(function (Response $response) {
// Handle successful response
})
->otherwise(function (Exception $exception) {
// Handle failed request
});
$promise->wait(); // Force the promise to be resolved
```
### Custom Responses
Sometimes you may want to use your response class. This is useful if you want to add your methods or overwrite Saloon's response methods. Saloon allows you to overwrite the response at a connector level for all requests or at a per-request level for a granular response.
The simplest way of registering a custom response is to use the `$response` property on either the connector or request.
{% tabs %}
{% tab title="Connector" %}
<pre class="language-php"><code class="lang-php">class ForgeConnector extends Connector
{
// ...
<strong> protected ?string $response = CustomResponse::class;
</strong>}
</code></pre>
{% endtab %}
{% tab title="Request" %}
<pre class="language-php"><code class="lang-php">class GetServersRequest extends Request
{
// ...
<strong> protected ?string $response = CustomResponse::class;
</strong>}
</code></pre>
{% endtab %}
{% endtabs %}
When you need a more advanced way to define a custom response, use the `resolveResponseClass` method on either the connector or request.
{% tabs %}
{% tab title="Connector" %}
```php
class ForgeConnector extends Connector
{
// ...
public function resolveResponseClass(): string
{
return CustomResponse::class;
}
}
```
{% endtab %}
{% tab title="Request" %}
```php
class GetServersRequest extends Request
{
// ...
public function resolveResponseClass(): string
{
return CustomResponse::class;
}
}
```
{% endtab %}
{% endtabs %}
]]>
</responses_md>
<sending_requests_md>
<![CDATA[
# 🚀 Sending Requests
To start sending requests, instantiate your connector and use the `send` method. This method accepts a request class.
```php
<?php
$forge = new ForgeConnector;
$request = new GetServersRequest;
$response = $forge->send($request);
```
### Asynchronous Requests
To send an asynchronous request, use the `sendAsync` method, and you will receive an instance of `PromiseInterface`. You should define a `then` and `otherwise` method to receive the response or exception.&#x20;
```php
<?php
$forge = new ForgeConnector('api-token');
$promise = $forge->sendAsync(new GetServersRequest);
$promise
->then(function (Response $response) {
// Handle Response
})
->otherwise(function (RequestException $exception) {
// Handle Exception
});
$promise->wait(); // Force the promise to be resolved
```
{% hint style="info" %}
Saloon uses Guzzle's Promises library. [Click here to learn more](https://github.com/guzzle/promises)
{% endhint %}
]]>
</sending_requests_md>
<testing>
<README_md>
<![CDATA[
# ✅ Testing
### Introduction
Writing tests for your API integrations is an important part of ensuring your application remains stable over time. This section of the documentation will walk you through how to use Saloon's testing helpers to mock API requests and write assertions to prove your application is handling requests and responses properly.
#### What should you test?
The general rule of thumb is that you should assume that the API you are integrating with has tests that ensure their API always returns what they tell you, so you don't have to worry about testing that specific endpoint returns the right data.
You should, however, always test how your application handles the API response that comes back. What happens if the API is down or it returns a 500 error? What happens if your access token expires? You should make sure your application can handle these scenarios.
#### Why use fake responses and fixtures instead of making the API calls every time?
When running your application tests, you are likely going to be running them over and over again which might incur charges or hit API rate limits. Instead, you can either write a fake response or Saloon can record an API call once and re-use that same response in all subsequent tests.&#x20;
Faking API responses are typically much faster as they do not require an internet connection or potentially long wait times.
### Getting Started
#### The MockClient
Saloon's testing starts with the **MockClient** class. This class can be instantiated locally for testing individual requests or used globally to test an API call nested deep in your application. Inside the MockClient, you can define requests and responses that should be returned instead of a real API call.
```php
use Saloon\Http\Faking\MockClient;
test('my test', function () {
$mockClient = new MockClient([
GetServersRequest::class => MockResponse::make(body: '', status: 200),
]);
$connector = new ForgeConnector;
$connector->withMockClient($mockClient);
});
```
You use the key of the MockClient to define a request that should be mocked. You can also use a URL pattern. You can use an asterisk (**\***) to act as a wild card.
```php
$mockClient = new MockClient([
'forge.laravel.com/api/v1/servers' => MockResponse::make(body: '', status: 200),
'forge.laravel.com/*' => MockResponse::make(body: '', status: 200),
]);
```
You can also use a sequence of responses which will be returned regardless of the API call.
```php
$mockClient = new MockClient([
MockResponse::make(body: 'First', status: 200),
MockResponse::make(body: 'Second', status: 200),
MockResponse::make(body: 'Third', status: 200),
]);
```
You can also use a closure for more advanced mock responses.
```php
$mockClient = new MockClient([
GetServersRequest::class => function (PendingRequest $pendingRequest) {
return MockResponse::make(...);
},
]);
```
#### Manual Fake Responses
There are two ways to define a response, you can do it manually with `MockResponse::make()` which is suitable for simple responses. It will accept three arguments: body, HTTP status code and headers. You may pass a string or an array into the body argument. If you provide an array, it will automatically be converted into JSON.
```php
$mockClient = new MockClient([
GetUserRequest::class => MockResponse::make(
body: ['user' => ['name' => 'Sam']],
status: 200,
headers: ['Content-Type' => 'application/json']
),
]);
```
#### Recorded Responses (Fixtures)
The second way to define a response is with `MockResponse::fixture()` this is suitable for testing larger API responses or to save time. With this, Saloon will make a real API call for the first time, and then store the API call inside of a file in your application, so that when you run the test again, the response will be used and not make a real API call.
```php
$mockClient = new MockClient([
GetUserRequest::class => MockResponse::fixture('user');
]);
```
### Testing your application
Now that you have an idea of how testing works in Saloon, let's put it into practice. This section of the documentation will teach you how to write a MockClient for your application. This guide assumes you have already set up a test suite for your application. If you are new to testing, we recommend [PEST](https://pestphp.com/).
{% hint style="info" %}
This section of the documentation uses the new **Global Mock Client** introduced in Saloon v3.5. Make sure that you are using this version of Saloon by running: `composer update "saloonphp/*"`
{% endhint %}
#### Setup
First of all, you will need to add a method which should run before every test in your application. Without this, the global mock client will leak into other tests and may break your test suite.&#x20;
{% tabs %}
{% tab title="PEST" %}
If you are using PEST, then you should add the following code to your `Pest.php` file.
```php
use Saloon\Http\Faking\MockClient;
uses()
->beforeEach(fn () => MockClient::destroyGlobal())
->in(__DIR__);
```
{% endtab %}
{% tab title="PHPUnit" %}
If you are using PHPUnit, then you should add the following line inside of your `setUp` method in your test or application's `TestCase.php` file.
```php
use Saloon\Http\Faking\MockClient;
class RequestTest extends TestCase
{
protected function setUp(): void
{
MockClient::destroyGlobal();
}
}
```
{% endtab %}
{% endtabs %}
{% hint style="info" %}
If you are using Laravel and have installed the [Laravel plugin](../../installable-plugins/laravel-integration.md), you do not need to destroy the global mock client before each test as the plugin will do this for you.
{% endhint %}
#### Writing tests
Let's say that your application has a controller which queries the Laravel Forge API and stores the list of servers in the database. We'll use the global mock client to return fake data so we don't send an API call to the external API.
Saloon will detect the global mock client and will use the mock response which you have defined instead of sending a real API call. It's recommended to define your mock at the top of a test.
{% tabs %}
{% tab title="Test" %}
```php
<?php
use Saloon\Http\Faking\MockClient;
use Saloon\Http\Faking\MockResponse;
test('can store servers in the database from laravel forge', function () {
MockClient::global([
GetServersRequest::class => MockResponse::make(
body: [
'data' => [
['name' => 'WEB-1', 'ip' => '192.168.0.1'],
['name' => 'WEB-2', 'ip' => '192.168.0.2'],
],
],
status: 200,
),
]);
$this->assertDatabaseCount('servers', 0);
// Call our controller which will invoke the Saloon request
$this
->getJson('/api/servers/sync')
->assertOk();
// Assert our database has created servers!
$this->assertDatabaseCount('servers', 2);
$this->assertDatabaseHas('servers', [
'name' => 'WEB-1',
'ip' => '192.168.0.1',
]);
$this->assertDatabaseHas('servers', [
'name' => 'WEB-2',
'ip' => '192.168.0.2',
]);
});
```
{% endtab %}
{% tab title="Controller Code" %}
```php
<?php
class ForgeController
{
// api/servers/sync
public function sync()
{
$forge = new ForgeConnector;
$servers = $forge->send(new GetServersRequest)->array('data');
foreach($servers as $server) {
DB::table('servers')->insert([
'name' => $server['name'],
'ip' => $server['ip'],
]);
}
return response('Synced servers', 200);
}
}
```
{% endtab %}
{% endtabs %}
### Recording Requests (Fixtures)
So far we have covered testing where you have to write your mock response yourself. This works well for small responses but most API responses will have a lot more data. As previously mentioned, Saloon supports recording requests in your tests. This works by allowing the test to make a real API call the first time and then on subsequent API calls, the same response will be used.&#x20;
This works by saving the response as a file in your application which can be committed in your project and used for later.
{% hint style="warning" %}
Make sure that you do not accidentally store sensitive information from the real API call. You can read more about redacting fixtures [here](./#redacting-recorded-responses-fixtures).
{% endhint %}
#### Example with fixtures
Let's take a look at the example above but with fixtures, as you can see nothing else has to change except that we are using `MockResponse::fixture()`. It accepts one argument which is the name of the fixture.
<pre class="language-php"><code class="lang-php">&#x3C;?php
use Saloon\Http\Faking\MockClient;
use Saloon\Http\Faking\MockResponse;
test('can store servers in the database from laravel forge', function () {
MockClient::global([
<strong> GetServersRequest::class => MockResponse::fixture('servers'),
</strong> ]);
$this->assertDatabaseCount('servers', 0);
// Call our controller which will invoke the Saloon request
$this
->getJson('/api/servers/sync')
->assertOk();
// Assert our database has created servers!
$this->assertDatabaseCount('servers', 2);
$this->assertDatabaseHas('servers', [
'name' => 'REAL-SERVER-1',
'ip' => '192.168.0.1',
]);
$this->assertDatabaseHas('servers', [
'name' => 'REAL-SERVER-2',
'ip' => '192.168.0.2',
]);
});
</code></pre>
#### Customising Fixture Location
By default, fixtures will be stored in `tests/Fixtures/Saloon`. You can customise the fixture location by using the `setFixturePath` method on the `MockConfig` class.
```php
MockConfig::setFixturePath('tests/other-directory');
```
#### Namespacing Fixtures
With multiple API integrations, having all the fixtures in one directory can get messy. You can use `/` to create folders for your fixtures.
```php
MockClient::global([
GetServersRequest::class => MockResponse::fixture('forge/servers'),
]);
```
{% hint style="warning" %}
You should be cautious when naming fixture folders - some operating systems like MacOS do not have case sensitivity for folders but some Unix-based systems like Linux do. This can result in fixtures not being found. It's recommended to use lowercase names.
{% endhint %}
#### Refreshing Fixtures
If you need to refresh a fixture, all you have to do is delete the JSON file and re-run your test. Saloon will see the file is no longer there and will re-record the request.
### Testing individual requests and SDKs
Occasionally, you might want to write unit tests for individual requests to ensure you are sending the right information to an API. If you are building an SDK, then you may wish to follow this guide for testing your SDK.
#### Using the withMockClient method on your connector
Instead of using the global mock client, you can create a local instance of a MockClient and pass it into your connector or request using the `withMockClient` method.
<pre class="language-php"><code class="lang-php">&#x3C;?php
test('a request sends the correct body', function () {
$mockClient = new MockClient([
CreateServerRequest::class => MockResponse::make('Server Created', 200),
]);
$connector = new ForgeConnector;
<strong> $connector->withMockClient($mockClient);
</strong>
$connector->send(new CreateServerRequest($name, $ip));
$mockClient->assertSent(function (Request $request) {
return $request->body()->all() === ['name' => $name, 'ip' => $ip];
});
});
</code></pre>
### Assertions
Saloon has a variety of built-in assertion methods which you can also use in your test to assert that Saloon is sending the right information to the third-party API. You can define a variable for your MockClient and call the various assertion methods inside of your test.
```php
<?php
use Saloon\Http\Faking\MockClient;
use Saloon\Http\Faking\MockResponse;
test('can store servers in the database from laravel forge', function () {
$mockClient = MockClient::global([
GetServersRequest::class => MockResponse::make(...),
]);
// Your application code...
$mockClient->assertSent(GetServersRequest::class);
$mockClient->assertSentCount(1);
});
```
Saloon has the following assertions:
#### assertSent / assertNotSent
These methods will check that a specific request has been sent.
```php
$mockClient->assertSent(GetServersRequest::class);
```
The method can also accept a closure which should return true for more advanced assertions.
```php
$mockClient->assertSent(function (Request $request, Response $response) {
return $request instanceof GetServersRequest;
});
```
#### assertSentCount
This method allows you to assert that a specific number of requests has been sent.
```php
$mockClient->assertSentCount(1);
```
The method also accepts a second argument which allows you to check the amount a specific request class was sent.
```php
$mockClient->assertSentCount(2, GetServersRequest::class);
$mockClient->assertSentCount(1, CreateServerRequest::class);
```
#### assertNothingSent
This method will assert that no requests have been sent.
```php
$mockClient->assertNothingSent();
```
### Preventing Stray Requests
You may want to prevent real API calls from being attempted in your tests. You can use `Config::preventStrayRequests()` which will throw an exception if a real API call is attempted.
{% tabs %}
{% tab title="PEST" %}
If you are using PEST, then you should add the following code to your `Pest.php` file.
```php
use Saloon\Config;
Config::preventStrayRequests();
```
{% endtab %}
{% tab title="PHPUnit" %}
If you are using PHPUnit, then you should add the following line inside of your `setUp` method in your test or application's `TestCase.php` file.
```php
use Saloon\Config;
class TestCase extends BaseTestCase
{
protected function setUp(): void
{
parent::setUp();
Config::preventStrayRequests();
}
}
```
{% endtab %}
{% endtabs %}
{% hint style="warning" %}
Fixtures will still be recorded and not counted as stray requests.
{% endhint %}
### Preventing Fixtures Being Recorded
While running tests, you may wish to prevent Saloon from attempting to record any further fixtures. This is useful for CI like GitHub actions where you shouldn't expect any fixtures to be recorded. You can use `MockConfig::throwOnMissingFixtures()` to prevent this from happening.
{% tabs %}
{% tab title="PEST" %}
If you are using PEST, then you should add the following code to your `Pest.php` file.
```php
use Saloon\MockConfig;
MockConfig::throwOnMissingFixtures();
```
{% endtab %}
{% tab title="PHPUnit" %}
If you are using PHPUnit, then you should add the following line inside of your `setUp` method in your test or application's `TestCase.php` file.
```php
use Saloon\MockConfig;
class TestCase extends BaseTestCase
{
protected function setUp(): void
{
MockConfig::throwOnMissingFixtures();
}
}
```
{% endtab %}
{% endtabs %}
### Redacting Recorded Responses / Fixtures
When using fixtures to record real responses from an API - sometimes the API will return some sensitive information that you shouldn't store in your application's repository, like names of real people, financial data or emails. With Saloon, you can create a custom fixture class and provide a few methods to obscure the information when the data is stored. You can even provide closures for the data replacement so you can use tools like Faker to replace data like-for-like.
{% hint style="info" %}
The first time the request is made and the fixture is stored, the original response won't be redacted. Only future requests made with the fixture will use the redacted recording.
{% endhint %}
First, create a new class in your tests directory and extend the base `Fixture` class provided by Saloon. Next, extend the `defineName` method and give the fixture a name. You can still use slashes in this directory to denote folders.
Now you can use a few different methods to redact your fixture data.&#x20;
* defineSensitiveHeaders for headers
* defineSensitiveJsonParameters for JSON responses
* defineSensitiveRegexPatterns for other body types
The methods expect a key => value array where the key is the property that is being redacted and the value is the replacement. You can use a string as the replacement or use a closure for more advanced replacement engines like using faker.
<pre class="language-php"><code class="lang-php">&#x3C;?php
namespace Tests\Fixtures\Forge\SingleServerFixture;
use Saloon\Http\Faking\Fixture;
class SingleServerFixture extends Fixture
{
protected function defineName(): string
{
return 'forge/singleServer';
}
<strong> protected function defineSensitiveHeaders(): array
</strong> {
return [
'Authorization' => 'REDACTED',
];
}
<strong> protected function defineSensitiveJsonParameters(): array
</strong> {
return [
'name' => 'REDACTED',
'password' => function () {
return faker()->password;
},
];
}
<strong> protected function defineSensitiveRegexPatterns(): array
</strong> {
return [
'/@[a-z0-9_]{0,100}/' => 'REDACTED-TWITTER-HANDLE',
];
}
}
</code></pre>
Once you have created your custom fixture class with the redaction configuration, you can simply use it instead of `MockResponse::fixture()` in your tests. All detection methods in the mock client work with this type of fixture too.
<pre class="language-php"><code class="lang-php">&#x3C;?php
$mockClient = new MockClient([
<strong> GetServerRequest::class => new SingleServerFixture;
</strong>]);
</code></pre>
### Using Saloon's Laravel Facade
If you have installed the [official Laravel plugin](../../installable-plugins/laravel-integration.md), then you can use the `Saloon::fake()` method as an alias of `MockClient::global()`.
```php
use Saloon\Laravel\Facades\Saloon;
Saloon::fake([
GetServersRequest::class => MockResponse::make(body: '', status: 200),
]);
```
]]>
</README_md>
</testing>
</the_basics>
<upgrade>
<upgrading_from_v2_md>
<![CDATA[
# 🌳 Upgrading from v2
Saloon version three comes with many improvements over version two, but most of the improvements were internal changes, so the upgrade to v3 from v2 shouldn't be too cumbersome. If you haven't already, it's recommended to read through ["What's new in v3"](whats-new-in-v3.md) to get an idea of what has changed before making the upgrade.
### Upgrade Saloon's Packages
First of all, you should bump Saloon to the next major version in your `composer.json` file. You should also update any of the following plugins if you have them installed.&#x20;
* `saloonphp/saloon` to `^3.0`
* `saloonphp/laravel-plugin` to `^3.0`
* `saloonphp/cache-plugin` to `^3.0`&#x20;
* `saloonphp/laravel-http-sender` to `^2.0`
* `saloonphp/rate-limit-plugin` to `^2.0`
* `saloonphp/pagination-plugin` to `^2.0`
After that, make sure to run the following command to update your `composer.lock` file and your `vendor` directory.&#x20;
```bash
composer update "saloonphp/*"
```
{% hint style="info" %}
The above command will just update the `saloonphp` libraries. If you have other libraries using Saloon you should make sure that they are updated as well.&#x20;
{% endhint %}
### <mark style="color:red;">High</mark> Impact Changes
High-impact changes will most likely be things that everyone needs to update.
#### Minimum TLS 1.2 version enforced
From version three, Saloon uses Guzzle `^7.6` which introduced a new option to configure a minimum TLS version. With Saloon v3, Saloon has set this default to **TLS 1.2.** If any of your API integrations require a lower level of TLS security, you can change the Saloon config while your application is loading.
```php
<?php
use Saloon\Config;
Config::$defaultTlsMethod = STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
```
You can [visit this page on the PHP documentation](https://www.php.net/manual/en/function.stream-socket-enable-crypto.php) to see the different TLS options available.&#x20;
{% hint style="info" %}
**TLS 1.1** has been deprecated since 2021 so it is unlikely that APIs are still using it, but older systems may have not upgraded yet.
{% endhint %}
#### New Paginators
With version three, paginators are no longer included in the core Saloon library and have been moved to a plugin. Additionally, the paginators have been completely rebuilt. Review if you have used any of the pagination functionality and follow the new [pagination guide](../digging-deeper/pagination/) to rebuild them in the new way.
#### Config and MockConfig classes have moved
Previously, Saloon's `Config` and `MockConfig` classes lived in a `Saloon\Helpers` directory. These classes have now been moved to the root `Saloon` namespace. You should update your `use` statements to the new namespace.
| Find | Replace |
| ------------------------------- | ----------------------- |
| `use Saloon\Helpers\Config` | `use Saloon\Config` |
| `use Saloon\Helpers\MockConfig` | `use Saloon\MockConfig` |
#### Core Interfaces Removed
Another decision that was made was to backtrack on the interfaces that were added in Saloon v2. This may not affect your integration, but a lot of the code examples and recommendations in v2 suggested using the contracts instead of the abstract classes, so you may have a few places in your application where you have imported the interface and not the class. You should find and replace the following main interfaces.
<table><thead><tr><th width="394">Find</th><th>Replace</th></tr></thead><tbody><tr><td><code>use Saloon\Contracts\Connector</code></td><td><code>use Saloon\Http\Connector</code></td></tr><tr><td><code>use Saloon\Contracts\Request</code></td><td><code>use Saloon\Http\Request</code></td></tr><tr><td><code>use Saloon\Contracts\PendingRequest</code></td><td><code>use Saloon\Http\PendingRequest</code></td></tr><tr><td><code>use Saloon\Contracts\Response</code></td><td><code>use Saloon\Http\Response</code></td></tr></tbody></table>
{% hint style="info" %}
There have been many other interfaces removed in v3 however it's unlikely that your application would have used them. They are listed below.
{% endhint %}
#### HasBody Trait Renamed to HasStringBody
During some of the code review to improve readability, the `HasBody` body trait has been renamed to `HasStringBody` to be more explicit. If any of your requests send a **plain string body**, you should find and replace this trait.
<table><thead><tr><th width="349">Find</th><th>Replace</th></tr></thead><tbody><tr><td><code>use Saloon\Traits\Body\HasBody</code></td><td><code>use Saloon\Traits\Body\HasStringBody</code></td></tr><tr><td><code>use HasBody</code></td><td><code>use HasStringBody</code></td></tr></tbody></table>
### <mark style="color:orange;">Medium</mark> Impact Changes
Medium-impact changes are changes that come up for some people but not others.
#### SendAndRetry method argument name changes
The second argument of the `$connector->sendAndRetry()` method has been renamed from `maxAttempts` to `tries`. If you are using named arguments then you will need to replace this name.
<pre class="language-php"><code class="lang-php">$connector->sendAndRetry(
request: $request,
<strong> tries: 5,
</strong>);
</code></pre>
#### SendAndRetry handler now expects a Request class and not PendingRequest
If you previously used the `handleRetry` argument when using `$connector->sendAndRetry()` - Saloon will now send an instance of `Request` instead of `PendingRequest` through the closure.
<pre class="language-php"><code class="lang-php">$connector->sendAndRetry(
request: $request,
tries: 5,
interval: 0,
<strong> handleRetry: static function (Throwable $exception, Request $request) {
</strong> // $request->headers()->add();
},
);
</code></pre>
#### Config Method Changes
With version three, some of the methods in the `Saloon\Config` class have been renamed. These changes have been listed below and you should make sure to rename them if you have used them in your application.
| Find | Replace |
| ----------------------------- | --------------------------------- |
| `Config::middleware()` | `Config::globalMiddleware()` |
| `Config::resetMiddleware()` | `Config::clearGlobalMiddleware()` |
| `Config::resolveSenderWith()` | `Config::setSenderResolver()` |
| `Config::setDefaultSender()` | `Config::$defaultSender =` |
### <mark style="color:blue;">Low</mark> Impact Changes
Low-impact changes are changes that are unlikely to come up unless you have overwritten some of Saloon's functionality or used its interfaces. It's still worth looking through to make sure you haven't missed anything.
#### Methods Renamed On MockResponse
On the `MockResponse` class Saloon had a few methods which have now been renamed to be more consistent with the rest of Saloon. These changes are:
* `getBody()` has changed to `body()`
* `getHeaders()` has changed to `headers()`
* `getStatus()` has changed to `status()`
#### Middleware Arguments Changed
Saloon v3 introduced the ability to reorder middleware. With this, the old `prepend` argument on middleware methods has been replaced with a `pipeOrder` argument. Additionally, the second argument for the middleware is now `name` .&#x20;
#### Methods Removed From PendingRequest Class
The following methods have now been removed from the `PendingRequest` class. You can use the suggested replacement if you are using them in your application.
<table><thead><tr><th width="325">Old</th><th>Suggested Replacement</th></tr></thead><tbody><tr><td><code>$pendingRequest->send()</code></td><td><code>$pendingRequest->getConnector()->send()</code></td></tr><tr><td><code>$pendingRequest->sendAsync()</code></td><td><code>$pendingRequest->getConnector()->sendAsync()</code></td></tr><tr><td><code>$pendingRequest->getSender()</code></td><td><code>$pendingRequest->getConnector()->sender()</code></td></tr></tbody></table>
#### Promise Execution Changes
Previously, Saloon's `PendingRequest` and middleware would be invoked before an asynchronous request was sent (using `$connector->sendAsync()`) - now Saloon has fixed this issue and the `PendingRequest` and middleware will only be invoked while the `Promise` is being resolved - at the same time as sending the request. If your application depended on the old way the middleware was invoked you should make sure your application isn't affected by this change.
#### SimulatedResponsePayload Class Renamed
Previously, the `MockResponse` would extend a class called `SimulatedResponsePayload`. This class has now been renamed to `FakeResponse`. Additionally, methods on the `PendingRequest` like `$pendingRequest->setSimulatedResponsePayload()` has been renamed to `$pendingRequest->setFakeResponse()`. If you were extending the old class or using the methods to set the fake response in middleware, you should rename these methods.
#### Sender Contract Updated
The `Sender` interface has been completely rewritten in Saloon v3. Instead of just the `sendRequest` method. You now have to define three methods. If you have made a custom sender in Saloon, you should make sure this is updated.
```php
<?php
interface Sender
{
/**
* Get the factory collection
*/
public function getFactoryCollection(): FactoryCollection;
/**
* Send the request synchronously
*/
public function send(PendingRequest $pendingRequest): Response;
/**
* Send the request asynchronously
*/
public function sendAsync(PendingRequest $pendingRequest): PromiseInterface;
}
```
#### Date Helper Removed
Saloon v2 shipped with a simple `Date` helper class that wrapped around PHP's `DateTime` classes. This has now been removed in Saloon v3.
#### Other Helpers Renamed And Made Final
Saloon v2 shipped with a few helpers to reduce dependencies. The `Str` and `Arr` helper methods have been renamed to `StringHelpers` and `ArrayHelpers` respectively to prevent accidental imports when using Laravel. Saloon's internal helper classes have also been declared `final` as these shouldn't be extended or used by your application.
#### Other Interfaces (Contracts) Removed
As mentioned above, Saloon v3 removes a lot of interfaces that weren't adding value to the library. Alongside the ones mentioned above, these other interfaces have been removed.
<details>
<summary>Show Removed Interfaces</summary>
* src/Contracts/Arrayable.php
* src/Contracts/Authenticatable.php
* src/Contracts/CanThrowRequestExceptions.php
* src/Contracts/Connector.php
* src/Contracts/DebuggingDriver.php
* src/Contracts/Dispatcher.php
* src/Contracts/HasConfig.php
* src/Contracts/HasDebugging.php
* src/Contracts/HasDelay.php
* src/Contracts/HasHeaders.php
* src/Contracts/HasMiddlewarePipeline.php
* src/Contracts/HasMockClient.php
* src/Contracts/HasPagination.php
* src/Contracts/HasQueryParams.php
* src/Contracts/IntegerStore.php
* src/Contracts/MiddlewarePipeline.php
* src/Contracts/MockClient.php
* src/Contracts/Paginator.php
* src/Contracts/PendingRequest.php
* src/Contracts/Pipeline.php
* src/Contracts/Pool.php
* src/Contracts/Request.php
* src/Contracts/Response.php
* src/Contracts/SimulatedResponsePayload.php
</details>
#### The Base BodyRepository Class Is No Longer Stringable
If you have made your own `BodyRepository` class from the base class and relied on the `__toString` method this has now been removed. Additionally, the interface has been updated and now requires a `toStream()` method. Inside here, you must define how to convert your body type into a Stream using the provided `StreamFactory`.
#### Timeout Enum Removed
The old `Timeout` enum has now been removed. If your application used to use this method, you should migrate to use `Config::$defaultConnectionTimeout` and `Config::$defaultRequestTimeout`.
#### ToArray Method Removed From MultipartValue
Previously, the `MultipartValue` object had a `toArray` method. Now that all body repositories are converted into Streams, this method is no longer required.
#### SimulatedSender Removed
The `SimulatedSender` class which would be used if you were mocking or had a cached response has now been removed.
### Did we miss anything?&#x20;
If we forgot to add something to this upgrade please let us know by [opening an issue](https://github.com/saloonphp/saloon/issues).&#x20;
]]>
</upgrading_from_v2_md>
<whats_new_in_v3_md>
<![CDATA[
# 🪄 What's new in v3
We are excited to announce the release of Saloon version three, which is designed to provide an even better developer experience than before. This version comes with numerous internal changes that enhance maintainability, performance, and code readability. We have taken into account the feedback we received from users of Saloon v2 and made intentional modifications to improve your experience.
To begin it's worth mentioning that Saloon has become a lot more lightweight in version three!
> **333 changed files** with <mark style="color:green;">**4,063 additions**</mark> and <mark style="color:red;">**7,576 deletions**</mark>
### Changes in version three
#### Improved Pagination
One of the biggest changes is how pagination in Saloon works. After seeing a few people use it - we felt that the developer experience could be better, so we revisited the whole feature. With Saloon v3, it no longer comes bundled into Saloon which reduces code but the pagination plugin provides a beautiful, expressive way to build and customise paginators.
One of the motivations for rebuilding the pagination was customisation. Previously, you would have to extend the paginator class you are already using and guess which methods needed to be changed. The new pagination has been massively simplified and each of the paginators use the same methods to apply their pagination. This makes them much easier to build and maintain. Here is how the pagination definition looks in version three:
```php
<?php
class SpotifyConnector extends Connector implements HasPagination
{
// ...
public function paginate(Request $request): PagedPaginator
{
// You define your own pagination class that extends one of Saloon's
// base pagination classes. This makes overwriting super easy.
return new class(connector: $this, request: $request) extends PagedPaginator
{
// Determine if we are on the last page...
protected function isLastPage(Response $response): bool
{
return is_null($response->json('next_page_url'));
}
// Get page items...
protected function getPageItems(Response $response, Request $request): array
{
return $response->json('items');
}
};
}
}
```
#### PSR Support
Another important change to Saloon v3 was to lean more into the building and use of PSR-7 requests. Previously, Saloon would use Guzzle's configuration options to build up query parameters, request body and the URI. Now, Saloon builds all PSR requests in-house and passes that request to the sender. This provides a few key benefits over letting Guzzle handle it:
* **Better developer experience** - with less hidden "magic" going on behind the scenes, you'll be able to debug and dump the PSR request Saloon creates to see how your request will be sent and what you're sending.
* **Decoupled further from Guzzle** - Saloon now only uses Guzzle to send your requests which means Saloon is much less dependent on Guzzle.
* **Lower-level customization** - You will be able to modify the PSR-7 request before it is sent, which gives you much better control than before.
* **New PSR methods -** You will now be able to create a PSR-7 request from a `PendingRequest` class.
{% hint style="info" %}
This is an internal change only, and you won't need to change how your implementation works. You'll still use connectors and requests.
{% endhint %}
#### Global Retry System
Another really exciting feature in version three is the ability to configure retrying functionality at a connector level for all of your requests. Previously, the only way to configure the retry functionality was to do it in-line like this:
```php
$response = $connector->sendAndRetry($request, 5);
```
This worked well, but it doesn't really follow one of Saloon's core values - to have beautiful, reusable code. Now you can define retrying at a class level. You just have to add the `$tries` property to either your connector or request to get it to work. After this, if you use `$connector->send()` and the request fails, it will be retried automatically. This is especially useful for SDK development or building against unstable APIs that need retrying at a connector level.
```php
class ForgeConnector extends Connector
{
/**
* The number of times a request should be retried if a failure response is returned.
*
* Set to null to disable the retry functionality.
*/
public ?int $tries = 5;
}
```
#### New Response Body Methods
Saloon version three has a few new methods for the `Response` class to make it even easier to use in your application.
* **saveBodyToFile($pathOrResource)** - This method allows you to save the raw body straight to a file or into an open file resource while keeping memory usage low.
* **getRawStream()** - This method is similar to the existing `stream()` method but will return a file `resource` instead of a `StreamInterface`. This method is really useful when passing the stream into other libraries like League's Flysystem library.
* **getConnector()** - This method is a nice shortcut to get the connector from the response.
* **getPsrRequest()** - This method gives you the PSR request that was built when sending the request - which is the lowest level request class before going through the HTTP client.
#### Removed Interfaces For Maintainability
With Saloon v2 - almost every class had an interface that acted as a blueprint for how the class should be structured. This was a decision made in version two to help make Saloon as flexible as possible. With version three, the decision was made to go back on this flexibility. While the added flexibility of giving the developer the freedom to make their own implementations - it added unnecessary complication and blocked Saloon from nice features and methods that could have been added before version three.
We believe that most Saloon users would just use Saloon's implementation and adequate customization can be achieved with inheritance.&#x20;
Many interfaces (contracts) were removed with version three but some of the major ones include:
* **Saloon\Contracts\Connector**
* **Saloon\Contracts\Request**
* **Saloon\Contracts\PendingRequest**
* **Saloon\Contracts\Response**
The abstract classes still exist, but without the interfaces - the maintainability of Saloon v3 has improved massively. This means that the current version can be maintained for longer and new features can be released faster and without major version changes.
#### Minimum TLS 1.2 Security
Saloon version three requires Guzzle `^7.6` which introduced support for a minimum TLS version. Since TLS 1.1 has been deprecated for almost three years, Saloon has made the minimum version **TLS 1.2**. This is to promote more secure API integrations and follow best practices however it can be changed using configuration if required.
#### Better Middleware Order
The previous version introduced support for middleware in Saloon and allowed users to add their own middleware. This also introduced some challenges with how some of Saloon's other features worked like mocking and authenticators. Often when the user adds their own middleware, they may not see headers from authenticators or mock clients being applied to the `PendingRequest` class. Saloon v3's middleware order has been improved for the best feature compatibility.
If you are curious, this is how the new middleware order looks:
1. Plugins are "booted"
2. Request and connector properties (Headers, Query Parameters, Config, Middleware) are merged
3. Request and connector body is merged
4. Request and connector delay is merged
5. Authenticators are invoked and their logic applied to the `PendingRequest`
6. Request and connector is "booted"
7. Headers are validated
8. Delay is invoked (if applied)
9. Middleware is run in the following order:
1. Global middleware
2. Mock client finds a fake response (if present)
3. Plugin middleware
4. **User-added middleware**
5. Debugging middleware is run (for the final object)
#### Simplified Debugging
Saloon v2 shipped with some useful debugging functionality. It came with numerous drivers for different outputs and worked nicely. With version three, it was decided that debugging should be simpler. From version three, if you need to debug your request or response, you can now use the `debug` method and pass in a closure to see either the `PendingRequest` , PSR request or `Response` class while it is being sent.&#x20;
Additionally, changes were made to the middleware pipeline to allow the debugging middleware to always run at the end of the pipeline, ensuring you'll always see the final result.
```php
$connector->debugRequest(static function (PendingRequest $pendingRequest, Requestinterface $psrRequest) {
dd($pendingRequest->headers());
});
$connector->debugResponse(static function (Response $response) {
dd($response->body());
});
```
#### Better Asynchronous Promise Handling
With Saloon v3, promises are handled better. Previously, the `PendingRequest` class would be built up, and middleware run and then the `Promise` was created. With version three, a promise is created straight away and the `PendingRequest` and middleware is only invoked when the promise is being sent. This is good for plugins that need to run when a request is being sent like the rate limit plugin.
#### SDK Helpers
Last, but not least - Saloon v3 has shipped with a `BaseResource` class which is a class that was recommended in the [Building SDKs](../the-basics/building-sdks.md#request-resources) chapter of the documentation. Since this class is only small and provides value to the SDK developer, this class has now been bundled.
]]>
</whats_new_in_v3_md>
</upgrade>
</laravel_api_calls_saloon_lib_docs>