Skip to content

Commit

Permalink
Analyzing a whole directory instead of a file
Browse files Browse the repository at this point in the history
  • Loading branch information
Franck Allimant committed Nov 13, 2024
1 parent 7879b0a commit ea56f2d
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 37 deletions.
7 changes: 7 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
name: "Auto Release"
on:
push:
branches: [ master, main ]
jobs:
release:
uses: thelia-modules/ReusableWorkflow/.github/workflows/auto_release.yml@main
56 changes: 41 additions & 15 deletions Command/ImportProducts.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,41 +7,67 @@
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Finder\Finder;
use Thelia\Command\ContainerAwareCommand;
use Thelia\Log\Tlog;

class ImportProducts extends ContainerAwareCommand
{
public const FILE_PATH = 'filePath';
public function __construct(private CsvProductImporterService $csvProductImporterService)
public const DIR_PATH = 'dirPath';
public function __construct(private readonly CsvProductImporterService $csvProductImporterService)
{
parent::__construct();
}

protected function configure(): void
{
$this
->setName('csvimporter:import-products')
->addArgument(self::FILE_PATH, InputArgument::REQUIRED, 'Path to CSV file to import');
->setName('csvimporter:import-catalog')
->addArgument(self::DIR_PATH, InputArgument::REQUIRED, 'Path to a directory which contains CSV file(s) and Images directory');
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->initRequest();
$filePath = $input->getArgument(self::FILE_PATH);
$baseDir = $input->getArgument(self::DIR_PATH);

$finder = Finder::create()
->files()
->in($baseDir)
->depth(0)
->name('*.csv')
;

$output->writeln("<info>Fetching directory $baseDir</info>");

$return = Command::SUCCESS;

$count = $errors = 0;

$output->writeln("<info>Starting to import : $filePath</info>");
foreach ($finder->getIterator() as $file) {
$output->writeln("<info>Starting to import : ".$file->getBasename()."</info>");

try {
$this->csvProductImporterService->importProductsFromCsv($filePath);
$output->writeln('<info>Import is a success !</info>');
} catch (\Exception $e) {
Tlog::getInstance()->addError("Erreur lors de l'importation : ".$e->getMessage());
$output->writeln('<error>Error : '.$e->getMessage().'</error>');
$count++;

return Command::FAILURE;
try {
$this->csvProductImporterService->importProductsFromCsv($file->getPathname());

$output->writeln('<info>Import is a success !</info>');
} catch (\Exception $e) {
Tlog::getInstance()->addError("Erreur lors de l'importation : ".$e->getMessage());
$output->writeln('<error>Error : '.$e->getMessage().'</error>');
if ($e->getPrevious()) {
$output->writeln('<error>Caused by : '.$e->getPrevious()->getMessage().'</error>');
}

$return = Command::FAILURE;

$errors++;
}
}

return Command::SUCCESS;
}
$output->writeln("<info>$count file(s) processed, $errors error(s).</info>");

return $return;
}
}
2 changes: 1 addition & 1 deletion Config/module.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<descriptive locale="fr_FR">
<title>CSV Importer</title>
</descriptive>
<version>1.0.0</version>
<version>1.0.1</version>
<author>
<name>Thelia</name>
<email>info@thelia.net</email>
Expand Down
63 changes: 63 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# CsvImporter

Ce module permet d'importer et de mettre à jour votre catalogue de produit via un ou plusieurs fichiers CSV,
et un répertoire d'images.

L'importation est réalisé en ligne de commande :

`php Thelia csvimporter:import-catalog <directory>`

`<directory>` est le répertoire où se trouvent les fichiers CSV et le répertoire `Images`

Ce répertoire doit être nommé Images, et se trouver dans le même répertoire que vos fichiers CSV

Les colonnes du fichier sont les suivantes (provisoire) :

