From 81f0ae24d4138e1998a26915ff05b44283e38356 Mon Sep 17 00:00:00 2001 From: Magicard Date: Fri, 7 Nov 2025 07:40:13 +0000 Subject: [PATCH 01/10] forked, removed key from env example --- .env.example | 3 --- artisan | 0 2 files changed, 3 deletions(-) mode change 100755 => 100644 artisan diff --git a/.env.example b/.env.example index 1c4b5d2..ef5b1ec 100644 --- a/.env.example +++ b/.env.example @@ -5,9 +5,6 @@ APP_DEBUG=true APP_TIMEZONE=UTC APP_URL=http://localhost -API_CAT=live_vPWHWd2VHESz6uk5ZbMoxjI5jp9EBlIK2LdX4DUwgu4EZhcCUXtWUSoYCb0Xccaj -API_DOG=live_PPrf0Hbi91a5qFabgHx4epZHQXpTjxSsGrqaNyj1uunCnVuxExRI4fZzohpV0Vdq - APP_LOCALE=en APP_FALLBACK_LOCALE=en APP_FAKER_LOCALE=en_US diff --git a/artisan b/artisan old mode 100755 new mode 100644 From f811afda77451e27c474ba024ccedfe34910fbb8 Mon Sep 17 00:00:00 2001 From: Magicard Date: Fri, 7 Nov 2025 08:07:03 +0000 Subject: [PATCH 02/10] cat service --- app/Services/CatService.php | 80 +++++++++++++++++++++++++++++++++++++ composer.json | 1 + composer.lock | 18 ++++----- config/services.php | 5 +++ 4 files changed, 95 insertions(+), 9 deletions(-) create mode 100644 app/Services/CatService.php diff --git a/app/Services/CatService.php b/app/Services/CatService.php new file mode 100644 index 0000000..e0e9127 --- /dev/null +++ b/app/Services/CatService.php @@ -0,0 +1,80 @@ +client= $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. + * + * @return array + */ + public function allBreeds(): array + { + return Cache::remember('cat_breeds', 1800, function (){ + try { + $response = $this->client->get('breeds'); + return $this->decodeResponse($response); + + + } catch (Throwable $error){ + return []; + } + }); + } + + /** + * Find a specific breed by its ID from The Cat API. + * + * @param string $breedId + * @return array|null + */ + 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. + * + * @param $response + * @return array + */ + private function decodeResponse($response): array + { + return json_decode((string) $response->getBody(), true) ?? []; + } +} 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'), + ] + ]; From e137a8724d49dbc22661a8e0f0b287d83112ff3e Mon Sep 17 00:00:00 2001 From: Magicard Date: Fri, 7 Nov 2025 08:14:29 +0000 Subject: [PATCH 03/10] routes, controller --- app/Http/Controllers/CatController.php | 46 ++++++++++++++++++++++++++ routes/web.php | 6 ++-- 2 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 app/Http/Controllers/CatController.php diff --git a/app/Http/Controllers/CatController.php b/app/Http/Controllers/CatController.php new file mode 100644 index 0000000..b59db48 --- /dev/null +++ b/app/Http/Controllers/CatController.php @@ -0,0 +1,46 @@ +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/routes/web.php b/routes/web.php index ef77af2..15711d3 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,7 +1,7 @@ name('cats.index'); +Route::get('/cats/{id}', [CatController::class, 'show'])->name('cats.show'); From 15f219c7f405c5c5e66ef8872a0ee4b86ae0b96a Mon Sep 17 00:00:00 2001 From: Magicard Date: Fri, 7 Nov 2025 09:01:36 +0000 Subject: [PATCH 04/10] working views --- [ | 0 app/Services/CatService.php | 18 +++--- ...I5jp9EBlIK2LdX4DUwgu4EZhcCUXtWUSoYCb0Xccaj | 0 resources/views/cats/index.blade.php | 60 +++++++++++++++++++ resources/views/cats/show.blade.php | 37 ++++++++++++ resources/views/layouts/app.blade.php | 2 +- routes/web.php | 7 ++- 7 files changed, 111 insertions(+), 13 deletions(-) create mode 100644 [ create mode 100644 live_vPWHWd2VHESz6uk5ZbMoxjI5jp9EBlIK2LdX4DUwgu4EZhcCUXtWUSoYCb0Xccaj create mode 100644 resources/views/cats/index.blade.php create mode 100644 resources/views/cats/show.blade.php diff --git a/[ b/[ new file mode 100644 index 0000000..e69de29 diff --git a/app/Services/CatService.php b/app/Services/CatService.php index e0e9127..cd1f4b2 100644 --- a/app/Services/CatService.php +++ b/app/Services/CatService.php @@ -17,13 +17,12 @@ class CatService */ public function __construct(?Client $client= null) { - //Chose passed in client other make a new one - $this->client= $client ?? new Client([ + $this->client = new Client([ 'base_uri' => config('services.cat-api.url'), 'headers' => [ - 'x-api-key' => config('services.cat-api.key') - ], - ]); + 'x-api-key' => config('services.cat-api.key'), + ], + ]); } /** @@ -33,18 +32,17 @@ public function __construct(?Client $client= null) */ public function allBreeds(): array { - return Cache::remember('cat_breeds', 1800, function (){ + return Cache::remember('cat_breeds', 1800, function () { try { $response = $this->client->get('breeds'); - return $this->decodeResponse($response); - - - } catch (Throwable $error){ + return json_decode((string) $response->getBody(), true) ?? []; + } catch (RequestException $e) { return []; } }); } + /** * Find a specific breed by its ID from The Cat API. * 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..fba7f99 --- /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') +
+

