diff --git a/src/Component/Akeneo/Client/ApiReader.php b/src/Component/Akeneo/Client/ApiReader.php index eb466749..5e21634d 100644 --- a/src/Component/Akeneo/Client/ApiReader.php +++ b/src/Component/Akeneo/Client/ApiReader.php @@ -2,190 +2,30 @@ namespace Misery\Component\Akeneo\Client; -use Misery\Component\Common\Client\ApiClientInterface; -use Misery\Component\Common\Client\ApiEndpointInterface; -use Misery\Component\Common\Client\Paginator; -use Misery\Component\Common\Utils\ValueFormatter; -use Misery\Component\Reader\ItemReader; +use App\Component\Common\Resource\EntityResourceInterface; use Misery\Component\Reader\ReaderInterface; class ApiReader implements ReaderInterface { - private $client; - private $page; - private $endpoint; - private $context; - private $activeEndpoint; - public function __construct( - ApiClientInterface $client, - ApiEndpointInterface $endpoint, - array $context - ) { - $this->client = $client; - $this->endpoint = $endpoint; - $this->context = $context; - } - - private function request($endpoint = false): array - { - if (!$endpoint) { - $endpoint = $this->endpoint->getAll(); - } - - // todo - create function for this. Check how filtering must be applied. - if(isset($this->context['limiters']['query_array'])) { - $endpoint = $this->client->getUrlGenerator()->generate($endpoint); - - $params = ['search' => json_encode($this->context['limiters']['query_array'])]; - if ($this->endpoint instanceof ApiProductModelsEndpoint || $this->endpoint instanceof ApiProductsEndpoint) { - $params['pagination_type'] = 'search_after'; - } - - return $this->client - ->search($endpoint, $params) - ->getResponse() - ->getContent(); - } - - if(isset($this->context['limiters']['querystring'])) { - $querystring = preg_replace('/\s+/', '+', $this->context['limiters']['querystring']); - $querystring = ValueFormatter::format($querystring, $this->context); - $endpoint = sprintf($querystring, $endpoint); - } - - $items = []; - if (isset($this->context['filters']) && !empty($this->context['filters'])) { - if ($this->endpoint instanceof ApiProductModelsEndpoint || $this->endpoint instanceof ApiProductsEndpoint) { - $endpoint = sprintf('%s?pagination_type=search_after&search=', $endpoint); - } else { - $endpoint = sprintf('%s?search=', $endpoint); - } - foreach ($this->context['filters'] as $attrCode => $filterValues) { - $valueChunks = array_chunk(array_values($filterValues),100); - foreach ($valueChunks as $filterChunk) { - $filter = [$attrCode => [['operator' => 'IN', 'value' => $filterChunk]]]; - $chunkEndpoint = sprintf('%s%s&limit=100', $endpoint, json_encode($filter)); - - $result = $this->client - ->get($this->client->getUrlGenerator()->generate($chunkEndpoint)) - ->getResponse() - ->getContent(); - - if (empty($items)){ - $items = $result; - - continue; - } - - $items['_embedded']['items'] = array_merge( - $items['_embedded']['items'], - $result['_embedded']['items'] - ); - } - } - - return $items; - } - - $url = $this->client->getUrlGenerator()->generate($endpoint); - if ($this->endpoint instanceof ApiProductModelsEndpoint || $this->endpoint instanceof ApiProductsEndpoint) { - if(!strpos($url, 'pagination_type')) { - if(isset($this->context['limiters']['querystring'])) { - $url = sprintf('%s&pagination_type=search_after', $url); - } else { - $url = sprintf('%s?pagination_type=search_after', $url); - } - } - } - - $items = $this->client - ->get($url) - ->getResponse() - ->getContent(); - - // when supplying a container we jump inside that container to find loopable items - if ($this->context['container']) { - if (array_key_exists($this->context['container'], $items)) { - $items['_embedded']['items'] = $items[$this->context['container']]; - unset($items[$this->context['container']]); - } - } - - return $items; - } + private readonly EntityResourceInterface $entityResource, + private ?\Iterator $cursor = null + ) {} public function read() { - if (isset($this->context['multiple'])) { - return $this->readMultiple(); + if ($this->cursor === null) { + $this->cursor = $this->entityResource->getAll(); } - - // TODO we need to align all readers together into this version - if (str_contains($this->endpoint::class, 'E5DalApi')) { - // new Paginator - if (null === $this->page) { - $this->page = $this->client->getPaginator($this->endpoint->getAll()); - } - $item = $this->page->current(); - $this->page->next(); - return $item; + if (!$this->cursor->valid()) { + return null; } - - if (null === $this->page) { - $this->page = Paginator::create($this->client, $this->request()); - } - - $item = $this->page->getItems()->current(); - if (!$item) { - $this->page = $this->page->getNextPage(); - if (!$this->page) { - return false; - } - $item = $this->page->getItems()->current(); - } - $this->page->getItems()->next(); - - unset($item['_links']); + $item = $this->cursor->current(); + $this->cursor->next(); return $item; } - public function readMultiple() - { - foreach ($this->context['list'] as $key => $endpointItem) { - if ($this->activeEndpoint !== $endpointItem || null === $this->page) { - $endpoint = sprintf($this->endpoint->getAll(), $endpointItem); - $this->page = Paginator::create($this->client, $this->request($endpoint)); - $this->activeEndpoint = $endpointItem; - } - - $item = $this->page->getItems()->current(); - if (!$item) { - $this->page = $this->page->getNextPage(); - if (!$this->page) { - unset($this->context['list'][$key]); - - return $this->readMultiple(); - } - $item = $this->page->getItems()->current(); - } - - $this->page->getItems()->next(); - if (!$item) { - unset($this->context['list'][$key]); - - return $this->readMultiple(); - } - - unset($item['_links']); - - return $item; - } - - return false; - } - public function getIterator(): \Iterator { while ($item = $this->read()) { @@ -195,45 +35,17 @@ public function getIterator(): \Iterator public function find(array $constraints): ReaderInterface { - // TODO we need to implement a find or search int the API - $reader = $this; - foreach ($constraints as $columnName => $rowValue) { - if (is_string($rowValue)) { - $rowValue = [$rowValue]; - } - - $reader = $reader->filter(static function ($row) use ($rowValue, $columnName) { - return in_array($row[$columnName], $rowValue); - }); - } - - return $reader; + throw new \RuntimeException('Not implemented'); } public function filter(callable $callable): ReaderInterface { - return new ItemReader($this->processFilter($callable)); - } - - private function processFilter(callable $callable): \Generator - { - foreach ($this->getIterator() as $key => $row) { - if (true === $callable($row)) { - yield $key => $row; - } - } + throw new \RuntimeException('Not implemented'); } public function map(callable $callable): ReaderInterface { - return new ItemReader($this->processMap($callable)); - } - - private function processMap(callable $callable): \Generator - { - foreach ($this->getIterator() as $key => $row) { - yield $key => $callable($row); - } + throw new \RuntimeException('Not implemented'); } public function getItems(): array diff --git a/src/Component/Akeneo/Client/ApiReaderOld.php b/src/Component/Akeneo/Client/ApiReaderOld.php new file mode 100644 index 00000000..a1eaacf2 --- /dev/null +++ b/src/Component/Akeneo/Client/ApiReaderOld.php @@ -0,0 +1,248 @@ +client = $client; + $this->endpoint = $endpoint; + $this->context = $context; + } + + private function request($endpoint = false): array + { + if (!$endpoint) { + $endpoint = $this->endpoint->getAll(); + } + + // todo - create function for this. Check how filtering must be applied. + if(isset($this->context['limiters']['query_array'])) { + $endpoint = $this->client->getUrlGenerator()->generate($endpoint); + + $params = ['search' => json_encode($this->context['limiters']['query_array'])]; + if ($this->endpoint instanceof ApiProductModelsEndpoint || $this->endpoint instanceof ApiProductsEndpoint) { + $params['pagination_type'] = 'search_after'; + } + + return $this->client + ->search($endpoint, $params) + ->getResponse() + ->getContent(); + } + + if(isset($this->context['limiters']['querystring'])) { + $querystring = preg_replace('/\s+/', '+', $this->context['limiters']['querystring']); + $querystring = ValueFormatter::format($querystring, $this->context); + $endpoint = sprintf($querystring, $endpoint); + } + + $items = []; + if (isset($this->context['filters']) && !empty($this->context['filters'])) { + if ($this->endpoint instanceof ApiProductModelsEndpoint || $this->endpoint instanceof ApiProductsEndpoint) { + $endpoint = sprintf('%s?pagination_type=search_after&search=', $endpoint); + } else { + $endpoint = sprintf('%s?search=', $endpoint); + } + foreach ($this->context['filters'] as $attrCode => $filterValues) { + $valueChunks = array_chunk(array_values($filterValues),100); + foreach ($valueChunks as $filterChunk) { + $filter = [$attrCode => [['operator' => 'IN', 'value' => $filterChunk]]]; + $chunkEndpoint = sprintf('%s%s&limit=100', $endpoint, json_encode($filter)); + + $result = $this->client + ->get($this->client->getUrlGenerator()->generate($chunkEndpoint)) + ->getResponse() + ->getContent(); + + if (empty($items)){ + $items = $result; + + continue; + } + + $items['_embedded']['items'] = array_merge( + $items['_embedded']['items'], + $result['_embedded']['items'] + ); + } + } + + return $items; + } + + $url = $this->client->getUrlGenerator()->generate($endpoint); + if ($this->endpoint instanceof ApiProductModelsEndpoint || $this->endpoint instanceof ApiProductsEndpoint) { + if(!strpos($url, 'pagination_type')) { + if(isset($this->context['limiters']['querystring'])) { + $url = sprintf('%s&pagination_type=search_after', $url); + } else { + $url = sprintf('%s?pagination_type=search_after', $url); + } + } + } + + $items = $this->client + ->get($url) + ->getResponse() + ->getContent(); + + // when supplying a container we jump inside that container to find loopable items + if ($this->context['container']) { + if (array_key_exists($this->context['container'], $items)) { + $items['_embedded']['items'] = $items[$this->context['container']]; + unset($items[$this->context['container']]); + } + } + + return $items; + } + + public function read() + { + if (isset($this->context['multiple'])) { + return $this->readMultiple(); + } + + // TODO we need to align all readers together into this version + if (str_contains($this->endpoint::class, 'E5DalApi')) { + // new Paginator + if (null === $this->page) { + $this->page = $this->client->getPaginator($this->endpoint->getAll()); + } + $item = $this->page->current(); + $this->page->next(); + return $item; + } + + if (null === $this->page) { + $this->page = Paginator::create($this->client, $this->request()); + } + + $item = $this->page->getItems()->current(); + if (!$item) { + $this->page = $this->page->getNextPage(); + if (!$this->page) { + return false; + } + $item = $this->page->getItems()->current(); + } + $this->page->getItems()->next(); + + unset($item['_links']); + + return $item; + } + + public function readMultiple() + { + foreach ($this->context['list'] as $key => $endpointItem) { + if ($this->activeEndpoint !== $endpointItem || null === $this->page) { + $endpoint = sprintf($this->endpoint->getAll(), $endpointItem); + $this->page = Paginator::create($this->client, $this->request($endpoint)); + $this->activeEndpoint = $endpointItem; + } + + $item = $this->page->getItems()->current(); + if (!$item) { + $this->page = $this->page->getNextPage(); + if (!$this->page) { + unset($this->context['list'][$key]); + + return $this->readMultiple(); + } + $item = $this->page->getItems()->current(); + } + + $this->page->getItems()->next(); + if (!$item) { + unset($this->context['list'][$key]); + + return $this->readMultiple(); + } + + unset($item['_links']); + + return $item; + } + + return false; + } + + public function getIterator(): \Iterator + { + while ($item = $this->read()) { + yield $item; + } + } + + public function find(array $constraints): ReaderInterface + { + // TODO we need to implement a find or search int the API + $reader = $this; + foreach ($constraints as $columnName => $rowValue) { + if (is_string($rowValue)) { + $rowValue = [$rowValue]; + } + + $reader = $reader->filter(static function ($row) use ($rowValue, $columnName) { + return in_array($row[$columnName], $rowValue); + }); + } + + return $reader; + } + + public function filter(callable $callable): ReaderInterface + { + return new ItemReader($this->processFilter($callable)); + } + + private function processFilter(callable $callable): \Generator + { + foreach ($this->getIterator() as $key => $row) { + if (true === $callable($row)) { + yield $key => $row; + } + } + } + + public function map(callable $callable): ReaderInterface + { + return new ItemReader($this->processMap($callable)); + } + + private function processMap(callable $callable): \Generator + { + foreach ($this->getIterator() as $key => $row) { + yield $key => $callable($row); + } + } + + public function getItems(): array + { + return iterator_to_array($this->getIterator()); + } + + public function clear(): void + { + // TODO: Implement clear() method. + } +} diff --git a/src/Component/Akeneo/Client/HttpReaderFactory.php b/src/Component/Akeneo/Client/HttpReaderFactory.php index d0c55c10..29c756aa 100644 --- a/src/Component/Akeneo/Client/HttpReaderFactory.php +++ b/src/Component/Akeneo/Client/HttpReaderFactory.php @@ -2,12 +2,18 @@ namespace Misery\Component\Akeneo\Client; +use App\Component\Akeneo\Api\Resources\AkeneoAttributeOptionsResource; +use App\Component\Akeneo\Api\Resources\AkeneoAttributesResource; +use App\Component\Akeneo\Api\Resources\AkeneoCategoryResource; +use App\Component\Akeneo\Api\Resources\AkeneoFamiliesResource; +use App\Component\Akeneo\Api\Resources\AkeneoProductsResource; +use App\Component\Akeneo\Api\Resources\Filter\GlobalQueryFilterParameters; +use App\Component\Common\Cursor\CursorInterface; +use App\Component\Common\Cursor\MultiCursor; use Assert\Assert; -use Misery\Component\Common\Client\Endpoint\BasicApiEndpoint; use Misery\Component\Common\Registry\RegisteredByNameInterface; use Misery\Component\Configurator\Configuration; use Misery\Component\Reader\ReaderInterface; -use Misery\Component\Writer\ItemWriterInterface; class HttpReaderFactory implements RegisteredByNameInterface { @@ -18,73 +24,103 @@ public function createFromConfiguration(array $configuration, Configuration $con 'type must be filled in.' )->notEmpty()->string()->inArray(['rest_api']); - if ($configuration['type'] === 'rest_api') { - Assert::that( - $configuration['endpoint'], - 'endpoint must be filled in.' - )->notEmpty()->string(); - - Assert::that( - $configuration['method'], - 'method must be filled in.' - )->notEmpty()->string()->inArray([ - 'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'MULTI_PATCH', 'get', 'post', 'put', 'delete', 'patch', 'multi_patch' - ]); + Assert::that( + $configuration['endpoint'], + 'endpoint must be filled in.' + )->notEmpty()->string(); - $endpoint = $configuration['endpoint']; - $method = $configuration['method']; + Assert::that( + $configuration['method'], + 'method must be filled in.' + )->notEmpty()->string()->inArray(['GET', 'get']); - Assert::that( - $endpoint, - 'endpoint must be valid.' - )->notNull()->notEmpty(); + $endpoint = $configuration['endpoint']; - $context = ['filters' => []]; - $context['container'] = $configuration['container'] ?? null; - $configContext = $config->getContext(); + $context = ['filters' => []]; + $context['container'] = $configuration['container'] ?? null; + $configContext = $config->getContext(); - if (isset($configuration['identifier_filter_list'])) { - $context['multiple'] = true; - $context['list'] = is_array($configuration['identifier_filter_list']) ? $configuration['identifier_filter_list'] : $config->getList($configuration['identifier_filter_list']); - } + if (isset($configuration['identifier_filter_list'])) { + $context['multiple'] = true; + $context['list'] = is_array($configuration['identifier_filter_list']) ? $configuration['identifier_filter_list'] : $config->getList($configuration['identifier_filter_list']); + } - if (isset($configuration['filters'])) { - $filters = $configuration['filters']; - foreach ($filters as $fieldCode => $filterConfig) { - foreach ($filterConfig as $filterType => $value) { - if ($filterType === 'list') { - $context['filters'][$fieldCode] = $config->getList($value); - } + if (isset($configuration['filters'])) { + $filters = $configuration['filters']; + foreach ($filters as $fieldCode => $filterConfig) { + foreach ($filterConfig as $filterType => $value) { + if ($filterType === 'list') { + $context['filters'][$fieldCode] = $config->getList($value); } } } + } - $context['limiters'] = $configuration['limiters'] ?? []; - if (isset($configuration['akeneo-filter'])) { - $akeneoFilter = $configuration['akeneo-filter']; - if (!isset($configContext['akeneo_filters'][$akeneoFilter])) { - throw new \Exception(sprintf('The configuration is using an Akeneo filter code (%s) wich is not linked to this job profile.', $configuration['akeneo-filter'])); - } + $context['limiters'] = $configuration['limiters'] ?? []; + if (isset($configuration['akeneo-filter'])) { + $akeneoFilter = $configuration['akeneo-filter']; + if (!isset($configContext['akeneo_filters'][$akeneoFilter])) { + throw new \Exception(sprintf('The configuration is using an Akeneo filter code (%s) wich is not linked to this job profile.', $configuration['akeneo-filter'])); + } + + // create query string + $context['limiters']['query_array'] = $configContext['akeneo_filters'][$akeneoFilter]['search']; + } - // create query string - $context['limiters']['query_array'] = $configContext['akeneo_filters'][$akeneoFilter]['search']; + $accountCode = $configuration['account'] ?? null; + if (!$accountCode) { + throw new \Exception(sprintf('Account "%s" not found.', $accountCode)); + } + $client = $config->getAccount($accountCode); + if (!$client) { + throw new \Exception(sprintf('Account "%s" not found.', $accountCode)); + } + $filterParameters = new GlobalQueryFilterParameters(); + + $queryString = $context['limiters']['querystring'] ?? null; + + if ($endpoint === 'attributes') { + $resource = new AkeneoAttributesResource($client, $filterParameters); + + return new ApiReader($resource); + } elseif ($endpoint === 'families') { + $resource = new AkeneoFamiliesResource($client, $filterParameters); + + return new ApiReader($resource); + } elseif ($endpoint === 'categories') { + $resource = new AkeneoCategoryResource($client); + + if ($queryString) { + $cursor = $resource->querystring($queryString); + + return new ApiReader($resource, $cursor); } + return new ApiReader($resource); + } elseif ($endpoint === 'products') { + $resource = new AkeneoProductsResource($client, $filterParameters); - $accountCode = (isset($configuration['account'])) ? $configuration['account'] : 'source_resource'; - $account = $config->getAccount($accountCode); + if ($queryString) { + $cursor = $resource->queryStringContent($queryString); - if (!$account) { - throw new \Exception(sprintf('Account "%s" not found.', $accountCode)); + return new ApiReader($resource, $cursor); } - return new ApiReader( - $account, - $account->getApiEndpoint($endpoint), - $context + $configContext - ); - } + return new ApiReader($resource); + } elseif ($endpoint === 'options') { + $resource = new AkeneoAttributeOptionsResource($client); + + if (isset($configuration['identifier_filter_list'])) { + $cursor = new MultiCursor(); + foreach ($configuration['identifier_filter_list'] as $identifier) { + $cursor->addCursor($resource->getAllByAttributeCode($identifier)); + } + return new ApiReader($resource, $cursor); + } - throw new \Exception('Unknown type: ' . $configuration['type']); + return new ApiReader($resource); + } else { + throw new \Exception('Unknown endpoint: ' . $endpoint); + } } public function getName(): string diff --git a/src/Component/Common/Client/ApiClientFactory.php b/src/Component/Common/Client/ApiClientFactory.php index cdf650d9..f03d41e6 100644 --- a/src/Component/Common/Client/ApiClientFactory.php +++ b/src/Component/Common/Client/ApiClientFactory.php @@ -2,14 +2,21 @@ namespace Misery\Component\Common\Client; -use Misery\Component\Akeneo\Client\AkeneoApiClientAccount; +use App\Component\Akeneo\Api\Client\AkeneoApiClientAccount; +use App\Component\Common\Client\ApiClient; +use App\Component\Common\Client\ApiCurlClient; +use App\Component\Common\Client\ApiClientInterface; +use Misery\Component\Common\Client\ApiClientInterface as BaseApiClientInterface; use Misery\Component\Connections\BusinessCentral\Client\MicrosoftDynamicsOauthAccount; use Misery\Component\Common\Registry\RegisteredByNameInterface; use Misery\Component\Connections\E5Dal\Client\E5DalAPIAccount; +/** + * This Factory looks like a specific multi-tool implementation + */ class ApiClientFactory implements RegisteredByNameInterface { - public function createFromConfiguration(array $account): ApiClientInterface + public function createFromConfiguration(array $account): ApiClientInterface|BaseApiClientInterface { $type = $account['type'] ?? null; if ($type === 'basic_auth') { @@ -28,7 +35,7 @@ public function createFromConfiguration(array $account): ApiClientInterface if ($type === 'microsoft_oauth') { try { // no need to authorize a basic auth - $client = new ApiClient($account['domain']); + $client = new ApiCurlClient($account['domain']); $account = new MicrosoftDynamicsOauthAccount( $account['client_id'], @@ -48,7 +55,7 @@ public function createFromConfiguration(array $account): ApiClientInterface if ($type === 'e5_dal_token') { try { // no need to authorize token is fixed - $client = new ApiClient($account['domain']); + $client = new ApiCurlClient($account['domain']); $account = new E5DalAPIAccount($account['token']); $client->authorize($account); @@ -60,16 +67,16 @@ public function createFromConfiguration(array $account): ApiClientInterface } try { - $client = new ApiClient($account['domain']); - $account = new AkeneoApiClientAccount( + rtrim($account['domain'], '/'), $account['username'], $account['password'], $account['client_id'], $account['secret'] ?? $account['client_secret'] ); - $client->authorize($account); + $client = new ApiClient($account); + $client->authorize(); return $client; } catch (\Exception $e) { diff --git a/src/Component/Configurator/Configuration.php b/src/Component/Configurator/Configuration.php index c806ce6e..a290808d 100644 --- a/src/Component/Configurator/Configuration.php +++ b/src/Component/Configurator/Configuration.php @@ -8,8 +8,8 @@ use Psr\Log\LoggerInterface; use Misery\Component\Action\ItemActionProcessor; use Misery\Component\BluePrint\BluePrint; -use Misery\Component\Common\Client\ApiClient; -use Misery\Component\Common\Client\ApiClientInterface; +use App\Component\Common\Client\ApiClient; +use App\Component\Common\Client\ApiClientInterface; use Misery\Component\Common\Collection\ArrayCollection; use Misery\Component\Common\FileManager\LocalFileManager; use Misery\Component\Common\Pipeline\Pipeline;