说到博客软件,你并不缺乏选择。然而,尽管有这样的选择,而且现代博客软件的功能非常丰富,但现有的选择是否一定是正确的选择?
当然,像WordPress、Ghost、Gatsby和Wix这样的软件功能非常丰富--而且它们的用户界面通常非常流畅。但是,你想在写博客内容的基础上,还要为安装、配置和保护它们而烦恼吗?更重要的是,你能证明一些博客软件所需要的预算是合理的吗?
也许,你想做的就是用你喜欢的编辑器来写你的网站内容,使用一种为网络写作而设计的简单格式--Markdown,而不是通过用户界面。
如果是这样的话,那么在本教程中,我将向你展示如何创建一个从Markdown文件中提取内容的博客。这个博客使用了Slim框架(第4版)、标准PHP库(SPL)和几个外部包,以保持代码库尽可能的小和轻。
让我们开始吧!
前提条件
你需要以下条件来学习本教程:
该应用程序将如何工作
在你深入了解之前,让我们快速回顾一下这个应用程序是如何工作的,这样你就知道会有什么期待。在最简单的情况下,该应用程序有两个组成部分:
- 一个带有YAML前件的Markdown文件。
- 一个
BlogItem实体,它对Markdown文件中的信息进行建模。
Markdown文件的YAML前事项包含了你可能期望在博客中看到的一组最小的属性,包括发布日期、作为文章链接的slug、提要、标题、用于在文章顶部呈现的文章图片和用于在社交媒体上分享的图片,以及一组标签和类别。Markdown内容是文章的内容。
该应用程序有两条路线:
- 默认路由,显示所有可用文章的列表。
- 一个路由,显示一个基于其slug的博客文章。
当应用程序加载默认路由时,它遍历应用程序的数据目录中的每个Markdown文件,创建一个BlogItem 实体数组,每个Markdown文件一个。然后,它显示一个无序的帖子标题列表,与帖子的标题相连接。
如果用户点击列表中的一个帖子,他们就会被带到视图路径,在那里使用路径中的slug检索一个BlogItem 实体(如果有的话),然后再渲染。
为了简化对Markdown文件的迭代过程,该应用程序很好地利用了标准PHP库(SPL)中的迭代器,包括现有的和自定义的。
什么是迭代器?
迭代器(如ArrayIterator,FilesystemIterator,LimitIterator, 和RegexIterator *)*允许你使用原生的 PHP 循环结构(如foreach 和while 循环)来迭代可遍历的对象,如数组和文件系统。
每个迭代器都支持一种特殊的迭代方式,用类的名字表示,比如只遍历数组或目录,或者限制被遍历的项目的数量。
例如,如果你想遍历一个文件系统,就像应用程序那样,你可以使用DirectoryIterator 。你可以在下面的例子中看到如何使用DirectoryIterator :
<?php
$iterator = new DirectoryIterator(__DIR__ . '/data/posts');
/** @var SplFileInfo $item */
foreach ($iterator as $item) {
echo $item->getFilename() . "\n";
}
DirectoryIterator 将给定目录中的每个项目作为一个SplFileInfo对象返回。使用SplFileInfo ,你可以快速确定当前项目是一个文件、目录还是一个符号链接,以及它是否可读、可写或可执行--所有你需要知道的关于常见文件系统项目的事情。
DirectoryIterator 本身就很强大,因为它让你省去了所有的代码,但当你把它和其他迭代器结合起来时,它就变得更加强大。
假设你想对指定目录中的项目进行分页处理,而不是将它们全部列出。要做到这一点,你可以把DirectoryIterator 传递给LimitIterator ,它允许迭代一个有限的项目子集,然后在这个子集上迭代。
下面的例子--与早期版本几乎相同--显示了如何迭代DirectoryIterator 中的第10-20条记录:
<?php
$iterator = new LimitIterator(new DirectoryIterator(__DIR__ . '/data/posts'), 10, 10);
/** @var SplFileInfo $item */
foreach ($iterator as $item) {
echo $item->getFilename() . "\n";
}
迭代器的内容比我在这里介绍的要多得多,但这些是你需要知道的要点,这样代码才有意义。如果你想深入了解迭代器和SPL,请查阅php[architect]的Mastering the SPL Library。
创建Markdown博客应用程序
现在是创建应用程序的时候了。要做到这一点,运行下面的命令来创建核心目录并切换到它:
mkdir slim-framework-markdown-blog
cd slim-framework-markdown-blog
接下来,创建应用程序的目录结构,它将有四个核心目录:
- data:这里存放所有博客文章的Markdown文件。
- 公共目录:这里存放静态资产和bootstrap文件,index.php。
- 资源:这里存放路由视图模板。
- src:这里存放应用程序的源文件。
要创建它们以及它们的子目录,请运行下面的命令:
mkdir -p \
data/posts \
public/{css,images} \
resources/templates \
src/Blog/{ContentAggregator,Entity,Iterator,Sorter}
如果你使用的是Microsoft Windows,请运行下面的命令:
md data\posts public/css ^
data\posts public/images ^
resources/templates ^
src/Blog/ContentAggregator ^
src/Blog/Entity ^
src/Blog/Iterator ^
src/Blog/Sorter
安装所需的软件包
现在,是时候安装博客将使用的外部包了;具体来说,就是五个:
包 | 描述 |
FrontYAML | 这是一个适用于PHP的YAML Front matter的实现。它可以解析YAML和Markdown。 |
PHP Markdown | 该库包括一个PHP Markdown解析器和一个用于其兄弟姐妹的PHP Markdown Extra。 |
PHP-DI | 被称为"人类的依赖注入容器",PHP-DI是一个相当直接和直观的DI容器。你将用它来实例化一次某些应用程序的资源,然后让它们对应用程序可用。 |
瘦身框架 | |
Slim-PSR7 | 你使用这个库来将PSR-7集成到应用程序中。严格来说,这并不是必须的,但我觉得它使应用程序更容易维护和移植。 |
Slim Framework Twig View | 你将使用这个包来使用Twig渲染视图内容;比如请求验证码的表单,以及上传图片和更简单的视图,比如成功输出。你可能已经注意到,Twig并不在这个列表中。这是因为Slim Framework Twig View需要它作为一个依赖项。 |
Twig Intl扩展 | 这个包是一个Twig扩展,它提供了国际化的支持,包括格式化日期的能力,应用程序将利用它。 |
要安装它们,请在你的终端上运行下面的命令,在项目的根目录下:
composer require --with-all-dependencies \
mnapoli/front-yaml \
michelf/php-markdown \
php-di/php-di \
slim/psr7 \
slim/slim \
slim/twig-view \
twig/intl-extra
在composer.json中添加PSR-4自动加载器配置
由于你将创建一些类,你需要在composer.json中添加PSR-4 Autoloader配置,以便这些类可以被应用所使用。要做到这一点,在composer.json中现有的require 部分之后添加下面的JSON片段:
"autoload": {
"psr-4": {
"MarkdownBlog\\": "src/Blog"
}
},
添加Slim Framework代码
现在,所需的软件包已经安装完毕,添加Slim框架的核心代码,你将在整个教程中逐步建立这些代码。要做到这一点,在公共目录下创建一个新文件,命名为index.php。然后,将下面的代码粘贴到新文件中:
<?php
declare(strict_types=1);
use DI\Container;
use Psr\Http\Message\{
ResponseInterface as Response,
ServerRequestInterface as Request
};
use Slim\Factory\AppFactory;
use Slim\Views\{Twig,TwigMiddleware};
use Twig\Extra\Intl\IntlExtension;
require __DIR__ . '/../vendor/autoload.php';
$container = new Container();
$container->set('view', function($c) {
$twig = Twig::create(__DIR__ . '/../resources/templates');
$twig->addExtension(new IntlExtension());
return $twig;
});
AppFactory::setContainer($container);
$app = AppFactory::create();
$app->add(TwigMiddleware::createFromContainer($app));
$app->map(['GET'], '/', function (Request $request, Response $response, array $args) {
return $this->get('view')->render($response, 'index.html.twig');
});
$app->run();
该代码导入了所需的类,并设置了应用程序的依赖注入(DI)容器。一个服务被注册到容器中,即应用程序的视图层,它由Twig驱动。Twig对象本身被初始化为应用程序模板的基本路径,这样它就知道在哪里可以找到它们。
之后,一个新的Slim应用程序被初始化($app)。默认的路由被注册,它只接受GET请求。对该路由的请求不会有什么作用。它将返回其模板的渲染内容。最后,通过调用run() 方法,应用程序被启动。
创建默认路由的模板
模板一开始不会做很多事情;只是显示网站的名字:"Slim Framework Markdown Blog"。 首先,在resources/templates中创建一个新文件,命名为index.html.twig,并在其中粘贴以下代码:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="/css/styles.css" rel="stylesheet">
<title>Slim Framework Markdown Blog</title>
</head>
<body>
<h1>Slim Framework Markdown Blog</h1>
</body>
</html>
然后,将CSS文件下载到public/css,命名为style.css。有了这些,让我们检查一下应用程序的核心是否工作。通过运行下面的命令来启动应用程序:
php -S 127.0.0.1:8080 -t public
然后,在你选择的浏览器中打开http://localhost:8080,你应该看到它看起来像下面的屏幕截图。

