Skip to content

Commit 3d8ea96

Browse files
committed
feat(article): added Get All and Show Article APIs
1 parent 8fbb984 commit 3d8ea96

File tree

7 files changed

+433
-4
lines changed

7 files changed

+433
-4
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Http\Controllers\Api\V1\Article;
6+
7+
use App\Http\Controllers\Controller;
8+
use App\Http\Requests\Api\V1\Article\GetArticlesRequest;
9+
use App\Http\Resources\Api\V1\Article\ArticleResource;
10+
use App\Services\ArticleService;
11+
use Dedoc\Scramble\Attributes\Group;
12+
use Illuminate\Http\JsonResponse;
13+
use Symfony\Component\HttpFoundation\Response;
14+
15+
#[Group('Articles', weight: 1)]
16+
class GetArticlesController extends Controller
17+
{
18+
public function __construct(
19+
private readonly ArticleService $articleService
20+
) {}
21+
22+
/**
23+
* Get Articles List
24+
*
25+
* Retrieve a paginated list of articles with optional filtering by category, tags, author, and search terms
26+
*
27+
* @unauthenticated
28+
*
29+
* @response array{status: true, message: string, data: array{data: ArticleResource[], links: array, meta: array}}
30+
*/
31+
public function __invoke(GetArticlesRequest $request): JsonResponse
32+
{
33+
try {
34+
$params = $request->withDefaults();
35+
36+
$articles = $this->articleService->getArticles($params);
37+
38+
$articleCollection = ArticleResource::collection($articles);
39+
40+
/**
41+
* Successful articles retrieval
42+
*/
43+
$articleCollectionData = $articleCollection->response()->getData(true);
44+
45+
// Ensure we have the expected array structure
46+
if (! is_array($articleCollectionData) || ! isset($articleCollectionData['data'], $articleCollectionData['meta'])) {
47+
throw new \RuntimeException('Unexpected response format from ArticleResource collection');
48+
}
49+
50+
return response()->apiSuccess(
51+
[
52+
'articles' => $articleCollectionData['data'],
53+
'meta' => $articleCollectionData['meta'],
54+
],
55+
__('common.success')
56+
);
57+
} catch (\Throwable $e) {
58+
/**
59+
* Internal server error
60+
*
61+
* @status 500
62+
*
63+
* @body array{status: false, message: string, data: null, error: null}
64+
*/
65+
return response()->apiError(
66+
__('common.something_went_wrong'),
67+
Response::HTTP_INTERNAL_SERVER_ERROR
68+
);
69+
}
70+
}
71+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Http\Controllers\Api\V1\Article;
6+
7+
use App\Http\Controllers\Controller;
8+
use App\Http\Resources\Api\V1\Article\ArticleResource;
9+
use App\Services\ArticleService;
10+
use Dedoc\Scramble\Attributes\Group;
11+
use Illuminate\Database\Eloquent\ModelNotFoundException;
12+
use Illuminate\Http\JsonResponse;
13+
use Symfony\Component\HttpFoundation\Response;
14+
15+
#[Group('Articles', weight: 1)]
16+
class ShowArticleController extends Controller
17+
{
18+
public function __construct(
19+
private readonly ArticleService $articleService
20+
) {}
21+
22+
/**
23+
* Get Article by Slug
24+
*
25+
* Retrieve a specific article by its slug identifier
26+
*
27+
* @unauthenticated
28+
*
29+
* @response array{status: true, message: string, data: ArticleResource}
30+
*/
31+
public function __invoke(string $slug): JsonResponse
32+
{
33+
try {
34+
$article = $this->articleService->getArticleBySlug($slug);
35+
36+
/**
37+
* Successful article retrieval
38+
*/
39+
return response()->apiSuccess(
40+
new ArticleResource($article),
41+
__('common.success')
42+
);
43+
} catch (ModelNotFoundException $e) {
44+
/**
45+
* Article not found
46+
*
47+
* @status 404
48+
*
49+
* @body array{status: false, message: string, data: null, error: null}
50+
*/
51+
return response()->apiError(
52+
__('common.not_found'),
53+
Response::HTTP_NOT_FOUND
54+
);
55+
} catch (\Throwable $e) {
56+
/**
57+
* Internal server error
58+
*
59+
* @status 500
60+
*
61+
* @body array{status: false, message: string, data: null, error: null}
62+
*/
63+
return response()->apiError(
64+
__('common.something_went_wrong'),
65+
Response::HTTP_INTERNAL_SERVER_ERROR
66+
);
67+
}
68+
}
69+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Http\Requests\Api\V1\Article;
6+
7+
use App\Enums\ArticleStatus;
8+
use Illuminate\Foundation\Http\FormRequest;
9+
use Illuminate\Validation\Rule;
10+
11+
class GetArticlesRequest extends FormRequest
12+
{
13+
/**
14+
* Determine if the user is authorized to make this request.
15+
*/
16+
public function authorize(): bool
17+
{
18+
return true;
19+
}
20+
21+
/**
22+
* Get the validation rules that apply to the request.
23+
*
24+
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
25+
*/
26+
public function rules(): array
27+
{
28+
return [
29+
'page' => ['integer', 'min:1'],
30+
'per_page' => ['integer', 'min:1', 'max:100'],
31+
'search' => ['string', 'max:255'],
32+
'status' => [Rule::enum(ArticleStatus::class)],
33+
'category_slug' => ['string'],
34+
'category_slug.*' => ['string', 'exists:categories,slug'],
35+
'tag_slug' => ['string'],
36+
'tag_slug.*' => ['string', 'exists:tags,slug'],
37+
'author_id' => ['integer', 'exists:users,id'],
38+
'created_by' => ['integer', 'exists:users,id'],
39+
'published_after' => ['date'],
40+
'published_before' => ['date'],
41+
'sort_by' => [Rule::in(['title', 'published_at', 'created_at', 'updated_at'])],
42+
'sort_direction' => [Rule::in(['asc', 'desc'])],
43+
];
44+
}
45+
46+
/**
47+
* Get the default values for missing parameters
48+
*
49+
* @return array<string, mixed>
50+
*/
51+
public function withDefaults(): array
52+
{
53+
return array_merge([
54+
'page' => 1,
55+
'per_page' => 15,
56+
'sort_by' => 'published_at',
57+
'sort_direction' => 'desc',
58+
'status' => ArticleStatus::PUBLISHED->value,
59+
], $this->validated());
60+
}
61+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Http\Resources\Api\V1\Article;
6+
7+
use Illuminate\Http\Request;
8+
use Illuminate\Http\Resources\Json\JsonResource;
9+
10+
/**
11+
* @mixin \App\Models\Article
12+
*/
13+
class ArticleResource extends JsonResource
14+
{
15+
/**
16+
* Transform the resource into an array.
17+
*
18+
* @return array<string, mixed>
19+
*/
20+
public function toArray(Request $request): array
21+
{
22+
return [
23+
'id' => $this->id,
24+
'slug' => $this->slug,
25+
'title' => $this->title,
26+
'subtitle' => $this->subtitle,
27+
'excerpt' => $this->excerpt,
28+
'content_html' => $this->content_html,
29+
'content_markdown' => $this->content_markdown,
30+
'featured_image' => $this->featured_image,
31+
'status' => $this->status,
32+
'published_at' => $this->published_at?->toISOString(),
33+
'meta_title' => $this->meta_title,
34+
'meta_description' => $this->meta_description,
35+
'created_at' => $this->created_at?->toISOString(),
36+
'updated_at' => $this->updated_at?->toISOString(),
37+
38+
// Relationships
39+
'author' => $this->whenLoaded('author', function () {
40+
return $this->author ? [
41+
'id' => $this->author->id,
42+
'name' => $this->author->name,
43+
'email' => $this->author->email,
44+
'avatar_url' => $this->author->avatar_url,
45+
'bio' => $this->author->bio,
46+
] : null;
47+
}),
48+
49+
'categories' => $this->whenLoaded('categories', function () {
50+
/** @var \Illuminate\Database\Eloquent\Collection<int, \App\Models\Category> $categories */
51+
$categories = $this->categories;
52+
53+
return $categories->map(function ($category) {
54+
return [
55+
'id' => $category->id,
56+
'name' => $category->name,
57+
'slug' => $category->slug,
58+
];
59+
})->values()->all();
60+
}),
61+
62+
'tags' => $this->whenLoaded('tags', function () {
63+
/** @var \Illuminate\Database\Eloquent\Collection<int, \App\Models\Tag> $tags */
64+
$tags = $this->tags;
65+
66+
return $tags->map(function ($tag) {
67+
return [
68+
'id' => $tag->id,
69+
'name' => $tag->name,
70+
'slug' => $tag->slug,
71+
];
72+
})->values()->all();
73+
}),
74+
75+
'authors' => $this->whenLoaded('authors', function () {
76+
/** @var \Illuminate\Database\Eloquent\Collection<int, \App\Models\User> $authors */
77+
$authors = $this->authors;
78+
79+
return $authors->map(function ($author) {
80+
/** @var \Illuminate\Database\Eloquent\Relations\Pivot|null $pivot */
81+
$pivot = $author->getAttribute('pivot');
82+
83+
return [
84+
'id' => $author->id,
85+
'name' => $author->name,
86+
'email' => $author->email,
87+
'avatar_url' => $author->avatar_url,
88+
'bio' => $author->bio,
89+
'role' => $pivot?->getAttribute('role'),
90+
];
91+
})->values()->all();
92+
}),
93+
94+
'comments_count' => $this->whenCounted('comments'),
95+
];
96+
}
97+
}

0 commit comments

Comments
 (0)