From 51ec4be176e97da17796b3e31e09d454cd8109d5 Mon Sep 17 00:00:00 2001 From: Lorenzo Ruozzi Date: Mon, 22 Jul 2024 12:28:45 +0200 Subject: [PATCH] Import attribute and option translations from Akeneo (#192) --- config/services/attribute.php | 23 +++ docs/architecture_and_customization.md | 17 +- docs/upgrade/upgrade-2.*.md | 12 +- docs/usage.md | 4 +- src/Attribute/Importer.php | 154 ++++++++++++++++ src/AttributeOptions/Importer.php | 104 +++-------- src/Product/ProductOptionsResolver.php | 5 + src/ProductAttributeHelperTrait.php | 2 + src/ProductOptionHelperTrait.php | 44 +++++ src/SyliusProductAttributeHelperTrait.php | 81 +++++++++ ...te_translations_from_akeneo_to_sylius.yaml | 11 ++ ...n_translations_from_akeneo_to_sylius.yaml} | 4 +- ..._attributes_or_sylius_product_options.yaml | 25 +++ ...te_translations_from_akeneo_to_sylius.yaml | 24 +++ ...n_translations_from_akeneo_to_sylius.yaml} | 20 +- ..._options_from_akeneo_to_sylius_option.yaml | 13 -- ..._options_from_akeneo_to_sylius_option.yaml | 13 -- tests/Integration/Attribute/ImporterTest.php | 172 ++++++++++++++++++ .../AttributeOptions/ImporterTest.php | 34 ---- 19 files changed, 603 insertions(+), 159 deletions(-) create mode 100644 config/services/attribute.php create mode 100644 src/Attribute/Importer.php create mode 100644 src/SyliusProductAttributeHelperTrait.php create mode 100644 tests/DataFixtures/ORM/resources/Importer/Attribute/it_imports_attribute_translations_from_akeneo_to_sylius.yaml rename tests/DataFixtures/ORM/resources/Importer/{AttributeOptions/it_imports_metric_attribute_labels_from_akeneo_to_sylius_option_translation.yaml => Attribute/it_imports_option_translations_from_akeneo_to_sylius.yaml} (81%) create mode 100644 tests/DataFixtures/ORM/resources/Importer/Attribute/it_returns_all_option_and_attributes_identifiers_that_are_also_sylius_attributes_or_sylius_product_options.yaml create mode 100644 tests/DataFixtures/ORM/resources/Importer/Attribute/it_updates_attribute_translations_from_akeneo_to_sylius.yaml rename tests/DataFixtures/ORM/resources/Importer/{AttributeOptions/it_updates_metric_attribute_labels_from_akeneo_to_sylius_option_translation.yaml => Attribute/it_updates_option_translations_from_akeneo_to_sylius.yaml} (53%) create mode 100644 tests/Integration/Attribute/ImporterTest.php diff --git a/config/services/attribute.php b/config/services/attribute.php new file mode 100644 index 00000000..f639d248 --- /dev/null +++ b/config/services/attribute.php @@ -0,0 +1,23 @@ +services(); + + $services->set('webgriffe_sylius_akeneo.attribute.importer', Importer::class) + ->args([ + service('event_dispatcher'), + service('webgriffe_sylius_akeneo.api_client'), + service('sylius.repository.product_option'), + service('sylius.factory.product_option_translation'), + service('sylius.repository.product_attribute'), + service('sylius.factory.product_attribute_translation'), + ]) + ->tag('webgriffe_sylius_akeneo.importer') + ; +}; diff --git a/docs/architecture_and_customization.md b/docs/architecture_and_customization.md index 49673f8b..f258d2ff 100644 --- a/docs/architecture_and_customization.md +++ b/docs/architecture_and_customization.md @@ -180,12 +180,21 @@ importer** (`Webgriffe\SyliusAkeneoPlugin\ProductAssociations\Importer`). This i associations to the corresponding Sylius products associations. The association types must already exist on Sylius with the same code they have on Akeneo. +### Attribute importer + +Another provided importer is the **attribute importer** (`\Webgriffe\SyliusAkeneoPlugin\Attribute\Importer`). This +importer imports the Akeneo attribute translations into Sylius attribute and option translations. +The attributes and options must already exist on Sylius with the same code they have on Akeneo to be imported. + ### Attribute options importer -Another provided importer is the **attribute options -importer** (`\Webgriffe\SyliusAkeneoPlugin\AttributeOptions\Importer`). This importer imports the Akeneo simple select -and multi select attributes options into Sylius select attributes. The select attributes must already exist on Sylius -with the same code they have on Akeneo. +Another provided importer is the **attribute options importer** +(`\Webgriffe\SyliusAkeneoPlugin\AttributeOptions\Importer`). This importer imports the Akeneo simple select +and multi select attributes options into Sylius select attributes. It imports also all attribute options that are used +on Sylius as product options. If the attribute has metrical type the values will not be imported because they could be +any value, it will be created by the ProductOptionValueResolver during product variant import. +The select attributes and the product options must already exist on Sylius +with the same code they have on Akeneo to be imported. ## Customize which Akeneo products to import diff --git a/docs/upgrade/upgrade-2.*.md b/docs/upgrade/upgrade-2.*.md index 216a96a0..094aa53a 100644 --- a/docs/upgrade/upgrade-2.*.md +++ b/docs/upgrade/upgrade-2.*.md @@ -5,6 +5,16 @@ nav_order: 0 parent: Upgrade --- +# Upgrade from `v2.7.0` to `v2.8.0` + +The v2.8.0 version introduces the Attribute importer. +If you want to import attribute and options translations from Akeneo you have to add the `--importer="Attribute"` option to the command that imports once a hour: + +```diff +- 0 * * * * /path/to/sylius/bin/console -e prod -q webgriffe:akeneo:import --all --importer="AttributeOptions" ++ 0 * * * * /path/to/sylius/bin/console -e prod -q webgriffe:akeneo:import --all --importer="Attribute" --importer="AttributeOptions" +``` + # Upgrade from `v2.4.0` to `v2.5.0` The v2.5.0 version now allow you to choose which product and product model to import through webhook entry point. @@ -15,7 +25,7 @@ Take a look at the [customization documentation](../architecture_and_customizati The v2.4.0 version introduces the Product Model importer. If you are using the webhook no changes are requested as it will be automatically enqueued on every update. If you are using the cronjob, you have to add the `--importer="ProductModel"` option to the command that imports every minute: -```git +```diff - * * * * * /path/to/sylius/bin/console -e prod -q webgriffe:akeneo:import --since-file=/path/to/sylius/var/storage/akeneo-import-sincefile.txt --importer="Product" --importer="ProductAssociations" + * * * * * /path/to/sylius/bin/console -e prod -q webgriffe:akeneo:import --since-file=/path/to/sylius/var/storage/akeneo-import-sincefile.txt --importer="Product" --importer="ProductModel" --importer="ProductAssociations" ``` diff --git a/docs/usage.md b/docs/usage.md index 07d5d2a7..27de6a89 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -459,7 +459,7 @@ It could be useful to add also this command to your scheduler to run automatical To make all importers and other plugin features work automatically the following is the suggested crontab: ``` -0 * * * * /path/to/sylius/bin/console -e prod -q webgriffe:akeneo:import --all --importer="AttributeOptions" +0 * * * * /path/to/sylius/bin/console -e prod -q webgriffe:akeneo:import --all --importer="Attribute" --importer="AttributeOptions" * * * * * /path/to/sylius/bin/console -e prod -q webgriffe:akeneo:import --since-file=/path/to/sylius/var/storage/akeneo-import-sincefile.txt --importer="Product" --importer="ProductModel" --importer="ProductAssociations" 0 */6 * * * /path/to/sylius/bin/console -e prod -q webgriffe:akeneo:reconcile 0 0 * * * /path/to/sylius/bin/console -e prod -q webgriffe:akeneo:cleanup-item-import-results @@ -467,7 +467,7 @@ To make all importers and other plugin features work automatically the following This will: -* Import the update of all attribute options every hour +* Import the attribute/option translations and import all attribute/option values every hour * Import, every minute, all products and product models that have been modified since the last execution, along with their associations * Reconcile Akeneo deleted products every 6 hours diff --git a/src/Attribute/Importer.php b/src/Attribute/Importer.php new file mode 100644 index 00000000..a9c4ece2 --- /dev/null +++ b/src/Attribute/Importer.php @@ -0,0 +1,154 @@ +} + */ +final class Importer implements ImporterInterface +{ + public const SIMPLESELECT_TYPE = 'pim_catalog_simpleselect'; + + public const MULTISELECT_TYPE = 'pim_catalog_multiselect'; + + public const BOOLEAN_TYPE = 'pim_catalog_boolean'; + + public const METRIC_TYPE = 'pim_catalog_metric'; + + public const AKENEO_ENTITY = 'Attribute'; + + use ProductOptionHelperTrait, SyliusProductAttributeHelperTrait; + + /** + * @param FactoryInterface $productOptionTranslationFactory + * @param RepositoryInterface $productAttributeRepository + * @param FactoryInterface $productAttributeTranslationFactory + */ + public function __construct( + private EventDispatcherInterface $eventDispatcher, + private AkeneoPimClientInterface $apiClient, + private ProductOptionRepositoryInterface $productOptionRepository, + private FactoryInterface $productOptionTranslationFactory, + private RepositoryInterface $productAttributeRepository, + private FactoryInterface $productAttributeTranslationFactory, + ) { + } + + public function getAkeneoEntity(): string + { + return self::AKENEO_ENTITY; + } + + public function getIdentifiersModifiedSince(DateTime $sinceDate): array + { + $searchBuilder = new SearchBuilder(); + $this->eventDispatcher->dispatch( + new IdentifiersModifiedSinceSearchBuilderBuiltEvent($this, $searchBuilder, $sinceDate), + ); + /** + * @psalm-suppress TooManyTemplateParams + * + * @var ResourceCursorInterface $akeneoAttributes + */ + $akeneoAttributes = $this->apiClient->getAttributeApi()->all(50, ['search' => $searchBuilder->getFilters()]); + + return array_merge( + $this->filterBySyliusAttributeCodes($akeneoAttributes), + $this->filterSyliusOptionCodes($akeneoAttributes), + ); + } + + public function import(string $identifier): void + { + /** @var AkeneoAttribute $akeneoAttribute */ + $akeneoAttribute = $this->apiClient->getAttributeApi()->get($identifier); + + $syliusProductAttribute = $this->productAttributeRepository->findOneBy(['code' => $identifier]); + if ($syliusProductAttribute instanceof ProductAttributeInterface) { + $this->importAttributeData($akeneoAttribute, $syliusProductAttribute); + } + + $syliusProductOption = $this->productOptionRepository->findOneBy(['code' => $identifier]); + if ($syliusProductOption instanceof ProductOptionInterface) { + $this->importOptionData($akeneoAttribute, $syliusProductOption); + } + } + + /** + * @return FactoryInterface + */ + private function getProductOptionTranslationFactory(): FactoryInterface + { + return $this->productOptionTranslationFactory; + } + + private function getProductOptionRepository(): ProductOptionRepositoryInterface + { + return $this->productOptionRepository; + } + + /** + * @return RepositoryInterface + */ + private function getProductAttributeRepository(): RepositoryInterface + { + return $this->productAttributeRepository; + } + + /** + * @param AkeneoAttribute $akeneoAttribute + */ + private function importAttributeData(array $akeneoAttribute, ProductAttributeInterface $syliusProductAttribute): void + { + $this->importProductAttributeTranslations($akeneoAttribute, $syliusProductAttribute); + $this->productAttributeRepository->add($syliusProductAttribute); + } + + /** + * @param AkeneoAttribute $akeneoAttribute + */ + private function importOptionData(array $akeneoAttribute, ProductOptionInterface $syliusProductOption): void + { + $this->importProductOptionTranslations($akeneoAttribute, $syliusProductOption); + $this->productOptionRepository->add($syliusProductOption); + // TODO: Update also the position of the option? The problem is that this position is on family variant entity! + } + + /** + * @param AkeneoAttribute $akeneoAttribute + */ + private function importProductAttributeTranslations(array $akeneoAttribute, ProductAttributeInterface $syliusProductAttribute): void + { + foreach ($akeneoAttribute['labels'] as $locale => $label) { + $productAttributeTranslation = $syliusProductAttribute->getTranslation($locale); + if ($productAttributeTranslation->getLocale() === $locale) { + $productAttributeTranslation->setName($label); + + continue; + } + $newProductAttributeTranslation = $this->productAttributeTranslationFactory->createNew(); + $newProductAttributeTranslation->setLocale($locale); + $newProductAttributeTranslation->setName($label); + $syliusProductAttribute->addTranslation($newProductAttributeTranslation); + } + } +} diff --git a/src/AttributeOptions/Importer.php b/src/AttributeOptions/Importer.php index a1e5a3e3..d16f1b8a 100644 --- a/src/AttributeOptions/Importer.php +++ b/src/AttributeOptions/Importer.php @@ -20,11 +20,13 @@ use Sylius\Component\Resource\Translation\Provider\TranslationLocaleProviderInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Contracts\Translation\TranslatorInterface; +use Webgriffe\SyliusAkeneoPlugin\Attribute\Importer as AttributeImporter; use Webgriffe\SyliusAkeneoPlugin\Event\IdentifiersModifiedSinceSearchBuilderBuiltEvent; use Webgriffe\SyliusAkeneoPlugin\ImporterInterface; use Webgriffe\SyliusAkeneoPlugin\ProductAttributeHelperTrait; use Webgriffe\SyliusAkeneoPlugin\ProductOptionHelperTrait; use Webgriffe\SyliusAkeneoPlugin\ProductOptionValueHelperTrait; +use Webgriffe\SyliusAkeneoPlugin\SyliusProductAttributeHelperTrait; use Webmozart\Assert\Assert; /** @@ -33,15 +35,10 @@ */ final class Importer implements ImporterInterface { - use ProductOptionHelperTrait, ProductOptionValueHelperTrait, ProductAttributeHelperTrait; - - private const SIMPLESELECT_TYPE = 'pim_catalog_simpleselect'; - - private const MULTISELECT_TYPE = 'pim_catalog_multiselect'; - - private const BOOLEAN_TYPE = 'pim_catalog_boolean'; - - private const METRIC_TYPE = 'pim_catalog_metric'; + use ProductOptionHelperTrait, + ProductOptionValueHelperTrait, + ProductAttributeHelperTrait, + SyliusProductAttributeHelperTrait; /** * @param RepositoryInterface $attributeRepository @@ -120,8 +117,6 @@ public function import(string $identifier): void /** @var AkeneoAttribute $attributeResponse */ $attributeResponse = $this->apiClient->getAttributeApi()->get($identifier); - // TODO: Update also the position of the option? The problem is that this position is on family variant entity! - $this->importProductOptionTranslations($attributeResponse, $option); $this->importOptionValues($attributeResponse, $option); } @@ -146,89 +141,25 @@ public function getIdentifiersModifiedSince(DateTime $sinceDate): array $akeneoAttributes = $this->apiClient->getAttributeApi()->all(50, ['search' => $searchBuilder->getFilters()]); return array_merge( - $this->filterBySyliusAttributeCodes($akeneoAttributes), + $this->filterBySyliusSelectAttributeCodes($akeneoAttributes), $this->filterSyliusOptionCodes($akeneoAttributes), ); } - /** - * Return the list of Akeneo attribute codes whose code is used as a code for a Sylius attribute - * - * @psalm-suppress TooManyTemplateParams - * - * @param ResourceCursorInterface $akeneoAttributes - * - * @return string[] - */ - private function filterBySyliusAttributeCodes(ResourceCursorInterface $akeneoAttributes): array - { - $syliusSelectAttributes = $this->attributeRepository->findBy(['type' => SelectAttributeType::TYPE]); - $syliusSelectAttributes = array_filter( - array_map( - static fn (ProductAttributeInterface $attribute): ?string => $attribute->getCode(), - $syliusSelectAttributes, - ), - ); - $attributeCodes = []; - /** @var AkeneoAttribute $akeneoAttribute */ - foreach ($akeneoAttributes as $akeneoAttribute) { - if (!in_array($akeneoAttribute['code'], $syliusSelectAttributes, true)) { - continue; - } - if ($akeneoAttribute['type'] !== self::SIMPLESELECT_TYPE && $akeneoAttribute['type'] !== self::MULTISELECT_TYPE) { - continue; - } - $attributeCodes[] = $akeneoAttribute['code']; - } - - return $attributeCodes; - } - - /** - * Return the list of Akeneo attribute codes whose code is used as a code for a Sylius attribute - * - * @psalm-suppress TooManyTemplateParams - * - * @param ResourceCursorInterface $akeneoAttributes - * - * @return string[] - */ - private function filterSyliusOptionCodes(ResourceCursorInterface $akeneoAttributes): array - { - $productOptionRepository = $this->optionRepository; - if (!$productOptionRepository instanceof ProductOptionRepositoryInterface) { - return []; - } - $akeneoAttributeCodes = []; - /** @var AkeneoAttribute $akeneoAttribute */ - foreach ($akeneoAttributes as $akeneoAttribute) { - if (!in_array($akeneoAttribute['type'], [self::SIMPLESELECT_TYPE, self::MULTISELECT_TYPE, self::BOOLEAN_TYPE, self::METRIC_TYPE], true)) { - continue; - } - $akeneoAttributeCodes[] = $akeneoAttribute['code']; - } - $syliusOptions = $productOptionRepository->findByCodes($akeneoAttributeCodes); - - return array_map( - static fn (ProductOptionInterface $option): string => (string) $option->getCode(), - $syliusOptions, - ); - } - /** * @param AkeneoAttribute $akeneoAttribute */ private function importOptionValues(array $akeneoAttribute, ProductOptionInterface $option): void { - if ($akeneoAttribute['type'] !== self::SIMPLESELECT_TYPE && - $akeneoAttribute['type'] !== self::MULTISELECT_TYPE && - $akeneoAttribute['type'] !== self::BOOLEAN_TYPE + if ($akeneoAttribute['type'] !== AttributeImporter::SIMPLESELECT_TYPE && + $akeneoAttribute['type'] !== AttributeImporter::MULTISELECT_TYPE && + $akeneoAttribute['type'] !== AttributeImporter::BOOLEAN_TYPE ) { return; } $attributeCode = $akeneoAttribute['code']; - if ($akeneoAttribute['type'] === self::BOOLEAN_TYPE) { + if ($akeneoAttribute['type'] === AttributeImporter::BOOLEAN_TYPE) { foreach ([true, false] as $booleanValue) { $optionValueCode = $this->getSyliusProductOptionValueCode($attributeCode, (string) $booleanValue); $productOptionValue = $this->getProductOptionValueFromOption($option, $optionValueCode); @@ -338,4 +269,17 @@ private function getAkeneoPimClient(): AkeneoPimClientInterface { return $this->apiClient; } + + private function getProductOptionRepository(): ?ProductOptionRepositoryInterface + { + return $this->optionRepository; + } + + /** + * @return RepositoryInterface + */ + private function getProductAttributeRepository(): RepositoryInterface + { + return $this->attributeRepository; + } } diff --git a/src/Product/ProductOptionsResolver.php b/src/Product/ProductOptionsResolver.php index 7bc8860b..e8520288 100644 --- a/src/Product/ProductOptionsResolver.php +++ b/src/Product/ProductOptionsResolver.php @@ -122,4 +122,9 @@ private function getProductOptionTranslationFactory(): FactoryInterface { return $this->productOptionTranslationFactory; } + + private function getProductOptionRepository(): ProductOptionRepositoryInterface + { + return $this->productOptionRepository; + } } diff --git a/src/ProductAttributeHelperTrait.php b/src/ProductAttributeHelperTrait.php index c6127855..8bd8bde9 100644 --- a/src/ProductAttributeHelperTrait.php +++ b/src/ProductAttributeHelperTrait.php @@ -9,6 +9,8 @@ use Sylius\Component\Product\Model\ProductAttributeInterface; /** + * @TODO Rename to something more Akeneo-specific, like AkeneoAttributeHelperTrait + * * @psalm-type AkeneoAttributeOption array{_links: array, code: string, attribute: string, sort_order: int, labels: array} */ trait ProductAttributeHelperTrait diff --git a/src/ProductOptionHelperTrait.php b/src/ProductOptionHelperTrait.php index fce54f2e..e3bcf2ab 100644 --- a/src/ProductOptionHelperTrait.php +++ b/src/ProductOptionHelperTrait.php @@ -4,9 +4,12 @@ namespace Webgriffe\SyliusAkeneoPlugin; +use Akeneo\Pim\ApiClient\Pagination\ResourceCursorInterface; use Sylius\Component\Product\Model\ProductOptionInterface; use Sylius\Component\Product\Model\ProductOptionTranslationInterface; +use Sylius\Component\Product\Repository\ProductOptionRepositoryInterface; use Sylius\Component\Resource\Factory\FactoryInterface; +use Webgriffe\SyliusAkeneoPlugin\Attribute\Importer as AttributeImporter; /** * @psalm-type AkeneoAttribute array{code: string, type: string, labels: array} @@ -19,6 +22,47 @@ trait ProductOptionHelperTrait */ abstract private function getProductOptionTranslationFactory(): FactoryInterface; + abstract private function getProductOptionRepository(): ?ProductOptionRepositoryInterface; + + /** + * Return the list of Akeneo attribute codes whose code is used as a code for a Sylius attribute + * + * @psalm-suppress TooManyTemplateParams + * @psalm-suppress UnnecessaryVarAnnotation + * + * @param ResourceCursorInterface $akeneoAttributes + * + * @return string[] + */ + private function filterSyliusOptionCodes(ResourceCursorInterface $akeneoAttributes): array + { + /** @var ?ProductOptionRepositoryInterface $productOptionRepository */ + $productOptionRepository = $this->getProductOptionRepository(); + if (!$productOptionRepository instanceof ProductOptionRepositoryInterface) { + return []; + } + $akeneoAttributeCodes = []; + /** @var AkeneoAttribute $akeneoAttribute */ + foreach ($akeneoAttributes as $akeneoAttribute) { + if (!in_array($akeneoAttribute['type'], [ + AttributeImporter::SIMPLESELECT_TYPE, + AttributeImporter::MULTISELECT_TYPE, + AttributeImporter::BOOLEAN_TYPE, + AttributeImporter::METRIC_TYPE, + ], true) + ) { + continue; + } + $akeneoAttributeCodes[] = $akeneoAttribute['code']; + } + $syliusOptions = $productOptionRepository->findByCodes($akeneoAttributeCodes); + + return array_map( + static fn (ProductOptionInterface $option): string => (string) $option->getCode(), + $syliusOptions, + ); + } + /** * @param AkeneoAttribute $akeneoAttribute */ diff --git a/src/SyliusProductAttributeHelperTrait.php b/src/SyliusProductAttributeHelperTrait.php new file mode 100644 index 00000000..375c35d2 --- /dev/null +++ b/src/SyliusProductAttributeHelperTrait.php @@ -0,0 +1,81 @@ +} + */ +trait SyliusProductAttributeHelperTrait +{ + /** + * @return RepositoryInterface + */ + abstract private function getProductAttributeRepository(): RepositoryInterface; + + /** + * Return the list of Akeneo attribute codes whose code is used as a code for a Sylius SELECT attribute + * + * @psalm-suppress TooManyTemplateParams + * + * @param ResourceCursorInterface $akeneoAttributes + * + * @return string[] + */ + private function filterBySyliusSelectAttributeCodes(ResourceCursorInterface $akeneoAttributes): array + { + $syliusSelectAttributes = $this->getProductAttributeRepository()->findBy(['type' => SelectAttributeType::TYPE]); + + return $this->filterBySyliusAttributes($syliusSelectAttributes, $akeneoAttributes); + } + + /** + * Return the list of Akeneo attribute codes whose code is used as a code for a Sylius attribute + * + * @psalm-suppress TooManyTemplateParams + * + * @param ResourceCursorInterface $akeneoAttributes + * + * @return string[] + */ + private function filterBySyliusAttributeCodes(ResourceCursorInterface $akeneoAttributes): array + { + $syliusAttributes = $this->getProductAttributeRepository()->findAll(); + + return $this->filterBySyliusAttributes($syliusAttributes, $akeneoAttributes); + } + + /** + * @psalm-suppress TooManyTemplateParams + * + * @param ProductAttributeInterface[] $syliusAttributes + * @param ResourceCursorInterface $akeneoAttributes + * + * @return string[] + */ + private function filterBySyliusAttributes(array $syliusAttributes, ResourceCursorInterface $akeneoAttributes): array + { + $syliusAttributes = array_filter( + array_map( + static fn (ProductAttributeInterface $attribute): ?string => $attribute->getCode(), + $syliusAttributes, + ), + ); + $attributeCodes = []; + /** @var AkeneoAttribute $akeneoAttribute */ + foreach ($akeneoAttributes as $akeneoAttribute) { + if (!in_array($akeneoAttribute['code'], $syliusAttributes, true)) { + continue; + } + $attributeCodes[] = $akeneoAttribute['code']; + } + + return $attributeCodes; + } +} diff --git a/tests/DataFixtures/ORM/resources/Importer/Attribute/it_imports_attribute_translations_from_akeneo_to_sylius.yaml b/tests/DataFixtures/ORM/resources/Importer/Attribute/it_imports_attribute_translations_from_akeneo_to_sylius.yaml new file mode 100644 index 00000000..f3a712cc --- /dev/null +++ b/tests/DataFixtures/ORM/resources/Importer/Attribute/it_imports_attribute_translations_from_akeneo_to_sylius.yaml @@ -0,0 +1,11 @@ +Sylius\Component\Locale\Model\Locale: + en_US: + code: "en_US" + it_IT: + code: "it_IT" + +Sylius\Component\Product\Model\ProductAttribute: + attribute: + code: 'size' + type: 'select' + storageType: 'json' diff --git a/tests/DataFixtures/ORM/resources/Importer/AttributeOptions/it_imports_metric_attribute_labels_from_akeneo_to_sylius_option_translation.yaml b/tests/DataFixtures/ORM/resources/Importer/Attribute/it_imports_option_translations_from_akeneo_to_sylius.yaml similarity index 81% rename from tests/DataFixtures/ORM/resources/Importer/AttributeOptions/it_imports_metric_attribute_labels_from_akeneo_to_sylius_option_translation.yaml rename to tests/DataFixtures/ORM/resources/Importer/Attribute/it_imports_option_translations_from_akeneo_to_sylius.yaml index b1f40b65..c3c3b495 100644 --- a/tests/DataFixtures/ORM/resources/Importer/AttributeOptions/it_imports_metric_attribute_labels_from_akeneo_to_sylius_option_translation.yaml +++ b/tests/DataFixtures/ORM/resources/Importer/Attribute/it_imports_option_translations_from_akeneo_to_sylius.yaml @@ -5,5 +5,5 @@ Sylius\Component\Locale\Model\Locale: code: "it_IT" Sylius\Component\Product\Model\ProductOption: - length: - code: 'length' + size: + code: 'size' diff --git a/tests/DataFixtures/ORM/resources/Importer/Attribute/it_returns_all_option_and_attributes_identifiers_that_are_also_sylius_attributes_or_sylius_product_options.yaml b/tests/DataFixtures/ORM/resources/Importer/Attribute/it_returns_all_option_and_attributes_identifiers_that_are_also_sylius_attributes_or_sylius_product_options.yaml new file mode 100644 index 00000000..cbf8981c --- /dev/null +++ b/tests/DataFixtures/ORM/resources/Importer/Attribute/it_returns_all_option_and_attributes_identifiers_that_are_also_sylius_attributes_or_sylius_product_options.yaml @@ -0,0 +1,25 @@ +Sylius\Component\Locale\Model\Locale: + en_US: + code: 'en_US' + it_IT: + code: 'it_IT' + +Sylius\Component\Product\Model\ProductAttribute: + material: + code: 'material' + type: 'select' + storageType: 'json' + other_attribute: + code: 'other_attribute' + type: 'text' + storageType: 'text' + +Sylius\Component\Product\Model\ProductOption: + size: + code: 'size' + length: + code: 'length' + sellable: + code: 'sellable' + product_option_of_wrong_type: + code: 'product_option_of_wrong_type' diff --git a/tests/DataFixtures/ORM/resources/Importer/Attribute/it_updates_attribute_translations_from_akeneo_to_sylius.yaml b/tests/DataFixtures/ORM/resources/Importer/Attribute/it_updates_attribute_translations_from_akeneo_to_sylius.yaml new file mode 100644 index 00000000..0df5f6c1 --- /dev/null +++ b/tests/DataFixtures/ORM/resources/Importer/Attribute/it_updates_attribute_translations_from_akeneo_to_sylius.yaml @@ -0,0 +1,24 @@ +Sylius\Component\Locale\Model\Locale: + en_US: + code: "en_US" + it_IT: + code: "it_IT" + +Sylius\Component\Product\Model\ProductAttribute: + size: + code: 'size' + type: 'select' + storageType: 'json' + translations: + - '@size_en_US' + - '@size_it_IT' + +Sylius\Component\Product\Model\ProductAttributeTranslation: + size_en_US: + name: 'Format' + locale: 'en_US' + translatable: '@size' + size_it_IT: + name: 'Formato' + locale: 'it_IT' + translatable: '@size' diff --git a/tests/DataFixtures/ORM/resources/Importer/AttributeOptions/it_updates_metric_attribute_labels_from_akeneo_to_sylius_option_translation.yaml b/tests/DataFixtures/ORM/resources/Importer/Attribute/it_updates_option_translations_from_akeneo_to_sylius.yaml similarity index 53% rename from tests/DataFixtures/ORM/resources/Importer/AttributeOptions/it_updates_metric_attribute_labels_from_akeneo_to_sylius_option_translation.yaml rename to tests/DataFixtures/ORM/resources/Importer/Attribute/it_updates_option_translations_from_akeneo_to_sylius.yaml index b9720d91..4aa07966 100644 --- a/tests/DataFixtures/ORM/resources/Importer/AttributeOptions/it_updates_metric_attribute_labels_from_akeneo_to_sylius_option_translation.yaml +++ b/tests/DataFixtures/ORM/resources/Importer/Attribute/it_updates_option_translations_from_akeneo_to_sylius.yaml @@ -5,18 +5,18 @@ Sylius\Component\Locale\Model\Locale: code: "it_IT" Sylius\Component\Product\Model\ProductOption: - length: - code: 'length' + size: + code: 'size' translations: - - '@length_en_US' - - '@length_it_IT' + - '@size_en_US' + - '@size_it_IT' Sylius\Component\Product\Model\ProductOptionTranslation: - length_en_US: - name: 'Large' + size_en_US: + name: 'Format' locale: 'en_US' - translatable: '@length' - length_it_IT: - name: 'Larghezza' + translatable: '@size' + size_it_IT: + name: 'Formato' locale: 'it_IT' - translatable: '@length' + translatable: '@size' diff --git a/tests/DataFixtures/ORM/resources/Importer/AttributeOptions/it_updates_all_select_attribute_options_from_akeneo_to_sylius_option.yaml b/tests/DataFixtures/ORM/resources/Importer/AttributeOptions/it_updates_all_select_attribute_options_from_akeneo_to_sylius_option.yaml index 7133fb57..17b5573d 100644 --- a/tests/DataFixtures/ORM/resources/Importer/AttributeOptions/it_updates_all_select_attribute_options_from_akeneo_to_sylius_option.yaml +++ b/tests/DataFixtures/ORM/resources/Importer/AttributeOptions/it_updates_all_select_attribute_options_from_akeneo_to_sylius_option.yaml @@ -7,19 +7,6 @@ Sylius\Component\Locale\Model\Locale: Sylius\Component\Product\Model\ProductOption: size: code: 'size' - translations: - - '@size_en_US' - - '@size_it_IT' - -Sylius\Component\Product\Model\ProductOptionTranslation: - size_en_US: - name: 'Format' - locale: 'en_US' - translatable: '@size' - size_it_IT: - name: 'Formato' - locale: 'it_IT' - translatable: '@size' Sylius\Component\Product\Model\ProductOptionValue: small: diff --git a/tests/DataFixtures/ORM/resources/Importer/AttributeOptions/it_updates_boolean_attribute_options_from_akeneo_to_sylius_option.yaml b/tests/DataFixtures/ORM/resources/Importer/AttributeOptions/it_updates_boolean_attribute_options_from_akeneo_to_sylius_option.yaml index 48cbc2e1..33e3ef22 100644 --- a/tests/DataFixtures/ORM/resources/Importer/AttributeOptions/it_updates_boolean_attribute_options_from_akeneo_to_sylius_option.yaml +++ b/tests/DataFixtures/ORM/resources/Importer/AttributeOptions/it_updates_boolean_attribute_options_from_akeneo_to_sylius_option.yaml @@ -7,19 +7,6 @@ Sylius\Component\Locale\Model\Locale: Sylius\Component\Product\Model\ProductOption: sellable: code: 'sellable' - translations: - - '@sellable_it_IT' - - '@sellable_en_US' - -Sylius\Component\Product\Model\ProductOptionTranslation: - sellable_en_US: - name: 'Enabled' - locale: 'en_US' - translatable: '@sellable' - sellable_it_IT: - name: 'Abilitato' - locale: 'it_IT' - translatable: '@sellable' Sylius\Component\Product\Model\ProductOptionValue: sellable_yes: diff --git a/tests/Integration/Attribute/ImporterTest.php b/tests/Integration/Attribute/ImporterTest.php new file mode 100644 index 00000000..42959e40 --- /dev/null +++ b/tests/Integration/Attribute/ImporterTest.php @@ -0,0 +1,172 @@ +importer = self::getContainer()->get('webgriffe_sylius_akeneo.attribute.importer'); + $fixtureLoader = self::getContainer()->get('fidry_alice_data_fixtures.loader.doctrine'); + $this->attributeRepository = self::getContainer()->get('sylius.repository.product_attribute'); + $this->optionRepository = self::getContainer()->get('sylius.repository.product_option'); + + /** + * @TODO: Move this methods to a generic class on some events on PHPUnit? + */ + InMemoryAttributeApi::clear(); + + InMemoryAttributeApi::addResource(new Attribute('material', AttributeType::SIMPLE_SELECT)); + InMemoryAttributeApi::addResource(new Attribute('text_attribute', AttributeType::TEXT)); + InMemoryAttributeApi::addResource(Attribute::create('size', [ + 'type' => AttributeType::SIMPLE_SELECT, + 'labels' => [ + 'en_US' => 'Size', + 'it_IT' => 'Taglia', + ], + ])); + InMemoryAttributeApi::addResource(Attribute::create('length', [ + 'type' => AttributeType::METRIC, + 'metric_family' => 'Length', + 'default_metric_unit' => 'CENTIMETER', + 'labels' => [ + 'en_US' => 'Length', + 'it_IT' => 'Lunghezza', + ], + ])); + InMemoryAttributeApi::addResource(Attribute::create('sellable', [ + 'type' => AttributeType::BOOLEAN, + 'labels' => [ + 'en_US' => 'Sellable', + 'it_IT' => 'Vendibile', + ], + ])); + + $ORMResourceFixturePath = DataFixture::path . '/ORM/resources/Importer/Attribute/' . $this->name() . '.yaml'; + if (file_exists($ORMResourceFixturePath)) { + $fixtureLoader->load( + [$ORMResourceFixturePath], + [], + [], + PurgeMode::createDeleteMode(), + ); + } else { + $fixtureLoader->load([], [], [], PurgeMode::createDeleteMode()); + } + } + + /** + * @test + */ + public function it_throws_if_attribute_does_not_exists_on_akeneo(): void + { + $this->expectException(NotFoundHttpException::class); + $this->importer->import('not_existent_attribute'); + } + + /** + * @test + */ + public function it_does_nothing_if_not_exists_sylius_attribute_or_option_with_that_code(): void + { + $this->importer->import('text_attribute'); + + $attribute = $this->attributeRepository->findOneBy(['code' => 'text_attribute']); + $this->assertNull($attribute); + + $option = $this->optionRepository->findOneBy(['code' => 'text_attribute']); + $this->assertNull($option); + } + + /** + * @test + */ + public function it_imports_attribute_translations_from_akeneo_to_sylius(): void + { + $this->importer->import('size'); + + $attribute = $this->attributeRepository->findOneBy(['code' => 'size']); + $this->assertInstanceOf(ProductAttributeInterface::class, $attribute); + $this->assertEquals('Size', $attribute->getTranslation('en_US')->getName()); + $this->assertEquals('Taglia', $attribute->getTranslation('it_IT')->getName()); + } + + /** + * @test + */ + public function it_updates_attribute_translations_from_akeneo_to_sylius(): void + { + $this->importer->import('size'); + + $attribute = $this->attributeRepository->findOneBy(['code' => 'size']); + $this->assertInstanceOf(ProductAttributeInterface::class, $attribute); + $this->assertEquals('Size', $attribute->getTranslation('en_US')->getName()); + $this->assertEquals('Taglia', $attribute->getTranslation('it_IT')->getName()); + } + + /** + * @test + */ + public function it_imports_option_translations_from_akeneo_to_sylius(): void + { + $this->importer->import('size'); + + $option = $this->optionRepository->findOneBy(['code' => 'size']); + $this->assertInstanceOf(ProductOptionInterface::class, $option); + $this->assertEquals('Size', $option->getTranslation('en_US')->getName()); + $this->assertEquals('Taglia', $option->getTranslation('it_IT')->getName()); + } + + /** + * @test + */ + public function it_updates_option_translations_from_akeneo_to_sylius(): void + { + $this->importer->import('size'); + + $option = $this->optionRepository->findOneBy(['code' => 'size']); + $this->assertInstanceOf(ProductOptionInterface::class, $option); + $this->assertEquals('Size', $option->getTranslation('en_US')->getName()); + $this->assertEquals('Taglia', $option->getTranslation('it_IT')->getName()); + } + + /** + * @test + */ + public function it_returns_all_option_and_attributes_identifiers_that_are_also_sylius_attributes_or_sylius_product_options(): void + { + InMemoryAttributeApi::addResource(Attribute::create('product_option_of_wrong_type', [ + 'type' => AttributeType::TEXT, + ])); + InMemoryAttributeApi::addResource(Attribute::create('other_attribute', [ + 'type' => AttributeType::TEXT, + ])); + + $identifiers = $this->importer->getIdentifiersModifiedSince(new DateTime()); + + $this->assertEquals(['material', 'other_attribute', 'size', 'length', 'sellable'], $identifiers); + } +} diff --git a/tests/Integration/AttributeOptions/ImporterTest.php b/tests/Integration/AttributeOptions/ImporterTest.php index 1d485e00..68598388 100644 --- a/tests/Integration/AttributeOptions/ImporterTest.php +++ b/tests/Integration/AttributeOptions/ImporterTest.php @@ -164,8 +164,6 @@ public function it_imports_all_select_attribute_options_from_akeneo_to_sylius_op $option = $this->optionRepository->findOneBy(['code' => 'size']); $this->assertInstanceOf(ProductOptionInterface::class, $option); - $this->assertEquals('Size', $option->getTranslation('en_US')->getName()); - $this->assertEquals('Taglia', $option->getTranslation('it_IT')->getName()); $optionValues = $option->getValues(); $this->assertCount(2, $optionValues); @@ -195,8 +193,6 @@ public function it_updates_all_select_attribute_options_from_akeneo_to_sylius_op $option = $this->optionRepository->findOneBy(['code' => 'size']); $this->assertInstanceOf(ProductOptionInterface::class, $option); - $this->assertEquals('Size', $option->getTranslation('en_US')->getName()); - $this->assertEquals('Taglia', $option->getTranslation('it_IT')->getName()); $optionValues = $option->getValues(); $this->assertCount(2, $optionValues); @@ -217,32 +213,6 @@ public function it_updates_all_select_attribute_options_from_akeneo_to_sylius_op $this->assertEquals('Grande', $smallOptionValue->getTranslation('it_IT')->getValue()); } - /** - * @test - */ - public function it_imports_metric_attribute_labels_from_akeneo_to_sylius_option_translation(): void - { - $this->importer->import('length'); - - $option = $this->optionRepository->findOneBy(['code' => 'length']); - $this->assertInstanceOf(ProductOptionInterface::class, $option); - $this->assertEquals('Length', $option->getTranslation('en_US')->getName()); - $this->assertEquals('Lunghezza', $option->getTranslation('it_IT')->getName()); - } - - /** - * @test - */ - public function it_updates_metric_attribute_labels_from_akeneo_to_sylius_option_translation(): void - { - $this->importer->import('length'); - - $option = $this->optionRepository->findOneBy(['code' => 'length']); - $this->assertInstanceOf(ProductOptionInterface::class, $option); - $this->assertEquals('Length', $option->getTranslation('en_US')->getName()); - $this->assertEquals('Lunghezza', $option->getTranslation('it_IT')->getName()); - } - /** * @test */ @@ -252,8 +222,6 @@ public function it_imports_boolean_attribute_options_from_akeneo_to_sylius_optio $option = $this->optionRepository->findOneBy(['code' => 'sellable']); $this->assertInstanceOf(ProductOptionInterface::class, $option); - $this->assertEquals('Sellable', $option->getTranslation('en_US')->getName()); - $this->assertEquals('Vendibile', $option->getTranslation('it_IT')->getName()); $optionValues = $option->getValues(); $this->assertCount(2, $optionValues); @@ -283,8 +251,6 @@ public function it_updates_boolean_attribute_options_from_akeneo_to_sylius_optio $option = $this->optionRepository->findOneBy(['code' => 'sellable']); $this->assertInstanceOf(ProductOptionInterface::class, $option); - $this->assertEquals('Sellable', $option->getTranslation('en_US')->getName()); - $this->assertEquals('Vendibile', $option->getTranslation('it_IT')->getName()); $optionValues = $option->getValues(); $this->assertCount(2, $optionValues);