Skip to content

Commit

Permalink
Add ItemImport handler (#123)
Browse files Browse the repository at this point in the history
  • Loading branch information
lruozzi9 committed Jun 20, 2022
1 parent 24a8862 commit 38efc40
Show file tree
Hide file tree
Showing 24 changed files with 241 additions and 288 deletions.
2 changes: 1 addition & 1 deletion features/importing_product_associations_from_queue.feature
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ Feature: Importing product associations from queue
And the store has a product "upsell-product-2" with code "upsell-product-2"
And there is one product associations to import with identifier "10627329" in the Akeneo queue
And the store has a product association type "Upsell" with a code "UPSELL"
When I import all items in queue
When I consume the messages
Then the product "10627329" should be associated to product "upsell-product-1" for association with code "UPSELL"
And the product "10627329" should be associated to product "upsell-product-2" for association with code "UPSELL"
18 changes: 8 additions & 10 deletions features/importing_products_from_queue.feature
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Feature: Importing products from queue
And the store is also available in "it_IT"
And there is one item to import with identifier "braided-hat-m" for the "Product" importer in the Akeneo queue
And there is one item to import with identifier "braided-hat-l" for the "Product" importer in the Akeneo queue
When I import all items in queue
When I consume the messages
Then the product "model-braided-hat" should exists with the right data
And the product variant "braided-hat-m" of product "model-braided-hat" should exists with the right data
And the product variant "braided-hat-l" of product "model-braided-hat" should exists with the right data
Expand All @@ -20,28 +20,26 @@ Feature: Importing products from queue
Given the store operates on a single channel
And the store is also available in "it_IT"
And there is one item to import with identifier "NOT_EXISTS" for the "Product" importer in the Akeneo queue
When I import all items in queue
Then the product "NOT_EXISTS" should not exists
And the queue item with identifier "NOT_EXISTS" for the "Product" importer has not been marked as imported
And the queue item with identifier "NOT_EXISTS" for the "Product" importer has an error message
When I consume the messages
Then the item import message for "NOT_EXISTS" identifier and the "Product" importer should have failed
And the product "NOT_EXISTS" should not exists

@cli
Scenario: Going on with subsequent product imports when any fail
Given the store operates on a single channel
And the store is also available in "it_IT"
And there is one item to import with identifier "NOT_EXISTS" for the "Product" importer in the Akeneo queue
And there is one item to import with identifier "braided-hat-m" for the "Product" importer in the Akeneo queue
When I import all items in queue
Then the product "NOT_EXISTS" should not exists
When I consume the messages
Then the item import message for "NOT_EXISTS" identifier and the "Product" importer should have failed
And the product "NOT_EXISTS" should not exists
And the product variant "braided-hat-m" of product "model-braided-hat" should exists with the right data
And the queue item with identifier "braided-hat-m" for the "Product" importer has been marked as imported
And the queue item with identifier "NOT_EXISTS" for the "Product" importer has not been marked as imported

@cli
Scenario: Importing products with images should not leave temporary files in temporary files directory
Given the store operates on a single channel
And the store is also available in "it_IT"
And there is one item to import with identifier "braided-hat-m" for the "Product" importer in the Akeneo queue
And there is one item to import with identifier "braided-hat-l" for the "Product" importer in the Akeneo queue
When I import all items in queue
When I consume the messages
Then there should not be any temporary file in the temporary files directory
5 changes: 0 additions & 5 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,8 @@

namespace Webgriffe\SyliusAkeneoPlugin\DependencyInjection;

use Sylius\Bundle\ResourceBundle\Controller\ResourceController;
use Sylius\Component\Resource\Factory\Factory;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Webgriffe\SyliusAkeneoPlugin\Doctrine\ORM\QueueItemRepository;
use Webgriffe\SyliusAkeneoPlugin\Entity\QueueItem;
use Webgriffe\SyliusAkeneoPlugin\Entity\QueueItemInterface;

final class Configuration implements ConfigurationInterface
{
Expand Down
2 changes: 2 additions & 0 deletions src/DependencyInjection/WebgriffeSyliusAkeneoExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ final class WebgriffeSyliusAkeneoExtension extends AbstractResourceExtension imp
'$productImageFactory' => 'sylius.factory.product_image',
'$productImageRepository' => 'sylius.repository.product_image',
'$apiClient' => 'webgriffe_sylius_akeneo.api_client',
'$temporaryFilesManager' => 'webgriffe_sylius_akeneo.temporary_file_manager',
],
],
'immutable_slug' => [
Expand Down Expand Up @@ -99,6 +100,7 @@ final class WebgriffeSyliusAkeneoExtension extends AbstractResourceExtension imp
'arguments' => [
'$apiClient' => 'webgriffe_sylius_akeneo.api_client',
'$filesystem' => 'filesystem',
'$temporaryFilesManager' => 'webgriffe_sylius_akeneo.temporary_file_manager',
],
],
'metric_property' => [
Expand Down
31 changes: 0 additions & 31 deletions src/EventSubscriber/CommandEventSubscriber.php

This file was deleted.

38 changes: 38 additions & 0 deletions src/EventSubscriber/ProductEventSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace Webgriffe\SyliusAkeneoPlugin\EventSubscriber;

use Sylius\Component\Core\Model\ImagesAwareInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\EventDispatcher\GenericEvent;
use Webmozart\Assert\Assert;

final class ProductEventSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
'sylius.product.pre_create' => ['removeImagesFileProperty', -50],
'sylius.product.pre_update' => ['removeImagesFileProperty', -50],
];
}

