wip: rewrite di chapter

This commit is contained in:
lubiana 2022-05-20 09:28:47 +02:00 committed by Andre Lubian
parent a4f171b98c
commit 988a532b78
18 changed files with 1667 additions and 0 deletions

View file

@ -2,6 +2,16 @@
### Dependency Injector
In the last chapter we rewrote our Actions to require the response-objet as a constructor parameter, and provided it
in the dispatcher section of our `Bootstrap.php`. As we only have one dependency this works really fine, but if we have
different classes with different dependencies our bootstrap file gets complicated quite quickly. Lets look at an example
to explain the problem and work on a solution.
#### Adding a clock service
Lets assume that we want to show the current time in our Hello action. We could easily just call use one of the many
ways to get the current time directly in the handle-method, but maybe we want to make that configurable and
A dependency injector resolves the dependencies of your class and makes sure that the correct objects are injected when
the class is instantiated.

49
app/composer.json Normal file
View file

@ -0,0 +1,49 @@
{
"name": "lubian/no-framework",
"autoload": {
"psr-4": {
"Lubian\\NoFramework\\": "src/"
}
},
"authors": [
{
"name": "example",
"email": "test@example.com"
}
],
"require": {
"php": ">=8.1",
"filp/whoops": "^2.14",
"laminas/laminas-diactoros": "^2.11",
"nikic/fast-route": "^1.3",
"psr/http-server-handler": "^1.0",
"psr/container": "^2.0"
},
"require-dev": {
"phpstan/phpstan": "^1.6",
"symfony/var-dumper": "^6.0",
"slevomat/coding-standard": "^7.2",
"symplify/easy-coding-standard": "^10.2",
"rector/rector": "^0.12.23",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan-strict-rules": "^1.2",
"thecodingmachine/phpstan-strict-rules": "^1.0"
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true,
"phpstan/extension-installer": true
}
},
"scripts": {
"serve": [
"Composer\\Config::disableProcessTimeout",
"php -S 0.0.0.0:1235 -t public"
],
"phpstan": "./vendor/bin/phpstan analyze",
"baseline": "./vendor/bin/phpstan analyze --generate-baseline",
"check": "./vendor/bin/ecs",
"fix": "./vendor/bin/ecs --fix",
"rector": "./vendor/bin/rector process"
}
}

1239
app/composer.lock generated Normal file

File diff suppressed because it is too large Load diff

37
app/config/container.php Normal file
View file

@ -0,0 +1,37 @@
<?php declare(strict_types=1);
return new class () implements \Psr\Container\ContainerInterface {
private readonly array $services;
public function __construct()
{
$this->services = [
\Psr\Http\Message\ServerRequestInterface::class => fn () => \Laminas\Diactoros\ServerRequestFactory::fromGlobals(),
\Psr\Http\Message\ResponseInterface::class => fn () => new \Laminas\Diactoros\Response(),
\FastRoute\Dispatcher::class => fn () => \FastRoute\simpleDispatcher(require __DIR__ . '/routes.php'),
\Lubian\NoFramework\Service\Time\Clock::class => fn () => new \Lubian\NoFramework\Service\Time\SystemClock(),
\Lubian\NoFramework\Action\Hello::class => fn () => new \Lubian\NoFramework\Action\Hello(
$this->get(\Psr\Http\Message\ResponseInterface::class),
$this->get(\Lubian\NoFramework\Service\Time\Clock::class)
),
\Lubian\NoFramework\Action\Other::class => fn () => new \Lubian\NoFramework\Action\Other(
$this->get(\Psr\Http\Message\ResponseInterface::class)
),
];
}
public function get(string $id)
{
if (! $this->has($id)) {
throw new class () extends \Exception implements \Psr\Container\NotFoundExceptionInterface {
};
}
return $this->services[$id]();
}
public function has(string $id): bool
{
return array_key_exists($id, $this->services);
}
};

10
app/config/routes.php Normal file
View file

@ -0,0 +1,10 @@
<?php declare(strict_types=1);
use FastRoute\RouteCollector;
use Lubian\NoFramework\Action\Hello;
use Lubian\NoFramework\Action\Other;
return function (RouteCollector $r) {
$r->addRoute('GET', '/hello[/{name}]', Hello::class);
$r->addRoute('GET', '/other', Other::class);
};

