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 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 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 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. ]]> - This page contains a collection of useful start-to-finish guides for doing specific things in Saloon. --- # πŸ“” How-to Guides ]]> 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. {% 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 %} ]]>
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) ]]>
🚨 ohdearapp/ohdear-php-sdk 1 https://github.com/ohdearapp/ohdear-php-sdk 🌲 astrotomic/ecologi-sdk 1 https://github.com/Astrotomic/ecologi-sdk πŸͺ astrotomic/tmdb-sdk 2 https://github.com/Astrotomic/tmdb-sdk πŸ’° myoutdeskllc/salesforce-php 3 https://github.com/MyOutDeskLLC/SalesforcePhp πŸ€‘ sawirricardo/midtrans-api https://github.com/sawirricardo/midtrans-api πŸ“ƒ carboneio/carbone-sdk-php https://github.com/carboneio/carbone-sdk-php πŸ“ž samuelmwangiw/africastalking-laravel https://github.com/SamuelMwangiW/africastalking-laravel πŸ’» samuelmwangiw/linode https://github.com/SamuelMwangiW/linode 🌱 alphaolomi/json-placeholder https://github.com/alphaolomi/json-placeholder πŸ’¬ flourishlabs/saloon-slack https://github.com/flourishlabs/saloon-slack πŸ“¦ alphaolomi/laravel-repman https://github.com/alphaolomi/laravel-repman πŸ“Š njoguamos/laravel-plausible https://github.com/njoguamos/laravel-plausible 🧠 probots-io/pinecone-php https://github.com/probots-io/pinecone-php πŸ‡¨πŸ‡΄ felipeva/api-colombia-php https://github.com/felipeva/api-colombia-php 🀝 deschutesdesigngroupllc/perscom-php-sdk https://github.com/DeschutesDesignGroupLLC/perscom-php-sdk ✈️ noahnxt/laravel-open-holiday-api https://github.com/NoahNxT/laravel-open-holidays-api πŸ’° njoguamos/laravel-pesapal https://github.com/njoguamos/laravel-pesapal πŸ”– oneduo/github-sdk-php https://github.com/oneduo/github-sdk-php πŸ’³ lloricode/paymaya-sdk-php https://github.com/lloricode/paymaya-sdk-php ]]>
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 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 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 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 pool($callback); // or $pool->setRequests($callback); ``` {% endtab %} {% tab title="Invokable Class" %} ```php 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 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 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 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 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 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(); ``` ]]> delay()->set(500); ``` {% endtab %} {% tab title="Request" %} ```php 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. {% endhint %} ]]> headers()->add('X-Current-Time', new DateTime); } } ``` {% endtab %} {% tab title="Request" %} ```php 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 middleware()->onRequest(function (PendingRequest $pendingRequest) { $pendingRequest->headers()->add('Authorization', 'Bearer ' . $token); }); ``` {% endtab %} {% tab title="Request" %} ```php 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 headers()->add('Authorization', 'Bearer ' . $token); } } ``` {% endtab %} {% tab title="Usage (Connector)" %} ```php middleware()->onRequest(new AuthenticateRequest); ``` {% endtab %} {% tab title="Usage (Request)" %} ```php 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 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 middleware()->onResponse(function (Response $response) { Logger::recordResponse($response); }); ``` {% endtab %} {% tab title="Request" %} ```php 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 middleware()->onResponse(new LogResponse); ``` {% endtab %} {% tab title="Usage (Request)" %} ```php 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 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 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 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`. 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.
<?php

use Saloon\Http\Request;
use Saloon\Http\PendingRequest;
use Saloon\Enums\PipeOrder;

class GetServersRequest extends Request
{
    // { ... }
    
