From 72f8a41abc0044567457e0d965fa4a8ede684a38 Mon Sep 17 00:00:00 2001 From: Volodymyr Stelmakh Date: Tue, 30 Apr 2024 17:14:43 +0200 Subject: [PATCH 01/12] update readme to refer new documentation --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f94a499..a08cb69 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Alibaba SDK [![build](https://github.com/kyto-gmbh/alibaba-sdk-php/actions/workflows/build.yml/badge.svg)](https://github.com/kyto-gmbh/alibaba-sdk-php/actions/workflows/build.yml) -Alibaba SDK for PHP. This package provides a structured interface to communicate with [Alibaba Open Platform](https://developer.alibaba.com/en/doc.htm?spm=a219a.7629140.0.0.188675fe5JPvEa#?docType=1&docId=118496). +Alibaba SDK for PHP. This package provides a structured interface to communicate with [Alibaba Open Platform](https://openapi.alibaba.com/doc/doc.htm?spm=a2o9m.11223882.0.0.1566722cTOuz7W#/?docId=19). > Note, package is in development therefore public interface could be changed in future releases. @@ -23,7 +23,7 @@ require __DIR__ . '/vendor/autoload.php'; use Kyto\Alibaba\Facade; -$alibaba = Facade::create('api-key', 'api-secret'); +$alibaba = Facade::create('app-key', 'app-secret'); $alibaba->category->get('0'); // @return Kyto\Alibaba\Model\Category ``` From 6f60630d1ff023f10eaa07abe966e237a78624b8 Mon Sep 17 00:00:00 2001 From: Volodymyr Stelmakh Date: Tue, 30 Apr 2024 17:16:32 +0200 Subject: [PATCH 02/12] update facade to use new authorization url --- src/Facade.php | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/Facade.php b/src/Facade.php index 91a9dd0..ed9bd05 100644 --- a/src/Facade.php +++ b/src/Facade.php @@ -19,14 +19,14 @@ class Facade /** * Facade factory method. Use this to create new Alibaba SDK interaction object. * - * @param string $apiKey Also referenced as "app key" in the Alibaba docs - * @param string $secret Also referenced as "app security" in the Alibaba docs + * @param string $key Also referenced as "AppKey" in the Alibaba docs + * @param string $secret Also referenced as "App Secret" in the Alibaba docs */ - public static function create(string $apiKey, string $secret): self + public static function create(string $key, string $secret): self { return new self( - $apiKey, - new Client($apiKey, $secret, HttpClient::create(), new Clock()), + $key, + new Client($key, $secret, HttpClient::create(), new Clock()), ); } @@ -34,7 +34,7 @@ public static function create(string $apiKey, string $secret): self * @internal */ public function __construct( - private string $apiKey, + private string $key, private Client $client, ) { $this->category = CategoryEndpoint::create($this->client); @@ -45,19 +45,17 @@ public function __construct( /** * Making GET request to this URL will ask to login to Alibaba and authorize this API key to have access * to the account. In other words client should visit this url and authorize App to access Alibaba account by API. - * @link https://developer.alibaba.com/en/doc.htm?spm=a219a.7629140.0.0.188675fe5JPvEa#?docType=1&docId=118416 + * @link https://openapi.alibaba.com/doc/doc.htm?spm=a2o9m.11193494.0.0.50dd3a3armsNgS#/?docId=56 * - * @param string $callbackUrl URL where authorization code returned. Via method GET in "code" parameter. + * @param string $callbackUrl URL where authorization code returned. Via method GET in "code" parameter. Should be + * the same as in the App settings in Alibaba. */ public function getAuthorizationUrl(string $callbackUrl): string { - return 'https://oauth.alibaba.com/authorize?' . http_build_query([ + return 'https://openapi-auth.alibaba.com/oauth/authorize?' . http_build_query([ 'response_type' => 'code', - 'client_id' => $this->apiKey, 'redirect_uri' => $callbackUrl, - 'State' => '1212', - 'view' => 'web', - 'sp' => 'ICBU', + 'client_id' => $this->key, ]); } } From 7e95aa5abc7b07520ea0b80452939ede195a3876 Mon Sep 17 00:00:00 2001 From: Volodymyr Stelmakh Date: Tue, 30 Apr 2024 18:35:44 +0200 Subject: [PATCH 03/12] update client to use openapi --- composer.json | 1 - src/Client.php | 47 +++++++++++------------ src/Exception/ResponseException.php | 45 +++++++++++++++++----- tests/ClientTest.php | 42 ++++++++++---------- tests/Exception/ResponseExceptionTest.php | 23 ++++++----- 5 files changed, 94 insertions(+), 64 deletions(-) diff --git a/composer.json b/composer.json index 93f2303..8f2db26 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,6 @@ "require": { "php": "^8.1", "ext-json": "*", - "ext-mbstring": "*", "symfony/http-client": "^5.4 || ^6" }, "require-dev": { diff --git a/src/Client.php b/src/Client.php index c9048a7..e3b711b 100644 --- a/src/Client.php +++ b/src/Client.php @@ -14,7 +14,7 @@ class Client { public function __construct( - private string $apiKey, + private string $key, private string $secret, private HttpClientInterface $httpClient, private Clock $clock, @@ -27,12 +27,13 @@ public function __construct( * @param mixed[] $payload JSON serializable data * @return mixed[] Decoded JSON as associative array */ - public function request(array $payload): array + public function request(string $endpoint, array $payload): array { + $endpoint = str_starts_with($endpoint, '/') ? $endpoint : '/' . $endpoint; $headers = ['User-Agent' => 'Kyto Alibaba Client']; - $body = $this->getBody($payload); + $body = $this->getBody($endpoint, $payload); - $response = $this->httpClient->request('POST', 'https://api.taobao.com/router/rest', [ + $response = $this->httpClient->request('POST', 'https://openapi-api.alibaba.com/rest' . $endpoint, [ 'headers' => $headers, 'body' => $body, ]); @@ -46,46 +47,45 @@ public function request(array $payload): array * @param mixed[] $payload * @return mixed[] */ - private function getBody(array $payload): array + private function getBody(string $endpoint, array $payload): array { $payload = array_merge([ - 'app_key' => $this->apiKey, + 'app_key' => $this->key, 'timestamp' => $this->getTimestamp(), - 'format' => 'json', - 'v' => '2.0', ], $payload); - return $this->getSignedBody($payload); + return $this->getSignedBody($endpoint, $payload); } /** * All API calls must include a valid signature. Requests with invalid signatures will be rejected. + * @link https://openapi.alibaba.com/doc/doc.htm?docId=19#/?docId=60 + * @link https://openapi.alibaba.com/doc/doc.htm?docId=19#/?docId=58 * * @param mixed[] $body * @return mixed[] Same body plus "sign_method" and "sign" values */ - private function getSignedBody(array $body): array + private function getSignedBody(string $endpoint, array $body): array { unset($body['sign']); - $body['sign_method'] = 'md5'; - + $body['sign_method'] = 'sha256'; ksort($body); - $hashString = ''; + + $hashString = $endpoint; foreach ($body as $key => $value) { $hashString .= $key . $value; } - $hashString = $this->secret . $hashString . $this->secret; - $body['sign'] = mb_strtoupper(md5($hashString)); + $body['sign'] = strtoupper(hash_hmac('sha256', $hashString, $this->secret)); return $body; } /** - * Required by Alibaba API specs to be in GMT+8 timezone + * Required by Alibaba to be in microseconds and (seems like) in UTC timezone. */ private function getTimestamp(): string { - return $this->clock->now('GMT+8')->format('Y-m-d H:i:s'); + return $this->clock->now('UTC')->format('Uv'); } /** @@ -93,14 +93,13 @@ private function getTimestamp(): string */ private function throwOnError(array $data): void { - $errorResponse = $data['error_response'] ?? null; - - if ($errorResponse !== null) { + if (isset($data['type'], $data['code'])) { throw new ResponseException( - $errorResponse['msg'], - (int) $errorResponse['code'], - $errorResponse['sub_msg'], - $errorResponse['sub_code'], + $data['type'], + $data['message'], + $data['code'], + $data['request_id'], + $data['_trace_id_'], ); } } diff --git a/src/Exception/ResponseException.php b/src/Exception/ResponseException.php index b1351c7..f7ec8ea 100644 --- a/src/Exception/ResponseException.php +++ b/src/Exception/ResponseException.php @@ -4,29 +4,56 @@ namespace Kyto\Alibaba\Exception; +/** + * Represents an error response from Alibaba API. + * @link https://openapi.alibaba.com/doc/doc.htm?docId=19#/?docId=63 + */ class ResponseException extends AlibabaException { /** * @internal */ public function __construct( + private string $type, string $message, - int $code, - private string $subMessage, - private string $subCode, + private string $erorrCode, + private string $requestId, + private string $traceId, ?\Throwable $previous = null ) { - $message = sprintf('%s. Sub-code: "%s". Sub-message: "%s".', $message, $this->subCode, $this->subMessage); - parent::__construct($message, $code, $previous); + $message = sprintf( + '%s. %s. Request id: "%s". Trace id: "%s".', + $this->erorrCode, + $message, + $this->requestId, + $this->traceId, + ); + parent::__construct($message, 0, $previous); } - public function getSubMessage(): string + /** + * @return string Known values are: + * - SYSTEM: API platform error + * - ISV: Business data error + * - ISP: Backend service error + */ + public function getType(): string + { + return $this->type; + } + + public function getErrorCode(): string + { + return $this->erorrCode; + } + + public function getRequestId(): string { - return $this->subMessage; + return $this->requestId; } - public function getSubCode(): string + public function getTraceId(): string { - return $this->subCode; + return $this->traceId; } } diff --git a/tests/ClientTest.php b/tests/ClientTest.php index 8ed76d1..686f807 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -14,8 +14,8 @@ class ClientTest extends TestCase { - private const API_KEY = 'api-key'; - private const API_SECRET = 'api-secret'; + private const KEY = 'app-key'; + private const SECRET = 'app-secret'; private MockObject $httpClient; private MockObject $clock; @@ -25,7 +25,7 @@ public function setUp(): void { $this->httpClient = $this->createMock(HttpClientInterface::class); $this->clock = $this->createMock(Clock::class); - $this->client = new Client(self::API_KEY, self::API_SECRET, $this->httpClient, $this->clock); + $this->client = new Client(self::KEY, self::SECRET, $this->httpClient, $this->clock); } public function tearDown(): void @@ -41,10 +41,10 @@ public function tearDown(): void * @dataProvider requestDataProvider * @param mixed[] $responseData */ - public function testRequest(bool $isSuccess, array $responseData): void + public function testRequest(bool $isSuccess, string $endpoint, array $responseData): void { - $timestamp = '2022-11-11 12:37:45'; - $timezone = 'GMT+8'; + $timestamp = '2024-04-30 18:17:25'; + $timezone = 'UTC'; $datetime = \DateTime::createFromFormat('Y-m-d H:i:s', $timestamp, new \DateTimeZone($timezone)); $this->clock @@ -61,20 +61,18 @@ public function testRequest(bool $isSuccess, array $responseData): void ->method('request') ->with( 'POST', - 'https://api.taobao.com/router/rest', + 'https://openapi-api.alibaba.com/rest/some/endpoint', [ 'headers' => [ 'User-Agent' => 'Kyto Alibaba Client', ], 'body' => [ - 'app_key' => self::API_KEY, - 'timestamp' => $timestamp, - 'format' => 'json', - 'v' => '2.0', + 'app_key' => self::KEY, + 'timestamp' => '1714501045000', 'hello' => 'world', 'test' => 'data', - 'sign_method' => 'md5', - 'sign' => 'E6B0CBA032759D6C2A4BC0136252672F', + 'sign_method' => 'sha256', + 'sign' => '99486884A406C07BC1EF420C886F8422B3FE18BD7420CF9CB65B82027430BF7C', ] ] ) @@ -84,7 +82,7 @@ public function testRequest(bool $isSuccess, array $responseData): void $this->expectException(ResponseException::class); } - $actual = $this->client->request(['hello' => 'world', 'test' => 'data']); + $actual = $this->client->request($endpoint, ['hello' => 'world', 'test' => 'data']); self::assertSame($responseData, $actual); } @@ -94,13 +92,15 @@ public function testRequest(bool $isSuccess, array $responseData): void public function requestDataProvider(): array { return [ - 'success' => [true, ['successful' => 'response']], - 'error' => [false, ['error_response' => [ - 'code' => '1', - 'msg' => 'Error happened', - 'sub_code' => 'api.error', - 'sub_msg' => 'Not working' - ]]], + 'success' => [true, '/some/endpoint', ['successful' => 'response']], + 'success, no slash prefix endpoint' => [true, 'some/endpoint', ['successful' => 'response']], + 'error' => [false, '/some/endpoint', [ + 'type' => 'ISP', + 'code' => 'ErrorHappened', + 'message' => 'Error happened please fix', + 'request_id' => '2101d05f17144750947504007', + '_trace_id_' => '21032cac17144750947448194e339b' + ]], ]; } } diff --git a/tests/Exception/ResponseExceptionTest.php b/tests/Exception/ResponseExceptionTest.php index 21f98ec..4bc63d4 100644 --- a/tests/Exception/ResponseExceptionTest.php +++ b/tests/Exception/ResponseExceptionTest.php @@ -11,18 +11,23 @@ class ResponseExceptionTest extends TestCase { public function testConstruct(): void { - $message = 'Message'; - $code = 1; - $subMessage = 'Sub-message'; - $subCode = 'sub.code'; + $type = 'SYSTEM'; + $message = 'Error happened please fix'; + $errorCode = 'ErrorHappened'; + $requestId = '2101d05f17144750947504007'; + $traceId = '21032cac17144750947448194e339b'; $previous = new \RuntimeException('Previous'); - $exception = new ResponseException($message, $code, $subMessage, $subCode, $previous); + $exception = new ResponseException($type, $message, $errorCode, $requestId, $traceId, $previous); - self::assertSame('Message. Sub-code: "sub.code". Sub-message: "Sub-message".', $exception->getMessage()); - self::assertSame($code, $exception->getCode()); - self::assertSame($subMessage, $exception->getSubMessage()); - self::assertSame($subCode, $exception->getSubCode()); + $expectedMessage = 'ErrorHappened. Error happened please fix. Request id: "2101d05f17144750947504007". ' + . 'Trace id: "21032cac17144750947448194e339b".'; + self::assertSame($expectedMessage, $exception->getMessage()); + + self::assertSame($type, $exception->getType()); + self::assertSame($errorCode, $exception->getErrorCode()); + self::assertSame($requestId, $exception->getRequestId()); + self::assertSame($traceId, $exception->getTraceId()); self::assertSame($previous, $exception->getPrevious()); } } From 21e86b1254e99cca174f915b24d9504f087cba29 Mon Sep 17 00:00:00 2001 From: Volodymyr Stelmakh Date: Thu, 2 May 2024 13:08:02 +0200 Subject: [PATCH 04/12] update token endpoint to use openapi --- src/Endpoint/TokenEndpoint.php | 8 ++-- src/Factory/TokenFactory.php | 40 +++++++++------- src/Model/Token.php | 3 +- tests/Factory/TokenFactoryTest.php | 73 +++++++++--------------------- 4 files changed, 51 insertions(+), 73 deletions(-) diff --git a/src/Endpoint/TokenEndpoint.php b/src/Endpoint/TokenEndpoint.php index 04be7e1..83d8ce0 100644 --- a/src/Endpoint/TokenEndpoint.php +++ b/src/Endpoint/TokenEndpoint.php @@ -8,6 +8,7 @@ use Kyto\Alibaba\Exception\ResponseException; use Kyto\Alibaba\Factory\TokenFactory; use Kyto\Alibaba\Model\Token; +use Kyto\Alibaba\Util\Clock; class TokenEndpoint { @@ -16,7 +17,7 @@ class TokenEndpoint */ public static function create(Client $client): self { - return new self($client, new TokenFactory()); + return new self($client, new TokenFactory(new Clock())); } /** @@ -30,15 +31,14 @@ public function __construct( /** * To obtain authorization code see corresponding facade method. - * @link https://open.taobao.com/api.htm?spm=a219a.7386653.0.0.41449b714zR8KI&docId=25388&docType=2&source=search + * @link https://openapi.alibaba.com/doc/api.htm?spm=a2o9m.11193531.0.0.2fabf453xGO6n7#/api?cid=4&path=/auth/token/create&methodType=GET/POST * @see \Kyto\Alibaba\Facade::getAuthorizationUrl * * @throws ResponseException|\JsonException */ public function new(string $authorizationCode): Token { - $data = $this->client->request([ - 'method' => 'taobao.top.auth.token.create', + $data = $this->client->request('/auth/token/create', [ 'code' => $authorizationCode, ]); diff --git a/src/Factory/TokenFactory.php b/src/Factory/TokenFactory.php index a74549a..8374be8 100644 --- a/src/Factory/TokenFactory.php +++ b/src/Factory/TokenFactory.php @@ -5,40 +5,48 @@ namespace Kyto\Alibaba\Factory; use Kyto\Alibaba\Model\Token; +use Kyto\Alibaba\Util\Clock; +/** + * @internal + */ class TokenFactory { + public function __construct( + private Clock $clock, + ) { + } + /** * @param mixed[] $data */ public function createToken(array $data): Token { - $jsonResult = $data['top_auth_token_create_response']['token_result']; - $token = json_decode($jsonResult, true, 512, JSON_THROW_ON_ERROR); + $baseDatetime = $this->clock->now(); $model = new Token(); - $model->userId = (string) $token['user_id']; - $model->userName = $token['user_nick'] ?? null; + $model->account = (string) $data['account']; - $model->token = (string) $token['access_token']; - $model->tokenExpireAt = $this->getMillisecondsAsDateTime((int) $token['expire_time']); + $model->token = (string) $data['access_token']; + $model->tokenExpireAt = $this->getExpiresInAsDateTime($baseDatetime, (int) $data['expires_in']); - $model->refreshToken = (string) $token['refresh_token']; - $model->refreshTokenExpireAt = $this->getMillisecondsAsDateTime((int) $token['refresh_token_valid_time']); + $model->refreshToken = (string) $data['refresh_token']; + $model->refreshTokenExpireAt = $this->getExpiresInAsDateTime($baseDatetime, (int) $data['refresh_expires_in']); return $model; } /** - * @param int $milliseconds Alibaba provides Unix time in milliseconds + * It is recommended by the Alibaba API docs to refresh the token 30 minutes before it expires. + * @link https://openapi.alibaba.com/doc/doc.htm?spm=a2o9m.11223882.0.0.1566722cTOuz7W#/?docId=56 */ - private function getMillisecondsAsDateTime(int $milliseconds): \DateTimeImmutable + private function getExpiresInAsDateTime(\DateTime $baseDatetime, int $expiresIn): \DateTimeImmutable { - $value = (string) ($milliseconds / 1000); - $datetime = \DateTimeImmutable::createFromFormat('U.u', $value); - if ($datetime === false) { - throw new \UnexpectedValueException(sprintf('Unable to parse "%s" as microtime.', $milliseconds)); - } - return $datetime; + $recommendedExpire = (int) ($expiresIn - (30 * 60)); // 30 minutes before actual expiration + $expiresIn = $recommendedExpire > 0 ? $recommendedExpire : $expiresIn; + + $modifier = sprintf('+%d seconds', $expiresIn); + $datetime = (clone $baseDatetime)->modify($modifier); + return \DateTimeImmutable::createFromInterface($datetime); } } diff --git a/src/Model/Token.php b/src/Model/Token.php index bfe8b4b..2e052ad 100644 --- a/src/Model/Token.php +++ b/src/Model/Token.php @@ -6,8 +6,7 @@ class Token { - public string $userId; - public ?string $userName; + public string $account; public string $token; public \DateTimeImmutable $tokenExpireAt; diff --git a/tests/Factory/TokenFactoryTest.php b/tests/Factory/TokenFactoryTest.php index a36283f..d7ad93a 100644 --- a/tests/Factory/TokenFactoryTest.php +++ b/tests/Factory/TokenFactoryTest.php @@ -6,82 +6,53 @@ use Kyto\Alibaba\Factory\TokenFactory; use Kyto\Alibaba\Model\Token; +use Kyto\Alibaba\Util\Clock; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class TokenFactoryTest extends TestCase { + private Clock&MockObject $clock; private TokenFactory $tokenFactory; public function setUp(): void { - $this->tokenFactory = new TokenFactory(); + $this->clock = $this->createMock(Clock::class); + $this->tokenFactory = new TokenFactory($this->clock); } public function tearDown(): void { unset( + $this->clock, $this->tokenFactory, ); } - /** - * @dataProvider createTokenDataProvider - * @param mixed[] $data - */ - public function testCreateToken(array $data, Token $expected): void + public function testCreateToken(): void { - $actual = $this->tokenFactory->createToken($data); - self::assertEquals($expected, $actual); - } - - /** - * @return mixed[] - */ - public function createTokenDataProvider(): array - { - $cases = []; - - $tokenData = [ - 'user_id' => '123', - 'user_nick' => 'example', - 'access_token' => 'access-token', - 'expire_time' => 1468663236386, - 'refresh_token' => 'refresh-token', - 'refresh_token_valid_time' => 1469643536337, - ]; - $tokenResult = json_encode($tokenData, JSON_THROW_ON_ERROR); - $data = ['top_auth_token_create_response' => ['token_result' => $tokenResult]]; - - $expected = new Token(); - $expected->userId = '123'; - $expected->userName = 'example'; - $expected->token = 'access-token'; - $expected->tokenExpireAt = (new \DateTimeImmutable())->setDate(2016, 7, 16)->setTime(10, 0, 36, 386000); - $expected->refreshToken = 'refresh-token'; - $expected->refreshTokenExpireAt = (new \DateTimeImmutable())->setDate(2016, 7, 27)->setTime(18, 18, 56, 337000); - - $cases['full-result'] = [$data, $expected]; - - $tokenData = [ - 'user_id' => '123', + $this->clock + ->method('now') + ->willReturn( + new \DateTime('2016-07-16T10:00:00'), + ); + + $data = [ + 'account' => 'user@example.com', 'access_token' => 'access-token', - 'expire_time' => 1468663236386, + 'expires_in' => 120, 'refresh_token' => 'refresh-token', - 'refresh_token_valid_time' => 1469643536337, + 'refresh_expires_in' => 7200, ]; - $tokenResult = json_encode($tokenData, JSON_THROW_ON_ERROR); - $data = ['top_auth_token_create_response' => ['token_result' => $tokenResult]]; $expected = new Token(); - $expected->userId = '123'; - $expected->userName = null; + $expected->account = 'user@example.com'; $expected->token = 'access-token'; - $expected->tokenExpireAt = (new \DateTimeImmutable())->setDate(2016, 7, 16)->setTime(10, 0, 36, 386000); + $expected->tokenExpireAt = (new \DateTimeImmutable())->setDate(2016, 7, 16)->setTime(10, 2, 0); $expected->refreshToken = 'refresh-token'; - $expected->refreshTokenExpireAt = (new \DateTimeImmutable())->setDate(2016, 7, 27)->setTime(18, 18, 56, 337000); - - $cases['no-username'] = [$data, $expected]; + $expected->refreshTokenExpireAt = (new \DateTimeImmutable())->setDate(2016, 7, 16)->setTime(11, 30, 0); - return $cases; + $actual = $this->tokenFactory->createToken($data); + self::assertEquals($expected, $actual); } } From 1a2d3cdcd7806002d8ec56f6023fab039bb3c0ca Mon Sep 17 00:00:00 2001 From: Volodymyr Stelmakh Date: Thu, 2 May 2024 15:07:17 +0200 Subject: [PATCH 05/12] update category get endpoint to use openapi --- src/Endpoint/CategoryEndpoint.php | 11 ++++++----- src/Factory/CategoryFactory.php | 6 +++--- tests/Endpoint/CategoryEndpointTest.php | 18 +++++++++++++----- tests/Factory/CategoryFactoryTest.php | 24 ++++++++++-------------- 4 files changed, 32 insertions(+), 27 deletions(-) diff --git a/src/Endpoint/CategoryEndpoint.php b/src/Endpoint/CategoryEndpoint.php index 09b33d2..9e37cde 100644 --- a/src/Endpoint/CategoryEndpoint.php +++ b/src/Endpoint/CategoryEndpoint.php @@ -11,6 +11,7 @@ use Kyto\Alibaba\Model\Category; use Kyto\Alibaba\Model\CategoryAttribute; use Kyto\Alibaba\Model\CategoryLevelAttribute; +use Kyto\Alibaba\Model\Token; class CategoryEndpoint { @@ -33,18 +34,18 @@ public function __construct( /** * Get product listing category - * @link https://developer.alibaba.com/en/doc.htm?spm=a219a.7629140.0.0.188675fe5JPvEa#?docType=2&docId=50064 + * @link https://openapi.alibaba.com/doc/api.htm?spm=a2o9m.11223882.0.0.1566722cTOuz7W#/api?cid=1&path=/icbu/product/category/get&methodType=GET/POST * * @param ?string $id Provide `null` to fetch root categories * @throws ResponseException */ - public function get(?string $id = null): Category + public function get(Token $token, ?string $id = null): Category { $id = $id ?? '0'; // '0' to fetch root categories - $data = $this->client->request([ - 'method' => 'alibaba.icbu.category.get.new', - 'cat_id' => $id + $data = $this->client->request('/icbu/product/category/get', [ + 'access_token' => $token->token, + 'cat_id' => $id, ]); return $this->categoryFactory->createCategory($data); diff --git a/src/Factory/CategoryFactory.php b/src/Factory/CategoryFactory.php index 1764804..9966e10 100644 --- a/src/Factory/CategoryFactory.php +++ b/src/Factory/CategoryFactory.php @@ -24,7 +24,7 @@ class CategoryFactory */ public function createCategory(array $data): Category { - $category = $data['alibaba_icbu_category_get_new_response']['category']; + $category = $data['result']['result']; $model = new Category(); $model->id = (string) $category['category_id']; @@ -32,8 +32,8 @@ public function createCategory(array $data): Category $model->nameCN = (string) ($category['cn_name'] ?? ''); $model->level = (int) $category['level']; $model->isLeaf = (bool) $category['leaf_category']; - $model->parents = Formatter::getAsArrayOfString($category['parent_ids']['number'] ?? []); - $model->children = Formatter::getAsArrayOfString($category['child_ids']['number'] ?? []); + $model->parents = Formatter::getAsArrayOfString($category['parent_ids'] ?? []); + $model->children = Formatter::getAsArrayOfString($category['child_ids'] ?? []); return $model; } diff --git a/tests/Endpoint/CategoryEndpointTest.php b/tests/Endpoint/CategoryEndpointTest.php index 2cbf359..e1c058e 100644 --- a/tests/Endpoint/CategoryEndpointTest.php +++ b/tests/Endpoint/CategoryEndpointTest.php @@ -10,6 +10,7 @@ use Kyto\Alibaba\Model\Category; use Kyto\Alibaba\Model\CategoryAttribute; use Kyto\Alibaba\Model\CategoryLevelAttribute; +use Kyto\Alibaba\Model\Token; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -43,16 +44,20 @@ public function testCreate(): void public function testGet(): void { + $accessToken = 'access-token'; $id = '1'; $data = ['response' => 'data']; $this->client ->expects(self::once()) ->method('request') - ->with([ - 'method' => 'alibaba.icbu.category.get.new', - 'cat_id' => $id, - ]) + ->with( + '/icbu/product/category/get', + [ + 'access_token' => $accessToken, + 'cat_id' => $id, + ] + ) ->willReturn($data); $category = new Category(); @@ -63,7 +68,10 @@ public function testGet(): void ->with($data) ->willReturn($category); - $actual = $this->categoryEndpoint->get($id); + $token = new Token(); + $token->token = $accessToken; + + $actual = $this->categoryEndpoint->get($token, $id); self::assertSame($category, $actual); } diff --git a/tests/Factory/CategoryFactoryTest.php b/tests/Factory/CategoryFactoryTest.php index 8e8d04f..cb9cde2 100644 --- a/tests/Factory/CategoryFactoryTest.php +++ b/tests/Factory/CategoryFactoryTest.php @@ -42,22 +42,20 @@ public function testCreateCategory(array $data, Category $expected): void } /** - * @return mixed[] + * @return \Generator */ - public function createCategoryDataProvider(): array + public function createCategoryDataProvider(): \Generator { - $cases = []; - $data = [ - 'alibaba_icbu_category_get_new_response' => [ - 'category' => [ + 'result' => [ + 'result' => [ 'category_id' => 1, 'name' => 'Example', 'cn_name' => '例子', 'level' => 2, 'leaf_category' => false, - 'parent_ids' => ['number' => [2, 3]], - 'child_ids' => ['number' => [4, 5]], + 'parent_ids' => [2, 3], + 'child_ids' => [4, 5], ], ], ]; @@ -71,11 +69,11 @@ public function createCategoryDataProvider(): array $model->parents = ['2', '3']; $model->children = ['4', '5']; - $cases['full-result'] = [$data, $model]; + yield 'full-result' => [$data, $model]; $data = [ - 'alibaba_icbu_category_get_new_response' => [ - 'category' => [ + 'result' => [ + 'result' => [ 'category_id' => 1, 'name' => 'Example', 'cn_name' => '例子', @@ -94,9 +92,7 @@ public function createCategoryDataProvider(): array $model->parents = []; $model->children = []; - $cases['no-parents-and-children'] = [$data, $model]; - - return $cases; + yield 'no-parents-and-children' => [$data, $model]; } /** From 640425f013f9675e83f0321a5595ae751fef522b Mon Sep 17 00:00:00 2001 From: Volodymyr Stelmakh Date: Thu, 2 May 2024 16:54:42 +0200 Subject: [PATCH 06/12] add endpoint to alibaba response exception --- src/Client.php | 5 +++-- src/Exception/ResponseException.php | 9 ++++++++- tests/Exception/ResponseExceptionTest.php | 9 ++++++--- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/Client.php b/src/Client.php index e3b711b..4dbcf93 100644 --- a/src/Client.php +++ b/src/Client.php @@ -39,7 +39,7 @@ public function request(string $endpoint, array $payload): array ]); $data = $response->toArray(); - $this->throwOnError($data); + $this->throwOnError($endpoint, $data); return $data; } @@ -91,10 +91,11 @@ private function getTimestamp(): string /** * @param mixed[] $data */ - private function throwOnError(array $data): void + private function throwOnError(string $endpoint, array $data): void { if (isset($data['type'], $data['code'])) { throw new ResponseException( + $endpoint, $data['type'], $data['message'], $data['code'], diff --git a/src/Exception/ResponseException.php b/src/Exception/ResponseException.php index f7ec8ea..165cbac 100644 --- a/src/Exception/ResponseException.php +++ b/src/Exception/ResponseException.php @@ -14,6 +14,7 @@ class ResponseException extends AlibabaException * @internal */ public function __construct( + private string $endpoint, private string $type, string $message, private string $erorrCode, @@ -22,15 +23,21 @@ public function __construct( ?\Throwable $previous = null ) { $message = sprintf( - '%s. %s. Request id: "%s". Trace id: "%s".', + '[%s] %s. Endpoint: "%s". Request id: "%s". Trace id: "%s".', $this->erorrCode, $message, + $this->endpoint, $this->requestId, $this->traceId, ); parent::__construct($message, 0, $previous); } + public function getEndpoint(): string + { + return $this->endpoint; + } + /** * @return string Known values are: * - SYSTEM: API platform error diff --git a/tests/Exception/ResponseExceptionTest.php b/tests/Exception/ResponseExceptionTest.php index 4bc63d4..cd637c2 100644 --- a/tests/Exception/ResponseExceptionTest.php +++ b/tests/Exception/ResponseExceptionTest.php @@ -11,6 +11,7 @@ class ResponseExceptionTest extends TestCase { public function testConstruct(): void { + $endpoint = '/example/test/get'; $type = 'SYSTEM'; $message = 'Error happened please fix'; $errorCode = 'ErrorHappened'; @@ -18,10 +19,12 @@ public function testConstruct(): void $traceId = '21032cac17144750947448194e339b'; $previous = new \RuntimeException('Previous'); - $exception = new ResponseException($type, $message, $errorCode, $requestId, $traceId, $previous); + $exception = new ResponseException($endpoint, $type, $message, $errorCode, $requestId, $traceId, $previous); - $expectedMessage = 'ErrorHappened. Error happened please fix. Request id: "2101d05f17144750947504007". ' - . 'Trace id: "21032cac17144750947448194e339b".'; + $expectedMessage = '[ErrorHappened] Error happened please fix.' + . ' Endpoint: "/example/test/get".' + . ' Request id: "2101d05f17144750947504007".' + . ' Trace id: "21032cac17144750947448194e339b".'; self::assertSame($expectedMessage, $exception->getMessage()); self::assertSame($type, $exception->getType()); From de1a1afccaedcd4fff4c21fb415133abadae49af Mon Sep 17 00:00:00 2001 From: Volodymyr Stelmakh Date: Fri, 3 May 2024 16:49:25 +0200 Subject: [PATCH 07/12] add another error response type to client --- src/Client.php | 11 +++++++++++ tests/ClientTest.php | 11 ++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/Client.php b/src/Client.php index 4dbcf93..6473e2a 100644 --- a/src/Client.php +++ b/src/Client.php @@ -103,5 +103,16 @@ private function throwOnError(string $endpoint, array $data): void $data['_trace_id_'], ); } + + if (isset($data['result']['success']) && (bool) $data['result']['success'] !== true) { + throw new ResponseException( + $endpoint, + 'SYSTEM', + $data['result']['message_info'], + $data['result']['msg_code'], + $data['request_id'], + $data['_trace_id_'], + ); + } } } diff --git a/tests/ClientTest.php b/tests/ClientTest.php index 686f807..4620648 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -94,13 +94,22 @@ public function requestDataProvider(): array return [ 'success' => [true, '/some/endpoint', ['successful' => 'response']], 'success, no slash prefix endpoint' => [true, 'some/endpoint', ['successful' => 'response']], - 'error' => [false, '/some/endpoint', [ + 'error 1' => [false, '/some/endpoint', [ 'type' => 'ISP', 'code' => 'ErrorHappened', 'message' => 'Error happened please fix', 'request_id' => '2101d05f17144750947504007', '_trace_id_' => '21032cac17144750947448194e339b' ]], + 'error 2' => [false, '/some/endpoint', [ + 'result' => [ + 'success' => false, + 'message_info' => 'Error happened please fix', + 'msg_code' => 'isp.error-happened', + ], + 'request_id' => '2101d05f17144750947504007', + '_trace_id_' => '21032cac17144750947448194e339b' + ]], ]; } } From b8b2d1f9d7670a0154938ee6661523f098a5dde8 Mon Sep 17 00:00:00 2001 From: Volodymyr Stelmakh Date: Mon, 6 May 2024 15:57:15 +0200 Subject: [PATCH 08/12] add token refresh endpoint --- README.md | 3 +- src/Client.php | 2 +- src/Endpoint/TokenEndpoint.php | 20 ++++++++++++- tests/Endpoint/TokenEndpointTest.php | 42 +++++++++++++++++++++++++--- 4 files changed, 60 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a08cb69..152955e 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,8 @@ Currently implemented endpoints: facade ├─ getAuthorizationUrl - Get user authorization url ├─ token/ - Token endpoint -│ └─ new - Obtain new session token +│ ├─ new - Obtain new access token +│ └─ refresh - Refresh access token ├─ category/ - Category endpoint │ ├─ get - Get product listing category │ ├─ getAttributes - Get system-defined attributes based on category ID diff --git a/src/Client.php b/src/Client.php index 6473e2a..423e8e8 100644 --- a/src/Client.php +++ b/src/Client.php @@ -107,7 +107,7 @@ private function throwOnError(string $endpoint, array $data): void if (isset($data['result']['success']) && (bool) $data['result']['success'] !== true) { throw new ResponseException( $endpoint, - 'SYSTEM', + 'SYSTEM', // it's empty for this type of error, therefore we use "SYSTEM" $data['result']['message_info'], $data['result']['msg_code'], $data['request_id'], diff --git a/src/Endpoint/TokenEndpoint.php b/src/Endpoint/TokenEndpoint.php index 83d8ce0..d6568ad 100644 --- a/src/Endpoint/TokenEndpoint.php +++ b/src/Endpoint/TokenEndpoint.php @@ -34,7 +34,7 @@ public function __construct( * @link https://openapi.alibaba.com/doc/api.htm?spm=a2o9m.11193531.0.0.2fabf453xGO6n7#/api?cid=4&path=/auth/token/create&methodType=GET/POST * @see \Kyto\Alibaba\Facade::getAuthorizationUrl * - * @throws ResponseException|\JsonException + * @throws ResponseException */ public function new(string $authorizationCode): Token { @@ -44,4 +44,22 @@ public function new(string $authorizationCode): Token return $this->tokenFactory->createToken($data); } + + /** + * @link https://openapi.alibaba.com/doc/api.htm?spm=a2o9m.11193531.0.0.2fabf453CIh7hC#/api?cid=4&path=/auth/token/refresh&methodType=GET/POST + * + * @throws ResponseException + */ + public function refresh(Token $token): Token + { + $data = $this->client->request('/auth/token/refresh', [ + 'refresh_token' => $token->refreshToken, + ]); + + if (!isset($data['account'])) { + $data['account'] = $token->account; + } + + return $this->tokenFactory->createToken($data); + } } diff --git a/tests/Endpoint/TokenEndpointTest.php b/tests/Endpoint/TokenEndpointTest.php index 26b926d..2c79ab1 100644 --- a/tests/Endpoint/TokenEndpointTest.php +++ b/tests/Endpoint/TokenEndpointTest.php @@ -47,10 +47,12 @@ public function testNew(): void $this->client ->expects(self::once()) ->method('request') - ->with([ - 'method' => 'taobao.top.auth.token.create', - 'code' => $authorizationCode, - ]) + ->with( + '/auth/token/create', + [ + 'code' => $authorizationCode, + ] + ) ->willReturn($data); $token = new Token(); @@ -64,4 +66,36 @@ public function testNew(): void $actual = $this->tokenEndpoint->new($authorizationCode); self::assertSame($token, $actual); } + + public function testRefresh(): void + { + $refreshToken = 'refresh-token'; + $data = ['response' => 'data', 'account' => 'user@example.com']; + + $token = new Token(); + $token->account = 'user@example.com'; + $token->refreshToken = $refreshToken; + + $this->client + ->expects(self::once()) + ->method('request') + ->with( + '/auth/token/refresh', + [ + 'refresh_token' => $refreshToken, + ] + ) + ->willReturn($data); + + $expected = new Token(); + + $this->tokenFactory + ->expects(self::once()) + ->method('createToken') + ->with($data) + ->willReturn($expected); + + $actual = $this->tokenEndpoint->refresh($token); + self::assertSame($expected, $actual); + } } From e1a53f607ab966f5582254b86180ded2b69c8fdb Mon Sep 17 00:00:00 2001 From: Volodymyr Stelmakh Date: Mon, 6 May 2024 16:18:34 +0200 Subject: [PATCH 09/12] remove obsolete category attribute endpoints --- README.md | 4 +- src/Endpoint/CategoryEndpoint.php | 63 ----- src/Exception/UnexpectedResultException.php | 19 -- src/Factory/CategoryFactory.php | 84 ------ src/Model/CategoryAttribute.php | 31 --- src/Model/CategoryAttributeValue.php | 15 -- src/Model/CategoryLevelAttribute.php | 14 - src/Model/CategoryLevelAttributeValue.php | 12 - tests/Endpoint/CategoryEndpointTest.php | 61 ----- .../UnexpectedResultExceptionTest.php | 24 -- tests/Factory/CategoryFactoryTest.php | 254 ------------------ 11 files changed, 1 insertion(+), 580 deletions(-) delete mode 100644 src/Exception/UnexpectedResultException.php delete mode 100644 src/Model/CategoryAttribute.php delete mode 100644 src/Model/CategoryAttributeValue.php delete mode 100644 src/Model/CategoryLevelAttribute.php delete mode 100644 src/Model/CategoryLevelAttributeValue.php delete mode 100644 tests/Exception/UnexpectedResultExceptionTest.php diff --git a/README.md b/README.md index 152955e..e3b4f69 100644 --- a/README.md +++ b/README.md @@ -37,9 +37,7 @@ facade │ ├─ new - Obtain new access token │ └─ refresh - Refresh access token ├─ category/ - Category endpoint -│ ├─ get - Get product listing category -│ ├─ getAttributes - Get system-defined attributes based on category ID -│ └─ getLevelAttribute - Get next-level attribute based on category, attribute and value ID (e.g. car_model values) +│ └─ get - Get product listing category └─ product/ - Product endpoint └─ getGroup - Get product group ``` diff --git a/src/Endpoint/CategoryEndpoint.php b/src/Endpoint/CategoryEndpoint.php index 9e37cde..f817510 100644 --- a/src/Endpoint/CategoryEndpoint.php +++ b/src/Endpoint/CategoryEndpoint.php @@ -6,11 +6,8 @@ use Kyto\Alibaba\Client; use Kyto\Alibaba\Exception\ResponseException; -use Kyto\Alibaba\Exception\UnexpectedResultException; use Kyto\Alibaba\Factory\CategoryFactory; use Kyto\Alibaba\Model\Category; -use Kyto\Alibaba\Model\CategoryAttribute; -use Kyto\Alibaba\Model\CategoryLevelAttribute; use Kyto\Alibaba\Model\Token; class CategoryEndpoint @@ -50,64 +47,4 @@ public function get(Token $token, ?string $id = null): Category return $this->categoryFactory->createCategory($data); } - - /** - * Get system-defined attributes based on category ID - * @link https://developer.alibaba.com/en/doc.htm?spm=a219a.7629140.0.0.188675fe5JPvEa#?docType=2&docId=25348 - * - * @return CategoryAttribute[] - * @throws ResponseException - */ - public function getAttributes(string $categoryId): array - { - $data = $this->client->request([ - 'method' => 'alibaba.icbu.category.attribute.get', - 'cat_id' => $categoryId, - ]); - - $result = []; - - $attributes = $data['alibaba_icbu_category_attribute_get_response']['attributes']['attribute']; - foreach ($attributes as $attribute) { - $result[] = $this->categoryFactory->createAttribute($attribute); - } - - return $result; - } - - /** - * Get next-level attribute based on category, attribute and optionally level attribute value ID. - * @link https://developer.alibaba.com/en/doc.htm?spm=a2728.12183079.k2mwm9fd.1.4b3630901WuQWY#?docType=2&docId=48659 - * - * @param ?string $valueId provide null to fetch root level - * @throws ResponseException|UnexpectedResultException - */ - public function getLevelAttribute( - string $categoryId, - string $attributeId, - ?string $valueId = null - ): CategoryLevelAttribute { - $attributeValueRequest = [ - 'cat_id' => $categoryId, - 'attr_id' => $attributeId, - 'value_id' => $valueId ?? '0' - ]; - - $data = $this->client->request([ - 'method' => 'alibaba.icbu.category.level.attr.get', - 'attribute_value_request' => json_encode($attributeValueRequest) - ]); - - $errorMessage = sprintf( - 'Result list for category id: "%s", attribute id: "%s", value id: "%s" is empty.', - $categoryId, - $attributeId, - $valueId - ); - - $attribute = $data['alibaba_icbu_category_level_attr_get_response']['result_list'] - ?? throw new UnexpectedResultException($errorMessage); - - return $this->categoryFactory->createLevelAttribute($attribute); - } } diff --git a/src/Exception/UnexpectedResultException.php b/src/Exception/UnexpectedResultException.php deleted file mode 100644 index 4cfb418..0000000 --- a/src/Exception/UnexpectedResultException.php +++ /dev/null @@ -1,19 +0,0 @@ - $data - */ - public function createAttribute(array $data): CategoryAttribute - { - $model = new CategoryAttribute(); - - $model->id = (string) $data['attr_id']; - $model->name = (string) $data['en_name']; - $model->isRequired = (bool) $data['required']; - - $model->inputType = InputType::from($data['input_type']); - $model->showType = ShowType::from($data['show_type']); - $model->valueType = ValueType::from($data['value_type']); - - $model->isSku = (bool) $data['sku_attribute']; - $model->hasCustomizeImage = (bool) $data['customize_image']; - $model->hasCustomizeValue = (bool) $data['customize_value']; - $model->isCarModel = (bool) $data['car_model']; - - $model->units = Formatter::getAsArrayOfString($data['units']['string'] ?? []); - - $values = $data['attribute_values']['attribute_value'] ?? []; - foreach ($values as $value) { - $model->values[] = $this->createAttributeValue($value); - } - - return $model; - } - - /** - * @param array $data - */ - public function createAttributeValue(array $data): CategoryAttributeValue - { - $model = new CategoryAttributeValue(); - - $model->id = (string) $data['attr_value_id']; - $model->name = (string) $data['en_name']; - $model->isSku = (bool) $data['sku_value']; - $model->childAttributes = Formatter::getAsArrayOfString($data['child_attrs']['number'] ?? []); - - return $model; - } - - /** - * @param array $data - */ - public function createLevelAttribute(array $data): CategoryLevelAttribute - { - $model = new CategoryLevelAttribute(); - - $model->id = (string) $data['property_id']; - $model->name = (string) $data['property_en_name']; - - $model->values = []; - $decodedValues = json_decode($data['values'], true); - foreach ($decodedValues as $value) { - $model->values[] = $this->createLevelAttributeValue($value); - } - - return $model; - } - - /** - * @param array $data - */ - public function createLevelAttributeValue(array $data): CategoryLevelAttributeValue - { - $model = new CategoryLevelAttributeValue(); - $model->name = (string) $data['name']; - $model->id = (string) $data['id']; - $model->isLeaf = isset($data['leaf']); - - return $model; - } } diff --git a/src/Model/CategoryAttribute.php b/src/Model/CategoryAttribute.php deleted file mode 100644 index 3b79432..0000000 --- a/src/Model/CategoryAttribute.php +++ /dev/null @@ -1,31 +0,0 @@ -categoryEndpoint->get($token, $id); self::assertSame($category, $actual); } - - public function testGetAttributes(): void - { - $id = '1'; - $attributes = [ - ['Attribute 1'], - ['Attribute 2'], - ]; - $data = ['alibaba_icbu_category_attribute_get_response' => ['attributes' => ['attribute' => $attributes]]]; - - $this->client - ->expects(self::once()) - ->method('request') - ->with([ - 'method' => 'alibaba.icbu.category.attribute.get', - 'cat_id' => $id, - ]) - ->willReturn($data); - - $result = [ - new CategoryAttribute(), - new CategoryAttribute(), - ]; - - $this->categoryFactory - ->expects(self::exactly(2)) - ->method('createAttribute') - ->withConsecutive(...array_map(static fn($item) => [$item], $attributes)) - ->willReturnOnConsecutiveCalls(...$result); - - $actual = $this->categoryEndpoint->getAttributes($id); - self::assertSame($result, $actual); - } - - public function testGetLevelAttributes(): void - { - $attributeValueRequestBody = '{"cat_id":"1","attr_id":"1","value_id":"0"}'; - $levelAttribute = ['LevelAttribute']; - $data = ['alibaba_icbu_category_level_attr_get_response' => ['result_list' => $levelAttribute]]; - - $this->client - ->expects(self::once()) - ->method('request') - ->with([ - 'method' => 'alibaba.icbu.category.level.attr.get', - 'attribute_value_request' => $attributeValueRequestBody, - ]) - ->willReturn($data); - - $result = new CategoryLevelAttribute(); - - $this->categoryFactory - ->expects(self::once()) - ->method('createLevelAttribute') - ->willReturn($result); - - $actual = $this->categoryEndpoint->getLevelAttribute('1', '1', null); - self::assertSame($result, $actual); - } } diff --git a/tests/Exception/UnexpectedResultExceptionTest.php b/tests/Exception/UnexpectedResultExceptionTest.php deleted file mode 100644 index e3d39d8..0000000 --- a/tests/Exception/UnexpectedResultExceptionTest.php +++ /dev/null @@ -1,24 +0,0 @@ -getMessage()); - self::assertSame($code, $exception->getCode()); - self::assertSame($previous, $exception->getPrevious()); - } -} diff --git a/tests/Factory/CategoryFactoryTest.php b/tests/Factory/CategoryFactoryTest.php index cb9cde2..e21ba71 100644 --- a/tests/Factory/CategoryFactoryTest.php +++ b/tests/Factory/CategoryFactoryTest.php @@ -4,15 +4,8 @@ namespace Kyto\Alibaba\Tests\Factory; -use Kyto\Alibaba\Enum\InputType; -use Kyto\Alibaba\Enum\ShowType; -use Kyto\Alibaba\Enum\ValueType; use Kyto\Alibaba\Factory\CategoryFactory; use Kyto\Alibaba\Model\Category; -use Kyto\Alibaba\Model\CategoryAttribute; -use Kyto\Alibaba\Model\CategoryAttributeValue; -use Kyto\Alibaba\Model\CategoryLevelAttribute; -use Kyto\Alibaba\Model\CategoryLevelAttributeValue; use PHPUnit\Framework\TestCase; class CategoryFactoryTest extends TestCase @@ -94,251 +87,4 @@ public function createCategoryDataProvider(): \Generator yield 'no-parents-and-children' => [$data, $model]; } - - /** - * @dataProvider createAttributeDataProvider - * @param mixed[] $data - */ - public function testCreateAttribute(array $data, CategoryAttribute $expected): void - { - $actual = $this->categoryFactory->createAttribute($data); - self::assertEquals($expected, $actual); - } - - /** - * @return mixed[] - */ - public function createAttributeDataProvider(): array - { - $cases = []; - - $data = [ - 'attr_id' => 1, - 'en_name' => 'Example', - 'required' => true, - 'input_type' => 'single_select', - 'show_type' => 'list_box', - 'value_type' => 'string', - 'sku_attribute' => false, - 'customize_image' => false, - 'customize_value' => false, - 'car_model' => false, - 'attribute_values' => [ - 'attribute_value' => [ - [ - 'attr_value_id' => 11, - 'en_name' => 'Value 1', - 'sku_value' => false, - 'child_attrs' => [ - 'number' => [2, 3] - ] - ], - [ - 'attr_value_id' => 12, - 'en_name' => 'Value 2', - 'sku_value' => true, - ], - ] - ], - ]; - - $model = new CategoryAttribute(); - $model->id = '1'; - $model->name = 'Example'; - $model->isRequired = true; - $model->inputType = InputType::SINGLE_SELECT; - $model->showType = ShowType::LIST_BOX; - $model->valueType = ValueType::STRING; - $model->isSku = false; - $model->hasCustomizeImage = false; - $model->hasCustomizeValue = false; - $model->isCarModel = false; - $model->units = []; - - $value1 = new CategoryAttributeValue(); - $value1->id = '11'; - $value1->name = 'Value 1'; - $value1->isSku = false; - $value1->childAttributes = ['2', '3']; - - $value2 = new CategoryAttributeValue(); - $value2->id = '12'; - $value2->name = 'Value 2'; - $value2->isSku = true; - $value2->childAttributes = []; - - $model->values = [$value1, $value2]; - - $cases['list_box'] = [$data, $model]; - - $data = [ - 'attr_id' => 1, - 'en_name' => 'Example', - 'required' => true, - 'input_type' => 'input', - 'show_type' => 'input', - 'value_type' => 'number', - 'sku_attribute' => false, - 'customize_image' => false, - 'customize_value' => false, - 'car_model' => false, - 'units' => ['string' => ['mm', 'cm']], - ]; - - $model = new CategoryAttribute(); - $model->id = '1'; - $model->name = 'Example'; - $model->isRequired = true; - $model->inputType = InputType::INPUT; - $model->showType = ShowType::INPUT; - $model->valueType = ValueType::NUMBER; - $model->isSku = false; - $model->hasCustomizeImage = false; - $model->hasCustomizeValue = false; - $model->isCarModel = false; - $model->units = ['mm', 'cm']; - $model->values = []; - - $cases['input'] = [$data, $model]; - - return $cases; - } - - /** - * @dataProvider createAttributeValueDataProvider - * @param mixed[] $data - */ - public function testCreateAttributeValue(array $data, CategoryAttributeValue $expected): void - { - $actual = $this->categoryFactory->createAttributeValue($data); - self::assertEquals($expected, $actual); - } - - /** - * @return mixed[] - */ - public function createAttributeValueDataProvider(): array - { - $cases = []; - - $data = [ - 'attr_value_id' => 11, - 'en_name' => 'Value 1', - 'sku_value' => false, - 'child_attrs' => [ - 'number' => [2, 3] - ] - ]; - - $model = new CategoryAttributeValue(); - $model->id = '11'; - $model->name = 'Value 1'; - $model->isSku = false; - $model->childAttributes = ['2', '3']; - - $cases['with-children'] = [$data, $model]; - - $data = [ - 'attr_value_id' => 11, - 'en_name' => 'Value 1', - 'sku_value' => false, - ]; - - $model = new CategoryAttributeValue(); - $model->id = '11'; - $model->name = 'Value 1'; - $model->isSku = false; - $model->childAttributes = []; - - $cases['no-children'] = [$data, $model]; - - return $cases; - } - - /** - * @dataProvider createLevelAttributeDataProvider - * @param mixed[] $data - */ - public function testCreateLevelAttribute(array $data, CategoryLevelAttribute $expected): void - { - $actual = $this->categoryFactory->createLevelAttribute($data); - self::assertEquals($expected, $actual); - } - - public function createLevelAttributeDataProvider(): \Generator - { - $data = [ - 'property_id' => '123', - 'property_en_name' => 'someName', - 'values' => '{}' - ]; - - $expected = new CategoryLevelAttribute(); - $expected->id = '123'; - $expected->name = 'someName'; - $expected->values = []; - - yield 'no values' => [$data, $expected]; - - $data = [ - 'property_id' => '123', - 'property_en_name' => 'someName', - 'values' => '[{"id":"1","name":"valueNoLeaf"},{"id":2,"name":"valueIsLeaf","leaf":true}]' - ]; - - $levelValueNoLeaf = new CategoryLevelAttributeValue(); - $levelValueNoLeaf->id = '1'; - $levelValueNoLeaf->name = 'valueNoLeaf'; - $levelValueNoLeaf->isLeaf = false; - - $levelValueIsLeaf = new CategoryLevelAttributeValue(); - $levelValueIsLeaf->id = '2'; - $levelValueIsLeaf->name = 'valueIsLeaf'; - $levelValueIsLeaf->isLeaf = true; - - $expected = new CategoryLevelAttribute(); - $expected->id = '123'; - $expected->name = 'someName'; - $expected->values = [$levelValueNoLeaf, $levelValueIsLeaf]; - - yield 'with values' => [$data, $expected]; - } - - /** - * @dataProvider createLevelAttributeValueDataProvider - * @param mixed[] $data - */ - public function testCreateLevelAttributeValue(array $data, CategoryLevelAttributeValue $expected): void - { - $actual = $this->categoryFactory->createLevelAttributeValue($data); - self::assertEquals($expected, $actual); - } - - public function createLevelAttributeValueDataProvider(): \Generator - { - $data = [ - "id" => "1", - "name" => "valueNoLeaf" - ]; - - $expected = new CategoryLevelAttributeValue(); - $expected->name = 'valueNoLeaf'; - $expected->id = '1'; - $expected->isLeaf = false; - - yield 'no leaf' => [$data, $expected]; - - $data = [ - "id" => "1", - "name" => "valueIsLeaf", - "leaf" => true - ]; - - $expected = new CategoryLevelAttributeValue(); - $expected->name = 'valueIsLeaf'; - $expected->id = '1'; - $expected->isLeaf = true; - - yield 'is leaf' => [$data, $expected]; - } } From 3114c2f5a080fef375489ef8214883fd07707c14 Mon Sep 17 00:00:00 2001 From: Volodymyr Stelmakh Date: Mon, 6 May 2024 16:22:59 +0200 Subject: [PATCH 10/12] fix facade get authorization url test --- tests/FacadeTest.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/FacadeTest.php b/tests/FacadeTest.php index 8d2db90..f1ea064 100644 --- a/tests/FacadeTest.php +++ b/tests/FacadeTest.php @@ -42,10 +42,12 @@ public function testGetAuthorizationUrl(): void $callbackURL = 'https://example.com/callback'; $actual = $this->facade->getAuthorizationUrl($callbackURL); $expected = sprintf( - 'https://oauth.alibaba.com/authorize?response_type=code&client_id=%s&redirect_uri=%s' - . '&State=1212&view=web&sp=ICBU', - urlencode(self::API_KEY), + 'https://openapi-auth.alibaba.com/oauth/authorize' + . '?response_type=code' + . '&redirect_uri=%s' + . '&client_id=%s', urlencode($callbackURL), + urlencode(self::API_KEY), ); self::assertSame($expected, $actual); From e344deb0f6a115f1d9dde51eea24684e0e585c26 Mon Sep 17 00:00:00 2001 From: Volodymyr Stelmakh Date: Mon, 6 May 2024 16:45:51 +0200 Subject: [PATCH 11/12] update product endpoint to use openapi --- README.md | 2 +- src/Endpoint/ProductEndpoint.php | 30 +++++++++++++++--------------- src/Factory/ProductFactory.php | 18 ------------------ src/Model/ProductGroup.php | 15 --------------- 4 files changed, 16 insertions(+), 49 deletions(-) delete mode 100644 src/Model/ProductGroup.php diff --git a/README.md b/README.md index e3b4f69..c44e3fb 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ facade ├─ category/ - Category endpoint │ └─ get - Get product listing category └─ product/ - Product endpoint - └─ getGroup - Get product group + └─ getSchema - Get rules and fields for new product release ``` ## Credits diff --git a/src/Endpoint/ProductEndpoint.php b/src/Endpoint/ProductEndpoint.php index e654c32..35d7b76 100644 --- a/src/Endpoint/ProductEndpoint.php +++ b/src/Endpoint/ProductEndpoint.php @@ -5,9 +5,8 @@ namespace Kyto\Alibaba\Endpoint; use Kyto\Alibaba\Client; -use Kyto\Alibaba\Exception\ResponseException; use Kyto\Alibaba\Factory\ProductFactory; -use Kyto\Alibaba\Model\ProductGroup; +use Kyto\Alibaba\Model\Category; use Kyto\Alibaba\Model\Token; class ProductEndpoint @@ -25,27 +24,28 @@ public static function create(Client $client): self */ public function __construct( private Client $client, - private ProductFactory $productFactory, + private ProductFactory $productFactory, // @phpstan-ignore-line ) { } /** - * Get product group information. - * @link https://developer.alibaba.com/en/doc.htm?spm=a219a.7629140.0.0.188675fe5JPvEa#?docType=2&docId=25299 + * Obtain the page rules and fill-in fields for new product release. + * @link https://openapi.alibaba.com/doc/api.htm?spm=a2o9m.11193531.0.0.2fabf453CIh7hC#/api?cid=1&path=/alibaba/icbu/product/schema/get&methodType=GET/POST * - * @param ?string $id Provide `null` to fetch root groups - * @throws ResponseException + * @param string $language Allowed values are not documented in the API docs. + * Seems like uses the same locale codes as on Alibaba website: + * en_US, es_ES, fr_FR, it_IT, de_DE, pt_PT, ru_RU, ja_JP, + * ar_SA, ko_KR, tr_TR, vi_VN, th_TH, id_ID, he_IL, hi_IN, zh_CN + * @return array */ - public function getGroup(Token $token, ?string $id = null): ProductGroup + public function getSchema(Token $token, Category $category, string $language = 'en_US'): array { - $id = $id ?? '-1'; // '-1' to fetch root groups - - $data = $this->client->request([ - 'method' => 'alibaba.icbu.product.group.get', - 'session' => $token->token, - 'group_id' => $id + $data = $this->client->request('/alibaba/icbu/product/schema/get', [ + 'cat_id' => $category->id, + 'access_token' => $token->token, + 'language' => $language, ]); - return $this->productFactory->createGroup($data); + return $data; // TODO: Add normalized model for response } } diff --git a/src/Factory/ProductFactory.php b/src/Factory/ProductFactory.php index 12a35d6..ad6141a 100644 --- a/src/Factory/ProductFactory.php +++ b/src/Factory/ProductFactory.php @@ -4,27 +4,9 @@ namespace Kyto\Alibaba\Factory; -use Kyto\Alibaba\Model\ProductGroup; -use Kyto\Alibaba\Util\Formatter; - /** * @internal */ class ProductFactory { - /** - * @param mixed[] $data - */ - public function createGroup(array $data): ProductGroup - { - $group = $data['alibaba_icbu_product_group_get_response']['product_group']; - - $model = new ProductGroup(); - $model->id = (string) $group['group_id']; - $model->name = (string) ($group['group_name'] ?? ''); - $model->parent = isset($group['parent_id']) ? (string) $group['parent_id'] : null; - $model->children = Formatter::getAsArrayOfString($group['children_id_list']['number'] ?? []); - - return $model; - } } diff --git a/src/Model/ProductGroup.php b/src/Model/ProductGroup.php deleted file mode 100644 index 5842adb..0000000 --- a/src/Model/ProductGroup.php +++ /dev/null @@ -1,15 +0,0 @@ - Date: Tue, 7 May 2024 12:43:17 +0200 Subject: [PATCH 12/12] add alibaba api workflow docs --- README.md | 2 ++ docs/workflow.md | 24 ++++++++++++++++++++++++ docs/workflow.svg | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 docs/workflow.md create mode 100644 docs/workflow.svg diff --git a/README.md b/README.md index c44e3fb..c811a71 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ $alibaba = Facade::create('app-key', 'app-secret'); $alibaba->category->get('0'); // @return Kyto\Alibaba\Model\Category ``` +> For the API usage workflow see [Alibaba API Workflow](docs/workflow.md). + ## Endpoints Currently implemented endpoints: diff --git a/docs/workflow.md b/docs/workflow.md new file mode 100644 index 0000000..43b51e5 --- /dev/null +++ b/docs/workflow.md @@ -0,0 +1,24 @@ +# Alibaba API Workflow + +To be able to interact with Alibaba Open Platform, you need to follow the steps below: +1. [Create the application](https://openapi.alibaba.com/app/index.htm?spm=a2o9m.11193487.0.0.35b913a0AlGs39#/app/create?_k=lrqyds) +on the [Alibaba Open Platform](https://openapi.alibaba.com/) specifying all the required permissions (app category). +This may require you to have an Alibaba account and company verification process to be complete. +2. Obtain the `app-key` and `app-secret` from the created application. +This is available in the application settings after Alibaba approves the application. +3. Authorize the application to access the seller data. +Use the [`Facade::getAuthorizationUrl`](../src/Facade.php#L53) method to get the URL to authorize the application. +This will require to log in as a seller and grant the permissions to the application. +As a result, you will be redirected to callback URL with the authorization code. +4. Obtain the `access-token` from the authorization response. +Use the [`Facade::token->new`](../src/Endpoint/TokenEndpoint.php#L39) method to get the `access-token` (and `refresh-token`). +5. Use the `access-token` to interact with the Alibaba Open Platform. + +> Every request to the Alibaba Open Platform requires the `access-token` and valid `signature`. +> Signature is a hash of the `app-key`, `app-secret`, `timestamp` and `request payload`. +> Signature is generated by the SDK automatically. + +See the following schema for the Alibaba API workflow: +[![Alibab API workflow](./workflow.svg)](./workflow.svg) + +For more details see the Alibaba Open Platform [quick start guide](https://openapi.alibaba.com/doc/doc.htm?spm=a2o9m.11223882.0.0.1566722cTOuz7W#/?docId=51). diff --git a/docs/workflow.svg b/docs/workflow.svg new file mode 100644 index 0000000..b3efcd2 --- /dev/null +++ b/docs/workflow.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +