logger->debug("Fetching {$action} from Arcadia API"); try { $incoId = new IncoId((string) $noArt); } catch (\DomainException $e) { $this->logger->warning("Invalid IncoId format for #{$noArt}: {$e->getMessage()}"); return null; } $inco = $this->incoService->findBy($incoId); if ($inco instanceof ApiError) { $this->logger->error("Failed to fetch {$action}: {$inco->message()}"); throw new \RuntimeException($inco->message()); } if (! ($inco instanceof LibInco)) { $this->logger->info("{$action} not found on Arcadia API (404)"); return null; } $compo = $this->incoService->findCompo($incoId); if ($compo instanceof ApiError) { $this->logger->warning("Failed to fetch composition for {$action}: {$compo->message()}"); $compo = null; } return $this->mapToDataDto($inco, $compo instanceof LibIncoCompo ? $compo : null); } public function getPreview(int $noArt): ?ArcadiaIncoPreviewDto { $data = $this->fetchInco($noArt); if ($data === null) { return null; } return new ArcadiaIncoPreviewDto( noArt: $data->noArt, name: $data->name, libelle: $data->libelle, brand: $data->brand, nutriscore: $data->nutriscore?->value, servings: $data->servings, allergens: $data->allergens, ingredients: $data->ingredients, preparation: $data->preparation, energyKcal: $data->nutrition->kcal, energyKj: $data->nutrition->kj, fat: $data->nutrition->fatG, satFat: $data->nutrition->satFatG, carbs: $data->nutrition->carbG, sugar: $data->nutrition->sugarG, fiber: $data->nutrition->fiberG, protein: $data->nutrition->proteinG, salt: $data->nutrition->saltG, ); } private function mapToDataDto(LibInco $inco, ?LibIncoCompo $compo): ArcadiaIncoDataDto { $ingredientsHtml = $this->buildIngredientsHtml($inco); $preparationHtml = ! empty($inco->preparationAdvices) ? '
' . nl2br(e(trim($inco->preparationAdvices))) . '
' : null; $sellingArgumentHtml = ! empty($inco->sellArguments) ? '' . nl2br(e(trim($inco->sellArguments))) . '
' : null; $servings = $inco->portions !== null ? (string) $inco->portions : null; if ($servings !== null && mb_strlen($servings) > 20) { $servings = mb_substr($servings, 0, 20); } $allergens = ! empty($inco->allergens) ? implode(', ', array_map(fn ($a) => $a->label, $inco->allergens)) : null; $nutriscore = $this->resolveNutriscore($inco, $compo); $nutrition = $compo !== null ? $this->mapNutrition($compo) : new NutritionalValues(); return new ArcadiaIncoDataDto( noArt: (int) $inco->id->value, name: $inco->name, libelle: $inco->libelle, brand: $inco->brand->label !== '' ? $inco->brand->label : $inco->brand->code->value, ingredients: $ingredientsHtml, preparation: $preparationHtml, servings: $servings, allergens: $allergens, sellingArgument: $sellingArgumentHtml, nutriscore: $nutriscore, nutriscoreArgs: null, nutrition: $nutrition, ); } private function buildIngredientsHtml(LibInco $inco): ?string { if ($inco->ingredients->isEmpty()) { return null; } $paragraphs = []; foreach ($inco->ingredients->groups as $title => $lines) { $text = implode(', ', $lines); if (is_string($title) && ! is_numeric($title) && $title !== '') { $paragraphs[] = '' . e($title) . ' : ' . e($text) . '
'; } else { $paragraphs[] = '' . e($text) . '
'; } } return implode('', $paragraphs); } private function resolveNutriscore(LibInco $inco, ?LibIncoCompo $compo): ?Nutriscore { $nutriscoreStr = $inco->nutriscore?->grade; if (empty($nutriscoreStr) && $compo !== null && ! empty($compo->nutriScore->classement)) { $nutriscoreStr = $compo->nutriScore->classement; } return ! empty($nutriscoreStr) ? Nutriscore::tryFrom(mb_strtoupper(trim($nutriscoreStr))) : null; } private function mapNutrition(LibIncoCompo $compo): NutritionalValues { $parseFloat = static function (?string $val): ?float { if ($val === null || trim($val) === '' || ! is_numeric(trim($val))) { return null; } $floatVal = (float) trim($val); return $floatVal >= 0 ? $floatVal : null; }; return new NutritionalValues( refWeightG: $parseFloat($compo->energie->tailleReference), kj: $parseFloat($compo->energie->valEnergKjReference), kcal: $parseFloat($compo->energie->valEnergKcalReference), fatG: $parseFloat($compo->macronutrimentsRef->matGrassReference), satFatG: $parseFloat($compo->macronutrimentsRef->acideGrasSatureReference), carbG: $parseFloat($compo->macronutrimentsRef->glucidesReference), sugarG: $parseFloat($compo->macronutrimentsRef->sucreReference), fiberG: $parseFloat($compo->macronutrimentsRef->fibAlimReference), proteinG: $parseFloat($compo->macronutrimentsRef->proteineReference), saltG: $parseFloat($compo->macronutrimentsRef->selReference), servingSizeG: $parseFloat($compo->energie->taillePortion), servingKj: $parseFloat($compo->energie->valEnergKjPortion), servingKcal: $parseFloat($compo->energie->valEnergKcalPortion), servingFatG: $parseFloat($compo->macronutrimentsPortion->matGrassPortion), servingSatFatG: $parseFloat($compo->macronutrimentsPortion->acideGrasSaturePortion), servingCarbG: $parseFloat($compo->macronutrimentsPortion->glucidesPortion), servingSugarG: $parseFloat($compo->macronutrimentsPortion->sucrePortion), servingFiberG: $parseFloat($compo->macronutrimentsPortion->fibAlimPortion), servingProteinG: $parseFloat($compo->macronutrimentsPortion->proteinePortion), servingSaltG: $parseFloat($compo->macronutrimentsPortion->selPortion), dailyEnergy: $parseFloat($compo->apportsReference->pourcentApportRefKjoP), dailyFat: $parseFloat($compo->apportsReference->pourcentApportRefFatP), dailySatFat: $parseFloat($compo->apportsReference->pourcentApportRefFatsatP), dailyCarb: $parseFloat($compo->apportsReference->pourcentApportRefChovalP), dailySugar: $parseFloat($compo->apportsReference->pourcentApportRefSugarP), dailyFiber: null, dailyProtein: $parseFloat($compo->apportsReference->pourcentApportRefProP), dailySalt: $parseFloat($compo->apportsReference->pourcentApportRefSalteqP), ); } }