/**
* When more than two variants with different images are handled by the same instance of Messenger
* the file property should be removed after having uploaded with the Sylius\Bundle\CoreBundle\EventListener\ImagesUploadListener.
* Otherwise, the next pre create/update product event will throw an error by getting content from the first image that was removed by the TemporaryFilesManager.
* See features/importing_products_from_queue.feature for having a real case.
*/
public function removeImagesFileProperty(GenericEvent $event): void
{
/** @var ImagesAwareInterface|mixed $subject */
$subject = $event->getSubject();
Assert::isInstanceOf($subject, ImagesAwareInterface::class);

foreach ($subject->getImages() as $image) {
$image->setFile(null);
}
}
}
44 changes: 44 additions & 0 deletions src/MessageHandler/ItemImportHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace Webgriffe\SyliusAkeneoPlugin\MessageHandler;

use Doctrine\ORM\EntityManagerInterface;
use RuntimeException;
use Webgriffe\SyliusAkeneoPlugin\ImporterInterface;
use Webgriffe\SyliusAkeneoPlugin\ImporterRegistryInterface;
use Webgriffe\SyliusAkeneoPlugin\Message\ItemImport;
use Webgriffe\SyliusAkeneoPlugin\TemporaryFilesManager;

final class ItemImportHandler
{
public function __construct(
private ImporterRegistryInterface $importerRegistry,
private TemporaryFilesManager $temporaryFilesManager,
private EntityManagerInterface $entityManager,
) {
}

public function __invoke(ItemImport $message): void
{
$akeneoIdentifier = $message->getAkeneoIdentifier();
$importer = $this->resolveImporter($message->getAkeneoEntity());
$importer->import($akeneoIdentifier);

$this->entityManager->flush();

$this->temporaryFilesManager->deleteAllTemporaryFiles();
}

private function resolveImporter(string $akeneoEntity): ImporterInterface
{
foreach ($this->importerRegistry->all() as $importer) {
if ($importer->getAkeneoEntity() === $akeneoEntity) {
return $importer;
}
}

throw new RuntimeException(sprintf('Cannot find suitable importer for entity "%s".', $akeneoEntity));
}
}
1 change: 1 addition & 0 deletions src/Resources/config/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,5 @@ sylius_grid:
framework:
messenger:
routing:
'Webgriffe\SyliusAkeneoPlugin\Message\ItemImport': main

12 changes: 10 additions & 2 deletions src/Resources/config/services.xml
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,7 @@
<argument>%webgriffe_sylius_akeneo.temporary_files_prefix%</argument>
</service>

