add shopware store-api

This commit is contained in:
team 1
2026-04-09 12:00:34 +02:00
parent 0ef3b43b30
commit 1aee32f1d8
13 changed files with 992 additions and 10 deletions

View File

@@ -0,0 +1,267 @@
<?php
declare(strict_types=1);
namespace App\Commerce;
use App\Commerce\Dto\CommerceSearchQuery;
final class CommerceQueryParser
{
/**
* @var string[]
*/
private array $knownColors = [
'schwarz',
'weiß',
'weiss',
'rot',
'blau',
'grün',
'gruen',
'gelb',
'grau',
'beige',
'rosa',
'pink',
'orange',
'braun',
];
/**
* @var string[]
*/
private array $knownCategories = [
'sneaker',
'schuhe',
'hoodie',
't-shirt',
'shirt',
'jacke',
'regenjacke',
'trinkflasche',
'flasche',
'rucksack',
'tasche',
'mütze',
'muetze',
'kappe',
'hose',
'pullover',
];
/**
* @var string[]
*/
private array $knownBrands = [
'nike',
'adidas',
'puma',
'reebok',
'under armour',
'new balance',
];
public function parse(string $originalPrompt, string $intent): CommerceSearchQuery
{
$normalized = $this->normalize($originalPrompt);
[$priceMin, $priceMax] = $this->extractPriceRange($normalized);
$sizes = $this->extractSizes($normalized);
$colors = $this->extractColors($normalized);
$brand = $this->extractBrand($normalized);
$category = $this->extractCategory($normalized);
$properties = [];
$searchText = $this->buildSearchText(
$normalized,
$colors,
$sizes,
$brand,
$priceMin,
$priceMax
);
return new CommerceSearchQuery(
originalPrompt: $originalPrompt,
normalizedPrompt: $normalized,
searchText: $searchText !== '' ? $searchText : $normalized,
category: $category,
brand: $brand,
colors: $colors,
sizes: $sizes,
properties: $properties,
priceMin: $priceMin,
priceMax: $priceMax,
intent: $intent,
needsLlmFallback: false,
);
}
private function normalize(string $prompt): string
{
$value = mb_strtolower(trim($prompt));
$value = str_replace(['€'], ' euro ', $value);
$value = preg_replace('/[^\p{L}\p{N}\s.,\-]/u', ' ', $value) ?? $value;
$value = preg_replace('/\s+/u', ' ', $value) ?? $value;
return trim($value);
}
/**
* @return array{0:?float,1:?float}
*/
private function extractPriceRange(string $prompt): array
{
$priceMin = 10;
$priceMax = null;
if (preg_match('/\bzwischen\s+(\d+(?:[.,]\d+)?)\s+und\s+(\d+(?:[.,]\d+)?)\s+euro\b/u', $prompt, $m) === 1) {
$a = $this->toFloat($m[1]);
$b = $this->toFloat($m[2]);
if ($a !== null && $b !== null) {
return [min($a, $b), max($a, $b)];
}
}
if (preg_match('/\b(?:unter|bis|max(?:imal)?)\s+(\d+(?:[.,]\d+)?)\s+euro\b/u', $prompt, $m) === 1) {
$priceMax = $this->toFloat($m[1]);
}
if (preg_match('/\b(?:ab|mindestens|min)\s+(\d+(?:[.,]\d+)?)\s+euro\b/u', $prompt, $m) === 1) {
$priceMin = $this->toFloat($m[1]);
}
return [$priceMin, $priceMax];
}
/**
* @return string[]
*/
private function extractSizes(string $prompt): array
{
$sizes = [];
if (preg_match_all('/\b(?:größe|groesse|grösse)\s*([a-z0-9.-]+)\b/u', $prompt, $matches) === false) {
return [];
}
foreach ($matches[1] as $size) {
$sizes[] = trim($size);
}
if (preg_match_all('/\b(xs|s|m|l|xl|xxl|xxxl)\b/u', $prompt, $tokenMatches) !== false) {
foreach ($tokenMatches[1] as $sizeToken) {
$sizes[] = trim($sizeToken);
}
}
return array_values(array_unique(array_filter($sizes, static fn ($v) => $v !== '')));
}
/**
* @return string[]
*/
private function extractColors(string $prompt): array
{
$colors = [];
foreach ($this->knownColors as $color) {
if (preg_match('/\b' . preg_quote($color, '/') . '\b/u', $prompt) === 1) {
$colors[] = $color;
}
}
return array_values(array_unique($colors));
}
private function extractBrand(string $prompt): ?string
{
foreach ($this->knownBrands as $brand) {
if (str_contains($prompt, $brand)) {
return $brand;
}
}
if (preg_match('/\bmarke\s+([a-z0-9][a-z0-9\s\-]+)/u', $prompt, $m) === 1) {
return trim($m[1]);
}
return null;
}
private function extractCategory(string $prompt): ?string
{
foreach ($this->knownCategories as $category) {
if (preg_match('/\b' . preg_quote($category, '/') . '\b/u', $prompt) === 1) {
return $category;
}
}
return null;
}
private function buildSearchText(
string $prompt,
array $colors,
array $sizes,
?string $brand,
?float $priceMin,
?float $priceMax
): string {
$text = ' ' . $prompt . ' ';
$phrasesToRemove = [
'ich suche',
'suche',
'habt ihr',
'gibt es',
'zeige mir',
'welches gerät',
'welche gerät',
'welches modell',
'welches ist besser',
'welches ist am besten',
'alternative',
'alternativen',
];
foreach ($phrasesToRemove as $phrase) {
$text = str_replace($phrase, ' ', $text);
}
foreach ($colors as $color) {
$text = preg_replace('/\b' . preg_quote($color, '/') . '\b/u', ' ', $text) ?? $text;
}
foreach ($sizes as $size) {
$text = preg_replace('/\b' . preg_quote($size, '/') . '\b/u', ' ', $text) ?? $text;
}
if ($brand !== null && $brand !== '') {
$text = str_replace($brand, ' ', $text);
}
if ($priceMin !== null || $priceMax !== null) {
if ($priceMin !== null || $priceMax !== null) {
$text = preg_replace('/\bzwischen\s+\d+(?:[.,]\d+)?\s+und\s+\d+(?:[.,]\d+)?\s*euro\b/u', ' ', $text) ?? $text;
$text = preg_replace('/\b(?:unter|bis|max(?:imal)?|ab|mindestens|min)\s+\d+(?:[.,]\d+)?\s*euro\b/u', ' ', $text) ?? $text;
$text = preg_replace('/\beuro\b/u', ' ', $text) ?? $text;
}
}
$text = preg_replace('/\s+/u', ' ', $text) ?? $text;
$text = trim($text, " \t\n\r\0\x0B-.,");
$tokens = array_filter(explode(' ', $text), static fn (string $token): bool => mb_strlen($token) > 1);
return trim(implode(' ', $tokens));
}
private function toFloat(string $value): ?float
{
$value = str_replace(',', '.', trim($value));
return is_numeric($value) ? (float) $value : null;
}
}