diff --git a/Dockerfile b/Dockerfile index 2a91ee08..3031b4f9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM php:5.6-cli-alpine +FROM php:7.1-cli-alpine ARG MOUNTPOINT=/var/www ARG COMPOSER_BIN_DIR=/usr/local/bin diff --git a/composer.json b/composer.json index 1540d5fa..fe216b87 100644 --- a/composer.json +++ b/composer.json @@ -27,9 +27,12 @@ }, "scripts": { "test": [ - "@php vendor/bin/phpunit", - "@php vendor/bin/sfcs src --progress -vvv" - ] + "@tests-unit", + "@phpcs" + ], + "tests-unit": "vendor/bin/phpunit", + "phpcs": "vendor/bin/sfcs src --progress -vvv", + "phpcsfix": "vendor/bin/sfcs src --progress -vvv --autofix" }, "scripts-descriptions": { "test" : "Run PHPUnit tests suites and Coding standards validator" diff --git a/docker-compose.yml b/docker-compose.yml index 9a5001d7..d941fb89 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,6 +2,7 @@ version: '3' services: sf-php-sdk-dev: + container_name: sfeed.php-sdk-dev image: php-sdk-dev build: context: . diff --git a/docs/development/development.md b/docs/development/development.md index 0d81efdc..d7f2c558 100644 --- a/docs/development/development.md +++ b/docs/development/development.md @@ -23,6 +23,11 @@ docker-compose build docker-compose run sf-php-sdk-dev composer install --dev ``` +3. Connect to container +```bash +docker-compose run sf-php-sdk-dev /bin/sh +``` + ## Code checks To help you test your code against our requirements, there is a composer test script configured : diff --git a/docs/manual/resources/order.md b/docs/manual/resources/order.md index f321f96b..2a47b427 100644 --- a/docs/manual/resources/order.md +++ b/docs/manual/resources/order.md @@ -259,3 +259,23 @@ $operation $orderApi->execute($operation); ``` + +### Upload documents + +To upload order documents, you need the following parameters : +1. [mandatory] `$reference` : Order reference (eg: 'reference1') +2. [mandatory] `$channelName` : The channel where the order is from (eg: 'amazon') +3. [mandatory] `$documents` : One or more documents to upload + +Example : + +```php +namespace ShoppingFeed\Sdk\Api\Order; + +$operation = new OrderOperation(); +$operation + ->uploadDocument('ref1', 'leroymerlin', new Document\Invoice('/tmp/amazon_ref1_invoice.pdf')) + ->uploadDocument('ref2', 'leroymerlin', new Document\Invoice('/tmp/amazon_ref2_invoice.pdf')); + +$orderApi->execute($operation); +``` diff --git a/src/Api/Order/Document/AbstractDocument.php b/src/Api/Order/Document/AbstractDocument.php new file mode 100644 index 00000000..afdd2e25 --- /dev/null +++ b/src/Api/Order/Document/AbstractDocument.php @@ -0,0 +1,35 @@ +path = $path; + $this->type = $type; + } + + public function getPath(): string + { + return $this->path; + } + + public function getType(): string + { + return $this->type; + } +} diff --git a/src/Api/Order/Document/Invoice.php b/src/Api/Order/Document/Invoice.php new file mode 100644 index 00000000..37fe5425 --- /dev/null +++ b/src/Api/Order/Document/Invoice.php @@ -0,0 +1,11 @@ +addOperation( + $reference, + $channelName, + self::TYPE_UPLOAD_DOCUMENTS, + ['document' => $document] + ); + + return $this; + } + /** * Execute all declared operations * @@ -192,10 +217,7 @@ public function execute(Hal\HalLink $link) $resources = new \ArrayObject(); foreach ($this->allowedOperationTypes as $type) { - $this->eachBatch( - $this->createRequestGenerator($type, $link, $requests), - $type - ); + $this->populateRequests($type, $link, $requests); } $link->batchSend( @@ -213,24 +235,81 @@ function (Hal\HalResource $batch) use ($resources) { ); } + private function populateRequests($type, Hal\HalLink $link, \ArrayAccess $requests): void + { + // Upload documents require dedicated processing because of file upload specificities + if (self::TYPE_UPLOAD_DOCUMENTS === $type) { + $this->populateRequestsForUploadDocuments($link, $requests); + return; + } + + $this->eachBatch( + function (array $chunk) use ($type, $link, &$requests) { + $requests[] = $link->createRequest( + 'POST', + ['operation' => $type], + ['order' => $chunk] + ); + }, + $type + ); + } + /** - * Create request generation callback + * Create requests for upload documents operation. We batch request by 20 + * to not send too many files at once. * - * @param string $type * @param Hal\HalLink $link * @param array $requests * - * @return \Closure + * @return \Psr\Http\Message\RequestInterface */ - private function createRequestGenerator($type, Hal\HalLink $link, \ArrayAccess $requests) + private function populateRequestsForUploadDocuments(Hal\HalLink $link, \ArrayAccess $requests) { - return function (array $chunk) use ($type, $link, &$requests) { + $type = self::TYPE_UPLOAD_DOCUMENTS; + + foreach (array_chunk($this->getOperations($type), 20) as $batch) { + $body = []; + $orders = []; + + foreach ($batch as $operation) { + /** @var AbstractDocument $document */ + $document = $operation['document']; + + $resource = fopen($document->getPath(), 'rb'); + + if (false === $resource) { + throw new RuntimeException( + sprintf('Unable to read "%s"', $document->getPath()) + ); + } + + $body[] = [ + 'name' => 'files[]', + 'contents' => $resource, + ]; + + $orders[] = [ + 'reference' => $operation['reference'], + 'channelName' => $operation['channelName'], + 'documents' => [ + ['type' => $document->getType()], + ], + ]; + } + + $body[] = [ + 'name' => 'body', + 'contents' => json_encode(['order' => $orders]), + ]; + $requests[] = $link->createRequest( 'POST', ['operation' => $type], - ['order' => $chunk] + $body, + ['Content-Type' => 'multipart/form-data'] ); - }; + } } /** diff --git a/src/Hal/HalLink.php b/src/Hal/HalLink.php index 82e3002f..794ab871 100644 --- a/src/Hal/HalLink.php +++ b/src/Hal/HalLink.php @@ -119,7 +119,7 @@ public function withAddedHref($path, $variables = []) { $instance = clone $this; $instance->href = rtrim($instance->getUri($variables), '/') . - '/' . ltrim($path, '/'); + '/' . ltrim($path, '/'); return $instance; } @@ -262,15 +262,23 @@ public function send($request, array $config = []) * * @return \Psr\Http\Message\RequestInterface */ - public function createRequest($method, array $variables = [], $body = null) + public function createRequest($method, array $variables = [], $body = null, $headers = []) { - $uri = $this->getUri($variables); - $method = strtoupper($method); - $headers = []; + $uri = $this->getUri($variables); + $method = strtoupper($method); - if ((null !== $body && '' !== $body) && in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'])) { - $headers['Content-Type'] = 'application/json'; - $body = Json::encode($body); + $hasBody = null !== $body && '' !== $body; + + if ($hasBody && in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'])) { + if (! isset($headers['Content-Type'])) { + $headers['Content-Type'] = 'application/json'; + } + + switch ($headers['Content-Type']) { + case 'application/json': + $body = Json::encode($body); + break; + } } return $this->client->createRequest($method, $uri, $headers, $body); diff --git a/src/Http/Adapter/GuzzleHTTPAdapter.php b/src/Http/Adapter/GuzzleHTTPAdapter.php index 3c7e789b..21201f1a 100644 --- a/src/Http/Adapter/GuzzleHTTPAdapter.php +++ b/src/Http/Adapter/GuzzleHTTPAdapter.php @@ -91,6 +91,11 @@ public function batchSend(array $requests, array $options = []) */ public function createRequest($method, $uri, array $headers = [], $body = null) { + if (isset($headers['Content-Type']) && 'multipart/form-data' === $headers['Content-Type']) { + unset($headers['Content-Type']); + $body = new GuzzleHttp\Psr7\MultipartStream($body); + } + return new GuzzleHttp\Psr7\Request($method, $uri, $headers, $body); } diff --git a/tests/unit/Api/Order/OrderOperationTest.php b/tests/unit/Api/Order/OrderOperationTest.php index 7534ec80..fa46fac8 100644 --- a/tests/unit/Api/Order/OrderOperationTest.php +++ b/tests/unit/Api/Order/OrderOperationTest.php @@ -1,11 +1,10 @@ operationCount; $i++) { $orderOperation->addOperation( 'ref' . $i, 'amazon', - Sdk\Api\Order\OrderOperation::TYPE_ACCEPT + Api\Order\OrderOperation::TYPE_ACCEPT ); } } @@ -34,12 +33,12 @@ private function generateOperations(Sdk\Api\Order\OrderOperation $orderOperation */ public function testAddOperation() { - $orderOperation = new Sdk\Api\Order\OrderOperation(); + $orderOperation = new Api\Order\OrderOperation(); $this->generateOperations($orderOperation); $this->assertEquals( $this->operationCount, - $orderOperation->count(Sdk\Api\Order\OrderOperation::TYPE_ACCEPT) + $orderOperation->count(Api\Order\OrderOperation::TYPE_ACCEPT) ); } @@ -49,7 +48,7 @@ public function testAddOperation() public function testAcceptOperation() { $instance = $this - ->getMockBuilder(Sdk\Api\Order\OrderOperation::class) + ->getMockBuilder(Api\Order\OrderOperation::class) ->setMethods(['addOperation']) ->getMock(); @@ -59,12 +58,12 @@ public function testAcceptOperation() ->with( 'ref1', 'amazon', - Sdk\Api\Order\OrderOperation::TYPE_ACCEPT, + Api\Order\OrderOperation::TYPE_ACCEPT, ['reason' => 'noreason'] ); $this->assertInstanceOf( - Sdk\Api\Order\OrderOperation::class, + Api\Order\OrderOperation::class, $instance->accept( 'ref1', 'amazon', @@ -79,7 +78,7 @@ public function testAcceptOperation() public function testCancelOperation() { $instance = $this - ->getMockBuilder(Sdk\Api\Order\OrderOperation::class) + ->getMockBuilder(Api\Order\OrderOperation::class) ->setMethods(['addOperation']) ->getMock(); @@ -89,12 +88,12 @@ public function testCancelOperation() ->with( 'ref1', 'amazon', - Sdk\Api\Order\OrderOperation::TYPE_CANCEL, + Api\Order\OrderOperation::TYPE_CANCEL, ['reason' => 'noreason'] ); $this->assertInstanceOf( - Sdk\Api\Order\OrderOperation::class, + Api\Order\OrderOperation::class, $instance->cancel( 'ref1', 'amazon', @@ -109,7 +108,7 @@ public function testCancelOperation() public function testRefuseOperation() { $instance = $this - ->getMockBuilder(Sdk\Api\Order\OrderOperation::class) + ->getMockBuilder(Api\Order\OrderOperation::class) ->setMethods(['addOperation']) ->getMock(); @@ -119,11 +118,11 @@ public function testRefuseOperation() ->with( 'ref1', 'amazon', - Sdk\Api\Order\OrderOperation::TYPE_REFUSE + Api\Order\OrderOperation::TYPE_REFUSE ); $this->assertInstanceOf( - Sdk\Api\Order\OrderOperation::class, + Api\Order\OrderOperation::class, $instance->refuse( 'ref1', 'amazon' @@ -137,7 +136,7 @@ public function testRefuseOperation() public function testShipOperation() { $instance = $this - ->getMockBuilder(Sdk\Api\Order\OrderOperation::class) + ->getMockBuilder(Api\Order\OrderOperation::class) ->setMethods(['addOperation']) ->getMock(); @@ -147,7 +146,7 @@ public function testShipOperation() ->with( 'ref1', 'amazon', - Sdk\Api\Order\OrderOperation::TYPE_SHIP, + Api\Order\OrderOperation::TYPE_SHIP, [ 'carrier' => 'ups', 'trackingNumber' => '123654abc', @@ -156,7 +155,7 @@ public function testShipOperation() ); $this->assertInstanceOf( - Sdk\Api\Order\OrderOperation::class, + Api\Order\OrderOperation::class, $instance->ship( 'ref1', 'amazon', @@ -167,6 +166,31 @@ public function testShipOperation() ); } + public function testUploadDocumentOperation() + { + $document = new Api\Order\Document\Invoice('/tmp/ref1_amazon_invoice.pdf'); + + $instance = $this + ->getMockBuilder(Api\Order\OrderOperation::class) + ->setMethods(['addOperation']) + ->getMock(); + + $instance + ->expects($this->once()) + ->method('addOperation') + ->with( + 'ref1', + 'amazon', + Api\Order\OrderOperation::TYPE_UPLOAD_DOCUMENTS, + ['document' => $document] + ); + + $this->assertInstanceOf( + Api\Order\OrderOperation::class, + $instance->uploadDocument('ref1', 'amazon', $document) + ); + } + /** * @throws \Exception */ @@ -181,7 +205,7 @@ public function testAcknowledgeOperation() ]; $instance = $this - ->getMockBuilder(Sdk\Api\Order\OrderOperation::class) + ->getMockBuilder(Api\Order\OrderOperation::class) ->setMethods(['addOperation']) ->getMock(); @@ -191,7 +215,7 @@ public function testAcknowledgeOperation() ->with( 'ref1', 'amazon', - Sdk\Api\Order\OrderOperation::TYPE_ACKNOWLEDGE, + Api\Order\OrderOperation::TYPE_ACKNOWLEDGE, $this->callback( function ($param) { return $param['status'] === 'success' @@ -203,7 +227,7 @@ function ($param) { ); $this->assertInstanceOf( - Sdk\Api\Order\OrderOperation::class, + Api\Order\OrderOperation::class, $instance->acknowledge(...$data) ); } @@ -219,7 +243,7 @@ public function testUnacknowledgeOperation() 'Unacknowledged', ]; $instance = $this - ->getMockBuilder(Sdk\Api\Order\OrderOperation::class) + ->getMockBuilder(Api\Order\OrderOperation::class) ->setMethods(['addOperation']) ->getMock(); @@ -229,11 +253,11 @@ public function testUnacknowledgeOperation() ->with( 'ref2', 'amazon2', - Sdk\Api\Order\OrderOperation::TYPE_UNACKNOWLEDGE + Api\Order\OrderOperation::TYPE_UNACKNOWLEDGE ); $this->assertInstanceOf( - Sdk\Api\Order\OrderOperation::class, + Api\Order\OrderOperation::class, $instance->unacknowledge(...$data) ); } @@ -243,7 +267,7 @@ public function testUnacknowledgeOperation() */ public function testAddWrongOperation() { - $orderOperation = new Sdk\Api\Order\OrderOperation(); + $orderOperation = new Api\Order\OrderOperation(); $this->expectException(Sdk\Exception\InvalidArgumentException::class); @@ -264,9 +288,9 @@ public function testExecute() $this->createMock(RequestInterface::class) ); - /** @var Sdk\Api\Order\OrderOperation|\PHPUnit_Framework_MockObject_MockObject $instance */ + /** @var Api\Order\OrderOperation|\PHPUnit_Framework_MockObject_MockObject $instance */ $instance = $this - ->getMockBuilder(Sdk\Api\Order\OrderOperation::class) + ->getMockBuilder(Api\Order\OrderOperation::class) ->setMethods(['getPoolSize']) ->getMock(); @@ -291,7 +315,7 @@ function (Sdk\Hal\HalResource $resource) use (&$resources) { ); $this->assertInstanceOf( - Sdk\Api\Order\OrderOperationResult::class, + Api\Order\OrderOperationResult::class, $instance->execute($link) ); } @@ -302,7 +326,7 @@ function (Sdk\Hal\HalResource $resource) use (&$resources) { public function testRefundOperation() { $instance = $this - ->getMockBuilder(Sdk\Api\Order\OrderOperation::class) + ->getMockBuilder(Api\Order\OrderOperation::class) ->setMethods(['addOperation']) ->getMock(); @@ -312,7 +336,7 @@ public function testRefundOperation() ->with( 'ref1', 'amazon', - Sdk\Api\Order\OrderOperation::TYPE_REFUND, + Api\Order\OrderOperation::TYPE_REFUND, [ 'refund' => [ 'shipping' => true, @@ -325,7 +349,7 @@ public function testRefundOperation() ); $this->assertInstanceOf( - Sdk\Api\Order\OrderOperation::class, + Api\Order\OrderOperation::class, $instance->refund( 'ref1', 'amazon', diff --git a/tests/unit/Hal/HalClientTest.php b/tests/unit/Hal/HalClientTest.php index b2e667d4..b5f0757a 100644 --- a/tests/unit/Hal/HalClientTest.php +++ b/tests/unit/Hal/HalClientTest.php @@ -1,10 +1,10 @@ expects($this->once()) ->method('getBody') - ->willReturn('{"foo":"bar", "foo2":"bar2"}'); + ->willReturn($this->createStream('{"foo":"bar", "foo2":"bar2"}')); $instance = new Hal\HalClient('http://fake.uri', $httpClient); $resource = $instance->createResource($response); @@ -155,7 +155,7 @@ public function testSendWithEmptyResponseSkipEncoding() $response = $this->createMock(Message\ResponseInterface::class); $response->method('getStatusCode')->willReturn(200); - $response->method('getBody')->willReturn(''); + $response->method('getBody')->willReturn($this->createStream('')); $client = $this->createMock(Http\Adapter\AdapterInterface::class); $client @@ -175,7 +175,7 @@ public function testItDecodeResponse() $response = $this->createMock(Message\ResponseInterface::class); $response->method('getStatusCode')->willReturn(200); - $response->method('getBody')->willReturn('{"status":"ok"}'); + $response->method('getBody')->willReturn($this->createStream('{"status":"ok"}')); $client = $this->createMock(Http\Adapter\AdapterInterface::class); $client @@ -196,4 +196,9 @@ public function testGetAdapter() $this->assertSame($httpClient, $instance->getAdapter()); } + + private function createStream(string $contents): Message\StreamInterface + { + return Psr7\Utils::streamFor($contents); + } }