feat(blog): add admin endpoint for blog details and cache invalidation

- Implemented `adminDetail()` method in `BlogController` to retrieve blog posts of any status for admin editing.
- Introduced `BlogCacheInvalidator` service to handle cache invalidation after blog create/update/delete actions.
- Updated existing methods in `BlogController` and `RepresentationBlogController` to call cache invalidation on blog modifications.
- Enhanced `BlogFormPage` and `RepresentationBlogFormPage` to utilize the new admin endpoint for fetching blog data.
- Added tests for `BlogCacheInvalidator` to ensure proper functionality and error handling.
- Updated documentation to reflect new API endpoint and cache invalidation behavior.
This commit is contained in:
hamed
2026-07-27 18:54:35 +03:30
parent e4edaea9b8
commit 15abcb5c8a
12 changed files with 972 additions and 47 deletions
+54
View File
@@ -24,6 +24,7 @@ class BlogController extends BaseController
private readonly CityRepository $cityRepo,
private readonly FileValidatorService $fileValidator,
private readonly \App\Blog\Service\BlogWriter $blogWriter,
private readonly \App\Blog\Service\BlogCacheInvalidator $cacheInvalidator,
private readonly string $projectDir,
) {}
@@ -203,6 +204,52 @@ class BlogController extends BaseController
}
#[OA\Get(
path: '/api/v1/admin/blog/{uuid}',
summary: 'Get one blog post of ANY status for the admin edit form',
security: [['bearerAuth' => []]],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, schema: new OA\Schema(type: 'string', format: 'uuid')),
],
responses: [
new OA\Response(
response: 200,
description: 'Blog detail of any status (draft/published/archived)',
content: new OA\JsonContent(
properties: [
new OA\Property(property: 'success', type: 'boolean', example: true),
new OA\Property(
property: 'data',
properties: [
new OA\Property(property: 'data', type: 'object'),
],
type: 'object'
),
]
)
),
new OA\Response(response: 401, description: 'Unauthorized'),
new OA\Response(response: 403, description: 'Forbidden — admin role required'),
new OA\Response(response: 404, description: 'Blog post not found'),
]
)]
#[IsGranted('ROLE_ADMIN')]
// requirement اجباری است: این route قبل از /api/v1/admin/blog/review-queue ثبت
// می‌شود و بدون الگوی uuid، آن مسیر را با uuid="review-queue" می‌دزدید.
#[Route('/api/v1/admin/blog/{uuid}', methods: ['GET'], requirements: ['uuid' => '[0-9a-fA-F-]{36}'])]
public function adminDetail(string $uuid): JsonResponse
{
// برخلاف detail() عمومی اینجا هیچ فیلتر status/city ای نیست: فرم ویرایش باید
// پیش‌نویس و آرشیو را هم کامل بارگذاری کند. شکل پاسخ عمداً همان double-nested
// اندپوینت عمومی است تا مصرف‌کنندهٔ فعلی (data.data.data) دست‌نخورده بماند.
$blog = $this->blogRepo->findByUuid($uuid);
if ($blog === null) {
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'مقاله یافت نشد', 404);
}
return $this->success(['data' => $blog->toArray()]);
}
#[OA\Post(
path: '/api/v1/blog',
summary: 'Create a new blog post (ROLE_ADMIN, or ROLE_IMPORTER as a pending-review draft)',
@@ -320,6 +367,7 @@ class BlogController extends BaseController
}
$this->blogRepo->save($blog);
$this->cacheInvalidator->invalidate($blog);
return $this->success(['data' => $blog->toArray()], 201);
}
@@ -392,6 +440,7 @@ class BlogController extends BaseController
$this->blogWriter->applySeoFields($blog, $data);
$this->blogRepo->save($blog);
$this->cacheInvalidator->invalidate($blog);
return $this->success(['data' => $blog->toArray()]);
}
@@ -489,6 +538,7 @@ class BlogController extends BaseController
}
$this->blogRepo->save($blog);
$this->cacheInvalidator->invalidate($blog);
return $this->success(['data' => $blog->toArray()]);
}
@@ -536,7 +586,11 @@ class BlogController extends BaseController
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'مقاله یافت نشد', 404);
}
// باطل‌سازی قبل از حذف: بعد از remove، Doctrine آبجکت را detach می‌کند و
// اتکا به مقادیر باقی‌مانده در حافظه شکننده است.
$this->cacheInvalidator->invalidate($blog);
$this->blogRepo->remove($blog);
return $this->success(['message' => 'مقاله با موفقیت حذف شد']);
}
@@ -5,6 +5,7 @@ namespace App\Blog\Controller;
use App\Auth\Entity\User;
use App\Blog\Entity\Blog;
use App\Blog\Repository\BlogRepository;
use App\Blog\Service\BlogCacheInvalidator;
use App\Blog\Service\BlogWriter;
use App\Location\Entity\City;
use App\Location\Repository\CityRepository;
@@ -33,6 +34,7 @@ class RepresentationBlogController extends BaseController
private readonly RepresentationRepository $repRepo,
private readonly CityRepository $cityRepo,
private readonly BlogWriter $writer,
private readonly BlogCacheInvalidator $cacheInvalidator,
) {}
private function currentRepresentation(User $user): Representation
@@ -124,6 +126,7 @@ class RepresentationBlogController extends BaseController
$blog->setSlug($blog->getSlug() . '-' . substr(uniqid(), -4));
}
$this->blogRepo->save($blog);
$this->cacheInvalidator->invalidate($blog);
return $this->success(['data' => $blog->toArray()], 201);
}
@@ -149,6 +152,7 @@ class RepresentationBlogController extends BaseController
}
$this->writer->applySeoFields($blog, $data);
$this->blogRepo->save($blog);
$this->cacheInvalidator->invalidate($blog);
return $this->success(['data' => $blog->toArray()]);
}
@@ -159,7 +163,9 @@ class RepresentationBlogController extends BaseController
{
$rep = $this->currentRepresentation($user);
$blog = $this->ownedBlogOr404($uuid, $rep);
$this->cacheInvalidator->invalidate($blog);
$this->blogRepo->remove($blog);
return $this->success(['message' => 'مقاله حذف شد']);
}
}