add 'adding content' chapter

This commit is contained in:
lubiana 2022-04-05 19:09:40 +02:00 committed by Andre Lubian
parent ab3227b75f
commit eb20213b94
20 changed files with 884 additions and 251 deletions

View file

@ -1,4 +1,4 @@
[<< previous](12-refactoring.md) | [next >>](14-invoker.md) [<< previous](12-refactoring.md) | [next >>](15-adding-content.md)
### Middleware ### Middleware
@ -295,4 +295,4 @@ Lets try if you can make the kernel work with our created Pipeline implementatio
pipeline a little bit, so that it can accept a class-string of a middleware and resolves that with the help of a pipeline a little bit, so that it can accept a class-string of a middleware and resolves that with the help of a
dependency container, if you want you can do that as well. dependency container, if you want you can do that as well.
[<< previous](12-refactoring.md) | [next >>](14-invoker.md) [<< previous](12-refactoring.md) | [next >>](15-adding-content.md)

248
15-adding-content.md Normal file
View file

@ -0,0 +1,248 @@
[<< previous](14-middleware.md) | [next >>](14-invoker.md)
### Adding Content
By now we did not really display anything but some examples to in our application and it is now time to make our app
display some content. For example we could our app be able to display the Markdown files used in this tutorial as
nicely rendered HTML Pages that can be viewed in the browser instead of the editor you are using.
So lets start by copying the markdown files to our app directory. I have created a new folder 'data/pages' and placed all
the markdown files in there.
Next we need a markdown parser, a pretty simple one is [Parsedown](https://parsedown.org/), if you want more features
you could also use the [Commonmark parser](https://commonmark.thephpleague.com/). I will choose Parsedown here, but you
can use whatever you like.
After installing Parsedown lets write a Markdownparser interface and an implementation using parsedown.
We only need one function that receives a string of Markdown and returns the HTML represantion (as a string as well).
```php
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Template;
interface MarkdownParser
{
public function parse(string $markdown): string;
}
```
By the namespace you will already have guessed that I called placed in interface in a file calles MarkdownParser.php in
the src/Template folder. Lets put our Parsedown implementation right next to it in a file called ParsedownParser.php
```php
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Template;
use Parsedown;
final class ParsedownRenderer implements MarkdownParser
{
public function __construct(private Parsedown $parser)
{
}
public function parse(string $markdown): string
{
return $this->parser->parse($markdown);
}
}
```
We could now use the ParsedownRender class directly in our actions by typehinting the classname as an argument to the
constructor or a method, but as we always want to rely on an interface instead of an implementation we need to define
the the ParsedownRenderer as the correct implementation for the MarkdownRenderer interface in the dependencies file:
```php
...
\Lubian\NoFramework\Template\MarkdownParser::class => fn(\Lubian\NoFramework\Template\ParsedownParser $p) => $p,
...
```
You can test that in our "Other.php" action and try out if the Parser works and is able to render Markdown to HTML:
```php
public function someFunctionName(ResponseInterface $response, MarkdownParser $parser): ResponseInterface
{
$html = $parser->parse('This *works* **too!**');
$response->getBody()->write($html);
return $response->withStatus(200);
}
```
But we want to display complete Pages written in Markdown, it would also be neat to be able to display a list of all
available pages. For that we need a few things:
Firstly we need two new Templates, one for the list of the Pages, and the second one for displaying a single pages
content. Create a new folder in `templates/page` with to files:
`templates/page/list.html`
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Pages</title>
<link rel="stylesheet" href="https://unpkg.com/@picocss/pico@latest/css/pico.classless.min.css">
</head>
<body>
<main>
<ul>
{{#pages}}
<li>
<a href="/page/{{title}}">{{id}}: {{title}}</a>
</li>
{{/pages}}
</ul>
</main>
</body>
</html>
```
This template iterates over a provided array of pages, each element consists of the two properties: an id and a title,
those are simply displayed using an unordered list.
`templates/page/show.html`
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{title}}</title>
<link rel="stylesheet" href="https://unpkg.com/@picocss/pico@latest/css/pico.classless.min.css">
</head>
<body>
<main>
{{{content}}}
</main>
</body>
</html>
```
The second templates displays a single rendered markdown page. As data it expects the title and the content as array.
I used an extra bracket for the content ```{{{content}}}``` so that the Mustache-Renderer does not escape the provided
html and thereby destroys the the parsed markdown.
You might have spotted that I added [Pico.css](https://picocss.com/) which is just a very small css framework to make the
pages a little bit nicer to look at. It mostly provides some typography styles that work great with rendered Markdown,
but you can leave that out or use any other css framework you like.
After you have taken care of the templating side we can now create an new Action class with two methods to display use
our markdown files and the templates to create the pages. As we have two templates I propose to use Two methods in our
Action:
`src/Action/Page.php`
```php
function show(string $name): \Psr\Http\Message\ResponseInterface;
function list(): \Psr\Http\Message\ResponseInterface;
```
Lets define two routes. `/page` should display the overview of all pages, and if the add the name of chapter to the
route, `/page/adding-content` for example, the show action should be called with the name as a variable:
`config/routes.php`
```php
$r->addRoute('GET', '/page', [Page::class, 'list']);
$r->addRoute('GET', '/page/{page}', [Page::class, 'show']);
```
Here is my Implementation. If have added a little regex replacement in the show method that replaces the links to the
next and previous chapter so that it works with our routing configuration.
`src/Action/Page.php`
```php
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Action;
use Lubian\NoFramework\Exception\InternalServerError;
use Lubian\NoFramework\Template\MarkdownParser;
use Lubian\NoFramework\Template\Renderer;
use Psr\Http\Message\ResponseInterface;
use function array_filter;
use function array_map;
use function array_values;
use function file_get_contents;
use function glob;
use function preg_replace;
use function str_contains;
use function str_replace;
use function substr;
class Page
{
public function __construct(
private ResponseInterface $response,
private MarkdownParser $parser,
private Renderer $renderer,
private string $pagesPath = __DIR__ . '/../../data/pages/'
) {
}
public function show(
string $page,
): ResponseInterface {
$page = array_values(
array_filter(
$this->getPages(),
fn (string $filename) => str_contains($filename, $page)
)
)[0];
$markdown = file_get_contents($page);
// fix the next and previous buttons to work with our routing
$markdown = preg_replace('/\(\d\d-/m', '(', $markdown);
$markdown = str_replace('.md)', ')', $markdown);
$page = str_replace([$this->pagesPath, '.md'], ['', ''], $page);
$data = [
'title' => substr($page, 3),
'content' => $this->parser->parse($markdown),
];
$html = $this->renderer->render('page/show', $data);
$this->response->getBody()->write($html);
return $this->response;
}
public function list(): ResponseInterface
{
$pages = array_map(function (string $page) {
$page = str_replace([$this->pagesPath, '.md'], ['', ''], $page);
return [
'id' => substr($page, 0, 2),
'title' => substr($page, 3),
];
}, $this->getPages());
$html = $this->renderer->render('page/list', ['pages' => $pages]);
$this->response->getBody()->write($html);
return $this->response;
}
/**
* @return string[]
*/
private function getPages(): array
{
$files = glob($this->pagesPath . '*.md');
if ($files === false) {
throw new InternalServerError('cannot read pages');
}
return $files;
}
}
```
You can now navigate your Browser to [localhost:1234/page][http://localhost:1234/page] and try out if everything works.
Of course this code is far from looking good. We heavily rely on the pages being files in the filesystem, and the action
should never be aware of the filesystem in the first place, also we have a lot of string replacements and other repetetive
code in the file. And phpstan is gonna scream at us a lot, but if we rewrite the code to satisfy all the checks we would
add even more lines to that simple class, so lets move on to the next chapter where we move all the logic to seperate
classes following our holy SOLID principles :)
[<< previous](14-middleware.md) | [next >>](14-invoker.md)

View file

@ -13,7 +13,8 @@
"middlewares/whoops": "^2.0", "middlewares/whoops": "^2.0",
"erusev/parsedown": "^1.7", "erusev/parsedown": "^1.7",
"symfony/cache": "^6.0", "symfony/cache": "^6.0",
"doctrine/orm": "^2.11" "doctrine/orm": "^2.11",
"league/commonmark": "^2.2"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {

460
app/composer.lock generated
View file

@ -4,8 +4,83 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "a3762dd11bab0c9e948d3a73b7f252b9", "content-hash": "1e8469bfebe6479a139b946b8aba49de",
"packages": [ "packages": [
{
"name": "dflydev/dot-access-data",
"version": "v3.0.1",
"source": {
"type": "git",
"url": "https://github.com/dflydev/dflydev-dot-access-data.git",
"reference": "0992cc19268b259a39e86f296da5f0677841f42c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/0992cc19268b259a39e86f296da5f0677841f42c",
"reference": "0992cc19268b259a39e86f296da5f0677841f42c",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"require-dev": {
"phpstan/phpstan": "^0.12.42",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.3",
"scrutinizer/ocular": "1.6.0",
"squizlabs/php_codesniffer": "^3.5",
"vimeo/psalm": "^3.14"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Dflydev\\DotAccessData\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Dragonfly Development Inc.",
"email": "info@dflydev.com",
"homepage": "http://dflydev.com"
},
{
"name": "Beau Simensen",
"email": "beau@dflydev.com",
"homepage": "http://beausimensen.com"
},
{
"name": "Carlos Frutos",
"email": "carlos@kiwing.it",
"homepage": "https://github.com/cfrutos"
},
{
"name": "Colin O'Dell",
"email": "colinodell@gmail.com",
"homepage": "https://www.colinodell.com"
}
],
"description": "Given a deep data structure, access data by dot notation.",
"homepage": "https://github.com/dflydev/dflydev-dot-access-data",
"keywords": [
"access",
"data",
"dot",
"notation"
],
"support": {
"issues": "https://github.com/dflydev/dflydev-dot-access-data/issues",
"source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.1"
},
"time": "2021-08-13T13:06:58+00:00"
},
{ {
"name": "doctrine/cache", "name": "doctrine/cache",
"version": "2.1.1", "version": "2.1.1",
@ -1155,6 +1230,192 @@
], ],
"time": "2022-03-29T20:12:16+00:00" "time": "2022-03-29T20:12:16+00:00"
}, },
{
"name": "league/commonmark",
"version": "2.2.3",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/commonmark.git",
"reference": "47b015bc4e50fd4438c1ffef6139a1fb65d2ab71"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/47b015bc4e50fd4438c1ffef6139a1fb65d2ab71",
"reference": "47b015bc4e50fd4438c1ffef6139a1fb65d2ab71",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"league/config": "^1.1.1",
"php": "^7.4 || ^8.0",
"psr/event-dispatcher": "^1.0",
"symfony/deprecation-contracts": "^2.1 || ^3.0",
"symfony/polyfill-php80": "^1.15"
},
"require-dev": {
"cebe/markdown": "^1.0",
"commonmark/cmark": "0.30.0",
"commonmark/commonmark.js": "0.30.0",
"composer/package-versions-deprecated": "^1.8",
"erusev/parsedown": "^1.0",
"ext-json": "*",
"github/gfm": "0.29.0",
"michelf/php-markdown": "^1.4",
"phpstan/phpstan": "^0.12.88 || ^1.0.0",
"phpunit/phpunit": "^9.5.5",
"scrutinizer/ocular": "^1.8.1",
"symfony/finder": "^5.3",
"symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0",
"unleashedtech/php-coding-standard": "^3.1",
"vimeo/psalm": "^4.7.3"
},
"suggest": {
"symfony/yaml": "v2.3+ required if using the Front Matter extension"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "2.3-dev"
}
},
"autoload": {
"psr-4": {
"League\\CommonMark\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Colin O'Dell",
"email": "colinodell@gmail.com",
"homepage": "https://www.colinodell.com",
"role": "Lead Developer"
}
],
"description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)",
"homepage": "https://commonmark.thephpleague.com",
"keywords": [
"commonmark",
"flavored",
"gfm",
"github",
"github-flavored",
"markdown",
"md",
"parser"
],
"support": {
"docs": "https://commonmark.thephpleague.com/",
"forum": "https://github.com/thephpleague/commonmark/discussions",
"issues": "https://github.com/thephpleague/commonmark/issues",
"rss": "https://github.com/thephpleague/commonmark/releases.atom",
"source": "https://github.com/thephpleague/commonmark"
},
"funding": [
{
"url": "https://www.colinodell.com/sponsor",
"type": "custom"
},
{
"url": "https://www.paypal.me/colinpodell/10.00",
"type": "custom"
},
{
"url": "https://github.com/colinodell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/league/commonmark",
"type": "tidelift"
}
],
"time": "2022-02-26T21:24:45+00:00"
},
{
"name": "league/config",
"version": "v1.1.1",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/config.git",
"reference": "a9d39eeeb6cc49d10a6e6c36f22c4c1f4a767f3e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/config/zipball/a9d39eeeb6cc49d10a6e6c36f22c4c1f4a767f3e",
"reference": "a9d39eeeb6cc49d10a6e6c36f22c4c1f4a767f3e",
"shasum": ""
},
"require": {
"dflydev/dot-access-data": "^3.0.1",
"nette/schema": "^1.2",
"php": "^7.4 || ^8.0"
},
"require-dev": {
"phpstan/phpstan": "^0.12.90",
"phpunit/phpunit": "^9.5.5",
"scrutinizer/ocular": "^1.8.1",
"unleashedtech/php-coding-standard": "^3.1",
"vimeo/psalm": "^4.7.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.2-dev"
}
},
"autoload": {
"psr-4": {
"League\\Config\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Colin O'Dell",
"email": "colinodell@gmail.com",
"homepage": "https://www.colinodell.com",
"role": "Lead Developer"
}
],
"description": "Define configuration arrays with strict schemas and access values with dot notation",
"homepage": "https://config.thephpleague.com",
"keywords": [
"array",
"config",
"configuration",
"dot",
"dot-access",
"nested",
"schema"
],
"support": {
"docs": "https://config.thephpleague.com/",
"issues": "https://github.com/thephpleague/config/issues",
"rss": "https://github.com/thephpleague/config/releases.atom",
"source": "https://github.com/thephpleague/config"
},
"funding": [
{
"url": "https://www.colinodell.com/sponsor",
"type": "custom"
},
{
"url": "https://www.paypal.me/colinpodell/10.00",
"type": "custom"
},
{
"url": "https://github.com/colinodell",
"type": "github"
}
],
"time": "2021-08-14T12:15:32+00:00"
},
{ {
"name": "middlewares/trailing-slash", "name": "middlewares/trailing-slash",
"version": "v2.0.1", "version": "v2.0.1",
@ -1378,6 +1639,153 @@
}, },
"time": "2022-01-21T06:08:36+00:00" "time": "2022-01-21T06:08:36+00:00"
}, },
{
"name": "nette/schema",
"version": "v1.2.2",
"source": {
"type": "git",
"url": "https://github.com/nette/schema.git",
"reference": "9a39cef03a5b34c7de64f551538cbba05c2be5df"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nette/schema/zipball/9a39cef03a5b34c7de64f551538cbba05c2be5df",
"reference": "9a39cef03a5b34c7de64f551538cbba05c2be5df",
"shasum": ""
},
"require": {
"nette/utils": "^2.5.7 || ^3.1.5 || ^4.0",
"php": ">=7.1 <8.2"
},
"require-dev": {
"nette/tester": "^2.3 || ^2.4",
"phpstan/phpstan-nette": "^0.12",
"tracy/tracy": "^2.7"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.2-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause",
"GPL-2.0-only",
"GPL-3.0-only"
],
"authors": [
{
"name": "David Grudl",
"homepage": "https://davidgrudl.com"
},
{
"name": "Nette Community",
"homepage": "https://nette.org/contributors"
}
],
"description": "📐 Nette Schema: validating data structures against a given Schema.",
"homepage": "https://nette.org",
"keywords": [
"config",
"nette"
],
"support": {
"issues": "https://github.com/nette/schema/issues",
"source": "https://github.com/nette/schema/tree/v1.2.2"
},
"time": "2021-10-15T11:40:02+00:00"
},
{
"name": "nette/utils",
"version": "v3.2.7",
"source": {
"type": "git",
"url": "https://github.com/nette/utils.git",
"reference": "0af4e3de4df9f1543534beab255ccf459e7a2c99"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nette/utils/zipball/0af4e3de4df9f1543534beab255ccf459e7a2c99",
"reference": "0af4e3de4df9f1543534beab255ccf459e7a2c99",
"shasum": ""
},
"require": {
"php": ">=7.2 <8.2"
},
"conflict": {
"nette/di": "<3.0.6"
},
"require-dev": {
"nette/tester": "~2.0",
"phpstan/phpstan": "^1.0",
"tracy/tracy": "^2.3"
},
"suggest": {
"ext-gd": "to use Image",
"ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()",
"ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()",
"ext-json": "to use Nette\\Utils\\Json",
"ext-mbstring": "to use Strings::lower() etc...",
"ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()",
"ext-xml": "to use Strings::length() etc. when mbstring is not available"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.2-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause",
"GPL-2.0-only",
"GPL-3.0-only"
],
"authors": [
{
"name": "David Grudl",
"homepage": "https://davidgrudl.com"
},
{
"name": "Nette Community",
"homepage": "https://nette.org/contributors"
}
],
"description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.",
"homepage": "https://nette.org",
"keywords": [
"array",
"core",
"datetime",
"images",
"json",
"nette",
"paginator",
"password",
"slugify",
"string",
"unicode",
"utf-8",
"utility",
"validation"
],
"support": {
"issues": "https://github.com/nette/utils/issues",
"source": "https://github.com/nette/utils/tree/v3.2.7"
},
"time": "2022-01-24T11:29:14+00:00"
},
{ {
"name": "nikic/fast-route", "name": "nikic/fast-route",
"version": "v1.3.0", "version": "v1.3.0",
@ -1763,6 +2171,56 @@
}, },
"time": "2021-11-05T16:50:12+00:00" "time": "2021-11-05T16:50:12+00:00"
}, },
{
"name": "psr/event-dispatcher",
"version": "1.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/event-dispatcher.git",
"reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0",
"reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0",
"shasum": ""
},
"require": {
"php": ">=7.2.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\EventDispatcher\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Standard interfaces for event handling.",
"keywords": [
"events",
"psr",
"psr-14"
],
"support": {
"issues": "https://github.com/php-fig/event-dispatcher/issues",
"source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0"
},
"time": "2019-01-08T18:20:26+00:00"
},
{ {
"name": "psr/http-factory", "name": "psr/http-factory",
"version": "1.0.1", "version": "1.0.1",

View file

@ -14,13 +14,14 @@ use Lubian\NoFramework\Http\Pipeline;
use Lubian\NoFramework\Http\RoutedRequestHandler; use Lubian\NoFramework\Http\RoutedRequestHandler;
use Lubian\NoFramework\Http\RouteMiddleware; use Lubian\NoFramework\Http\RouteMiddleware;
use Lubian\NoFramework\Repository\CachedMarkdownPageRepo; use Lubian\NoFramework\Repository\CachedMarkdownPageRepo;
use Lubian\NoFramework\Repository\DoctrineMarkdownPageRepo;
use Lubian\NoFramework\Repository\MarkdownPageFilesystem; use Lubian\NoFramework\Repository\MarkdownPageFilesystem;
use Lubian\NoFramework\Repository\MarkdownPageRepo; use Lubian\NoFramework\Repository\MarkdownPageRepo;
use Lubian\NoFramework\Service\Time\Now; use Lubian\NoFramework\Service\Time\Now;
use Lubian\NoFramework\Service\Time\SystemClockNow; use Lubian\NoFramework\Service\Time\SystemClockNow;
use Lubian\NoFramework\Settings; use Lubian\NoFramework\Settings;
use Lubian\NoFramework\Template\MarkdownParser;
use Lubian\NoFramework\Template\MustacheRenderer; use Lubian\NoFramework\Template\MustacheRenderer;
use Lubian\NoFramework\Template\ParsedownParser;
use Lubian\NoFramework\Template\Renderer; use Lubian\NoFramework\Template\Renderer;
use Mustache_Engine as ME; use Mustache_Engine as ME;
use Mustache_Loader_FilesystemLoader as MLF; use Mustache_Loader_FilesystemLoader as MLF;
@ -43,6 +44,7 @@ return [
RequestFactory::class => fn (DiactorosRequestFactory $rf) => $rf, RequestFactory::class => fn (DiactorosRequestFactory $rf) => $rf,
CacheInterface::class => fn (FilesystemAdapter $a) => $a, CacheInterface::class => fn (FilesystemAdapter $a) => $a,
MarkdownPageRepo::class => fn (CachedMarkdownPageRepo $r) => $r, MarkdownPageRepo::class => fn (CachedMarkdownPageRepo $r) => $r,
MarkdownParser::class => fn (ParsedownParser $p) => $p,
// Factories // Factories
ResponseInterface::class => fn (ResponseFactory $rf) => $rf->createResponse(), ResponseInterface::class => fn (ResponseFactory $rf) => $rf->createResponse(),

View file

@ -1,13 +1,11 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
use Lubian\NoFramework\Http\RouteMiddleware; use Lubian\NoFramework\Http\RouteMiddleware;
use Lubian\NoFramework\Middleware\CacheMiddleware;
use Middlewares\TrailingSlash; use Middlewares\TrailingSlash;
use Middlewares\Whoops; use Middlewares\Whoops;
return [ return [
Whoops::class, Whoops::class,
TrailingSlash::class, TrailingSlash::class,
CacheMiddleware::class,
RouteMiddleware::class, RouteMiddleware::class,
]; ];

View file

@ -2,18 +2,15 @@
namespace Lubian\NoFramework\Action; namespace Lubian\NoFramework\Action;
use Lubian\NoFramework\Template\MarkdownParser;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
final class Other final class Other
{ {
public function someFunctionName(ResponseInterface $response): ResponseInterface public function someFunctionName(ResponseInterface $response, MarkdownParser $parser): ResponseInterface
{ {
$body = $response->getBody(); $html = $parser->parse('This *works* **too!**');
$response->getBody()->write($html);
$body->write('This works too!'); return $response->withStatus(200);
return $response
->withStatus(200)
->withBody($body);
} }
} }

View file

@ -2,50 +2,79 @@
namespace Lubian\NoFramework\Action; namespace Lubian\NoFramework\Action;
use Lubian\NoFramework\Model\MarkdownPage; use Lubian\NoFramework\Exception\InternalServerError;
use Lubian\NoFramework\Repository\MarkdownPageFilesystem; use Lubian\NoFramework\Template\MarkdownParser;
use Lubian\NoFramework\Repository\MarkdownPageRepo;
use Lubian\NoFramework\Template\Renderer; use Lubian\NoFramework\Template\Renderer;
use Parsedown;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use function array_filter;
use function array_map;
use function array_values;
use function file_get_contents;
use function glob;
use function preg_replace; use function preg_replace;
use function str_contains;
use function str_replace; use function str_replace;
use function substr;
class Page class Page
{ {
public function __construct( public function __construct(
private ResponseInterface $response, private ResponseInterface $response,
private MarkdownPageRepo $repo, private MarkdownParser $parser,
private Parsedown $parsedown,
private Renderer $renderer, private Renderer $renderer,
){} private string $pagesPath = __DIR__ . '/../../data/pages/'
) {
}
public function show( public function show(
string $page, string $page,
): ResponseInterface { ): ResponseInterface {
$page = $this->repo->byTitle($page); $page = array_values(
$content = $this->linkFilter($page->content); array_filter(
$content = $this->parsedown->parse($content); $this->getPages(),
$html = $this->renderer->render('page', ['content' => $content, 'title' => $page->title]); fn (string $filename) => str_contains($filename, $page)
)
)[0];
$markdown = file_get_contents($page);
// fix the next and previous buttons to work with our routing
$markdown = preg_replace('/\(\d\d-/m', '(', $markdown);
$markdown = str_replace('.md)', ')', $markdown);
$page = str_replace([$this->pagesPath, '.md'], ['', ''], $page);
$data = [
'title' => substr($page, 3),
'content' => $this->parser->parse($markdown),
];
$html = $this->renderer->render('page/show', $data);
$this->response->getBody()->write($html); $this->response->getBody()->write($html);
return $this->response; return $this->response;
} }
public function list(): ResponseInterface public function list(): ResponseInterface
{ {
$pages = array_map( $pages = array_map(function (string $page) {
fn (MarkdownPage $p) => ['title' => $p->title, 'id' => $p->id], $page = str_replace([$this->pagesPath, '.md'], ['', ''], $page);
$this->repo->all() return [
); 'id' => substr($page, 0, 2),
$html = $this->renderer->render('pagelist', ['pages' => $pages]); 'title' => substr($page, 3),
];
}, $this->getPages());
$html = $this->renderer->render('page/list', ['pages' => $pages]);
$this->response->getBody()->write($html); $this->response->getBody()->write($html);
return $this->response; return $this->response;
} }
private function linkFilter(string $content): string /**
* @return string[]
*/
private function getPages(): array
{ {
$content = preg_replace('/\(\d\d-/m', '(', $content); $files = glob($this->pagesPath . '*.md');
return str_replace('.md)', ')', $content); if ($files === false) {
throw new InternalServerError('cannot read pages');
}
return $files;
} }
} }

View file

@ -1,7 +1,4 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
namespace Lubian\NoFramework\Factory; namespace Lubian\NoFramework\Factory;
@ -13,7 +10,10 @@ use Lubian\NoFramework\Settings;
final class DoctrineEm final class DoctrineEm
{ {
public function __construct(private Settings $settings){} public function __construct(private Settings $settings)
{
}
public function create(): EntityManagerInterface public function create(): EntityManagerInterface
{ {
$config = Setup::createConfiguration($this->settings->doctrine['devMode']); $config = Setup::createConfiguration($this->settings->doctrine['devMode']);

View file

@ -19,8 +19,7 @@ final class CacheMiddleware implements MiddlewareInterface
private CacheInterface $cache, private CacheInterface $cache,
private Response\Serializer $serializer, private Response\Serializer $serializer,
private Settings $settings, private Settings $settings,
) ) {
{
} }
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface

View file

@ -3,16 +3,14 @@
namespace Lubian\NoFramework\Model; namespace Lubian\NoFramework\Model;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
#[Entity] #[Entity]
class MarkdownPage class MarkdownPage
{ {
public function __construct( public function __construct(
#[Id, Column, GeneratedValue] #[Id,
Column,
GeneratedValue]
public int |null $id = null, public int |null $id = null,
#[Column] #[Column]
public string $title, public string $title,

View file

@ -1,7 +1,4 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
namespace Lubian\NoFramework\Repository; namespace Lubian\NoFramework\Repository;
@ -10,12 +7,14 @@ use Doctrine\ORM\EntityRepository;
use Lubian\NoFramework\Exception\NotFound; use Lubian\NoFramework\Exception\NotFound;
use Lubian\NoFramework\Model\MarkdownPage; use Lubian\NoFramework\Model\MarkdownPage;
use function random_int;
use function usleep;
final class DoctrineMarkdownPageRepo implements MarkdownPageRepo final class DoctrineMarkdownPageRepo implements MarkdownPageRepo
{ {
/** /** @var EntityRepository<MarkdownPage> */
* @var EntityRepository<MarkdownPage>
*/
private EntityRepository $repo; private EntityRepository $repo;
public function __construct( public function __construct(
private EntityManagerInterface $entityManager private EntityManagerInterface $entityManager
) { ) {
@ -27,13 +26,13 @@ final class DoctrineMarkdownPageRepo implements MarkdownPageRepo
*/ */
public function all(): array public function all(): array
{ {
usleep(rand(500, 1500) * 1000); usleep(random_int(500, 1500) * 1000);
return $this->repo->findAll(); return $this->repo->findAll();
} }
public function byId(int $id): MarkdownPage public function byId(int $id): MarkdownPage
{ {
usleep(rand(500, 1500) * 1000); usleep(random_int(500, 1500) * 1000);
$page = $this->repo->findOneBy(['id' => $id]); $page = $this->repo->findOneBy(['id' => $id]);
if (! $page instanceof MarkdownPage) { if (! $page instanceof MarkdownPage) {
throw new NotFound; throw new NotFound;
@ -43,7 +42,7 @@ final class DoctrineMarkdownPageRepo implements MarkdownPageRepo
public function byTitle(string $title): MarkdownPage public function byTitle(string $title): MarkdownPage
{ {
usleep(rand(500, 1500) * 1000); usleep(random_int(500, 1500) * 1000);
$page = $this->repo->findOneBy(['title' => $title]); $page = $this->repo->findOneBy(['title' => $title]);
if (! $page instanceof MarkdownPage) { if (! $page instanceof MarkdownPage) {
throw new NotFound; throw new NotFound;

View file

@ -13,6 +13,7 @@ use function count;
use function file_get_contents; use function file_get_contents;
use function glob; use function glob;
use function is_array; use function is_array;
use function random_int;
use function str_replace; use function str_replace;
use function substr; use function substr;
use function usleep; use function usleep;
@ -31,7 +32,7 @@ final class MarkdownPageFilesystem implements MarkdownPageRepo
$fileNames = glob($this->dataPath . '*.md'); $fileNames = glob($this->dataPath . '*.md');
assert(is_array($fileNames)); assert(is_array($fileNames));
return array_map(function (string $name): MarkdownPage { return array_map(function (string $name): MarkdownPage {
usleep(rand(200, 500) * 1000); usleep(random_int(200, 500) * 1000);
$content = file_get_contents($name); $content = file_get_contents($name);
$name = str_replace($this->dataPath, '', $name); $name = str_replace($this->dataPath, '', $name);
$name = str_replace('.md', '', $name); $name = str_replace('.md', '', $name);

View file

@ -0,0 +1,28 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Template;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\GithubFlavoredMarkdownExtension;
use League\CommonMark\MarkdownConverter;
final class GithubMarkdownRenderer implements MarkdownRenderer
{
private MarkdownConverter $engine;
public function __construct(
CommonMarkCoreExtension $commonMarkCoreExtension,
GithubFlavoredMarkdownExtension $githubFlavoredMarkdownExtension,
) {
$environment = new Environment([]);
$environment->addExtension($commonMarkCoreExtension);
$environment->addExtension($githubFlavoredMarkdownExtension);
$this->engine = new MarkdownConverter($environment);
}
public function render(string $markdown): string
{
return (string) $this->engine->convert($markdown);
}
}

View file

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Template;
interface MarkdownParser
{
public function parse(string $markdown): string;
}

View file

@ -0,0 +1,17 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Template;
use Parsedown;
final class ParsedownParser implements MarkdownParser
{
public function __construct(private Parsedown $parser)
{
}
public function parse(string $markdown): string
{
return $this->parser->parse($markdown);
}
}

View file

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Pages</title>
<link rel="stylesheet" href="https://unpkg.com/@picocss/pico@latest/css/pico.classless.min.css">
</head>
<body>
<main>
<ul>
{{#pages}}
<li>
<a href="/page/{{title}}">{{id}}: {{title}}</a>
</li>
{{/pages}}
</ul>
</main>
</body>
</html>

View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{title}}</title>
<link rel="stylesheet" href="https://unpkg.com/@picocss/pico@latest/css/pico.classless.min.css">
</head>
<body>
<main>
{{{content}}}
</main>
</body>
</html>

View file

@ -4,191 +4,9 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "83df32885d5f14b3e3e8dd6d88fafd23", "content-hash": "bad16ec07065a74b5b41da54ff6ca57e",
"packages": [ "packages": [],
{ "packages-dev": [],
"name": "filp/whoops",
"version": "2.14.5",
"source": {
"type": "git",
"url": "https://github.com/filp/whoops.git",
"reference": "a63e5e8f26ebbebf8ed3c5c691637325512eb0dc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filp/whoops/zipball/a63e5e8f26ebbebf8ed3c5c691637325512eb0dc",
"reference": "a63e5e8f26ebbebf8ed3c5c691637325512eb0dc",
"shasum": ""
},
"require": {
"php": "^5.5.9 || ^7.0 || ^8.0",
"psr/log": "^1.0.1 || ^2.0 || ^3.0"
},
"require-dev": {
"mockery/mockery": "^0.9 || ^1.0",
"phpunit/phpunit": "^4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.3",
"symfony/var-dumper": "^2.6 || ^3.0 || ^4.0 || ^5.0"
},
"suggest": {
"symfony/var-dumper": "Pretty print complex values better with var-dumper available",
"whoops/soap": "Formats errors as SOAP responses"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.7-dev"
}
},
"autoload": {
"psr-4": {
"Whoops\\": "src/Whoops/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Filipe Dobreira",
"homepage": "https://github.com/filp",
"role": "Developer"
}
],
"description": "php error handling for cool kids",
"homepage": "https://filp.github.io/whoops/",
"keywords": [
"error",
"exception",
"handling",
"library",
"throwable",
"whoops"
],
"support": {
"issues": "https://github.com/filp/whoops/issues",
"source": "https://github.com/filp/whoops/tree/2.14.5"
},
"funding": [
{
"url": "https://github.com/denis-sokolov",
"type": "github"
}
],
"time": "2022-01-07T12:00:00+00:00"
},
{
"name": "psr/log",
"version": "3.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
"reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001",
"reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001",
"shasum": ""
},
"require": {
"php": ">=8.0.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Log\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for logging libraries",
"homepage": "https://github.com/php-fig/log",
"keywords": [
"log",
"psr",
"psr-3"
],
"support": {
"source": "https://github.com/php-fig/log/tree/3.0.0"
},
"time": "2021-07-14T16:46:02+00:00"
}
],
"packages-dev": [
{
"name": "phpstan/phpstan",
"version": "1.4.10",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "898c479c39caa727bedf4311dd294a8f4e250e72"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/898c479c39caa727bedf4311dd294a8f4e250e72",
"reference": "898c479c39caa727bedf4311dd294a8f4e250e72",
"shasum": ""
},
"require": {
"php": "^7.1|^8.0"
},
"conflict": {
"phpstan/phpstan-shim": "*"
},
"bin": [
"phpstan",
"phpstan.phar"
],
"type": "library",
"autoload": {
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "PHPStan - PHP Static Analysis Tool",
"support": {
"issues": "https://github.com/phpstan/phpstan/issues",
"source": "https://github.com/phpstan/phpstan/tree/1.4.10"
},
"funding": [
{
"url": "https://github.com/ondrejmirtes",
"type": "github"
},
{
"url": "https://github.com/phpstan",
"type": "github"
},
{
"url": "https://www.patreon.com/phpstan",
"type": "patreon"
},
{
"url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan",
"type": "tidelift"
}
],
"time": "2022-03-14T10:25:45+00:00"
}
],
"aliases": [], "aliases": [],
"minimum-stability": "stable", "minimum-stability": "stable",
"stability-flags": [], "stability-flags": [],