Files
michaelschiemer/src/Application/Search/SearchRequest.php
Michael Schiemer 55a330b223 Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
2025-08-11 20:13:26 +02:00

225 lines
7.3 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Application\Search;
use App\Framework\Exception\ErrorCode;
use App\Framework\Exception\FrameworkException;
use App\Framework\Http\Request;
/**
* Represents a search request with validation
*/
final readonly class SearchRequest
{
/**
* @param array<string, array> $filters
* @param array<string, float> $boosts
* @param array<string> $fields
* @param array<string> $highlight
*/
public function __construct(
public string $query,
public array $filters = [],
public array $boosts = [],
public array $fields = [],
public array $highlight = [],
public int $limit = 20,
public int $offset = 0,
public ?string $sortBy = null,
public string $sortDirection = 'asc',
public bool $sortByRelevance = true,
public bool $enableHighlighting = true,
public bool $enableFuzzy = false,
public float $minScore = 0.0
) {
}
public static function fromHttpRequest(Request $request): self
{
$query = $request->query;
// Parse search query
$searchQuery = $query->get('q', '*');
// Parse filters
$filters = [];
if ($query->has('filters')) {
$filtersParam = $query->get('filters');
if (is_string($filtersParam)) {
$filters = json_decode($filtersParam, true) ?? [];
} elseif (is_array($filtersParam)) {
$filters = $filtersParam;
}
}
// Parse individual filter parameters (filter[field][type]=value)
foreach ($query->toArray() as $key => $value) {
if (preg_match('/^filter\[([^]]+)](?:\[([^]]+)])?$/', $key, $matches)) {
$field = $matches[1];
$type = $matches[2] ?? 'equals';
if (! isset($filters[$field])) {
$filters[$field] = [];
}
$filters[$field]['type'] = $type;
$filters[$field]['value'] = $value;
}
}
// Parse boosts
$boosts = [];
if ($query->has('boosts')) {
$boostsParam = $query->get('boosts');
if (is_string($boostsParam)) {
$boosts = json_decode($boostsParam, true) ?? [];
} elseif (is_array($boostsParam)) {
$boosts = $boostsParam;
}
}
// Parse fields restriction
$fields = [];
if ($query->has('fields')) {
$fieldsParam = $query->get('fields');
if (is_string($fieldsParam)) {
$fields = array_map('trim', explode(',', $fieldsParam));
} elseif (is_array($fieldsParam)) {
$fields = $fieldsParam;
}
}
// Parse highlight fields
$highlight = [];
if ($query->has('highlight')) {
$highlightParam = $query->get('highlight');
if (is_string($highlightParam)) {
$highlight = array_map('trim', explode(',', $highlightParam));
} elseif (is_array($highlightParam)) {
$highlight = $highlightParam;
}
} elseif ($query->getBool('enable_highlighting', true)) {
// Default highlighting for text fields
$highlight = ['title', 'content', 'description'];
}
// Parse pagination
$limit = min(100, max(1, $query->getInt('limit', 20)));
$offset = max(0, $query->getInt('offset', 0));
// Alternative pagination via page parameter
if ($query->has('page')) {
$page = max(1, $query->getInt('page', 1));
$perPage = min(100, max(1, $query->getInt('per_page', 20)));
$offset = ($page - 1) * $perPage;
$limit = $perPage;
}
// Parse sorting
$sortBy = $query->get('sort_by');
$sortDirection = strtolower($query->get('sort_direction', 'asc'));
$sortByRelevance = $query->getBool('sort_by_relevance', ! $sortBy);
// Validate sort direction
if (! in_array($sortDirection, ['asc', 'desc'])) {
throw FrameworkException::create(
ErrorCode::VAL_BUSINESS_RULE_VIOLATION,
'Invalid sort direction. Must be "asc" or "desc"'
);
}
// Parse advanced options
$enableHighlighting = $query->getBool('enable_highlighting', true);
$enableFuzzy = $query->getBool('enable_fuzzy', false);
$minScore = max(0.0, $query->getFloat('min_score', 0.0));
return new self(
query: $searchQuery,
filters: $filters,
boosts: $boosts,
fields: $fields,
highlight: $highlight,
limit: $limit,
offset: $offset,
sortBy: $sortBy,
sortDirection: $sortDirection,
sortByRelevance: $sortByRelevance,
enableHighlighting: $enableHighlighting,
enableFuzzy: $enableFuzzy,
minScore: $minScore
);
}
public function validate(): void
{
if (empty(trim($this->query))) {
throw FrameworkException::create(
ErrorCode::VAL_BUSINESS_RULE_VIOLATION,
'Search query cannot be empty'
);
}
if ($this->limit < 1 || $this->limit > 100) {
throw FrameworkException::create(
ErrorCode::VAL_BUSINESS_RULE_VIOLATION,
'Limit must be between 1 and 100'
);
}
if ($this->offset < 0) {
throw FrameworkException::create(
ErrorCode::VAL_BUSINESS_RULE_VIOLATION,
'Offset cannot be negative'
);
}
if ($this->minScore < 0) {
throw FrameworkException::create(
ErrorCode::VAL_BUSINESS_RULE_VIOLATION,
'Minimum score cannot be negative'
);
}
// Validate filters
foreach ($this->filters as $field => $filterData) {
if (! is_array($filterData) || ! isset($filterData['type'])) {
throw FrameworkException::create(
ErrorCode::VAL_BUSINESS_RULE_VIOLATION,
"Invalid filter format for field '{$field}'"
);
}
}
// Validate boosts
foreach ($this->boosts as $field => $boost) {
if (! is_numeric($boost) || $boost < 0) {
throw FrameworkException::create(
ErrorCode::VAL_BUSINESS_RULE_VIOLATION,
"Invalid boost value for field '{$field}'. Must be a positive number"
);
}
}
}
public function toArray(): array
{
return [
'query' => $this->query,
'filters' => $this->filters,
'boosts' => $this->boosts,
'fields' => $this->fields,
'highlight' => $this->highlight,
'limit' => $this->limit,
'offset' => $this->offset,
'sort_by' => $this->sortBy,
'sort_direction' => $this->sortDirection,
'sort_by_relevance' => $this->sortByRelevance,
'enable_highlighting' => $this->enableHighlighting,
'enable_fuzzy' => $this->enableFuzzy,
'min_score' => $this->minScore,
];
}
}