$customData
17 | * @property Merchant $merchant
18 | * @property Timestamp $createdAt
19 | * @property Timestamp $updatedAt
20 | */
21 | class Variant extends ApiObject
22 | {
23 | protected $defaultValues = [
24 |
25 | ];
26 |
27 | protected $classMap = [
28 | 'price' => \Scayle\StorefrontApi\Models\Price::class,
29 | 'stock' => \Scayle\StorefrontApi\Models\Stock::class,
30 | ];
31 |
32 | protected $collectionClassMap = [
33 | 'attributes' => \Scayle\StorefrontApi\Models\Attribute::class,
34 | 'advancedAttributes' => \Scayle\StorefrontApi\Models\AdvancedAttribute::class,
35 | ];
36 |
37 | protected $collection2dClassMap = [
38 | ];
39 |
40 | protected $polymorphic = [
41 | ];
42 |
43 | protected $polymorphicCollections = [
44 | ];
45 | }
--------------------------------------------------------------------------------
/lib/Models/Brand.php:
--------------------------------------------------------------------------------
1 | \Scayle\StorefrontApi\Models\CustomData::class,
29 | ];
30 |
31 | protected $collection2dClassMap = [
32 | ];
33 |
34 | protected $polymorphic = [
35 | ];
36 |
37 | protected $polymorphicCollections = [
38 | ];
39 | }
--------------------------------------------------------------------------------
/lib/Services/ServiceFactory.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | protected $classMap = [
14 | 'baskets' => \Scayle\StorefrontApi\Services\BasketService::class,
15 | 'attributes' => \Scayle\StorefrontApi\Services\AttributeService::class,
16 | 'brands' => \Scayle\StorefrontApi\Services\BrandService::class,
17 | 'categories' => \Scayle\StorefrontApi\Services\CategoryService::class,
18 | 'filters' => \Scayle\StorefrontApi\Services\FilterService::class,
19 | 'typeahead' => \Scayle\StorefrontApi\Services\TypeaheadService::class,
20 | 'campaign' => \Scayle\StorefrontApi\Services\CampaignService::class,
21 | 'navigation' => \Scayle\StorefrontApi\Services\NavigationService::class,
22 | 'products' => \Scayle\StorefrontApi\Services\ProductService::class,
23 | 'shopConfigurations' => \Scayle\StorefrontApi\Services\ShopConfigurationService::class,
24 | 'variants' => \Scayle\StorefrontApi\Services\VariantService::class,
25 | 'wishlists' => \Scayle\StorefrontApi\Services\WishlistService::class,
26 | 'redirects' => \Scayle\StorefrontApi\Services\RedirectService::class,
27 | 'matchRedirects' => \Scayle\StorefrontApi\Services\MatchRedirectService::class,
28 | 'promotions' => \Scayle\StorefrontApi\Services\PromotionService::class,
29 | ];
30 | }
--------------------------------------------------------------------------------
/lib/Models/BasketItem.php:
--------------------------------------------------------------------------------
1 | serviceFactory === null) {
36 | $this->serviceFactory = new ServiceFactory($this);
37 | }
38 |
39 | return $this->serviceFactory->get($name);
40 | }
41 | }
--------------------------------------------------------------------------------
/lib/Exceptions/ApiErrorException.php:
--------------------------------------------------------------------------------
1 | statusCode = $statusCode;
26 | $this->errors = $this->parseErrors($responseErrors);
27 | parent::__construct("Errors occured while handling the API request ", $statusCode);
28 | }
29 |
30 | /**
31 | * @return ApiError|null
32 | */
33 | public function getFirstError()
34 | {
35 | return empty($this->errors) ? null : $this->errors[0];
36 | }
37 |
38 | /**
39 | * @return ApiError[]
40 | */
41 | public function getErrors()
42 | {
43 | return $this->errors;
44 | }
45 |
46 | /**
47 | * @return int
48 | */
49 | public function getStatusCode()
50 | {
51 | return $this->statusCode;
52 | }
53 |
54 | /**
55 | * @param array $errors
56 | * @return ApiError[]
57 | */
58 | private function parseErrors($errors)
59 | {
60 | $adminApiErrors = [];
61 |
62 | if (array_key_exists('errors', $errors)) {
63 | foreach ($errors['errors'] as $error) {
64 | $adminApiErrors[] = new ApiError($error['errorKey'], $error['message'], $error['context']);
65 | }
66 | } else {
67 | $code = $errors['code'] ?? $this->getStatusCode();
68 | $adminApiErrors[] = new ApiError($code, $errors['message'] ?? '', '');
69 | }
70 |
71 |
72 | return $adminApiErrors;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/.gitlab-ci.yml:
--------------------------------------------------------------------------------
1 | workflow:
2 | rules:
3 | - if: $CI_PIPELINE_SOURCE == "pipeline" && $API_COMMIT_SHA
4 | - if: $CI_PIPELINE_SOURCE == "web" && $API_COMMIT_SHA
5 |
6 | default:
7 | image: docker:20
8 | services:
9 | - name: docker:dind
10 | command: ["--tls=false"]
11 | tags:
12 | - ay-shared-runner
13 | before_script:
14 | # Configure docker to also use our auth config for building the docker image
15 | # https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#option-3-use-docker_auth_config
16 | - mkdir -p $HOME/.docker
17 | - echo $DOCKER_AUTH_CONFIG > $HOME/.docker/config.json
18 |
19 | - apk add --no-cache curl git
20 |
21 | - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
22 | - docker pull $CI_REGISTRY/aboutyou/scayle/core-engine/storefront-unit/storefront-api/sdks/generator:latest
23 |
24 | stages:
25 | - build
26 |
27 | Build SDK:
28 | stage: build
29 | script:
30 | - git clone https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/aboutyou/scayle/core-engine/storefront-unit/storefront-api/storefront-api.git .api
31 | - cd .api
32 | - git checkout $API_COMMIT_SHA
33 | - cd ..
34 |
35 | - echo "Initializing Git"
36 | - git config user.name $SDK_AUTOMATION_USER
37 | - git config user.email $SDK_AUTOMATION_EMAIL
38 | - git remote set-url origin https://$SDK_AUTOMATION_USER:$SDK_AUTOMATION_TOKEN@gitlab.com/${CI_PROJECT_PATH}.git
39 |
40 | - git checkout -b chore/update-sdk-$API_COMMIT_SHA
41 |
42 | - cp .api/http/docs/swagger.yaml swagger.yaml
43 | - docker run
44 | --rm
45 | -v "$(pwd)":/gen/out/php
46 | -v "$(pwd)/swagger.yaml":/gen/swagger.yaml
47 | $CI_REGISTRY/aboutyou/scayle/core-engine/storefront-unit/storefront-api/sdks/generator:latest
48 | php ./src/CodeGen.php generate php-storefront swagger.yaml
49 |
50 | - git add .
51 | - 'git commit -m "chore: Upgrade to $API_COMMIT_SHA"'
52 | - git push -o merge_request.create --set-upstream origin chore/update-sdk-$API_COMMIT_SHA
53 |
--------------------------------------------------------------------------------
/lib/Models/Category.php:
--------------------------------------------------------------------------------
1 | \Scayle\StorefrontApi\Models\Category::class,
32 | ];
33 |
34 | protected $collectionClassMap = [
35 | 'children' => \Scayle\StorefrontApi\Models\Category::class,
36 | ];
37 |
38 | protected $collection2dClassMap = [
39 | ];
40 |
41 | protected $polymorphic = [
42 | ];
43 |
44 | protected $polymorphicCollections = [
45 | ];
46 | }
--------------------------------------------------------------------------------
/lib/Services/WishlistService.php:
--------------------------------------------------------------------------------
1 | request('post', $this->resolvePath('wishlists/%s/items', $wishlistId), $combinedOptions, \Scayle\StorefrontApi\Models\Wishlist::class, $model);
28 | }
29 |
30 | /**
31 | * Description
32 | *
33 | * @param string $wishlistId
34 | * @param array $options additional options like limit or filters
35 | *
36 | * @return \Scayle\StorefrontApi\Models\Wishlist
37 | * @throws ClientExceptionInterface
38 | * @throws ApiErrorException
39 | */
40 | public function get($wishlistId, $options = [])
41 | {
42 | $combinedOptions = $options;
43 |
44 | return $this->request('get', $this->resolvePath('wishlists/%s', $wishlistId), $combinedOptions, \Scayle\StorefrontApi\Models\Wishlist::class);
45 | }
46 |
47 | /**
48 | * Description
49 | *
50 | * @param string $wishlistId
51 | * @param string $itemKey
52 | * @param array $options additional options like limit or filters
53 | *
54 | * @return \Scayle\StorefrontApi\Models\Wishlist
55 | * @throws ClientExceptionInterface
56 | * @throws ApiErrorException
57 | */
58 | public function remove($wishlistId, $itemKey, $options = [])
59 | {
60 | $combinedOptions = $options;
61 |
62 | return $this->request('delete', $this->resolvePath('wishlists/%s/items/%s', $wishlistId, $itemKey), $combinedOptions, \Scayle\StorefrontApi\Models\Wishlist::class);
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/tests/WishlistTest.php:
--------------------------------------------------------------------------------
1 | loadFixture('WishlistAddItemRequest.json');
12 |
13 | $requestEntity = new \Scayle\StorefrontApi\Models\CreateWishlistBody($expectedRequestJson);
14 | $this->assertJsonStringEqualsJsonString(json_encode($expectedRequestJson), $requestEntity->toJson());
15 |
16 | $responseEntity = $this->api->wishlists->AddItem('1', $requestEntity, []);
17 |
18 | $expectedResponseJson = $this->loadFixture('WishlistAddItemResponse.json');
19 | $this->assertInstanceOf(\Scayle\StorefrontApi\Models\Wishlist::class, $responseEntity);
20 | $this->assertJsonStringEqualsJsonString(json_encode($expectedResponseJson), $responseEntity->toJson());
21 |
22 | $this->assertPropertyHasTheCorrectType($responseEntity, 'items', \Scayle\StorefrontApi\Models\WishlistItem::class);
23 |
24 |
25 |
26 | }
27 |
28 | public function testGet()
29 | {
30 | $responseEntity = $this->api->wishlists->Get('1', []);
31 |
32 | $expectedResponseJson = $this->loadFixture('WishlistGetResponse.json');
33 | $this->assertInstanceOf(\Scayle\StorefrontApi\Models\Wishlist::class, $responseEntity);
34 | $this->assertJsonStringEqualsJsonString(json_encode($expectedResponseJson), $responseEntity->toJson());
35 |
36 | $this->assertPropertyHasTheCorrectType($responseEntity, 'items', \Scayle\StorefrontApi\Models\WishlistItem::class);
37 |
38 |
39 |
40 | }
41 |
42 | public function testRemove()
43 | {
44 | $responseEntity = $this->api->wishlists->Remove('1', '1', []);
45 |
46 | $expectedResponseJson = $this->loadFixture('WishlistRemoveResponse.json');
47 | $this->assertInstanceOf(\Scayle\StorefrontApi\Models\Wishlist::class, $responseEntity);
48 | $this->assertJsonStringEqualsJsonString(json_encode($expectedResponseJson), $responseEntity->toJson());
49 |
50 | $this->assertPropertyHasTheCorrectType($responseEntity, 'items', \Scayle\StorefrontApi\Models\WishlistItem::class);
51 |
52 |
53 |
54 | }
55 |
56 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 |
4 | SCAYLE Storefront API PHP SDK
5 |
6 |
7 |
11 |
12 |
13 | The SCAYLE Storefront API PHP SDK streamlines interactions with Storefront REST APIs.
14 |
15 |
16 |
17 |
18 |
19 | ## Getting Started
20 |
21 | Visit the [Quickstart Guide](https://new.scayle.dev/en/developer-guide/introduction/apis#sdk-setup) to set up the Storefront API PHP SDK
22 |
23 | Visit the [Docs](https://new.scayle.dev) to learn more about our system requirements.
24 |
25 | ## Installation
26 | ```PHP
27 | # Install the PHP library with Composer
28 | `composer require scayle/storefront-api`
29 |
30 | # Initialise SDK
31 | use Scayle\StorefrontApi\StorefrontClient;
32 |
33 | $client = new StorefrontClient([
34 | 'apiUrl' => 'https://{{tenant-space}}.storefront.api.scayle.cloud/v1/',
35 | 'shopId' => 1001,
36 | 'auth' => [
37 | 'type' => 'token',
38 | 'token' => '{{Access-Token}}',
39 | ],
40 | ]);
41 | ```
42 | ## What is Scayle ?
43 |
44 | [SCAYLE](https://scayle.com) is a full-featured e-commerce software solution that comes with flexible APIs. Within SCAYLE, you can manage all aspects of your shop, such as products, stocks, customers, and transactions.
45 |
46 | Learn more about Scayles’s architecture and commerce modules in the [Docs] (https://new.scayle.dev/en/user-guide).
47 |
48 |
49 |
50 | ## Community
51 |
52 | The community and core teams are available in [GitHub](https://github.com/scayle/), where you can ask for support, discuss roadmap, and share ideas.
53 |
54 | ## Other channels
55 |
56 | - [GitHub Issues](https://github.com/scayle/storefront-api-php-sdk/issues)
57 | - [LinkedIn](https://www.linkedin.com/company/scaylecommerce/)
58 | - [Jobs](https://careers.smartrecruiters.com/ABOUTYOUGmbH/scayle)
59 | - [AboutYou Tech Blog](https://aboutyou.tech/)
60 |
61 | ## License
62 | Licensed under the [MIT](https://opensource.org/license/mit/)
63 |
--------------------------------------------------------------------------------
/lib/Models/Product.php:
--------------------------------------------------------------------------------
1 | $advancedAttributes
8 | * @property array $attributes
9 | * @property BaseCategory[] $baseCategories
10 | * @property ProductCategory[][] $categories
11 | * @property DefiningAttribute $definingAttributes
12 | * @property Image[] $images
13 | * @property ResponseCustomData $customData
14 | * @property bool $isActive Identifies whether a product is active or not
15 | * @property bool $isNew Identifies whether a product is new or not
16 | * @property bool $isSoldOut Identifies if a product is still available to sell
17 | * @property string $masterKey Identifies the master product which this product belongs
18 | * @property Timestamp $firstLiveAt
19 | * @property PriceRange $priceRange
20 | * @property ReductionRange $reductionRange
21 | * @property LowestPriorPrice $lowestPriorPrice
22 | * @property string $referenceKey
23 | * @property int[] $searchCategoryIds
24 | * @property Product[] $siblings list of Products
25 | * @property Variant[] $variants
26 | * @property Timestamp $createdAt
27 | * @property Timestamp $updatedAt
28 | * @property Timestamp $indexedAt
29 | */
30 | class Product extends ApiObject
31 | {
32 | protected $defaultValues = [
33 |
34 | ];
35 |
36 | protected $classMap = [
37 | 'definingAttributes' => \Scayle\StorefrontApi\Models\DefiningAttribute::class,
38 | 'priceRange' => \Scayle\StorefrontApi\Models\PriceRange::class,
39 | 'reductionRange' => \Scayle\StorefrontApi\Models\ReductionRange::class,
40 | 'lowestPriorPrice' => \Scayle\StorefrontApi\Models\LowestPriorPrice::class,
41 | ];
42 |
43 | protected $collectionClassMap = [
44 | 'attributes' => \Scayle\StorefrontApi\Models\Attribute::class,
45 | 'advancedAttributes' => \Scayle\StorefrontApi\Models\AdvancedAttribute::class,
46 | 'images' => \Scayle\StorefrontApi\Models\Image::class,
47 | 'siblings' => \Scayle\StorefrontApi\Models\Product::class,
48 | 'baseCategories' => \Scayle\StorefrontApi\Models\BaseCategory::class,
49 | 'variants' => \Scayle\StorefrontApi\Models\Variant::class,
50 | ];
51 |
52 | protected $collection2dClassMap = [
53 | 'categories' => \Scayle\StorefrontApi\Models\ProductCategory::class,
54 | ];
55 |
56 | protected $polymorphic = [
57 | ];
58 |
59 | protected $polymorphicCollections = [
60 | ];
61 | }
--------------------------------------------------------------------------------
/lib/Services/CategoryService.php:
--------------------------------------------------------------------------------
1 | request('get', 'categories', $combinedOptions, \Scayle\StorefrontApi\Models\CategoryCollection::class);
26 | }
27 |
28 | /**
29 | * Description
30 | *
31 | * @param array $categoryIds
32 | * @param array $options additional options like limit or filters
33 | *
34 | * @return \Scayle\StorefrontApi\Models\CategoryCollection
35 | * @throws ClientExceptionInterface
36 | * @throws ApiErrorException
37 | */
38 | public function getByIds($categoryIds, $options = [])
39 | {
40 | $combinedOptions = $options;
41 | $combinedOptions["ids"] = implode(',', $categoryIds);
42 |
43 | return $this->request('get', 'categories', $combinedOptions, \Scayle\StorefrontApi\Models\CategoryCollection::class);
44 | }
45 |
46 | /**
47 | * Description
48 | *
49 | * @param int $categoryId
50 | * @param array $options additional options like limit or filters
51 | *
52 | * @return \Scayle\StorefrontApi\Models\Category
53 | * @throws ClientExceptionInterface
54 | * @throws ApiErrorException
55 | */
56 | public function getById($categoryId, $options = [])
57 | {
58 | $combinedOptions = $options;
59 |
60 | return $this->request('get', $this->resolvePath('categories/%s', $categoryId), $combinedOptions, \Scayle\StorefrontApi\Models\Category::class);
61 | }
62 |
63 | /**
64 | * Description
65 | *
66 | * @param string $categoryPath
67 | * @param array $options additional options like limit or filters
68 | *
69 | * @return \Scayle\StorefrontApi\Models\Category
70 | * @throws ClientExceptionInterface
71 | * @throws ApiErrorException
72 | */
73 | public function getByPath($categoryPath, $options = [])
74 | {
75 | $combinedOptions = $options;
76 |
77 | return $this->request('get', $this->resolvePath('categories/%s', $categoryPath), $combinedOptions, \Scayle\StorefrontApi\Models\Category::class);
78 | }
79 |
80 | }
81 |
--------------------------------------------------------------------------------
/tests/BasketTest.php:
--------------------------------------------------------------------------------
1 | api->baskets->Get('1', []);
12 |
13 | $expectedResponseJson = $this->loadFixture('BasketGetResponse.json');
14 | $this->assertInstanceOf(\Scayle\StorefrontApi\Models\Basket::class, $responseEntity);
15 | $this->assertJsonStringEqualsJsonString(json_encode($expectedResponseJson), $responseEntity->toJson());
16 |
17 |
18 |
19 |
20 | }
21 |
22 | public function testAddItem()
23 | {
24 | $expectedRequestJson = $this->loadFixture('BasketAddItemRequest.json');
25 |
26 | $requestEntity = new \Scayle\StorefrontApi\Models\CreateBasketItemBody($expectedRequestJson);
27 | $this->assertJsonStringEqualsJsonString(json_encode($expectedRequestJson), $requestEntity->toJson());
28 |
29 | $responseEntity = $this->api->baskets->AddItem('1', $requestEntity, []);
30 |
31 | $expectedResponseJson = $this->loadFixture('BasketAddItemResponse.json');
32 | $this->assertInstanceOf(\Scayle\StorefrontApi\Models\Basket::class, $responseEntity);
33 | $this->assertJsonStringEqualsJsonString(json_encode($expectedResponseJson), $responseEntity->toJson());
34 |
35 |
36 |
37 |
38 | }
39 |
40 | public function testRemove()
41 | {
42 | $responseEntity = $this->api->baskets->Remove('1', '1', []);
43 |
44 | $expectedResponseJson = $this->loadFixture('BasketRemoveResponse.json');
45 | $this->assertInstanceOf(\Scayle\StorefrontApi\Models\Basket::class, $responseEntity);
46 | $this->assertJsonStringEqualsJsonString(json_encode($expectedResponseJson), $responseEntity->toJson());
47 |
48 |
49 |
50 |
51 | }
52 |
53 | public function testUpdate()
54 | {
55 | $expectedRequestJson = $this->loadFixture('BasketUpdateRequest.json');
56 |
57 | $requestEntity = new \Scayle\StorefrontApi\Models\UpdateBasketItemBody($expectedRequestJson);
58 | $this->assertJsonStringEqualsJsonString(json_encode($expectedRequestJson), $requestEntity->toJson());
59 |
60 | $responseEntity = $this->api->baskets->Update('1', '1', $requestEntity, []);
61 |
62 | $expectedResponseJson = $this->loadFixture('BasketUpdateResponse.json');
63 | $this->assertInstanceOf(\Scayle\StorefrontApi\Models\Basket::class, $responseEntity);
64 | $this->assertJsonStringEqualsJsonString(json_encode($expectedResponseJson), $responseEntity->toJson());
65 |
66 |
67 |
68 |
69 | }
70 |
71 | }
--------------------------------------------------------------------------------
/lib/Services/BasketService.php:
--------------------------------------------------------------------------------
1 | request('get', $this->resolvePath('baskets/%s', $basketId), $combinedOptions, \Scayle\StorefrontApi\Models\Basket::class);
27 | }
28 |
29 | /**
30 | * Description
31 | *
32 | * @param string $basketId
33 | * @param \Scayle\StorefrontApi\Models\CreateBasketItemBody $model the model to create or update
34 | * @param array $options additional options like limit or filters
35 | *
36 | * @return \Scayle\StorefrontApi\Models\Basket
37 | * @throws ClientExceptionInterface
38 | * @throws ApiErrorException
39 | */
40 | public function addItem($basketId, $model, $options = [])
41 | {
42 | $combinedOptions = $options;
43 |
44 | return $this->request('post', $this->resolvePath('baskets/%s/items', $basketId), $combinedOptions, \Scayle\StorefrontApi\Models\Basket::class, $model);
45 | }
46 |
47 | /**
48 | * Description
49 | *
50 | * @param string $basketId
51 | * @param string $itemKey
52 | * @param array $options additional options like limit or filters
53 | *
54 | * @return \Scayle\StorefrontApi\Models\Basket
55 | * @throws ClientExceptionInterface
56 | * @throws ApiErrorException
57 | */
58 | public function remove($basketId, $itemKey, $options = [])
59 | {
60 | $combinedOptions = $options;
61 |
62 | return $this->request('delete', $this->resolvePath('baskets/%s/items/%s', $basketId, $itemKey), $combinedOptions, \Scayle\StorefrontApi\Models\Basket::class);
63 | }
64 |
65 | /**
66 | * Description
67 | *
68 | * @param string $basketId
69 | * @param string $itemKey
70 | * @param \Scayle\StorefrontApi\Models\UpdateBasketItemBody $model the model to create or update
71 | * @param array $options additional options like limit or filters
72 | *
73 | * @return \Scayle\StorefrontApi\Models\Basket
74 | * @throws ClientExceptionInterface
75 | * @throws ApiErrorException
76 | */
77 | public function update($basketId, $itemKey, $model, $options = [])
78 | {
79 | $combinedOptions = $options;
80 |
81 | return $this->request('patch', $this->resolvePath('baskets/%s/items/%s', $basketId, $itemKey), $combinedOptions, \Scayle\StorefrontApi\Models\Basket::class, $model);
82 | }
83 |
84 | }
85 |
--------------------------------------------------------------------------------
/lib/Services/AbstractService.php:
--------------------------------------------------------------------------------
1 | client = $client;
24 | }
25 |
26 | /**
27 | * @param string $method the http method
28 | * @param string $relativeUrl the relative url of endpoint
29 | * @param string|null $modelClass the classname of which the response gets transformed to
30 | * @param array $options array of additional options
31 | * @param ApiObject|null $body the request body object
32 | *
33 | * @return mixed|null
34 | *
35 | * @throws ClientExceptionInterface
36 | * @throws ApiErrorException
37 | */
38 | protected function request($method, $relativeUrl, $options = [], $modelClass = null, $body = null)
39 | {
40 | try {
41 | if ($body instanceof ApiObject) {
42 | $body = $body->toJson();
43 | } elseif ($body !== null) {
44 | $body = json_encode($body);
45 | }
46 |
47 | $response = $this->client->request($method, $relativeUrl, $options, $body);
48 | $statusCode = $response->getStatusCode();
49 |
50 | $responseBody = $response->getBody()->getContents();
51 | // Catching all NON 2xx status codes for further error processing
52 | if ($statusCode < 200 || $statusCode >= 300) {
53 | $responseJson = json_decode($responseBody, true);
54 | throw new ApiErrorException(is_null($responseJson) ? [] : $responseJson, $statusCode);
55 | }
56 |
57 | if ($responseBody && $modelClass && class_exists($modelClass)) {
58 | $responseJson = json_decode($responseBody, true);
59 |
60 | if (!is_subclass_of($modelClass, ApiCollection::class)) {
61 | return new $modelClass($responseJson);
62 | }
63 |
64 | return new $modelClass(['entities' => $responseJson]);
65 | } else {
66 | return json_decode($responseBody, true);
67 | }
68 | } catch (ClientExceptionInterface $e) {
69 | throw $e;
70 | }
71 | }
72 |
73 | /**
74 | * @param string $path
75 | * @param mixed ...$params
76 | *
77 | * @return string
78 | */
79 | protected function resolvePath($path, ...$params)
80 | {
81 | return vsprintf($path, $params);
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/tests/CategoryTest.php:
--------------------------------------------------------------------------------
1 | api->categories->GetRoots( []);
12 |
13 | $expectedResponseJson = $this->loadFixture('CategoryGetRootsResponse.json');
14 | $this->assertInstanceOf(\Scayle\StorefrontApi\Models\CategoryCollection::class, $responseEntity);
15 | $this->assertJsonStringEqualsJsonString(json_encode($expectedResponseJson), $responseEntity->toJson());
16 |
17 | $this->assertPropertyHasTheCorrectType($responseEntity, 'parent', \Scayle\StorefrontApi\Models\Category::class);
18 | $this->assertPropertyHasTheCorrectType($responseEntity, 'children', \Scayle\StorefrontApi\Models\Category::class);
19 |
20 |
21 | foreach ($responseEntity->getEntities() as $collectionEntity) {
22 | $this->assertInstanceOf(\Scayle\StorefrontApi\Models\Category::class, $collectionEntity);
23 | $this->assertPropertyHasTheCorrectType($collectionEntity, 'parent', \Scayle\StorefrontApi\Models\Category::class);
24 | $this->assertPropertyHasTheCorrectType($collectionEntity, 'children', \Scayle\StorefrontApi\Models\Category::class);
25 |
26 | }
27 | }
28 |
29 | public function testGetByIds()
30 | {
31 | $responseEntity = $this->api->categories->GetByIds('1', []);
32 |
33 | $expectedResponseJson = $this->loadFixture('CategoryGetByIdsResponse.json');
34 | $this->assertInstanceOf(\Scayle\StorefrontApi\Models\CategoryCollection::class, $responseEntity);
35 | $this->assertJsonStringEqualsJsonString(json_encode($expectedResponseJson), $responseEntity->toJson());
36 |
37 | $this->assertPropertyHasTheCorrectType($responseEntity, 'parent', \Scayle\StorefrontApi\Models\Category::class);
38 | $this->assertPropertyHasTheCorrectType($responseEntity, 'children', \Scayle\StorefrontApi\Models\Category::class);
39 |
40 |
41 | foreach ($responseEntity->getEntities() as $collectionEntity) {
42 | $this->assertInstanceOf(\Scayle\StorefrontApi\Models\Category::class, $collectionEntity);
43 | $this->assertPropertyHasTheCorrectType($collectionEntity, 'parent', \Scayle\StorefrontApi\Models\Category::class);
44 | $this->assertPropertyHasTheCorrectType($collectionEntity, 'children', \Scayle\StorefrontApi\Models\Category::class);
45 |
46 | }
47 | }
48 |
49 | public function testGetById()
50 | {
51 | $responseEntity = $this->api->categories->GetById('1', []);
52 |
53 | $expectedResponseJson = $this->loadFixture('CategoryGetByIdResponse.json');
54 | $this->assertInstanceOf(\Scayle\StorefrontApi\Models\Category::class, $responseEntity);
55 | $this->assertJsonStringEqualsJsonString(json_encode($expectedResponseJson), $responseEntity->toJson());
56 |
57 | $this->assertPropertyHasTheCorrectType($responseEntity, 'parent', \Scayle\StorefrontApi\Models\Category::class);
58 | $this->assertPropertyHasTheCorrectType($responseEntity, 'children', \Scayle\StorefrontApi\Models\Category::class);
59 |
60 |
61 |
62 | }
63 |
64 | public function testGetByPath()
65 | {
66 | $responseEntity = $this->api->categories->GetByPath('1', []);
67 |
68 | $expectedResponseJson = $this->loadFixture('CategoryGetByPathResponse.json');
69 | $this->assertInstanceOf(\Scayle\StorefrontApi\Models\Category::class, $responseEntity);
70 | $this->assertJsonStringEqualsJsonString(json_encode($expectedResponseJson), $responseEntity->toJson());
71 |
72 | $this->assertPropertyHasTheCorrectType($responseEntity, 'parent', \Scayle\StorefrontApi\Models\Category::class);
73 | $this->assertPropertyHasTheCorrectType($responseEntity, 'children', \Scayle\StorefrontApi\Models\Category::class);
74 |
75 |
76 |
77 | }
78 |
79 | }
--------------------------------------------------------------------------------
/tests/ProductTest.php:
--------------------------------------------------------------------------------
1 | api->products->GetById('1', []);
12 |
13 | $expectedResponseJson = $this->loadFixture('ProductGetByIdResponse.json');
14 | $this->assertInstanceOf(\Scayle\StorefrontApi\Models\Product::class, $responseEntity);
15 | $this->assertJsonStringEqualsJsonString(json_encode($expectedResponseJson), $responseEntity->toJson());
16 |
17 | $this->assertPropertyHasTheCorrectType($responseEntity, 'attributes', \Scayle\StorefrontApi\Models\Attribute::class);
18 | $this->assertPropertyHasTheCorrectType($responseEntity, 'advancedAttributes', \Scayle\StorefrontApi\Models\AdvancedAttribute::class);
19 | $this->assertPropertyHasTheCorrectType($responseEntity, 'categories', \Scayle\StorefrontApi\Models\ProductCategory::class);
20 | $this->assertPropertyHasTheCorrectType($responseEntity, 'definingAttributes', \Scayle\StorefrontApi\Models\DefiningAttribute::class);
21 | $this->assertPropertyHasTheCorrectType($responseEntity, 'images', \Scayle\StorefrontApi\Models\Image::class);
22 | $this->assertPropertyHasTheCorrectType($responseEntity, 'priceRange', \Scayle\StorefrontApi\Models\PriceRange::class);
23 | $this->assertPropertyHasTheCorrectType($responseEntity, 'reductionRange', \Scayle\StorefrontApi\Models\ReductionRange::class);
24 | $this->assertPropertyHasTheCorrectType($responseEntity, 'lowestPriorPrice', \Scayle\StorefrontApi\Models\LowestPriorPrice::class);
25 | $this->assertPropertyHasTheCorrectType($responseEntity, 'siblings', \Scayle\StorefrontApi\Models\Product::class);
26 | $this->assertPropertyHasTheCorrectType($responseEntity, 'baseCategories', \Scayle\StorefrontApi\Models\BaseCategory::class);
27 | $this->assertPropertyHasTheCorrectType($responseEntity, 'variants', \Scayle\StorefrontApi\Models\Variant::class);
28 |
29 |
30 |
31 | }
32 |
33 | public function testQuery()
34 | {
35 | $responseEntity = $this->api->products->Query( []);
36 |
37 | $expectedResponseJson = $this->loadFixture('ProductQueryResponse.json');
38 | $this->assertInstanceOf(\Scayle\StorefrontApi\Models\ProductsResponse::class, $responseEntity);
39 | $this->assertJsonStringEqualsJsonString(json_encode($expectedResponseJson), $responseEntity->toJson());
40 |
41 | $this->assertPropertyHasTheCorrectType($responseEntity, 'attributes', \Scayle\StorefrontApi\Models\Attribute::class);
42 | $this->assertPropertyHasTheCorrectType($responseEntity, 'advancedAttributes', \Scayle\StorefrontApi\Models\AdvancedAttribute::class);
43 | $this->assertPropertyHasTheCorrectType($responseEntity, 'categories', \Scayle\StorefrontApi\Models\ProductCategory::class);
44 | $this->assertPropertyHasTheCorrectType($responseEntity, 'definingAttributes', \Scayle\StorefrontApi\Models\DefiningAttribute::class);
45 | $this->assertPropertyHasTheCorrectType($responseEntity, 'images', \Scayle\StorefrontApi\Models\Image::class);
46 | $this->assertPropertyHasTheCorrectType($responseEntity, 'priceRange', \Scayle\StorefrontApi\Models\PriceRange::class);
47 | $this->assertPropertyHasTheCorrectType($responseEntity, 'reductionRange', \Scayle\StorefrontApi\Models\ReductionRange::class);
48 | $this->assertPropertyHasTheCorrectType($responseEntity, 'lowestPriorPrice', \Scayle\StorefrontApi\Models\LowestPriorPrice::class);
49 | $this->assertPropertyHasTheCorrectType($responseEntity, 'siblings', \Scayle\StorefrontApi\Models\Product::class);
50 | $this->assertPropertyHasTheCorrectType($responseEntity, 'baseCategories', \Scayle\StorefrontApi\Models\BaseCategory::class);
51 | $this->assertPropertyHasTheCorrectType($responseEntity, 'variants', \Scayle\StorefrontApi\Models\Variant::class);
52 |
53 |
54 |
55 | }
56 |
57 | }
--------------------------------------------------------------------------------
/tests/BaseApiTestCase.php:
--------------------------------------------------------------------------------
1 | api = new StorefrontClient([
22 | 'apiUrl' => getenv('API_URL') ? getenv('API_URL') : 'http://127.0.0.1:4010',
23 | 'accessToken' => 'abc123',
24 | ]);
25 | }
26 |
27 | /**
28 | * Gets a protected property from an ApiObject
29 | *
30 | * @param \Scayle\StorefrontApi\Models\ApiObject $apiObject
31 | * @param string $propertyName
32 | *
33 | * @return mixed|null
34 | *
35 | * @throws \ReflectionException
36 | */
37 | private function getProtectedProperty($apiObject, $propertyName)
38 | {
39 | $reflect = new \ReflectionClass($apiObject);
40 |
41 | $props = $reflect->getProperties(ReflectionProperty::IS_PROTECTED);
42 |
43 | foreach ($props as $prop) {
44 | if ($prop->getName() === $propertyName) {
45 | $prop->setAccessible(true);
46 |
47 | return $prop->getValue($apiObject);
48 | }
49 | }
50 |
51 | return null;
52 | }
53 |
54 | /**
55 | * @param \Scayle\StorefrontApi\Models\ApiObject $apiObject an ApiObject instance
56 | * @param string $propertyName the property to type check
57 | * @param string $className the expected classname
58 | * @throws \ReflectionException
59 | */
60 | protected function assertPropertyHasTheCorrectType($apiObject, $propertyName, $className)
61 | {
62 | $attributes = $this->getProtectedProperty($apiObject, '_attributes');
63 |
64 | foreach ($attributes as $objPropertyName => $objPropertyValue) {
65 | if ($objPropertyName === $propertyName) {
66 | if (is_array($objPropertyValue)) {
67 | foreach ($objPropertyValue as $objPropertyItem) {
68 | $this->assertInstanceOf($className, $objPropertyItem);
69 | }
70 | } else {
71 | $this->assertInstanceOf($className, $objPropertyValue);
72 | }
73 |
74 | break;
75 | }
76 | }
77 | }
78 |
79 | /**
80 | * @param ApiObject $apiObject an ApiObject instance
81 | * @param string $propertyName the property to type check
82 | * @param string $discriminator the discriminator field name
83 | * @param array $mapping mapping of discriminator value to concrete class
84 | * @throws \ReflectionException
85 | */
86 | protected function assertPropertyHasCorrectPolymorphicType($apiObject, $propertyName, $discriminator, $mapping)
87 | {
88 | $attributes = $this->getProtectedProperty($apiObject, '_attributes');
89 |
90 | foreach ($attributes as $objPropertyName => $objPropertyValue) {
91 | if ($objPropertyName === $propertyName) {
92 | if (is_array($objPropertyValue)) {
93 | foreach ($objPropertyValue as $objPropertyItem) {
94 | $discriminatorValue = $objPropertyItem->{$discriminator};
95 | $this->assertArrayHasKey($discriminatorValue, $mapping);
96 | $className = $mapping[$discriminatorValue];
97 | $this->assertInstanceOf($className, $objPropertyItem);
98 | }
99 | } else {
100 | $discriminatorValue = $objPropertyValue->{$discriminator};
101 | $className = $mapping[$discriminatorValue];
102 | $this->assertArrayHasKey($discriminatorValue, $mapping);
103 | $this->assertInstanceOf($className, $objPropertyValue);
104 | }
105 |
106 | break;
107 | }
108 | }
109 | }
110 |
111 | protected function loadFixture(string $filename) : array
112 | {
113 | $filename = __DIR__ . '/fixtures/' . $filename;
114 | $this->assertFileExists($filename, "Fixtures do not exist. Are you sure you have valid request and response examples in your OpenAPI specification?");
115 | return json_decode(file_get_contents($filename), true);
116 | }
117 | }
--------------------------------------------------------------------------------
/lib/AbstractApi.php:
--------------------------------------------------------------------------------
1 |
35 | */
36 | private $config;
37 |
38 | /**
39 | * AbstractAdminApi constructor.
40 | * @param array $config
41 | * @example ['apiUrl' => 'http://cloud.aboutyou.com', 'accessToken' => 'myToken']
42 | * @param ClientInterface $httpClient
43 | */
44 | public function __construct($config = [], $httpClient = null)
45 | {
46 | $this->validateConfig($config);
47 |
48 | $this->config = $config;
49 | $this->httpClient = $httpClient ?: new Client();
50 | }
51 |
52 | /**
53 | * @return string
54 | */
55 | public function getApiUrl()
56 | {
57 | return $this->config[self::API_URL];
58 | }
59 |
60 | /**
61 | * @return array
62 | */
63 | public function getAuth()
64 | {
65 | return $this->config[self::AUTH];
66 | }
67 |
68 | /**
69 | * @return string
70 | */
71 | public function getShopId()
72 | {
73 | return $this->config[self::SHOP_ID];
74 | }
75 |
76 | /**
77 | * @param string $method
78 | * @param string $relativePath
79 | * @param array $options
80 | * @param null|string $body
81 | *
82 | * @return ResponseInterface
83 | *
84 | * @throws ClientExceptionInterface
85 | */
86 | public function request($method, $relativePath, $options = [], $body = null)
87 | {
88 | $url = $this->getApiUrl() . $relativePath . $this->makeQueryString($options);
89 |
90 | $headers = $this->getAuthHeader();
91 | $headers[self::SHOP_HEADER_NAME] = $this->getShopId();
92 | $headers['Content-Type'] = 'application/json';
93 | $headers['Accept'] = 'application/json, */*';
94 | $headers['X-SDK'] = 'php/' . self::VERSION;
95 |
96 | $request = new Request($method, $url, $headers, $body);
97 | return $this->httpClient->sendRequest($request);
98 | }
99 |
100 | private function getAuthHeader()
101 | {
102 | $auth = $this->getAuth();
103 | if (array_key_exists(self::AUTH_TOKEN, $auth)) {
104 | return [
105 | self::AUTH_HEADER_NAME => $auth[self::AUTH_TOKEN],
106 | ];
107 | }
108 |
109 | $credentials = base64_encode($auth[self::AUTH_USERNAME] . ':' . $auth[self::AUTH_PASSWORD]);
110 |
111 | return [
112 | 'Authorization' => 'Basic ' . $credentials,
113 | ];
114 | }
115 |
116 | private function makeQueryString(array $options): string
117 | {
118 | if (empty($options)) {
119 | return '';
120 | }
121 |
122 | foreach ($options as &$value) {
123 | if (is_bool($value)) {
124 | $value = $value ? 'true' : 'false';
125 | }
126 | }
127 |
128 | unset($value);
129 |
130 | return '?' . http_build_query($options);
131 | }
132 |
133 | /**
134 | * @param array $config
135 | *
136 | * @throws InvalidArgumentException
137 | */
138 | private function validateConfig($config)
139 | {
140 | if (empty($config[self::API_URL])) {
141 | $message = sprintf('%s cannot be empty', self::API_URL);
142 | throw new InvalidArgumentException($message);
143 | }
144 |
145 | if (empty($config[self::SHOP_ID])) {
146 | $message = sprintf('%s cannot be empty', self::SHOP_ID);
147 | throw new InvalidArgumentException($message);
148 | }
149 |
150 | if (empty($config[self::AUTH])) {
151 | $message = sprintf('%s cannot be empty', self::AUTH);
152 | throw new InvalidArgumentException($message);
153 | }
154 |
155 | $auth = $config[self::AUTH];
156 | if ((empty($auth[self::AUTH_TYPE]) || empty($auth[self::AUTH_TOKEN])) && (empty($auth[self::AUTH_USERNAME]) || empty($auth[self::AUTH_PASSWORD]))) {
157 | $message = sprintf('%s array must consist of either type and token or username and password', self::AUTH);
158 | throw new InvalidArgumentException($message);
159 | }
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/lib/Models/ApiObject.php:
--------------------------------------------------------------------------------
1 | serializer = new \Scayle\StorefrontApi\Serializers\ModelSerializer();
50 | $attributes = $this->mergeDefaultValues($attributes);
51 | $this->_attributes = $this->unserialize($attributes);
52 | }
53 |
54 | public function __set($name, $value)
55 | {
56 | $this->_attributes[$name] = $value;
57 | }
58 |
59 | public function &__get($name)
60 | {
61 | if (array_key_exists($name, $this->_attributes)) {
62 | return $this->_attributes[$name];
63 | }
64 |
65 | $nullRef = null;
66 | return $nullRef;
67 | }
68 |
69 | public function __isset($name)
70 | {
71 | return isset($this->_attributes[$name]);
72 | }
73 |
74 | /**
75 | * @return array
76 | */
77 | public function jsonSerialize()
78 | {
79 | $serialized = [];
80 |
81 | foreach ($this->_attributes as $key => $value) {
82 | if ($value instanceof ApiObject) {
83 | $value = $value->jsonSerialize();
84 | }
85 |
86 | $serialized[$key] = $value;
87 | }
88 |
89 | return $serialized;
90 | }
91 |
92 | /**
93 | * @return false|string
94 | */
95 | public function toJson()
96 | {
97 | return json_encode($this->_attributes);
98 | }
99 |
100 | /**
101 | * @param array $attributes
102 | *
103 | * @return array mixed
104 | */
105 | private function mergeDefaultValues($attributes)
106 | {
107 | $diff = array_diff_key($this->defaultValues, $attributes);
108 | $attributes = array_merge($attributes, $diff);
109 |
110 | return $attributes;
111 | }
112 |
113 | /**
114 | * @param array $attributes
115 | * @return array
116 | */
117 | private function unserialize($attributes)
118 | {
119 | $unserialized = [];
120 |
121 | foreach ($attributes as $key => $value) {
122 | if (is_null($value)) {
123 | $unserialized[$key] = $value;
124 | continue;
125 | }
126 |
127 | // Handle nested single object instantiation
128 | if (array_key_exists($key, $this->classMap)) {
129 | $value = new $this->classMap[$key]($value);
130 | }
131 |
132 | // Handle nested object collection instantiation
133 | if (array_key_exists($key, $this->collectionClassMap)) {
134 | $nestedObjects = [];
135 | foreach ($value as $nestedKey => $nestedValue) {
136 | $nestedObjects[$nestedKey] = $this->serializer->parse($this->collectionClassMap[$key], $nestedValue);
137 | }
138 |
139 | $value = $nestedObjects;
140 | }
141 |
142 | if (array_key_exists($key, $this->collection2dClassMap)) {
143 | $nestedObjects = [];
144 | foreach ($value as $nestedValue) {
145 | $nestedObjects[] = array_map(fn($v) => new $this->collection2dClassMap[$key]($v), $nestedValue);
146 | }
147 |
148 | $value = $nestedObjects;
149 | }
150 |
151 |
152 | // Handle single nested object polymorphism
153 | if (array_key_exists($key, $this->polymorphic)) {
154 | $discriminator = $this->polymorphic[$key]['discriminator'];
155 | $discriminatorValue = $attributes[$discriminator];
156 | $className = $this->polymorphic[$key]['mapping'][$discriminatorValue];
157 | $value = new $className([$key => $value]);
158 | }
159 |
160 | // Handle nested object collection polymorphism
161 | if (array_key_exists($key, $this->polymorphicCollections)) {
162 | $discriminator = $this->polymorphicCollections[$key]['discriminator'];
163 | $objects = [];
164 | foreach ($attributes[$key] as $nestedAttribute) {
165 | $discriminatorValue = $nestedAttribute[$discriminator];
166 | if (array_key_exists($discriminatorValue, $this->polymorphicCollections[$key]['mapping'])) {
167 | $className = $this->polymorphicCollections[$key]['mapping'][$discriminatorValue];
168 | $objects[] = new $className($nestedAttribute);
169 | }
170 | }
171 |
172 | $value = $objects;
173 | }
174 |
175 | $unserialized[$key] = $value;
176 | }
177 |
178 | return $unserialized;
179 | }
180 | }
181 |
182 |
--------------------------------------------------------------------------------