This commit is contained in:
lubiana 2022-03-31 08:34:30 +02:00 committed by Andre Lubian
parent 48c9c9467d
commit b12cf019e7
107 changed files with 8372 additions and 186 deletions

View file

@ -0,0 +1,38 @@
<?php declare(strict_types=1);
/*
* This document has been generated with
* https://mlocati.github.io/php-cs-fixer-configurator/#version:3.1.0|configurator
* you can change this configuration by importing this file.
*/
$config = new PhpCsFixer\Config();
return $config
->setRiskyAllowed(true)
->setRules([
'@PSR12:risky' => true,
'@PSR12' => true,
'@PHP80Migration' => true,
'@PHP80Migration:risky' => true,
'@PHP81Migration' => true,
'array_indentation' => true,
'include' => true,
'blank_line_after_opening_tag' => false,
'native_constant_invocation' => true,
'new_with_braces' => false,
'native_function_invocation' => [
'include' => ['@all']
],
'no_unused_imports' => true,
'global_namespace_import' => [
'import_classes' => true,
'import_constants' => true,
'import_functions' => true,
],
'ordered_interfaces' => true,
])
->setFinder(
PhpCsFixer\Finder::create()
->in([
__DIR__ . '/src',
__DIR__ . '/config'
])
);

View file

@ -0,0 +1,9 @@
<?xml version="1.0"?>
<ruleset>
<arg name="basepath" value="."/>
<file>src</file>
<file>config</file>
<rule ref="HardMode"/>
</ruleset>

View file

@ -0,0 +1,50 @@
{
"name": "lubian/no-framework",
"require": {
"php": "^8.1",
"filp/whoops": "^2.14",
"laminas/laminas-diactoros": "^2.8",
"nikic/fast-route": "^1.3",
"psr/http-server-handler": "^1.0",
"php-di/php-di": "^6.3",
"mustache/mustache": "^2.14",
"psr/http-server-middleware": "^1.0",
"middlewares/trailing-slash": "^2.0",
"middlewares/whoops": "^2.0"
},
"autoload": {
"psr-4": {
"Lubian\\NoFramework\\": "src/"
}
},
"authors": [
{
"name": "lubian",
"email": "test@example.com"
}
],
"require-dev": {
"phpstan/phpstan": "^1.5",
"php-cs-fixer/shim": "^3.8",
"symfony/var-dumper": "^6.0",
"squizlabs/php_codesniffer": "^3.6",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan-strict-rules": "^1.1",
"thecodingmachine/phpstan-strict-rules": "^1.0",
"mnapoli/hard-mode": "^0.3.0",
"psalm/phar": "^4.22"
},
"config": {
"allow-plugins": {
"phpstan/extension-installer": true,
"dealerdirect/phpcodesniffer-composer-installer": true
}
},
"scripts": {
"serve": "php -S 0.0.0.0:1234 -t public",
"phpstan": "./vendor/bin/phpstan analyze",
"baseline": "./vendor/bin/phpstan analyze --generate-baseline",
"check": "./vendor/bin/phpcs",
"fix": "./vendor/bin/php-cs-fixer fix && ./vendor/bin/phpcbf"
}
}

1815
implementation/14-middleware/composer.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,42 @@
<?php declare(strict_types=1);
use FastRoute\Dispatcher;
use Laminas\Diactoros\ResponseFactory;
use Lubian\NoFramework\Factory\DiactorosRequestFactory;
use Lubian\NoFramework\Factory\PipelineProvider;
use Lubian\NoFramework\Factory\RequestFactory;
use Lubian\NoFramework\Http\BasicEmitter;
use Lubian\NoFramework\Http\Emitter;
use Lubian\NoFramework\Http\InvokerRoutedHandler;
use Lubian\NoFramework\Http\Pipeline;
use Lubian\NoFramework\Http\RoutedRequestHandler;
use Lubian\NoFramework\Http\RouteMiddleware;
use Lubian\NoFramework\Service\Time\Now;
use Lubian\NoFramework\Service\Time\SystemClockNow;
use Lubian\NoFramework\Settings;
use Lubian\NoFramework\Template\MustacheRenderer;
use Lubian\NoFramework\Template\Renderer;
use Mustache_Engine as ME;
use Mustache_Loader_FilesystemLoader as MLF;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use function FastRoute\simpleDispatcher;
return [
ResponseInterface::class => fn (ResponseFactory $rf) => $rf->createResponse(),
ServerRequestInterface::class => fn (RequestFactory $rf) => $rf->fromGlobals(),
Now::class => fn (SystemClockNow $n) => $n,
Renderer::class => fn (Mustache_Engine $e) => new MustacheRenderer($e),
MLF::class => fn (Settings $s) => new MLF($s->templateDir, ['extension' => $s->templateExtension]),
ME::class => fn (MLF $mfl) => new ME(['loader' => $mfl]),
ResponseFactoryInterface::class => fn (ResponseFactory $rf) => $rf,
Emitter::class => fn (BasicEmitter $e) => $e,
RoutedRequestHandler::class => fn (InvokerRoutedHandler $h) => $h,
MiddlewareInterface::class => fn (RouteMiddleware $r) => $r,
Dispatcher::class => fn () => simpleDispatcher(require __DIR__ . '/routes.php'),
RequestFactory::class => fn (DiactorosRequestFactory $rf) => $rf,
Pipeline::class => fn (PipelineProvider $p) => $p->getPipeline(),
];

View file

@ -0,0 +1,11 @@
<?php declare(strict_types=1);
use Lubian\NoFramework\Http\RouteMiddleware;
use Middlewares\TrailingSlash;
use Middlewares\Whoops;
return [
Whoops::class,
TrailingSlash::class,
RouteMiddleware::class,
];

View file

@ -0,0 +1,12 @@
<?php declare(strict_types=1);
use FastRoute\RouteCollector;
use Lubian\NoFramework\Action\Hello;
use Lubian\NoFramework\Action\Other;
use Psr\Http\Message\ResponseInterface as Response;
return function (RouteCollector $r): void {
$r->addRoute('GET', '/hello[/{name}]', Hello::class);
$r->addRoute('GET', '/another-route', [Other::class, 'someFunctionName']);
$r->addRoute('GET', '/', fn (Response $r) => $r->withStatus(302)->withHeader('Location', '/hello'));
};

View file

@ -0,0 +1,11 @@
<?php declare(strict_types=1);
use Lubian\NoFramework\Settings;
return new Settings(
environment: 'dev',
dependenciesFile: __DIR__ . '/dependencies.php',
middlewaresFile: __DIR__ . '/middlewares.php',
templateDir: __DIR__ . '/../templates',
templateExtension: '.html',
);

View file

@ -0,0 +1,7 @@
parameters:
ignoreErrors:
-
message: "#^Parameter \\#1 \\$callable of method Invoker\\\\InvokerInterface\\:\\:call\\(\\) expects array\\|\\(callable\\(\\)\\: mixed\\)\\|string, mixed given\\.$#"
count: 1
path: src/Http/InvokerRoutedHandler.php

View file

@ -0,0 +1,8 @@
includes:
- phpstan-baseline.neon
parameters:
level: max
paths:
- src
- config

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,5 @@
<?php
declare(strict_types=1);
require __DIR__ . '/../src/Bootstrap.php';

View file

@ -0,0 +1,31 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Action;
use Lubian\NoFramework\Service\Time\Now;
use Lubian\NoFramework\Template\Renderer;
use Psr\Http\Message\ResponseInterface;
final class Hello
{
public function __invoke(
ResponseInterface $response,
Now $now,
Renderer $renderer,
string $name = 'Stranger',
): ResponseInterface {
$body = $response->getBody();
$data = [
'now' => $now()->format('H:i:s'),
'name' => $name,
];
$content = $renderer->render('hello', $data);
$body->write($content);
return $response
->withStatus(200)
->withBody($body);
}
}

View file

