diff --git a/public/robots.txt.dist b/public/robots.txt.dist
index 8abe6fcf..750ebd68 100644
--- a/public/robots.txt.dist
+++ b/public/robots.txt.dist
@@ -7,3 +7,6 @@ Disallow: /
# To be used in PRODUCTION
User-Agent: *
Allow: /
+
+Content-Signal: ai-train=yes, search=yes, ai-input=yes
+Sitemap: https://www.dotkernel.com/sitemap.xml
diff --git a/src/App/src/Factory/GetIndexViewHandlerFactory.php b/src/App/src/Factory/GetIndexViewHandlerFactory.php
index 6fda5e30..8a580348 100644
--- a/src/App/src/Factory/GetIndexViewHandlerFactory.php
+++ b/src/App/src/Factory/GetIndexViewHandlerFactory.php
@@ -12,6 +12,7 @@
use Psr\Container\NotFoundExceptionInterface;
use function assert;
+use function getcwd;
class GetIndexViewHandlerFactory
{
@@ -26,6 +27,6 @@ public function __invoke(ContainerInterface $container, string $requestedName):
$template = $container->get(TemplateRendererInterface::class);
assert($template instanceof TemplateRendererInterface);
- return new GetIndexViewHandler($template, $postRepository);
+ return new GetIndexViewHandler($template, $postRepository, getcwd() . '/public/md-pages');
}
}
diff --git a/src/App/src/Handler/GetIndexViewHandler.php b/src/App/src/Handler/GetIndexViewHandler.php
index f2fdf39b..73d360e9 100644
--- a/src/App/src/Handler/GetIndexViewHandler.php
+++ b/src/App/src/Handler/GetIndexViewHandler.php
@@ -4,23 +4,41 @@
namespace Light\App\Handler;
+use Fig\Http\Message\StatusCodeInterface;
use Laminas\Diactoros\Response\HtmlResponse;
+use Laminas\Diactoros\Response\TextResponse;
use Light\Blog\Repository\PostRepository;
use Mezzio\Template\TemplateRendererInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
+use function file_get_contents;
+use function is_file;
+use function str_contains;
+
class GetIndexViewHandler implements RequestHandlerInterface
{
public function __construct(
protected TemplateRendererInterface $template,
protected PostRepository $postRepository,
+ protected string $mdPagesPath,
) {
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
+ if (str_contains($request->getHeaderLine('Accept'), 'text/markdown')) {
+ $markdownFile = $this->mdPagesPath . '/index.md';
+ if (is_file($markdownFile)) {
+ return new TextResponse(
+ (string) file_get_contents($markdownFile),
+ StatusCodeInterface::STATUS_OK,
+ ['Content-Type' => 'text/markdown; charset=utf-8'],
+ );
+ }
+ }
+
$posts = $this->postRepository->getRecentPosts(3);
return new HtmlResponse(
$this->template->render('app::index', ['posts' => $posts])
diff --git a/src/App/templates/layout/default.html.twig b/src/App/templates/layout/default.html.twig
index 33f8b021..b5499bc9 100644
--- a/src/App/templates/layout/default.html.twig
+++ b/src/App/templates/layout/default.html.twig
@@ -14,6 +14,8 @@
+
+
{% block head_links %}{% endblock %}
diff --git a/src/Blog/src/ConfigProvider.php b/src/Blog/src/ConfigProvider.php
index 55955446..4a5b38af 100644
--- a/src/Blog/src/ConfigProvider.php
+++ b/src/Blog/src/ConfigProvider.php
@@ -9,6 +9,7 @@
use Light\Blog\Factory\Author\AuthorCollectionHandlerFactory;
use Light\Blog\Factory\Author\AuthorResourceHandlerFactory;
use Light\Blog\Factory\Author\AuthorResourceRepositoryFactory;
+use Light\Blog\Factory\BlogServiceFactory;
use Light\Blog\Factory\Category\CategoryCollectionHandlerFactory;
use Light\Blog\Factory\Category\CategoryCollectionRepositoryFactory;
use Light\Blog\Factory\Category\CategoryResourceHandlerFactory;
@@ -28,6 +29,8 @@
use Light\Blog\Repository\CategoryRepository;
use Light\Blog\Repository\PostRepository;
use Light\Blog\Repository\TagRepository;
+use Light\Blog\Service\BlogService;
+use Light\Blog\Service\BlogServiceInterface;
use Mezzio\Application;
class ConfigProvider
@@ -51,6 +54,7 @@ public function __invoke(): array
* @return array{
* delegators: array>,
* factories: array,
+ * aliases: array,
* }
*/
private function getDependencies(): array
@@ -73,6 +77,10 @@ private function getDependencies(): array
GetAuthorCollectionHandler::class => AuthorCollectionHandlerFactory::class,
TagRepository::class => TagResourceRepositoryFactory::class,
GetTagResourceHandler::class => TagResourceHandlerFactory::class,
+ BlogService::class => BlogServiceFactory::class,
+ ],
+ 'aliases' => [
+ BlogServiceInterface::class => BlogService::class,
],
];
}
diff --git a/src/Blog/src/Factory/Author/AuthorResourceHandlerFactory.php b/src/Blog/src/Factory/Author/AuthorResourceHandlerFactory.php
index fa6392f2..3709085f 100644
--- a/src/Blog/src/Factory/Author/AuthorResourceHandlerFactory.php
+++ b/src/Blog/src/Factory/Author/AuthorResourceHandlerFactory.php
@@ -8,6 +8,7 @@
use Light\Blog\Repository\AuthorRepository;
use Light\Blog\Repository\CategoryRepository;
use Light\Blog\Repository\PostRepository;
+use Light\Blog\Service\BlogServiceInterface;
use Mezzio\Template\TemplateRendererInterface;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
@@ -27,11 +28,13 @@ public function __invoke(ContainerInterface $container, string $requestedName):
$template = $container->get(TemplateRendererInterface::class);
$postRepository = $container->get(PostRepository::class);
$categoryRepository = $container->get(CategoryRepository::class);
+ $blogService = $container->get(BlogServiceInterface::class);
assert($repository instanceof AuthorRepository);
assert($template instanceof TemplateRendererInterface);
assert($postRepository instanceof PostRepository);
+ assert($blogService instanceof BlogServiceInterface);
- return new GetAuthorResourceHandler($template, $repository, $postRepository, $categoryRepository);
+ return new GetAuthorResourceHandler($template, $repository, $postRepository, $categoryRepository, $blogService);
}
}
diff --git a/src/Blog/src/Factory/BlogServiceFactory.php b/src/Blog/src/Factory/BlogServiceFactory.php
new file mode 100644
index 00000000..65400300
--- /dev/null
+++ b/src/Blog/src/Factory/BlogServiceFactory.php
@@ -0,0 +1,30 @@
+get(TemplateRendererInterface::class);
+ assert($template instanceof TemplateRendererInterface);
+
+ return new BlogService($template, getcwd() . '/public/md-articles');
+ }
+}
diff --git a/src/Blog/src/Factory/Category/CategoryResourceHandlerFactory.php b/src/Blog/src/Factory/Category/CategoryResourceHandlerFactory.php
index 604f991b..ebe4bd8b 100644
--- a/src/Blog/src/Factory/Category/CategoryResourceHandlerFactory.php
+++ b/src/Blog/src/Factory/Category/CategoryResourceHandlerFactory.php
@@ -6,6 +6,7 @@
use Light\Blog\Handler\GetCategoryResourceHandler;
use Light\Blog\Repository\CategoryRepository;
+use Light\Blog\Service\BlogServiceInterface;
use Mezzio\Template\TemplateRendererInterface;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
@@ -21,12 +22,14 @@ class CategoryResourceHandlerFactory
*/
public function __invoke(ContainerInterface $container, string $requestedName): GetCategoryResourceHandler
{
- $repository = $container->get(CategoryRepository::class);
- $template = $container->get(TemplateRendererInterface::class);
+ $repository = $container->get(CategoryRepository::class);
+ $template = $container->get(TemplateRendererInterface::class);
+ $blogService = $container->get(BlogServiceInterface::class);
assert($repository instanceof CategoryRepository);
assert($template instanceof TemplateRendererInterface);
+ assert($blogService instanceof BlogServiceInterface);
- return new GetCategoryResourceHandler($template, $repository);
+ return new GetCategoryResourceHandler($template, $repository, $blogService);
}
}
diff --git a/src/Blog/src/Factory/Post/PostResourceHandlerFactory.php b/src/Blog/src/Factory/Post/PostResourceHandlerFactory.php
index db678905..bcb26890 100644
--- a/src/Blog/src/Factory/Post/PostResourceHandlerFactory.php
+++ b/src/Blog/src/Factory/Post/PostResourceHandlerFactory.php
@@ -7,6 +7,7 @@
use Light\Blog\Handler\GetPostResourceHandler;
use Light\Blog\Repository\CategoryRepository;
use Light\Blog\Repository\PostRepository;
+use Light\Blog\Service\BlogServiceInterface;
use Mezzio\Template\TemplateRendererInterface;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
@@ -25,10 +26,17 @@ public function __invoke(ContainerInterface $container, string $requestedName):
$repository = $container->get(PostRepository::class);
$categoryRepository = $container->get(CategoryRepository::class);
$template = $container->get(TemplateRendererInterface::class);
+ $blogService = $container->get(BlogServiceInterface::class);
assert($repository instanceof PostRepository);
assert($template instanceof TemplateRendererInterface);
+ assert($blogService instanceof BlogServiceInterface);
- return new GetPostResourceHandler($template, $repository, $categoryRepository);
+ return new GetPostResourceHandler(
+ $template,
+ $repository,
+ $categoryRepository,
+ $blogService
+ );
}
}
diff --git a/src/Blog/src/Factory/Tag/TagResourceHandlerFactory.php b/src/Blog/src/Factory/Tag/TagResourceHandlerFactory.php
index 7a95340b..fcc5307f 100644
--- a/src/Blog/src/Factory/Tag/TagResourceHandlerFactory.php
+++ b/src/Blog/src/Factory/Tag/TagResourceHandlerFactory.php
@@ -7,6 +7,7 @@
use Light\Blog\Handler\GetTagResourceHandler;
use Light\Blog\Repository\CategoryRepository;
use Light\Blog\Repository\TagRepository;
+use Light\Blog\Service\BlogServiceInterface;
use Mezzio\Template\TemplateRendererInterface;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
@@ -25,11 +26,13 @@ public function __invoke(ContainerInterface $container, string $requestedName):
$repository = $container->get(TagRepository::class);
$template = $container->get(TemplateRendererInterface::class);
$categoryRepository = $container->get(CategoryRepository::class);
+ $blogService = $container->get(BlogServiceInterface::class);
assert($repository instanceof TagRepository);
assert($template instanceof TemplateRendererInterface);
assert($categoryRepository instanceof CategoryRepository);
+ assert($blogService instanceof BlogServiceInterface);
- return new GetTagResourceHandler($template, $repository, $categoryRepository);
+ return new GetTagResourceHandler($template, $repository, $categoryRepository, $blogService);
}
}
diff --git a/src/Blog/src/Handler/GetAuthorResourceHandler.php b/src/Blog/src/Handler/GetAuthorResourceHandler.php
index 24e1cded..8814989f 100644
--- a/src/Blog/src/Handler/GetAuthorResourceHandler.php
+++ b/src/Blog/src/Handler/GetAuthorResourceHandler.php
@@ -4,13 +4,12 @@
namespace Light\Blog\Handler;
-use Fig\Http\Message\StatusCodeInterface;
use Laminas\Diactoros\Response\HtmlResponse;
use Light\App\Helper\Paginator;
-use Light\Blog\Entity\Author;
use Light\Blog\Repository\AuthorRepository;
use Light\Blog\Repository\CategoryRepository;
use Light\Blog\Repository\PostRepository;
+use Light\Blog\Service\BlogServiceInterface;
use Mezzio\Template\TemplateRendererInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
@@ -23,6 +22,7 @@ public function __construct(
protected AuthorRepository $authorRepository,
protected PostRepository $postRepository,
protected CategoryRepository $categoryRepository,
+ protected BlogServiceInterface $blogService,
) {
}
@@ -31,7 +31,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface
$authorSlug = $request->getAttribute('slug');
$author = $this->authorRepository->getAuthorResource($authorSlug);
if (! $author) {
- return $this->notFound($this->authorRepository->getAuthorsWithPublishedPosts());
+ return $this->blogService->authorNotFound($this->authorRepository->getAuthorsWithPublishedPosts());
}
$categories = $this->categoryRepository->getCategories();
@@ -51,17 +51,4 @@ public function handle(ServerRequestInterface $request): ResponseInterface
])
);
}
-
- /**
- * @param Author[] $authors
- */
- private function notFound(array $authors): HtmlResponse
- {
- return new HtmlResponse(
- $this->template->render('error::404', [
- 'authors' => $authors,
- ]),
- StatusCodeInterface::STATUS_NOT_FOUND
- );
- }
}
diff --git a/src/Blog/src/Handler/GetCategoryResourceHandler.php b/src/Blog/src/Handler/GetCategoryResourceHandler.php
index 8bbdd354..8133028a 100644
--- a/src/Blog/src/Handler/GetCategoryResourceHandler.php
+++ b/src/Blog/src/Handler/GetCategoryResourceHandler.php
@@ -4,11 +4,10 @@
namespace Light\Blog\Handler;
-use Fig\Http\Message\StatusCodeInterface;
use Laminas\Diactoros\Response\HtmlResponse;
use Light\App\Helper\Paginator;
-use Light\Blog\Entity\Category;
use Light\Blog\Repository\CategoryRepository;
+use Light\Blog\Service\BlogServiceInterface;
use Mezzio\Template\TemplateRendererInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
@@ -20,6 +19,7 @@ class GetCategoryResourceHandler implements RequestHandlerInterface
public function __construct(
protected TemplateRendererInterface $template,
protected CategoryRepository $categoryRepository,
+ protected BlogServiceInterface $blogService,
) {
}
@@ -29,10 +29,10 @@ public function handle(ServerRequestInterface $request): ResponseInterface
$categories = $this->categoryRepository->getCategories();
$category = $this->categoryRepository->getCategoryResource($categorySlug);
if ($category === null) {
- return $this->notFound($categories);
+ return $this->blogService->notFound($categories);
}
if (! $category->isVisible()) {
- return $this->gone($categories);
+ return $this->blogService->gone($categories);
}
$meta = $category;
@@ -55,33 +55,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface
);
return new HtmlResponse($html);
} catch (Throwable $e) {
- return $this->notFound($categories);
+ return $this->blogService->notFound($categories);
}
}
-
- /**
- * @param Category[] $categories
- */
- private function notFound(array $categories): HtmlResponse
- {
- return new HtmlResponse(
- $this->template->render('error::404', [
- 'categories' => $categories,
- ]),
- 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 e154385a..2b9cf898 100644
--- a/src/Blog/src/Handler/GetPostResourceHandler.php
+++ b/src/Blog/src/Handler/GetPostResourceHandler.php
@@ -6,39 +6,56 @@
use Fig\Http\Message\StatusCodeInterface;
use Laminas\Diactoros\Response\HtmlResponse;
-use Light\Blog\Entity\Category;
+use Laminas\Diactoros\Response\TextResponse;
use Light\Blog\Enum\PostStatusEnum;
use Light\Blog\Repository\CategoryRepository;
use Light\Blog\Repository\PostRepository;
+use Light\Blog\Service\BlogServiceInterface;
use Mezzio\Template\TemplateRendererInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Throwable;
+use function file_get_contents;
+use function str_contains;
+
class GetPostResourceHandler implements RequestHandlerInterface
{
public function __construct(
private readonly TemplateRendererInterface $template,
private readonly PostRepository $articleRepository,
private readonly CategoryRepository $categoryRepository,
+ private readonly BlogServiceInterface $blogService,
) {
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
- $slug = $request->getAttribute('slug');
- $categorySlug = $request->getAttribute('categorySlug');
- $categories = $this->categoryRepository->getCategories();
- $article = $this->articleRepository->getArticleResource($slug, $categorySlug);
+ $slug = (string) $request->getAttribute('slug');
+ $categorySlug = (string) $request->getAttribute('categorySlug');
+
+ if (str_contains($request->getHeaderLine('Accept'), 'text/markdown')) {
+ $markdownFile = $this->blogService->resolveMarkdownFilePath($categorySlug, $slug);
+ if ($markdownFile !== null) {
+ return new TextResponse(
+ (string) file_get_contents($markdownFile),
+ StatusCodeInterface::STATUS_OK,
+ ['Content-Type' => 'text/markdown; charset=utf-8'],
+ );
+ }
+ }
+
+ $categories = $this->categoryRepository->getCategories();
+ $article = $this->articleRepository->getArticleResource($slug, $categorySlug);
if ($article === null) {
- return $this->notFound($categories);
+ return $this->blogService->notFound($categories);
}
if ($article->getStatus() === PostStatusEnum::Archived) {
- return $this->gone($categories);
+ return $this->blogService->gone($categories);
}
if ($article->getStatus() !== PostStatusEnum::Published) {
- return $this->notFound($categories);
+ return $this->blogService->notFound($categories);
}
$meta = $article;
$adjacent = $this->articleRepository->getAdjacentPosts($article);
@@ -55,33 +72,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface
);
return new HtmlResponse($html);
} catch (Throwable $e) {
- return $this->notFound($categories);
+ return $this->blogService->notFound($categories);
}
}
-
- /**
- * @param Category[] $categories
- */
- private function notFound(array $categories): HtmlResponse
- {
- return new HtmlResponse(
- $this->template->render('error::404', [
- 'categories' => $categories,
- ]),
- 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/GetTagResourceHandler.php b/src/Blog/src/Handler/GetTagResourceHandler.php
index ffce0a3e..a03a9e47 100644
--- a/src/Blog/src/Handler/GetTagResourceHandler.php
+++ b/src/Blog/src/Handler/GetTagResourceHandler.php
@@ -4,12 +4,11 @@
namespace Light\Blog\Handler;
-use Fig\Http\Message\StatusCodeInterface;
use Laminas\Diactoros\Response\HtmlResponse;
use Light\App\Helper\Paginator;
-use Light\Blog\Entity\Category;
use Light\Blog\Repository\CategoryRepository;
use Light\Blog\Repository\TagRepository;
+use Light\Blog\Service\BlogServiceInterface;
use Mezzio\Template\TemplateRendererInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
@@ -22,6 +21,7 @@ public function __construct(
protected TemplateRendererInterface $template,
protected TagRepository $tagRepository,
protected CategoryRepository $categoryRepository,
+ protected BlogServiceInterface $blogService,
) {
}
@@ -31,7 +31,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface
$categories = $this->categoryRepository->getCategories();
$tag = $this->tagRepository->getTagResource($tagSlug);
if ($tag === null) {
- return $this->notFound($categories);
+ return $this->blogService->notFound($categories);
}
$meta = $tag;
@@ -54,20 +54,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface
);
return new HtmlResponse($html);
} catch (Throwable $e) {
- return $this->notFound($categories);
+ return $this->blogService->notFound($categories);
}
}
-
- /**
- * @param Category[] $categories
- */
- private function notFound(array $categories): HtmlResponse
- {
- return new HtmlResponse(
- $this->template->render('error::404', [
- 'categories' => $categories,
- ]),
- StatusCodeInterface::STATUS_NOT_FOUND
- );
- }
}
diff --git a/src/Blog/src/Service/BlogService.php b/src/Blog/src/Service/BlogService.php
new file mode 100644
index 00000000..e3efab7a
--- /dev/null
+++ b/src/Blog/src/Service/BlogService.php
@@ -0,0 +1,87 @@
+template->render('error::404', [
+ 'categories' => $categories,
+ ]),
+ StatusCodeInterface::STATUS_NOT_FOUND
+ );
+ }
+
+ /**
+ * @param Category[] $categories
+ */
+ public function gone(array $categories): HtmlResponse
+ {
+ return new HtmlResponse(
+ $this->template->render('error::410', [
+ 'categories' => $categories,
+ ]),
+ StatusCodeInterface::STATUS_GONE
+ );
+ }
+
+ /**
+ * @param Author[] $authors
+ */
+ public function authorNotFound(array $authors): HtmlResponse
+ {
+ return new HtmlResponse(
+ $this->template->render('error::404', [
+ 'authors' => $authors,
+ ]),
+ StatusCodeInterface::STATUS_NOT_FOUND
+ );
+ }
+
+ public function resolveMarkdownFilePath(string $categorySlug, string $slug): ?string
+ {
+ if ($categorySlug === '' || $slug === '') {
+ return null;
+ }
+
+ $base = realpath($this->articlesPath);
+ if ($base === false) {
+ return null;
+ }
+ $base = rtrim($base, '/');
+ $realPath = realpath($base . '/' . $categorySlug . '/' . $slug . '.md');
+ if ($realPath === false || ! is_file($realPath)) {
+ return null;
+ }
+
+ if (! str_starts_with($realPath, $base . '/')) {
+ return null;
+ }
+
+ return $realPath;
+ }
+}
diff --git a/src/Blog/src/Service/BlogServiceInterface.php b/src/Blog/src/Service/BlogServiceInterface.php
new file mode 100644
index 00000000..32d08071
--- /dev/null
+++ b/src/Blog/src/Service/BlogServiceInterface.php
@@ -0,0 +1,33 @@
+get(CategoryRepository::class);
$template = $container->get(TemplateRendererInterface::class);
assert($template instanceof TemplateRendererInterface);
+ $pageService = $container->get(PageServiceInterface::class);
+ assert($pageService instanceof PageServiceInterface);
- return new GetPageViewHandler($template, $categoryRepository, $postRepository);
+ return new GetPageViewHandler(
+ $template,
+ $categoryRepository,
+ $postRepository,
+ $pageService
+ );
}
}
diff --git a/src/Page/src/Factory/PageServiceFactory.php b/src/Page/src/Factory/PageServiceFactory.php
index 61c26c4c..c3a9fc56 100644
--- a/src/Page/src/Factory/PageServiceFactory.php
+++ b/src/Page/src/Factory/PageServiceFactory.php
@@ -8,6 +8,8 @@
use Light\Page\Service\PageServiceInterface;
use Psr\Container\ContainerInterface;
+use function getcwd;
+
class PageServiceFactory
{
/**
@@ -15,6 +17,6 @@ class PageServiceFactory
*/
public function __invoke(ContainerInterface $container, string $requestedName): PageServiceInterface
{
- return new PageService();
+ return new PageService(getcwd() . '/public/md-pages');
}
}
diff --git a/src/Page/src/Handler/GetPageViewHandler.php b/src/Page/src/Handler/GetPageViewHandler.php
index 6171b8f3..3c531894 100644
--- a/src/Page/src/Handler/GetPageViewHandler.php
+++ b/src/Page/src/Handler/GetPageViewHandler.php
@@ -4,27 +4,46 @@
namespace Light\Page\Handler;
+use Fig\Http\Message\StatusCodeInterface;
use Laminas\Diactoros\Response\HtmlResponse;
+use Laminas\Diactoros\Response\TextResponse;
use Light\Blog\Repository\CategoryRepository;
use Light\Blog\Repository\PostRepository;
+use Light\Page\Service\PageServiceInterface;
use Mezzio\Router\RouteResult;
use Mezzio\Template\TemplateRendererInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
+use function file_get_contents;
+use function str_contains;
+
class GetPageViewHandler implements RequestHandlerInterface
{
public function __construct(
protected TemplateRendererInterface $template,
protected CategoryRepository $categoryRepository,
protected PostRepository $postRepository,
+ protected PageServiceInterface $pageService,
) {
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
- $template = $request->getAttribute(RouteResult::class)->getMatchedRouteName();
+ $template = $request->getAttribute(RouteResult::class)->getMatchedRouteName();
+
+ if (str_contains($request->getHeaderLine('Accept'), 'text/markdown')) {
+ $markdownFile = $this->pageService->resolveMarkdownFilePath($template);
+ if ($markdownFile !== null) {
+ return new TextResponse(
+ (string) file_get_contents($markdownFile),
+ StatusCodeInterface::STATUS_OK,
+ ['Content-Type' => 'text/markdown; charset=utf-8'],
+ );
+ }
+ }
+
$posts = $this->postRepository->getRecentPosts(3);
$categories = $this->categoryRepository->getCategories();
return new HtmlResponse(
diff --git a/src/Page/src/Service/PageService.php b/src/Page/src/Service/PageService.php
index 13645776..d571b83c 100644
--- a/src/Page/src/Service/PageService.php
+++ b/src/Page/src/Service/PageService.php
@@ -4,6 +4,24 @@
namespace Light\Page\Service;
+use function explode;
+use function is_file;
+
class PageService implements PageServiceInterface
{
+ public function __construct(private readonly string $mdPagesPath)
+ {
+ }
+
+ public function resolveMarkdownFilePath(string $routeName): ?string
+ {
+ $slug = explode('::', $routeName, 2)[1] ?? '';
+ if ($slug === '') {
+ return null;
+ }
+
+ $filePath = $this->mdPagesPath . '/' . $slug . '.md';
+
+ return is_file($filePath) ? $filePath : null;
+ }
}
diff --git a/src/Page/src/Service/PageServiceInterface.php b/src/Page/src/Service/PageServiceInterface.php
index e47d9bea..9b3f3dc5 100644
--- a/src/Page/src/Service/PageServiceInterface.php
+++ b/src/Page/src/Service/PageServiceInterface.php
@@ -6,4 +6,9 @@
interface PageServiceInterface
{
+ /**
+ * Resolves the markdown file backing a `page::{slug}` route (e.g. `page::api` -> `api.md`),
+ * or null when the route isn't a static page or has no markdown counterpart.
+ */
+ public function resolveMarkdownFilePath(string $routeName): ?string;
}
diff --git a/test/Unit/App/Handler/GetIndexViewHandlerTest.php b/test/Unit/App/Handler/GetIndexViewHandlerTest.php
index 3ebb14ed..fc40527d 100644
--- a/test/Unit/App/Handler/GetIndexViewHandlerTest.php
+++ b/test/Unit/App/Handler/GetIndexViewHandlerTest.php
@@ -13,6 +13,13 @@
use Mezzio\Template\TemplateRendererInterface;
use PHPUnit\Framework\MockObject\Exception;
+use function file_put_contents;
+use function mkdir;
+use function rmdir;
+use function sys_get_temp_dir;
+use function uniqid;
+use function unlink;
+
class GetIndexViewHandlerTest extends UnitTest
{
/**
@@ -36,7 +43,7 @@ public function testHandleRendersTheIndexTemplateWithTheThreeMostRecentPosts():
return '';
});
- $handler = new GetIndexViewHandler($template, $postRepository);
+ $handler = new GetIndexViewHandler($template, $postRepository, '');
$response = $handler->handle(new ServerRequest());
$this->assertSame($posts, $captured['posts']);
@@ -44,4 +51,51 @@ public function testHandleRendersTheIndexTemplateWithTheThreeMostRecentPosts():
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('', (string) $response->getBody());
}
+
+ /**
+ * @throws Exception
+ */
+ public function testHandleReturnsTheMarkdownFileWhenAcceptRequestsIt(): void
+ {
+ $mdPagesPath = sys_get_temp_dir() . '/' . uniqid('dk-pages-', true);
+ mkdir($mdPagesPath);
+ file_put_contents($mdPagesPath . '/index.md', '# Dotkernel');
+
+ $postRepository = $this->createMock(PostRepository::class);
+ $postRepository->expects($this->never())->method('getRecentPosts');
+
+ $template = $this->createMock(TemplateRendererInterface::class);
+ $template->expects($this->never())->method('render');
+
+ $handler = new GetIndexViewHandler($template, $postRepository, $mdPagesPath);
+ $response = $handler->handle(
+ (new ServerRequest())->withHeader('Accept', 'text/markdown')
+ );
+
+ $this->assertSame(200, $response->getStatusCode());
+ $this->assertStringContainsString('text/markdown', $response->getHeaderLine('Content-Type'));
+ $this->assertSame('# Dotkernel', (string) $response->getBody());
+
+ unlink($mdPagesPath . '/index.md');
+ rmdir($mdPagesPath);
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function testHandleFallsBackToHtmlWhenNoMarkdownFileExists(): void
+ {
+ $postRepository = $this->createMock(PostRepository::class);
+ $postRepository->expects($this->once())->method('getRecentPosts')->willReturn([]);
+
+ $template = $this->createMock(TemplateRendererInterface::class);
+ $template->expects($this->once())->method('render')->willReturn('');
+
+ $handler = new GetIndexViewHandler($template, $postRepository, sys_get_temp_dir());
+ $response = $handler->handle(
+ (new ServerRequest())->withHeader('Accept', 'text/markdown')
+ );
+
+ $this->assertStringContainsString('text/html', $response->getHeaderLine('Content-Type'));
+ }
}
diff --git a/test/Unit/Blog/Factory/BlogServiceFactoryTest.php b/test/Unit/Blog/Factory/BlogServiceFactoryTest.php
new file mode 100644
index 00000000..55c76971
--- /dev/null
+++ b/test/Unit/Blog/Factory/BlogServiceFactoryTest.php
@@ -0,0 +1,37 @@
+createStub(TemplateRendererInterface::class);
+ $container = $this->createStub(ContainerInterface::class);
+ $container->method('get')->willReturn($template);
+
+ $service = (new BlogServiceFactory())($container, BlogService::class);
+
+ $this->assertInstanceOf(BlogService::class, $service);
+ $this->assertSame($template, (new ReflectionProperty(BlogService::class, 'template'))->getValue($service));
+ $this->assertSame(
+ getcwd() . '/public/md-articles',
+ (new ReflectionProperty(BlogService::class, 'articlesPath'))->getValue($service)
+ );
+ }
+}
diff --git a/test/Unit/Blog/Handler/GetAuthorResourceHandlerTest.php b/test/Unit/Blog/Handler/GetAuthorResourceHandlerTest.php
index f294f1b1..d88a8a6e 100644
--- a/test/Unit/Blog/Handler/GetAuthorResourceHandlerTest.php
+++ b/test/Unit/Blog/Handler/GetAuthorResourceHandlerTest.php
@@ -6,6 +6,8 @@
use Doctrine\ORM\Query;
use Doctrine\ORM\Tools\Pagination\Paginator as DoctrinePaginator;
+use Fig\Http\Message\StatusCodeInterface;
+use Laminas\Diactoros\Response\HtmlResponse;
use Laminas\Diactoros\ServerRequest;
use Light\Blog\Entity\Author;
use Light\Blog\Entity\Category;
@@ -13,6 +15,7 @@
use Light\Blog\Repository\AuthorRepository;
use Light\Blog\Repository\CategoryRepository;
use Light\Blog\Repository\PostRepository;
+use Light\Blog\Service\BlogServiceInterface;
use LightTest\Unit\UnitTest;
use Mezzio\Template\TemplateRendererInterface;
use PHPUnit\Framework\MockObject\Exception;
@@ -55,7 +58,16 @@ public function testHandleRendersTheAuthorResourceTemplateWhenAuthorExists(): vo
return '';
});
- $handler = new GetAuthorResourceHandler($template, $authorRepository, $postRepository, $categoryRepository);
+ $blogService = $this->createMock(BlogServiceInterface::class);
+ $blogService->expects($this->never())->method('authorNotFound');
+
+ $handler = new GetAuthorResourceHandler(
+ $template,
+ $authorRepository,
+ $postRepository,
+ $categoryRepository,
+ $blogService,
+ );
$response = $handler->handle((new ServerRequest())->withAttribute('slug', 'gabi'));
$this->assertSame(200, $response->getStatusCode());
@@ -80,16 +92,21 @@ public function testHandleReturnsNotFoundWhenAuthorDoesNotExist(): void
$postRepository->expects($this->never())->method('getArticleByAuthor');
$template = $this->createMock(TemplateRendererInterface::class);
- $template->expects($this->once())->method('render')
- ->willReturnCallback(function (string $name, mixed $parameters = []) use ($authors): string {
- $this->assertSame('error::404', $name);
- $this->assertIsArray($parameters);
- $this->assertSame($authors, $parameters['authors']);
-
- return '';
- });
-
- $handler = new GetAuthorResourceHandler($template, $authorRepository, $postRepository, $categoryRepository);
+ $template->expects($this->never())->method('render');
+
+ $blogService = $this->createMock(BlogServiceInterface::class);
+ $blogService->expects($this->once())
+ ->method('authorNotFound')
+ ->with($authors)
+ ->willReturn(new HtmlResponse('', StatusCodeInterface::STATUS_NOT_FOUND));
+
+ $handler = new GetAuthorResourceHandler(
+ $template,
+ $authorRepository,
+ $postRepository,
+ $categoryRepository,
+ $blogService,
+ );
$response = $handler->handle((new ServerRequest())->withAttribute('slug', 'adminxx'));
$this->assertSame(404, $response->getStatusCode());
diff --git a/test/Unit/Blog/Handler/GetCategoryResourceHandlerTest.php b/test/Unit/Blog/Handler/GetCategoryResourceHandlerTest.php
index 5c7281c4..67ce8642 100644
--- a/test/Unit/Blog/Handler/GetCategoryResourceHandlerTest.php
+++ b/test/Unit/Blog/Handler/GetCategoryResourceHandlerTest.php
@@ -4,10 +4,13 @@
namespace LightTest\Unit\Blog\Handler;
+use Fig\Http\Message\StatusCodeInterface;
+use Laminas\Diactoros\Response\HtmlResponse;
use Laminas\Diactoros\ServerRequest;
use Light\Blog\Entity\Category;
use Light\Blog\Handler\GetCategoryResourceHandler;
use Light\Blog\Repository\CategoryRepository;
+use Light\Blog\Service\BlogServiceInterface;
use LightTest\Unit\UnitTest;
use Mezzio\Template\TemplateRendererInterface;
use PHPUnit\Framework\MockObject\Exception;
@@ -50,9 +53,20 @@ private function handle(?Category $category): ResponseInterface
$categoryRepository->expects($this->once())->method('getCategoryResource')->willReturn($category);
$template = $this->createMock(TemplateRendererInterface::class);
- $template->expects($this->once())->method('render')->willReturn('');
+ $template->expects($this->never())->method('render');
- $handler = new GetCategoryResourceHandler($template, $categoryRepository);
+ $blogService = $this->createMock(BlogServiceInterface::class);
+ if ($category === null) {
+ $blogService->expects($this->once())->method('notFound')->with($categories)
+ ->willReturn(new HtmlResponse('', StatusCodeInterface::STATUS_NOT_FOUND));
+ $blogService->expects($this->never())->method('gone');
+ } else {
+ $blogService->expects($this->once())->method('gone')->with($categories)
+ ->willReturn(new HtmlResponse('', StatusCodeInterface::STATUS_GONE));
+ $blogService->expects($this->never())->method('notFound');
+ }
+
+ $handler = new GetCategoryResourceHandler($template, $categoryRepository, $blogService);
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
index c78176a4..a5c3cd88 100644
--- a/test/Unit/Blog/Handler/GetPostResourceHandlerTest.php
+++ b/test/Unit/Blog/Handler/GetPostResourceHandlerTest.php
@@ -4,6 +4,8 @@
namespace LightTest\Unit\Blog\Handler;
+use Fig\Http\Message\StatusCodeInterface;
+use Laminas\Diactoros\Response\HtmlResponse;
use Laminas\Diactoros\ServerRequest;
use Light\Blog\Entity\Category;
use Light\Blog\Entity\Post;
@@ -11,11 +13,17 @@
use Light\Blog\Handler\GetPostResourceHandler;
use Light\Blog\Repository\CategoryRepository;
use Light\Blog\Repository\PostRepository;
+use Light\Blog\Service\BlogServiceInterface;
use LightTest\Unit\UnitTest;
use Mezzio\Template\TemplateRendererInterface;
use PHPUnit\Framework\MockObject\Exception;
use Psr\Http\Message\ResponseInterface;
+use function file_put_contents;
+use function sys_get_temp_dir;
+use function uniqid;
+use function unlink;
+
class GetPostResourceHandlerTest extends UnitTest
{
/**
@@ -57,7 +65,61 @@ public function testHandleReturnsNotFoundWhenArticleIsNotPublished(): void
/**
* @throws Exception
*/
- private function handle(?Post $article): ResponseInterface
+ public function testHandleReturnsTheMarkdownFileWhenAcceptRequestsIt(): void
+ {
+ $markdownFile = sys_get_temp_dir() . '/' . uniqid('dk-article-', true) . '.md';
+ file_put_contents($markdownFile, '# A slug');
+
+ $blogService = $this->createMock(BlogServiceInterface::class);
+ $blogService
+ ->expects($this->once())
+ ->method('resolveMarkdownFilePath')
+ ->with('a-category', 'a-slug')
+ ->willReturn($markdownFile);
+
+ $postRepository = $this->createMock(PostRepository::class);
+ $categoryRepository = $this->createMock(CategoryRepository::class);
+ $postRepository->expects($this->never())->method('getArticleResource');
+ $categoryRepository->expects($this->never())->method('getCategories');
+
+ $handler = new GetPostResourceHandler(
+ $this->createStub(TemplateRendererInterface::class),
+ $postRepository,
+ $categoryRepository,
+ $blogService,
+ );
+
+ $response = $handler->handle(
+ (new ServerRequest())
+ ->withAttribute('slug', 'a-slug')
+ ->withAttribute('categorySlug', 'a-category')
+ ->withHeader('Accept', 'text/markdown')
+ );
+
+ $this->assertSame(200, $response->getStatusCode());
+ $this->assertStringContainsString('text/markdown', $response->getHeaderLine('Content-Type'));
+ $this->assertSame('# A slug', (string) $response->getBody());
+
+ unlink($markdownFile);
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function testHandleFallsBackToHtmlWhenNoMarkdownFileExistsForTheRequestedArticle(): void
+ {
+ $article = $this->createStub(Post::class);
+ $article->method('getStatus')->willReturn(PostStatusEnum::Draft);
+
+ $response = $this->handle($article, withAccept: 'text/markdown');
+
+ $this->assertStringContainsString('text/html', $response->getHeaderLine('Content-Type'));
+ }
+
+ /**
+ * @throws Exception
+ */
+ private function handle(?Post $article, string $withAccept = ''): ResponseInterface
{
$categories = [$this->createStub(Category::class)];
@@ -68,12 +130,22 @@ private function handle(?Post $article): ResponseInterface
$categoryRepository->expects($this->once())->method('getCategories')->willReturn($categories);
$template = $this->createMock(TemplateRendererInterface::class);
- $template->expects($this->once())->method('render')->willReturn('');
+ $template->expects($this->never())->method('render');
- $handler = new GetPostResourceHandler($template, $postRepository, $categoryRepository);
+ $blogService = $this->createStub(BlogServiceInterface::class);
+ $blogService->method('resolveMarkdownFilePath')->willReturn(null);
+ $blogService->method('notFound')
+ ->willReturn(new HtmlResponse('', StatusCodeInterface::STATUS_NOT_FOUND));
+ $blogService->method('gone')
+ ->willReturn(new HtmlResponse('', StatusCodeInterface::STATUS_GONE));
- return $handler->handle(
- (new ServerRequest())->withAttribute('slug', 'a-slug')->withAttribute('categorySlug', 'a-category')
- );
+ $handler = new GetPostResourceHandler($template, $postRepository, $categoryRepository, $blogService);
+
+ $request = (new ServerRequest())->withAttribute('slug', 'a-slug')->withAttribute('categorySlug', 'a-category');
+ if ($withAccept !== '') {
+ $request = $request->withHeader('Accept', $withAccept);
+ }
+
+ return $handler->handle($request);
}
}
diff --git a/test/Unit/Blog/Service/BlogServiceTest.php b/test/Unit/Blog/Service/BlogServiceTest.php
new file mode 100644
index 00000000..de0f06a8
--- /dev/null
+++ b/test/Unit/Blog/Service/BlogServiceTest.php
@@ -0,0 +1,139 @@
+createStub(TemplateRendererInterface::class), '');
+
+ $this->assertContainsOnlyInstancesOf(BlogServiceInterface::class, [$service]);
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function testNotFoundRendersThe404TemplateWithTheCategories(): void
+ {
+ $categories = [$this->createStub(Category::class)];
+
+ $template = $this->createMock(TemplateRendererInterface::class);
+ $template->expects($this->once())->method('render')
+ ->with('error::404', ['categories' => $categories])
+ ->willReturn('');
+
+ $response = (new BlogService($template, ''))->notFound($categories);
+
+ $this->assertSame(404, $response->getStatusCode());
+ $this->assertSame('', (string) $response->getBody());
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function testGoneRendersThe410TemplateWithTheCategories(): void
+ {
+ $categories = [$this->createStub(Category::class)];
+
+ $template = $this->createMock(TemplateRendererInterface::class);
+ $template->expects($this->once())->method('render')
+ ->with('error::410', ['categories' => $categories])
+ ->willReturn('');
+
+ $response = (new BlogService($template, ''))->gone($categories);
+
+ $this->assertSame(410, $response->getStatusCode());
+ $this->assertSame('', (string) $response->getBody());
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function testAuthorNotFoundRendersThe404TemplateWithTheAuthors(): void
+ {
+ $authors = [$this->createStub(Author::class)];
+
+ $template = $this->createMock(TemplateRendererInterface::class);
+ $template->expects($this->once())->method('render')
+ ->with('error::404', ['authors' => $authors])
+ ->willReturn('');
+
+ $response = (new BlogService($template, ''))->authorNotFound($authors);
+
+ $this->assertSame(404, $response->getStatusCode());
+ $this->assertSame('', (string) $response->getBody());
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function testResolveMarkdownFilePathReturnsTheFileForTheCategoryAndSlug(): void
+ {
+ $articlesPath = sys_get_temp_dir() . '/' . uniqid('dk-articles-', true);
+ mkdir($articlesPath . '/a-category', 0777, true);
+ file_put_contents($articlesPath . '/a-category/a-slug.md', '# A slug');
+
+ $service = new BlogService($this->createStub(TemplateRendererInterface::class), $articlesPath);
+ $filePath = $service->resolveMarkdownFilePath('a-category', 'a-slug');
+
+ $this->assertSame($articlesPath . '/a-category/a-slug.md', $filePath);
+
+ unlink($articlesPath . '/a-category/a-slug.md');
+ rmdir($articlesPath . '/a-category');
+ rmdir($articlesPath);
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function testResolveMarkdownFilePathReturnsNullWhenNoFileExists(): void
+ {
+ $service = new BlogService($this->createStub(TemplateRendererInterface::class), sys_get_temp_dir());
+
+ $this->assertNull($service->resolveMarkdownFilePath('a-category', 'missing-slug'));
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function testResolveMarkdownFilePathReturnsNullForAnEmptyCategoryOrSlug(): void
+ {
+ $service = new BlogService($this->createStub(TemplateRendererInterface::class), sys_get_temp_dir());
+
+ $this->assertNull($service->resolveMarkdownFilePath('', 'a-slug'));
+ $this->assertNull($service->resolveMarkdownFilePath('a-category', ''));
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function testResolveMarkdownFilePathRefusesToEscapeTheArticlesPath(): void
+ {
+ $articlesPath = sys_get_temp_dir() . '/' . uniqid('dk-articles-', true);
+ mkdir($articlesPath);
+
+ $service = new BlogService($this->createStub(TemplateRendererInterface::class), $articlesPath);
+
+ $this->assertNull($service->resolveMarkdownFilePath('..', 'passwd'));
+
+ rmdir($articlesPath);
+ }
+}
diff --git a/test/Unit/Page/Factory/GetPageViewHandlerFactoryTest.php b/test/Unit/Page/Factory/GetPageViewHandlerFactoryTest.php
index 8cfde7e8..8e1712cd 100644
--- a/test/Unit/Page/Factory/GetPageViewHandlerFactoryTest.php
+++ b/test/Unit/Page/Factory/GetPageViewHandlerFactoryTest.php
@@ -8,6 +8,7 @@
use Light\Blog\Repository\PostRepository;
use Light\Page\Factory\GetPageViewHandlerFactory;
use Light\Page\Handler\GetPageViewHandler;
+use Light\Page\Service\PageServiceInterface;
use LightTest\Unit\UnitTest;
use Mezzio\Template\TemplateRendererInterface;
use PHPUnit\Framework\MockObject\Exception;
@@ -19,20 +20,22 @@ class GetPageViewHandlerFactoryTest extends UnitTest
/**
* @throws Exception
*/
- public function testInvokeInjectsTheRendererAndBothRepositories(): void
+ public function testInvokeInjectsTheRendererBothRepositoriesAndThePageService(): void
{
$template = $this->createStub(TemplateRendererInterface::class);
$categoryRepository = $this->createStub(CategoryRepository::class);
$postRepository = $this->createStub(PostRepository::class);
+ $pageService = $this->createStub(PageServiceInterface::class);
$handler = (new GetPageViewHandlerFactory())(
- $this->createContainer($template, $categoryRepository, $postRepository),
+ $this->createContainer($template, $categoryRepository, $postRepository, $pageService),
GetPageViewHandler::class
);
$this->assertSame($template, $this->readProperty($handler, 'template'));
$this->assertSame($categoryRepository, $this->readProperty($handler, 'categoryRepository'));
$this->assertSame($postRepository, $this->readProperty($handler, 'postRepository'));
+ $this->assertSame($pageService, $this->readProperty($handler, 'pageService'));
}
/**
@@ -42,6 +45,7 @@ private function createContainer(
TemplateRendererInterface $template,
CategoryRepository $categoryRepository,
PostRepository $postRepository,
+ PageServiceInterface $pageService,
): ContainerInterface {
$container = $this->createStub(ContainerInterface::class);
$container
@@ -49,6 +53,7 @@ private function createContainer(
->willReturnCallback(fn (string $id): mixed => match ($id) {
TemplateRendererInterface::class => $template,
CategoryRepository::class => $categoryRepository,
+ PageServiceInterface::class => $pageService,
default => $postRepository,
});
diff --git a/test/Unit/Page/Factory/PageServiceFactoryTest.php b/test/Unit/Page/Factory/PageServiceFactoryTest.php
index 034790b5..2f3d4a9c 100644
--- a/test/Unit/Page/Factory/PageServiceFactoryTest.php
+++ b/test/Unit/Page/Factory/PageServiceFactoryTest.php
@@ -9,6 +9,9 @@
use LightTest\Unit\UnitTest;
use PHPUnit\Framework\MockObject\Exception;
use Psr\Container\ContainerInterface;
+use ReflectionProperty;
+
+use function getcwd;
class PageServiceFactoryTest extends UnitTest
{
@@ -24,6 +27,10 @@ public function testInvokeReturnsThePageService(): void
// The declared return type is the interface; what matters is which implementation it builds.
$this->assertInstanceOf(PageService::class, $service);
+ $this->assertSame(
+ getcwd() . '/public/md-pages',
+ (new ReflectionProperty(PageService::class, 'mdPagesPath'))->getValue($service)
+ );
}
/**
diff --git a/test/Unit/Page/Handler/GetPageViewHandlerTest.php b/test/Unit/Page/Handler/GetPageViewHandlerTest.php
index 66cc98ad..cf91de01 100644
--- a/test/Unit/Page/Handler/GetPageViewHandlerTest.php
+++ b/test/Unit/Page/Handler/GetPageViewHandlerTest.php
@@ -10,12 +10,18 @@
use Light\Blog\Repository\CategoryRepository;
use Light\Blog\Repository\PostRepository;
use Light\Page\Handler\GetPageViewHandler;
+use Light\Page\Service\PageServiceInterface;
use LightTest\Unit\UnitTest;
use Mezzio\Router\RouteResult;
use Mezzio\Template\TemplateRendererInterface;
use PHPUnit\Framework\MockObject\Exception;
use Psr\Http\Message\ServerRequestInterface;
+use function file_put_contents;
+use function sys_get_temp_dir;
+use function uniqid;
+use function unlink;
+
class GetPageViewHandlerTest extends UnitTest
{
/**
@@ -102,6 +108,48 @@ public function testHandlePassesTheCategoriesToTheTemplate(): void
->handle($this->createRequest('page::queue'));
}
+ /**
+ * @throws Exception
+ */
+ public function testHandleReturnsTheMarkdownFileWhenAcceptRequestsIt(): void
+ {
+ $markdownFile = sys_get_temp_dir() . '/' . uniqid('dk-page-', true) . '.md';
+ file_put_contents($markdownFile, '# Dotkernel API');
+
+ $pageService = $this->createMock(PageServiceInterface::class);
+ $pageService
+ ->expects($this->once())
+ ->method('resolveMarkdownFilePath')
+ ->with('page::api')
+ ->willReturn($markdownFile);
+
+ $postRepository = $this->createMock(PostRepository::class);
+ $postRepository->expects($this->never())->method('getRecentPosts');
+
+ $response = $this->createHandler(postRepository: $postRepository, pageService: $pageService)
+ ->handle($this->createRequest('page::api', accept: 'text/markdown'));
+
+ $this->assertSame(StatusCodeInterface::STATUS_OK, $response->getStatusCode());
+ $this->assertStringContainsString('text/markdown', $response->getHeaderLine('Content-Type'));
+ $this->assertSame('# Dotkernel API', (string) $response->getBody());
+
+ unlink($markdownFile);
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function testHandleFallsBackToHtmlWhenNoMarkdownFileExistsForTheRequestedPage(): void
+ {
+ $pageService = $this->createStub(PageServiceInterface::class);
+ $pageService->method('resolveMarkdownFilePath')->willReturn(null);
+
+ $response = $this->createHandler(pageService: $pageService)
+ ->handle($this->createRequest('page::contact', accept: 'text/markdown'));
+
+ $this->assertStringContainsString('text/html', $response->getHeaderLine('Content-Type'));
+ }
+
/**
* @throws Exception
*/
@@ -109,29 +157,37 @@ private function createHandler(
?TemplateRendererInterface $template = null,
?CategoryRepository $categoryRepository = null,
?PostRepository $postRepository = null,
+ ?PageServiceInterface $pageService = null,
): GetPageViewHandler {
if (! $template instanceof TemplateRendererInterface) {
$template = $this->createStub(TemplateRendererInterface::class);
$template->method('render')->willReturn('');
}
+ if (! $pageService instanceof PageServiceInterface) {
+ $pageService = $this->createStub(PageServiceInterface::class);
+ $pageService->method('resolveMarkdownFilePath')->willReturn(null);
+ }
+
return new GetPageViewHandler(
$template,
$categoryRepository ?? $this->createStub(CategoryRepository::class),
$postRepository ?? $this->createStub(PostRepository::class),
+ $pageService,
);
}
/**
* @throws Exception
*/
- private function createRequest(string $routeName): ServerRequestInterface
+ private function createRequest(string $routeName, string $accept = ''): ServerRequestInterface
{
$routeResult = $this->createStub(RouteResult::class);
$routeResult->method('getMatchedRouteName')->willReturn($routeName);
$request = $this->createStub(ServerRequestInterface::class);
$request->method('getAttribute')->willReturn($routeResult);
+ $request->method('getHeaderLine')->willReturn($accept);
return $request;
}
diff --git a/test/Unit/Page/Handler/PageHandlerTest.php b/test/Unit/Page/Handler/PageHandlerTest.php
index 02a4bcf2..c8d52929 100644
--- a/test/Unit/Page/Handler/PageHandlerTest.php
+++ b/test/Unit/Page/Handler/PageHandlerTest.php
@@ -8,6 +8,7 @@
use Light\Blog\Repository\CategoryRepository;
use Light\Blog\Repository\PostRepository;
use Light\Page\Handler\GetPageViewHandler;
+use Light\Page\Service\PageServiceInterface;
use LightTest\Unit\UnitTest;
use Mezzio\Router\RouteResult;
use Mezzio\Template\TemplateRendererInterface;
@@ -55,11 +56,18 @@ public function testHandle(): void
->method('getAttribute')
->willReturn($routeResult);
+ $request
+ ->method('getHeaderLine')
+ ->willReturn('');
+
$template
->method('render')
->willReturn('' . $routeName . '
');
- $handler = new GetPageViewHandler($template, $categoryRepository, $postRepository);
+ $pageService = $this->createStub(PageServiceInterface::class);
+ $pageService->method('resolveMarkdownFilePath')->willReturn(null);
+
+ $handler = new GetPageViewHandler($template, $categoryRepository, $postRepository, $pageService);
$response = $handler->handle($request);
diff --git a/test/Unit/Page/Service/PageServiceTest.php b/test/Unit/Page/Service/PageServiceTest.php
index eb15edb0..44d5f77d 100644
--- a/test/Unit/Page/Service/PageServiceTest.php
+++ b/test/Unit/Page/Service/PageServiceTest.php
@@ -8,10 +8,45 @@
use Light\Page\Service\PageServiceInterface;
use LightTest\Unit\UnitTest;
+use function file_put_contents;
+use function mkdir;
+use function rmdir;
+use function sys_get_temp_dir;
+use function uniqid;
+use function unlink;
+
class PageServiceTest extends UnitTest
{
public function testWillInstantiate(): void
{
- $this->assertContainsOnlyInstancesOf(PageServiceInterface::class, [new PageService()]);
+ $this->assertContainsOnlyInstancesOf(PageServiceInterface::class, [new PageService('')]);
+ }
+
+ public function testResolveMarkdownFilePathReturnsTheFileForTheRouteSlug(): void
+ {
+ $mdPagesPath = sys_get_temp_dir() . '/' . uniqid('dk-pages-', true);
+ mkdir($mdPagesPath);
+ file_put_contents($mdPagesPath . '/api.md', '# Dotkernel API');
+
+ $filePath = (new PageService($mdPagesPath))->resolveMarkdownFilePath('page::api');
+
+ $this->assertSame($mdPagesPath . '/api.md', $filePath);
+
+ unlink($mdPagesPath . '/api.md');
+ rmdir($mdPagesPath);
+ }
+
+ public function testResolveMarkdownFilePathReturnsNullWhenNoFileExists(): void
+ {
+ $filePath = (new PageService(sys_get_temp_dir()))->resolveMarkdownFilePath('page::contact');
+
+ $this->assertNull($filePath);
+ }
+
+ public function testResolveMarkdownFilePathReturnsNullForARouteNameWithNoSlug(): void
+ {
+ $filePath = (new PageService(sys_get_temp_dir()))->resolveMarkdownFilePath('routewithnoslug');
+
+ $this->assertNull($filePath);
}
}