2026-01-12 12:07:17 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Http\Controllers\Front;
|
|
|
|
|
|
2026-01-20 11:34:25 +01:00
|
|
|
use App\Http\Requests\Front\Search\SearchAjaxRequest;
|
|
|
|
|
use App\Http\Requests\Front\Search\SearchRequest;
|
|
|
|
|
use App\Models\Document;
|
|
|
|
|
use App\Models\DynamicPage;
|
|
|
|
|
use App\Models\News;
|
|
|
|
|
use App\Enums\Document\DocumentStatusEnum;
|
|
|
|
|
use App\Enums\News\NewsStatusEnum;
|
|
|
|
|
use App\Enums\DynamicPages\PageStatusEnum;
|
2026-01-12 12:07:17 +01:00
|
|
|
use Illuminate\Http\Request;
|
|
|
|
|
use Illuminate\Support\Str;
|
2026-01-20 11:34:25 +01:00
|
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
|
|
|
use Mcamara\LaravelLocalization\LaravelLocalization;
|
2026-01-12 12:07:17 +01:00
|
|
|
use Uwil\LaravelCommon\Controllers\Controller;
|
|
|
|
|
use Uwil\LaravelCommon\Helpers\CollectionHelper;
|
|
|
|
|
|
|
|
|
|
class SearchController extends Controller
|
|
|
|
|
{
|
|
|
|
|
/**
|
2026-01-20 11:34:25 +01:00
|
|
|
* Access to the search page.
|
2026-01-12 12:07:17 +01:00
|
|
|
*
|
2026-01-20 11:34:25 +01:00
|
|
|
* @param \App\Http\Requests\Front\Search\SearchRequest $request
|
2026-01-12 12:07:17 +01:00
|
|
|
* @return \Illuminate\Contracts\View\View
|
|
|
|
|
*/
|
2026-01-20 11:34:25 +01:00
|
|
|
public function search(SearchRequest $request)
|
2026-01-12 12:07:17 +01:00
|
|
|
{
|
2026-01-20 11:34:25 +01:00
|
|
|
$queryKey = trans('front.search.query');
|
2026-01-12 12:07:17 +01:00
|
|
|
$searchTerm = $request->validated()[$queryKey];
|
2026-01-20 11:34:25 +01:00
|
|
|
|
|
|
|
|
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;
|
2026-01-12 12:07:17 +01:00
|
|
|
|
2026-01-20 11:34:25 +01:00
|
|
|
app()->setLocale($currentLocale);
|
2026-01-12 12:07:17 +01:00
|
|
|
$searchQuery = $this->buildExactSearchQuery($searchTerm);
|
|
|
|
|
|
2026-01-20 11:34:25 +01:00
|
|
|
$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('');
|
2026-01-12 12:07:17 +01:00
|
|
|
|
2026-01-20 11:34:25 +01:00
|
|
|
// Pagination.
|
|
|
|
|
$paginationHtml = view('components.front.search.pagination', [
|
|
|
|
|
'models' => $models,
|
|
|
|
|
])->render();
|
|
|
|
|
|
|
|
|
|
return response()->json([
|
|
|
|
|
'items' => $itemsHtml,
|
|
|
|
|
'pagination' => $paginationHtml,
|
|
|
|
|
'totalHits' => $models['totalHits'],
|
|
|
|
|
]);
|
2026-01-12 12:07:17 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Set strict search term.
|
|
|
|
|
*
|
|
|
|
|
* @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);
|
|
|
|
|
}
|
2026-01-20 11:34:25 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Highlights all search terms in a text.
|
|
|
|
|
*
|
|
|
|
|
* @param string $text
|
|
|
|
|
* @param string $searchTerm
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
public static function highlightTerms(string $text, string $searchTerm, int $contextLength = 150): string
|
|
|
|
|
{
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$result = implode('… ', $snippets);
|
|
|
|
|
$result = preg_replace('/\s*…\s*(?:…\s*)+/', ' … ', $result);
|
|
|
|
|
|
|
|
|
|
return trim($result);
|
|
|
|
|
}
|
2026-01-12 12:07:17 +01:00
|
|
|
}
|