@ -0,0 +1,19 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Action;
use Psr\Http\Message\ResponseInterface;
final class Other
{
public function someFunctionName(ResponseInterface $response): ResponseInterface
{
$body = $response->getBody();
$body->write('This works too!');
return $response
->withStatus(200)
->withBody($body);
}
}

View file

@ -0,0 +1,40 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework;
use Lubian\NoFramework\Factory\FileSystemSettingsProvider;
use Lubian\NoFramework\Factory\SettingsContainerProvider;
use Throwable;
use Whoops\Handler\PrettyPageHandler;
use Whoops\Run;
use function assert;
use function error_log;
use function error_reporting;
use const E_ALL;
require __DIR__ . '/../vendor/autoload.php';
error_reporting(E_ALL);
$settingsProvider = new FileSystemSettingsProvider(__DIR__ . '/../config/settings.php');
$container = (new SettingsContainerProvider($settingsProvider))->getContainer();
$settings = $settingsProvider->getSettings();
$whoops = new Run;
if ($settings->environment === 'dev') {
$whoops->pushHandler(new PrettyPageHandler);
} else {
$whoops->pushHandler(function (Throwable $e): void {
error_log('Error: ' . $e->getMessage(), (int) $e->getCode());
echo 'An Error happened';
});
}
$whoops->register();
$app = $container->get(Kernel::class);
assert($app instanceof Kernel);
$app->run();

View file

@ -0,0 +1,9 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Exception;
use Exception;
final class InternalServerError extends Exception
{
}

View file

@ -0,0 +1,9 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Exception;
use Exception;
final class MethodNotAllowed extends Exception
{
}

View file

@ -0,0 +1,9 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Exception;
use Exception;
final class NotFound extends Exception
{
}

View file

@ -0,0 +1,10 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Factory;
use Psr\Container\ContainerInterface;
interface ContainerProvider
{
public function getContainer(): ContainerInterface;
}

View file

@ -0,0 +1,28 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Factory;
use Laminas\Diactoros\ServerRequestFactory;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UriInterface;
final class DiactorosRequestFactory implements RequestFactory
{
public function __construct(private readonly ServerRequestFactory $factory)
{
}
public function fromGlobals(): ServerRequestInterface
{
return $this->factory::fromGlobals();
}
/**
* @param UriInterface|string $uri
* @param array<mixed> $serverParams
*/
public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface
{
return $this->factory->createServerRequest($method, $uri, $serverParams);
}
}

View file

@ -0,0 +1,22 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Factory;
use Lubian\NoFramework\Settings;
use function assert;
final class FileSystemSettingsProvider implements SettingsProvider
{
public function __construct(
private string $filePath
) {
}
public function getSettings(): Settings
{
$settings = require $this->filePath;
assert($settings instanceof Settings);
return $settings;
}
}

View file

@ -0,0 +1,25 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Factory;
use Lubian\NoFramework\Http\ContainerPipeline;
use Lubian\NoFramework\Http\Pipeline;
use Lubian\NoFramework\Http\RoutedRequestHandler;
use Lubian\NoFramework\Settings;
use Psr\Container\ContainerInterface;
class PipelineProvider
{
public function __construct(
private Settings $settings,
private RoutedRequestHandler $tip,
private ContainerInterface $container,
) {
}
public function getPipeline(): Pipeline
{
$middlewares = require $this->settings->middlewaresFile;
return new ContainerPipeline($middlewares, $this->tip, $this->container);
}
}

View file

@ -0,0 +1,11 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Factory;
use Psr\Http\Message\ServerRequestFactoryInterface;
use Psr\Http\Message\ServerRequestInterface;
interface RequestFactory extends ServerRequestFactoryInterface
{
public function fromGlobals(): ServerRequestInterface;
}

View file

@ -0,0 +1,25 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Factory;
use DI\ContainerBuilder;
use Lubian\NoFramework\Settings;
use Psr\Container\ContainerInterface;
final class SettingsContainerProvider implements ContainerProvider
{
public function __construct(
private SettingsProvider $settingsProvider,
) {
}
public function getContainer(): ContainerInterface
{
$builder = new ContainerBuilder;
$settings = $this->settingsProvider->getSettings();
$dependencies = require $settings->dependenciesFile;
$dependencies[Settings::class] = fn (): Settings => $settings;
$builder->addDefinitions($dependencies);
return $builder->build();
}
}

View file

@ -0,0 +1,10 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Factory;
use Lubian\NoFramework\Settings;
interface SettingsProvider
{
public function getSettings(): Settings;
}

View file

@ -0,0 +1,38 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Http;
use Psr\Http\Message\ResponseInterface;
use function header;
use function sprintf;
use function strtolower;
final class BasicEmitter implements Emitter
{
public function emit(ResponseInterface $response, bool $withoutBody = false): void
{
foreach ($response->getHeaders() as $name => $values) {
$first = strtolower($name) !== 'set-cookie';
foreach ($values as $value) {
$header = sprintf('%s: %s', $name, $value);
header($header, $first);
$first = false;
}
}
$statusLine = sprintf(
'HTTP/%s %s %s',
$response->getProtocolVersion(),
$response->getStatusCode(),
$response->getReasonPhrase()
);
header($statusLine, true, $response->getStatusCode());
if ($withoutBody) {
return;
}
echo $response->getBody();
}
}

View file

@ -0,0 +1,82 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Http;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use function array_reverse;
use function assert;
use function is_string;
class ContainerPipeline implements Pipeline
{
/**
* @param array<MiddlewareInterface|class-string> $middlewares
* @param RequestHandlerInterface $tip
* @param ContainerInterface $container
*/
public function __construct(
private array $middlewares,
private RequestHandlerInterface $tip,
private ContainerInterface $container,
) {
}
public function dispatch(ServerRequestInterface $request): ResponseInterface
{
$this->buildStack();
return $this->tip->handle($request);
}
private function buildStack(): void
{
foreach (array_reverse($this->middlewares) as $middleware) {
$next = $this->tip;
if ($middleware instanceof MiddlewareInterface) {
$this->tip = $this->wrapMiddleware($middleware, $next);
}
if (is_string($middleware)) {
$this->tip = $this->wrapResolvedMiddleware($middleware, $next);
}
}
}
private function wrapResolvedMiddleware(string $middleware, RequestHandlerInterface $next): RequestHandlerInterface
{
return new class ($middleware, $next, $this->container) implements RequestHandlerInterface {
public function __construct(
private readonly string $middleware,
private readonly RequestHandlerInterface $handler,
private readonly ContainerInterface $container,
) {
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
$middleware = $this->container->get($this->middleware);
assert($middleware instanceof MiddlewareInterface);
return $middleware->process($request, $this->handler);
}
};
}
private function wrapMiddleware(MiddlewareInterface $middleware, RequestHandlerInterface $next): RequestHandlerInterface
{
return new class ($middleware, $next) implements RequestHandlerInterface {
public function __construct(
private readonly MiddlewareInterface $middleware,
private readonly RequestHandlerInterface $handler,
) {
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
return $this->middleware->process($request, $this->handler);
}
};
}
}

View file

@ -0,0 +1,10 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Http;
use Psr\Http\Message\ResponseInterface;
interface Emitter
{
public function emit(ResponseInterface $response, bool $withoutBody = false): void;
}

View file

@ -0,0 +1,34 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Http;
use Invoker\InvokerInterface;
use Lubian\NoFramework\Exception\InternalServerError;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
final class InvokerRoutedHandler implements RoutedRequestHandler
{
public function __construct(
private readonly InvokerInterface $invoker,
private string $routeAttributeName = '__route_handler',
) {
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
$handler = $request->getAttribute($this->routeAttributeName, false);
$vars = $request->getAttributes();
$vars['request'] = $request;
$response = $this->invoker->call($handler, $vars);
if (! $response instanceof ResponseInterface) {
throw new InternalServerError('Handler returned invalid response');
}
return $response;
}
public function setRouteAttributeName(string $routeAttributeName = '__route_handler'): void
{
$this->routeAttributeName = $routeAttributeName;
}
}

