add perfomance chapters

This commit is contained in:
lubiana 2022-04-06 23:43:03 +02:00 committed by Andre Lubian
parent 9a1f78947b
commit ececd7dcb5
101 changed files with 8014 additions and 62 deletions

View file

@ -174,7 +174,7 @@ return $config
The PHPCodesniffer is sort of a combination of the previous tools, it checks for a defined codingstyle and some extra
rules that are not just stylechanges but instead enforces extra rules in if-statements, exception handling etc.
it provides the `phpcs` command to check for violations and the `phpcbf` command to actually fix most of the violations.
it provides the phpcs command to check for violations and the phpcbf command to actually fix most of the violations.
Without configuration the tool tries to apply the PSR12 standard just like the php-cs-fixer, but as you might have
guessed we are adding some extra rules.
@ -216,7 +216,7 @@ PHPCBF CAN FIX THE 4 MARKED SNIFF VIOLATIONS AUTOMATICALLY
Time: 639ms; Memory: 10MB
```
You can then use `./vendor/bin/phpcbf` to try to fix them.
You can then use `./vendor/bin/phpcbf` to try to fix them
#### Symfony Var-Dumper

View file

@ -172,7 +172,6 @@ return [
```
Now we can change our Bootstrap.php file to use the new Factories for the creation of the Initial Objects:
require __DIR__ . '/../vendor/autoload.php';
```php
...

View file

@ -97,10 +97,6 @@ use Psr\Http\Server\RequestHandlerInterface;
interface RoutedRequestHandler extends RequestHandlerInterface
{
/**
* sets the Name of the ServerRequest attribute where the route
* information should be stored.
*/
public function setRouteAttributeName(string $routeAttributeName = '__route_handler'): void;
}
```

View file