    public function boot(PendingRequest $pendingRequest): void
    {
        $request->middleware()->onRequest(new RecordResponse, order: PipeOrder::LAST);
    }
}
### 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 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 sender()->getGuzzleClient(); $handlerStack = $this->sender()->getHandlerStack(); } } ``` ]]>
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 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. In the following example, I will pass in the `$clientId` and the `$clientSecret` as constructor arguments and overwrite the OAuth2 config. ```php 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. ```php 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 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 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 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 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 getAccessToken(requestModifier: function (Request $request) { $request->query()->add('access_type', 'offline'); }); ``` {% endtab %} {% tab title="All Requests" %}
<?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')
        ->setRequestModifier(function (GetClientCredentialsTokenRequest $request) {
            //
        }),
}
{% 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
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 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.
<?php

use Saloon\Http\Connector;
use Saloon\Traits\OAuth2\AuthorizationCodeGrant;

class SpotifyConnector extends Connector
{
    use AuthorizationCodeGrant;

    public function __construct(string $clientId, string $clientSecret)
    {
        $this->oauthConfig()->setClientId($clientId);
        $this->oauthConfig()->setClientSecret($clientSecret);
    }

    // ...
}
### 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 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.
<?php

$connector = new SpotifyConnector;
$authorizationUrl = $connector->getAuthorizationUrl();

$state = $authConnector->getState(); // '8484b43fdjfdnfdj3llls...'
### 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.
<?php

$connector = new SpotifyConnector;
$authenticator = $authConnector->getAccessToken($code);

// 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);
{% 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 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 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 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 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 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 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 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 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) ]]>
<?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 { // }; } } {% 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.
<?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
        {
            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');
            }
        };
    }
}
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.
public function paginate(Request $request): CursorPaginator
{
    return new class(connector: $this, request: $request) extends CursorPaginator
    {
        protected ?int $perPageLimit = 100;
        
        // ...
    }
}
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) { ... } ``` ### **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.
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) {
                $request->query()->add('pageCursor', $this->getNextCursor($this->currentResponse));
            }
    
            if (isset($this->perPageLimit)) {
                $request->query()->add('pageSize', $this->perPageLimit);
            }
    
            return $request;
        }
    };
}
### **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. ]]>
<?php use Saloon\Http\Response; use Saloon\Http\Connector; use Saloon\PaginationPlugin\Paginator; 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 { // }; } } {% 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.
<?php

use Saloon\Http\Response;
use Saloon\Http\Connector;
use Saloon\PaginationPlugin\Paginator;
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
            {
                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
            {
                $request->headers()->add('X-Page', $this->currentPage);

                $request->headers()->add('X-Per-Page', $this->perPageLimit);

                return $request;
            }
        };
    }
}
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.
public function paginate(Request $request): Paginator
{
    return new class(connector: $this, request: $request) extends Paginator
    {
        protected ?int $perPageLimit = 100;

        // ...
    };
}
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. ]]>
<?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 { // }; } } {% 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.
<?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
        {
            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');
            }
        };
    }
}
First, we'll define a `perPageLimit` of 100. This means that every request will attempt to use a "limit" of 100. 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.** ```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.
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');
        }
        
        protected function applyPagination(Request $request): Request
        {
            $request->query()->merge([
                'top' => $this->perPageLimit,
                'skip' => $this->getOffset(),
            ]);
    
            return $request;
        }
    };
}
{% 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. ]]>
<?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 { // }; } } {% 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 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. 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.
public function paginate(Request $request): PagedPaginator
{
    return new class(connector: $this, request: $request) extends PagedPaginator
    {
        protected ?int $perPageLimit = 100;
        
        // ...
    }
}
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) { ... } ``` ### **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.
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
        {
            $request->query()->add('currentPage', $this->currentPage);
    
            if (isset($this->perPageLimit)) {
                $request->query()->add('pageSize', $this->perPageLimit);
            }
    
            return $request;
        }
    };
}
### **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. ]]>
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.
<?php

use Saloon\Http\Request;
use Saloon\PaginationPlugin\Contracts\Paginatable;

class GetLikedSongsRequest extends Request implements Paginatable
{
    // ...
}
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.
<?php

use Saloon\Http\Connector;
use Saloon\PaginationPlugin\Paginator;
use Saloon\PaginationPlugin\Contracts\HasPagination;

class SpotifyConnector extends Connector implements HasPagination
{
    // ...
    
    public function paginate(Request $request): Paginator
    {
        //
    }
}
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 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 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 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 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.
<?php

$spotifyConnector = new SpotifyConnector;

$paginator = $spotifyConnector->paginate(new GetLikedSongsRequest);

$collection = $paginator->collect(throughItems: false);
{% 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" %}
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
            {
                return $response->dto();
            }
        }
    }
}
{% endtab %} {% tab title="Request" %}
<?php

class GetLikedSongsRequest extends Request
{
    // {...}
    
    public function createDtoFromResponse(Response $response): array
    {
        return array_map(function (array $song) {
            return Song::fromArray($song);
        }, $response->json('items'));
    }
}
{% 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.
<?php

class SpotifyConnector extends Connector implements HasPagination
{
    public function paginate(Request $request): PagedPaginator
    {
        return new class(connector: $this, request: $request) extends PagedPaginator
        {
            // ...
            
            protected function getTotalPages(Response $response): int
            {
                return $response->json('total_results');
            }
        }
    }
}
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.
<?php

$spotifyConnector = new SpotifyConnector;

$paginator = $spotifyConnector->paginate(new GetLikedSongsRequest);

$paginator->async();

foreach($paginator as $promise) {
    // $promise->then()
}
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.
<?php

$spotifyConnector = new SpotifyConnector;

$paginator = $spotifyConnector->paginate(new GetLikedSongsRequest);

$pool = $paginator->pool(concurrency: 10);

$pool->withResponseHandler(function (Response $response) { 
    // Handle each response

    $status = $response->status();
    $data = $response->body(); 
});

$promise = $pool->send();

$promise->wait();
#### 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 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.
<?php

$spotifyConnector = new SpotifyConnector;
$request = new GetLikedSongsRequest;

$paginator = $request->paginate($spotifyConnector);
#### 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.
<?php

$spotifyConnector = new SpotifyConnector;

$paginator = $spotifyConnector->paginate(new GetLikedSongsRequest);

$pages = count($paginator);
{% 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.
public function paginate(Connector $connector): Paginator;
{
    return new class(connector: $connector, request: $this) extends PagedPaginator
    {
        protected bool $detectInfiniteLoop = false;
    }
}
{% hint style="warning" %} Detecting infinite loops with asynchronous pagination is not supported. {% endhint %} ]]>
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. ```php 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 %} ]]> class ForgeConnector extends Connector { public ?int $tries = 3; } 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.
class ForgeConnector extends Connector
{
    public ?int $tries = 3;
    