View file

@ -0,0 +1,11 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Http;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
interface Pipeline
{
public function dispatch(ServerRequestInterface $request): ResponseInterface;
}

View file

@ -0,0 +1,69 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Http;
use FastRoute\Dispatcher;
use Lubian\NoFramework\Exception\InternalServerError;
use Lubian\NoFramework\Exception\MethodNotAllowed;
use Lubian\NoFramework\Exception\NotFound;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Throwable;
final class RouteMiddleware implements MiddlewareInterface
{
public function __construct(
private readonly Dispatcher $dispatcher,
private readonly ResponseFactoryInterface $responseFactory,
private readonly string $routeAttributeName = '__route_handler',
) {
}
private function decorateRequest(
ServerRequestInterface $request,
): ServerRequestInterface {
$routeInfo = $this->dispatcher->dispatch(
$request->getMethod(),
$request->getUri()->getPath(),
);
if ($routeInfo[0] === Dispatcher::METHOD_NOT_ALLOWED) {
throw new MethodNotAllowed;
}
if ($routeInfo[0] === Dispatcher::FOUND) {
foreach ($routeInfo[2] as $attributeName => $attributeValue) {
$request = $request->withAttribute($attributeName, $attributeValue);
}
return $request->withAttribute(
$this->routeAttributeName,
$routeInfo[1]
);
}
throw new NotFound;
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
try {
$request = $this->decorateRequest($request);
} catch (NotFound) {
$response = $this->responseFactory->createResponse(404);
$response->getBody()->write('Not Found');
return $response;
} catch (MethodNotAllowed) {
return $this->responseFactory->createResponse(405);
} catch (Throwable $t) {
throw new InternalServerError($t->getMessage(), $t->getCode(), $t);
}
if ($handler instanceof RoutedRequestHandler) {
$handler->setRouteAttributeName($this->routeAttributeName);
}
return $handler->handle($request);
}
}

View file

@ -0,0 +1,10 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Http;
use Psr\Http\Server\RequestHandlerInterface;
interface RoutedRequestHandler extends RequestHandlerInterface
{
public function setRouteAttributeName(string $routeAttributeName = '__route_handler'): void;
}

View file

@ -0,0 +1,32 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework;
use Lubian\NoFramework\Factory\RequestFactory;
use Lubian\NoFramework\Http\Emitter;
use Lubian\NoFramework\Http\Pipeline;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
final class Kernel implements RequestHandlerInterface
{
public function __construct(
private readonly RequestFactory $requestFactory,
private readonly Pipeline $pipeline,
private readonly Emitter $emitter,
) {
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
return $this->pipeline->dispatch($request);
}
public function run(): void
{
$request = $this->requestFactory->fromGlobals();
$response = $this->handle($request);
$this->emitter->emit($response);
}
}

View file

@ -0,0 +1,10 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Service\Time;
use DateTimeImmutable;
interface Now
{
public function __invoke(): DateTimeImmutable;
}

View file

@ -0,0 +1,13 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Service\Time;
use DateTimeImmutable;
final class SystemClockNow implements Now
{
public function __invoke(): DateTimeImmutable
{
return new DateTimeImmutable;
}
}

View file

@ -0,0 +1,15 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework;
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,
) {
}
}

View file

@ -0,0 +1,17 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Template;
use Mustache_Engine;
final class MustacheRenderer implements Renderer
{
public function __construct(private Mustache_Engine $engine)
{
}
public function render(string $template, array $data = []): string
{
return $this->engine->render($template, $data);
}
}

View file

@ -0,0 +1,11 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Template;
interface Renderer
{
/**
* @param array<string, mixed> $data
*/
public function render(string $template, array $data = []): string;
}

View file

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello World</title>
</head>
<body>
<h1>Hello {{name}}</h1>
<p>The time is {{now}}</p>
</body>
</html>