add perfomance chapters
This commit is contained in:
parent
9a1f78947b
commit
ececd7dcb5
101 changed files with 8014 additions and 62 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
...
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
```
|
||||
|
|
|
@ -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)
|
||||
|
|
253
app/data/pages/15-adding-content.md
Normal file
253
app/data/pages/15-adding-content.md
Normal 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)
|
265
app/data/pages/16-data-repository.md
Normal file
265
app/data/pages/16-data-repository.md
Normal 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)
|
43
app/data/pages/17-performance.md
Normal file
43
app/data/pages/17-performance.md
Normal 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)
|
252
app/data/pages/18-caching.md
Normal file
252
app/data/pages/18-caching.md
Normal 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)
|
Loading…
Add table
Add a link
Reference in a new issue