addArgument('file', InputArgument::REQUIRED, 'Chemin absolu vers le fichier XLSX') ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Simule sans persister en BDD') ; } protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $file = (string) $input->getArgument('file'); $dryRun = (bool) $input->getOption('dry-run'); if (!file_exists($file)) { $io->error(sprintf('Fichier introuvable : %s', $file)); return Command::FAILURE; } $io->title('Feed bovins depuis '.basename($file)); if ($dryRun) { $io->warning('Dry-run activé : aucune écriture en BDD.'); } try { $spreadsheet = IOFactory::load($file); } catch (Throwable $e) { $io->error('Impossible de lire le fichier : '.$e->getMessage()); return Command::FAILURE; } $sheet = $spreadsheet->getActiveSheet(); $highestRow = $sheet->getHighestRow(); // Pré-chargement des fournisseurs pour des lookups rapides (insensible casse). $supplierByName = []; foreach ($this->em->getRepository(Supplier::class)->findAll() as $supplier) { $supplierByName[mb_strtoupper($supplier->getName())] = $supplier; } // Pré-chargement des bâtiments par code (insensible casse). $buildingByCode = []; foreach ($this->em->getRepository(Building::class)->findAll() as $building) { $buildingByCode[mb_strtoupper($building->getCode())] = $building; } $bovineRepo = $this->em->getRepository(Bovine::class); $stats = [ 'total' => 0, 'updated' => 0, 'notFound' => 0, 'invalid' => 0, 'supplierMissing' => 0, 'buildingMissing' => 0, ]; $missingNationalNumbers = []; $missingSuppliers = []; $missingBuildings = []; $io->progressStart($highestRow); for ($row = 1; $row <= $highestRow; ++$row) { ++$stats['total']; $rawNationalNumber = (string) ($sheet->getCell([1, $row])->getValue() ?? ''); $rawSupplier = (string) ($sheet->getCell([2, $row])->getValue() ?? ''); $rawWeight = $sheet->getCell([3, $row])->getValue(); $rawPrice = $sheet->getCell([4, $row])->getValue(); $rawBuilding = (string) ($sheet->getCell([5, $row])->getValue() ?? ''); $rawNationalNumber = trim($rawNationalNumber); if ('' === $rawNationalNumber) { ++$stats['invalid']; $io->progressAdvance(); continue; } // Garde : strip "FR" + espace optionnel uniquement s'il est présent. $nationalNumber = preg_replace('/^FR\s*/i', '', $rawNationalNumber); $bovine = $bovineRepo->findOneBy(['nationalNumber' => $nationalNumber]); if (null === $bovine) { ++$stats['notFound']; $missingNationalNumbers[] = $nationalNumber; $io->progressAdvance(); continue; } // Lookup supplier (peut être null si introuvable ou colonne vide). $supplier = null; $supplierName = mb_strtoupper(trim($rawSupplier)); if ('' !== $supplierName) { $supplier = $supplierByName[$supplierName] ?? null; if (null === $supplier) { ++$stats['supplierMissing']; $missingSuppliers[$supplierName] = ($missingSuppliers[$supplierName] ?? 0) + 1; } } $weight = is_numeric($rawWeight) ? (int) $rawWeight : null; $price = is_numeric($rawPrice) ? (float) $rawPrice : null; if (null !== $weight) { $bovine->setReceivedWeight($weight); } if (null !== $price) { $bovine->setPricePerKg($price); } $bovine->setSupplier($supplier); // Bâtiment direct : on n'écrase pas une affectation à une case existante. $buildingCode = mb_strtoupper(trim($rawBuilding)); if ('' !== $buildingCode && null === $bovine->getBuildingCase()) { $building = $buildingByCode[$buildingCode] ?? null; if (null !== $building) { $bovine->setBuilding($building); } else { ++$stats['buildingMissing']; $missingBuildings[$buildingCode] = ($missingBuildings[$buildingCode] ?? 0) + 1; } } ++$stats['updated']; $io->progressAdvance(); } $io->progressFinish(); if (!$dryRun) { $this->em->flush(); } $io->section('Résultats'); $io->table( ['Métrique', 'Valeur'], [ ['Lignes totales', $stats['total']], ['Bovins mis à jour', $stats['updated']], ['Bovins introuvables', $stats['notFound']], ['Lignes invalides', $stats['invalid']], ['Fournisseurs introuvables (supplier=null)', $stats['supplierMissing']], ['Bâtiments introuvables (building non set)', $stats['buildingMissing']], ] ); if ([] !== $missingNationalNumbers) { $preview = array_slice($missingNationalNumbers, 0, 10); $io->warning(sprintf( '%d bovin(s) introuvable(s). Aperçu : %s%s', count($missingNationalNumbers), implode(', ', $preview), count($missingNationalNumbers) > 10 ? '…' : '', )); } if ([] !== $missingSuppliers) { $list = []; foreach ($missingSuppliers as $name => $count) { $list[] = sprintf('%s (%d)', $name, $count); } $io->warning('Fournisseurs introuvables (bovins rattachés en null) : '.implode(', ', $list)); } if ([] !== $missingBuildings) { $list = []; foreach ($missingBuildings as $code => $count) { $list[] = sprintf('%s (%d)', $code, $count); } $io->warning('Bâtiments introuvables (champ non renseigné) : '.implode(', ', $list)); } if ($dryRun) { $io->success('Dry-run terminé. Relance sans --dry-run pour persister.'); } else { $io->success('Feed terminé avec succès.'); } return Command::SUCCESS; } }