- Titre du produit (Description)
- Famille
- Sous-Famille
- Marque
- Niveau 1
- Niveau 2
- Niveau 3
- Niveau 4
- Description courte
- Description Longue
- Règle de taxe
- Prix du Produit HT
- Prix TTC
- Poids
- IMG
- Référence Produit (Code)
- D:Couleur
- C:Type de Batterie
- C: Contenance
- C: Ohm
- EAN

Une colonne commençant par `D:` est une valeur de déclinaison
Une colonne commençant par `C:` est une valeur de caractéristique

Les colonnes ne doivent pas suivre un ordre particulier. Le fichier CsvImporter/Config/csv_mapping.yaml permet de définir
le mapping des colonnes avec leur rôle dans Thelia, hors déclinaisons et caractéristiques, qui sont dynamiques. Exemple :

```yaml
mappings:
product_reference: "Référence Produit (Code)"
product_title: "Titre du produit (Description)"
family: "Famille"
sub_family: "Sous-Famille"
brand: "Marque"
level_1: "Niveau 1"
level_2: "Niveau 2"
level_3: "Niveau 3"
level_4: "Niveau 4"
tax_rule: "Règle de taxe"
price_excl_tax: "Prix du Produit HT"
price_incl_tax: "Prix TTC"
weight: "Poids"
ean: "EAN"
short_description: "Description courte"
long_description: "Description Longue"
image: "IMG"
```
14 changes: 12 additions & 2 deletions Service/CsvParserService.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ public function __construct()

public function mapToArray(array $productData): array
{
static $lastMappedData = [];

$mappedData = [];
foreach ($this->getFeatureColumns($productData) as $featureColumn => $featureTitle) {
$mappedData['features'][substr($featureColumn, 2)] = $productData[$featureColumn];
Expand All @@ -26,10 +28,18 @@ public function mapToArray(array $productData): array
$mappedData['attributes'][substr($attributeColumn, 2)] = $productData[$attributeColumn];
}
foreach ($this->mapping as $fieldName => $header) {
$mappedData[$fieldName] = $productData[$header] ?? null;
// On empty fields, re-use last valid value if we have one
$mappedData[$fieldName] =
empty($productData[$header]) ?
($lastMappedData[$fieldName] ?? null) :
$productData[$header]
;

if (! empty($mappedData[$fieldName])) {
$lastMappedData[$fieldName] = $mappedData[$fieldName];
}
}


return $mappedData;
}

Expand Down
54 changes: 35 additions & 19 deletions Service/CsvProductImporterService.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
namespace CsvImporter\Service;

use Propel\Runtime\Exception\PropelException;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Thelia\Core\Event\Attribute\AttributeAvCreateEvent;
Expand Down Expand Up @@ -55,7 +55,6 @@
use Thelia\Model\FeatureTemplateQuery;
use Thelia\Model\Product;
use Thelia\Model\ProductImageQuery;
use Thelia\Model\ProductQuery;
use Thelia\Model\ProductSaleElements;
use Thelia\Model\ProductSaleElementsProductImageQuery;
use Thelia\Model\ProductSaleElementsQuery;
Expand Down Expand Up @@ -90,9 +89,9 @@ class CsvProductImporterService
private ?string $previousTaxRate = null;

public function __construct(
private EventDispatcherInterface $dispatcher,
private FileManager $fileManager,
private CsvParserService $csvParser
private readonly EventDispatcherInterface $dispatcher,
private readonly FileManager $fileManager,
private readonly CsvParserService $csvParser
)
{
}
Expand All @@ -111,12 +110,12 @@ public function importProductsFromCsv($filePath, Country $country = null, string
throw new \RuntimeException("File does not exists: $filePath");
}

if (($handle = fopen($filePath, 'r')) === false) {
if (($handle = fopen($filePath, 'rb')) === false) {
throw new \RuntimeException("Cannot open file: $filePath");
}

$headers = fgetcsv($handle, 1000);
while (($data = fgetcsv($handle, 1000)) !== false) {
$headers = fgetcsv($handle);
while (($data = fgetcsv($handle)) !== false) {
if (!$productData = array_combine($headers, $data)) {
throw new \RuntimeException('Problem while combining headers and data.');
}
Expand Down Expand Up @@ -160,7 +159,7 @@ private function findOrCreateTax(array $productData, Country $country, string $l
}

$taxEvent = $this->createTax($locale, $taxTitle, $taxPercent);
$taxRuleEvent = $this->createTaxRule($locale, $taxTitle, $country, $taxEvent->getTax()->getId());
$taxRuleEvent = $this->createTaxRule($locale, $taxTitle, $country, $taxEvent->getTax()?->getId());

return $taxRuleEvent->getTaxRule();
}
Expand Down Expand Up @@ -276,20 +275,23 @@ public function incrementLevel(string $level): string
}

/**
* @param array $productData
* @param Country $country
* @param string $locale
* @param Category $category
* @return Product
* @throws PropelException
* @throws \JsonException
*/
private function findOrCreateProduct(array $productData, Country $country, string $locale, Category $category): Product
{
$product = ProductQuery::create()
->useProductI18nQuery()
->filterByTitle($productData[self::TITLE_COLUMN])
->filterByLocale($locale)
->endUse()
->findOne();
if ($product) {
return $product;
// Retrouver le produit à partir de la ref du PSE
if (null !== $pse = ProductSaleElementsQuery::create()
->findOneByRef($productData[self::REF_COLUMN])) {
return $pse->getProduct();
}

// Pas de PSE associé à cette ref. Créer un nouveau produit
$newProduct = $this->dispatchProductEvent(new ProductCreateEvent(), $productData, $locale, $category, $country, true);
Tlog::getInstance()->addInfo('Produit créé : '.$productData[self::TITLE_COLUMN]);

Expand All @@ -311,19 +313,26 @@ private function dispatchProductEvent(ProductCreateEvent|ProductUpdateEvent $eve
->setTaxRuleId($this->findOrCreateTax($productData, $country, $locale)->getId())
->setVisible(1)
->setCurrencyId(1);

if ($event instanceof ProductUpdateEvent) {
$event
->setChapo($productData[self::SHORT_DESCRIPTION_COLUMN])
->setDescription($productData[self::LONG_DESCRIPTION_COLUMN])
->setBrandId($this->findOrCreateBrand($productData, $locale)->getId());
}
$event = $this->dispatcher->dispatch($event, $isNew ? TheliaEvents::PRODUCT_CREATE : TheliaEvents::PRODUCT_UPDATE);

$event = $this->dispatcher->dispatch(
$event,
$isNew ? TheliaEvents::PRODUCT_CREATE : TheliaEvents::PRODUCT_UPDATE
);

/** @var Product $product */
$product = $event->getProduct();
$product->setTemplateId(
$this->findOrCreateTemplate($productData[self::LEVEL1_COLUMN], $locale)
->getId()
);

$product->save();

return $product;
Expand Down Expand Up @@ -576,9 +585,16 @@ private function findOrCreateFeatureAv(string $featureAvTitle, Feature $feature,
private function addImages(Product $product, ProductSaleElements $productSaleElements, array $productData): void
{
$filePath = $productData[self::IMAGE_COLUMN];

if (!$filePath) {
return;
}

if (! file_exists($filePath)) {
Tlog::getInstance()->addWarning('Image not found : ' . $filePath);
return;
}

$fileName = basename($filePath);

$productImage = ProductImageQuery::create()
Expand All @@ -594,7 +610,7 @@ private function addImages(Product $product, ProductSaleElements $productSaleEle
$event->setModel($productImage)
->setUploadedFile($uploadedFile);
$this->dispatcher->dispatch($event, TheliaEvents::IMAGE_SAVE);
$this->addProductSaleElementImage($productSaleElements, $product, $event->getUploadedFile()->getFilename());
$this->addProductSaleElementImage($productSaleElements, $product, $event->getUploadedFile()?->getFilename());
}

private function copyFile(string $filePath): string
Expand Down

0 comments on commit ea56f2d

Please sign in to comment.