89
app/ecs.php Normal file
View file

@ -0,0 +1,89 @@
<?php declare(strict_types=1);
use PhpCsFixer\Fixer\Import\OrderedImportsFixer;
use PhpCsFixer\Fixer\Operator\NewWithBracesFixer;
use PhpCsFixer\Fixer\PhpTag\BlankLineAfterOpeningTagFixer;
use SlevomatCodingStandard\Sniffs\Classes\ClassConstantVisibilitySniff;
use SlevomatCodingStandard\Sniffs\ControlStructures\NewWithoutParenthesesSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\AlphabeticallySortedUsesSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\DisallowGroupUseSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\MultipleUsesPerLineSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\NamespaceSpacingSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\ReferenceUsedNamesOnlySniff;
use SlevomatCodingStandard\Sniffs\Namespaces\UseSpacingSniff;
use SlevomatCodingStandard\Sniffs\TypeHints\DeclareStrictTypesSniff;
use SlevomatCodingStandard\Sniffs\TypeHints\UnionTypeHintFormatSniff;
use Symplify\EasyCodingStandard\Config\ECSConfig;
use Symplify\EasyCodingStandard\ValueObject\Set\SetList;
return static function (ECSConfig $config): void {
$config->parallel();
$config->paths([__DIR__ . '/src', __DIR__ . '/config', __DIR__ . '/ecs.php', __DIR__ . '/rector.php']);
$config->skip([BlankLineAfterOpeningTagFixer::class, OrderedImportsFixer::class, NewWithBracesFixer::class]);
$config->sets([
SetList::PSR_12,
SetList::STRICT,
SetList::ARRAY,
SetList::SPACES,
SetList::DOCBLOCK,
SetList::CLEAN_CODE,
SetList::COMMON,
SetList::COMMENTS,
SetList::NAMESPACES,
SetList::SYMPLIFY,
SetList::CONTROL_STRUCTURES,
]);
// force visibility declaration on class constants
$config->ruleWithConfiguration(ClassConstantVisibilitySniff::class, [
'fixable' => true,
]);
// sort all use statements
$config->rules([
AlphabeticallySortedUsesSniff::class,
DisallowGroupUseSniff::class,
MultipleUsesPerLineSniff::class,
NamespaceSpacingSniff::class,
]);
// import all namespaces, and event php core functions and classes
$config->ruleWithConfiguration(
ReferenceUsedNamesOnlySniff::class,
[
'allowFallbackGlobalConstants' => false,
'allowFallbackGlobalFunctions' => false,
'allowFullyQualifiedGlobalClasses' => false,
'allowFullyQualifiedGlobalConstants' => false,
'allowFullyQualifiedGlobalFunctions' => false,
'allowFullyQualifiedNameForCollidingClasses' => true,
'allowFullyQualifiedNameForCollidingConstants' => true,
'allowFullyQualifiedNameForCollidingFunctions' => true,
'searchAnnotations' => true,
]
);
// define newlines between use statements
$config->ruleWithConfiguration(UseSpacingSniff::class, [
'linesCountBeforeFirstUse' => 1,
'linesCountBetweenUseTypes' => 1,
'linesCountAfterLastUse' => 1,
]);
// strict types declaration should be on same line as opening tag
$config->ruleWithConfiguration(DeclareStrictTypesSniff::class, [
'declareOnFirstLine' => true,
'spacesCountAroundEqualsSign' => 0,
]);
// disallow ?Foo typehint in favor of Foo|null
$config->ruleWithConfiguration(UnionTypeHintFormatSniff::class, [
'withSpaces' => 'no',
'shortNullable' => 'no',
'nullPosition' => 'last',
]);
// Remove useless parentheses in new statements
$config->rule(NewWithoutParenthesesSniff::class);
};

View file

@ -0,0 +1,6 @@
parameters:
ignoreErrors:
-
message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#"
count: 1
path: src/Bootstrap.php

8
app/phpstan.neon Normal file
View file

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

