252 lines
7.6 KiB
PHP
252 lines
7.6 KiB
PHP
<?php
|
|||
|
|
|
||
|
|
namespace App\Http\Controllers\API;
|
||
|
|
|
||
|
|
use App\Http\Controllers\Controller;
|
||
|
|
use App\Http\Resources\CityResource;
|
||
|
|
use App\Http\Resources\AttractionResource;
|
||
|
|
use App\Models\City;
|
||
|
|
use App\Jobs\FetchCityDataJob;
|
||
|
|
use App\Services\WikipediaService;
|
||
|
|
use Illuminate\Http\Request;
|
||
|
|
use Illuminate\Support\Facades\Http;
|
||
|
|
|
||
|
|
class CityController extends Controller
|
||
|
|
{
|
||
|
|
/**
|
||
|
|
* Search for cities
|
||
|
|
*/
|
||
|
|
public function search(Request $request, WikipediaService $wikipediaService)
|
||
|
|
{
|
||
|
|
$validated = $request->validate([
|
||
|
|
'q' => 'required|string|min:2|max:255',
|
||
|
|
]);
|
||
|
|
|
||
|
|
$query = $validated['q'];
|
||
|
|
|
||
|
|
// Add to search history if session is available (for web routes)
|
||
|
|
if ($request->hasSession()) {
|
||
|
|
$this->addToSearchHistory($request, $query);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Search in local database first
|
||
|
|
$localResults = $this->searchLocalCities($query);
|
||
|
|
|
||
|
|
$wikipediaResults = null;
|
||
|
|
|
||
|
|
// If no local results, fetch from Wikipedia and create new city
|
||
|
|
if ($localResults->isEmpty()) {
|
||
|
|
$newCity = $this->createCityFromWikipedia($query, $wikipediaService);
|
||
|
|
|
||
|
|
if ($newCity) {
|
||
|
|
$localResults = collect([$newCity]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$response = [
|
||
|
|
'local' => CityResource::collection($localResults),
|
||
|
|
'wikipedia' => $wikipediaResults,
|
||
|
|
];
|
||
|
|
|
||
|
|
// Add recent searches if session is available
|
||
|
|
if ($request->hasSession()) {
|
||
|
|
$response['recent_searches'] = $request->session()->get('search_history', []);
|
||
|
|
}
|
||
|
|
|
||
|
|
return response()->json($response);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Add search query to session history
|
||
|
|
* Note: This method should only be called when hasSession() is true
|
||
|
|
*/
|
||
|
|
private function addToSearchHistory(Request $request, string $query): void
|
||
|
|
{
|
||
|
|
if (!$request->hasSession()) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
$searchHistory = $request->session()->get('search_history', []);
|
||
|
|
|
||
|
|
// Remove duplicate if exists
|
||
|
|
$searchHistory = array_filter($searchHistory, function($item) use ($query) {
|
||
|
|
return strtolower($item) !== strtolower($query);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Add new search to the beginning
|
||
|
|
array_unshift($searchHistory, $query);
|
||
|
|
|
||
|
|
// Keep only last 10 searches
|
||
|
|
$searchHistory = array_slice($searchHistory, 0, 10);
|
||
|
|
|
||
|
|
$request->session()->put('search_history', $searchHistory);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Search for cities in local database
|
||
|
|
*/
|
||
|
|
private function searchLocalCities(string $query)
|
||
|
|
{
|
||
|
|
return City::where('name', 'LIKE', "%{$query}%")
|
||
|
|
->orWhere('country', 'LIKE', "%{$query}%")
|
||
|
|
->limit(10)
|
||
|
|
->get();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create a city from Wikipedia data
|
||
|
|
*/
|
||
|
|
private function createCityFromWikipedia(string $query, WikipediaService $wikipediaService)
|
||
|
|
{
|
||
|
|
try {
|
||
|
|
$wikipediaData = $wikipediaService->getCitySummary($query);
|
||
|
|
|
||
|
|
if (!$wikipediaData) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
$countryName = $this->fetchCountryName($wikipediaData['title']);
|
||
|
|
|
||
|
|
$city = City::create([
|
||
|
|
'name' => $wikipediaData['title'],
|
||
|
|
'country' => $countryName,
|
||
|
|
'latitude' => $wikipediaData['latitude'] ?? null,
|
||
|
|
'longitude' => $wikipediaData['longitude'] ?? null,
|
||
|
|
'wikipedia_summary' => $wikipediaData['summary'] ?? null,
|
||
|
|
'wikipedia_url' => $wikipediaData['url'] ?? null,
|
||
|
|
'thumbnail_url' => $wikipediaData['thumbnail'] ?? null,
|
||
|
|
'image_url' => $wikipediaData['image'] ?? null,
|
||
|
|
'wikipedia_page_id' => $wikipediaData['page_id'] ?? null,
|
||
|
|
'wikipedia_cached_at' => now(),
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Dispatch job to fetch attractions for the newly created city
|
||
|
|
if ($city && $city->latitude && $city->longitude) {
|
||
|
|
\App\Jobs\FetchAttractionsJob::dispatch($city);
|
||
|
|
}
|
||
|
|
|
||
|
|
return $city;
|
||
|
|
} catch (\Exception $e) {
|
||
|
|
\Log::error('Failed to create city from Wikipedia', [
|
||
|
|
'query' => $query,
|
||
|
|
'error' => $e->getMessage(),
|
||
|
|
]);
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Fetch country name from external API
|
||
|
|
*/
|
||
|
|
private function fetchCountryName(string $cityName)
|
||
|
|
{
|
||
|
|
try {
|
||
|
|
$response = Http::post('https://countriesnow.space/api/v0.1/countries/population/cities', [
|
||
|
|
'city' => $cityName,
|
||
|
|
]);
|
||
|
|
|
||
|
|
if ($response->successful()) {
|
||
|
|
$data = $response->json();
|
||
|
|
return $data['data']['country'] ?? 'Unknown';
|
||
|
|
}
|
||
|
|
|
||
|
|
return 'Unknown';
|
||
|
|
} catch (\Exception $e) {
|
||
|
|
\Log::error('Failed to fetch country name', [
|
||
|
|
'city' => $cityName,
|
||
|
|
'error' => $e->getMessage(),
|
||
|
|
]);
|
||
|
|
|
||
|
|
return 'Unknown';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Display the specified city
|
||
|
|
*/
|
||
|
|
public function show(City $city)
|
||
|
|
{
|
||
|
|
$city->loadCount('attractions');
|
||
|
|
|
||
|
|
// fetch attractions, or dispatch a job to fetch them
|
||
|
|
|
||
|
|
return new CityResource($city);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get attractions for a city with pagination
|
||
|
|
*/
|
||
|
|
public function attractions(Request $request, City $city)
|
||
|
|
{
|
||
|
|
$validated = $request->validate([
|
||
|
|
'category' => 'nullable|string|max:50',
|
||
|
|
'limit' => 'nullable|integer|min:1|max:100',
|
||
|
|
'page' => 'nullable|integer|min:1',
|
||
|
|
]);
|
||
|
|
|
||
|
|
$query = $city->attractions()
|
||
|
|
->select(['id', 'city_id', 'name', 'description', 'latitude', 'longitude', 'category', 'rating', 'image_url'])
|
||
|
|
->orderBy('name', 'asc');
|
||
|
|
|
||
|
|
// Filter by category if provided
|
||
|
|
if ($request->has('category') && $request->category) {
|
||
|
|
$query->where('category', $request->category);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Use pagination for better performance
|
||
|
|
$perPage = $request->input('limit', 20);
|
||
|
|
$attractions = $query->paginate($perPage);
|
||
|
|
|
||
|
|
return AttractionResource::collection($attractions);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get map data for a city with optimized query
|
||
|
|
*/
|
||
|
|
public function mapData(City $city)
|
||
|
|
{
|
||
|
|
// Eager load only necessary columns for map display
|
||
|
|
$city->load(['attractions' => function ($query) {
|
||
|
|
$query->select(['id', 'city_id', 'name', 'latitude', 'longitude', 'category']);
|
||
|
|
}]);
|
||
|
|
|
||
|
|
return response()->json([
|
||
|
|
'city' => [
|
||
|
|
'name' => $city->name,
|
||
|
|
'latitude' => (float) $city->latitude,
|
||
|
|
'longitude' => (float) $city->longitude,
|
||
|
|
],
|
||
|
|
'attractions' => $city->attractions->map(function ($attraction) {
|
||
|
|
return [
|
||
|
|
'id' => $attraction->id,
|
||
|
|
'name' => $attraction->name,
|
||
|
|
'latitude' => (float) $attraction->latitude,
|
||
|
|
'longitude' => (float) $attraction->longitude,
|
||
|
|
'category' => $attraction->category,
|
||
|
|
];
|
||
|
|
}),
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Add a new city (Protected)
|
||
|
|
*/
|
||
|
|
public function store(Request $request)
|
||
|
|
{
|
||
|
|
$validated = $request->validate([
|
||
|
|
'name' => 'required|string|max:255',
|
||
|
|
'country' => 'required|string|max:255',
|
||
|
|
'latitude' => 'nullable|numeric|between:-90,90',
|
||
|
|
'longitude' => 'nullable|numeric|between:-180,180',
|
||
|
|
]);
|
||
|
|
|
||
|
|
$city = City::create($validated);
|
||
|
|
|
||
|
|
// Dispatch job to fetch data from Wikipedia
|
||
|
|
FetchCityDataJob::dispatch($city);
|
||
|
|
|
||
|
|
return new CityResource($city);
|
||
|
|
}
|
||
|
|
}
|