From eb20213b940774a8eed8b3a43f6d4ac46e251da1 Mon Sep 17 00:00:00 2001 From: lubiana Date: Tue, 5 Apr 2022 19:09:40 +0200 Subject: [PATCH] add 'adding content' chapter --- 14-middleware.md | 4 +- 15-adding-content.md | 248 ++++++++++ app/composer.json | 3 +- app/composer.lock | 460 +++++++++++++++++- app/config/dependencies.php | 4 +- app/config/middlewares.php | 2 - app/src/Action/Other.php | 13 +- app/src/Action/Page.php | 69 ++- app/src/Factory/DoctrineEm.php | 12 +- app/src/Factory/SettingsContainerProvider.php | 2 +- app/src/Middleware/CacheMiddleware.php | 5 +- app/src/Model/MarkdownPage.php | 10 +- .../Repository/DoctrineMarkdownPageRepo.php | 27 +- app/src/Repository/MarkdownPageFilesystem.php | 3 +- app/src/Template/GithubMarkdownRenderer.php | 28 ++ app/src/Template/MarkdownParser.php | 8 + app/src/Template/ParsedownParser.php | 17 + app/templates/page/list.html | 19 + app/templates/page/show.html | 13 + implementation/02-composer/composer.lock | 188 +------ 20 files changed, 884 insertions(+), 251 deletions(-) create mode 100644 15-adding-content.md create mode 100644 app/src/Template/GithubMarkdownRenderer.php create mode 100644 app/src/Template/MarkdownParser.php create mode 100644 app/src/Template/ParsedownParser.php create mode 100644 app/templates/page/list.html create mode 100644 app/templates/page/show.html diff --git a/14-middleware.md b/14-middleware.md index e698327..087e26b 100644 --- a/14-middleware.md +++ b/14-middleware.md @@ -1,4 +1,4 @@ -[<< previous](12-refactoring.md) | [next >>](14-invoker.md) +[<< previous](12-refactoring.md) | [next >>](15-adding-content.md) ### 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 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) diff --git a/15-adding-content.md b/15-adding-content.md new file mode 100644 index 0000000..d1a7348 --- /dev/null +++ b/15-adding-content.md @@ -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 +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 + + + + + Pages + + + +
+ +
+ + +``` + +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 + + + + + {{title}} + + + +
+ {{{content}}} +
+ + +``` + +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 +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) diff --git a/app/composer.json b/app/composer.json index 04edde0..4809539 100644 --- a/app/composer.json +++ b/app/composer.json @@ -13,7 +13,8 @@ "middlewares/whoops": "^2.0", "erusev/parsedown": "^1.7", "symfony/cache": "^6.0", - "doctrine/orm": "^2.11" + "doctrine/orm": "^2.11", + "league/commonmark": "^2.2" }, "autoload": { "psr-4": { diff --git a/app/composer.lock b/app/composer.lock index 904cfb4..648f2d5 100644 --- a/app/composer.lock +++ b/app/composer.lock @@ -4,8 +4,83 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a3762dd11bab0c9e948d3a73b7f252b9", + "content-hash": "1e8469bfebe6479a139b946b8aba49de", "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", "version": "2.1.1", @@ -1155,6 +1230,192 @@ ], "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", "version": "v2.0.1", @@ -1378,6 +1639,153 @@ }, "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", "version": "v1.3.0", @@ -1763,6 +2171,56 @@ }, "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", "version": "1.0.1", diff --git a/app/config/dependencies.php b/app/config/dependencies.php index 4a97f31..e2a3925 100644 --- a/app/config/dependencies.php +++ b/app/config/dependencies.php @@ -14,13 +14,14 @@ use Lubian\NoFramework\Http\Pipeline; use Lubian\NoFramework\Http\RoutedRequestHandler; use Lubian\NoFramework\Http\RouteMiddleware; use Lubian\NoFramework\Repository\CachedMarkdownPageRepo; -use Lubian\NoFramework\Repository\DoctrineMarkdownPageRepo; use Lubian\NoFramework\Repository\MarkdownPageFilesystem; use Lubian\NoFramework\Repository\MarkdownPageRepo; use Lubian\NoFramework\Service\Time\Now; use Lubian\NoFramework\Service\Time\SystemClockNow; use Lubian\NoFramework\Settings; +use Lubian\NoFramework\Template\MarkdownParser; use Lubian\NoFramework\Template\MustacheRenderer; +use Lubian\NoFramework\Template\ParsedownParser; use Lubian\NoFramework\Template\Renderer; use Mustache_Engine as ME; use Mustache_Loader_FilesystemLoader as MLF; @@ -43,6 +44,7 @@ return [ RequestFactory::class => fn (DiactorosRequestFactory $rf) => $rf, CacheInterface::class => fn (FilesystemAdapter $a) => $a, MarkdownPageRepo::class => fn (CachedMarkdownPageRepo $r) => $r, + MarkdownParser::class => fn (ParsedownParser $p) => $p, // Factories ResponseInterface::class => fn (ResponseFactory $rf) => $rf->createResponse(), diff --git a/app/config/middlewares.php b/app/config/middlewares.php index 459547e..71dd461 100644 --- a/app/config/middlewares.php +++ b/app/config/middlewares.php @@ -1,13 +1,11 @@ getBody(); - - $body->write('This works too!'); - - return $response - ->withStatus(200) - ->withBody($body); + $html = $parser->parse('This *works* **too!**'); + $response->getBody()->write($html); + return $response->withStatus(200); } } diff --git a/app/src/Action/Page.php b/app/src/Action/Page.php index 9fb86c2..6a3aad0 100644 --- a/app/src/Action/Page.php +++ b/app/src/Action/Page.php @@ -2,50 +2,79 @@ namespace Lubian\NoFramework\Action; -use Lubian\NoFramework\Model\MarkdownPage; -use Lubian\NoFramework\Repository\MarkdownPageFilesystem; -use Lubian\NoFramework\Repository\MarkdownPageRepo; +use Lubian\NoFramework\Exception\InternalServerError; +use Lubian\NoFramework\Template\MarkdownParser; use Lubian\NoFramework\Template\Renderer; -use Parsedown; 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 MarkdownPageRepo $repo, - private Parsedown $parsedown, + private MarkdownParser $parser, private Renderer $renderer, - ){} + private string $pagesPath = __DIR__ . '/../../data/pages/' + ) { + } + public function show( string $page, ): ResponseInterface { - $page = $this->repo->byTitle($page); - $content = $this->linkFilter($page->content); - $content = $this->parsedown->parse($content); - $html = $this->renderer->render('page', ['content' => $content, 'title' => $page->title]); + $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( - fn (MarkdownPage $p) => ['title' => $p->title, 'id' => $p->id], - $this->repo->all() - ); - $html = $this->renderer->render('pagelist', ['pages' => $pages]); + $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; - } - private function linkFilter(string $content): string + /** + * @return string[] + */ + private function getPages(): array { - $content = preg_replace('/\(\d\d-/m', '(', $content); - return str_replace('.md)', ')', $content); + $files = glob($this->pagesPath . '*.md'); + if ($files === false) { + throw new InternalServerError('cannot read pages'); + } + return $files; } } diff --git a/app/src/Factory/DoctrineEm.php b/app/src/Factory/DoctrineEm.php index be24e7f..b0be39b 100644 --- a/app/src/Factory/DoctrineEm.php +++ b/app/src/Factory/DoctrineEm.php @@ -1,7 +1,4 @@ -settings->doctrine['devMode']); @@ -29,4 +29,4 @@ final class DoctrineEm $config, ); } -} \ No newline at end of file +} diff --git a/app/src/Factory/SettingsContainerProvider.php b/app/src/Factory/SettingsContainerProvider.php index baf278b..5b89266 100644 --- a/app/src/Factory/SettingsContainerProvider.php +++ b/app/src/Factory/SettingsContainerProvider.php @@ -20,7 +20,7 @@ final class SettingsContainerProvider implements ContainerProvider $dependencies = require $settings->dependenciesFile; $dependencies[Settings::class] = $settings; $builder->addDefinitions($dependencies); - // $builder->enableCompilation('/tmp'); + // $builder->enableCompilation('/tmp'); return $builder->build(); } } diff --git a/app/src/Middleware/CacheMiddleware.php b/app/src/Middleware/CacheMiddleware.php index 2ff6f6e..1bd58c5 100644 --- a/app/src/Middleware/CacheMiddleware.php +++ b/app/src/Middleware/CacheMiddleware.php @@ -19,13 +19,12 @@ final class CacheMiddleware implements MiddlewareInterface private CacheInterface $cache, private Response\Serializer $serializer, private Settings $settings, - ) - { + ) { } public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - if ($request->getMethod() === 'GET' && !$this->settings->isDev()) { + if ($request->getMethod() === 'GET' && ! $this->settings->isDev()) { $key = (string) $request->getUri(); $key = base64_encode($key); $callback = fn () => $handler->handle($request); diff --git a/app/src/Model/MarkdownPage.php b/app/src/Model/MarkdownPage.php index 73f001d..bae383c 100644 --- a/app/src/Model/MarkdownPage.php +++ b/app/src/Model/MarkdownPage.php @@ -3,17 +3,15 @@ namespace Lubian\NoFramework\Model; 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] class MarkdownPage { public function __construct( - #[Id, Column, GeneratedValue] - public int|null $id = null, + #[Id, + Column, + GeneratedValue] + public int |null $id = null, #[Column] public string $title, #[Column(type: Types::TEXT)] diff --git a/app/src/Repository/DoctrineMarkdownPageRepo.php b/app/src/Repository/DoctrineMarkdownPageRepo.php index f107c9e..8d1d457 100644 --- a/app/src/Repository/DoctrineMarkdownPageRepo.php +++ b/app/src/Repository/DoctrineMarkdownPageRepo.php @@ -1,7 +1,4 @@ - - */ + /** @var EntityRepository */ private EntityRepository $repo; + public function __construct( private EntityManagerInterface $entityManager - ){ + ) { $this->repo = $this->entityManager->getRepository(MarkdownPage::class); } @@ -27,15 +26,15 @@ final class DoctrineMarkdownPageRepo implements MarkdownPageRepo */ public function all(): array { - usleep(rand(500, 1500) * 1000); + usleep(random_int(500, 1500) * 1000); return $this->repo->findAll(); } public function byId(int $id): MarkdownPage { - usleep(rand(500, 1500) * 1000); + usleep(random_int(500, 1500) * 1000); $page = $this->repo->findOneBy(['id' => $id]); - if (!$page instanceof MarkdownPage){ + if (! $page instanceof MarkdownPage) { throw new NotFound; } return $page; @@ -43,9 +42,9 @@ final class DoctrineMarkdownPageRepo implements MarkdownPageRepo public function byTitle(string $title): MarkdownPage { - usleep(rand(500, 1500) * 1000); + usleep(random_int(500, 1500) * 1000); $page = $this->repo->findOneBy(['title' => $title]); - if (!$page instanceof MarkdownPage){ + if (! $page instanceof MarkdownPage) { throw new NotFound; } return $page; @@ -57,4 +56,4 @@ final class DoctrineMarkdownPageRepo implements MarkdownPageRepo $this->entityManager->flush(); return $page; } -} \ No newline at end of file +} diff --git a/app/src/Repository/MarkdownPageFilesystem.php b/app/src/Repository/MarkdownPageFilesystem.php index 1d70998..25dfd97 100644 --- a/app/src/Repository/MarkdownPageFilesystem.php +++ b/app/src/Repository/MarkdownPageFilesystem.php @@ -13,6 +13,7 @@ use function count; use function file_get_contents; use function glob; use function is_array; +use function random_int; use function str_replace; use function substr; use function usleep; @@ -31,7 +32,7 @@ final class MarkdownPageFilesystem implements MarkdownPageRepo $fileNames = glob($this->dataPath . '*.md'); assert(is_array($fileNames)); return array_map(function (string $name): MarkdownPage { - usleep(rand(200, 500) * 1000); + usleep(random_int(200, 500) * 1000); $content = file_get_contents($name); $name = str_replace($this->dataPath, '', $name); $name = str_replace('.md', '', $name); diff --git a/app/src/Template/GithubMarkdownRenderer.php b/app/src/Template/GithubMarkdownRenderer.php new file mode 100644 index 0000000..121504b --- /dev/null +++ b/app/src/Template/GithubMarkdownRenderer.php @@ -0,0 +1,28 @@ +addExtension($commonMarkCoreExtension); + $environment->addExtension($githubFlavoredMarkdownExtension); + $this->engine = new MarkdownConverter($environment); + } + + public function render(string $markdown): string + { + return (string) $this->engine->convert($markdown); + } +} diff --git a/app/src/Template/MarkdownParser.php b/app/src/Template/MarkdownParser.php new file mode 100644 index 0000000..d404005 --- /dev/null +++ b/app/src/Template/MarkdownParser.php @@ -0,0 +1,8 @@ +parser->parse($markdown); + } +} diff --git a/app/templates/page/list.html b/app/templates/page/list.html new file mode 100644 index 0000000..bf42348 --- /dev/null +++ b/app/templates/page/list.html @@ -0,0 +1,19 @@ + + + + + Pages + + + +
+ +
+ + \ No newline at end of file diff --git a/app/templates/page/show.html b/app/templates/page/show.html new file mode 100644 index 0000000..ebe707a --- /dev/null +++ b/app/templates/page/show.html @@ -0,0 +1,13 @@ + + + + + {{title}} + + + +
+ {{{content}}} +
+ + \ No newline at end of file diff --git a/implementation/02-composer/composer.lock b/implementation/02-composer/composer.lock index 31ecc9c..6d84747 100644 --- a/implementation/02-composer/composer.lock +++ b/implementation/02-composer/composer.lock @@ -4,191 +4,9 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "83df32885d5f14b3e3e8dd6d88fafd23", - "packages": [ - { - "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" - } - ], + "content-hash": "bad16ec07065a74b5b41da54ff6ca57e", + "packages": [], + "packages-dev": [], "aliases": [], "minimum-stability": "stable", "stability-flags": [],