rules = $rules === [] ? $this->defaultRules() : array_values(array_filter($rules, static fn ($rule): bool => $rule instanceof QualityRule)); if ($this->rules === []) throw new \InvalidArgumentException('At least one quality rule is required.'); } public function evaluate(UniversalProductInterface $product): QualityProfile { $values = $this->values($product); $byDimension = []; $recommendations = []; $missing = []; $critical = []; $warnings = []; $weightedTotal = 0.0; $totalWeight = array_sum(array_map(static fn (QualityRule $rule): float => $rule->getWeight(), $this->rules)); foreach ($this->rules as $rule) { $evidence = $rule->evaluate($values[$rule->getField()] ?? null); $byDimension[$rule->getDimension()][] = ['rule' => $rule, 'evidence' => $evidence]; $weightedTotal += $evidence->getScore() * $rule->getWeight(); if ($evidence->isSatisfied()) continue; $missing[] = $rule->getField(); $message = $evidence->getReason(); if ($rule->isCritical()) $critical[] = $message; else $warnings[] = $message; $priority = $rule->isCritical() ? QualityRecommendation::CRITICAL : ($rule->getWeight() >= 4 ? QualityRecommendation::HIGH : QualityRecommendation::MEDIUM); $recommendations[] = new QualityRecommendation($rule->getField(), $priority, $rule->getImpact(), ($rule->getWeight() / max(1.0, $totalWeight)) * (100 - $evidence->getScore()), $message); } $dimensions = []; $strengths = []; foreach (QualityDimension::ALL as $name) { $items = $byDimension[$name] ?? []; $sum = 0.0; $weight = 0.0; $evidence = []; foreach ($items as $item) { $sum += $item['evidence']->getScore() * $item['rule']->getWeight(); $weight += $item['rule']->getWeight(); $evidence[] = $item['evidence']; } $dimension = new QualityDimension($name, new QualityScore($weight > 0 ? $sum / $weight : 0), $evidence); $dimensions[$name] = $dimension; if ($dimension->getScore()->getValue() >= 80) $strengths[] = $name; } usort($recommendations, static fn (QualityRecommendation $left, QualityRecommendation $right): int => $right->getEstimatedImprovement() <=> $left->getEstimatedImprovement()); return new QualityProfile(new QualityScore($totalWeight > 0 ? $weightedTotal / $totalWeight : 0), $dimensions, $recommendations, $missing, $critical, $warnings, $strengths); } private function defaultRules(): array { return [ new QualityRule(QualityDimension::IDENTITY, 'id', 5, QualityRule::PRESENT, 1, true, 'Enables stable product identity'), new QualityRule(QualityDimension::IDENTITY, 'title', 6, QualityRule::MIN_LENGTH, 3, true, 'Enables product recognition'), new QualityRule(QualityDimension::IDENTITY, 'domain', 3), new QualityRule(QualityDimension::IDENTIFIERS, 'identifiers', 6, QualityRule::MIN_COUNT, 1, true, 'Improves provider and marketplace matching'), new QualityRule(QualityDimension::IDENTIFIERS, 'gtin', 3, QualityRule::PRESENT, 1, false, 'Improves universal product matching'), new QualityRule(QualityDimension::METADATA, 'metadata', 3, QualityRule::MIN_COUNT, 4), new QualityRule(QualityDimension::COMMERCIAL, 'price', 5, QualityRule::PRESENT, 1, true, 'Enables commercial use'), new QualityRule(QualityDimension::COMMERCIAL, 'currency', 3), new QualityRule(QualityDimension::SEO, 'seo_title', 3, QualityRule::MIN_LENGTH, 10), new QualityRule(QualityDimension::SEO, 'meta_description', 4, QualityRule::MIN_LENGTH, 50, false, 'Improves search visibility'), new QualityRule(QualityDimension::SEO, 'keywords', 2, QualityRule::MIN_COUNT, 2), new QualityRule(QualityDimension::SEO, 'slug', 2), new QualityRule(QualityDimension::MEDIA, 'gallery', 2, QualityRule::MIN_COUNT, 1), new QualityRule(QualityDimension::ATTRIBUTES, 'attributes', 4, QualityRule::MIN_COUNT, 3, false, 'Improves product detail'), new QualityRule(QualityDimension::SPECIFICATIONS, 'specifications', 3, QualityRule::MIN_COUNT, 1), new QualityRule(QualityDimension::BRAND, 'brand', 4, QualityRule::PRESENT, 1, false, 'Improves trust and discoverability'), new QualityRule(QualityDimension::MANUFACTURER, 'manufacturer', 3), new QualityRule(QualityDimension::CATEGORIES, 'categories', 5, QualityRule::MIN_COUNT, 1, true, 'Enables product classification'), new QualityRule(QualityDimension::IMAGES, 'images', 6, QualityRule::MIN_COUNT, 1, true, 'Enables a commercially usable listing'), new QualityRule(QualityDimension::DESCRIPTIONS, 'description', 5, QualityRule::MIN_LENGTH, 100, false, 'Improves conversion and SEO'), new QualityRule(QualityDimension::DESCRIPTIONS, 'short_description', 2, QualityRule::MIN_LENGTH, 30), new QualityRule(QualityDimension::LOCALIZATION, 'languages', 2, QualityRule::MIN_COUNT, 1), new QualityRule(QualityDimension::LOCALIZATION, 'countries', 1, QualityRule::MIN_COUNT, 1), new QualityRule(QualityDimension::PUBLICATION_READINESS, 'publication_targets', 4, QualityRule::MIN_COUNT, 1, false, 'Prepares channel publication'), new QualityRule(QualityDimension::SPECIFICATIONS, 'dimensions', 2, QualityRule::MIN_COUNT, 1, false, 'Improves logistics and marketplace validation'), ]; } private function values(UniversalProductInterface $product): array { $metadata = $this->unwrapMap($product->getMetadata()->getFields()); $identifiers = $this->unwrapMap($product->getIdentifiers()->getAll()); $gtin = null; foreach (['GTIN', 'EAN13', 'EAN8', 'UPC-A', 'UPC', 'ISBN13'] as $type) if (isset($identifiers[$type])) { $gtin = $identifiers[$type]; break; } return array_merge($metadata, [ 'id' => $product->getIdentity()->getId()->getCurrentValue(), 'title' => $product->getIdentity()->getTitle()->getCurrentValue(), 'domain' => $product->getIdentity()->getDomain()->getCurrentValue(), 'identifiers' => $identifiers, 'gtin' => $gtin, 'metadata' => array_filter($metadata, static fn ($value): bool => $value !== null && $value !== '' && $value !== []), 'price' => $this->value($product->getCommercial()->get('price')), 'currency' => $this->value($product->getCommercial()->get('currency')), 'seo_title' => $this->value($product->getSeo()->getTitle()), 'meta_description' => $this->value($product->getSeo()->getMetaDescription()), 'keywords' => $this->value($product->getSeo()->getKeywords()), 'slug' => $this->value($product->getSeo()->getSlug()), 'images' => $product->getMedia()->getImages()->getCurrentValue(), 'gallery' => $product->getMedia()->getGallery()->getCurrentValue(), 'dimensions' => $product->getDimensions()->getDimensions()->getCurrentValue(), 'publication_targets' => $product->getPublication()->getTargets(), ]); } private function unwrapMap(array $versions): array { $values = []; foreach ($versions as $name => $version) if ($version instanceof ProductVersion) $values[$name] = $version->getCurrentValue(); return $values; } private function value(?ProductVersion $version) { return $version === null ? null : $version->getCurrentValue(); } }