<service id="webgriffe_sylius_akeneo.event_subscriber.command" class="Webgriffe\SyliusAkeneoPlugin\EventSubscriber\CommandEventSubscriber">
<argument type="service" id="webgriffe_sylius_akeneo.temporary_file_manager" />
<service id="webgriffe_sylius_akeneo.event_subscriber.product" class="Webgriffe\SyliusAkeneoPlugin\EventSubscriber\ProductEventSubscriber">
<tag name="kernel.event_subscriber" />
</service>

Expand Down Expand Up @@ -139,5 +138,14 @@
<service id="webgriffe_sylius_akeneo.converter.unit_measurement_value" class="Webgriffe\SyliusAkeneoPlugin\Converter\UnitMeasurementValueConverter">
<argument type="service" id="webgriffe_sylius_akeneo.api_client" />
</service>

<service id="webgriffe_sylius_akeneo.message_handler.item_import"
class="Webgriffe\SyliusAkeneoPlugin\MessageHandler\ItemImportHandler">
<argument type="service" id="webgriffe_sylius_akeneo.importer_registry"/>
<argument type="service" id="webgriffe_sylius_akeneo.temporary_file_manager"/>
<argument type="service" id="doctrine.orm.entity_manager"/>
<tag name="messenger.message_handler"/>
</service>

</services>
</container>
13 changes: 10 additions & 3 deletions src/ValueHandler/FileAttributeValueHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
use Akeneo\Pim\ApiClient\AkeneoPimClientInterface;
use Akeneo\Pim\ApiClient\Exception\HttpException;
use InvalidArgumentException;
use const JSON_THROW_ON_ERROR;
use SplFileInfo;
use Sylius\Component\Channel\Model\ChannelInterface;
use Sylius\Component\Core\Model\ProductInterface;
use Sylius\Component\Core\Model\ProductVariantInterface;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpKernel\Exception\HttpException as SymfonyHttpException;
use Webgriffe\SyliusAkeneoPlugin\TemporaryFilesManagerInterface;
use Webgriffe\SyliusAkeneoPlugin\ValueHandlerInterface;
use Webmozart\Assert\Assert;

Expand All @@ -24,6 +26,7 @@ final class FileAttributeValueHandler implements ValueHandlerInterface
public function __construct(
private AkeneoPimClientInterface $apiClient,
private Filesystem $filesystem,
private TemporaryFilesManagerInterface $temporaryFilesManager,
private string $akeneoAttributeCode,
private string $downloadPath,
) {
Expand Down Expand Up @@ -137,14 +140,18 @@ private function downloadFile(string $mediaCode): SplFileInfo
$bodyContents = $response->getBody()->getContents();
if ($statusClass !== 2) {
/** @var array $responseResult */
$responseResult = json_decode($bodyContents, true, 512, \JSON_THROW_ON_ERROR);
$responseResult = json_decode($bodyContents, true, 512, JSON_THROW_ON_ERROR);

throw new SymfonyHttpException((int) $responseResult['code'], (string) $responseResult['message']);
}
$tempName = tempnam(sys_get_temp_dir(), 'akeneo-');
Assert::string($tempName);
$tempName = $this->generateTempFilePath();
file_put_contents($tempName, $bodyContents);

return new File($tempName);
}

private function generateTempFilePath(): string
{
return $this->temporaryFilesManager->generateTemporaryFilePath();
}
}
21 changes: 14 additions & 7 deletions src/ValueHandler/ImageValueHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Webgriffe\SyliusAkeneoPlugin\ValueHandler;

use Akeneo\Pim\ApiClient\AkeneoPimClientInterface;
use InvalidArgumentException;
use SplFileInfo;
use Sylius\Component\Channel\Model\ChannelInterface;
use Sylius\Component\Core\Model\ProductImageInterface;
Expand All @@ -14,6 +15,7 @@
use Sylius\Component\Resource\Repository\RepositoryInterface;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Webgriffe\SyliusAkeneoPlugin\TemporaryFilesManager;
use Webgriffe\SyliusAkeneoPlugin\ValueHandlerInterface;
use Webmozart\Assert\Assert;

