diff --git a/spec/ValueHandler/ProductOptionValueHandlerSpec.php b/spec/ValueHandler/ProductOptionValueHandlerSpec.php index 0209d06f..64099f96 100644 --- a/spec/ValueHandler/ProductOptionValueHandlerSpec.php +++ b/spec/ValueHandler/ProductOptionValueHandlerSpec.php @@ -23,7 +23,6 @@ use Sylius\Component\Resource\Repository\RepositoryInterface; use Sylius\Component\Resource\Translation\Provider\TranslationLocaleProviderInterface; use Symfony\Contracts\Translation\TranslatorInterface; -use Webgriffe\SyliusAkeneoPlugin\ApiClientInterface; use Webgriffe\SyliusAkeneoPlugin\ValueHandler\ProductOptionValueHandler; use Webgriffe\SyliusAkeneoPlugin\ValueHandlerInterface; @@ -61,6 +60,7 @@ public function let( ): void { $productVariant->getCode()->willReturn(self::VARIANT_CODE); $productVariant->getProduct()->willReturn($product); + $productVariant->getOptionValues()->willReturn(new ArrayCollection()); $product->getCode()->willReturn(self::PRODUCT_CODE); $product->getOptions()->willReturn(new ArrayCollection([$productOption->getWrappedObject()])); $productOption->getCode()->willReturn(self::OPTION_CODE); diff --git a/src/ValueHandler/ProductOptionValueHandler.php b/src/ValueHandler/ProductOptionValueHandler.php index f645bbd6..a45bd71d 100644 --- a/src/ValueHandler/ProductOptionValueHandler.php +++ b/src/ValueHandler/ProductOptionValueHandler.php @@ -99,6 +99,8 @@ public function handle($productVariant, string $optionCode, array $akeneoValue): /** @var string|array|bool|int $akeneoValueData */ $akeneoValueData = $akeneoValue[0]['data']; + $productVariant->getOptionValues()->clear(); + $productOption = $this->getProductOption($optionCode, $productVariant, $product); /** @var string $attributeType */ diff --git a/tests/InMemory/Client/Api/InMemoryApi.php b/tests/InMemory/Client/Api/InMemoryApi.php new file mode 100644 index 00000000..01bd2357 --- /dev/null +++ b/tests/InMemory/Client/Api/InMemoryApi.php @@ -0,0 +1,146 @@ + + */ + public static array $resources = []; + + /** @return class-string */ + abstract protected function getResourceClass(): string; + + public static function addResource(ResourceInterface $resource) + { + self::$resources[$resource->getIdentifier()] = $resource; + } + + public function create(string $code, array $data = []): int + { + $class = $this->getResourceClass(); + Assert::isInstanceOf($class, ResourceInterface::class); + + self::$resources[] = call_user_func([$class, 'create'], [$code, $data]); + + return 201; + } + + public function delete(string $code): int + { + if (!array_key_exists($code, self::$resources)) { + throw $this->createNotFoundException(); + } + unset(self::$resources[$code]); + + return 204; + } + + public function listPerPage(int $limit = 100, bool $withCount = false, array $queryParameters = []): PageInterface + { + // TODO: Implement listPerPage() method. + } + + public function all(int $pageSize = 100, array $queryParameters = []): ResourceCursorInterface + { + return new class(new ArrayIterator(self::$resources), $pageSize) implements ResourceCursorInterface { + public function __construct(private ArrayIterator $iterator, private int $pageSize) + { + } + + public function current() + { + return $this->iterator->current(); + } + + public function next(): void + { + $this->iterator->next(); + } + + public function key(): mixed + { + return $this->iterator->key(); + } + + public function valid(): bool + { + return $this->iterator->valid(); + } + + public function rewind(): void + { + $this->iterator->rewind(); + } + + public function getPageSize(): ?int + { + return $this->pageSize; + } + }; + } + + public function get(string $code, array $queryParameters = []): array + { + if (!array_key_exists($code, self::$resources)) { + throw $this->createNotFoundException(); + } + + return (array) self::$resources[$code]; + } + + public function upsert(string $code, array $data = []): int + { + // TODO: Implement upsert() method. + } + + public function upsertAsync(string $code, array $data = []): PromiseInterface|Promise + { + // TODO: Implement upsertAsync() method. + } + + public function upsertList(StreamInterface|array $resources): \Traversable + { + // TODO: Implement upsertList() method. + } + + public function upsertAsyncList(StreamInterface|array $resources): PromiseInterface|Promise + { + // TODO: Implement upsertAsyncList() method. + } + + private function createNotFoundException(): NotFoundHttpException + { + return new NotFoundHttpException('Resource not found', new Request('GET', '/'), new Response(404)); + } +} diff --git a/tests/InMemory/Client/Api/InMemoryAttributeApi.php b/tests/InMemory/Client/Api/InMemoryAttributeApi.php new file mode 100644 index 00000000..92e93b7a --- /dev/null +++ b/tests/InMemory/Client/Api/InMemoryAttributeApi.php @@ -0,0 +1,16 @@ +> + */ + public static array $attributeOptions = []; + + public static function addResource(AttributeOption $attributeOption): void + { + self::$attributeOptions[$attributeOption->attribute][$attributeOption->code] = $attributeOption; + } + + public function get($attributeCode, $code): array + { + if (!array_key_exists($attributeCode, self::$attributeOptions)) { + throw $this->createNotFoundException(); + } + $attributeOptions = self::$attributeOptions[$attributeCode]; + if (!array_key_exists($code, $attributeOptions)) { + throw $this->createNotFoundException(); + } + + return (array) $attributeOptions[$code]; + } + + public function create($attributeCode, $attributeOptionCode, array $data = []): int + { + self::$attributeOptions[] = AttributeOption::create($attributeCode, $attributeOptionCode, $data); + + return 201; + } + + public function listPerPage($attributeCode, $limit = 100, $withCount = false, array $queryParameters = []): PageInterface + { + // TODO: Implement listPerPage() method. + } + + public function all($attributeCode, $pageSize = 10, array $queryParameters = []): ResourceCursorInterface + { + // TODO: Implement all() method. + } + + public function upsert($attributeCode, $attributeOptionCode, array $data = []): int + { + // TODO: Implement upsert() method. + } + + public function upsertAsync($attributeCode, $attributeOptionCode, array $data = []): PromiseInterface|Promise + { + // TODO: Implement upsertAsync() method. + } + + public function upsertList($attributeCode, $attributeOptions): \Traversable + { + // TODO: Implement upsertList() method. + } + + public function upsertAsyncList($attributeCode, $attributeOptions): PromiseInterface|Promise + { + // TODO: Implement upsertAsyncList() method. + } + + private function createNotFoundException(): NotFoundHttpException + { + return new NotFoundHttpException('Attribute option not found', new Request('GET', '/'), new Response(404)); + } +} diff --git a/tests/InMemory/Client/Api/InMemoryProductApi.php b/tests/InMemory/Client/Api/InMemoryProductApi.php new file mode 100644 index 00000000..0d74ba35 --- /dev/null +++ b/tests/InMemory/Client/Api/InMemoryProductApi.php @@ -0,0 +1,17 @@ + $this->code, + 'type' => $this->type, + 'group' => $this->group, + 'unique' => $this->unique, + 'useable_as_grid_filter' => $this->useableAsGridFilter, + 'allowed_extensions' => $this->allowedExtension, + 'metric_family' => $this->metricFamily, + 'default_metric_unit' => $this->defaultMetricUnit, + 'reference_data_name' => $this->referenceDataName, + 'available_locales' => $this->availableLocales, + 'max_characters' => $this->maxCharacters, + 'validation_rule' => $this->validationRule, + 'validation_regexp' => $this->validationRegexp, + 'wysiwyg_enabled' => $this->wysiwygEnabled, + 'number_min' => $this->numberMin, + 'number_max' => $this->numberMax, + 'decimals_allowed' => $this->decimalsAllowed, + 'negative_allowed' => $this->negativeAllowed, + 'date_min' => $this->dateMin, + 'date_max' => $this->dateMax, + 'max_file_size' => $this->maxFileSize, + 'minimum_input_length' => $this->minimumInputLength, + 'sort_order' => $this->sortOrder, + 'localizable' => $this->localizable, + 'scopable' => $this->scopable, + 'labels' => $this->labels, + 'guidelines' => $this->guidelines, + 'auto_option_sorting' => $this->autoOptionSorting, + 'default_value' => $this->defaultValue, + 'group_labels' => $this->groupLabels, + ]; + } + + public function getIdentifier(): string + { + return $this->code; + } +} diff --git a/tests/InMemory/Client/Api/Model/AttributeOption.php b/tests/InMemory/Client/Api/Model/AttributeOption.php new file mode 100644 index 00000000..0302ebd9 --- /dev/null +++ b/tests/InMemory/Client/Api/Model/AttributeOption.php @@ -0,0 +1,43 @@ + $this->code, + 'attribute' => $this->attribute, + 'sort_order' => $this->sortOrder, + 'labels' => $this->labels, + ]; + } + + public function getIdentifier(): string + { + return $this->code; + } +} diff --git a/tests/InMemory/Client/Api/Model/AttributeType.php b/tests/InMemory/Client/Api/Model/AttributeType.php new file mode 100644 index 00000000..2065553c --- /dev/null +++ b/tests/InMemory/Client/Api/Model/AttributeType.php @@ -0,0 +1,11 @@ + $values + * @param array $associations + * @param array $quantifiedAssociations + */ + private function __construct( + public string $identifier, + public bool $enabled = true, + public ?string $family = null, + public array $categories = [], + public array $groups = [], + public ?string $parent = null, + public array $values = [], + public array $associations = [], + public array $quantifiedAssociations = [], + ) { + $now = new DateTimeImmutable(); + $this->created = $now; + $this->updated = $now; + } + + public static function create(string $code, array $data = []): self + { + return new self( + $code, + $data['enabled'] ?? true, + $data['family'] ?? null, + $data['categories'] ?? [], + $data['groups'] ?? [], + $data['parent'] ?? null, + $data['values'] ?? [], + $data['associations'] ?? [], + $data['quantifiedAssociations'] ?? [], + ); + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + public function __serialize(): array + { + return [ + 'identifier' => $this->identifier, + 'enabled' => $this->enabled, + 'family' => $this->family, + 'categories' => $this->categories, + 'groups' => $this->groups, + 'parent' => $this->parent, + 'values' => $this->values, + 'created' => $this->created->format('c'), + 'updated' => $this->updated->format('c'), + 'associations' => $this->associations, + 'quantified_associations' => $this->quantifiedAssociations, + ]; + } + + public function __unserialize(array $data): void + { + $this->identifier = $data['identifier']; + $this->enabled = $data['enabled']; + $this->family = $data['family']; + $this->categories = $data['categories']; + $this->groups = $data['groups']; + $this->parent = $data['parent']; + $this->values = $data['values']; + $this->created = $data['created']; + $this->updated = $data['updated']; + $this->associations = $data['associations']; + $this->quantifiedAssociations = $data['quantified_associations']; + } +} diff --git a/tests/InMemory/Client/Api/Model/ResourceInterface.php b/tests/InMemory/Client/Api/Model/ResourceInterface.php new file mode 100644 index 00000000..8a7477fe --- /dev/null +++ b/tests/InMemory/Client/Api/Model/ResourceInterface.php @@ -0,0 +1,14 @@ + + +Sylius\Component\Core\Model\ProductVariant: + box-variant: + code: "BOX_VARIANT" + product: "@box-product" + optionValues: + - "@format-133x32" diff --git a/tests/Integration/Product/ImporterTest.php b/tests/Integration/Product/ImporterTest.php index 338743ba..a8baf0a8 100644 --- a/tests/Integration/Product/ImporterTest.php +++ b/tests/Integration/Product/ImporterTest.php @@ -13,9 +13,22 @@ use Sylius\Component\Core\Model\ProductInterface; use Sylius\Component\Core\Model\ProductVariantInterface; use Sylius\Component\Core\Repository\ProductRepositoryInterface; +use Sylius\Component\Product\Model\ProductOptionValueInterface; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\Filesystem\Filesystem; +use Tests\Webgriffe\SyliusAkeneoPlugin\InMemory\Client\Api\InMemoryAttributeApi; +use Tests\Webgriffe\SyliusAkeneoPlugin\InMemory\Client\Api\InMemoryAttributeOptionApi; +use Tests\Webgriffe\SyliusAkeneoPlugin\InMemory\Client\Api\InMemoryProductApi; +use Tests\Webgriffe\SyliusAkeneoPlugin\InMemory\Client\Api\Model\Attribute; +use Tests\Webgriffe\SyliusAkeneoPlugin\InMemory\Client\Api\Model\AttributeOption; +use Tests\Webgriffe\SyliusAkeneoPlugin\InMemory\Client\Api\Model\AttributeType; +use Tests\Webgriffe\SyliusAkeneoPlugin\InMemory\Client\Api\Model\Product; +use Tests\Webgriffe\SyliusAkeneoPlugin\InMemory\Client\InMemoryAkeneoPimClient; +use Tests\Webgriffe\SyliusAkeneoPlugin\Integration\DataFixtures\DataFixture; use Webgriffe\SyliusAkeneoPlugin\ImporterInterface; +use Webgriffe\SyliusAkeneoPlugin\PriorityValueHandlersResolver; +use Webgriffe\SyliusAkeneoPlugin\Product\Importer; +use Webgriffe\SyliusAkeneoPlugin\ValueHandler\ProductOptionValueHandler; final class ImporterTest extends KernelTestCase { @@ -796,4 +809,87 @@ public function it_enables_product_without_variants_while_importing_a_new_one(): self::assertTrue($product->isEnabled()); self::assertTrue($productVariant->isEnabled()); } + + /** + * @test + * + * @TODO This tests adds also the new in memory implementation of Akeneo API client. + * To use that only on this specific test we have overriden the importer definition. + * Obviously, when the new implementation will be the default one, all this rewrite should be removed! + */ + public function it_does_not_duplicate_product_option_values_when_changed(): void + { + $this->fixtureLoader->load( + [ + DataFixture::path . '/ORM/resources/Locale/en_US.yaml', + DataFixture::path . '/ORM/resources/Product/box.yaml', + ], + [], + [], + PurgeMode::createDeleteMode(), + ); + $akeneoPimClient = new InMemoryAkeneoPimClient(); + $productOptionValueHandler = new ProductOptionValueHandler( + $akeneoPimClient, + self::getContainer()->get('sylius.repository.product_option'), + self::getContainer()->get('sylius.factory.product_option_value'), + self::getContainer()->get('sylius.factory.product_option_value_translation'), + self::getContainer()->get('sylius.repository.product_option_value'), + self::getContainer()->get('sylius.translation_locale_provider'), + self::getContainer()->get('translator'), + ); + $valueHandlersResolver = new PriorityValueHandlersResolver(); + $valueHandlersResolver->add($productOptionValueHandler); + $this->importer = new Importer( + self::getContainer()->get('sylius.factory.product_variant'), + self::getContainer()->get('sylius.repository.product_variant'), + self::getContainer()->get('sylius.repository.product'), + $akeneoPimClient, + $valueHandlersResolver, + self::getContainer()->get('sylius.factory.product'), + self::getContainer()->get('webgriffe_sylius_akeneo.product.taxons_resolver'), + self::getContainer()->get('webgriffe_sylius_akeneo.product.product_options_resolver'), + self::getContainer()->get('event_dispatcher'), + self::getContainer()->get('webgriffe_sylius_akeneo.product.channels_resolver'), + self::getContainer()->get('webgriffe_sylius_akeneo.product.status_resolver'), + self::getContainer()->get('sylius.factory.product_taxon'), + self::getContainer()->get('webgriffe_sylius_akeneo.product.variant_status_resolver'), + self::getContainer()->get('webgriffe_sylius_akeneo.product.validator'), + ); + $akeneoProduct = Product::create('BOX_VARIANT', [ + 'values' => ['format' => [[ + 'locale' => null, + 'scope' => null, + 'data' => '133x48', + ]]], + 'parent' => 'BOX', + ]); + InMemoryProductApi::addResource($akeneoProduct); + $akeneoFormatAttribute = Attribute::create('format', [ + 'type' => AttributeType::SIMPLE_SELECT, + ]); + InMemoryAttributeApi::addResource($akeneoFormatAttribute); + $akeneoFormatAttributeOption = AttributeOption::create( + $akeneoFormatAttribute->getIdentifier(), + '133x48', + 1, + ['en_US' => 'Format 133x48'], + ); + InMemoryAttributeOptionApi::addResource($akeneoFormatAttributeOption); + + $this->importer->import('BOX_VARIANT'); + + /** @var ProductVariantInterface[] $allVariants */ + $allVariants = $this->productVariantRepository->findAll(); + $this->assertCount(1, $allVariants); + $variant = reset($allVariants); + $this->assertInstanceOf(ProductVariantInterface::class, $variant); + $this->assertInstanceOf(ProductInterface::class, $variant->getProduct()); + $this->assertEquals('BOX', $variant->getProduct()->getCode()); + $this->assertCount(1, $variant->getOptionValues()); + $optionValue = $variant->getOptionValues()->first(); + $this->assertInstanceOf(ProductOptionValueInterface::class, $optionValue); + $this->assertEquals('format', $optionValue->getOptionCode()); + $this->assertEquals('Format 133x48', $optionValue->getValue()); + } }