ldc-groupe/app/Http/Controllers/Front/SearchController.php
2026-02-11 09:15:43 +01:00

188 lines
6.2 KiB
PHP

<?php
namespace App\Http\Controllers\Front;
use App\Http\Requests\Front\Search\SearchAjaxRequest;
use App\Http\Requests\Front\Search\SearchRequest;
use Uwil\LaravelCommon\Controllers\Controller;
class SearchController extends Controller
{
/**
* Access to the search page.
*
* @param \App\Http\Requests\Front\Search\SearchRequest $request
* @return \Illuminate\Contracts\View\View
*/
public function search(SearchRequest $request): \Illuminate\Contracts\View\View
{
$queryKey = trans('front.search.query');
$searchTerm = $request->validated()[$queryKey];
return view('front.pages.search', compact('searchTerm'));
}
/**
* Load results from search.
*
* @param \App\Http\Requests\Front\Search\SearchAjaxRequest $request
* @return \Illuminate\Http\JsonResponse
*/
public function ajaxSearchLoadResults(SearchAjaxRequest $request): \Illuminate\Http\JsonResponse
{
$class = $request->validated()['class'];
$searchTerm = $request->validated()['search'];
$currentPage = (int) $request->validated()['page'];
$currentLocale = $request->validated()['locale'];
$perPage = 5;
app()->setLocale($currentLocale);
$searchQuery = $this->buildExactSearchQuery($searchTerm);
$models = $class::search($searchQuery, function ($engine, $query, $options) use ($class, $currentLocale) {
$options['attributesToSearchOn'] = $class::searchTransFields($currentLocale);
return $engine->search($query, $options);
})
->where('status', $class::searchStatusEnum())
->paginateRaw(perPage: $perPage, page: $currentPage);
// Render items.
$itemsHtml = collect($models['hits'])->map(fn ($result) => view('components.front.search.card', [
'class' => $class,
'result' => $result,
'mainField' => $class::searchMainField(),
'routeName' => $class::searchRouteName($result['id_key'] ?? null),
'customBreadcrumbs' => true,
'searchTerm' => $searchTerm,
'currentLocale' => $currentLocale,
])->render())->implode('');
// Pagination.
$paginationHtml = view('components.front.search.pagination', [
'models' => $models,
])->render();
return response()->json([
'items' => $itemsHtml,
'pagination' => $paginationHtml,
'totalHits' => $models['totalHits'],
]);
}
/**
* Set strict search term.
*
* @param string $searchTerm
* @return string
*/
private function buildExactSearchQuery(string $searchTerm): string
{
$words = preg_split('/\s+/', trim($searchTerm), -1, PREG_SPLIT_NO_EMPTY);
if (count($words) === 1) {
return '"' . $words[0] . '"';
}
$exactTerms = array_map(function ($word) {
return $word . '*';
}, $words);
return '+' . implode(' +', $exactTerms);
}
/**
* Highlights all search terms in a text.
*
* @param string $text
* @param string $searchTerm
* @param integer $contextLength
* @return string
* @phpcs:disable Generic.Metrics.CyclomaticComplexity
*/
public static function highlightTerms(string $text, string $searchTerm, int $contextLength = 150): string
{
// phpcs:enable
if (trim($searchTerm) === '') {
return $text;
}
// Clean terms list.
$words = preg_split('/\s+/', $searchTerm, -1, PREG_SPLIT_NO_EMPTY);
if (empty($words)) {
return $text;
}
$fullText = strip_tags($text);
$textLength = strlen($fullText);
$ranges = [];
// Collection of relevant ranges around each word.
foreach ($words as $word) {
$pattern = '/(?<!\w)' . preg_quote($word, '/') . '(?!\w)/iu';
// Check if current word match exactly with terms.
if (preg_match_all($pattern, $fullText, $matches, PREG_OFFSET_CAPTURE)) {
foreach ($matches[0] as $match) {
$matchPos = $match[1];
$matchLen = strlen($match[0]);
$ranges[] = [
'start' => max(0, $matchPos - $contextLength),
'end' => min($textLength, $matchPos + $matchLen + $contextLength),
];
}
}
}
// Return the entire text if there isn't matches.
if (empty($ranges)) {
return $text;
}
// Merging overlapping intervals.
usort($ranges, fn($a, $b) => $a['start'] <=> $b['start']);
$merged = [];
foreach ($ranges as $r) {
if (empty($merged) || $r['start'] > $merged[array_key_last($merged)]['end']) {
$merged[] = $r;
} else {
$merged[array_key_last($merged)]['end'] = max(
$merged[array_key_last($merged)]['end'],
$r['end']
);
}
}
$snippets = [];
foreach ($merged as $r) {
$snippet = substr($fullText, $r['start'], $r['end'] - $r['start']);
// Cut cleanly without breaking the words.
if ($r['start'] > 0) {
$snippet = preg_replace('/^\S*\s/', '…', $snippet, 1);
}
if ($r['end'] < $textLength) {
$snippet = preg_replace('/\s\S*$/', '…', $snippet, 1);
}
// Highlighting searched words.
foreach ($words as $word) {
$pattern = '/(?<!\w)' . preg_quote($word, '/') . '(?!\w)/iu';
$snippet = preg_replace(
$pattern,
'<span class="search-highlight">$0</span>',
$snippet
);
}
$snippets[] = trim($snippet);
if (count($snippets) >= 3) {
break;
}
} //end foreach
$result = implode('… ', $snippets);
$result = preg_replace('/\s*…\s*(?:…\s*)+/', ' … ', $result);
return trim($result);
}
}