    public ?int $retryInterval = 1000;
}
#### 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.
class ForgeConnector extends Connector
{
    public ?int $tries = 3;
    
    public ?int $retryInterval = 500;
    
    public ?bool $useExponentialBackoff = true;
}
### 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.
class ForgeConnector extends Connector
{
    public ?int $tries = 3;
    
    public ?bool $throwOnMaxTries = false;
}
### 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.
<?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 && $exception->getResponse()->status() === 401) {
            $request->authenticate(new TokenAuthenticator($this->getNewToken()));
        }
        
        return true;
    }
}
### 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 sendAndRetry( request: new UnreliableRequest, tries: 3, interval: 500, handleRetry: function (FatalRequestException|RequestException $exception, Request $request) { // }, throw: true, useExponentialBackoff: true, ); ``` ]]>
send(); ``` ]]>
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 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 disableCaching(); $response = $forge->send($request); ``` ### Source Code To view the source code of this plugin, [click here](https://github.com/Sammyjo20/saloon-cache-plugin). ]]> 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.
use Saloon\Http\Connector;
use Saloon\RateLimitPlugin\Stores\MemoryStore;
use Saloon\RateLimitPlugin\Traits\HasRateLimits;

class SpotifyConnector extends Connector
{
    use HasRateLimits;
    
    public function __construct()
    {
        $this->detectTooManyAttempts = false;
    }
}
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
use Saloon\Http\Connector;
use Saloon\RateLimitPlugin\Stores\MemoryStore;
use Saloon\RateLimitPlugin\Traits\HasRateLimits;

class SpotifyConnector extends Connector
{
    use HasRateLimits;
    
