wip: rewrite di chapter
This commit is contained in:
parent
a4f171b98c
commit
988a532b78
18 changed files with 1667 additions and 0 deletions
|
@ -2,6 +2,16 @@
|
||||||
|
|
||||||
### Dependency Injector
|
### 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
|
A dependency injector resolves the dependencies of your class and makes sure that the correct objects are injected when
|
||||||
the class is instantiated.
|
the class is instantiated.
|
||||||
|
|
||||||
|
|
49
app/composer.json
Normal file
49
app/composer.json
Normal 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
1239
app/composer.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
37
app/config/container.php
Normal file
37
app/config/container.php
Normal 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
10
app/config/routes.php
Normal 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
89
app/ecs.php
Normal 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);
|
||||||
|
};
|
6
app/phpstan-baseline.neon
Normal file
6
app/phpstan-baseline.neon
Normal 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
8
app/phpstan.neon
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
includes:
|
||||||
|
- phpstan-baseline.neon
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
level: max
|
||||||
|
paths:
|
||||||
|
- src
|
||||||
|
- config
|
3
app/public/index.php
Normal file
3
app/public/index.php
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
require __DIR__ . '/../src/Bootstrap.php';
|
12
app/rector.php
Normal file
12
app/rector.php
Normal 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
29
app/src/Action/Hello.php
Normal 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
24
app/src/Action/Other.php
Normal 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
104
app/src/Bootstrap.php
Normal 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();
|
9
app/src/Exception/InternalServerError.php
Normal file
9
app/src/Exception/InternalServerError.php
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Lubian\NoFramework\Exception;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
final class InternalServerError extends Exception
|
||||||
|
{
|
||||||
|
}
|
9
app/src/Exception/MethodNotAllowed.php
Normal file
9
app/src/Exception/MethodNotAllowed.php
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Lubian\NoFramework\Exception;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
final class MethodNotAllowed extends Exception
|
||||||
|
{
|
||||||
|
}
|
9
app/src/Exception/NotFound.php
Normal file
9
app/src/Exception/NotFound.php
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Lubian\NoFramework\Exception;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
final class NotFound extends Exception
|
||||||
|
{
|
||||||
|
}
|
8
app/src/Service/Time/Clock.php
Normal file
8
app/src/Service/Time/Clock.php
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Lubian\NoFramework\Service\Time;
|
||||||
|
|
||||||
|
interface Clock
|
||||||
|
{
|
||||||
|
public function now(): \DateTimeImmutable;
|
||||||
|
}
|
12
app/src/Service/Time/SystemClock.php
Normal file
12
app/src/Service/Time/SystemClock.php
Normal 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue