diff --git a/Console/Command/SynonymDeduplicateCommand.php b/Console/Command/SynonymDeduplicateCommand.php index 3f5f582c0..f58d2d357 100644 --- a/Console/Command/SynonymDeduplicateCommand.php +++ b/Console/Command/SynonymDeduplicateCommand.php @@ -2,11 +2,13 @@ namespace Algolia\AlgoliaSearch\Console\Command; +use Algolia\AlgoliaSearch\Helper\ArrayDeduplicator; use Algolia\AlgoliaSearch\Service\AlgoliaConnector; use Algolia\AlgoliaSearch\Service\Product\IndexOptionsBuilder; use Algolia\AlgoliaSearch\Service\StoreNameFetcher; use Magento\Framework\App\State; use Magento\Framework\Console\Cli; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Store\Model\StoreManagerInterface; use Symfony\Component\Console\Input\InputInterface; @@ -20,6 +22,7 @@ public function __construct( protected StoreNameFetcher $storeNameFetcher, protected StoreManagerInterface $storeManager, protected IndexOptionsBuilder $indexOptionsBuilder, + protected ArrayDeduplicator $arrayDeduplicator, ?string $name = null ) { parent::__construct($state, $storeNameFetcher, $name); @@ -52,6 +55,7 @@ protected function getAdditionalDefinition(): array /** * @throws NoSuchEntityException + * @throws LocalizedException */ protected function execute(InputInterface $input, OutputInterface $output): int { @@ -114,7 +118,7 @@ public function dedupeSynonymsForStore(int $storeId): void $indexOptions = $this->indexOptionsBuilder->buildEntityIndexOptions($storeId); $settings = $this->algoliaConnector->getSettings($indexOptions); - $deduped = $this->dedupeSpecificSettings(['synonyms', 'altCorrections'], $settings); + $deduped = $this->arrayDeduplicator->dedupeSpecificSettings(['synonyms', 'altCorrections'], $settings); //bring over as is (no de-dupe necessary) if (array_key_exists('placeholders', $settings)) { @@ -139,41 +143,4 @@ public function dedupeSynonymsForAllStores(): void $this->dedupeSynonymsForStore($storeId); } } - - /** - * @param string[] $settingNames - * @param array $settings - * @return array - */ - protected function dedupeSpecificSettings(array $settingNames, array $settings): array - { - return array_filter( - array_combine( - $settingNames, - array_map( - fn($settingName) => isset($settings[$settingName]) - ? $this->dedupeArrayOfArrays($settings[$settingName]) - : null, - $settingNames - ) - ), - fn($val) => $val !== null - ); - } - - /** - * Find and remove the duplicates in an array of indexed arrays - * Does not work with associative arrays - * @param array $data - * @return array - */ - protected function dedupeArrayOfArrays(array $data): array { - $encoded = array_map('json_encode', $data); - $unique = array_values(array_unique($encoded)); - $decoded = array_map( - fn($item) => json_decode((string) $item, true), - $unique - ); - return $decoded; - } } diff --git a/Helper/ArrayDeduplicator.php b/Helper/ArrayDeduplicator.php new file mode 100644 index 000000000..457d34191 --- /dev/null +++ b/Helper/ArrayDeduplicator.php @@ -0,0 +1,51 @@ + $settings + * @return array + */ + public function dedupeSpecificSettings(array $settingNames, array $settings): array + { + $processedSettings = []; + foreach ($settingNames as $settingName) { + $processedSettings[$settingName] = isset($settings[$settingName]) + ? $this->dedupeArrayOfArrays($settings[$settingName]) + : null; + } + + $filteredSettings = []; + foreach ($processedSettings as $key => $value) { + if ($value !== null) { + $filteredSettings[$key] = $value; + } + } + + return $filteredSettings; + } + + /** + * Find and remove the duplicates in an array of indexed arrays + * Does not work with associative arrays + * @param array $data + * @return array + */ + public function dedupeArrayOfArrays(array $data): array { + $encoded = []; + foreach ($data as $item) { + $encoded[] = json_encode($item); + } + $unique = array_values(array_unique($encoded)); + + $decoded = []; + foreach ($unique as $item) { + $decoded[] = json_decode((string) $item, true); + } + + return $decoded; + } +} diff --git a/Test/Unit/Helper/ArrayDeduplicatorTest.php b/Test/Unit/Helper/ArrayDeduplicatorTest.php new file mode 100644 index 000000000..2bcf13a58 --- /dev/null +++ b/Test/Unit/Helper/ArrayDeduplicatorTest.php @@ -0,0 +1,176 @@ +deduplicator = new ArrayDeduplicator(); + } + + public static function dedupeArrayOfArraysProvider(): array + { + return [ + 'empty array' => [ + 'input' => [], + 'expectedCount' => 0, + 'expectedItems' => [] + ], + 'no duplicates' => [ + 'input' => [ + ['a' => 1, 'b' => 2], + ['a' => 2, 'b' => 3], + ['a' => 3, 'b' => 4] + ], + 'expectedCount' => 3, + 'expectedItems' => [ + ['a' => 1, 'b' => 2], + ['a' => 2, 'b' => 3], + ['a' => 3, 'b' => 4] + ] + ], + 'exact duplicates' => [ + 'input' => [ + ['a' => 1, 'b' => 2], + ['a' => 1, 'b' => 2], // duplicate + ['a' => 2, 'b' => 3] + ], + 'expectedCount' => 2, + 'expectedItems' => [ + ['a' => 1, 'b' => 2], + ['a' => 2, 'b' => 3] + ] + ], + 'multiple duplicates' => [ + 'input' => [ + ['id' => 1, 'name' => 'test'], + ['id' => 2, 'name' => 'test2'], + ['id' => 1, 'name' => 'test'], // duplicate + ['id' => 3, 'name' => 'test3'], + ['id' => 2, 'name' => 'test2'] // duplicate + ], + 'expectedCount' => 3, + 'expectedItems' => [ + ['id' => 1, 'name' => 'test'], + ['id' => 2, 'name' => 'test2'], + ['id' => 3, 'name' => 'test3'] + ] + ], + 'duplicate synonyms' => [ + 'input' => [ + ['gray', 'grey'], + ['trousers', 'pants'], + ['ipad', 'tablet'], + ['caulk', 'caulking'], + ['trousers', 'pants'], // duplicate + ['molding', 'moldings', 'moulding', 'mouldings'], + ['trash', 'garbage'], + ['molding', 'moldings', 'moulding', 'mouldings'], // duplicate + ], + 'expectedCount' => 6, + 'expectedItems' => [ + ['gray', 'grey'], + ['trousers', 'pants'], + ['ipad', 'tablet'], + ['caulk', 'caulking'], + ['molding', 'moldings', 'moulding', 'mouldings'], + ['trash', 'garbage'], + ] + ], + 'duplicate alt corrections' => [ + 'input' => [ + [ 'word' => 'trousers', 'nbTypos' => 1, 'correction' => 'pants' ], + [ 'word' => 'rod', 'nbTypos' => 1, 'correction' => 'bar' ], + [ 'word' => 'bell', 'nbTypos' => 1, 'correction' => 'buzzer' ], + [ 'word' => 'rod', 'nbTypos' => 1, 'correction' => 'bar' ], // duplicate + [ 'word' => 'blind', 'nbTypos' => 1, 'correction' => 'shade' ], + [ 'word' => 'blind', 'nbTypos' => 2, 'correction' => 'shade' ], // not a duplicate + [ 'word' => 'trousers', 'nbTypos' => 1, 'correction' => 'pants' ], // duplicate + ], + 'expectedCount' => 5, + 'expectedItems' => [ + [ 'word' => 'trousers', 'nbTypos' => 1, 'correction' => 'pants' ], + [ 'word' => 'rod', 'nbTypos' => 1, 'correction' => 'bar' ], + [ 'word' => 'bell', 'nbTypos' => 1, 'correction' => 'buzzer' ], + [ 'word' => 'blind', 'nbTypos' => 1, 'correction' => 'shade' ], + [ 'word' => 'blind', 'nbTypos' => 2, 'correction' => 'shade' ], + ] + ] + ]; + } + + /** + * @dataProvider dedupeArrayOfArraysProvider + */ + public function testDedupeArrayOfArrays(array $input, int $expectedCount, array $expectedItems): void + { + $result = $this->deduplicator->dedupeArrayOfArrays($input); + + $this->assertCount($expectedCount, $result); + + foreach ($expectedItems as $expectedItem) { + $this->assertContains($expectedItem, $result); + } + } + + public function testDedupeArrayOfArraysKeepsOrderOfFirstOccurrences(): void + { + $data = [ + ['id' => 1], + ['id' => 2], + ['id' => 1], // duplicate + ]; + + $result = $this->deduplicator->dedupeArrayOfArrays($data); + + $this->assertSame([['id' => 1], ['id' => 2]], $result); + } + + public function testDedupeSpecificSettingsOnlyProcessesRequestedSettings(): void + { + $settings = [ + 'synonyms' => [ + ['red' => 'rouge'], + ['red' => 'rouge'], + ], + 'altCorrections' => [ + [ 'word' => 'bell', 'nbTypos' => 1, 'correction' => 'buzzer' ], + ], + 'placeholder' => [ + ['foo' => 'bar'], + ], + ]; + + $result = $this->deduplicator->dedupeSpecificSettings( + ['synonyms', 'altCorrections'], + $settings + ); + + $this->assertArrayHasKey('synonyms', $result); + $this->assertCount(1, $result['synonyms']); // deduped + $this->assertArrayHasKey('altCorrections', $result); + $this->assertCount(1, $result['altCorrections']); + $this->assertArrayNotHasKey('placeholders', $result); + } + + public function testDedupeSpecificSettingsHandlesMissingKeys(): void + { + $settings = [ + 'synonyms' => [['red', 'rouge']], + ]; + + $result = $this->deduplicator->dedupeSpecificSettings( + ['synonyms', 'altCorrections'], + $settings + ); + + $this->assertArrayHasKey('synonyms', $result); + $this->assertArrayNotHasKey('altCorrections', $result); + } +}