Code Monkey home page Code Monkey logo

laravel-spapi's Introduction

Total downloads Latest stable version License

Selling Partner API wrapper for Laravel

Simplify connecting to the Selling Partner API with Laravel. Uses jlevers/selling-partner-api under the hood.

Related packages


This package is developed and maintained by Highside Labs. If you need support integrating with Amazon's (or any other e-commerce platform's) APIs, we're happy to help! Shoot us an email at [email protected]. We'd love to hear from you :)

If you've found any of our packages useful, please consider becoming a Sponsor, or making a donation via the button below. We appreciate any and all support you can provide!

Donate to Highside Labs


There is a more in-depth guide to using this package on our blog.

Installation

$ composer require highsidelabs/laravel-spapi

Table of Contents


Overview

This library has two modes:

  1. Single-seller mode, which you should use if you only plan to make requests to the Selling Partner API with a single set of credentials (most people fall into this category, so if you're not sure, this is probably you).
  2. Multi-seller mode, which makes it easy to make requests to the Selling Partner API from within Laravel when you have multiple sets of SP API credentials (for instance, if you operate multiple seller accounts, or operate one seller account in multiple regions).

Single-seller mode

Setup

  1. Publish the config file:
$ php artisan vendor:publish --provider="HighsideLabs\LaravelSpApi\SellingPartnerApiServiceProvider" --tag="config"
  1. Add these environment variables to your .env:
SPAPI_AWS_ACCESS_KEY_ID=
SPAPI_AWS_SECRET_ACCESS_KEY=
SPAPI_LWA_CLIENT_ID=
SPAPI_LWA_CLIENT_SECRET=
SPAPI_LWA_REFRESH_TOKEN=

# Optional
# SPAPI_AWS_ROLE_ARN=
# SPAPI_ENDPOINT_REGION=

If in Seller Central, you configured your SP API app with an IAM role ARN rather than an IAM user ARN, you'll need to put that ARN in the SPAPI_AWS_ROLE_ARN environment variable. Otherwise, you can leave it blank. Set SPAPI_ENDPOINT_REGION to the region code for the endpoint you want to use (EU for Europe, FE for Far East, or NA for North America).

You're ready to go!

Usage

All of the API classes supported by jlevers/selling-partner-api can be type-hinted. This example assumes you have access to the Selling Partner Insights role in your SP API app configuration (so that you can call SellersV1Api::getMarketplaceParticipations()), but the same principle applies to type-hinting any other Selling Partner API class.

use Illuminate\Http\JsonResponse;
use SellingPartnerApi\Api\SellersV1Api as SellersApi;
use SellingPartnerApi\ApiException;

class SpApiController extends Controller
{
    public function index(SellersApi $api): JsonResponse
    {
        try {
            $result = $api->getMarketplaceParticipations();
            return response()->json($result);
        } catch (ApiException $e) {
            $jsonBody = json_decode($e->getResponseBody());
            return response()->json($jsonBody, $e->getCode());
        }
    }
}

Multi-seller mode

Setup

  1. Publish the config file:
# Publish config/spapi.php file
$ php artisan vendor:publish --provider="HighsideLabs\LaravelSpApi\SellingPartnerApiServiceProvider" --tag="config"
  1. Update the configuration to support multi-seller usage.

    • Change the installation_type in config/spapi.php to multi.
    • If the different sets of seller credentials you plan to use aren't all associated with the same set of AWS credentials (access key ID, secret access key, and optionally role ARN), make sure to change the aws.dynamic key to true. If you don't make that change before running migrations (the next step), the fields for AWS credentials won't be added to the database. (If you're not sure if this change applies to you, it probably doesn't.)
  2. Publish the multi-seller migrations:

# Publish migrations to database/migrations/
$ php artisan vendor:publish --provider="HighsideLabs\LaravelSpApi\SellingPartnerApiServiceProvider" --tag="multi"
  1. Run the database migrations to set up the spapi_sellers and spapi_credentials tables (corresponding to the HighsideLabs\LaravelSpApi\Models\Seller and HighsideLabs\LaravelSpApi\Models\Credentials models, respectively):
$ php artisan migrate
  1. Add these environment variables to your .env (unless you changed the aws.dynamic configuration flag to true in step 2):
SPAPI_AWS_ACCESS_KEY_ID=
SPAPI_AWS_SECRET_ACCESS_KEY=

Usage

First you'll need to create a Seller, and some Credentials for that seller. The Seller and Credentials models work just like any other Laravel model.

use HighsideLabs\LaravelSpApi\Models;

$seller = Models\Seller::create(['name' => 'MySeller']);
$credentials = Models\Credentials::create([
    'seller_id' => $seller->id,
    // You can find your selling partner ID/merchant ID by going to
    // https://<regional-seller-central-domain>/sw/AccountInfo/MerchantToken/step/MerchantToken
    'selling_partner_id' => '<AMAZON SELLER ID>',
    // Can be NA, EU, or FE
    'region' => 'NA',
    // The LWA client ID and client secret for the SP API application these credentials were created with
    'client_id' => 'amzn....',
    'client_secret' => 'fec9/aw....',
    // The LWA refresh token for this seller
    'refresh_token' => 'IWeB|....',

    // If you have the `aws.dynamic` config flag set to true, you'll also need these attributes:
    // 'access_key_id' => 'AKIA....',
    // 'secret_access_key' => '23pasdf....',
    // // Only necessary if you configured your SP API setup with an IAM role ARN, otherwise can be omitted
    // // 'role_arn' => 'arn:aws:iam::....',  
]);

Once you have credentials in the database, you can use them like this:

use HighsideLabs\LaravelSpApi\Models\Credentials;
use Illuminate\Http\JsonResponse;
use SellingPartnerApi\Api\SellersV1Api as SellersApi;
use SellingPartnerApi\ApiException;

class SpApiController extends Controller
{
    public function __construct(SellersApi $api)
    {
        // Retrieve the credentials we just created
        $creds = Credentials::first();
        $this->api = $creds->useOn($api);
        // You can now make calls to the SP API with $creds using $this->api!
    }

    public function index(): JsonResponse
    {
        try {
            $result = $this->api->getMarketplaceParticipations();
            return response()->json($result);
        } catch (ApiException $e) {
            $jsonBody = json_decode($e->getResponseBody());
            return response()->json($jsonBody, $e->getCode());
        }
    }
}

Or, if you want to use a Selling Partner API class without auto-injecting it, you can quickly create one like this:

use HighsideLabs\LaravelSpApi\SellingPartnerApi;
use SellingPartnerApi\Api\SellersV1Api as SellersApi;

$creds = Credentials::first();
$api = SellingPartnerApi::makeApi(SellersApi::class, $creds);

laravel-spapi's People

Contributors

jlevers avatar mannikj avatar victorbordo avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

laravel-spapi's Issues

Mock All External Requests

First of all, thank you for this great package!

However, I'm struggling with mocking the API calls in my tests. I managed to create mocks for the different API classes like OrderV0Api. This is the basic code to create the mocks of the API classes.

    protected function credentials(): Credentials
    {
        return new Credentials($this->credentialsArray());
    }
    
    public function credentialsArray(): array
    {
        return [
            'client_id' => config('spapi.single.lwa.client_id'),
            'client_secret' => config('spapi.single.lwa.client_secret'),
            'refresh_token' => config('spapi.single.lwa.refresh_token'),
            'role_arn' => config('spapi.aws.role_arn'),
            'region' => config('spapi.single.endpoint'),
        ];
    }

    private const EMPTY_CONFIG = [
        'lwaClientId' => '',
        'lwaClientSecret' => '',
        'lwaRefreshToken' => '',
        'awsAccessKeyId' => '',
        'awsSecretAccessKey' => '',
        'endpoint' => Endpoint::EU_SANDBOX,
    ];

    public function mockSpapi(string $apiClass, array $responses, $mockTokensApi = true)
    {
        if ($mockTokensApi) {
            $this->mockTokensApi();
        }

        $mock = new MockHandler($responses);

        $handlerStack = HandlerStack::create($mock);
        $client = new Client(['handler' => $handlerStack]);

        $creds = $this->credentials();

        $config = $creds->toSpApiConfiguration();

        $config->setEndpoint(Endpoint::EU_SANDBOX);
        //$this->mockAuthentication($config);

        $this->app->singleton($apiClass, fn () => new $apiClass($config, $client));
    }

But I realized later that this does not mock the Token requests etc. which are made in the background before calling these API endpoints, because the classes create their own instances of API classes which then create the unmocked requests.
So I tinkered a bit with it and tried to mock the authentication part as well.
But I am very confused about the different classes like AuthorizationSigner, RequestSigner, Authentication, Credentials, Configuration and what not. You can see that in the code I've come up with so far:

     public function mockAuthentication(Configuration $configuration)
    {
        $accessToken = "the-access-token";

        $authSigner = $this->createMock(AuthorizationSignerContract::class);
        $authSigner->expects($this->any())
            ->method('sign')
            ->will(
                $this->returnCallback(function ($request) {
                    return $request;
                })
            );

        $client = new Client([
            'handler' => new MockHandler([
                new Response(200, [], "{\"access_token\": \"{$accessToken}\", \"expires_in\": 60}")
            ]),
        ]);

        $auth = new Authentication(
            self::EMPTY_CONFIG + [
                'authorizationSigner' => $authSigner,
                'authenticationClient' => $client,
            ]
        );

        $configuration->setRequestSigner($auth);
    }

    public function mockTokensApi(array $responses = null)
    {
        $accessToken = "the-access-token";

        $responses = !$responses
            ? [
                new Response(200, [], "{\"access_token\": \"{$accessToken}\", \"expires_in\": 3660}"),
            ]
            : $responses;

        $this->mockSpapi(TokensV20210301Api::class, $responses, false);
    }

You see its quite chaotic because I don't understand 100% what I'm actually doing.

I found that setting the Endpoint to EU_SANDBOX should avoid requesting restrictedDataTokens, but this can not be set from .env vars, because the function SellingPartnerApi::regionToEndpoint throws an exception when SPAPI_ENDPOINT_REGION=EU_SANDBOX.

I just want a setup that ensures that there are no outgoing requests made. I would appreciate help on how to set this up in a simple but robust way.

Unresolvable dependency resolving when calling getFeaturedOfferExpectedPriceBatch

Problem description:

Hi. I've been using this library for a while. Thanks for your great efforts.
Currently I'm able to use methods like getItemOffers, getItemOffersBatch etc, so my setup seems OK. But when I try to call getFeaturedOfferExpectedPriceBatch, I get the error below.
When I try to debug, seems the error rises before my code below.
I'm using Laravel 10.45.1 and Php 8.1.10

Error:

Illuminate \ Contracts \ Container \ BindingResolutionException
Unresolvable dependency resolving [Parameter #0 [ <required> array $configurationOptions ]] in class SellingPartnerApi\Configuration

Code

$skus = [
    'my-sku-1',
    'my-sku-2',
];
$marketplaceId = 'ATVPDKIKX0DER';

$requests = array_map(function ($sku) use ($marketplaceId) {
    return new \SellingPartnerApi\Model\ProductPricingV20220501\FeaturedOfferExpectedPriceRequest([
        'uri' => "/products/pricing/2022-05-01/offer/featuredOfferExpectedPrice",
        'method' => 'GET',
        'marketplace_id' => $marketplaceId,
        'sku' => $sku,
    ]);
}, $skus);

$body = new \SellingPartnerApi\Model\ProductPricingV20220501\GetFeaturedOfferExpectedPriceBatchRequest(['requests' => $requests]);

try {
    $result = $api->getFeaturedOfferExpectedPriceBatch($body);
    return response()->json($result);
} catch (ApiException $e) {
    $jsonBody = json_decode($e->getResponseBody());
    return response()->json($jsonBody, $e->getCode());
}

Seller Central SP API config page screenshot

image

Service Provider Is Very Slow

$this->apiClasses = SellingPartnerApi::getSpApiClasses();

The code above to find all the api classes runs very slowly. It takes about 0.5 seconds on my machine each time it is run. Since the service provider is created at every framework boot, this has a huge performance impact. In fact we noticed that our test suite was slowed down drastically - it took basically 5 times longer only because the above piece of code. Not to mention that the service provider also tries to obtain auth tokens every time it runs like discussed in another issue (#2 ). This impact is not even factored it.

I can think of some quick fixes to improve the performance of this service provider related to class loading by also preparing the setup to be a bit more flexible for later improvements:

  • Make the class registration optional (e.g. by config/env var), so it can be deactivated during tests. The user can then either create mock classes manually or just leave it activated as before. Mocking is another topic on its own and it would be great if this would also be baked into the package, but this is not subject of this issue.
  • Move the class finding (snippet above) to a place where it is only executed when the classes are actually needed (class registration enabled). Currently the api class finding is done immediately in the class constructor. It should be moved to the register function instead.

The above changes would only speed up the tests by deactivating the class registration completely. In production, we can not use a steamroller approach like this, because the binding is actually needed, so that the app knows how to resolve these classes with their correct configuration.

To reduce the performance impact in production we could:

  • either move the class finding code to the config file. This way the classes can be cached together with the rest of config, if config caching is enabled. In the service provider's register we could use the config helper to get these classes, it should be way faster when the config is cached.
  • or remove the class finding and list all the classes explicitly
  • change the singleton binding to a normal binding.
    I am not exactly sure why singleton was chosen for this matter, but in fact when binding something as a singleton, the app will immediately call the resolve callback to make sure the instance exists. I guess using normal binding should work just as well and maybe behave more like the user would expect. It would be more a on-demand creation than creating all the objects even if they are not used at all. Of course each call would resolve it's own instance which could potentially lead to issues, but I don't think it actually will.

If my suggestions feel valid to you, I could prepare a pull request. I'm looking forward to hear your thoughts on my ideas.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.