确认应用程序工作后,按ctrl+c停止应用程序。
创建一个博客文章实体
现在,你将开始创建应用程序的代码,首先是BlogItem 实体。在src/Blog/Entity中创建一个名为BlogItem.php的新文件,并在其中添加以下代码:
<?php
declare(strict_types=1);
namespace MarkdownBlog\Entity;
use DateTime;
use Michelf\MarkdownExtra;
class BlogItem
{
private DateTime $publishDate;
private string $slug = '';
private string $title = '';
private string $image = '';
private string $synopsis = '';
private string $content = '';
private array $categories = [];
private array $tags = [];
public function __construct(array $options = [])
{
$this->populate($options);
}
public function populate(array $options = [])
{
$properties = get_class_vars(__CLASS__);
foreach ($options as $key => $value) {
if (array_key_exists($key, $properties) && !empty($value)) {
$this->$key = ($key === 'publishDate')
? new \DateTime($value)
: $value;
}
}
}
public function getPublishDate(): DateTime
{
return $this->publishDate;
}
public function getSlug(): string
{
return $this->slug;
}
public function getImage(): string
{
return $this->image;
}
public function getTitle(): string
{
return $this->title;
}
public function getContent(): string
{
$markdownParser = new MarkdownExtra();
return $markdownParser->defaultTransform($this->content);
}
public function getTags(): array
{
return $this->tags;
}
public function getCategories(): array
{
return $this->categories;
}
public function getSynopsis(): string
{
return $this->synopsis ?? '';
}
}
这个类并没有做很多事情。它设置了八个属性($publishDate,$slug,$title,$image,$synopsis,$content,$categories, 和$tags ),它们将存储来自Markdown文件的某一方面的数据。
populate 方法从数据的关联数组中填充,或者说填充实体,并且每个属性都可以通过附带的getter方法来访问。
唯一值得注意的是,$publishDate 被初始化为一个DateTime对象,使用提供的值,并且getContent 在返回之前将其Markdown内容转换为HTML。
为Markdown文件创建一个过滤器
现在,是时候创建MarkdownFileFilterIterator ,ContentAggregatorFilesystem ,同时在data/posts目录上迭代,以过滤掉不是Markdown文件的文件。为了节省时间和精力,MarkdownFileFilterIterator 将扩展FilterIterator,这是作为标准PHP库(SPL)一部分的许多 迭代器中的一个。
如果你想深入了解SPL,请查阅php[architecture]的Mastering the SPL Library。
为什么要扩展FilterIterator ? 嗯,由于博客的内容来自Markdown文件,它需要对其中的一个或多个文件进行迭代。然而,一个特定的目录可能包含任何数量的文件类型,比如文本文件、AsciiDoc文件、电子表格等等,而不仅仅是Markdown文件。
如果没有FilterIterator ,你就会有一个相当冗长的迭代过程,它要检查一个文件是否是一个文件而不是一个目录,以及它是否是一个Markdown文件而不是一个电子表格、符号链接等等。对我来说,这样做的时候都是相当混乱的。
然而,通过扩展FilterIterator ,你可以把文件过滤逻辑移到一个可重用的类中,让迭代逻辑被重用,而不是在整个代码库中重复,把它与迭代逻辑分开,大大简化了迭代逻辑。
在src/Blog/Iterator 中创建一个新的 PHP 文件MarkdownFileFilterIterator.php,并将下面的代码粘贴到其中:
<?php
declare(strict_types=1);
namespace MarkdownBlog\Iterator;
use \DirectoryIterator;
use \SplFileInfo;
class MarkdownFileFilterIterator extends \FilterIterator
{
public function __construct(DirectoryIterator $iterator)
{
parent::__construct($iterator);
$this->rewind();
}
public function accept(): bool
{
/** @var SplFileInfo $item */
$item = $this->getInnerIterator()->current();
if (!$item instanceof SplFileInfo) {
return false;
}
if ($item->isDot() || !$item->isFile() || !$item->isReadable()) {
return false;
}
if (!in_array($item->getExtension(), ['md', 'markdown'])) {
return false;
}
return true;
}
}
该类的构造函数接收一个DirectoryIterator ,它可以迭代一个目录和其中包含的任何文件,并将其设置为该类的内部迭代器。DirectoryIterator 也会遍历指定目录下的任何目录,但不会进入其中任何一个目录。
accept 方法是神奇发生的地方。在那里,它检查当前的项目是否是一个SplFileInfo 的实例。这一点通过使用DirectoryIterator 来处理。 然后它(相当粗略地)过滤掉任何文件:
- 是一个点文件或一个目录
- 不可读
- 没有
.md或.markdown的扩展名
创建一个内容聚合器来聚合Markdown的内容
接下来,你将创建一个内容聚合器,由一个接口(ContentAggregatorInterface )和两个类(ContentAggregatorFilesystem,ContentAggregatorFactory )组成,它遍历所有提供的Markdown文件并为每个文件提供一个BlogItem 实体数组。
ContentAggregatorInterface
在src/Blog/ContentAggregator 中,创建一个名为ContentAggregatorInterface.php 的新文件,并在其中添加以下代码:
<?php
declare(strict_types=1);
namespace MarkdownBlog\ContentAggregator;
use MarkdownBlog\Entity\BlogItem;
interface ContentAggregatorInterface
{
public function findItemBySlug(string $slug): ?BlogItem;
public function getItems(): array;
}
这个接口定义了两个方法:getItems 和findItemBySlug ,ContentAggregatorFilesystem 将实现这两个方法。这不是严格意义上的需要,然而,我更喜欢根据一个接口,而不是一个具体的规范来编码。
ContentAggregatorFilesystem
接下来,在src/Blog/ContentAggregator 中创建第二个新文件,命名为ContentAggregatorFilesystem.php,并在其中添加以下代码。
<?php
namespace MarkdownBlog\ContentAggregator;
use MarkdownBlog\Iterator\MarkdownFileFilterIterator;
use MarkdownBlog\Entity\BlogItem;
use Mni\FrontYAML\Document;
use Mni\FrontYAML\Parser;
class ContentAggregatorFilesystem implements ContentAggregatorInterface
{
protected Parser $fileParser;
protected MarkdownFileFilterIterator $fileIterator;
private array $items = [];
public function __construct(
MarkdownFileFilterIterator $fileIterator,
Parser $fileParser
) {
$this->fileParser = $fileParser;
$this->fileIterator = $fileIterator;
$this->buildItemsList();
}
public function getItems(): array
{
return $this->items;
}
protected function buildItemsList(): void
{
foreach ($this->fileIterator as $file) {
$article = $this->buildItemFromFile($file);
if (! is_null($article)) {
$this->items[] = $article;
}
}
}
public function findItemBySlug(string $slug): ?BlogItem
{
foreach ($this->items as $article) {
if ($article->getSlug() === $slug) {
return $article;
}
}
return null;
}
public function buildItemFromFile(\SplFileInfo $file): ?BlogItem
{
$fileContent = file_get_contents($file->getPathname());
$document = $this->fileParser->parse($fileContent, false);
$item = new BlogItem();
$item->populate($this->getItemData($document));
return $item;
}
public function getItemData(Document $document): array
{
return [
'publishDate' => $document->getYAML()['publish_date'] ?? '',
'slug' => $document->getYAML()['slug'] ?? '',
'synopsis' => $document->getYAML()['synopsis'] ?? '',
'title' => $document->getYAML()['title'] ?? '',
'image' => $document->getYAML()['image'] ?? '',
'categories' => $document->getYAML()['categories'] ?? [],
'tags' => $document->getYAML()['tags'] ?? [],
'content' => $document->getContent(),
];
}
}
该类实现了ContentAggregatorInterface ,并负责从本地文件系统的文件中聚合博客内容。它相当长,所以让我们一步一步来:
public function __construct(
MarkdownFileFilterIterator $fileIterator,
Parser $fileParser
) {
$this->fileIterator = $fileIterator;
$this->fileParser = $fileParser;
$this->buildItemsList();
}
该类的构造函数需要两个参数,一个是MarkdownFileFilterIterator ,一个是\Mni\FrontYAML\Parser 。第一个是一个迭代器,允许快速遍历可用的Markdown数据文件。第二个则提供了从这些文件中解析相关信息的功能。
在从两个参数中初始化了两个类成员变量之后,buildItemsList 方法被调用,以聚合来自Markdown文件的博客项目数据:
protected function buildItemsList(): void
{
foreach ($this->fileIterator as $file) {
$article = $this->buildItemFromFile($file);
if (! is_null($article)) {
$this->items[] = $article;
}
}
}
public function getItems(): array
{
return $this->items;
}
接下来,我们定义了两个函数:buildItemsList 和getItems:
buildItemsList这是使用 对Markdown文件进行迭代的地方。对于迭代器中的每个文件,通过将文件传递给 方法来初始化一个 对象,然后将其添加到 数组中。MarkdownFileFilterIteratorbuildItemFromFileBlogItemitemsgetItems返回在 中聚合的 对象的列表。buildItemsListBlogItem
public function findItemBySlug(string $slug): ?BlogItem
{
foreach ($this->items as $article) {
if ($article->getSlug() === $slug) {
return $article;
}
}
return null;
}
下一个函数,findItemBySlug ,接收一个文章的标题,在可用的BlogItem 对象中寻找一个与标题匹配的对象,如果找到则返回。否则,它将返回null :
public function buildItemFromFile(\SplFileInfo $file): ?BlogItem
{
$fileContent = file_get_contents($file->getPathname());
$document = $this->fileParser->parse($fileContent, false);
$item = new BlogItem();
$item->populate($this->getItemData($document));
return $item;
}
buildItemFromFile SplFileInfo 对象,并使用它来检索一个文件的内容,然后将其传递给 方法。这个方法解析出YAML前言和Markdown内容到一个数组中,然后用它来水化并返回一个新的 对象。getItemData BlogItem:
public function getItemData(Document $document): array
{
return [
'publishDate' => $document->getYAML()['publish_date'] ?? '',
'slug' => $document->getYAML()['slug'] ?? '',
'synopsis' => $document->getYAML()['synopsis'] ?? '',
'title' => $document->getYAML()['title'] ?? '',
'image' => $document->getYAML()['image'] ?? '',
'categories' => $document->getYAML()['categories'] ?? [],
'tags' => $document->getYAML()['tags'] ?? [],
'content' => $document->getContent(),
];
}
getItemData 接受一个 对象,其中包含YAML前篇和Markdown内容。这个对象被用来填充并返回一个从文件中聚合的信息的关联数组。\Mni\FrontYAML\Document
ContentAggregatorFactory
最后,在src/Blog/ContentAggregator 中创建第三个新文件,名为ContentAggregatorFactory.php,并在其中添加以下代码:
<?php
declare(strict_types=1);
namespace MarkdownBlog\ContentAggregator;
use MarkdownBlog\Iterator\MarkdownFileFilterIterator;
class ContentAggregatorFactory
{
public function __invoke(array $config): ContentAggregatorInterface
{
$iterator = new MarkdownFileFilterIterator(
new \DirectoryIterator($config['path'])
);
return new ContentAggregatorFilesystem($iterator, $config['parser']);
}
}
该类的__invoke 方法用一个DirectoryIterator 来实例化一个MarkdownFileFilterIterator ,该方法又用Markdown文件目录的路径来实例化。然后,MarkdownFileFilterIterator 和一个\Mni\FrontYAML\Parser 对象被用来实例化并返回一个ContentAggregatorFilesystem 对象。
更新Composer的自动加载器
现在核心类已经被创建了,你需要更新Composer的自动加载器,这样它们就会被找到。要做到这一点,在项目的根目录下运行以下命令:
composer dump-autoload
在DI容器中注册内容聚合服务
在类和接口准备好后,你现在需要向DI容器注册一个新的服务,这样你就可以在整个应用程序中访问聚合的内容。要做到这一点,在public/index.php中,更新代码到第一个路由的定义,如下面的代码例子中所强调的:
<?php
declare(strict_types=1);
use DI\Container;
use MarkdownBlog\ContentAggregator\ContentAggregatorFactory;
use MarkdownBlog\ContentAggregator\ContentAggregatorInterface;
use Mni\FrontYAML\Parser;
use Psr\Http\Message\{
ResponseInterface as Response,
ServerRequestInterface as Request
};
use Slim\Factory\AppFactory;
use Slim\Views\{Twig,TwigMiddleware};
use Twig\Extra\Intl\IntlExtension;
require __DIR__ . '/../vendor/autoload.php';
$container = new Container();
$container->set('view', function($c) {
$twig = Twig::create(__DIR__ . '/../resources/templates');
$twig->addExtension(new IntlExtension());
return $twig;
});
$container->set(
ContentAggregatorInterface::class,
fn() => (new ContentAggregatorFactory())->__invoke([
'path' => __DIR__ . '/../data/posts',
'parser' => new Parser(),
])
);
AppFactory::setContainer($container);
$app = AppFactory::create();
$app->add(TwigMiddleware::createFromContainer($app));
这些变化注册了一个新的服务,它将是一个ContentAggregatorFilesystem 对象,由ContentAggregatorFactory ,以ContentAggregatorInterface::class 为键进行初始化。
创建一个查看所有博客文章的路由
接下来要做的是重构默认的路由,以便它能呈现出博客项目。要做到这一点,首先,更新public/index.php中的默认路由,以符合下面的代码示例:
$app->map(['GET'], '/', function (Request $request, Response $response, array $args) {
$view = $this->get('view');
/** @var ContentAggregatorInterface $contentAggregator */
$contentAggregator = $this->get(ContentAggregatorInterface::class);
return $view->render(
$response,
'index.html.twig',
['items' => $contentAggregator->getItems()]
);
});
这些更新从DI容器中检索ContentAggregatorInterface 服务,并设置一个视图模板项,items ,它包含一个BlogItem 对象的列表。
更新默认路由的模板以呈现博客文章项目
由于默认路由的模板现在可以访问博客文章的列表,它需要被更新以呈现它们。要做到这一点,请更新resources/templates/index.html.twig,以匹配下面的代码:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="/css/styles.css" rel="stylesheet">
<title>Slim Framework Markdown Blog</title>
</head>
<body>
<h1>Slim Framework Markdown Blog</h1>
<h2>Items</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-4 sm:gap-y-8">
{% for item in items %}
<div>
<img src="/images/posts/{{ item.image }}" class="rounded-lg">
<a class="no-underline" href="/item/{{ item.slug }}"><h3>{{ item.title }}</h3></a>
{{ item.publishDate|format_datetime(pattern="dd.MM.Y") }}
</div>
{% endfor %}
</div>
<footer>
<p>© Matthew Setter. <a href="/">Impressum</a>. <a href="/">Privacy Policy</a>. <a href="/">Terms of Use</a>.
<a href="/">Disclaimer</a> </p>
</footer>
</body>
</html>
模板现在使用一个for 循环来遍历可用的博客文章项目(items),呈现它们的标题、lug和发布日期(使用Twig的format_date函数进行格式化)。在public/images/posts目录下的文章项目的图片,也会使用文章的图片值来呈现。
创建一个路由来查看一个单独的博客文章
在对默认路由模板的修改中,列表将每个项目的标题包裹在一个锚标签中,这个锚标签链接到/item{{ item.slug }} ,例如,可能呈现为/item/hello-world 。
鉴于此,你需要创建一个可以处理这些请求的路由。要做到这一点,在public/index.php的默认路由下添加第二个路由,其代码如下:
$app->map(['GET'], '/item/{slug}', function (Request $request, Response $response, array $args) {
$view = $this->get('view');
/** @var ContentAggregatorInterface $contentAggregator */
$contentAggregator = $this->get(ContentAggregatorInterface::class);
return $view->render(
$response,
'view.html.twig',
['item' => $contentAggregator->findItemBySlug($args['slug'])]
);
});
与默认路由类似,视图项目路由从应用程序的DI容器中检索ContentAggregatorInterface 和view 服务。然后,它通过调用内容聚合器的findItemBySlug 方法来检索一个BlogItem 对象,并将从请求中检索到的项目名称传递给它。然后,BlogItem 作为视图模板变量被传递,模板被渲染并返回。
创建视图路由的模板
接下来,你必须创建路由的视图模板。要做到这一点,在resources/templates中创建一个名为view.html.twig的新文件,并将下面的代码粘贴到其中:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="/css/styles.css" rel="stylesheet">
<title>{{ item.title }} | Slim Framework Markdown Blog</title>
</head>
<body>
<header class="grid grid-cols-2 gap-2 mb-2 md:mb-4">
<div class="text-left">
<a href="/"
class="text-sm md:text-base text-stone-800 font-bold no-underline hover:underline transition ease-in-out delay-150 duration-300 underline-offset-4 decoration-2"
>Slim Framework Markdown Blog</a>
</div>
<div class="text-right">
<a href="/" title="Back to the home page"
class="text-sm md:text-base text-stone-500 hover:underline hover:text-stone-600"
>← Back to the home page</a>
</div>
</header>
<div class="content border-t-4 border-slate-700 pt-2 md:pt-6 mt-4">
<h1 class="mb-0 pb-0">{{ item.title }}</h1>
<p class="border-slate-700 font-bold mt-2 pt-0 pb-2 md:pb-2 mb-2">
By Matthew Setter, {{ item.publishDate|format_datetime(pattern="dd.MM.Y") }}
</p>
<img src="/images/posts/{{ item.image }}" class="rounded-lg mb-4">
<p class="synopsis">{{ item.synopsis }}</p>
{{ item.content|raw }}
</div>
<footer>
<p>© Matthew Setter. <a href="/">Impressum</a>. <a href="/">Privacy Policy</a>. <a href="/">Terms of Use</a>.
<a href="/">Disclaimer</a> </p>
</footer>
</body>
</html>
它与默认路由的模板非常相似。主要区别在于,它显示一个博客项目的细节,并在页面的标题前加上博客项目的标题。
创建Markdown源文件
现在,创建一个初始的博客文章,这样就会有一些内容可迭代和查看。在data/posts中创建一个名为hello-world.md的新文件,并在其中添加以下内容:
---
publish_date: 10.01.2022
slug: hello-world
synopsis: Hello and welcome to the blog. In this, the first post, I'll step you through what the blog is about and all the awesome things you're going to learn about by reading it.
title: Hey! Welcome to the blog!
image: hello-world.png
categories:
- General
tags:
- Getting Started
---
# Hello world!
Welcome to the Slim Framework Markdown Blog. This is your first post. Edit or delete it, then **start writing!**
你可以随意改变YAML前言和帖子的Markdown内容,只要你认为合适。接下来,下载一组预制的Markdown文件到data/posts目录,并下载一组帖子图片到public/images/posts目录,这样,应用程序就更像一个实时版本。
测试应用程序
现在你已经有了所有的代码,在测试应用程序是否工作之前,通过运行下面的命令启动它
php -S 127.0.0.1:8080 -t public
然后,在你选择的浏览器中打开http://localhost:8080。这一次,你应该看到一个有一个或多个博客文章的列表,就像下面的截图。
如果你点击帖子的名字,你就可以查看帖子,它看起来就像下面的截图。