    protected function getTooManyAttemptsLimiter(): ?Limit
    {
        return Limit::custom($this->handleTooManyAttempts(...))->sleep();
    }
}
{% 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.
use Saloon\Http\Connector;
use Saloon\RateLimitPlugin\Traits\HasRateLimits;

class SpotifyConnector extends Connector
{
    use HasRateLimits;
    
    public function __construct()
    {
        $this->rateLimitingEnabled = false;
    }
}

// Or...

$connector = new SpotifyConnector;
$connector->useRateLimitPlugin(false);
]]>
CommandDescriptionphp artisan saloon:connectorCreates a new connector - You can provide an optional --oauth option if you would like to create an OAuth2 connector.php artisan saloon:requestCreates a new requestphp artisan saloon:responseCreates a custom responsephp artisan saloon:pluginCreates a pluginphp artisan saloon:authCreates a custom authenticatorphp artisan saloon:listList all your API integrations ### 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 app->register(SaloonServiceProvider::class); } ``` ]]> 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. ]]>
### 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. ]]>
<?php trait HasLogging { public function bootHasLogging(PendingRequest $pendingRequest): void { ray($pendingRequest); } } {% 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)" %}
<?php

class ForgeConnector extends Connector
{
    use HasLogging;
}
{% 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.
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. {% hint style="warning" %} Take caution when adding the plugin to both the connector and the request, because the plugin will be executed twice. {% endhint %} ]]>
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 Belgian Waffles $5.95 Two of our famous Belgian Waffles with plenty of real maple syrup 650 Strawberry Belgian Waffles $7.95 Light Belgian waffles covered with strawberries and whipped cream 900 Berry-Berry Belgian Waffles $8.95 Light Belgian waffles covered with an assortment of fresh berries and whipped cream 900 ``` {% 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 write('root', [ 'ubuntu-version' => $this->ubuntuVersion, 'type' => $this->type, 'provider' => $this->provider, ]); // Same as... // // // ubuntuVersion // type // provider // } } ``` ]]>

Introducing Saloon v3

### 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 %} ]]>
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 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 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 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 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 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 headers()->add(...); // $pendingRequest->config()->add(...); } } ``` ```php 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 %} ]]> '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 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 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.
<?php

use Saloon\Http\BaseResource;
use Saloon\Http\Response;

class PokemonResource extends BaseResource
{
     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));
     }
}
#### 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 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. ]]>
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.
class ForgeConnector extends Connector
{
    // ...

    protected function defaultHeaders(): array
    {
        return [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ];
    }
}
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); ``` ]]>
json(); return new Server( id: $data['id'], name: $data['name'], ipAddress: $data['ip'], ); } } ``` This is what the `Server` DTO looks like: ```php 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" %}
<?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
    {
        return '/servers/' . $this->server->id;
    }
    
    protected function defaultBody(): array
    {
        return [
            'name' => $this->server->name,
        ];
    }
    
    public function createDtoFromResponse(Response $response): mixed
    {
        $data = $response->json();
    
        return new Server(
            id: $data['id'],
            name: $data['name'],
            ipAddress: $data['ip'],
        );
    }
}
{% endtab %} {% tab title="Usage" %} ```php 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.
<?php

use Saloon\Http\Response;
use Saloon\Traits\Responses\HasResponse;
use Saloon\Contracts\DataObjects\WithResponse;

class Server implements WithResponse
{
    use HasResponse;

    public function __construct(
        public readonly int $id,
        public readonly string $name,
        public readonly string $ipAddress,
    ){}
}
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 dto(); $response = $server->getResponse(); ``` ]]>
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 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 debugRequest(function (PendingRequest $pendingRequest, RequestInterface $psrRequest) { ray($psrRequest); }); $connector->debugResponse(function (Response $response, ResponseInterface $psrResponse) { ray($psrResponse); }); ``` ]]> 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 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 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" %}
<?php

use Saloon\Http\Connector;
use Saloon\Http\Response;

class ForgeConnector extends Connector
{
    // {...}
    
    public function hasRequestFailed(Response $response): ?bool
    {
        return str_contains($response->body(), 'Server Error');
    }
}
{% endtab %} {% tab title="Request" %}
<?php

use Saloon\Http\Request;
use Saloon\Http\Response;

class ErrorRequest extends Request
{
    // {...}
    
