add chapter about data repositories, and start work on perfomance chapter

This commit is contained in:
lubiana 2022-04-06 01:21:17 +02:00 committed by Andre Lubian
parent eb20213b94
commit 9a1f78947b
165 changed files with 14028 additions and 2028 deletions

View file

@ -12,8 +12,6 @@
"middlewares/trailing-slash": "^2.0",
"middlewares/whoops": "^2.0",
"erusev/parsedown": "^1.7",
"symfony/cache": "^6.0",
"doctrine/orm": "^2.11",
"league/commonmark": "^2.2"
},
"autoload": {

2006
app/composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,8 @@
<?php declare(strict_types=1);
use Doctrine\ORM\EntityManagerInterface;
use FastRoute\Dispatcher;
use Laminas\Diactoros\ResponseFactory;
use Lubian\NoFramework\Factory\DiactorosRequestFactory;
use Lubian\NoFramework\Factory\DoctrineEm;
use Lubian\NoFramework\Factory\PipelineProvider;
use Lubian\NoFramework\Factory\RequestFactory;
use Lubian\NoFramework\Http\BasicEmitter;
@ -13,8 +11,7 @@ use Lubian\NoFramework\Http\InvokerRoutedHandler;
use Lubian\NoFramework\Http\Pipeline;
use Lubian\NoFramework\Http\RoutedRequestHandler;
use Lubian\NoFramework\Http\RouteMiddleware;
use Lubian\NoFramework\Repository\CachedMarkdownPageRepo;
use Lubian\NoFramework\Repository\MarkdownPageFilesystem;
use Lubian\NoFramework\Repository\FileSystemMarkdownPageRepo;
use Lubian\NoFramework\Repository\MarkdownPageRepo;
use Lubian\NoFramework\Service\Time\Now;
use Lubian\NoFramework\Service\Time\SystemClockNow;
@ -43,8 +40,8 @@ return [
RoutedRequestHandler::class => fn (InvokerRoutedHandler $h) => $h,
RequestFactory::class => fn (DiactorosRequestFactory $rf) => $rf,
CacheInterface::class => fn (FilesystemAdapter $a) => $a,
MarkdownPageRepo::class => fn (CachedMarkdownPageRepo $r) => $r,
MarkdownParser::class => fn (ParsedownParser $p) => $p,
MarkdownPageRepo::class => fn (FileSystemMarkdownPageRepo $r) => $r,
// Factories
ResponseInterface::class => fn (ResponseFactory $rf) => $rf->createResponse(),
@ -54,7 +51,5 @@ return [
ME::class => fn (MLF $mfl) => new ME(['loader' => $mfl]),
Dispatcher::class => fn () => simpleDispatcher(require __DIR__ . '/routes.php'),
Pipeline::class => fn (PipelineProvider $p) => $p->getPipeline(),
MarkdownPageFilesystem::class => fn (Settings $s) => new MarkdownPageFilesystem($s->pagesPath),
CachedMarkdownPageRepo::class => fn (CacheInterface $c, MarkdownPageFilesystem $r, Settings $s) => new CachedMarkdownPageRepo($c, $r, $s),
EntityManagerInterface::class => fn (DoctrineEm $f) => $f->create(),
FileSystemMarkdownPageRepo::class => fn (Settings $s) => new FileSystemMarkdownPageRepo($s->pagesPath),
];

View file

@ -9,15 +9,4 @@ return new Settings(
templateDir: __DIR__ . '/../templates',
templateExtension: '.html',
pagesPath: __DIR__ . '/../data/pages/',
connection: [
'driver' => 'pdo_sqlite',
'user' => '',
'password' => '',
'path' => __DIR__ . '/../data/db.sqlite',
],
doctrine: [
'devMode' => true,
'metadataDirs' => [__DIR__ . '/../src/Model/'],
'cacheDir' => __DIR__ . '/../data/cache/',
],
);

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

@ -49,11 +49,7 @@ namespace Lubian\NoFramework\Template;
interface Renderer
{
/**
* @param string $template
* @param array<string, mixed> $data
* @return string
*/
/** @param array<string, mixed> $data */
public function render(string $template, array $data = []) : string;
}
```

View file

@ -2,20 +2,17 @@
namespace Lubian\NoFramework\Action;
use Lubian\NoFramework\Exception\InternalServerError;
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_filter;
use function array_map;
use function array_values;
use function file_get_contents;
use function glob;
use function assert;
use function is_string;
use function preg_replace;
use function str_contains;
use function str_replace;
use function substr;
class Page
{
@ -23,30 +20,25 @@ class Page
private ResponseInterface $response,
private MarkdownParser $parser,
private Renderer $renderer,
private string $pagesPath = __DIR__ . '/../../data/pages/'
private MarkdownPageRepo $repo,
) {
}
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);
$page = $this->repo->byName($page);
// fix the next and previous buttons to work with our routing
$markdown = preg_replace('/\(\d\d-/m', '(', $markdown);
$markdown = str_replace('.md)', ')', $markdown);
$content = preg_replace('/\(\d\d-/m', '(', $page->content);
assert(is_string($content));
$content = str_replace('.md)', ')', $content);
$page = str_replace([$this->pagesPath, '.md'], ['', ''], $page);
$data = [
'title' => substr($page, 3),
'content' => $this->parser->parse($markdown),
'title' => $page->title,
'content' => $this->parser->parse($content),
];
$html = $this->renderer->render('page/show', $data);
$this->response->getBody()->write($html);
return $this->response;
@ -54,27 +46,15 @@ class Page
public function list(): ResponseInterface
{
$pages = array_map(function (string $page) {
$page = str_replace([$this->pagesPath, '.md'], ['', ''], $page);
$pages = array_map(function (MarkdownPage $page) {
return [
'id' => substr($page, 0, 2),
'title' => substr($page, 3),
'id' => $page->id,
'title' => $page->content,
];
}, $this->getPages());
}, $this->repo->all());
$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;
}
}

View file

@ -1,40 +0,0 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Middleware;
use Laminas\Diactoros\Response;
use Lubian\NoFramework\Settings;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
use function base64_encode;
final class CacheMiddleware implements MiddlewareInterface
{
public function __construct(
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()) {
$key = (string) $request->getUri();
$key = base64_encode($key);
$callback = fn () => $handler->handle($request);
$cached = $this->cache->get($key, function (ItemInterface $item) use ($callback) {
$item->expiresAfter(120);
$response = $callback();
return $this->serializer::toString($response);
});
return $this->serializer::fromString($cached);
}
return $handler->handle($request);
}
}

View file

@ -2,19 +2,11 @@
namespace Lubian\NoFramework\Model;
use Doctrine\DBAL\Types\Types;
#[Entity]
class MarkdownPage
{
public function __construct(
#[Id,
Column,
GeneratedValue]
public int |null $id = null,
#[Column]
public string $title,
#[Column(type: Types::TEXT)]
public string $content,
) {
}

View file

@ -1,62 +0,0 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Repository;
use Lubian\NoFramework\Model\MarkdownPage;
use Lubian\NoFramework\Settings;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
class CachedMarkdownPageRepo implements MarkdownPageRepo
{
public function __construct(
private readonly CacheInterface $cache,
private readonly MarkdownPageRepo $repo,
private readonly Settings $settings,
) {
}
/**
* @inheritDoc
*/
public function all(): array
{
$callback = fn () => $this->repo->all();
if ($this->settings->isDev()) {
return $callback();
}
return $this->cache->get('ALLPAGES', function (ItemInterface $item) use ($callback) {
$item->expiresAfter(30);
return $callback();
});
}
public function byId(int $id): MarkdownPage
{
$callback = fn () => $this->repo->byId($id);
if ($this->settings->isDev()) {
return $callback();
}
return $this->cache->get('PAGE' . $id, function (ItemInterface $item) use ($callback) {
$item->expiresAfter(30);
return $callback();
});
}
public function byTitle(string $title): MarkdownPage
{
$callback = fn () => $this->repo->byTitle($title);
if ($this->settings->isDev()) {
return $callback();
}
return $this->cache->get('PAGE' . $title, function (ItemInterface $item) use ($callback) {
$item->expiresAfter(30);
return $callback();
});
}
public function save(MarkdownPage $page): MarkdownPage
{
return $this->repo->save($page);
}
}

View file

@ -1,59 +0,0 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Repository;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Lubian\NoFramework\Exception\NotFound;
use Lubian\NoFramework\Model\MarkdownPage;
use function random_int;
use function usleep;
final class DoctrineMarkdownPageRepo implements MarkdownPageRepo
{
/** @var EntityRepository<MarkdownPage> */
private EntityRepository $repo;
public function __construct(
private EntityManagerInterface $entityManager
) {
$this->repo = $this->entityManager->getRepository(MarkdownPage::class);
}
/**
* @inheritDoc
*/
public function all(): array
{
usleep(random_int(500, 1500) * 1000);
return $this->repo->findAll();
}
public function byId(int $id): MarkdownPage
{
usleep(random_int(500, 1500) * 1000);
$page = $this->repo->findOneBy(['id' => $id]);
if (! $page instanceof MarkdownPage) {
throw new NotFound;
}
return $page;
}
public function byTitle(string $title): MarkdownPage
{
usleep(random_int(500, 1500) * 1000);
$page = $this->repo->findOneBy(['title' => $title]);
if (! $page instanceof MarkdownPage) {
throw new NotFound;
}
return $page;
}
public function save(MarkdownPage $page): MarkdownPage
{
$this->entityManager->persist($page);
$this->entityManager->flush();
return $page;
}
}

View file

@ -0,0 +1,61 @@
<?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];
}
}

View file

@ -1,69 +0,0 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Repository;
use Lubian\NoFramework\Exception\NotFound;
use Lubian\NoFramework\Model\MarkdownPage;
use function array_filter;
use function array_map;
use function array_values;
use function assert;
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;
final class MarkdownPageFilesystem implements MarkdownPageRepo
{
public function __construct(private readonly string $dataPath)
{
}
/**
* @return MarkdownPage[]
*/
public function all(): array
{
$fileNames = glob($this->dataPath . '*.md');
assert(is_array($fileNames));
return array_map(function (string $name): MarkdownPage {
usleep(random_int(200, 500) * 1000);
$content = file_get_contents($name);
$name = str_replace($this->dataPath, '', $name);
$name = str_replace('.md', '', $name);
$id = (int) substr($name, 0, 2);
$title = substr($name, 3);
return new MarkdownPage($id, $title, $content);
}, $fileNames);
}
public function byId(int $id): MarkdownPage
{
$callback = fn (MarkdownPage $p): bool => $p->id === $id;
$filtered = array_values(array_filter($this->all(), $callback));
if (count($filtered) === 0) {
throw new NotFound;
}
return $filtered[0];
}
public function byTitle(string $title): MarkdownPage
{
$callback = fn (MarkdownPage $p): bool => $p->title === $title;
$filtered = array_values(array_filter($this->all(), $callback));
if (count($filtered) === 0) {
throw new NotFound;
}
return $filtered[0];
}
public function save(MarkdownPage $page): MarkdownPage
{
return $page;
}
}

View file

@ -2,18 +2,14 @@
namespace Lubian\NoFramework\Repository;
use Lubian\NoFramework\Exception\NotFound;
use Lubian\NoFramework\Model\MarkdownPage;
interface MarkdownPageRepo
{
/**
* @return MarkdownPage[]
*/
/** @return MarkdownPage[] */
public function all(): array;
public function byId(int $id): MarkdownPage;
public function byTitle(string $title): MarkdownPage;
public function save(MarkdownPage $page): MarkdownPage;
/** @throws NotFound */
public function byName(string $name): MarkdownPage;
}

View file

@ -4,10 +4,6 @@ namespace Lubian\NoFramework;
final class Settings
{
/**
* @param array{driver: string, user: string, password: string, path: string} $connection
* @param array{devMode: bool, metadataDirs: string[], cacheDir: string} $doctrine
*/
public function __construct(
public readonly string $environment,
public readonly string $dependenciesFile,
@ -15,19 +11,6 @@ final class Settings
public readonly string $templateDir,
public readonly string $templateExtension,
public readonly string $pagesPath,
/**
* @var array{driver: string, user: string, password: string, path: string}
*/
public readonly array $connection,
/**
* @var array{devMode: bool, metadataDirs: string[], cacheDir: string}
*/
public readonly array $doctrine,
) {
}
public function isDev(): bool
{
return $this->environment === 'dev';
}
}

View file

@ -1,28 +0,0 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Template;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\GithubFlavoredMarkdownExtension;
use League\CommonMark\MarkdownConverter;
final class GithubMarkdownRenderer implements MarkdownRenderer
{
private MarkdownConverter $engine;
public function __construct(
CommonMarkCoreExtension $commonMarkCoreExtension,
GithubFlavoredMarkdownExtension $githubFlavoredMarkdownExtension,
) {
$environment = new Environment([]);
$environment->addExtension($commonMarkCoreExtension);
$environment->addExtension($githubFlavoredMarkdownExtension);
$this->engine = new MarkdownConverter($environment);
}
public function render(string $markdown): string
{
return (string) $this->engine->convert($markdown);
}
}

View file

@ -1,13 +1,17 @@
<!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">
</head>
<body>
<main>
{{{content}}}
</main>
</body>
<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>