diff --git a/.env.example b/.env.example index 1c4b5d2..b1ccf19 100644 --- a/.env.example +++ b/.env.example @@ -5,8 +5,8 @@ APP_DEBUG=true APP_TIMEZONE=UTC APP_URL=http://localhost -API_CAT=live_vPWHWd2VHESz6uk5ZbMoxjI5jp9EBlIK2LdX4DUwgu4EZhcCUXtWUSoYCb0Xccaj -API_DOG=live_PPrf0Hbi91a5qFabgHx4epZHQXpTjxSsGrqaNyj1uunCnVuxExRI4fZzohpV0Vdq +CAT_API_KEY= +CAT_API_URL= APP_LOCALE=en APP_FALLBACK_LOCALE=en diff --git a/README.md b/README.md index 5a41b5a..8edd610 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,29 @@ # Shopblocks PHP Developer Test: Pet-é-dex -Welcome to the **Shopblocks PHP Developer Test: Pet-é-dex**! This project evaluates your skills in working with APIs, building user interfaces, and implementing core functionality using PHP and Laravel. While design skills are a bonus, we will focus mainly on usability, performance, and security. +## Teo Notes +Here’s a quick rundown of what I did: + +- Removed the API key from the example `.env` + Didn’t want to leave sensitive stuff lying around. Bots scan GitHub for keys all the time, so better safe than sorry. + +- Cached the API results + I added caching for both the full list of breeds and individual breed details. This keeps the site fast, avoids hammering the API, and respects their rate limits. + +- Kept the controller simple with a `CatService` + All the API calls and caching live in the service now, so the controller just handles requests and returns views. Makes it easier to read and maintain. + +- Redirect to http.cat on 404 + Little fun touch and also handles missing data gracefully instead of breaking the page. + +If I had more time, here’s what I’d do next: + +- Query the API directly for searches instead of filtering in Blade, so results are accurate and performance stays solid even with a big list. +- Add more tests to cover API calls, caching, and edge cases. +- Pagination to keep the pages snappy for large datasets. +- Vue.js for smoother, interactive searching and filtering. +- Better styling to make it look more polished and professional. + + ## Introduction diff --git a/[ b/[ new file mode 100644 index 0000000..e69de29 diff --git a/app/Http/Controllers/CatController.php b/app/Http/Controllers/CatController.php new file mode 100644 index 0000000..d86d876 --- /dev/null +++ b/app/Http/Controllers/CatController.php @@ -0,0 +1,47 @@ +catService = $catService; + } + + /** + * Display a listing of cat breeds. + * + */ + public function index(): View + { + $breeds = $this->catService->allBreeds(); + + return view('cats.index', compact('breeds')); + } + + /** + * Display the specified cat breed. + */ + public function show(string $breedId): RedirectResponse|View + { + $breed = $this->catService->findBreedById($breedId); + + if (!$breed) { + return redirect()->away("https://http.cat/404"); + } + + return view('cats.show', compact('breed')); + } +} diff --git a/app/Services/CatService.php b/app/Services/CatService.php new file mode 100644 index 0000000..dd35c6e --- /dev/null +++ b/app/Services/CatService.php @@ -0,0 +1,68 @@ +client = new Client([ + 'base_uri' => config('services.cat-api.url'), + 'headers' => [ + 'x-api-key' => config('services.cat-api.key'), + ], + ]); + } + + /** + * Fetch all cat breeds from The Cat API. + */ + public function allBreeds(): array + { + return Cache::remember('cat_breeds', 1800, function () { + try { + $response = $this->client->get('breeds'); + return json_decode((string) $response->getBody(), true) ?? []; + + } catch (RequestException $e) { + return []; + } + }); + } + + + /** + * Find a specific breed by its ID from The Cat API. + */ + public function findBreedById(string $breedId): ?array + { + return Cache::remember("breeds.{$breedId}", 1800, function () use ($breedId){ + try { + $response = $this->client->get("images/search", [ + 'query' => ['breed_ids' => $breedId], + ]); + return collect($this->decodeResponse($response))->first(); + + } catch (Throwable $error){ + return null; + } + }); + } + + /** + * Decode the JSON response from The Cat API. + */ + private function decodeResponse($response): array + { + return json_decode((string) $response->getBody(), true) ?? []; + } +} diff --git a/artisan b/artisan old mode 100755 new mode 100644 diff --git a/composer.json b/composer.json index f943d3a..ea811b3 100644 --- a/composer.json +++ b/composer.json @@ -6,6 +6,7 @@ "license": "MIT", "require": { "php": "^8.2", + "guzzlehttp/guzzle": "^7.9", "laravel/framework": "^11.9", "laravel/tinker": "^2.9" }, diff --git a/composer.lock b/composer.lock index 50b3de7..9e1df6f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8031ad7e2edbc06178aa81ff880874fb", + "content-hash": "848001a59e99cc9b61044b18639705e9", "packages": [ { "name": "brick/math", @@ -645,16 +645,16 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.9.2", + "version": "7.9.3", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "d281ed313b989f213357e3be1a179f02196ac99b" + "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b", - "reference": "d281ed313b989f213357e3be1a179f02196ac99b", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", + "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", "shasum": "" }, "require": { @@ -751,7 +751,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.9.2" + "source": "https://github.com/guzzle/guzzle/tree/7.9.3" }, "funding": [ { @@ -767,7 +767,7 @@ "type": "tidelift" } ], - "time": "2024-07-24T11:22:20+00:00" + "time": "2025-03-27T13:37:11+00:00" }, { "name": "guzzlehttp/promises", @@ -7832,12 +7832,12 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { "php": "^8.2" }, - "platform-dev": [], + "platform-dev": {}, "plugin-api-version": "2.6.0" } diff --git a/config/services.php b/config/services.php index 27a3617..141156c 100644 --- a/config/services.php +++ b/config/services.php @@ -35,4 +35,9 @@ ], ], + 'cat-api' => [ + 'url' => env('CAT_API_URL', 'https://api.thecatapi.com/v1/'), + 'key' => env('CAT_API_KEY'), + ] + ]; diff --git a/live_vPWHWd2VHESz6uk5ZbMoxjI5jp9EBlIK2LdX4DUwgu4EZhcCUXtWUSoYCb0Xccaj b/live_vPWHWd2VHESz6uk5ZbMoxjI5jp9EBlIK2LdX4DUwgu4EZhcCUXtWUSoYCb0Xccaj new file mode 100644 index 0000000..e69de29 diff --git a/resources/views/cats/index.blade.php b/resources/views/cats/index.blade.php new file mode 100644 index 0000000..17eeb80 --- /dev/null +++ b/resources/views/cats/index.blade.php @@ -0,0 +1,60 @@ +@extends('layouts.app') + +@section('title', 'Cat Breeds') + +@push('style') + +@endpush + +@section('content') +
No breeds found.
+ @endforelse +{{ $info['description'] ?? 'No description available.' }}
+ +Breed information unavailable.
+ @endif +