saloonphp / saloon Goto Github PK
View Code? Open in Web Editor NEW๐ค Build beautiful API integrations and SDKs with Saloon
Home Page: https://docs.saloon.dev
License: MIT License
๐ค Build beautiful API integrations and SDKs with Saloon
Home Page: https://docs.saloon.dev
License: MIT License
I think it would be useful to add various hooks into Saloon which can be added which can be useful for logging. For example I could add a hook before a request has happened, after a request has happened, after a successful response and after a failure response.
This could be added to the connector or the request (or both) and can be used.
There could also be a shouldSendRequest() which can return false if a provided condition fails
Just wanted to add an issue mentioning that I'm adding support for request caching in Saloon soon. It's going to use the popular Guzzle caching plugin https://github.com/Kevinrob/guzzle-cache-middleware but I'm hoping to extend it slightly.
This caching plugin will allow you to define explicit caching rules, like how long a response should be cached for.
This caching plugin will work automatically as it will use the caching headers (if provided) on the response.
Both plugins will allow you to purge the cache at any time and see if a SaloonResponse has been cached.
Hi! I have a problem with a Request, I am using HasFormBody trait for simulate this, with Laravel HTTP Client works!
$post = Http::asForm()->post('https://api.service.com/oauth2/token', [
'grant_type' => 'client_credentials',
'client_id' => '9bf9f97e-be08-11ed-afa1-0242ac120002',
'client_secret' => 'xf0f18ae1e45bac5e22246024b90a4af1f403d86202b290aef46dcfa23abe8c',
]);
$token = $post->json('access_token');
But when I try with this, the request fails me.
<?php
namespace App\Http\Integrations\Medplum\Requests;
use Saloon\Contracts\Body\HasBody;
use Saloon\Enums\Method;
use Saloon\Http\Request;
use Saloon\Traits\Body\HasFormBody;
class GetAuthToken extends Request implements HasBody
{
use HasFormBody;
/**
* Define the HTTP method
*
* @var Method
*/
protected Method $method = Method::POST;
/**
* Define the endpoint for the request
*
* @return string
*/
public function resolveEndpoint(): string
{
return '/oauth2/token';
}
protected function defaultBody(): array
{
return [
'grant_type' => 'client_credentials',
'client_id' => '9bf9f97e-be08-11ed-afa1-0242ac120002',
'client_secret' => 'xf0f18ae1e45bac5e22246024b90a4af1f403d86202b290aef46dcfa23abe8c',
'redirect_uri' => 'http://localhost:80/api'
];
}
}
$authorizeRequest = new GetAuthToken();
$code = $connector->send($authorizeRequest);
Anyone with the same error? I don't know if I'm doing something wrong
I have a response with 'data'
key, where there are several items. Normally (without automatic pagination), I'm able to get the collection of DTOs based on these suggestions: #186 using just $response->dto()
.
Now, I wanted to have the automatic pagination (I use OffsetPaginator
), so instead of
$response = $this->connector()->send($this->request);
I used:
$paginator = $this->connector()->paginate($this->request);
everything is properly configured, as I can run
foreach($paginator as $response) {
$pageData = $response->json('data');
}
as well I may create a collection of individual "raw" items in response: $paginator->collect('data')
.
I wanted to create DTOs based on the combined paginator results, and tried $paginator->dto()
, but got: "Call to undefined method OffsetPaginator::dto()"
I know, I can easily build the DTOs manually:
$dtos = $paginator->collect('data')->map(
fn ($post) => PostDto::fromItem($post),
);
but it would be nice to have a "helper/shortcut" method dto()
as with single responses which would use the same createDtoFromResponse()
method and we could similarly use $paginator->dto()
.
Hi all!
Saloon is pretty cool! Helps me to integrate API's in a clean way in my Laravel application.
However, I was wondering how to handle expectations when testing. You are able to use mocked responses, but you never know if they are called, how many times, and with which data.
I already understand how to create fake MockResponse
s, but in my tests I want to validate what data has been passed to the response, and make sure it is called (once, never on x times).
For now I came up with this solution myself:
/**
* Helper method to mock a MockResponse object to validate if the request has been sent.
*/
protected function mockMockResponse(mixed $data = [], int $status = 200, ?InvokedCount $expected = null): MockObject
{
$mock = $this->getMockBuilder(MockResponse::class)
->onlyMethods(['getStatus'])
->setConstructorArgs([ $data, $status ])
->getMock();
$mock
->expects($expected ?: $this->once())
->method('getStatus')
->willReturn($status);
return $mock;
}
// Use the mock for Saloon
Saloon::fake([
SendMessageRequest::class => $this->mockMockResponse(['name' => 'Sam'], 200),
]);
So I was wondering:
Hello, love the package!
I think it could be beneficial to have a sensible default (but configurable) circuit breaker pattern plugin available to use (opt in only). A circuit breaker pattern is good for use with external API integrations for multiple reasons: alerts when an external API reaches a failure threshold, preventing subsequent requests for a time period if the external API is known to be down, prevention of further security-type lockouts (if a request is triggering an external APIโs request limits, for example).
Is there any interest in this?
Thanks!
I'm trying to develop a plugin to proxy a request to a provided URL.
For example, the original request would go to https://abc.xyz/test, and i'd like to proxy-it to https://proxy.me?to=https://abc.xyz/test.
Is this possible? I don't see any change right now apart from modifying the connector and request urls.
Saloon has given people the option to make requests by calling the request class or using the connector by specifying requests in the $requests property on the connector, but I was thinking of another one:
$connector = new ForgeConnector();
$connector->request(new GetServersRequest); // Init the request but don't send it
$connector->send(new GetServersRequest); // Send the request right away
This gives people the ability to define defaults on the connector while not having to register their request and also get
type-hinting in their IDE.
I'm aware of an issue with AssertSentJson if there are multiple requests that have been sent using the same Saloon request but with different data.
How to reproduce:
AssertSentJson
method to check the data in the second requestHello,
I am using Saloon 2.0.0-beta6, Laravel 9.19 and trying to transform the following curl request to Saloon code:
curl --location --request POST 'https://api.sandbox.com/photo/api/v2/photos' \
--header 'Content-Type: multipart/form-data' \
--header 'Authorization: Bearer MYBEAREAR' \
--form 'ans="{\"version\": \"0.1\",\"type\": \"image\",\"owner\": {\"id\": \"sandbox.id\"},\"additional_properties\": {\"originalUrl\": \"https://cdn.player.com/v2/media/poster.jpp\" },\"subtitle\": \"Test image\"}";type=application/json'
I have a connector:
<?php
namespace App\Http\Integrations\PhotoCenter;
use Saloon\Contracts\Authenticator;
use Saloon\Http\Auth\TokenAuthenticator;
use Saloon\Http\Connector;
class PhotoCenterConnector extends Connector {
/**
* The Base URL of the API
*
* @return string
*/
public function resolveBaseUrl(): string {
return (string) env( 'API_PHOTO_BASE' );
}
/**
* Default headers for every request
*
* @return string[]
*/
protected function defaultHeaders(): array {
return [
'Content-Type' => 'multipart/form-data',
//'Accept' => 'application/json',
];
}
/**
* Default HTTP client options
*
* @return string[]
*/
protected function defaultConfig(): array {
return [];
}
protected function defaultAuth(): ?Authenticator {
return new TokenAuthenticator( env( 'ACCESS_TOKEN' ) );
}
}
And the request:
<?php
namespace App\Http\Integrations\PhotoCenter\Requests;
use Saloon\Contracts\Body\HasBody;
use Saloon\Enums\Method;
use Saloon\Http\Request;
use Saloon\Traits\Body\HasMultipartBody;
class CreatePhotoCenterRequest extends Request implements HasBody {
use \Saloon\Traits\Body\HasBody;
/**
* Define the HTTP method
*
* @var Method
*/
protected Method $method = Method::POST;
/**
* Define the endpoint for the request
*
* @return string
*/
public function resolveEndpoint(): string {
return 'photos';
}
}
My example code:
$photo_connector = new PhotoCenterConnector();
$request = new CreatePhotoCenterRequest();
$request
->body()
->set('ans="{"version": "0.1","type": "image","owner": {"id": "sandbox.id"},"additional_properties": {"originalUrl": "https://cdn.jwplayer.com/v2/media/poster.jpg" },"subtitle": "Test image from"}";type=application/json');
/*
// Other example
->add(
name: 'ans',
contents: '{"version": "0.1","type": "image","owner": {"id": "sandbox.id"},"additional_properties": {"originalUrl": "https://cdn.player.com/v2/media/poster.jpg" },"subtitle": "Test image from"}',
filename: 'myfile.png',
headers: [
'type' => 'application/json'
] );*/
$response = $photo_connector->send( $request );
dd( $response );
but I always get the same error:
array:5 [โผ // app/Http/Controllers/JWPlayerController.php:401
"error" => "Payload Too Large"
"exception" => "org.springframework.web.multipart.MultipartException"
"message" => "Could not parse multipart servlet request; nested exception is java.io.IOException: org.apache.tomcat.util.http.fileupload.FileUploadException: the request was rejected because no multipart boundary was found "
"status" => 413
"timestamp" => 1676008975851
]
but from the curl command, the request does work.
What would be the correct way to convert the curl command to Saloon code?
Thank you very much for your help.
Some APIs use a query param based Key authentication. For sure that's possible with a defaultQuery()
method but somehow feels wrong. So how about a QUeryAuthenticator
or similar named class with a signature like new QueryAuth(string $name, string $value)
. ๐ค
Hi, we need to use 'client_credentials' to get an oauth token before every other api call we make to a third party api call. Well we only need a new token if the previous one has expired, they have a 2 hour life.
Wondering how I can create a custom authenticator so it adds the latest token to every other request we need to make
Thanks,
Brian Smith
Can version 2 be integrated with the RicorocksDigitalAgency\Soap package using custom senders? If yes, can you please give me an example of such a sender?
Thank you in advance!
Just wanted to open an issue here to let people know that I am currently working on a mocking system for Saloon. It's going to introduce two ways to mock in Saloon, one way would be the "Larvel" way - inspired by the wonderful testing that is provided by Illuminate/Http
. The other way would require a bit more intervention but I hope would still be useful especially when writing SDKs.
Laravel mocking will have slightly more features than the standard PHP mocking that can be used within SDKs. This is because Laravel has a super powerful service container. See the testing page for the HTTP Client in the Laravel docs. I will recreate how it works there.
SDK mocking is slightly different, but the idea is that you will be able to tell Saloon to send a fake response when a request has been created.
$mockClient = new SaloonMockClient();
$mockClient->addResponse(200, $data, $headers);
$mockClient->addResponse(500, $data, $headers);
$response = (new SaloonRequest)->send($mockClient); // 200
$response = (new SaloonRequest)->send($mockClient); // 500
Here's an example of how it could be implemented
$sdk = new ForgeSdk($token, $saloonMockClient);
$sdk->getServers(); // Internally sends $saloonRequest->send($this->mockClient);
This was my approach to implementing plugin based authentication. Hopefully it's not an anti-pattern for how you envisioned Saloon being used. Putting docs here in case they can be brought into the official docs if it's an acceptable solution.
In release v0.8.0 the authentication feature withToken
was added. This trait adds the functionality to requests:
$request = UserRequest::make()->withToken('Sammyjo20');
This can be great for one offs, but depending on the size of the API you're interacting with, can feel less clean than simply new
ing up a request class and having it happen behind the scenes.
There are a few pain points which can be addressed using plugin based authentication:
While it is shown here that using defaultHeaders()
is possible, with expiring tokens this approach does not feel as streamlined.
Lastly, to use Salesforce as an example, per-user OAuth configurations may have radically different endpoints. To validate and get the OAuth configuration, then initialize the wrapper and not need to worry about subsequent requests, design wise, feels more like what Saloon was going for.
In your wrapping class that invokes the API, you can add the following properties.
// MyApiWrapper.php
protected static string $token;
public static function token() : string
{
return self::$token;
}
These can be supplied during construction of your wrapper. For my use with Salesforce, it looks like this (where $accessToken
is an oauth token that has not expired):
public function __construct(string $accessToken, string $instanceUrl, string $apiVersion)
{
self::$token = $accessToken;
self::$instanceUrl = $instanceUrl;
self::$apiVersion = $apiVersion;
}
Then, create a plugin with the following:
// Plugins/WithAuthorizationHeader.php
public function bootWithAuthorizationHeader() : void
$this->mergeHeaders([
'Authorization' => 'Bearer ' . MyApiWrapper::token()
]);
}
Finally, in your connector class(es), simply include the Trait
// Connectors\MyConnector.php
use WithAuthorizationHeader;
The connector will automatically merge the token into each request.
Hi,
For a project these past few months I have been using a simpler, self created implementation of saloon (wish I had known this amazing package existed sooner). I've been looking through the documentation & code (of both v1 & v2), but i'm missing 2 (common) features that I need quite often while working with external api's:
the first one is pagination handling (for which I see there is already a draft? #152).
The second is rate limit handling.
Are you open to accepting PR's for rate limit handling? Since I'm new to this package could you give some basic pointers on how/where to start with this.
Kind regards,
Anthony
This package looks really nice.
Currently I am using https://github.com/spatie/data-transfer-object to cast the json response to a class and vice versa.
Do you know any good way how to integrate libraries like this into the Request
classes?
As some endpoints pass the parameters through the body using POST method (and some through query using GET), I wanted to specifically build the cache key using this information as well.
The CacheKeyHelper
already uses $query = $pendingRequest->query()->all();
and in my own cacheKey(PendingRequest $pendingRequest)
implementation, I wanted to similarly use: $body = $pendingRequest->body()->all();
. Unfortunately the body()
is not accesible there, I get Call to a member function all() on null
.
The \Sammyjo20\Saloon\Helpers\URLHelper::join()
method does not respect a full URL in the $endpoint
argument but still prefixes it with the $baseUrl
.
Some APIs have a few endpoints on another base URL/domain. I would like to override the default base URL with a full URL in the request endpoint instead of creating a whole new connector.
The already used filter_var($endpoint, FILTER_VALIDATE_URL)
could be used to early return in case the $endpoint
is already a full and valid URL. Possibly also combined with parse_url()
to check scheme and host are defined.
Right now I'm running in the following curl error which fully bubbles through all layers. Even with the AlwaysThrowsOnErrors
trait it fully bubbles through and doesn't get wrapped up in a \GuzzleHttp\Exception\ConnectException
or similar.
cURL error 6: Could not resolve host:
api.steampowered.comhttps
(see https://curl.haxx.se/libcurl/c/libcurl-errors.html) forhttps://api.steampowered.comhttps//steamcommunity.com/actions/QueryLocations?key=xyz&format=json
class SteamConnector extends SaloonConnector
{
public function defineBaseUrl(): string
{
return 'https://api.steampowered.com';
}
}
class QueryLocationsRequest extends SaloonRequest
{
protected ?string $method = 'GET';
public function defineEndpoint(): string
{
$query = '';
if ($this->countryCode) {
$query .= "/{$this->countryCode}";
if ($this->stateCode) {
$query .= "/{$this->stateCode}";
}
}
return "https://steamcommunity.com/actions/QueryLocations{$query}";
}
}
First, I just must say this is an exceptionally great package, perfectly thought out and with great scalability. WOW!
I have some feedback on the documentation, in the places where I got blocked during the implementation. When looking at https://docs.saloon.dev/digging-deeper/oauth2-authentication#refreshing-access-tokens, there is:
$authenticator = $user->auth; // Your authenticator class.
which makes me think, what the heck is $user->auth??? Is this a field? Is this a relationship? What have I missed?
I bet it would be more clear if the field was named authenticator
and comment was something like this:
// $authenticator stored in the model using OAuthAuthenticatorCast or EncryptedOAuthAuthenticatorCast
Later, the docs at https://docs.saloon.dev/digging-deeper/oauth2-authentication#building-a-method-to-create-an-authenticated-instance-of-your-sdk show the spotify()
method example. But within this, there should be no $user->
but $this->
as we are already in the User class.
PS. I would as well suggest splitting this method into two for better readability:
public function spotify(): SpotifyApiConnector
{
$authenticator = $this->refreshAccessToken($this->spotify_authenticator);
$spotify = new SpotifyApiConnector();
$spotify->authenticate($authenticator);
return $spotify;
}
public function refreshAccessToken(
OAuthAuthenticator $authenticator,
$forceRefresh = false
): OAuthAuthenticator {
if ($authenticator->hasExpired() || $forceRefresh) {
$authenticator = SpotifyAuthConnector::make()->refreshAccessToken($authenticator);
$this->spotify_authenticator = $authenticator;
$this->save();
}
return $authenticator;
}
Hey everyone, thanks so much for all the support that you have given for Saloon. I can't believe it's almost at 500 stars on GitHub and receiving over 150 installs a day. I just wanted to take a moment to thank you all! ๐๐
That being said, there are some good things coming to Saloon. I'm working on version 2, which will help create a road for the future of Saloon, as well as improving developer experience and making your life easier.
Here is a summary of the changes that are going to happen.
Currently, Saloon will run your request through the "Request Manager" which merges all of the headers, config, and everything else from the connector and request into one. With Version 2, I am introducing the "PendingSaloonRequest". Inside of here, this is where all of the logic like merging properties together and running your plugins will happen. This is so the building of a request is entirely separate from the sending of requests. After that, it will be sent to the "Guzzle Sender", and the class will receive the full PendingSaloonRequest with all the final configuration and headers before sending.
I am changing to this new flow because eventually, I want Saloon to be HTTP client agnostic and allow you to use any client you like, so you don't have to use Guzzle if you don't want - and if Guzzle decides to be abandoned, Saloon won't be left in the dark.
Here's the new flow in detail.
The only exception to the flow in the image above is that it will not create a PSR-7 request just yet, I am still working that one out.
$requestโheaders()โpush()
.To help move the dependency on Guzzle, Saloon has also implanted its own middleware pipeline for requests and responses. This will replace the old Guzzle Handler support and response interceptors. You will be able to define request pipes and response pipes to modify the request/response before it is sent or given back to the user.
$request = new GetForgeServersRequest;
$request->middleware()
->addRequestPipe(function (PendingSaloonRequest $request) {
//
})
->addRequestPipe(new MyInvokableClass)
->addResponsePipe(function (SaloonResponse $response) {
//
})
Saloon's middleware pipeline will also be supported for asynchronous requests, so even if you have a pool of requests being sent simultaneously, they can each have their own middleware pipeline, which is something that Guzzle does not support with their existing handler stack logic, since you can only have one handler stack per client.
Middleware pipes can be added anywhere. Inside the request/connector, added by plugins, or even applied right before a request is sent. It will really allow you to tap into Saloon.
Saloonโs external API will still remain the same, like the following:
<?php
$request = new GetForgeServersRequest;
$response = $request->send($mockClient);
$connnector = new ForgeConnector;
$request = $connector->request(new GetForgeServersRequest)->send();
$response = $request->send();
// Or
$connector->send($request);
There will be some additions to the external API, like interacting with request properties
<?php
// Before
$request = new GetForgeServersRequest;
$request->addHeader('X-User-Name', 'Sammy');
$request->addConfig('debug', true);
$config = $request->getConfig(); // Array
// Now
$request->headers()->push('X-User-Name', 'Sammy');
$request->config()->push('debug', true);
$config = $request->config()->all();
// Same with config, handlers, response interceptors, etc.
There will also be some new features, like the ability to set a mock client on the connector or a request, so it doesnโt have to be passed into the send of every request.
<?php
$request = new GetForgeServersRequest;
$request->withMockClient($mockClient);
$request->send(); // Won't need to set it here!
// Even more useful
$connector = new ForgeConnector;
$connector->withMockClient($mockClient);
// All requests using the connector will use the same mock client!
$connector->request($requestA)->send();
$connector->request($requestB)->send();
Hi,
One of my needs is to be able to use a request rate limit, because normally my jobs run for long periods but need to respect the API's request limit.
I'm creating a plugin to use the Spatie/GuzzleRateLimiterMIddleware
Would it be useful to anyone else?
I understand that this can be solved by a custom cache key but I think this should either be changed or clarified in the documentation.
Saloon offers cache requests through the use of the Trait "AlwaysCacheResponses". However by default the cache key is not including the URL query parameters. The documentation states that "By default, the cache key will be built up from the full URL of the request".
Then the expected behavior should be that the cache key includes the query parameters as a "full URL" according to RFC 1738 describes a HTTP URL as following:
An HTTP URL takes the form:
http://<host>:<port>/<path>?<searchpart>"
According to the docs, I should be able to call
$pendingRequest->authenticate(...)
from the retry handler, however it does not seem to be updating the header on the pending request. I have created a reproduction test case here.
test('you can authenticate the pending request inside the retry handler', function () {
$mockClient = new MockClient([
MockResponse::make(['name' => 'Sam'], 401),
MockResponse::make(['name' => 'Gareth'], 200),
]);
$connector = new TestConnector;
$connector->withMockClient($mockClient);
$response = $connector->sendAndRetry(new UserRequest, 2, 0, function (Exception $exception, PendingRequest $pendingRequest) {
$pendingRequest->authenticate(new \Saloon\Http\Auth\TokenAuthenticator('newToken'));
return true;
});
expect($response->status())->toBe(200);
expect($response->json())->toEqual(['name' => 'Gareth']);
expect($response->getPendingRequest()->headers()->get('Authorization'))->toEqual('Bearer newToken');
});
The test results in Failed asserting that null matches expected 'Bearer newToken'.
So already you can create your own mock responses but it would be really cool if you could create your own "fixtures" for requests.
Ideas
fixture()
method on your request that Saloon can useMockResponse::fixture($array)
MockResponse::fixtureFile($jsonFile)
This package looks promising to implement API SDKs.
I was thinking about testing requests with this package - would make sense to use illuminate/http
as it can be mocked internally without having to re-write mocks within this package.
Http::fake([
'https://forge.laravel.com/*' => Http::response(['foo' => 'bar'], 200, $headers),
]);
$request = new GetForgeServerRequest(serverId: '123456');
$response = $request->send();
$data = $response->json();
I might as well submit a PR to make this change, but that would leave a lot of changes (perhaps). ๐ค
HubSpot OAuth2 authorization base url is https://app.hubspot.com while the api base url is https://api.hubapi.com
I don't see the option to override the base url in getAuthorizationUrl
What I'm doing is using str_replace to overcome this
<a href="{{ str_replace(config('services.hubspot.api_url'), 'https://app.hubspot.com', $authorizeUrl) }}">Login</a>
https://developers.hubspot.com/docs/api/working-with-oauth#initiating-an-integration-with-oauth-2-0
Just had an idea for Saloon, wondering if this would be useful.
Currently the only way to trigger requests would be to instansiate the request, but what if there was a "connector" first option too?
For example:
$connector = new ForgeConnector($myArgs);
$connector->getForgeServer($requestConstructorArgs)->send();
This means you could construct your own connector and pass it arguments. I don't think it would be a difficult thing to implement, but I think it would be really useful. Not 100% sure how I would define type-hints.
Internally it could look something like this:
public function getForgeServer($args): GetForgeServerRequest
{
return $this->forwardRequest(GetForgeServerRequest::class, $args);
}
The "forwardRequest" method would instantiate the request, and pass it the args.
If this is manually defined, it could help with typehinting.
I could also make a trait for Laravel which will "auto discover" requests by looking through an expected file structure.
It is possible to have dynamic base url in a connector?
The base url of the endpoint changes but it uses the same standard so i think Saloon can help me.
Thanks
Hi, great package!
I'm trying to make a sdk for an API I'm using. I forked the template and started building it.
My connector is as follows:
class Asaas extends SaloonConnector
{
/**
* Define the base URL for the API
*
* @var string
*/
protected string $apiBaseUrl = 'https://www.asaas.com';
/**
* Define the base URL for the Sandbox API
*
* @var string
*/
protected string $apiSandboxBaseUrl = 'https://sandbox.asaas.com';
/**
* Define the ambient to use the API
*
* @var Ambient
*/
protected Ambient $ambient = Ambient::PRODUCTION;
/**
* Define the API key for the account
*
* @var string
*/
protected string $apiKey = ':api_key';
/**
* Custom response that all requests will return.
*
* @var string|null
*/
protected ?string $response = AsaasResponse::class;
/**
* The requests/services on the Asaas.
*
* @var array
*/
protected array $requests = [
'customers' => CustomerCollection::class,
];
/**
* Define the base URL of the API.
*
* @return string
*/
public function defineBaseUrl(): string
{
return $this->ambient === Ambient::PRODUCTION ? $this->apiBaseUrl : $this->apiSandboxBaseUrl;
}
/**
* @param string $apiKey
* @param Ambient|null $ambient
*/
public function __construct(string $apiKey, Ambient $ambient = null)
{
$this->apiKey = $apiKey;
if (isset($ambient)) {
$this->ambient = $ambient;
}
}
public function defaultAuth(): ?AuthenticatorInterface
{
return new Authenticator($this->apiKey);
}
/**
* Define any default headers.
*
* @return array
*/
public function defaultHeaders(): array
{
return [];
}
/**
* Define any default config.
*
* @return array
*/
public function defaultConfig(): array
{
return [];
}
}
My Request:
class ListCustomersRequest extends SaloonRequest
{
protected ?string $connector = Asaas::class;
protected ?string $method = Saloon::GET;
/**
* Define allowed filters to be applied to the customer
*
* @var array|string[]
*/
protected array $allowedCustomerFilters = ['name', 'cpfCnpj', 'externalReference'];
/**
* Define page to be fetched
*
* @var int|mixed
*/
protected int $page;
/**
* Define limit of records to be fetched (max.: 100)
*
* @var int|mixed
*/
protected int $limit;
/**
* @param Customer $customer
* @param int $page
* @param int $limit
*/
public function __construct(
public Customer $customer,
int $page = 1,
int $limit = 25,
) {
$this->page = max($page, 1);
$this->limit = max(10, min($limit, 100));
}
/**
* @return string
*/
public function defineEndpoint(): string
{
return '/customers' . ($this->customer->id !== null ? "/{$this->customer->id}" : '');
}
/**
* @return array
*/
public function defaultQuery(): array
{
return [
...array_filter(
$this->customer->toArray(),
fn ($value, $prop) => ! empty($value) && in_array($prop, $this->allowedCustomerFilters),
ARRAY_FILTER_USE_BOTH
),
'offset' => ($this->page - 1) * $this->limit,
'limit' => $this->limit,
];
}
}
I've made a test for this request, but it just doesn't work. I tried everything, including configuring the curl options of Guzzle but nothing works.
it('fetches a list of all customers in account.', function () {
$asaas = new Asaas(
'xxxxxxxx',
Ambient::SANDBOX,
);
$response = $asaas->customers()->get();
dump($response->body());
expect($response)->toBeInstanceOf(SaloonResponse::class);
});
However, I made another test using Guzzle directly, with minimal configuration, and it works just fine
it('fetches a list of all customers in account with guzzle.', function () {
$client = new Client([
'handler' => HandlerStack::create(),
]);
$config = [
'headers' => [
'access_token' => 'xxxxxxxxxxxx',
],
];
$request = new Request('GET', 'https://sandbox.asaas.com/api/v3/customers');
$res = $client->send($request, $config);
$body = $res->getBody()->getContents();
dump($body);
expect($body)->json();
});
In the first test, I get a HTML page with the title login, and if I put a function on the property on_redirect
in the defaultConfig
I can see that it redirects me exactly one time for the route login/auth
, but the second test return the JSON with the results.
Any idea of what am I doing wrong?
Could you provide means to give json parameters when building the body. I would like to not escape my urls within the body
This is a great project - exactly what I needed to standardise our API connectivity as we use a lot of integrations. I realise this is a problem with the third party API I'm using not confirming to REST specifications but want to check if anything can be done.
We use an API which has the baseUrl format api.xxx.com/v2
Requests are then made to endpoints in the format model=accounts&action=load
This leads to an actual URL of api.xxx.com/v2?model=accounts&action=load
Saloon constructs the URL by combining the two parts, trimming the right forward slash (if present) and then inserting a forward slash - e.g api.xxx.com/v2/?model=accounts&action=load
- this produces a 404 error as it is now referencing a non existent directory.
I have tried using handlers but you can't modify the base URL once set (done in ManagesGuzzle
).
The easy solution is to just use the full URL in the Request class but this takes away some of the simplicity that this package created.
I can't think of any other way of modifying the URL before the request is made - however an option on the SaloonConnector class could allow the behaviour to be configured at runtime per connector (or alternatively, an option in the global config to add slashes on a global basis). I'm happy to submit a pull request if this is something you'd be interested in adding
This is really an excellent package, very useful to me
But I don't know how to record the return result of each request,
Call the addHandle
method to achieve this?
first, thank you for this very nice package, I really like the idea and your concept. the very first question that comes into my mind is: how do I implement a "retry x times with x seconds sleep between" with this? would it make sense to implement a interceptor here or is this something that should be implemented outside of this package? thank for your advice ๐
I needed the client_crendentials
grant for an API. I was able to implement this by reusing most of the code of the authorization_code
grant. I want to PR this, but there were some differences I'm unsure of how to implement.
Some of the things I encountered:
code
is needed as required by \Sammyjo20\Saloon\Http\OAuth2\GetAccessTokenRequest::__construct()
\Sammyjo20\Saloon\Helpers\OAuth2\OAuthConfig::validate()
the redirect uri is checked, but this is not required for the client_credentials
grant
AuthorizationCodeOAuthConfig
and ClientCredentialsOAuthConfig
with a common (abstract?) parentOAuthConfig
and handle validation differently based on thatclient_credentials
grant does not require a refresh token (altough some implementations do provide it)
\Sammyjo20\Saloon\Interfaces\OAuthAuthenticatorInterface
or implemented optionally?Hi, I'm trying to extend my connector.
The extended connector implements the AlwaysCacheResponses trait.
But I'm getting the following error (notice: is not a valid.)
The provided connector is not a valid. The class must also extend SaloonConnector.
The word also, also feels a bit weird, I do not believe PHP classes can extend multiple classes ?
Question: I this not a valid approach ?
Base Connector:
<?php
namespace App\Http\Integrations\Service;
use Sammyjo20\Saloon\Http\SaloonConnector;
use Sammyjo20\Saloon\Traits\Plugins\AcceptsJson;
class BaseConnector extends SaloonConnector
{
use AcceptsJson;
public function defineBaseUrl(): string
{
return config('services.service.base_url');
}
}
Extended Connector:
<?php
namespace App\Http\Integrations\Service;
use App\Http\Integrations\Service\BaseConnector;
use Illuminate\Support\Facades\Cache;
use Sammyjo20\SaloonCachePlugin\Drivers\LaravelCacheDriver;
use Sammyjo20\SaloonCachePlugin\Interfaces\DriverInterface;
use Sammyjo20\SaloonCachePlugin\Traits\AlwaysCacheResponses;
class CachedConnector extends BaseConnector
{
use AlwaysCacheResponses;
public function cacheDriver(): DriverInterface
{
return new LaravelCacheDriver(Cache::store('file'));
}
public function cacheTTLInSeconds(): int
{
return 7200;
}
}
A Request:
<?php
namespace App\Http\Integrations\Service\Requests;
use Sammyjo20\Saloon\Constants\Saloon;
use Sammyjo20\Saloon\Http\SaloonRequest;
use Sammyjo20\Saloon\Http\SaloonResponse;
use App\Http\Integrations\Mfiles\CachedConnector;
class GetLanguagesRequest extends SaloonRequest
{
use CastsToDto;
/**
* The connector class.
*
* @var string|null
*/
protected ?string $connector = CachedConnector::class;
/**
* The HTTP verb the request will use.
*
* @var string|null
*/
protected ?string $method = Saloon::GET;
/**
* The endpoint of the request.
*
* @return string
*/
public function defineEndpoint(): string
{
return config('services.service.endpoints.languages.get');
}
}
It would be super cool to be able to record saloon responses that can be used later on in your tests.
Maybe should write a wrapper around https://github.com/dshafik/guzzlehttp-vcr
As already discussed on Twitter - the illuminate/support
package comes with some handy classes but also a ton of Laravel-specific ones. Requiring it only because of the Arr
and/or Str
helpers isn't really "right" and questions the "plain php" nature of that package, as there's a specific Laravel wrapper.
It will also tie this package's versioning to the Laravel lifecycle even if there hasn't been anything changed relevant to that package. As otherwise it won't be installable on the latest Laravel projects - at the same time it should support the lowest Illuminate version possible as with Illuminate and Laravel comes also PHP constraints which will possibly limit the usability of the package even further. In my experience Symfony devs also don't really like to load illuminate/support as it comes with a ton of classes absolutely not needed and will make navigating IDE more complicated.
So far I've seen the only classes used are the following. All should be "easy" to replace with a custom Util
class or even plain PHP.
Illuminate\Support\Arr
Illuminate\Support\Str
Illuminate\Support\Collection
Currently to call a request and have it return json, it looks like this:
$response = Connector::getTotalRequest('users')->send()->json()
I want all requests to return json by default, instead of a SaloonResponse
object, like so:
$response = Connector::getTotalRequest('users')
I tried making a custom response, but it enforces that only a SaloonResponse
object is returned, but even then I still need to call send()
. How could I achieve this without having to overwrite things?
Hi!
It was wonderful to discover that you already had thought about automatic pagination :)
I had a few observations while implementing OffsetPaginator
in our project.
$noTotal
, you could make a isFinished()
check based on the count of currently fetched items?isFinished()
will return true / throw an exception?defaultQuery()
in the connector, it seems these values are not merged in at applyPagination()
. Only values defined at defaultQuery()
in the request are merged.$paginator->setCurrentOffset()
is not implemented.I solved some of the above issues using a custom paginator:
use Saloon\Contracts\Request;
use Saloon\Enums\Method;
use Saloon\Http\Paginators\OffsetPaginator;
class CustomOffsetPaginator extends OffsetPaginator
{
protected int $iterationsCnt = 0;
protected int $iterationsMax = 100;
protected function isFinished(): bool
{
$this->iterationsCnt++;
if ($this->iterationsCnt > $this->iterationsMax) {
return true;
}
return count($this->currentResponse->json('data')) < $this->limit();
}
protected function applyPagination(Request $request): void
{
if ($this->originalRequest->getMethod() === Method::POST) {
$request->body()->merge([
$this->getLimitKeyName() => $this->limit(),
$this->getOffsetKeyName() => $this->currentOffset(),
]);
} else {
$request->query()->merge([
$this->getLimitKeyName() => $this->limit(),
$this->getOffsetKeyName() => $this->currentOffset(),
]);
}
}
}
IMHO it would be great to have the support for for different request methods (GET/POST, etc.) and stopping the loop without the "total" information out of the box in the library.
Was looking for ways to mock not so common exceptions as Guzzle/ConnectExceptions
I think closures could work as in laravel HttpClient
https://laravel.com/docs/9.x/http-client#fake-callback
Also guzzle handlers allow these kind of mocked exceptions
https://docs.guzzlephp.org/en/stable/testing.html#mock-handler
Any ideas?
Any chance you could get this to work with Laravel Queues so I can just add ShouldQueue to a request and any time I call it it would be done asynchronously via the queue?
I came here to say great work on putting this together. It looks good and it's really nicely organised. I've only been reading code and haven't had a chance to try it out yet, but I feel like this could become a great package to make integrating with lots of APIs easier!
One thing I picked up on pretty quickly though is how dependent it is on Guzzle (I love Guzzle btw, so it's fine for me). I know it's early days for this package and I'm sure (/hope) you have plans in mind to make this not such a hard dependency as it feels quite useful for such a fundamental (and PSR-compliant) library to be easily replaceable.
To that end though, one thing that I spotted that will kind of make this harder is that the request config (and possibly other structures) is exposed directly to Guzzle: your SaloonRequest
class (or more correctly, the CollectsConfig
trait) gathers it all up and essentially passes it Guzzle unfiltered in your RequestManager
.
It feels like a good goal would be to abstract this away so that Guzzle could be swapped out as needed.
Eventually you'd end up with a standard set of config options that work across HTTP client libraries. Even if your internal structure matches Guzzle's for convenience (and it may stay that way for a long time), you could introduce a transform layer between your structure and the HTTP client being used so that devs can use a consistent interface across libraries.
You and users of this package would be less susceptible to changes on Guzzle's side, for example if they suddenly removed an option, you could help maintain backwards compatibility for folks.
You could then also remove Guzzle as a dependency, which means fewer potential conflicts for people too.
What is a good place to add validations for requests before they are submitted? For example, a request requires atleast 2 values on an array passed to it. I want to add this validation before the request is submitted and throw an error. What would be a good place to do this?
I realised that after I had built the mocking functionality in Saloon, I had forgotten to add assertions for the mocking. Coming soon, you will be able to assert the following things in your tests:
Hi, I am trying to use the SoloRequest as shown in the documentation but here is what i get :
Am i missing something?
PHP 8.1
Laravel 10.4
Saloon 2.4
Hello!
Was wondering if it's possible to use the built-in pagination without supplying a next_page_url
? The API I'm using only supplies the below for pagination.
"pagination": { "pageNumber": "1", "pageSize": "100", "totalAvailable": "640" }
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.