Cat Breeds

+ +
+
+ + +
+
+ + @php + $filtered = collect($breeds)->filter(function ($breed) { + $query = request('search'); + return !$query || stripos($breed['name'], $query) !== false; + }); + @endphp + + +
+@endsection diff --git a/resources/views/cats/show.blade.php b/resources/views/cats/show.blade.php new file mode 100644 index 0000000..d305518 --- /dev/null +++ b/resources/views/cats/show.blade.php @@ -0,0 +1,37 @@ +@extends('layouts.app') + +@section('title', $breed['breeds'][0]['name'] ?? 'Cat Details') + +@section('content') +
+ ← Back to all breeds + + @if(isset($breed['breeds'][0])) + @php $info = $breed['breeds'][0]; @endphp + +
+
+
+ @if(!empty($breed['url'])) + {{ $info['name'] }} + @endif +
+
+
+

{{ $info['name'] }}

+

{{ $info['description'] ?? 'No description available.' }}

+ +
    +
  • Origin: {{ $info['origin'] ?? 'Unknown' }}
  • +
  • Temperament: {{ $info['temperament'] ?? 'Unknown' }}
  • +
  • Life Span: {{ $info['life_span'] ?? 'N/A' }} years
  • +
+
+
+
+
+ @else +

Breed information unavailable.