    public function hasRequestFailed(Response $response): ?bool
    {
        return str_contains($response->body(), 'Server Error');
    }
}
{% 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\Request; use Saloon\Contracts\Body\HasBody; class CreateServerRequest extends Request implements HasBody { protected Method $method = Method::POST; } 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.
<?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;
}
{% 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. ```php $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 payload; } } ``` {% endtab %} {% tab title="Using a DTO (Data Transfer Object)" %}
<?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 Server $server
    ){}
    
    protected function defaultBody(): array
    {
        return [
            'ubuntu_version' => $this->server->ubuntuVersion,
            'type' => $this->server->type,
            'provider' => $this->server->provider,
        ];
    }
}
{% 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 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 'Sam', ]; } } ``` ]]>
<?php use Saloon\Http\Request; use Saloon\Contracts\Body\HasBody; use Saloon\Enums\Method; class CreateServerRequest extends Request implements HasBody { protected Method $method = Method::POST; } 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.
<?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;
}
{% 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 $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 payload; } } ``` {% endtab %} {% tab title="Using a DTO (Data Transfer Object)" %}
<?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 Server $server
    ){}
    
    protected function defaultBody(): array
    {
        return [
            'ubuntu_version' => $this->server->ubuntuVersion,
            'type' => $this->server->type,
            'provider' => $this->server->provider,
        ];
    }
}
{% 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 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 '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 body()->setJsonFlags(JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR); } } ``` ]]>
<?php use Saloon\Http\Request; use Saloon\Contracts\Body\HasBody; class UploadProfilePictureRequest extends Request implements HasBody { protected Method $method = Method::POST; } 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.
<?php

use Saloon\Http\Request;
use Saloon\Contracts\Body\HasBody;
use Saloon\Traits\Body\HasMultipartBody;

class UploadProfilePictureRequest extends Request implements HasBody
{
    use HasMultipartBody;

    protected Method $method = Method::POST;
}
{% 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 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 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\Request; use Saloon\Contracts\Body\HasBody; class CreateServerRequest extends Request implements HasBody { protected Method $method = Method::POST; } 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 %} ]]> <?php use Saloon\Http\Request; use Saloon\Contracts\Body\HasBody; class CreateServerRequest extends Request implements HasBody { protected Method $method = Method::POST; } 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.
<?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;
    
    // Define our own Content-Type...
    
    protected function defaultHeaders(): array
    {
        return [
            'Content-Type' => 'text/plain',
        ];
    }
}
{% 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. ```php 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 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; class CreateServerRequest extends Request implements HasBody { protected Method $method = Method::POST; } 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.
<?php

use Saloon\Http\Request;
use Saloon\Contracts\Body\HasBody;
use Saloon\Traits\Body\HasStringBody;

class CreateServerRequest extends Request implements HasBody
{
    use HasStringBody;

    protected Method $method = Method::POST;
    
    // Define our own Content-Type...
    
    protected function defaultHeaders(): array
    {
        return [
            'Content-Type' => 'text/plain',
        ];
    }
}
{% 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.
<?php

use Saloon\Http\Request;
use Saloon\Contracts\Body\HasBody;
use Saloon\Traits\Body\HasStringBody;

class CreateServerRequest extends Request implements HasBody
{
    use HasStringBody;

    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!';
    }
}
### 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** * isEmpty(): bool -> **Check if the body is empty** * isNotEmpty(): bool -> **Check if the body is not empty** ```php 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; class CreateServerRequest extends Request implements HasBody { protected Method $method = Method::POST; } 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.
<?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;
}
{% 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. ```php ' . $this->ubuntuVersion . ' ' . $this->type . ' ' . $this->provider . ' 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** * isEmpty(): bool -> **Check if the body is empty** * isNotEmpty(): bool -> **Check if the body is not empty** ```php body()->set(''); $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 '; } } ``` ### 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 %} ]]>
If you have installed the Laravel plugin, you can use the **php artisan saloon:request** command to create a request. ### 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'); ``` ]]> 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.
MethodDescription
statusReturns the response status code.
headersReturns all response headers
headerReturns a given header
bodyReturns the raw response body as a string
jsonRetrieves a JSON response body and json_decodes it into an array.
arrayAlias of json
collectRetrieves a JSON response body and json_decodes it into a Laravel Collection. Requires illuminate/collections.
objectRetrieves a JSON response body and json_decodes it into an object.
xmlReaderUsed for XML responses - returns a XML Wrangler reader. Requires saloonphp/xml-wrangler.
domUsed for HTML responses - returns a Symfony DOM Crawler instance. Requires symfony/dom-crawler.
streamReturns the response body as an instance of StreamInterface
saveBodyToFileAllows you to save the raw body to a file or open file resource.
dtoConverts the response into a data-transfer object. You must define your DTO first, click here to read more.
dtoOrFailWill work just like dto but will throw an exception if the response is considered "failed".
ok, successful, redirect, failed, clientError, serverErrorMethods used to determine if a request was successful or not based on status code. The failed method can be customised.
throwWill throw an exception if the response is considered "failed".
getPendingRequestReturns the PendingRequest class that was built up for the request.
getPsrRequestReturns the PSR-7 request that was built up by Saloon
getPsrResponseReturn the PSR-7 response that was built up by the HTTP client/sender.
### 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 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" %}
class ForgeConnector extends Connector
{
    // ...
    
    protected ?string $response = CustomResponse::class;
}
{% endtab %} {% tab title="Request" %}
class GetServersRequest extends Request
{
    // ...
    
    protected ?string $response = CustomResponse::class;
}
{% 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 %} ]]>
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. ```php 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 %} ]]> 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. {% 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 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 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. 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.
<?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::fixture('servers'),
    ]);

    $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',
    ]);
});
#### 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.
<?php

test('a request sends the correct body', function () {
    $mockClient = new MockClient([
        CreateServerRequest::class => MockResponse::make('Server Created', 200),
    ]);

    $connector = new ForgeConnector;
    $connector->withMockClient($mockClient);
    
    $connector->send(new CreateServerRequest($name, $ip));
    
    $mockClient->assertSent(function (Request $request) {
        return $request->body()->all() === ['name' => $name, 'ip' => $ip];
    });
});
### 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 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. * 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.
<?php

namespace Tests\Fixtures\Forge\SingleServerFixture;

use Saloon\Http\Faking\Fixture;

class SingleServerFixture extends Fixture
{
    protected function defineName(): string
    {
        return 'forge/singleServer';
    }
    
    protected function defineSensitiveHeaders(): array
    {
        return [
            'Authorization' => 'REDACTED',
        ];
    }

    protected function defineSensitiveJsonParameters(): array
    {
        return [
            'name' => 'REDACTED',
            'password' => function () {
                return faker()->password;
            },
        ];
    }
    
    protected function defineSensitiveRegexPatterns(): array
    {
        return [
            '/@[a-z0-9_]{0,100}/' => 'REDACTED-TWITTER-HANDLE',
        ];
    } 
}
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.
<?php

$mockClient = new MockClient([
    GetServerRequest::class => new SingleServerFixture;
]);
### 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), ]); ``` ]]>
High 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 FindReplaceuse Saloon\Contracts\Connectoruse Saloon\Http\Connectoruse Saloon\Contracts\Requestuse Saloon\Http\Requestuse Saloon\Contracts\PendingRequestuse Saloon\Http\PendingRequestuse Saloon\Contracts\Responseuse Saloon\Http\Response {% 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.
FindReplace
use Saloon\Traits\Body\HasBodyuse Saloon\Traits\Body\HasStringBody
use HasBodyuse HasStringBody
### Medium 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.
$connector->sendAndRetry(
    request: $request,
    tries: 5,
);
#### 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.
$connector->sendAndRetry(
    request: $request,
    tries: 5,
    interval: 0,
    handleRetry: static function (Throwable $exception, Request $request) {
        // $request->headers()->add();
    },
);
#### 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 =` | ### Low 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` . #### 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.
OldSuggested Replacement
$pendingRequest->send()$pendingRequest->getConnector()->send()
$pendingRequest->sendAsync()$pendingRequest->getConnector()->sendAsync()
$pendingRequest->getSender()$pendingRequest->getConnector()->sender()
#### 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 Show Removed Interfaces * 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 #### 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? If we forgot to add something to this upgrade please let us know by [opening an issue](https://github.com/saloonphp/saloon/issues). ]]>
**333 changed files** with **4,063 additions** and **7,576 deletions** ### 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 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. 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. 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. ]]>