Skip to content

Commit 6b53386

Browse files
committed
feat(categories & tags): added New APIs for categories and tags
1 parent bb9226c commit 6b53386

File tree

8 files changed

+260
-0
lines changed

8 files changed

+260
-0
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Http\Controllers\Api\V1\Category;
6+
7+
use App\Http\Controllers\Controller;
8+
use App\Http\Resources\Api\V1\Category\CategoryResource;
9+
use App\Services\ArticleService;
10+
use Dedoc\Scramble\Attributes\Group;
11+
use Illuminate\Http\JsonResponse;
12+
use Symfony\Component\HttpFoundation\Response;
13+
14+
#[Group('Categories', weight: 2)]
15+
class GetCategoriesController extends Controller
16+
{
17+
public function __construct(private readonly ArticleService $articleService) {}
18+
19+
/**
20+
* Get All Categories
21+
*
22+
* @unauthenticated
23+
*
24+
* @response array{status: true, message: string, data: CategoryResource[]}
25+
*/
26+
public function __invoke(): JsonResponse
27+
{
28+
try {
29+
$categories = $this->articleService->getAllCategories();
30+
31+
return response()->apiSuccess(
32+
CategoryResource::collection($categories),
33+
__('common.success')
34+
);
35+
} catch (\Throwable $e) {
36+
return response()->apiError(
37+
__('common.error'),
38+
Response::HTTP_INTERNAL_SERVER_ERROR,
39+
null,
40+
$e->getMessage()
41+
);
42+
}
43+
}
44+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Http\Controllers\Api\V1\Tag;
6+
7+
use App\Http\Controllers\Controller;
8+
use App\Http\Resources\Api\V1\Tag\TagResource;
9+
use App\Services\ArticleService;
10+
use Dedoc\Scramble\Attributes\Group;
11+
use Illuminate\Http\JsonResponse;
12+
use Symfony\Component\HttpFoundation\Response;
13+
14+
#[Group('Tags', weight: 2)]
15+
class GetTagsController extends Controller
16+
{
17+
public function __construct(private readonly ArticleService $articleService) {}
18+
19+
/**
20+
* Get All Tags
21+
*
22+
* @unauthenticated
23+
*
24+
* @response array{status: true, message: string, data: TagResource[]}
25+
*/
26+
public function __invoke(): JsonResponse
27+
{
28+
try {
29+
$tags = $this->articleService->getAllTags();
30+
31+
return response()->apiSuccess(
32+
TagResource::collection($tags),
33+
__('common.success')
34+
);
35+
} catch (\Throwable $e) {
36+
return response()->apiError(
37+
__('common.error'),
38+
Response::HTTP_INTERNAL_SERVER_ERROR,
39+
null,
40+
$e->getMessage()
41+
);
42+
}
43+
}
44+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Http\Resources\Api\V1\Category;
6+
7+
use Illuminate\Http\Resources\Json\JsonResource;
8+
9+
/**
10+
* @mixin \App\Models\Category
11+
*/
12+
class CategoryResource extends JsonResource
13+
{
14+
/**
15+
* Transform the resource into an array.
16+
*
17+
* @param \Illuminate\Http\Request $request
18+
* @return array<string, mixed>
19+
*/
20+
public function toArray($request): array
21+
{
22+
return [
23+
'id' => $this->id,
24+
'name' => $this->name,
25+
'slug' => $this->slug,
26+
];
27+
}
28+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Http\Resources\Api\V1\Tag;
6+
7+
use Illuminate\Http\Resources\Json\JsonResource;
8+
9+
/**
10+
* @mixin \App\Models\Tag
11+
*/
12+
class TagResource extends JsonResource
13+
{
14+
/**
15+
* Transform the resource into an array.
16+
*
17+
* @param \Illuminate\Http\Request $request
18+
* @return array<string, mixed>
19+
*/
20+
public function toArray($request): array
21+
{
22+
return [
23+
'id' => $this->id,
24+
'name' => $this->name,
25+
'slug' => $this->slug,
26+
];
27+
}
28+
}

app/Services/ArticleService.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,4 +123,24 @@ private function applyFilters(Builder $query, array $params): void
123123
->where('published_at', '<=', now());
124124
}
125125
}
126+
127+
/**
128+
* Get all categories
129+
*
130+
* @return \Illuminate\Database\Eloquent\Collection<int, \App\Models\Category>
131+
*/
132+
public function getAllCategories()
133+
{
134+
return \App\Models\Category::query()->get(['id', 'name', 'slug']);
135+
}
136+
137+
/**
138+
* Get all tags
139+
*
140+
* @return \Illuminate\Database\Eloquent\Collection<int, \App\Models\Tag>
141+
*/
142+
public function getAllTags()
143+
{
144+
return \App\Models\Tag::query()->get(['id', 'name', 'slug']);
145+
}
126146
}

