From c2abb94e0ab886b15abcdd91351049a314836ae2 Mon Sep 17 00:00:00 2001
From: Pierre Gauthier <pigau@smile.fr>
Date: Fri, 9 Feb 2024 15:04:01 +0100
Subject: [PATCH] Move data cleaning code in a dedicated command

---
 README.md                                     |  6 +-
 src/Command/StructureClean.php                | 65 ++++++++++++++
 src/Command/StructureSync.php                 |  2 +-
 src/Resources/config/services/commands.xml    |  5 ++
 src/Synchronizer/AbstractSynchronizer.php     |  8 ++
 src/Synchronizer/CatalogSynchronizer.php      | 60 +++++++++----
 .../SourceFieldOptionSynchronizer.php         | 52 +++++++++--
 src/Synchronizer/SourceFieldSynchronizer.php  | 89 +++++++------------
 8 files changed, 200 insertions(+), 87 deletions(-)
 create mode 100644 src/Command/StructureClean.php

diff --git a/README.md b/README.md
index 3fecc85..0c4f061 100644
--- a/README.md
+++ b/README.md
@@ -57,7 +57,7 @@
     - Open Sylius Admin, head to Configuration > Gally and configure the Gally endpoint (URL, credentials)
 - Run this commands from your Sylius instance. This commands must be runned only once to synchronize the structure.
     ```shell
-        bin/console gally:structure-sync   # Sync catalog et source field data with gally
+        bin/console gally:structure:sync   # Sync catalog et source field data with gally
     ```
 - Run a full index from Sylius to Gally. This command can be run only once. Afterwards, the modified products are automatically synchronized.
     ```shell
@@ -66,6 +66,10 @@
 - At this step, you should be able to see your product and source field in the Gally backend.
 - They should also appear in your Sylius frontend when searching or browsing categories.
 - And you're done !
+- You can also run the command to clean data that are not present in sylius anymore:
+    ```shell
+        bin/console gally:structure:clean 
+    ```
 
 ## noUiSlider 
 
diff --git a/src/Command/StructureClean.php b/src/Command/StructureClean.php
new file mode 100644
index 0000000..5d73aa7
--- /dev/null
+++ b/src/Command/StructureClean.php
@@ -0,0 +1,65 @@
+<?php
+/**
+ * DISCLAIMER
+ *
+ * Do not edit or add to this file if you wish to upgrade Gally to newer versions in the future.
+ *
+ * @package   Gally
+ * @author    Stephan Hochdörfer <S.Hochdoerfer@bitexpert.de>, Gally Team <elasticsuite@smile.fr>
+ * @copyright 2022-present Smile
+ * @license   Open Software License v. 3.0 (OSL-3.0)
+ */
+
+declare(strict_types=1);
+
+namespace Gally\SyliusPlugin\Command;
+
+use Gally\SyliusPlugin\Synchronizer\AbstractSynchronizer;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class StructureClean extends Command
+{
+    protected static $defaultName = 'gally:structure:clean';
+
+    /**
+     * @param AbstractSynchronizer[] $synchronizers
+     */
+    public function __construct(
+        private iterable $synchronizers
+    ) {
+        parent::__construct();
+    }
+
+    protected function configure(): void
+    {
+        $this->setDescription('Remove all entity from gally that not exist anymore on sylius side.')
+            ->addOption('force', 'f', InputOption::VALUE_NONE, 'Really remove the listed entity from the gally.')
+            ->addOption('quiet', 'q', InputOption::VALUE_NONE, 'Don\'t list deleted entities.');
+    }
+
+    protected function execute(InputInterface $input, OutputInterface $output): int
+    {
+        $output->writeln('');
+        $isDryRun = !$input->getOption('force');
+        $isQuiet = $input->getOption('quiet');
+
+        if ($isDryRun) {
+            $output->writeln("<error>Running in dry run mode, add -f to really delete entities from Gally.</error>");
+            $output->writeln('');
+        }
+
+        foreach ($this->synchronizers as $synchronizer) {
+            $time = microtime(true);
+            $message = "<comment>Clean {$synchronizer->getEntityClass()}</comment>";
+            $output->writeln("$message ...");
+            $synchronizer->cleanAll($isDryRun, $isQuiet);
+            $time = number_format(microtime(true) - $time, 2);
+            $output->writeln("  Cleaned ($time)s\n");
+        }
+
+        return 0;
+    }
+}
diff --git a/src/Command/StructureSync.php b/src/Command/StructureSync.php
index e35a345..f4f3013 100644
--- a/src/Command/StructureSync.php
+++ b/src/Command/StructureSync.php
@@ -21,7 +21,7 @@
 
 class StructureSync extends Command
 {
-    protected static $defaultName = 'gally:structure-sync';
+    protected static $defaultName = 'gally:structure:sync';
 
     /**
      * @param AbstractSynchronizer[] $synchronizers
diff --git a/src/Resources/config/services/commands.xml b/src/Resources/config/services/commands.xml
index 8c728a3..a1547b7 100644
--- a/src/Resources/config/services/commands.xml
+++ b/src/Resources/config/services/commands.xml
@@ -12,5 +12,10 @@
             <argument type="tagged_iterator" tag="gally.entity.indexer"/>
             <tag name="console.command"/>
         </service>
+
+        <service id="Gally\SyliusPlugin\Command\StructureClean">
+            <argument type="tagged_iterator" tag="gally.entity.synchronizer"/>
+            <tag name="console.command"/>
+        </service>
     </services>
 </container>
diff --git a/src/Synchronizer/AbstractSynchronizer.php b/src/Synchronizer/AbstractSynchronizer.php
index 15cf4d4..ec1355b 100644
--- a/src/Synchronizer/AbstractSynchronizer.php
+++ b/src/Synchronizer/AbstractSynchronizer.php
@@ -84,6 +84,14 @@ public function fetchEntity(ModelInterface $entity): ?ModelInterface
 
     abstract protected function getIdentity(ModelInterface $entity): string;
 
+    /**
+     * Remove all entity from gally that not exist anymore on sylius side.
+     */
+    public function cleanAll(bool $dryRun = true, bool $quiet = false): void
+    {
+
+    }
+
     protected function buildFetchAllParams(int $page): array
     {
         return [
diff --git a/src/Synchronizer/CatalogSynchronizer.php b/src/Synchronizer/CatalogSynchronizer.php
index 04a0c38..d4e4829 100644
--- a/src/Synchronizer/CatalogSynchronizer.php
+++ b/src/Synchronizer/CatalogSynchronizer.php
@@ -63,10 +63,7 @@ public function getIdentity(ModelInterface $entity): string
     public function synchronizeAll(): void
     {
         $this->fetchEntities();
-
-        $this->catalogCodes = array_flip($this->getAllEntityCodes());
         $this->localizedCatalogSynchronizer->fetchEntities();
-        $this->localizedCatalogCodes = array_flip($this->localizedCatalogSynchronizer->getAllEntityCodes());
 
         // synchronize all channels where the Gally integration is active
         $channels = $this->channelRepository->findBy(['gallyActive' => 1]);
@@ -75,18 +72,6 @@ public function synchronizeAll(): void
         foreach ($channels as $channel) {
             $this->synchronizeItem(['channel' => $channel]);
         }
-
-        foreach (array_flip($this->localizedCatalogCodes) as $localizedCatalogCode) {
-            /** @var LocalizedCatalogCatalogRead $localizedCatalog */
-            $localizedCatalog = $this->localizedCatalogSynchronizer->getEntityFromApi($localizedCatalogCode);
-            $this->localizedCatalogSynchronizer->deleteEntity($localizedCatalog->getId());
-        }
-
-        foreach (array_flip($this->catalogCodes) as $catalogCode) {
-            /** @var CatalogCatalogRead $catalog */
-            $catalog = $this->getEntityFromApi($catalogCode);
-            $this->deleteEntity($catalog->getId());
-        }
     }
 
     public function synchronizeItem(array $params): ?ModelInterface
@@ -108,12 +93,51 @@ public function synchronizeItem(array $params): ?ModelInterface
                 'locale' => $locale,
                 'catalog' => $catalog,
             ]);
+        }
 
-            unset($this->localizedCatalogCodes[$this->localizedCatalogSynchronizer->getIdentity($localizedCatalog)]);
+        return $catalog;
+    }
+
+    public function cleanAll(bool $dryRun = true, bool $quiet = false): void
+    {
+        $this->fetchEntities();
+
+        $this->catalogCodes = array_flip($this->getAllEntityCodes());
+        $this->localizedCatalogSynchronizer->fetchEntities();
+        $this->localizedCatalogCodes = array_flip($this->localizedCatalogSynchronizer->getAllEntityCodes());
+
+        // Synchronize all channels where the Gally integration is active
+        $channels = $this->channelRepository->findBy(['gallyActive' => 1]);
+
+        /** @var Channel[] $channels */
+        foreach ($channels as $channel) {
+            /** @var LocaleInterface $locale */
+            foreach ($channel->getLocales() as $locale) {
+                unset($this->localizedCatalogCodes[$channel->getCode() . '_' . $locale->getCode()]);
+            }
+            unset($this->catalogCodes[$channel->getCode()]);
         }
 
-        unset($this->catalogCodes[$this->getIdentity($catalog)]);
+        foreach (array_flip($this->localizedCatalogCodes) as $localizedCatalogCode) {
+            /** @var LocalizedCatalogCatalogRead $localizedCatalog */
+            $localizedCatalog = $this->localizedCatalogSynchronizer->getEntityFromApi($localizedCatalogCode);
+            if (!$quiet) {
+                print("  Delete localized catalog {$localizedCatalog->getId()}\n");
+            }
+            if (!$dryRun) {
+                $this->localizedCatalogSynchronizer->deleteEntity($localizedCatalog->getId());
+            }
+        }
 
-        return $catalog;
+        foreach (array_flip($this->catalogCodes) as $catalogCode) {
+            /** @var CatalogCatalogRead $catalog */
+            $catalog = $this->getEntityFromApi($catalogCode);
+            if (!$quiet) {
+                print("  Delete catalog {$catalog->getId()}\n");
+            }
+            if (!$dryRun) {
+                $this->deleteEntity($catalog->getId());
+            }
+        }
     }
 }
diff --git a/src/Synchronizer/SourceFieldOptionSynchronizer.php b/src/Synchronizer/SourceFieldOptionSynchronizer.php
index 74afc9a..447cde6 100644
--- a/src/Synchronizer/SourceFieldOptionSynchronizer.php
+++ b/src/Synchronizer/SourceFieldOptionSynchronizer.php
@@ -72,7 +72,6 @@ public function getIdentity(ModelInterface $entity): string
 
     public function synchronizeAll(): void
     {
-        $this->sourceFieldOptionCodes = array_flip($this->getAllEntityCodes());
         $this->sourceFieldSynchronizer->fetchEntities();
 
         $metadataName = strtolower((new \ReflectionClass(Product::class))->getShortName());
@@ -134,12 +133,6 @@ public function synchronizeAll(): void
         }
 
         $this->runBulk();
-
-        foreach (array_flip($this->sourceFieldOptionCodes) as $sourceFieldOptionCode) {
-            /** @var SourceFieldOptionSourceFieldOptionRead $sourceFieldOption */
-            $sourceFieldOption = $this->getEntityFromApi($sourceFieldOptionCode);
-            $this->deleteEntity($sourceFieldOption->getId());
-        }
     }
 
     public function synchronizeItem(array $params): ?ModelInterface
@@ -172,11 +165,52 @@ public function synchronizeItem(array $params): ?ModelInterface
         $sourceFieldOption = new SourceFieldOptionSourceFieldOptionWrite($data);
         $this->addEntityToBulk($sourceFieldOption);
 
-        unset($this->sourceFieldOptionCodes[$this->getIdentity($sourceFieldOption)]);
-
         return $sourceFieldOption;
     }
 
+    public function cleanAll(bool $dryRun = true, bool $quiet = false): void
+    {
+        $this->sourceFieldOptionCodes = array_flip($this->getAllEntityCodes());
+        $this->sourceFieldSynchronizer->fetchEntities();
+
+        $metadataName = strtolower((new \ReflectionClass(Product::class))->getShortName());
+        /** @var MetadataMetadataRead $metadata */
+        $metadata = $this->metadataSynchronizer->synchronizeItem(['entity' => $metadataName]);
+
+        /** @var ProductAttribute[] $attributes */
+        $attributes = $this->productAttributeRepository->findAll();
+        foreach ($attributes as $attribute) {
+            if ('select' === $attribute->getType()) {
+                $sourceField = $this->sourceFieldSynchronizer->getEntityByCode($metadata, $attribute->getCode());
+                $configuration = $attribute->getConfiguration();
+                foreach ($configuration['choices'] ?? [] as $code => $choice) {
+                    unset($this->sourceFieldOptionCodes['/source_fields/' . $sourceField->getId() . $code]);
+                }
+            }
+        }
+
+        /** @var ProductOption[] $options */
+        $options = $this->productOptionRepository->findAll();
+        foreach ($options as $option) {
+            $sourceField = $this->sourceFieldSynchronizer->getEntityByCode($metadata, $option->getCode());
+            /** @var ProductOptionValueInterface $value */
+            foreach ($option->getValues() as $value) {
+                unset($this->sourceFieldOptionCodes['/source_fields/' . $sourceField->getId() . $value->getCode()]);
+            }
+        }
+
+        foreach (array_flip($this->sourceFieldOptionCodes) as $sourceFieldOptionCode) {
+            /** @var SourceFieldOptionSourceFieldOptionRead $sourceFieldOption */
+            $sourceFieldOption = $this->getEntityFromApi($sourceFieldOptionCode);
+            if (!$quiet) {
+                print("  Delete sourceFieldOption {$sourceFieldOption->getSourceField()} {$sourceFieldOption->getCode()}\n");
+            }
+            if (!$dryRun) {
+                $this->deleteEntity($sourceFieldOption->getId());
+            }
+        }
+    }
+
     public function fetchEntity(ModelInterface $entity): ?ModelInterface
     {
         /** @var SourceFieldOptionSourceFieldOptionWrite $entity */
diff --git a/src/Synchronizer/SourceFieldSynchronizer.php b/src/Synchronizer/SourceFieldSynchronizer.php
index e02a332..e48b7d7 100644
--- a/src/Synchronizer/SourceFieldSynchronizer.php
+++ b/src/Synchronizer/SourceFieldSynchronizer.php
@@ -70,43 +70,18 @@ public function getIdentity(ModelInterface $entity): string
 
     public function synchronizeAll(): void
     {
-        $this->sourceFieldCodes = array_flip($this->getAllEntityCodes());
-
         $metadataName = strtolower((new \ReflectionClass(Product::class))->getShortName());
         $metadata = $this->metadataSynchronizer->synchronizeItem(['entity' => $metadataName]);
 
         /** @var ProductAttribute[] $attributes */
         $attributes = $this->productAttributeRepository->findAll();
         foreach ($attributes as $attribute) {
-            $options = [];
-            if ('select' === $attribute->getType()) {
-                $position = 0;
-                $configuration = $attribute->getConfiguration();
-                $choices = $configuration['choices'] ?? [];
-                foreach ($choices as $code => $choice) {
-                    $translations = [];
-                    foreach ($choice ?? [] as $locale => $translation) {
-                        $translations[] = [
-                            'locale' => $locale,
-                            'translation' => $translation,
-                        ];
-                    }
-                    $options[$position] = [
-                        'code' => $code,
-                        'translations' => $translations,
-                        'position' => $position,
-                    ];
-                    ++$position;
-                }
-            }
-
             $this->synchronizeItem([
                 'metadata' => $metadata,
                 'field' => [
                     'code' => $attribute->getCode(),
                     'type' => self::getGallyType($attribute->getType()),
                     'translations' => $attribute->getTranslations(),
-                    'options' => $options,
                 ],
             ]);
         }
@@ -114,48 +89,17 @@ public function synchronizeAll(): void
         /** @var ProductOption[] $options */
         $options = $this->productOptionRepository->findAll();
         foreach ($options as $option) {
-            $optionValues = [];
-            $position = 0;
-            foreach ($option->getValues() as $value) {
-                $translations = [];
-                foreach ($value->getTranslations() as $translation) {
-                    /** @var ProductOptionValueTranslation $translation */
-                    $translations[] = [
-                        'locale' => $translation->getLocale(),
-                        'translation' => $translation->getValue(),
-                    ];
-                }
-
-                /** @var ProductOptionValueInterface $value */
-                $optionValues[$position] = [
-                    'code' => $value->getCode(),
-                    'translations' => $translations,
-                    'position' => $position,
-                ];
-
-                ++$position;
-            }
-
             $this->synchronizeItem([
                 'metadata' => $metadata,
                 'field' => [
                     'code' => $option->getCode(),
                     'type' => self::getGallyType('select'),
                     'translations' => $option->getTranslations(),
-                    'options' => $optionValues,
                 ],
             ]);
         }
 
         $this->runBulk();
-
-        foreach (array_flip($this->sourceFieldCodes) as $sourceFieldCode) {
-            /** @var SourceFieldSourceFieldRead $sourceField */
-            $sourceField = $this->getEntityFromApi($sourceFieldCode);
-            if (!$sourceField->getIsSystem()) {
-                $this->deleteEntity($sourceField->getId());
-            }
-        }
     }
 
     public function synchronizeItem(array $params): ?ModelInterface
@@ -192,11 +136,40 @@ public function synchronizeItem(array $params): ?ModelInterface
         $sourceField = new SourceFieldSourceFieldWrite($data);
         $this->addEntityToBulk($sourceField);
 
-        unset($this->sourceFieldCodes[$this->getIdentity($sourceField)]);
-
         return $sourceField;
     }
 
+    public function cleanAll(bool $dryRun = true, bool $quiet = false): void
+    {
+        $this->sourceFieldCodes = array_flip($this->getAllEntityCodes());
+
+        $metadataName = strtolower((new \ReflectionClass(Product::class))->getShortName());
+        $metadata = $this->metadataSynchronizer->synchronizeItem(['entity' => $metadataName]);
+
+        /** @var ProductAttribute[] $attributes */
+        $attributes = $this->productAttributeRepository->findAll();
+        foreach ($attributes as $attribute) {
+            unset($this->sourceFieldCodes['/metadata/' . $metadata->getId() . $attribute->getCode()]);
+        }
+
+        /** @var ProductOption[] $options */
+        $options = $this->productOptionRepository->findAll();
+        foreach ($options as $option) {
+            unset($this->sourceFieldCodes['/metadata/' . $metadata->getId() . $option->getCode()]);
+        }
+
+        foreach (array_flip($this->sourceFieldCodes) as $sourceFieldCode) {
+            /** @var SourceFieldSourceFieldRead $sourceField */
+            $sourceField = $this->getEntityFromApi($sourceFieldCode);
+            if (!$sourceField->getIsSystem() && !$quiet) {
+                print("  Delete sourceField {$sourceField->getMetadata()} {$sourceField->getCode()}\n");
+            }
+            if (!$sourceField->getIsSystem() && !$dryRun) {
+                $this->deleteEntity($sourceField->getId());
+            }
+        }
+    }
+
     public static function getGallyType(string $type): string
     {
         switch ($type) {