3
app/public/index.php Normal file
View file

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

12
app/rector.php Normal file
View file

@ -0,0 +1,12 @@
<?php declare(strict_types=1);
use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\LevelSetList;
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->paths([__DIR__ . '/src', __DIR__ . '/config', __DIR__ . '/ecs.php', __DIR__ . '/rector.php']);
$rectorConfig->importNames();
$rectorConfig->sets([LevelSetList::UP_TO_PHP_81]);
};

29
app/src/Action/Hello.php Normal file
View file

@ -0,0 +1,29 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Action;
use Lubian\NoFramework\Service\Time\Clock;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
final class Hello implements RequestHandlerInterface
{
public function __construct(private readonly ResponseInterface $response, private readonly Clock $clock)
{
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
$name = $request->getAttribute('name', 'Stranger');
$body = $this->response->getBody();
$time = $this->clock->now()->format('H:i:s');
$body->write('Hello ' . $name . '!<br />');
$body->write('The Time is: ' . $time);
return $this->response->withBody($body)
->withStatus(200);
}
}

24
app/src/Action/Other.php Normal file
View file

@ -0,0 +1,24 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Action;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
final class Other implements RequestHandlerInterface
{
public function __construct(private readonly ResponseInterface $response)
{
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
$body = $this->response->getBody();
$body->write('This works too!');
return $this->response->withBody($body)
->withStatus(200);
}
}

104
app/src/Bootstrap.php Normal file
View file

@ -0,0 +1,104 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework;
use FastRoute\Dispatcher;
use Laminas\Diactoros\Response;
use Lubian\NoFramework\Exception\InternalServerError;
use Lubian\NoFramework\Exception\MethodNotAllowed;
use Lubian\NoFramework\Exception\NotFound;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Throwable;
use Whoops\Handler\PrettyPageHandler;
use Whoops\Run;
use function assert;
use function error_log;
use function error_reporting;
use function getenv;
use function header;
use function sprintf;
use function strtolower;
use const E_ALL;
require __DIR__ . '/../vendor/autoload.php';
$environment = getenv('ENVIRONMENT') ?: 'dev';
error_reporting(E_ALL);
$whoops = new Run;
if ($environment === 'dev') {
$whoops->pushHandler(new PrettyPageHandler);
} else {
$whoops->pushHandler(function (Throwable $t) {
error_log('ERROR: ' . $t->getMessage(), $t->getCode());
echo 'Oooopsie';
});
}
$whoops->register();
$container = require __DIR__ . '/../config/container.php';
assert($container instanceof ContainerInterface);
$request = $container->get(ServerRequestInterface::class);
assert($request instanceof ServerRequestInterface);
$dispatcher = $container->get(Dispatcher::class);
assert($dispatcher instanceof Dispatcher);
$routeInfo = $dispatcher->dispatch($request->getMethod(), $request->getUri() ->getPath(),);
try {
switch ($routeInfo[0]) {
case Dispatcher::FOUND:
$className = $routeInfo[1];
$handler = $container->get($className);
assert($handler instanceof RequestHandlerInterface);
foreach ($routeInfo[2] as $attributeName => $attributeValue) {
$request = $request->withAttribute($attributeName, $attributeValue);
}
$response = $handler->handle($request);
break;
case Dispatcher::METHOD_NOT_ALLOWED:
throw new MethodNotAllowed;
case Dispatcher::NOT_FOUND:
default:
throw new NotFound;
}
} catch (MethodNotAllowed) {
$response = (new Response)->withStatus(405);
$response->getBody()
->write('Method not Allowed');
} catch (NotFound) {
$response = (new Response)->withStatus(404);
$response->getBody()
->write('Not Found');
} catch (Throwable $t) {
throw new InternalServerError($t->getMessage(), $t->getCode(), $t);
}
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());
echo $response->getBody();

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,8 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Service\Time;
interface Clock
{
public function now(): \DateTimeImmutable;
}

View file

@ -0,0 +1,12 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Service\Time;
final class SystemClock implements Clock
{
public function now(): \DateTimeImmutable
{
return new \DateTimeImmutable();
}
}