routes/api_v1.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,10 @@
2424
Route::get('/', \App\Http\Controllers\Api\V1\Article\GetArticlesController::class)->name('api.v1.articles.index');
2525
Route::get('/{slug}', \App\Http\Controllers\Api\V1\Article\ShowArticleController::class)->name('api.v1.articles.show');
2626
});
27+
28+
// Category Routes (Public)
29+
Route::get('categories', \App\Http\Controllers\Api\V1\Category\GetCategoriesController::class)->name('api.v1.categories.index');
30+
31+
// Tag Routes (Public)
32+
Route::get('tags', \App\Http\Controllers\Api\V1\Tag\GetTagsController::class)->name('api.v1.tags.index');
2733
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use App\Models\Category;
6+
7+
describe('API/V1/Category/GetCategoriesController', function () {
8+
it('can get all categories', function () {
9+
Category::factory()->count(3)->create();
10+
11+
$response = $this->getJson('/api/v1/categories');
12+
13+
$response->assertStatus(200)
14+
->assertJson(['status' => true])
15+
->assertJson(['message' => __('common.success')])
16+
->assertJsonStructure([
17+
'data' => [
18+
'*' => [
19+
'id',
20+
'name',
21+
'slug',
22+
],
23+
],
24+
]);
25+
26+
expect($response->json('data'))->toHaveCount(3);
27+
});
28+
29+
it('returns error if service throws', function () {
30+
$this->mock(\App\Services\ArticleService::class, function ($mock) {
31+
$mock->shouldReceive('getAllCategories')
32+
->andThrow(new Exception('fail'));
33+
});
34+
35+
$response = $this->getJson('/api/v1/categories');
36+
37+
$response->assertStatus(500)
38+
->assertJson(['status' => false])
39+
->assertJson(['message' => __('common.error')])
40+
->assertJsonStructure([
41+
'data',
42+
'error',
43+
]);
44+
});
45+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use App\Models\Tag;
6+
7+
describe('API/V1/Tag/GetTagsController', function () {
8+
it('can get all tags', function () {
9+
Tag::factory()->count(4)->create();
10+
11+
$response = $this->getJson('/api/v1/tags');
12+
13+
$response->assertStatus(200)
14+
->assertJson(['status' => true])
15+
->assertJson(['message' => __('common.success')])
16+
->assertJsonStructure([
17+
'data' => [
18+
'*' => [
19+
'id',
20+
'name',
21+
'slug',
22+
],
23+
],
24+
]);
25+
26+
expect($response->json('data'))->toHaveCount(4);
27+
});
28+
29+
it('returns error if service throws', function () {
30+
$this->mock(\App\Services\ArticleService::class, function ($mock) {
31+
$mock->shouldReceive('getAllTags')
32+
->andThrow(new Exception('fail'));
33+
});
34+
35+
$response = $this->getJson('/api/v1/tags');
36+
37+
$response->assertStatus(500)
38+
->assertJson(['status' => false])
39+
->assertJson(['message' => __('common.error')])
40+
->assertJsonStructure([
41+
'data',
42+
'error',
43+
]);
44+
});
45+
});

0 commit comments

Comments
 (0)