p43C
This commit is contained in:
278
src/Commerce/ProductRoleResolver.php
Normal file
278
src/Commerce/ProductRoleResolver.php
Normal file
@@ -0,0 +1,278 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Commerce;
|
||||
|
||||
use App\Commerce\Dto\ShopProductResult;
|
||||
|
||||
final class ProductRoleResolver
|
||||
{
|
||||
public const ROLE_UNKNOWN = 'unknown';
|
||||
public const ROLE_MAIN_DEVICE = 'main_device';
|
||||
public const ROLE_ACCESSORY_OR_CONSUMABLE = 'accessory_or_consumable';
|
||||
public const ROLE_AMBIGUOUS_MIXED = 'ambiguous_mixed_role';
|
||||
|
||||
public const COMPATIBILITY_UNKNOWN = 'unknown';
|
||||
public const COMPATIBILITY_COMPATIBLE = 'compatible';
|
||||
public const COMPATIBILITY_AMBIGUOUS_KEEP_SEPARATE = 'ambiguous_keep_separate';
|
||||
public const COMPATIBILITY_INCOMPATIBLE_ACCESSORY_FOR_MAIN_DEVICE_REQUEST = 'incompatible_accessory_for_main_device_request';
|
||||
public const COMPATIBILITY_INCOMPATIBLE_MAIN_DEVICE_FOR_ACCESSORY_REQUEST = 'incompatible_main_device_for_accessory_request';
|
||||
|
||||
/**
|
||||
* @param string[] $accessoryIntentKeywords
|
||||
* @param string[] $mainDeviceIntentKeywords
|
||||
* @param string[] $directMainDeviceRequestPatterns
|
||||
*/
|
||||
public function resolveRequestedRole(
|
||||
string $prompt,
|
||||
array $accessoryIntentKeywords,
|
||||
array $mainDeviceIntentKeywords,
|
||||
array $directMainDeviceRequestPatterns,
|
||||
callable $normalize
|
||||
): string {
|
||||
$normalized = mb_strtolower($normalize($prompt), 'UTF-8');
|
||||
$hasAccessoryIntent = $this->containsAnyKeyword($normalized, $accessoryIntentKeywords, $normalize, true);
|
||||
$hasMainDeviceIntent = $this->containsAnyKeyword($normalized, $mainDeviceIntentKeywords, $normalize, true);
|
||||
|
||||
if ($hasAccessoryIntent && !$this->matchesAnyPattern($normalized, $directMainDeviceRequestPatterns)) {
|
||||
return self::ROLE_ACCESSORY_OR_CONSUMABLE;
|
||||
}
|
||||
|
||||
if ($hasMainDeviceIntent) {
|
||||
return self::ROLE_MAIN_DEVICE;
|
||||
}
|
||||
|
||||
if ($hasAccessoryIntent) {
|
||||
return self::ROLE_ACCESSORY_OR_CONSUMABLE;
|
||||
}
|
||||
|
||||
return self::ROLE_UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $accessoryKeywords
|
||||
* @param string[] $deviceKeywords
|
||||
*/
|
||||
public function resolveProductRole(
|
||||
ShopProductResult $product,
|
||||
array $accessoryKeywords,
|
||||
array $deviceKeywords,
|
||||
callable $normalize,
|
||||
bool $detectAmbiguousPrimaryRole
|
||||
): string {
|
||||
$primaryRole = $this->resolvePrimaryProductRole(
|
||||
product: $product,
|
||||
accessoryKeywords: $accessoryKeywords,
|
||||
deviceKeywords: $deviceKeywords,
|
||||
normalize: $normalize,
|
||||
detectAmbiguousPrimaryRole: $detectAmbiguousPrimaryRole
|
||||
);
|
||||
|
||||
if ($primaryRole !== self::ROLE_UNKNOWN) {
|
||||
return $primaryRole;
|
||||
}
|
||||
|
||||
$corpus = mb_strtolower($this->buildProductCorpus($product), 'UTF-8');
|
||||
$isAccessory = $this->containsAnyKeyword($corpus, $accessoryKeywords, $normalize, true);
|
||||
$isMainDevice = $this->containsAnyKeyword($corpus, $deviceKeywords, $normalize, true);
|
||||
|
||||
if ($isAccessory) {
|
||||
return self::ROLE_ACCESSORY_OR_CONSUMABLE;
|
||||
}
|
||||
|
||||
if ($isMainDevice) {
|
||||
return self::ROLE_MAIN_DEVICE;
|
||||
}
|
||||
|
||||
return self::ROLE_UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $accessoryKeywords
|
||||
* @param string[] $deviceKeywords
|
||||
*/
|
||||
public function resolvePrimaryProductRole(
|
||||
ShopProductResult $product,
|
||||
array $accessoryKeywords,
|
||||
array $deviceKeywords,
|
||||
callable $normalize,
|
||||
bool $detectAmbiguousPrimaryRole
|
||||
): string {
|
||||
$primaryText = mb_strtolower($normalize($this->buildPrimaryProductIdentity($product)), 'UTF-8');
|
||||
|
||||
if ($primaryText === '') {
|
||||
return self::ROLE_UNKNOWN;
|
||||
}
|
||||
|
||||
$isAccessory = $this->containsAnyKeyword($primaryText, $accessoryKeywords, $normalize, true);
|
||||
$isMainDevice = $this->containsAnyKeyword($primaryText, $deviceKeywords, $normalize, true);
|
||||
|
||||
if ($detectAmbiguousPrimaryRole) {
|
||||
if ($isAccessory && !$isMainDevice) {
|
||||
return self::ROLE_ACCESSORY_OR_CONSUMABLE;
|
||||
}
|
||||
|
||||
if ($isMainDevice && !$isAccessory) {
|
||||
return self::ROLE_MAIN_DEVICE;
|
||||
}
|
||||
|
||||
if ($isAccessory && $isMainDevice) {
|
||||
return self::ROLE_AMBIGUOUS_MIXED;
|
||||
}
|
||||
|
||||
return self::ROLE_UNKNOWN;
|
||||
}
|
||||
|
||||
if ($isAccessory) {
|
||||
return self::ROLE_ACCESSORY_OR_CONSUMABLE;
|
||||
}
|
||||
|
||||
if ($isMainDevice) {
|
||||
return self::ROLE_MAIN_DEVICE;
|
||||
}
|
||||
|
||||
return self::ROLE_UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $accessoryKeywords
|
||||
* @param string[] $deviceKeywords
|
||||
*/
|
||||
public function isAccessoryLikeProduct(
|
||||
ShopProductResult $product,
|
||||
array $accessoryKeywords,
|
||||
array $deviceKeywords,
|
||||
callable $normalize
|
||||
): bool {
|
||||
$primaryRole = $this->resolvePrimaryProductRole(
|
||||
product: $product,
|
||||
accessoryKeywords: $accessoryKeywords,
|
||||
deviceKeywords: $deviceKeywords,
|
||||
normalize: $normalize,
|
||||
detectAmbiguousPrimaryRole: true
|
||||
);
|
||||
|
||||
if ($primaryRole === self::ROLE_ACCESSORY_OR_CONSUMABLE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($primaryRole === self::ROLE_MAIN_DEVICE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->containsAnyKeyword(
|
||||
$normalize($this->buildProductCorpus($product)),
|
||||
$accessoryKeywords,
|
||||
$normalize,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $accessoryKeywords
|
||||
* @param string[] $deviceKeywords
|
||||
*/
|
||||
public function isDeviceLikeProduct(
|
||||
ShopProductResult $product,
|
||||
array $accessoryKeywords,
|
||||
array $deviceKeywords,
|
||||
callable $normalize
|
||||
): bool {
|
||||
$primaryRole = $this->resolvePrimaryProductRole(
|
||||
product: $product,
|
||||
accessoryKeywords: $accessoryKeywords,
|
||||
deviceKeywords: $deviceKeywords,
|
||||
normalize: $normalize,
|
||||
detectAmbiguousPrimaryRole: true
|
||||
);
|
||||
|
||||
if ($primaryRole === self::ROLE_MAIN_DEVICE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($primaryRole === self::ROLE_ACCESSORY_OR_CONSUMABLE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->containsAnyKeyword(
|
||||
$normalize($this->buildProductCorpus($product)),
|
||||
$deviceKeywords,
|
||||
$normalize,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
public function resolveCompatibility(string $requestedRole, string $inferredRole): string
|
||||
{
|
||||
if ($requestedRole === self::ROLE_UNKNOWN || $inferredRole === self::ROLE_UNKNOWN) {
|
||||
return self::COMPATIBILITY_UNKNOWN;
|
||||
}
|
||||
|
||||
if ($requestedRole === self::ROLE_MAIN_DEVICE && $inferredRole === self::ROLE_ACCESSORY_OR_CONSUMABLE) {
|
||||
return self::COMPATIBILITY_INCOMPATIBLE_ACCESSORY_FOR_MAIN_DEVICE_REQUEST;
|
||||
}
|
||||
|
||||
if ($requestedRole === self::ROLE_ACCESSORY_OR_CONSUMABLE && $inferredRole === self::ROLE_MAIN_DEVICE) {
|
||||
return self::COMPATIBILITY_INCOMPATIBLE_MAIN_DEVICE_FOR_ACCESSORY_REQUEST;
|
||||
}
|
||||
|
||||
if ($inferredRole === self::ROLE_AMBIGUOUS_MIXED) {
|
||||
return self::COMPATIBILITY_AMBIGUOUS_KEEP_SEPARATE;
|
||||
}
|
||||
|
||||
return self::COMPATIBILITY_COMPATIBLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $keywords
|
||||
*/
|
||||
private function containsAnyKeyword(string $text, array $keywords, callable $normalize, bool $normalizeKeyword): bool
|
||||
{
|
||||
foreach ($keywords as $keyword) {
|
||||
$keyword = (string) $keyword;
|
||||
$candidate = $normalizeKeyword ? mb_strtolower($normalize($keyword), 'UTF-8') : $normalize($keyword);
|
||||
|
||||
if ($candidate !== '' && str_contains($text, $candidate)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $patterns
|
||||
*/
|
||||
private function matchesAnyPattern(string $text, array $patterns): bool
|
||||
{
|
||||
foreach ($patterns as $pattern) {
|
||||
if (preg_match((string) $pattern, $text) === 1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function buildPrimaryProductIdentity(ShopProductResult $product): string
|
||||
{
|
||||
return implode(' ', array_filter([
|
||||
$product->name,
|
||||
$product->url,
|
||||
]));
|
||||
}
|
||||
|
||||
private function buildProductCorpus(ShopProductResult $product): string
|
||||
{
|
||||
return implode(' ', array_filter([
|
||||
$product->name,
|
||||
$product->productNumber,
|
||||
$product->manufacturer,
|
||||
implode(' ', $product->highlights),
|
||||
$product->description,
|
||||
$product->customFields,
|
||||
$product->url,
|
||||
]));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user