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:
@@ -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' => 'مقاله حذف شد']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Blog\Service;
|
||||
|
||||
use App\Blog\Entity\Blog;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
/**
|
||||
* پس از هر نوشتن روی بلاگ، کش ISR سایت عمومی (nobat724_front) را باطل میکند.
|
||||
*
|
||||
* سایت پاسخ `GET /api/v1/blog/{slug}` را با `revalidate: 3600` و tag ذخیره
|
||||
* میکند؛ بدون این فراخوانی، تغییر پنل ادمین تا یک ساعت روی سایت دیده نمیشود.
|
||||
*
|
||||
* قرارداد: `POST {webhookUrl}` با هدر `X-Revalidate-Secret` و بدنهٔ
|
||||
* `{"tags": ["blog-<slug>", "blog-<uuid>", "blog-list"]}`.
|
||||
*
|
||||
* fail-open: شکست شبکه فقط لاگ میشود و هرگز ذخیرهٔ مقاله را نمیشکند.
|
||||
*/
|
||||
class BlogCacheInvalidator
|
||||
{
|
||||
public function __construct(
|
||||
private readonly HttpClientInterface $httpClient,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly string $webhookUrl, // خالی = سایت عمومی پیکربندی نشده
|
||||
private readonly string $webhookSecret,
|
||||
) {}
|
||||
|
||||
public function invalidate(Blog $blog): void
|
||||
{
|
||||
if ($this->webhookUrl === '' || $this->webhookSecret === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
// مقادیر قبل از هر تغییر بعدی برداشته میشوند تا حذف مقاله هم قابل باطلسازی باشد.
|
||||
$tags = array_values(array_filter([
|
||||
$blog->getSlug() !== '' ? 'blog-' . $blog->getSlug() : null,
|
||||
'blog-' . $blog->getUuid(),
|
||||
'blog-list',
|
||||
]));
|
||||
|
||||
try {
|
||||
$this->httpClient->request('POST', $this->webhookUrl, [
|
||||
'json' => ['tags' => $tags],
|
||||
'headers' => ['X-Revalidate-Secret' => $this->webhookSecret],
|
||||
'timeout' => 3,
|
||||
])->getStatusCode();
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->warning('blog cache invalidation failed', [
|
||||
'uuid' => $blog->getUuid(),
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user