diff --git a/.scrutinizer.yml b/.scrutinizer.yml index 0346e0e..5899b26 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -14,7 +14,7 @@ build: analysis: environment: php: - version: 8.3.3 + version: 8.3.26 tests: override: - php-scrutinizer-run diff --git a/src/BEditaClient.php b/src/BEditaClient.php index 1266115..b7107c3 100644 --- a/src/BEditaClient.php +++ b/src/BEditaClient.php @@ -39,6 +39,56 @@ public function authenticate(string $username, string $password): ?array return $this->post('/auth', $body, ['Content-Type' => 'application/json']); } + /** + * Bulk edit objects using `POST /bulk/edit` endpoint. + * If the endpoint is not available, it fallback to edit one by one (retrocompatible way). + * + * $objects is an array of : , e.g.: + * [ + * 'articles' => [1,2,3], + * 'documents' => [4,5,6], + * ] + * + * The $attributes is an array of attributes to modify, e.g.: + * [ + * 'title' => 'New title', + * 'status' => 'off', + * ] + * + * @param array $objects Object data to indentify objects to edit + * @param array $attributes Data to modify + * @return array + */ + public function bulkEdit(array $objects, array $attributes): array + { + $result = ['data' => ['saved' => [], 'errors' => []]]; + try { + $result = (array)$this->post( + '/bulk/edit', + json_encode(['data' => compact('attributes', 'objects')]), + ['Content-Type' => 'application/json'], + ); + } catch (Exception $e) { + // fallback to edit one by one, to be retrocompatible + $types = array_keys($objects); + foreach ($types as $type) { + foreach ($objects[$type] as $id) { + try { + $response = $this->save($type, $attributes + ['id' => (string)$id]); + $result['data']['saved'][] = $response['data']['id']; + } catch (Exception $e) { + $result['data']['errors'][] = [ + 'id' => $id, + 'message' => $e->getMessage(), + ]; + } + } + } + } + + return $result; + } + /** * GET a list of resources or objects of a given type * @@ -61,8 +111,12 @@ public function getObjects(string $type = 'objects', ?array $query = null, ?arra * @param array|null $headers Custom request headers * @return array|null Response in array format */ - public function getObject(string|int $id, string $type = 'objects', ?array $query = null, ?array $headers = null): ?array - { + public function getObject( + string|int $id, + string $type = 'objects', + ?array $query = null, + ?array $headers = null, + ): ?array { return $this->get(sprintf('/%s/%s', $type, $id), $query, $headers); } @@ -76,8 +130,13 @@ public function getObject(string|int $id, string $type = 'objects', ?array $quer * @param array|null $headers Custom request headers * @return array|null Response in array format */ - public function getRelated(string|int $id, string $type, string $relation, ?array $query = null, ?array $headers = null): ?array - { + public function getRelated( + string|int $id, + string $type, + string $relation, + ?array $query = null, + ?array $headers = null, + ): ?array { return $this->get(sprintf('/%s/%s/%s', $type, $id, $relation), $query, $headers); } @@ -91,9 +150,18 @@ public function getRelated(string|int $id, string $type, string $relation, ?arra * @param array|null $headers Custom request headers * @return array|null Response in array format */ - public function addRelated(string|int $id, string $type, string $relation, array $data, ?array $headers = null): ?array - { - return $this->post(sprintf('/%s/%s/relationships/%s', $type, $id, $relation), json_encode(compact('data')), $headers); + public function addRelated( + string|int $id, + string $type, + string $relation, + array $data, + ?array $headers = null, + ): ?array { + return $this->post( + sprintf('/%s/%s/relationships/%s', $type, $id, $relation), + json_encode(compact('data')), + $headers, + ); } /** @@ -106,9 +174,18 @@ public function addRelated(string|int $id, string $type, string $relation, array * @param array|null $headers Custom request headers * @return array|null Response in array format */ - public function removeRelated(string|int $id, string $type, string $relation, array $data, ?array $headers = null): ?array - { - return $this->delete(sprintf('/%s/%s/relationships/%s', $type, $id, $relation), json_encode(compact('data')), $headers); + public function removeRelated( + string|int $id, + string $type, + string $relation, + array $data, + ?array $headers = null, + ): ?array { + return $this->delete( + sprintf('/%s/%s/relationships/%s', $type, $id, $relation), + json_encode(compact('data')), + $headers, + ); } /** @@ -121,9 +198,23 @@ public function removeRelated(string|int $id, string $type, string $relation, ar * @param array|null $headers Custom request headers * @return array|null Response in array format */ - public function replaceRelated(string|int $id, string $type, string $relation, array $data, ?array $headers = null): ?array - { - return $this->patch(sprintf('/%s/%s/relationships/%s', $type, $id, $relation), json_encode(compact('data')), $headers); + public function replaceRelated( + string|int $id, + string $type, + string $relation, + array $data, + ?array $headers = null, + ): ?array { + return $this->patch( + sprintf( + '/%s/%s/relationships/%s', + $type, + $id, + $relation, + ), + json_encode(compact('data')), + $headers, + ); } /** @@ -318,10 +409,12 @@ public function addStreamToMedia(string $streamId, string $id, string $type): vo 'id' => $id, 'type' => $type, ], - ]) + ]), ); if (empty($response)) { - throw new BEditaClientException('Invalid response from PATCH ' . sprintf('/streams/%s/relationships/object', $id)); + throw new BEditaClientException( + 'Invalid response from PATCH ' . sprintf('/streams/%s/relationships/object', $id), + ); } } @@ -360,7 +453,7 @@ public function schema(string $type): ?array return $this->get( sprintf('/model/schema/%s', $type), null, - ['Accept' => 'application/schema+json'] + ['Accept' => 'application/schema+json'], ); } @@ -374,7 +467,7 @@ public function relationData(string $name): ?array { return $this->get( sprintf('/model/relations/%s', $name), - ['include' => 'left_object_types,right_object_types'] + ['include' => 'left_object_types,right_object_types'], ); } @@ -394,7 +487,7 @@ public function restoreObject(string|int $id, string $type): ?array 'id' => $id, 'type' => $type, ], - ]) + ]), ); } diff --git a/src/BaseClient.php b/src/BaseClient.php index 77f9c0a..c535f07 100644 --- a/src/BaseClient.php +++ b/src/BaseClient.php @@ -245,8 +245,13 @@ public function refreshTokens(): void * @param \Psr\Http\Message\StreamInterface|resource|string|null $body Request body. * @return \Psr\Http\Message\ResponseInterface */ - protected function sendRequestRetry(string $method, string $path, ?array $query = null, ?array $headers = null, $body = null): ResponseInterface - { + protected function sendRequestRetry( + string $method, + string $path, + ?array $query = null, + ?array $headers = null, + $body = null, + ): ResponseInterface { try { return $this->sendRequest($method, $path, $query, $headers, $body); } catch (BEditaClientException $e) { @@ -275,8 +280,13 @@ protected function sendRequestRetry(string $method, string $path, ?array $query * @param \Psr\Http\Message\StreamInterface|resource|string|null $body Request body. * @return \Psr\Http\Message\ResponseInterface */ - protected function refreshAndRetry(string $method, string $path, ?array $query = null, ?array $headers = null, $body = null): ResponseInterface - { + protected function refreshAndRetry( + string $method, + string $path, + ?array $query = null, + ?array $headers = null, + $body = null, + ): ResponseInterface { $this->refreshTokens(); unset($headers['Authorization']); @@ -294,8 +304,13 @@ protected function refreshAndRetry(string $method, string $path, ?array $query = * @return \Psr\Http\Message\ResponseInterface * @throws \BEdita\SDK\BEditaClientException Throws an exception if server response code is not 20x. */ - protected function sendRequest(string $method, string $path, ?array $query = null, ?array $headers = null, $body = null): ResponseInterface - { + protected function sendRequest( + string $method, + string $path, + ?array $query = null, + ?array $headers = null, + $body = null, + ): ResponseInterface { $uri = $this->requestUri($path, $query); $headers = array_merge($this->defaultHeaders, (array)$headers); diff --git a/src/LogTrait.php b/src/LogTrait.php index 8886cc6..96694ec 100644 --- a/src/LogTrait.php +++ b/src/LogTrait.php @@ -76,7 +76,7 @@ public function logRequest(RequestInterface $request): void $request->getMethod(), $request->getUri(), $this->requestHeadersCleanup($request), - $this->requestBodyCleanup($request) + $this->requestBodyCleanup($request), ); $this->logger->info($msg); } @@ -155,7 +155,7 @@ public function logResponse(ResponseInterface $response): void $response->getStatusCode(), $response->getReasonPhrase(), json_encode($response->getHeaders()), - $this->responseBodyCleanup($response) + $this->responseBodyCleanup($response), ); $this->logger->info($msg); } diff --git a/tests/TestCase/BEditaClientTest.php b/tests/TestCase/BEditaClientTest.php index fdc0676..7f54b6e 100644 --- a/tests/TestCase/BEditaClientTest.php +++ b/tests/TestCase/BEditaClientTest.php @@ -135,6 +135,106 @@ private function authenticate(): void $this->client->setupTokens($response['meta']); } + /** + * Test `bulkEdit` method + * + * @return void + */ + public function testBulkEdit(): void + { + $this->authenticate(); + // create 2 documents with status draft, one locked, one not locked + $response = $this->client->save('documents', [ + 'title' => 'this is a test document 1', + 'status' => 'draft', + ]); + $id1 = $response['data']['id']; + $response = $this->client->save('documents', [ + 'title' => 'this is a test document 2', + 'status' => 'draft', + ]); + $id2 = $response['data']['id']; + // lock the second document + $this->client->patch( + sprintf('/documents/%s', $id2), + json_encode([ + 'data' => [ + 'id' => $id2, + 'type' => 'documents', + 'meta' => [ + 'locked' => true, + ], + ], + ]), + ['Content-Type' => 'application/vnd.api+json'], + ); + $objects = ['documents' => [$id1, $id2]]; + $attributes = ['status' => 'on']; + $response = $this->client->bulkEdit($objects, $attributes); + $response = $response['data']; + static::assertNotEmpty($response); + static::assertArrayHasKey('saved', $response); + static::assertArrayNotHasKey('error', $response); + static::assertEquals([$id1], $response['saved']); + static::assertEquals([['id' => $id2, 'message' => 'Operation not allowed on "locked" objects']], $response['errors']); + } + + /** + * Test `bulkEdit` method retrocompatibility mode + * + * @return void + */ + public function testBulkEditRetrocompatibility(): void + { + // mock $this->post('/bulk/edit') to return an exception, to force use retrocompatibility mode + $client = new class ($this->apiBaseUrl, $this->apiKey) extends BEditaClient { + public function post(string $path, ?string $body = null, ?array $headers = null): ?array + { + if ($path === '/bulk/edit') { + throw new BEditaClientException('[404] Not Found', 404); + } + + return parent::post($path, $body, $headers); + } + }; + $response = $client->authenticate($this->adminUser, $this->adminPassword); + $client->setupTokens($response['meta']); + // create 2 documents with status draft, one locked, one not locked + $response = $client->save('documents', [ + 'title' => 'this is a test document 1', + 'status' => 'draft', + ]); + $id1 = $response['data']['id']; + $response = $client->save('documents', [ + 'title' => 'this is a test document 2', + 'status' => 'draft', + ]); + $id2 = $response['data']['id']; + // lock the second document + $client->patch( + sprintf('/documents/%s', $id2), + json_encode([ + 'data' => [ + 'id' => $id2, + 'type' => 'documents', + 'meta' => [ + 'locked' => true, + ], + ], + ]), + ['Content-Type' => 'application/vnd.api+json'], + ); + $objects = ['documents' => [$id1, $id2]]; + $attributes = ['status' => 'on']; + $response = $client->bulkEdit($objects, $attributes); + $response = $response['data']; + static::assertNotEmpty($response); + static::assertArrayHasKey('saved', $response); + static::assertArrayNotHasKey('error', $response); + static::assertEquals([$id1], $response['saved']); + static::assertEquals([['id' => $id2, 'message' => '[403] Not Found']], $response['errors']); + } + /** * Test `getObjects` method * @@ -1162,15 +1262,15 @@ function ($document) { 'type' => $document['data']['type'], ]; }, - $documents - ) + $documents, + ), ); static::assertIsArray($addRelated); static::assertArrayHasKey('links', $addRelated); static::assertArrayHasKey('self', $addRelated['links']); static::assertStringContainsString( sprintf('/folders/%s/relationships/children', $folder['data']['id']), - $addRelated['links']['self'] + $addRelated['links']['self'], ); // get folder children @@ -1191,7 +1291,7 @@ function ($document) { static::assertArrayHasKey('self', $getRelated['links']); static::assertStringContainsString( sprintf('/folders/%s/children', $folder['data']['id']), - $getRelated['links']['self'] + $getRelated['links']['self'], ); // remove 5 documents from folder children @@ -1206,15 +1306,15 @@ function ($document) { 'type' => $document['data']['type'], ]; }, - array_slice($documents, 0, 5) - ) + array_slice($documents, 0, 5), + ), ); static::assertIsArray($removeRelated); static::assertArrayHasKey('links', $removeRelated); static::assertArrayHasKey('self', $removeRelated['links']); static::assertStringContainsString( sprintf('/folders/%s/relationships/children', $folder['data']['id']), - $removeRelated['links']['self'] + $removeRelated['links']['self'], ); // get again folder children: should be 5 @@ -1234,8 +1334,8 @@ function ($document) { 'type' => $document['data']['type'], ]; }, - $documentsReplace - ) + $documentsReplace, + ), ); // get again folder children: should be 2 @@ -1254,8 +1354,8 @@ function ($document) { 'type' => $document['data']['type'], ]; }, - $documentsReplace - ) + $documentsReplace, + ), ); // get again folder children: should be 0 diff --git a/tests/TestCase/BaseClientTest.php b/tests/TestCase/BaseClientTest.php index cda298c..b7750aa 100644 --- a/tests/TestCase/BaseClientTest.php +++ b/tests/TestCase/BaseClientTest.php @@ -436,7 +436,7 @@ public function sendRequest( string $path, ?array $query = null, ?array $headers = null, - $body = null + $body = null, ): ResponseInterface { throw new class extends BEditaClientException { public function __construct() diff --git a/tests/TestCase/MyBaseClient.php b/tests/TestCase/MyBaseClient.php index b5160c8..0123368 100644 --- a/tests/TestCase/MyBaseClient.php +++ b/tests/TestCase/MyBaseClient.php @@ -32,7 +32,7 @@ public function sendRequestRetry( string $path, ?array $query = null, ?array $headers = null, - $body = null + $body = null, ): ResponseInterface { return parent::sendRequestRetry($method, $path, $query, $headers, $body); } @@ -45,7 +45,7 @@ public function sendRequest( string $path, ?array $query = null, ?array $headers = null, - $body = null + $body = null, ): ResponseInterface { return parent::sendRequest($method, $path, $query, $headers, $body); }