Expand All @@ -23,6 +25,7 @@ public function __construct(
private FactoryInterface $productImageFactory,
private RepositoryInterface $productImageRepository,
private AkeneoPimClientInterface $apiClient,
private TemporaryFilesManager $temporaryFilesManager,
private string $akeneoAttributeCode,
private string $syliusImageType,
) {
Expand All @@ -42,7 +45,7 @@ public function supports($subject, string $attribute, array $value): bool
public function handle($subject, string $attribute, array $value): void
{
if (!$subject instanceof ProductVariantInterface) {
throw new \InvalidArgumentException(
throw new InvalidArgumentException(
sprintf(
'This image value handler only supports instances of %s, %s given.',
ProductVariantInterface::class,
Expand Down Expand Up @@ -137,15 +140,15 @@ private function getValue(array $value, ProductInterface $product): ?string
$productChannelCodes = array_map(static fn (ChannelInterface $channel): ?string => $channel->getCode(), $product->getChannels()->toArray());
foreach ($value as $valueData) {
if (!is_array($valueData)) {
throw new \InvalidArgumentException(sprintf('Invalid Akeneo value data: expected an array, "%s" given.', gettype($valueData)));
throw new InvalidArgumentException(sprintf('Invalid Akeneo value data: expected an array, "%s" given.', gettype($valueData)));
}
// todo: we should throw here? it seeme that API won't never return an empty array
if (!array_key_exists('data', $valueData)) {
continue;
}

if (!array_key_exists('scope', $valueData)) {
throw new \InvalidArgumentException('Invalid Akeneo value data: required "scope" information was not found.');
throw new InvalidArgumentException('Invalid Akeneo value data: required "scope" information was not found.');
}
if ($valueData['scope'] !== null && !in_array($valueData['scope'], $productChannelCodes, true)) {
continue;
Expand All @@ -154,13 +157,13 @@ private function getValue(array $value, ProductInterface $product): ?string
/** @psalm-suppress MixedAssignment */
$data = $valueData['data'];
if (!is_string($data) && null !== $data) {
throw new \InvalidArgumentException(sprintf('Invalid Akeneo value data: expected a string or null value, got "%s".', gettype($data)));
throw new InvalidArgumentException(sprintf('Invalid Akeneo value data: expected a string or null value, got "%s".', gettype($data)));
}

return $data;
}

throw new \InvalidArgumentException('Invalid Akeneo value data: cannot find the media code.');
throw new InvalidArgumentException('Invalid Akeneo value data: cannot find the media code.');
}

private function downloadFile(string $mediaCode): SplFileInfo
Expand All @@ -174,10 +177,14 @@ private function downloadFile(string $mediaCode): SplFileInfo

throw new HttpException((int) $responseResult['code'], (string) $responseResult['message']);
}
$tempName = tempnam(sys_get_temp_dir(), 'akeneo-');
Assert::string($tempName);
$tempName = $this->generateTempFilePath();
file_put_contents($tempName, $bodyContents);

return new File($tempName);
}

private function generateTempFilePath(): string
{
return $this->temporaryFilesManager->generateTemporaryFilePath();
}
}
4 changes: 4 additions & 0 deletions tests/Application/config/packages/test/messenger.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
framework:
messenger:
transports:
main: 'in-memory://'
30 changes: 0 additions & 30 deletions tests/Behat/Context/Cli/ConsumeCommandContext.php
Original file line number Diff line number Diff line change
@@ -1,30 +0,0 @@
<?php

declare(strict_types=1);

namespace Tests\Webgriffe\SyliusAkeneoPlugin\Behat\Context\Cli;

use Behat\Behat\Context\Context;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Tester\ApplicationTester;
use Symfony\Component\HttpKernel\KernelInterface;
use Webgriffe\SyliusAkeneoPlugin\Command\ConsumeCommand;

final class ConsumeCommandContext implements Context
{
public function __construct(private KernelInterface $kernel, private ConsumeCommand $consumeCommand)
{
}

/**
* @When /^I import all items in queue$/
*/
public function iImportAllItemsInQueue(): void
{
$application = new Application($this->kernel);
$application->setAutoExit(false);
$application->add($this->consumeCommand);
$applicationTester = new ApplicationTester($application);
$applicationTester->run(['command' => 'webgriffe:akeneo:consume']);
}
}
Loading

0 comments on commit 38efc40

Please sign in to comment.