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); } }