创建一个排序器,按相反的日期顺序对帖子进行排序
虽然这个应用是有效的,但你可能注意到,帖子呈现的顺序是基于Markdown文件名的,而不是基于帖子的发布日期的。这不是一个合乎逻辑的,也不是一个直观的事情。让我们改进一下应用程序,把文章从最新的到最旧的排序,就像你在一个现代博客上所期望的那样。
要做到这一点,你要创建一个类,我们可以把它传递给PHP的usort函数。如果你不熟悉这个函数,它将遍历一个数组,并根据一个自定义的回调进行排序。
回调对数组中的当前和下一个元素进行比较,如果第一个参数被认为分别小于、等于或大于第二个参数,则返回一个小于、等于或大于零的整数。两篇博文的比较点将是它们的发布日期。
在src/Sorter 中创建一个名为SortByReverseDateOrder.php的新文件。然后,在该文件中,粘贴下面的代码:
<?php
declare(strict_types=1);
namespace MarkdownBlog\Sorter;
use MarkdownBlog\Entity\BlogItem;
class SortByReverseDateOrder
{
public function __invoke(BlogItem $a, BlogItem $b): int
{
$firstDate = $a->getPublishDate();
$secondDate = $b->getPublishDate();
if ($firstDate == $secondDate) {
return 0;
}
return ($firstDate > $secondDate) ? -1 : 1;
}
}
这个类是一个可调用的,因为它实现了__invoke 魔法方法。该方法接收两个BlogItem 对象并根据它们的发布日期进行比较。下面是比较结果的作用:
- 如果发布日期相同,该方法返回
0,因为顺序不需要改变。 - 如果第一篇的发布日期大于第二篇的发布日期,该方法返回
-1,因为第一篇博文需要在第二篇之前进行排序。 - 否则,该方法返回
1,因为第二篇博文需要排序在第一篇之前。
类已经准备好了,请更新public/index.php中第一个路由的主体,以符合下面的代码。我已经强调了需要修改的行:
$app->map(['GET'], '/', function (Request $request, Response $response, array $args) {
$view = $this->get('view');
/** @var ContentAggregatorInterface $contentAggregator */
$contentAggregator = $this->get(ContentAggregatorInterface::class);
$items = $contentAggregator->getItems();
$sorter = new \MarkdownBlog\Sorter\SortByReverseDateOrder();
usort($items, $sorter);
return $view->render(
$response,
'index.html.twig',
['items' => $items]
);
});
在这个版本中,首先检索汇总的项目,然后通过将它们和一个SortByReverseDateOrder 实例一起传递给PHP的usort 方法来进行排序。然后,排序后的项目被作为一个视图变量传递。
如果你再次打开这个应用程序,你会看到帖子列表现在是按相反的日期顺序排序的,如下图所示。

