From a8638085f602dacfda0c96da106f2e3f0dfc7011 Mon Sep 17 00:00:00 2001 From: OStefan2001 Date: Thu, 13 Aug 2026 11:14:31 +0300 Subject: [PATCH] Archived posts with 410 --- .../src/Migration/Version20260813075850.php | 31 ++++++++ src/App/templates/error/410.html.twig | 33 ++++++++ src/Blog/src/Enum/PostStatusEnum.php | 1 + .../Handler/GetCategoryResourceHandler.php | 16 ++++ .../src/Handler/GetPostResourceHandler.php | 20 +++++ src/Blog/src/Repository/PostRepository.php | 4 +- .../GetCategoryResourceHandlerTest.php | 59 ++++++++++++++ .../Handler/GetPostResourceHandlerTest.php | 79 +++++++++++++++++++ 8 files changed, 240 insertions(+), 3 deletions(-) create mode 100644 src/App/src/Migration/Version20260813075850.php create mode 100644 src/App/templates/error/410.html.twig create mode 100644 test/Unit/Blog/Handler/GetCategoryResourceHandlerTest.php create mode 100644 test/Unit/Blog/Handler/GetPostResourceHandlerTest.php diff --git a/src/App/src/Migration/Version20260813075850.php b/src/App/src/Migration/Version20260813075850.php new file mode 100644 index 00000000..8b5d099e --- /dev/null +++ b/src/App/src/Migration/Version20260813075850.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE post CHANGE status status ENUM(\'draft\', \'published\', \'private\', \'archived\') DEFAULT \'draft\' NOT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE post CHANGE status status ENUM(\'draft\', \'published\', \'private\') DEFAULT \'draft\' NOT NULL'); + } +} diff --git a/src/App/templates/error/410.html.twig b/src/App/templates/error/410.html.twig new file mode 100644 index 00000000..b1e8b9ec --- /dev/null +++ b/src/App/templates/error/410.html.twig @@ -0,0 +1,33 @@ +{% extends '@layout/default.html.twig' %} + +{% block title %}410 Gone{% endblock %} +{% block canonical %}{% endblock %} + +{% block content %} +
+
+ Oops! This is awkward. +

410

+

+ The content was misleading, old and obsolete, so we removed it. + It is gone for good and won't be coming back. +

+ + + + {% if categories is defined and categories|length > 0 %} +
+ {% for category in categories %} + + {{ category.name }} + + {% endfor %} +
+ {% endif %} +
+
+{% endblock %} diff --git a/src/Blog/src/Enum/PostStatusEnum.php b/src/Blog/src/Enum/PostStatusEnum.php index eb5d5181..acba9854 100644 --- a/src/Blog/src/Enum/PostStatusEnum.php +++ b/src/Blog/src/Enum/PostStatusEnum.php @@ -11,6 +11,7 @@ enum PostStatusEnum: string case Draft = 'draft'; case Published = 'published'; case Private = 'private'; + case Archived = 'archived'; /** * @return non-empty-string[] diff --git a/src/Blog/src/Handler/GetCategoryResourceHandler.php b/src/Blog/src/Handler/GetCategoryResourceHandler.php index aca3b78f..8bbdd354 100644 --- a/src/Blog/src/Handler/GetCategoryResourceHandler.php +++ b/src/Blog/src/Handler/GetCategoryResourceHandler.php @@ -31,6 +31,9 @@ public function handle(ServerRequestInterface $request): ResponseInterface if ($category === null) { return $this->notFound($categories); } + if (! $category->isVisible()) { + return $this->gone($categories); + } $meta = $category; $queryParams = $request->getQueryParams(); @@ -68,4 +71,17 @@ private function notFound(array $categories): HtmlResponse StatusCodeInterface::STATUS_NOT_FOUND ); } + + /** + * @param Category[] $categories + */ + private function gone(array $categories): HtmlResponse + { + return new HtmlResponse( + $this->template->render('error::410', [ + 'categories' => $categories, + ]), + StatusCodeInterface::STATUS_GONE + ); + } } diff --git a/src/Blog/src/Handler/GetPostResourceHandler.php b/src/Blog/src/Handler/GetPostResourceHandler.php index ce4508dd..e154385a 100644 --- a/src/Blog/src/Handler/GetPostResourceHandler.php +++ b/src/Blog/src/Handler/GetPostResourceHandler.php @@ -7,6 +7,7 @@ use Fig\Http\Message\StatusCodeInterface; use Laminas\Diactoros\Response\HtmlResponse; use Light\Blog\Entity\Category; +use Light\Blog\Enum\PostStatusEnum; use Light\Blog\Repository\CategoryRepository; use Light\Blog\Repository\PostRepository; use Mezzio\Template\TemplateRendererInterface; @@ -33,6 +34,12 @@ public function handle(ServerRequestInterface $request): ResponseInterface if ($article === null) { return $this->notFound($categories); } + if ($article->getStatus() === PostStatusEnum::Archived) { + return $this->gone($categories); + } + if ($article->getStatus() !== PostStatusEnum::Published) { + return $this->notFound($categories); + } $meta = $article; $adjacent = $this->articleRepository->getAdjacentPosts($article); try { @@ -64,4 +71,17 @@ private function notFound(array $categories): HtmlResponse StatusCodeInterface::STATUS_NOT_FOUND ); } + + /** + * @param Category[] $categories + */ + private function gone(array $categories): HtmlResponse + { + return new HtmlResponse( + $this->template->render('error::410', [ + 'categories' => $categories, + ]), + StatusCodeInterface::STATUS_GONE + ); + } } diff --git a/src/Blog/src/Repository/PostRepository.php b/src/Blog/src/Repository/PostRepository.php index db43225f..89246989 100644 --- a/src/Blog/src/Repository/PostRepository.php +++ b/src/Blog/src/Repository/PostRepository.php @@ -37,9 +37,7 @@ public function getArticleResource(string $slug, ?string $categorySlug = null): ->select('articles') ->from(Post::class, 'articles') ->where('articles.slug = :slug') - ->andWhere('articles.status = :published') - ->setParameter('slug', $slug) - ->setParameter('published', PostStatusEnum::Published); + ->setParameter('slug', $slug); if ($categorySlug !== null) { $qb->leftJoin('articles.category', 'category') diff --git a/test/Unit/Blog/Handler/GetCategoryResourceHandlerTest.php b/test/Unit/Blog/Handler/GetCategoryResourceHandlerTest.php new file mode 100644 index 00000000..5c7281c4 --- /dev/null +++ b/test/Unit/Blog/Handler/GetCategoryResourceHandlerTest.php @@ -0,0 +1,59 @@ +handle(null); + + $this->assertSame(404, $response->getStatusCode()); + } + + /** + * @throws Exception + */ + public function testHandleReturnsGoneWhenCategoryIsNotVisible(): void + { + $category = $this->createStub(Category::class); + $category->method('isVisible')->willReturn(false); + + $response = $this->handle($category); + + $this->assertSame(410, $response->getStatusCode()); + } + + /** + * @throws Exception + */ + private function handle(?Category $category): ResponseInterface + { + $categories = [$this->createStub(Category::class)]; + + $categoryRepository = $this->createMock(CategoryRepository::class); + $categoryRepository->expects($this->once())->method('getCategories')->willReturn($categories); + $categoryRepository->expects($this->once())->method('getCategoryResource')->willReturn($category); + + $template = $this->createMock(TemplateRendererInterface::class); + $template->expects($this->once())->method('render')->willReturn(''); + + $handler = new GetCategoryResourceHandler($template, $categoryRepository); + + return $handler->handle((new ServerRequest())->withAttribute('slug', 'a-category')); + } +} diff --git a/test/Unit/Blog/Handler/GetPostResourceHandlerTest.php b/test/Unit/Blog/Handler/GetPostResourceHandlerTest.php new file mode 100644 index 00000000..c78176a4 --- /dev/null +++ b/test/Unit/Blog/Handler/GetPostResourceHandlerTest.php @@ -0,0 +1,79 @@ +handle(null); + + $this->assertSame(404, $response->getStatusCode()); + } + + /** + * @throws Exception + */ + public function testHandleReturnsGoneWhenArticleIsArchived(): void + { + $article = $this->createStub(Post::class); + $article->method('getStatus')->willReturn(PostStatusEnum::Archived); + + $response = $this->handle($article); + + $this->assertSame(410, $response->getStatusCode()); + } + + /** + * @throws Exception + */ + public function testHandleReturnsNotFoundWhenArticleIsNotPublished(): void + { + $article = $this->createStub(Post::class); + $article->method('getStatus')->willReturn(PostStatusEnum::Draft); + + $response = $this->handle($article); + + $this->assertSame(404, $response->getStatusCode()); + } + + /** + * @throws Exception + */ + private function handle(?Post $article): ResponseInterface + { + $categories = [$this->createStub(Category::class)]; + + $postRepository = $this->createMock(PostRepository::class); + $postRepository->expects($this->once())->method('getArticleResource')->willReturn($article); + + $categoryRepository = $this->createMock(CategoryRepository::class); + $categoryRepository->expects($this->once())->method('getCategories')->willReturn($categories); + + $template = $this->createMock(TemplateRendererInterface::class); + $template->expects($this->once())->method('render')->willReturn(''); + + $handler = new GetPostResourceHandler($template, $postRepository, $categoryRepository); + + return $handler->handle( + (new ServerRequest())->withAttribute('slug', 'a-slug')->withAttribute('categorySlug', 'a-category') + ); + } +}