96 lines
3.3 KiB
PHP
96 lines
3.3 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace App;
|
|
|
|
final class Search
|
|
{
|
|
public function __construct(private ?\PDO $pdo) {}
|
|
|
|
public function searchEvents(string $query, int $limit = 100, ?array $geo = null): array
|
|
{
|
|
if (!$this->pdo) return [];
|
|
|
|
$q = trim($query);
|
|
$hasGeo = isset($geo['lat'], $geo['lng']) && is_numeric($geo['lat']) && is_numeric($geo['lng']);
|
|
if ($q === '' && !$hasGeo) return [];
|
|
|
|
$tokens = array_filter(preg_split('/\s+/', $q) ?: [], fn($t) => $t !== '');
|
|
if (!$tokens) {
|
|
$tokens = [$q];
|
|
}
|
|
|
|
$conditions = [];
|
|
$params = [];
|
|
$i = 0;
|
|
foreach ($tokens as $tok) {
|
|
$tok = trim($tok);
|
|
if ($tok === '') continue;
|
|
$ph = [
|
|
't' . $i . 'a',
|
|
't' . $i . 'b',
|
|
't' . $i . 'c',
|
|
't' . $i . 'd',
|
|
't' . $i . 'e',
|
|
't' . $i . 'f',
|
|
];
|
|
$conditions[] = "(title LIKE :{$ph[0]} OR teaser_public LIKE :{$ph[1]} OR description LIKE :{$ph[2]} OR city LIKE :{$ph[3]} OR region LIKE :{$ph[4]} OR zip LIKE :{$ph[5]})";
|
|
foreach ($ph as $p) {
|
|
$params[$p] = '%' . $tok . '%';
|
|
}
|
|
$i++;
|
|
}
|
|
$whereParts = [
|
|
"starts_at >= NOW()",
|
|
"status != 'cancelled'",
|
|
];
|
|
if ($conditions) {
|
|
$whereParts[] = implode(' AND ', $conditions);
|
|
}
|
|
|
|
$sql = "SELECT id, title, teaser_public, description, city, region, zip, starts_at, visibility, allow_kids, location_label, lat, lng";
|
|
$distanceFiltering = false;
|
|
|
|
if ($hasGeo) {
|
|
$lat = (float)$geo['lat'];
|
|
$lng = (float)$geo['lng'];
|
|
$radius = isset($geo['radius']) && is_numeric($geo['radius']) ? max(0.1, (float)$geo['radius']) : 5.0;
|
|
$sql .= ",
|
|
(6371 * ACOS(LEAST(1,
|
|
COS(RADIANS(:glat)) * COS(RADIANS(lat)) * COS(RADIANS(lng) - RADIANS(:glng)) +
|
|
SIN(RADIANS(:glat)) * SIN(RADIANS(lat))
|
|
))) AS distance_km";
|
|
$distanceFiltering = true;
|
|
|
|
$latRange = $radius / 111.0;
|
|
$lngRange = $radius / (111.0 * max(0.1, cos($lat * M_PI / 180)));
|
|
$whereParts[] = "(lat IS NOT NULL AND lng IS NOT NULL)";
|
|
$whereParts[] = "(lat BETWEEN :latMin AND :latMax)";
|
|
$whereParts[] = "(lng BETWEEN :lngMin AND :lngMax)";
|
|
$params['glat'] = $lat;
|
|
$params['glng'] = $lng;
|
|
$params['latMin'] = $lat - $latRange;
|
|
$params['latMax'] = $lat + $latRange;
|
|
$params['lngMin'] = $lng - $lngRange;
|
|
$params['lngMax'] = $lng + $lngRange;
|
|
$params['radius'] = $radius;
|
|
}
|
|
|
|
$where = $whereParts ? ('WHERE ' . implode(' AND ', $whereParts)) : '';
|
|
$sql .= " FROM events $where";
|
|
if ($distanceFiltering) {
|
|
$sql .= " HAVING distance_km <= :radius";
|
|
$sql .= " ORDER BY distance_km ASC, starts_at ASC";
|
|
} else {
|
|
$sql .= " ORDER BY starts_at ASC";
|
|
}
|
|
$sql .= " LIMIT :lim";
|
|
|
|
$stmt = $this->pdo->prepare($sql);
|
|
$execParams = $params;
|
|
$execParams['lim'] = $limit;
|
|
$stmt->execute($execParams);
|
|
return $stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [];
|
|
}
|
|
}
|