Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/App/src/Migration/Version20260813075850.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Migration;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260813075850 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}

public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->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');
}
}
33 changes: 33 additions & 0 deletions src/App/templates/error/410.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{% extends '@layout/default.html.twig' %}

{% block title %}410 Gone{% endblock %}
{% block canonical %}{% endblock %}

{% block content %}
<section class="hero">
<div class="wrap">
<span class="eyebrow">Oops! This is awkward.</span>
<h1>410</h1>
<p class="sub">
The content was misleading, old and obsolete, so we removed it.
It is gone for good and won't be coming back.
</p>

<div class="hero-ctas">
<a class="btn btn-primary" href="{{ path('app::index') }}">Back to Home</a>
<a class="btn btn-ghost" href="{{ url('page::blog') }}">Browse new posts</a>
</div>

{% if categories is defined and categories|length > 0 %}
<div class="d-flex flex-wrap gap-2 mt-4">
{% for category in categories %}
<a href="{{ url('page::category-resource', {slug: category.slug}) }}"
class="badge bg-primary-subtle text-primary text-uppercase fw-bold text-decoration-none px-3 py-2 rounded-pill">
{{ category.name }}
</a>
{% endfor %}
</div>
{% endif %}
</div>
</section>
{% endblock %}
1 change: 1 addition & 0 deletions src/Blog/src/Enum/PostStatusEnum.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ enum PostStatusEnum: string
case Draft = 'draft';
case Published = 'published';
case Private = 'private';
case Archived = 'archived';

/**
* @return non-empty-string[]
Expand Down
16 changes: 16 additions & 0 deletions src/Blog/src/Handler/GetCategoryResourceHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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
);
}
}
20 changes: 20 additions & 0 deletions src/Blog/src/Handler/GetPostResourceHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -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
);
}
}
4 changes: 1 addition & 3 deletions src/Blog/src/Repository/PostRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
59 changes: 59 additions & 0 deletions test/Unit/Blog/Handler/GetCategoryResourceHandlerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

namespace LightTest\Unit\Blog\Handler;

use Laminas\Diactoros\ServerRequest;
use Light\Blog\Entity\Category;
use Light\Blog\Handler\GetCategoryResourceHandler;
use Light\Blog\Repository\CategoryRepository;
use LightTest\Unit\UnitTest;
use Mezzio\Template\TemplateRendererInterface;
use PHPUnit\Framework\MockObject\Exception;
use Psr\Http\Message\ResponseInterface;

class GetCategoryResourceHandlerTest extends UnitTest
{
/**
* @throws Exception
*/
public function testHandleReturnsNotFoundWhenCategoryDoesNotExist(): void
{
$response = $this->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('<html lang="en"></html>');

$handler = new GetCategoryResourceHandler($template, $categoryRepository);

return $handler->handle((new ServerRequest())->withAttribute('slug', 'a-category'));
}
}
79 changes: 79 additions & 0 deletions test/Unit/Blog/Handler/GetPostResourceHandlerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

declare(strict_types=1);

namespace LightTest\Unit\Blog\Handler;

use Laminas\Diactoros\ServerRequest;
use Light\Blog\Entity\Category;
use Light\Blog\Entity\Post;
use Light\Blog\Enum\PostStatusEnum;
use Light\Blog\Handler\GetPostResourceHandler;
use Light\Blog\Repository\CategoryRepository;
use Light\Blog\Repository\PostRepository;
use LightTest\Unit\UnitTest;
use Mezzio\Template\TemplateRendererInterface;
use PHPUnit\Framework\MockObject\Exception;
use Psr\Http\Message\ResponseInterface;

class GetPostResourceHandlerTest extends UnitTest
{
/**
* @throws Exception
*/
public function testHandleReturnsNotFoundWhenArticleDoesNotExist(): void
{
$response = $this->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('<html lang="en"></html>');

$handler = new GetPostResourceHandler($template, $postRepository, $categoryRepository);

return $handler->handle(
(new ServerRequest())->withAttribute('slug', 'a-slug')->withAttribute('categorySlug', 'a-category')
);
}
}