
<?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();
}
}
```
]]>
<?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
<?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.
]]>
<?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 %}
]]>
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,
);
```
]]>
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);
]]>
--oauth option if you would like to create an OAuth2 connector..png)
<?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.
.png)

Introducing Saloon v3
<?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.
]]>
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);
```
]]>
<?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();
```
]]>
<?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
| Method | Description |
|---|---|
status | Returns the response status code. |
headers | Returns all response headers |
header | Returns a given header |
body | Returns the raw response body as a string |
json | Retrieves a JSON response body and json_decodes it into an array. |
array | Alias of json |
collect | Retrieves a JSON response body and json_decodes it into a Laravel Collection. Requires illuminate/collections. |
object | Retrieves a JSON response body and json_decodes it into an object. |
xmlReader | Used for XML responses - returns a XML Wrangler reader. Requires saloonphp/xml-wrangler. |
dom | Used for HTML responses - returns a Symfony DOM Crawler instance. Requires symfony/dom-crawler. |
stream | Returns the response body as an instance of StreamInterface |
saveBodyToFile | Allows you to save the raw body to a file or open file resource. |
dto | Converts the response into a data-transfer object. You must define your DTO first, click here to read more. |
dtoOrFail | Will work just like dto but will throw an exception if the response is considered "failed". |
ok, successful, redirect, failed, clientError, serverError | Methods used to determine if a request was successful or not based on status code. The failed method can be customised. |
throw | Will throw an exception if the response is considered "failed". |
getPendingRequest | Returns the PendingRequest class that was built up for the request. |
getPsrRequest | Returns the PSR-7 request that was built up by Saloon |
getPsrResponse | Return the PSR-7 response that was built up by the HTTP client/sender. |
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 %}
]]>
<?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),
]);
```
]]>
use Saloon\Contracts\Connectoruse Saloon\Http\Connectoruse Saloon\Contracts\Requestuse Saloon\Http\Requestuse Saloon\Contracts\PendingRequestuse Saloon\Http\PendingRequestuse Saloon\Contracts\Responseuse Saloon\Http\Response| Find | Replace |
|---|---|
use Saloon\Traits\Body\HasBody | use Saloon\Traits\Body\HasStringBody |
use HasBody | use HasStringBody |
$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.
| Old | Suggested Replacement |
|---|---|
$pendingRequest->send() | $pendingRequest->getConnector()->send() |
$pendingRequest->sendAsync() | $pendingRequest->getConnector()->sendAsync() |
$pendingRequest->getSender() | $pendingRequest->getConnector()->sender() |