为发布的帖子创建一个过滤器
现在是最后一个类的时候了,这个类可以过滤掉任何预定在未来某个时间的帖子,名为PublishedItemFilterIterator 。这个类将比较今天的日期和博客项目的发布日期,如果发布日期是在未来,则返回false。在src/Iterator 中,创建一个新文件,命名为PublishedItemFilterIterator.php,并将下面的代码粘贴到其中。
<?php
declare(strict_types=1);
namespace MarkdownBlog\Iterator;
use DateTime, Iterator;
use MarkdownBlog\Entity\BlogItem;
class PublishedItemFilterIterator extends \FilterIterator
{
public function __construct(Iterator $iterator)
{
parent::__construct($iterator);
$this->rewind();
}
public function accept(): bool
{
/** @var BlogItem $episode */
$episode = $this->getInnerIterator()->current();
return $episode->getPublishDate() <= new DateTime();
}
}
更新默认路由以查看已发布的帖子
接下来,在public/index.php中,用下面的版本替换默认路由:
$app->map(['GET'], '/', function (Request $request, Response $response, array $args) {
$view = $this->get('view');
/** @var ContentAggregatorInterface $contentAggregator */
$contentAggregator = $this->get(ContentAggregatorInterface::class);
$sorter = new \MarkdownBlog\Sorter\SortByReverseDateOrder();
$items = $contentAggregator->getItems();
usort($items, $sorter);
$iterator = new \MarkdownBlog\Iterator\PublishedItemFilterIterator(
new ArrayIterator($items)
);
return $view->render(
$response,
'index.html.twig',
['items' => $iterator]
);
});
在突出显示的几行中,你可以看到在数组被排序后,它被用来初始化一个新的ArrayIterator,反过来,它被用来初始化一个PublishedItemFilterIterator ($iterator)。然后,$iterator 被传递给渲染的模板作为博客文章项目的来源。
如果你再次打开应用程序,你会发现现在看不到发布日期在未来的文章了,如下图所示。
这就是如何用PHP创建一个Markdown博客
使用Slim框架、标准PHP库(SPL)和两个外部包,你已经知道了在PHP中创建一个Markdown博客是多么简单。
由于它从带有YAML前题的Markdown文件中提取内容,你不需要学习新的界面来使用它。相反,你可以直接开始写你的博客内容,只需将Markdown文件存储在data/posts目录中。去吧,写吧,享受吧。
