265 lines
8.2 KiB
Markdown
265 lines
8.2 KiB
Markdown
[<< 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)
|