@ -1,4 +1,4 @@
[<< previous](12-refactoring.md) | [next >>](14-invoker.md)
[<< previous](12-refactoring.md) | [next >>](15-adding-content.md)
### Middleware
@ -153,8 +153,6 @@ class ContainerPipeline implements Pipeline
{
/**
* @param array<MiddlewareInterface|class-string> $middlewares
* @param RequestHandlerInterface $tip
* @param ContainerInterface $container
*/
public function __construct(
private array $middlewares,
@ -295,4 +293,11 @@ 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)
**A quick note about docblocks:** You might have noticed, that I rarely add docblocks to my the code in the examples, and
when I do it seems kind of random. My philosophy is that I only add docblocks when there is no way to automatically get
the exact type from the code itself. For me docblocks only serve two purposes: help my IDE to understand what it choices
it has for code completion and to help the static analysis to better understand the code. There is a great blogpost
about the [cost and value of DocBlocks](https://localheinz.com/blog/2018/05/06/cost-and-value-of-docblocks/), although it
is written in 2018 at a time before PHP 7.4 was around everything written there is still valid today.
[<< previous](12-refactoring.md) | [next >>](15-adding-content.md)

View file

@ -0,0 +1,253 @@
[<< previous](14-middleware.md) | [next >>](16-data-repository.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">
<link rel="stylesheet"
href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.0/styles/default.min.css">
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
</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. There is also some Javascript that adds syntax
highlighting to the code.
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 >>](16-data-repository.md)

View file

@ -0,0 +1,265 @@
[<< previous](15-adding-content.md) | [next >>](17-performance.md)
## Data Repository
At the end of the last chapter I mentioned being unhappy with our Pages action, because there is to much stuff happening
there. We are firstly receiving some Arguments, then we are using those to query the filesytem for the given page,
loading the specific file from the filesystem, rendering the markdown, passing the markdown to the template renderer,
adding the resulting html to the response and then returning the response.
In order to make our pageaction independent from the filesystem and move the code that is responsible for reading the
files
to a better place I want to introduce
the [Repository Pattern](https://designpatternsphp.readthedocs.io/en/latest/More/Repository/README.html).
I want to start by creating a class that represents the Data that is included in a page so that. For now I can spot
three
distrinct attributes.
* the ID (or chapternumber)
* the title (or name)
* the content
Currently all those properties are always available, but we might later be able to create new pages and store them, but
at that point in time we are not yet aware of the new available ID, so we should leave that property nullable. This
allows
us to create an object without an id and let the code that actually saves the object to a persistant store define a
valid
id on saving.
Lets create an new Namespace called `Model` and put a `MarkdownPage.php` class in there:
```php
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Model;
class MarkdownPage
{
public function __construct(
public string $title,
public string $content,
public int|null $id = null,
) {
}
}
```
These small Model classes are one of my most favorite features in newer PHP-Versions, because they are almost as easy
to create as an on-the-fly array but give us the great benefit of type safety as well as full code completion in our
IDEs.
There is a [great blogpost](https://stitcher.io/blog/evolution-of-a-php-object) that highlights how these kind of
objects
have evolved in PHP from version 5.6 to 8.1, as I personally first started writing proper php with 5.4 it really baffles
me how far the language has evolved in these last years.
Next we can define our interface for the repository, for our current usecase I see only two needed methods:
* get all pages
* get one page by name
The `all()` method should return an array of all available pages (or an empty one if there are none), and the
`byName(string $name)` method should either return exactly one page or throw a NotFound-Exception. You decide to return
`false` or `null` if no page with the given name could be found, but I personally prefer exception, as that keeps the
return type checking simpler and we can decide at what layer of the application we want to handle a miss on that
function.
With that said we can now define create a `Repository` namespace and place a `MarkdownPageRepo.php` there:
```php
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Repository;
use Lubian\NoFramework\Exception\NotFound;
use Lubian\NoFramework\Model\MarkdownPage;
interface MarkdownPageRepo
{
/** @return MarkdownPage[] */
public function all(): array;
/** @throws NotFound */
public function byName(string $name): MarkdownPage;
}
```
Now we can write an implementation for this interface and move our code from to Action there:
`src/Repository/FilesystemMarkdownPageRepo.php`
```php
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Repository;
use Lubian\NoFramework\Exception\InternalServerError;
use Lubian\NoFramework\Exception\NotFound;
use Lubian\NoFramework\Model\MarkdownPage;
use function array_filter;
use function array_map;
use function array_values;
use function count;
use function file_get_contents;
use function glob;
use function str_replace;
use function substr;
final class FileSystemMarkdownPageRepo implements MarkdownPageRepo
{
public function __construct(
private readonly string $dataPath
) {
}
/** @inheritDoc */
public function all(): array
{
$files = glob($this->dataPath . '*.md');
if ($files === false) {
throw new InternalServerError('cannot read pages');
}
return array_map(function (string $filename) {
$content = file_get_contents($filename);
if ($content === false) {
throw new InternalServerError('cannot read pages');
}
$idAndTitle = str_replace([$this->dataPath, '.md'], ['', ''], $filename);
return new MarkdownPage(
(int) substr($idAndTitle, 0, 2),
substr($idAndTitle, 3),
$content
);
}, $files);
}
public function byName(string $name): MarkdownPage
{
$pages = array_values(
array_filter(
$this->all(),
fn (MarkdownPage $p) => $p->title === $name,
)
);
if (count($pages) !== 1) {
throw new NotFound;
}
return $pages[0];
}
}
```
With that in place we need to add the required `$pagesPath` to our settings class and add specify that in our
configuration.
`src/Settings.php`
```php
final class Settings
{
public function __construct(
public readonly string $environment,
public readonly string $dependenciesFile,
public readonly string $middlewaresFile,
public readonly string $templateDir,
public readonly string $templateExtension,
public readonly string $pagesPath,
) {
}
}
```
`config/settings.php`
```php
return new Settings(
environment: 'prod',
dependenciesFile: __DIR__ . '/dependencies.php',
middlewaresFile: __DIR__ . '/middlewares.php',
templateDir: __DIR__ . '/../templates',
templateExtension: '.html',
pagesPath: __DIR__ . '/../data/pages/',
);
```
Of course we need to define the correct implementation for the container to choose when we are requesting the Repository
interface:
`conf/dependencies.php`
```php
MarkdownPageRepo::class => fn (FileSystemMarkdownPageRepo $r) => $r,
FileSystemMarkdownPageRepo::class => fn (Settings $s) => new FileSystemMarkdownPageRepo($s->pagesPath),
```
Now you can request the MarkdownPageRepo Interface in your page action and use the defined functions to get the
MarkdownPage
Objects. My `src/Action/Page.php` looks like this now:
```php
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Action;
use Lubian\NoFramework\Model\MarkdownPage;
use Lubian\NoFramework\Repository\MarkdownPageRepo;
use Lubian\NoFramework\Template\MarkdownParser;
use Lubian\NoFramework\Template\Renderer;
use Psr\Http\Message\ResponseInterface;
use function array_map;
use function assert;
use function is_string;
use function preg_replace;
use function str_replace;
class Page
{
public function __construct(
private ResponseInterface $response,
private MarkdownParser $parser,
private Renderer $renderer,
private MarkdownPageRepo $repo,
) {
}
public function show(
string $page,
): ResponseInterface {
$page = $this->repo->byName($page);
// fix the next and previous buttons to work with our routing
$content = preg_replace('/\(\d\d-/m', '(', $page->content);
assert(is_string($content));
$content = str_replace('.md)', ')', $content);
$data = [
'title' => $page->title,
'content' => $this->parser->parse($content),
];
$html = $this->renderer->render('page/show', $data);
$this->response->getBody()->write($html);
return $this->response;
}
public function list(): ResponseInterface
{
$pages = array_map(function (MarkdownPage $page) {
return [
'id' => $page->id,
'title' => $page->content,
];
}, $this->repo->all());
$html = $this->renderer->render('page/list', ['pages' => $pages]);
$this->response->getBody()->write($html);
return $this->response;
}
}
```
Check the page in your browser if everything still works, don't forget to run phpstan and the others fixers before
committing your changes and moving on to the next chapter.
[<< previous](15-adding-content.md) | [next >>](17-performance.md)

View file

@ -0,0 +1,43 @@
[<< previous](16-data-repository.md) | [next >>](18-caching.md)
## Autoloading performance
Although our application is still very small and you should not really experience any performance issues right now,
there are still some things we can already consider and take a look at. If I check the network tab in my browser it takes
about 90-400ms to show a simple rendered markdownpage, with is sort of ok but in my opinion way to long as we are not
really doing anything and do not connect to any external services. Mostly we are just reading around 16 markdown files,
a template, some config files here and there and parse some markdown. So that should not really take that long.
The problem is, that we heavily rely on autoloading for all our class files, in the `src` folder. And there are also
quite a lot of other files in composers `vendor` directory. To understand while this is becomming we should make
ourselves familiar with how [autoloading in php](https://www.php.net/manual/en/language.oop5.autoload.php) works.
The basic idea is, that every class that php encounters has to be loaded from somewhere in the filesystem, we could
just require the files manually but that is tedious, unflexible and can often cause errors.
The problem we are now facing is that the composer autoloader has some rules to determine from where in the filesystem
a class definition might be placed, then the autoloader tries to locate a file by the namespace and classname and if it
exists includes that file.
If we only have a handfull of classes that does not take a lot of time, but as we are growing with our application this
easily takes longer than necesery, but fortunately composer has some options to speed up the class loading.
Take a few minutes to read the documentation about [composer autoloader optimization](https://getcomposer.org/doc/articles/autoloader-optimization.md)
You can try all 3 levels of optimizations, but we are going to stick with the first one for now, so lets create an
optimized classmap.
`composer dump-autoload -o`
After composer has finished you can start the devserver again with `composer serve` and take a look at the network tab
in your browsers devtools.
In my case the response time falls down to under an average of 30ms with some spikes in between, but all in all it looks really good.
You can also try out the different optimization levels and see if you can spot any differences.
Although the composer manual states not to use the optimtization in a dev environment I personally have not encountered
any errors with the first level of optimizations, so we can use that level here. If you add the line from the documentation
to your `composer.json` so that the autoloader gets optimized everytime we install new packages.
[<< previous](16-data-repository.md) | [next >>](18-caching.md)

View file

@ -0,0 +1,252 @@
[<< previous](17-performance.md) | [next >>](19-database.md)
**DISClAIMER** I do not really have a lot of experience when it comes to caching, so this chapter is mostly some random
thoughts and ideas I wanted to explore when writing this tutorial, you should definitely take everything that is being
said here with caution and try to read up on some other sources. But that holds true for the whole tutorial anyway :)
## Caching
In the last chapter we greatly improved the perfomance for the lookup of all our classfiles, but currently we do not
have any real bottlenecks in our application like complex queries.
But in a real application we are going to execute some really heavy and time intensive database queries that can take
quite a while to be completed.
We can simulate that by adding a simple delay in our `FileSystemMarkdownPageRepo`.
```php
return array_map(function (string $filename) {
usleep(rand(100, 400) * 1000);
$content = file_get_contents($filename);
if ($content === false) {
throw new InternalServerError('cannot read pages');
}
$idAndTitle = str_replace([$this->dataPath, '.md'], ['', ''], $filename);
return new MarkdownPage(
(int) substr($idAndTitle, 0, 2),
substr($idAndTitle, 3),
$content
);
});
```
Here I added a function that pauses the scripts execution for a random time between 100 and 400ms for every markdownpage
in every call of the `all()` method.
If you open any page or even the listAction in you browser you will see, that it takes quite a time to render that page.
Although this is a silly example we do not really need to query the database on every request, so lets add a way to cache
the database results between requests.
The PHP-Community has already adressed the issue of having easy to use access to cache libraries, there is the
[PSR-6 Caching Interface](https://www.php-fig.org/psr/psr-6) which gives us easy access to many different implementations,
then there is also a much simpler [PSR-16 Simple Cache](https://www.php-fig.org/psr/psr-16) which makes the use even more
easy, and most Caching Libraries implement Both interfaces anyway. You would think that this is more than enough solutions
to satisfy all the Caching needs around, but the Symfony People decided that Caching should be even simpler and easier
to use and defined their own [Interface](https://symfony.com/doc/current/components/cache.html#cache-component-contracts)
which only needs two methods. You should definitely take a look at the linked documentation as it really blew my mind
when I first encountered it.
The basic idea is that you provide a callback that computes the requested value. The Cache implementation then checks
if it already has the value stored somewhere and if it doesnt it just executes the callback and stores the value for
future calls.
It is really simple and great to use. In a real world application you should definitely use that or a PSR-16 implementation
but for this tutorial I wanted to roll out my own solution, so here we go.
As always we are going to define an interface first, I am going to call it EasyCache and place it in the `Service/Cache`
namespace. I will require only one method which is base on the Symfony Cache Contract, and hast a key, a callback, and
the duration that the item should be cached as arguments.
```php
<?php
declare(strict_types=1);
namespace Lubian\NoFramework\Service\Cache;
interface EasyCache
{
/** @param callable(): mixed $callback */
public function get(string $key, callable $callback, int $ttl = 0): mixed;
}
```
For the implementation I am going to use the [APCu Extension](https://www.php.net/manual/en/ref.apcu.php) for PHP, but
if you are particularly adventurous you can write an implementation using memcache, the filesystem, a database, redis
or whatever you desire.
For the sake of writing as less code as possible here is my simple `ApcuCache.php`
```php
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Service\Cache;
use function apcu_add;
use function apcu_fetch;
final class ApcuCache implements EasyCache
{
public function get(string $key, callable $callback, int $ttl = 0): mixed
{
$success = false;
$result = apcu_fetch($key, $success);
if ($success === true) {
return $result;
}
$result = $callback();
apcu_add($key, $result, $ttl);
return $result;
}
}
```
Now that we have a usable implementation for our cache we can write an implementation of our `MarkdownPageRepo` interface
that usese the Cache and a Repository implementation to speed up the time exepensive calls.
So lets create a new class called `CachedMarkdownPageRepo`:
```php
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Repository;
use Lubian\NoFramework\Model\MarkdownPage;
use Lubian\NoFramework\Service\Cache\EasyCache;
use function base64_encode;
final class CachedMarkdownPageRepo implements MarkdownPageRepo
{
public function __construct(
private EasyCache $cache,
private MarkdownPageRepo $repo,
) {
}
/**
* @inheritDoc
*/
public function all(): array
{
$key = base64_encode(self::class . 'all');
return $this->cache->get(
$key,
fn () => $this->repo->all(),
300
);
}
public function byName(string $name): MarkdownPage
{
$key = base64_encode(self::class . 'byName' . $name);
return $this->cache->get(
$key,
fn () => $this->repo->byName($name),
300
);
}
}
```
This simple wrapper just requires an EasyCache implementation and a MarkdownPageRepo in the constructor and uses them
to cache all queries for 5 minutes. The beauty is that we are not dependent on any implementation here, so we can switch
out the Repository or the Cache at any point down the road if we want to.
In order to use that we need to update our `config/dependencies.php` to add an alias for the EasyCache interface as well
as defining our CachedMarkdownPageRepo as implementation for the MarkdownPageRepo interface:
```php
MarkdownPageRepo::class => fn (CachedMarkdownPageRepo $r) => $r,
EasyCache::class => fn (ApcuCache $c) => $c,
```
If we try to access our webpage now, we are getting an error, as PHP-DI has detected a circular dependency that cannot
be autowired.
The Problem is that our CachedMarkdownPageRepo ist defined as the implementation for the MarkdownPageRepo, but it also
requires that exact interface as a dependency. To resolve this issue we need to manually tell the container how to build
the CachedMarkdownPageRepo by adding another line to the `config/dependencies.php` file:
```php
CachedMarkdownPageRepo::class => fn (EasyCache $c, FileSystemMarkdownPageRepo $r) => new CachedMarkdownPageRepo($c, $r),
```
Here we explicitly require the FileSystemMarkdownPageRepo and us that to create the CachedMarkdownPageRepo object.
When you now navigate to the pages list or to a specific page the first load should take a while (because of our added delay)
but the following request should be answered blazingly fast.
Before moving on to the next chapter we can take the caching approach even further, in the middleware chapter I talked
about a simple CachingMiddleware that caches all the GET-Request for some seconds, as they should not change that often,
and we can bypass most of our application logic if we just complelety cache away the responses our application generates,
and return them quite early in our Middleware-Pipeline befor the router gets called, or the invoker calls the action,
which itself uses some other services to fetch all the needed data.
We will introduce a new `Middleware` namespace to place our `Cache.php` middleware:
```php
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Middleware;
use Laminas\Diactoros\Response\Serializer;
use Lubian\NoFramework\Service\Cache\EasyCache;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use function base64_encode;
final class Cache implements MiddlewareInterface
{
public function __construct(
private EasyCache $cache,
private Serializer $serializer,
) {
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if ($request->getMethod() !== 'GET') {
return $handler->handle($request);
}
$keyHash = base64_encode($request->getUri()->getPath());
$result = $this->cache->get(
$keyHash,
fn () => Serializer::toString($handler->handle($request)),
300
);
return Serializer::fromString($result);
}
}
```
The code is quite straight forward, but you might be confused by the Responseserializer I have added here, we need this
because the response body is a stream object, which doesnt always gets serialized correctly, therefore I use a class from
the laminas project to to all the heavy lifting for us.
We need to add the now middleware to the `config/middlewares.php` file.
```php
<?php declare(strict_types=1);
use Lubian\NoFramework\Http\RouteMiddleware;
use Lubian\NoFramework\Middleware\Cache;
use Middlewares\TrailingSlash;
use Middlewares\Whoops;
return [
Whoops::class,
Cache::class,
TrailingSlash::class,
RouteMiddleware::class,
];
```
You can now use your browser to look if everything works as expected.
**Disclaimer** in a real application you would take some more consideration when it comes to caching and this simple
response cache would quickly get in you way, but as I said earlier this chapter was mostly me playing around with some
ideas I had in writing this tutorial.
[<< previous](17-performance.md) | [next >>](19-database.md)