+ @endif +
+@endsection diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 9d0df2c..c5867ae 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -7,7 +7,7 @@ - + @stack('style') diff --git a/routes/web.php b/routes/web.php index 15711d3..27c72b7 100644 --- a/routes/web.php +++ b/routes/web.php @@ -3,5 +3,8 @@ use App\Http\Controllers\CatController; use Illuminate\Support\Facades\Route; -Route::get('/', [CatController::class, 'index'])->name('cats.index'); -Route::get('/cats/{id}', [CatController::class, 'show'])->name('cats.show'); +Route::get('/cats', [CatController::class, 'index'])->name('cats.index'); // index page +Route::get('/cats/{id}', [CatController::class, 'show'])->name('cats.show'); // detail page +Route::get('/', function() { + return redirect()->route('cats.index'); +}); From f20433cf8d1bed58b7360bb437ca1ac994fa3256 Mon Sep 17 00:00:00 2001 From: Magicard Date: Fri, 7 Nov 2025 09:02:29 +0000 Subject: [PATCH 05/10] center cats --- resources/views/cats/index.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/cats/index.blade.php b/resources/views/cats/index.blade.php index fba7f99..17eeb80 100644 --- a/resources/views/cats/index.blade.php +++ b/resources/views/cats/index.blade.php @@ -35,7 +35,7 @@ }); @endphp -
+
@forelse($filtered as $breed)
From 02d4df1f89fff1a789bf027643dc893660dd804d Mon Sep 17 00:00:00 2001 From: Magicard Date: Fri, 7 Nov 2025 09:15:23 +0000 Subject: [PATCH 06/10] remove unnecesary comments --- app/Http/Controllers/CatController.php | 1 + app/Services/CatService.php | 10 ---------- routes/web.php | 4 ++-- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/app/Http/Controllers/CatController.php b/app/Http/Controllers/CatController.php index b59db48..d86d876 100644 --- a/app/Http/Controllers/CatController.php +++ b/app/Http/Controllers/CatController.php @@ -22,6 +22,7 @@ public function __construct(CatService $catService) /** * Display a listing of cat breeds. + * */ public function index(): View { diff --git a/app/Services/CatService.php b/app/Services/CatService.php index cd1f4b2..ca9e84b 100644 --- a/app/Services/CatService.php +++ b/app/Services/CatService.php @@ -12,8 +12,6 @@ class CatService /** * Constructor to initialize the HTTP client. - * - * @param Client|null $client */ public function __construct(?Client $client= null) { @@ -27,8 +25,6 @@ public function __construct(?Client $client= null) /** * Fetch all cat breeds from The Cat API. - * - * @return array */ public function allBreeds(): array { @@ -45,9 +41,6 @@ public function allBreeds(): array /** * Find a specific breed by its ID from The Cat API. - * - * @param string $breedId - * @return array|null */ public function findBreedById(string $breedId): ?array { @@ -67,9 +60,6 @@ public function findBreedById(string $breedId): ?array /** * Decode the JSON response from The Cat API. - * - * @param $response - * @return array */ private function decodeResponse($response): array { diff --git a/routes/web.php b/routes/web.php index 27c72b7..089e1b7 100644 --- a/routes/web.php +++ b/routes/web.php @@ -3,8 +3,8 @@ use App\Http\Controllers\CatController; use Illuminate\Support\Facades\Route; -Route::get('/cats', [CatController::class, 'index'])->name('cats.index'); // index page -Route::get('/cats/{id}', [CatController::class, 'show'])->name('cats.show'); // detail page +Route::get('/cats', [CatController::class, 'index'])->name('cats.index'); +Route::get('/cats/{id}', [CatController::class, 'show'])->name('cats.show'); Route::get('/', function() { return redirect()->route('cats.index'); }); From 6f90e3145e7c3fbb46ed02b155ba09fd477afa97 Mon Sep 17 00:00:00 2001 From: Magicard Date: Fri, 7 Nov 2025 09:22:56 +0000 Subject: [PATCH 07/10] added the new env vars to example --- .env.example | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.env.example b/.env.example index ef5b1ec..b1ccf19 100644 --- a/.env.example +++ b/.env.example @@ -5,6 +5,9 @@ APP_DEBUG=true APP_TIMEZONE=UTC APP_URL=http://localhost +CAT_API_KEY= +CAT_API_URL= + APP_LOCALE=en APP_FALLBACK_LOCALE=en APP_FAKER_LOCALE=en_US From 3bf9424c7a0d5a836611cba4ec0787e51277d8ab Mon Sep 17 00:00:00 2001 From: Magicard Date: Fri, 7 Nov 2025 09:24:11 +0000 Subject: [PATCH 08/10] spacing --- app/Services/CatService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Services/CatService.php b/app/Services/CatService.php index ca9e84b..dd35c6e 100644 --- a/app/Services/CatService.php +++ b/app/Services/CatService.php @@ -32,6 +32,7 @@ public function allBreeds(): array try { $response = $this->client->get('breeds'); return json_decode((string) $response->getBody(), true) ?? []; + } catch (RequestException $e) { return []; } @@ -49,7 +50,6 @@ public function findBreedById(string $breedId): ?array $response = $this->client->get("images/search", [ 'query' => ['breed_ids' => $breedId], ]); - return collect($this->decodeResponse($response))->first(); } catch (Throwable $error){ From 09a41e9b3311c15db21ded31bfdf048ab18d615e Mon Sep 17 00:00:00 2001 From: Teodor Sava Date: Fri, 7 Nov 2025 09:38:05 +0000 Subject: [PATCH 09/10] Update README with Teo Notes Added notes on changes made and future improvements. --- README.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5a41b5a..cc04b6e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,22 @@ # 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 +Changes: +- Removed key from key.example for security reasons +- Cache the api results to increase speed and responsiveness +- Used Guzzle instead of Http +- Kept Controller clean by using Service instead +- Redirect to Http.Cat if it returns 404 +- Use services.php + +Things i would add given more time: +- Query the api directly instead of filtering the blade file +- Create more tests for the app +- Add pagination +- Use Vue instead +- Make it prettier + + ## Introduction From 33aec00858c5cceee054b6c0a47693aec9b1928f Mon Sep 17 00:00:00 2001 From: Teodor Sava Date: Fri, 7 Nov 2025 09:45:15 +0000 Subject: [PATCH 10/10] Update README w better notes Expanded the README with detailed notes on changes made, including API key removal, caching implementation, service usage, and future improvements. --- README.md | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index cc04b6e..8edd610 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,27 @@ # Shopblocks PHP Developer Test: Pet-é-dex ## Teo Notes -Changes: -- Removed key from key.example for security reasons -- Cache the api results to increase speed and responsiveness -- Used Guzzle instead of Http -- Kept Controller clean by using Service instead -- Redirect to Http.Cat if it returns 404 -- Use services.php - -Things i would add given more time: -- Query the api directly instead of filtering the blade file -- Create more tests for the app -- Add pagination -- Use Vue instead -- Make it prettier +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.