add perfomance chapters
This commit is contained in:
parent
7a0f368e00
commit
3dd9f5a1d9
101 changed files with 8014 additions and 62 deletions
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)
|
Loading…
Add table
Add a link
Reference in a new issue