feature-api: Übername des Slim-Skeleton
This commit is contained in:
@@ -21,3 +21,4 @@
|
|||||||
/fuel/app/cache/*/*
|
/fuel/app/cache/*/*
|
||||||
/fuel/app/config/crypt.php
|
/fuel/app/config/crypt.php
|
||||||
|
|
||||||
|
.idea
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
coverage/
|
||||||
|
vendor/
|
||||||
|
logs/*
|
||||||
|
!/logs/README.md
|
||||||
|
.phpunit.result.cache
|
||||||
|
/composer.lock
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use TorstenHettstedt\OptLogin\Application\Settings\SettingsInterface;
|
||||||
|
use DI\ContainerBuilder;
|
||||||
|
use Monolog\Handler\StreamHandler;
|
||||||
|
use Monolog\Logger;
|
||||||
|
use Monolog\Processor\UidProcessor;
|
||||||
|
use Psr\Container\ContainerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
return function (ContainerBuilder $containerBuilder) {
|
||||||
|
$containerBuilder->addDefinitions([
|
||||||
|
LoggerInterface::class => function (ContainerInterface $c) {
|
||||||
|
$settings = $c->get(SettingsInterface::class);
|
||||||
|
|
||||||
|
$loggerSettings = $settings->get('logger');
|
||||||
|
$logger = new Logger($loggerSettings['name']);
|
||||||
|
|
||||||
|
$processor = new UidProcessor();
|
||||||
|
$logger->pushProcessor($processor);
|
||||||
|
|
||||||
|
$handler = new StreamHandler($loggerSettings['path'], $loggerSettings['level']);
|
||||||
|
$logger->pushHandler($handler);
|
||||||
|
|
||||||
|
return $logger;
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use TorstenHettstedt\OptLogin\Application\Middleware\SessionMiddleware;
|
||||||
|
use Slim\App;
|
||||||
|
|
||||||
|
return function (App $app) {
|
||||||
|
$app->add(SessionMiddleware::class);
|
||||||
|
};
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use TorstenHettstedt\OptLogin\Domain\User\UserRepository;
|
||||||
|
use TorstenHettstedt\OptLogin\Infrastructure\Persistence\User\InMemoryUserRepository;
|
||||||
|
use DI\ContainerBuilder;
|
||||||
|
|
||||||
|
return function (ContainerBuilder $containerBuilder) {
|
||||||
|
// Here we map our UserRepository interface to its in memory implementation
|
||||||
|
$containerBuilder->addDefinitions([
|
||||||
|
UserRepository::class => \DI\autowire(InMemoryUserRepository::class),
|
||||||
|
]);
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use TorstenHettstedt\OptLogin\Application\Actions\User\ListUsersAction;
|
||||||
|
use TorstenHettstedt\OptLogin\Application\Actions\User\ViewUserAction;
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Slim\App;
|
||||||
|
use Slim\Interfaces\RouteCollectorProxyInterface as Group;
|
||||||
|
|
||||||
|
return function (App $app) {
|
||||||
|
$app->options('/{routes:.*}', function (Request $request, Response $response) {
|
||||||
|
// CORS Pre-Flight OPTIONS Request Handler
|
||||||
|
return $response;
|
||||||
|
});
|
||||||
|
|
||||||
|
$app->get('/', function (Request $request, Response $response) {
|
||||||
|
$response->getBody()->write('Hello world!');
|
||||||
|
return $response;
|
||||||
|
});
|
||||||
|
|
||||||
|
$app->group('/users', function (Group $group) {
|
||||||
|
$group->get('', ListUsersAction::class);
|
||||||
|
$group->get('/{id}', ViewUserAction::class);
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use TorstenHettstedt\OptLogin\Application\Settings\Settings;
|
||||||
|
use TorstenHettstedt\OptLogin\Application\Settings\SettingsInterface;
|
||||||
|
use DI\ContainerBuilder;
|
||||||
|
use Monolog\Logger;
|
||||||
|
|
||||||
|
return function (ContainerBuilder $containerBuilder) {
|
||||||
|
|
||||||
|
// Global Settings Object
|
||||||
|
$containerBuilder->addDefinitions([
|
||||||
|
SettingsInterface::class => function () {
|
||||||
|
return new Settings([
|
||||||
|
'displayErrorDetails' => true, // Should be set to false in production
|
||||||
|
'logError' => false,
|
||||||
|
'logErrorDetails' => false,
|
||||||
|
'logger' => [
|
||||||
|
'name' => 'slim-app',
|
||||||
|
'path' => isset($_ENV['docker']) ? 'php://stdout' : __DIR__ . '/../logs/app.log',
|
||||||
|
'level' => Logger::DEBUG,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
paths:
|
||||||
|
tests: tests
|
||||||
|
output: tests/_output
|
||||||
|
data: tests/_data
|
||||||
|
support: tests/_support
|
||||||
|
envs: tests/_envs
|
||||||
|
actor_suffix: Tester
|
||||||
|
extensions:
|
||||||
|
enabled:
|
||||||
|
- Codeception\Extension\RunFailed
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"name": "torsten-hettstedt/otp-login",
|
||||||
|
"description": "In Vereinen ist es wichtig, Helfern für eine bestimmte Zeit CRUD-Rechte zu geben. Es ist meist nicht notwendig, dass der Helfer dazu unbedingt einen Account benötigt. Das genaue Vorgehen wird in der Readme bzw. per UML festgelegt.",
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"keywords": [
|
||||||
|
"microframework",
|
||||||
|
"rest",
|
||||||
|
"router",
|
||||||
|
"psr7"
|
||||||
|
],
|
||||||
|
"license": "GPL-3.0-or-later",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Torsten Lucke",
|
||||||
|
"email": "arbeit@torsten-hettstedt.de"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"php": "^8.0",
|
||||||
|
"ext-pdo": "*",
|
||||||
|
"ext-json": "*",
|
||||||
|
"monolog/monolog": "^2.4",
|
||||||
|
"php-di/php-di": "^6.3",
|
||||||
|
"slim/psr7": "^1.5",
|
||||||
|
"slim/slim": "^4.10",
|
||||||
|
"php-di/slim-bridge": "^3.1.0",
|
||||||
|
"jetbrains/phpstorm-attributes": "^1.0.0",
|
||||||
|
"myclabs/php-enum": "^1.8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpstan/phpstan": "^0.12.80",
|
||||||
|
"codeception/codeception": "^4.1.18",
|
||||||
|
"codeception/module-phpbrowser": "^1.0.0",
|
||||||
|
"codeception/module-asserts": "^1.0.0",
|
||||||
|
"codeception/module-db": "^1.1.0",
|
||||||
|
"codeception/module-rest": "^1.2.8"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"process-timeout": 0,
|
||||||
|
"sort-packages": true,
|
||||||
|
"allow-plugins": {
|
||||||
|
"phpstan/extension-installer": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"TorstenHettstedt\\OptLogin\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"TorstenHettstedt\\OptLogin\\Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "php -S localhost:8080 -t public",
|
||||||
|
"test": "vendor/bin/codecept run unit"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
version: '3.7'
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
logs:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
services:
|
||||||
|
slim:
|
||||||
|
image: php:7-alpine
|
||||||
|
working_dir: /var/www
|
||||||
|
command: php -S 0.0.0.0:8080 -t public
|
||||||
|
environment:
|
||||||
|
docker: "true"
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
volumes:
|
||||||
|
- .:/var/www
|
||||||
|
- logs:/var/www/logs
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Your Slim Framework application's log files will be written to this directory.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
parameters:
|
||||||
|
level: 6
|
||||||
|
paths:
|
||||||
|
- src
|
||||||
|
- tests/unit
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
Options All -Indexes
|
||||||
|
|
||||||
|
<Files .htaccess>
|
||||||
|
order allow,deny
|
||||||
|
deny from all
|
||||||
|
</Files>
|
||||||
|
|
||||||
|
<IfModule mod_rewrite.c>
|
||||||
|
RewriteEngine On
|
||||||
|
|
||||||
|
# Redirect to HTTPS
|
||||||
|
# RewriteEngine On
|
||||||
|
# RewriteCond %{HTTPS} off
|
||||||
|
# RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
|
||||||
|
|
||||||
|
# Some hosts may require you to use the `RewriteBase` directive.
|
||||||
|
# Determine the RewriteBase automatically and set it as environment variable.
|
||||||
|
# If you are using Apache aliases to do mass virtual hosting or installed the
|
||||||
|
# project in a subdirectory, the base path will be prepended to allow proper
|
||||||
|
# resolution of the index.php file and to redirect to the correct URI. It will
|
||||||
|
# work in environments without path prefix as well, providing a safe, one-size
|
||||||
|
# fits all solution. But as you do not need it in this case, you can comment
|
||||||
|
# the following 2 lines to eliminate the overhead.
|
||||||
|
RewriteCond %{REQUEST_URI}::$1 ^(/.+)/(.*)::\2$
|
||||||
|
RewriteRule ^(.*) - [E=BASE:%1]
|
||||||
|
|
||||||
|
# If the above doesn't work you might need to set the `RewriteBase` directive manually, it should be the
|
||||||
|
# absolute physical path to the directory that contains this htaccess file.
|
||||||
|
# RewriteBase /
|
||||||
|
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteRule ^ index.php [QSA,L]
|
||||||
|
</IfModule>
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use TorstenHettstedt\OptLogin\Application\Handlers\HttpErrorHandler;
|
||||||
|
use TorstenHettstedt\OptLogin\Application\Handlers\ShutdownHandler;
|
||||||
|
use TorstenHettstedt\OptLogin\Application\ResponseEmitter\ResponseEmitter;
|
||||||
|
use TorstenHettstedt\OptLogin\Application\Settings\SettingsInterface;
|
||||||
|
use DI\ContainerBuilder;
|
||||||
|
use Slim\Factory\AppFactory;
|
||||||
|
use Slim\Factory\ServerRequestCreatorFactory;
|
||||||
|
|
||||||
|
require __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
// Instantiate PHP-DI ContainerBuilder
|
||||||
|
$containerBuilder = new ContainerBuilder();
|
||||||
|
|
||||||
|
if (false) { // Should be set to true in production
|
||||||
|
/** @noinspection PhpUnreachableStatementInspection */
|
||||||
|
$containerBuilder->enableCompilation(__DIR__ . '/../var/cache');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up settings
|
||||||
|
$settings = require __DIR__ . '/../app/settings.php';
|
||||||
|
$settings($containerBuilder);
|
||||||
|
|
||||||
|
// Set up dependencies
|
||||||
|
$dependencies = require __DIR__ . '/../app/dependencies.php';
|
||||||
|
$dependencies($containerBuilder);
|
||||||
|
|
||||||
|
// Set up repositories
|
||||||
|
$repositories = require __DIR__ . '/../app/repositories.php';
|
||||||
|
$repositories($containerBuilder);
|
||||||
|
|
||||||
|
// Build PHP-DI Container instance
|
||||||
|
$container = $containerBuilder->build();
|
||||||
|
|
||||||
|
// Instantiate the app
|
||||||
|
AppFactory::setContainer($container);
|
||||||
|
$app = AppFactory::create();
|
||||||
|
$callableResolver = $app->getCallableResolver();
|
||||||
|
|
||||||
|
// Register middleware
|
||||||
|
$middleware = require __DIR__ . '/../app/middleware.php';
|
||||||
|
$middleware($app);
|
||||||
|
|
||||||
|
// Register routes
|
||||||
|
$routes = require __DIR__ . '/../app/routes.php';
|
||||||
|
$routes($app);
|
||||||
|
|
||||||
|
/** @var SettingsInterface $settings */
|
||||||
|
$settings = $container->get(SettingsInterface::class);
|
||||||
|
|
||||||
|
$displayErrorDetails = $settings->get('displayErrorDetails');
|
||||||
|
$logError = $settings->get('logError');
|
||||||
|
$logErrorDetails = $settings->get('logErrorDetails');
|
||||||
|
|
||||||
|
// Create Request object from globals
|
||||||
|
$serverRequestCreator = ServerRequestCreatorFactory::create();
|
||||||
|
$request = $serverRequestCreator->createServerRequestFromGlobals();
|
||||||
|
|
||||||
|
// Create Error Handler
|
||||||
|
$responseFactory = $app->getResponseFactory();
|
||||||
|
$errorHandler = new HttpErrorHandler($callableResolver, $responseFactory);
|
||||||
|
|
||||||
|
// Create Shutdown Handler
|
||||||
|
$shutdownHandler = new ShutdownHandler($request, $errorHandler, $displayErrorDetails);
|
||||||
|
register_shutdown_function($shutdownHandler);
|
||||||
|
|
||||||
|
// Add Routing Middleware
|
||||||
|
$app->addRoutingMiddleware();
|
||||||
|
|
||||||
|
// Add Body Parsing Middleware
|
||||||
|
$app->addBodyParsingMiddleware();
|
||||||
|
|
||||||
|
// Add Error Middleware
|
||||||
|
$errorMiddleware = $app->addErrorMiddleware($displayErrorDetails, $logError, $logErrorDetails);
|
||||||
|
$errorMiddleware->setDefaultErrorHandler($errorHandler);
|
||||||
|
|
||||||
|
// Run App & Emit Response
|
||||||
|
$response = $app->handle($request);
|
||||||
|
$responseEmitter = new ResponseEmitter();
|
||||||
|
$responseEmitter->emit($response);
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\OptLogin\Application\Actions;
|
||||||
|
|
||||||
|
use TorstenHettstedt\OptLogin\Domain\DomainException\DomainRecordNotFoundException;
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Slim\Exception\HttpBadRequestException;
|
||||||
|
use Slim\Exception\HttpNotFoundException;
|
||||||
|
|
||||||
|
abstract class Action
|
||||||
|
{
|
||||||
|
protected LoggerInterface $logger;
|
||||||
|
|
||||||
|
protected Request $request;
|
||||||
|
|
||||||
|
protected Response $response;
|
||||||
|
|
||||||
|
protected array $args;
|
||||||
|
|
||||||
|
public function __construct(LoggerInterface $logger)
|
||||||
|
{
|
||||||
|
$this->logger = $logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws HttpNotFoundException
|
||||||
|
* @throws HttpBadRequestException
|
||||||
|
*/
|
||||||
|
public function __invoke(Request $request, Response $response, array $args): Response
|
||||||
|
{
|
||||||
|
$this->request = $request;
|
||||||
|
$this->response = $response;
|
||||||
|
$this->args = $args;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return $this->action();
|
||||||
|
} catch (DomainRecordNotFoundException $e) {
|
||||||
|
throw new HttpNotFoundException($this->request, $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws DomainRecordNotFoundException
|
||||||
|
* @throws HttpBadRequestException
|
||||||
|
*/
|
||||||
|
abstract protected function action(): Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array|object
|
||||||
|
*/
|
||||||
|
protected function getFormData()
|
||||||
|
{
|
||||||
|
return $this->request->getParsedBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return mixed
|
||||||
|
* @throws HttpBadRequestException
|
||||||
|
*/
|
||||||
|
protected function resolveArg(string $name)
|
||||||
|
{
|
||||||
|
if (!isset($this->args[$name])) {
|
||||||
|
throw new HttpBadRequestException($this->request, "Could not resolve argument `{$name}`.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->args[$name];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array|object|null $data
|
||||||
|
*/
|
||||||
|
protected function respondWithData($data = null, int $statusCode = 200): Response
|
||||||
|
{
|
||||||
|
$payload = new ActionPayload($statusCode, $data);
|
||||||
|
|
||||||
|
return $this->respond($payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function respond(ActionPayload $payload): Response
|
||||||
|
{
|
||||||
|
$json = json_encode($payload, JSON_PRETTY_PRINT);
|
||||||
|
$this->response->getBody()->write($json);
|
||||||
|
|
||||||
|
return $this->response
|
||||||
|
->withHeader('Content-Type', 'application/json')
|
||||||
|
->withStatus($payload->getStatusCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\OptLogin\Application\Actions;
|
||||||
|
|
||||||
|
use JetBrains\PhpStorm\ArrayShape;
|
||||||
|
use JsonSerializable;
|
||||||
|
|
||||||
|
class ActionError implements JsonSerializable
|
||||||
|
{
|
||||||
|
public const BAD_REQUEST = 'BAD_REQUEST';
|
||||||
|
public const INSUFFICIENT_PRIVILEGES = 'INSUFFICIENT_PRIVILEGES';
|
||||||
|
public const NOT_ALLOWED = 'NOT_ALLOWED';
|
||||||
|
public const NOT_IMPLEMENTED = 'NOT_IMPLEMENTED';
|
||||||
|
public const RESOURCE_NOT_FOUND = 'RESOURCE_NOT_FOUND';
|
||||||
|
public const SERVER_ERROR = 'SERVER_ERROR';
|
||||||
|
public const UNAUTHENTICATED = 'UNAUTHENTICATED';
|
||||||
|
public const VALIDATION_ERROR = 'VALIDATION_ERROR';
|
||||||
|
public const VERIFICATION_ERROR = 'VERIFICATION_ERROR';
|
||||||
|
|
||||||
|
private string $type;
|
||||||
|
|
||||||
|
private string $description;
|
||||||
|
|
||||||
|
public function __construct(string $type, ?string $description)
|
||||||
|
{
|
||||||
|
$this->type = $type;
|
||||||
|
$this->description = $description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getType(): string
|
||||||
|
{
|
||||||
|
return $this->type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setType(string $type): self
|
||||||
|
{
|
||||||
|
$this->type = $type;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return $this->description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDescription(?string $description = null): self
|
||||||
|
{
|
||||||
|
$this->description = $description;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ArrayShape(['type' => "string", 'description' => "null|string"])] #[\ReturnTypeWillChange]
|
||||||
|
public function jsonSerialize(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => $this->type,
|
||||||
|
'description' => $this->description,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\OptLogin\Application\Actions;
|
||||||
|
|
||||||
|
use JetBrains\PhpStorm\ArrayShape;
|
||||||
|
use JsonSerializable;
|
||||||
|
|
||||||
|
class ActionPayload implements JsonSerializable
|
||||||
|
{
|
||||||
|
private int $statusCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array|object|null
|
||||||
|
*/
|
||||||
|
private $data;
|
||||||
|
|
||||||
|
private ?ActionError $error;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
int $statusCode = 200,
|
||||||
|
$data = null,
|
||||||
|
?ActionError $error = null
|
||||||
|
) {
|
||||||
|
$this->statusCode = $statusCode;
|
||||||
|
$this->data = $data;
|
||||||
|
$this->error = $error;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStatusCode(): int
|
||||||
|
{
|
||||||
|
return $this->statusCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array|null|object
|
||||||
|
*/
|
||||||
|
public function getData()
|
||||||
|
{
|
||||||
|
return $this->data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getError(): ?ActionError
|
||||||
|
{
|
||||||
|
return $this->error;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ArrayShape(['statusCode' => "int", 'data' => "mixed", 'error' => "\App\Application\Actions\ActionError|null"])] #[\ReturnTypeWillChange]
|
||||||
|
public function jsonSerialize(): array
|
||||||
|
{
|
||||||
|
$payload = [
|
||||||
|
'statusCode' => $this->statusCode,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($this->data !== null) {
|
||||||
|
$payload['data'] = $this->data;
|
||||||
|
} elseif ($this->error !== null) {
|
||||||
|
$payload['error'] = $this->error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\OptLogin\Application\Actions\User;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
|
||||||
|
class ListUsersAction extends UserAction
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
protected function action(): Response
|
||||||
|
{
|
||||||
|
$users = $this->userRepository->findAll();
|
||||||
|
|
||||||
|
$this->logger->info("Users list was viewed.");
|
||||||
|
|
||||||
|
return $this->respondWithData($users);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\OptLogin\Application\Actions\User;
|
||||||
|
|
||||||
|
use TorstenHettstedt\OptLogin\Application\Actions\Action;
|
||||||
|
use TorstenHettstedt\OptLogin\Domain\User\UserRepository;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
abstract class UserAction extends Action
|
||||||
|
{
|
||||||
|
protected UserRepository $userRepository;
|
||||||
|
|
||||||
|
public function __construct(LoggerInterface $logger, UserRepository $userRepository)
|
||||||
|
{
|
||||||
|
parent::__construct($logger);
|
||||||
|
$this->userRepository = $userRepository;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\OptLogin\Application\Actions\User;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
|
||||||
|
class ViewUserAction extends UserAction
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
protected function action(): Response
|
||||||
|
{
|
||||||
|
$userId = (int) $this->resolveArg('id');
|
||||||
|
$user = $this->userRepository->findUserOfId($userId);
|
||||||
|
|
||||||
|
$this->logger->info("User of id `${userId}` was viewed.");
|
||||||
|
|
||||||
|
return $this->respondWithData($user);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\OptLogin\Application\Handlers;
|
||||||
|
|
||||||
|
use TorstenHettstedt\OptLogin\Application\Actions\ActionError;
|
||||||
|
use TorstenHettstedt\OptLogin\Application\Actions\ActionPayload;
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Slim\Exception\HttpBadRequestException;
|
||||||
|
use Slim\Exception\HttpException;
|
||||||
|
use Slim\Exception\HttpForbiddenException;
|
||||||
|
use Slim\Exception\HttpMethodNotAllowedException;
|
||||||
|
use Slim\Exception\HttpNotFoundException;
|
||||||
|
use Slim\Exception\HttpNotImplementedException;
|
||||||
|
use Slim\Exception\HttpUnauthorizedException;
|
||||||
|
use Slim\Handlers\ErrorHandler as SlimErrorHandler;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
class HttpErrorHandler extends SlimErrorHandler
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected function respond(): Response
|
||||||
|
{
|
||||||
|
$exception = $this->exception;
|
||||||
|
$statusCode = 500;
|
||||||
|
$error = new ActionError(
|
||||||
|
ActionError::SERVER_ERROR,
|
||||||
|
'An internal error has occurred while processing your request.'
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($exception instanceof HttpException) {
|
||||||
|
$statusCode = $exception->getCode();
|
||||||
|
$error->setDescription($exception->getMessage());
|
||||||
|
|
||||||
|
if ($exception instanceof HttpNotFoundException) {
|
||||||
|
$error->setType(ActionError::RESOURCE_NOT_FOUND);
|
||||||
|
} elseif ($exception instanceof HttpMethodNotAllowedException) {
|
||||||
|
$error->setType(ActionError::NOT_ALLOWED);
|
||||||
|
} elseif ($exception instanceof HttpUnauthorizedException) {
|
||||||
|
$error->setType(ActionError::UNAUTHENTICATED);
|
||||||
|
} elseif ($exception instanceof HttpForbiddenException) {
|
||||||
|
$error->setType(ActionError::INSUFFICIENT_PRIVILEGES);
|
||||||
|
} elseif ($exception instanceof HttpBadRequestException) {
|
||||||
|
$error->setType(ActionError::BAD_REQUEST);
|
||||||
|
} elseif ($exception instanceof HttpNotImplementedException) {
|
||||||
|
$error->setType(ActionError::NOT_IMPLEMENTED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!($exception instanceof HttpException)
|
||||||
|
&& $exception instanceof Throwable
|
||||||
|
&& $this->displayErrorDetails
|
||||||
|
) {
|
||||||
|
$error->setDescription($exception->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = new ActionPayload($statusCode, null, $error);
|
||||||
|
$encodedPayload = json_encode($payload, JSON_PRETTY_PRINT);
|
||||||
|
|
||||||
|
$response = $this->responseFactory->createResponse($statusCode);
|
||||||
|
$response->getBody()->write($encodedPayload);
|
||||||
|
|
||||||
|
return $response->withHeader('Content-Type', 'application/json');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\OptLogin\Application\Handlers;
|
||||||
|
|
||||||
|
use TorstenHettstedt\OptLogin\Application\ResponseEmitter\ResponseEmitter;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Slim\Exception\HttpInternalServerErrorException;
|
||||||
|
|
||||||
|
class ShutdownHandler
|
||||||
|
{
|
||||||
|
private Request $request;
|
||||||
|
|
||||||
|
private HttpErrorHandler $errorHandler;
|
||||||
|
|
||||||
|
private bool $displayErrorDetails;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
Request $request,
|
||||||
|
HttpErrorHandler $errorHandler,
|
||||||
|
bool $displayErrorDetails
|
||||||
|
) {
|
||||||
|
$this->request = $request;
|
||||||
|
$this->errorHandler = $errorHandler;
|
||||||
|
$this->displayErrorDetails = $displayErrorDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __invoke()
|
||||||
|
{
|
||||||
|
$error = error_get_last();
|
||||||
|
if ($error) {
|
||||||
|
$errorFile = $error['file'];
|
||||||
|
$errorLine = $error['line'];
|
||||||
|
$errorMessage = $error['message'];
|
||||||
|
$errorType = $error['type'];
|
||||||
|
$message = 'An error while processing your request. Please try again later.';
|
||||||
|
|
||||||
|
if ($this->displayErrorDetails) {
|
||||||
|
switch ($errorType) {
|
||||||
|
case E_USER_ERROR:
|
||||||
|
$message = "FATAL ERROR: {$errorMessage}. ";
|
||||||
|
$message .= " on line {$errorLine} in file {$errorFile}.";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case E_USER_WARNING:
|
||||||
|
$message = "WARNING: {$errorMessage}";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case E_USER_NOTICE:
|
||||||
|
$message = "NOTICE: {$errorMessage}";
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$message = "ERROR: {$errorMessage}";
|
||||||
|
$message .= " on line {$errorLine} in file {$errorFile}.";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$exception = new HttpInternalServerErrorException($this->request, $message);
|
||||||
|
$response = $this->errorHandler->__invoke(
|
||||||
|
$this->request,
|
||||||
|
$exception,
|
||||||
|
$this->displayErrorDetails,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
$responseEmitter = new ResponseEmitter();
|
||||||
|
$responseEmitter->emit($response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\OptLogin\Application\Middleware;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Psr\Http\Server\MiddlewareInterface as Middleware;
|
||||||
|
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
|
||||||
|
|
||||||
|
class SessionMiddleware implements Middleware
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function process(Request $request, RequestHandler $handler): Response
|
||||||
|
{
|
||||||
|
if (isset($_SERVER['HTTP_AUTHORIZATION'])) {
|
||||||
|
session_start();
|
||||||
|
$request = $request->withAttribute('session', $_SESSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $handler->handle($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\OptLogin\Application\ResponseEmitter;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Slim\ResponseEmitter as SlimResponseEmitter;
|
||||||
|
|
||||||
|
class ResponseEmitter extends SlimResponseEmitter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function emit(ResponseInterface $response): void
|
||||||
|
{
|
||||||
|
// This variable should be set to the allowed host from which your API can be accessed with
|
||||||
|
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
||||||
|
|
||||||
|
$response = $response
|
||||||
|
->withHeader('Access-Control-Allow-Credentials', 'true')
|
||||||
|
->withHeader('Access-Control-Allow-Origin', $origin)
|
||||||
|
->withHeader(
|
||||||
|
'Access-Control-Allow-Headers',
|
||||||
|
'X-Requested-With, Content-Type, Accept, Origin, Authorization',
|
||||||
|
)
|
||||||
|
->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS')
|
||||||
|
->withHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
||||||
|
->withAddedHeader('Cache-Control', 'post-check=0, pre-check=0')
|
||||||
|
->withHeader('Pragma', 'no-cache');
|
||||||
|
|
||||||
|
if (ob_get_contents()) {
|
||||||
|
ob_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::emit($response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\OptLogin\Application\Settings;
|
||||||
|
|
||||||
|
class Settings implements SettingsInterface
|
||||||
|
{
|
||||||
|
private array $settings;
|
||||||
|
|
||||||
|
public function __construct(array $settings)
|
||||||
|
{
|
||||||
|
$this->settings = $settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function get(string $key = '')
|
||||||
|
{
|
||||||
|
return (empty($key)) ? $this->settings : $this->settings[$key];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\OptLogin\Application\Settings;
|
||||||
|
|
||||||
|
interface SettingsInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param string $key
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function get(string $key = '');
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\OptLogin\Domain\DomainException;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
abstract class DomainException extends Exception
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\OptLogin\Domain\DomainException;
|
||||||
|
|
||||||
|
class DomainRecordNotFoundException extends DomainException
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\OptLogin\Domain\User;
|
||||||
|
|
||||||
|
use JetBrains\PhpStorm\ArrayShape;
|
||||||
|
use JsonSerializable;
|
||||||
|
|
||||||
|
class User implements JsonSerializable
|
||||||
|
{
|
||||||
|
private ?int $id;
|
||||||
|
|
||||||
|
private string $username;
|
||||||
|
|
||||||
|
private string $firstName;
|
||||||
|
|
||||||
|
private string $lastName;
|
||||||
|
|
||||||
|
public function __construct(?int $id, string $username, string $firstName, string $lastName)
|
||||||
|
{
|
||||||
|
$this->id = $id;
|
||||||
|
$this->username = strtolower($username);
|
||||||
|
$this->firstName = ucfirst($firstName);
|
||||||
|
$this->lastName = ucfirst($lastName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUsername(): string
|
||||||
|
{
|
||||||
|
return $this->username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFirstName(): string
|
||||||
|
{
|
||||||
|
return $this->firstName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLastName(): string
|
||||||
|
{
|
||||||
|
return $this->lastName;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ArrayShape(['id' => "int|null", 'username' => "string", 'firstName' => "string", 'lastName' => "string"])] #[\ReturnTypeWillChange]
|
||||||
|
public function jsonSerialize(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'username' => $this->username,
|
||||||
|
'firstName' => $this->firstName,
|
||||||
|
'lastName' => $this->lastName,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\OptLogin\Domain\User;
|
||||||
|
|
||||||
|
use TorstenHettstedt\OptLogin\Domain\DomainException\DomainRecordNotFoundException;
|
||||||
|
|
||||||
|
class UserNotFoundException extends DomainRecordNotFoundException
|
||||||
|
{
|
||||||
|
public $message = 'The user you requested does not exist.';
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\OptLogin\Domain\User;
|
||||||
|
|
||||||
|
interface UserRepository
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return User[]
|
||||||
|
*/
|
||||||
|
public function findAll(): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $id
|
||||||
|
* @return User
|
||||||
|
* @throws UserNotFoundException
|
||||||
|
*/
|
||||||
|
public function findUserOfId(int $id): User;
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\OptLogin\Infrastructure\Persistence\User;
|
||||||
|
|
||||||
|
use TorstenHettstedt\OptLogin\Domain\User\User;
|
||||||
|
use TorstenHettstedt\OptLogin\Domain\User\UserNotFoundException;
|
||||||
|
use TorstenHettstedt\OptLogin\Domain\User\UserRepository;
|
||||||
|
|
||||||
|
class InMemoryUserRepository implements UserRepository
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var User[]
|
||||||
|
*/
|
||||||
|
private array $users;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param User[]|null $users
|
||||||
|
*/
|
||||||
|
public function __construct(array $users = null)
|
||||||
|
{
|
||||||
|
$this->users = $users ?? [
|
||||||
|
1 => new User(1, 'bill.gates', 'Bill', 'Gates'),
|
||||||
|
2 => new User(2, 'steve.jobs', 'Steve', 'Jobs'),
|
||||||
|
3 => new User(3, 'mark.zuckerberg', 'Mark', 'Zuckerberg'),
|
||||||
|
4 => new User(4, 'evan.spiegel', 'Evan', 'Spiegel'),
|
||||||
|
5 => new User(5, 'jack.dorsey', 'Jack', 'Dorsey'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function findAll(): array
|
||||||
|
{
|
||||||
|
return array_values($this->users);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function findUserOfId(int $id): User
|
||||||
|
{
|
||||||
|
if (!isset($this->users[$id])) {
|
||||||
|
throw new UserNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->users[$id];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
namespace Helper;
|
||||||
|
|
||||||
|
// here you can define custom actions
|
||||||
|
// all public methods declared in helper class will be available in $I
|
||||||
|
|
||||||
|
class Unit extends \Codeception\Module
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inherited Methods
|
||||||
|
* @method void wantToTest($text)
|
||||||
|
* @method void wantTo($text)
|
||||||
|
* @method void execute($callable)
|
||||||
|
* @method void expectTo($prediction)
|
||||||
|
* @method void expect($prediction)
|
||||||
|
* @method void amGoingTo($argumentation)
|
||||||
|
* @method void am($role)
|
||||||
|
* @method void lookForwardTo($achieveValue)
|
||||||
|
* @method void comment($description)
|
||||||
|
* @method void pause()
|
||||||
|
*
|
||||||
|
* @SuppressWarnings(PHPMD)
|
||||||
|
*/
|
||||||
|
class UnitTester extends \Codeception\Actor
|
||||||
|
{
|
||||||
|
use _generated\UnitTesterActions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define custom actions here
|
||||||
|
*/
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# Codeception Test Suite Configuration
|
||||||
|
#
|
||||||
|
# Suite for unit or integration tests.
|
||||||
|
|
||||||
|
actor: UnitTester
|
||||||
|
modules:
|
||||||
|
enabled:
|
||||||
|
- Asserts
|
||||||
|
- \Helper\Unit
|
||||||
|
step_decorators: ~
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\OptLogin\Tests\unit\Application\Actions;
|
||||||
|
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use Exception;
|
||||||
|
use Psr\Container\ContainerExceptionInterface;
|
||||||
|
use Psr\Container\NotFoundExceptionInterface;
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use TorstenHettstedt\OptLogin\Application\Actions\Action;
|
||||||
|
use TorstenHettstedt\OptLogin\Application\Actions\ActionPayload;
|
||||||
|
use TorstenHettstedt\OptLogin\Tests\unit\TestCase;
|
||||||
|
|
||||||
|
class ActionTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @throws ContainerExceptionInterface
|
||||||
|
* @throws NotFoundExceptionInterface
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function testActionSetsHttpCodeInRespond()
|
||||||
|
{
|
||||||
|
$app = $this->getAppInstance();
|
||||||
|
$container = $app->getContainer();
|
||||||
|
$logger = $container->get(LoggerInterface::class);
|
||||||
|
|
||||||
|
$testAction = new class ($logger) extends Action {
|
||||||
|
public function __construct(
|
||||||
|
LoggerInterface $loggerInterface
|
||||||
|
) {
|
||||||
|
parent::__construct($loggerInterface);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function action(): Response
|
||||||
|
{
|
||||||
|
return $this->respond(
|
||||||
|
new ActionPayload(
|
||||||
|
202,
|
||||||
|
[
|
||||||
|
'willBeDoneAt' => (new DateTimeImmutable())->format(DateTimeInterface::ATOM)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$app->get('/test-action-response-code', $testAction);
|
||||||
|
$request = $this->createRequest('GET', '/test-action-response-code');
|
||||||
|
$response = $app->handle($request);
|
||||||
|
|
||||||
|
$this->assertEquals(202, $response->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws ContainerExceptionInterface
|
||||||
|
* @throws NotFoundExceptionInterface
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function testActionSetsHttpCodeRespondData()
|
||||||
|
{
|
||||||
|
$app = $this->getAppInstance();
|
||||||
|
$container = $app->getContainer();
|
||||||
|
$logger = $container->get(LoggerInterface::class);
|
||||||
|
|
||||||
|
$testAction = new class ($logger) extends Action {
|
||||||
|
public function __construct(
|
||||||
|
LoggerInterface $loggerInterface
|
||||||
|
) {
|
||||||
|
parent::__construct($loggerInterface);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function action(): Response
|
||||||
|
{
|
||||||
|
return $this->respondWithData(
|
||||||
|
[
|
||||||
|
'willBeDoneAt' => (new DateTimeImmutable())->format(DateTimeInterface::ATOM)
|
||||||
|
],
|
||||||
|
202
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$app->get('/test-action-response-code', $testAction);
|
||||||
|
$request = $this->createRequest('GET', '/test-action-response-code');
|
||||||
|
$response = $app->handle($request);
|
||||||
|
|
||||||
|
$this->assertEquals(202, $response->getStatusCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\OptLogin\Tests\unit\Application\Actions\User;
|
||||||
|
|
||||||
|
use Codeception\Stub\Expected;
|
||||||
|
use DI\Container;
|
||||||
|
use Exception;
|
||||||
|
use TorstenHettstedt\OptLogin\Application\Actions\ActionPayload;
|
||||||
|
use TorstenHettstedt\OptLogin\Domain\User\User;
|
||||||
|
use TorstenHettstedt\OptLogin\Domain\User\UserRepository;
|
||||||
|
use TorstenHettstedt\OptLogin\Tests\unit\TestCase;
|
||||||
|
|
||||||
|
class ListUserActionTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function testAction()
|
||||||
|
{
|
||||||
|
$app = $this->getAppInstance();
|
||||||
|
|
||||||
|
/** @var Container $container */
|
||||||
|
$container = $app->getContainer();
|
||||||
|
|
||||||
|
$user = new User(1, 'bill.gates', 'Bill', 'Gates');
|
||||||
|
|
||||||
|
$userRepository = $this->makeEmpty(UserRepository::class, [
|
||||||
|
'findAll' => Expected::once([$user])
|
||||||
|
]);
|
||||||
|
|
||||||
|
$container->set(UserRepository::class, $userRepository);
|
||||||
|
|
||||||
|
$request = $this->createRequest('GET', '/users');
|
||||||
|
$response = $app->handle($request);
|
||||||
|
|
||||||
|
$payload = (string) $response->getBody();
|
||||||
|
$expectedPayload = new ActionPayload(200, [$user]);
|
||||||
|
$serializedPayload = json_encode($expectedPayload, JSON_PRETTY_PRINT);
|
||||||
|
|
||||||
|
$this->assertEquals($serializedPayload, $payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\OptLogin\Tests\unit\Application\Actions\User;
|
||||||
|
|
||||||
|
use Codeception\Stub\Expected;
|
||||||
|
use DI\Container;
|
||||||
|
use Exception;
|
||||||
|
use Slim\Middleware\ErrorMiddleware;
|
||||||
|
use TorstenHettstedt\OptLogin\Application\Actions\ActionError;
|
||||||
|
use TorstenHettstedt\OptLogin\Application\Actions\ActionPayload;
|
||||||
|
use TorstenHettstedt\OptLogin\Application\Handlers\HttpErrorHandler;
|
||||||
|
use TorstenHettstedt\OptLogin\Domain\User\User;
|
||||||
|
use TorstenHettstedt\OptLogin\Domain\User\UserNotFoundException;
|
||||||
|
use TorstenHettstedt\OptLogin\Domain\User\UserRepository;
|
||||||
|
use TorstenHettstedt\OptLogin\Tests\unit\TestCase;
|
||||||
|
|
||||||
|
class ViewUserActionTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function testAction()
|
||||||
|
{
|
||||||
|
$app = $this->getAppInstance();
|
||||||
|
|
||||||
|
/** @var Container $container */
|
||||||
|
$container = $app->getContainer();
|
||||||
|
|
||||||
|
$user = new User(1, 'bill.gates', 'Bill', 'Gates');
|
||||||
|
|
||||||
|
$userRepository = $this->makeEmpty(UserRepository::class, [
|
||||||
|
'findUserOfId' => Expected::once($user)
|
||||||
|
]);
|
||||||
|
|
||||||
|
$container->set(UserRepository::class, $userRepository);
|
||||||
|
|
||||||
|
$request = $this->createRequest('GET', '/users/1');
|
||||||
|
$response = $app->handle($request);
|
||||||
|
|
||||||
|
$payload = (string) $response->getBody();
|
||||||
|
$expectedPayload = new ActionPayload(200, $user);
|
||||||
|
$serializedPayload = json_encode($expectedPayload, JSON_PRETTY_PRINT);
|
||||||
|
|
||||||
|
$this->assertEquals($serializedPayload, $payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function testActionThrowsUserNotFoundException()
|
||||||
|
{
|
||||||
|
$app = $this->getAppInstance();
|
||||||
|
|
||||||
|
$callableResolver = $app->getCallableResolver();
|
||||||
|
$responseFactory = $app->getResponseFactory();
|
||||||
|
|
||||||
|
$errorHandler = new HttpErrorHandler($callableResolver, $responseFactory);
|
||||||
|
$errorMiddleware = new ErrorMiddleware($callableResolver, $responseFactory, true, false, false);
|
||||||
|
$errorMiddleware->setDefaultErrorHandler($errorHandler);
|
||||||
|
|
||||||
|
$app->add($errorMiddleware);
|
||||||
|
|
||||||
|
/** @var Container $container */
|
||||||
|
$container = $app->getContainer();
|
||||||
|
|
||||||
|
$userRepository = $this->makeEmpty(UserRepository::class, [
|
||||||
|
'findUserOfId' => Expected::once(function() {throw new UserNotFoundException();})
|
||||||
|
]);
|
||||||
|
|
||||||
|
$container->set(UserRepository::class, $userRepository);
|
||||||
|
|
||||||
|
$request = $this->createRequest('GET', '/users/1');
|
||||||
|
$response = $app->handle($request);
|
||||||
|
|
||||||
|
$payload = (string) $response->getBody();
|
||||||
|
$expectedError = new ActionError(ActionError::RESOURCE_NOT_FOUND, 'The user you requested does not exist.');
|
||||||
|
$expectedPayload = new ActionPayload(404, null, $expectedError);
|
||||||
|
$serializedPayload = json_encode($expectedPayload, JSON_PRETTY_PRINT);
|
||||||
|
|
||||||
|
$this->assertEquals($serializedPayload, $payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\OptLogin\Tests\unit\Domain\User;
|
||||||
|
|
||||||
|
use TorstenHettstedt\OptLogin\Domain\User\User;
|
||||||
|
use TorstenHettstedt\OptLogin\Tests\unit\TestCase;
|
||||||
|
|
||||||
|
class UserTest extends TestCase
|
||||||
|
{
|
||||||
|
public function userProvider(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[1, 'bill.gates', 'Bill', 'Gates'],
|
||||||
|
[2, 'steve.jobs', 'Steve', 'Jobs'],
|
||||||
|
[3, 'mark.zuckerberg', 'Mark', 'Zuckerberg'],
|
||||||
|
[4, 'evan.spiegel', 'Evan', 'Spiegel'],
|
||||||
|
[5, 'jack.dorsey', 'Jack', 'Dorsey'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider userProvider
|
||||||
|
* @param int $id
|
||||||
|
* @param string $username
|
||||||
|
* @param string $firstName
|
||||||
|
* @param string $lastName
|
||||||
|
*/
|
||||||
|
public function testGetters(int $id, string $username, string $firstName, string $lastName)
|
||||||
|
{
|
||||||
|
$user = new User($id, $username, $firstName, $lastName);
|
||||||
|
|
||||||
|
$this->assertEquals($id, $user->getId());
|
||||||
|
$this->assertEquals($username, $user->getUsername());
|
||||||
|
$this->assertEquals($firstName, $user->getFirstName());
|
||||||
|
$this->assertEquals($lastName, $user->getLastName());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider userProvider
|
||||||
|
* @param int $id
|
||||||
|
* @param string $username
|
||||||
|
* @param string $firstName
|
||||||
|
* @param string $lastName
|
||||||
|
*/
|
||||||
|
public function testJsonSerialize(int $id, string $username, string $firstName, string $lastName)
|
||||||
|
{
|
||||||
|
$user = new User($id, $username, $firstName, $lastName);
|
||||||
|
|
||||||
|
$expectedPayload = json_encode([
|
||||||
|
'id' => $id,
|
||||||
|
'username' => $username,
|
||||||
|
'firstName' => $firstName,
|
||||||
|
'lastName' => $lastName,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals($expectedPayload, json_encode($user));
|
||||||
|
}
|
||||||
|
}
|
||||||
+56
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\OptLogin\Tests\unit\Infrastructure\Persistence\User;
|
||||||
|
|
||||||
|
use TorstenHettstedt\OptLogin\Domain\User\User;
|
||||||
|
use TorstenHettstedt\OptLogin\Domain\User\UserNotFoundException;
|
||||||
|
use TorstenHettstedt\OptLogin\Infrastructure\Persistence\User\InMemoryUserRepository;
|
||||||
|
use TorstenHettstedt\OptLogin\Tests\unit\TestCase;
|
||||||
|
|
||||||
|
class InMemoryUserRepositoryTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testFindAll()
|
||||||
|
{
|
||||||
|
$user = new User(1, 'bill.gates', 'Bill', 'Gates');
|
||||||
|
|
||||||
|
$userRepository = new InMemoryUserRepository([1 => $user]);
|
||||||
|
|
||||||
|
$this->assertEquals([$user], $userRepository->findAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFindAllUsersByDefault()
|
||||||
|
{
|
||||||
|
$users = [
|
||||||
|
1 => new User(1, 'bill.gates', 'Bill', 'Gates'),
|
||||||
|
2 => new User(2, 'steve.jobs', 'Steve', 'Jobs'),
|
||||||
|
3 => new User(3, 'mark.zuckerberg', 'Mark', 'Zuckerberg'),
|
||||||
|
4 => new User(4, 'evan.spiegel', 'Evan', 'Spiegel'),
|
||||||
|
5 => new User(5, 'jack.dorsey', 'Jack', 'Dorsey'),
|
||||||
|
];
|
||||||
|
|
||||||
|
$userRepository = new InMemoryUserRepository();
|
||||||
|
|
||||||
|
$this->assertEquals(array_values($users), $userRepository->findAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws UserNotFoundException
|
||||||
|
*/
|
||||||
|
public function testFindUserOfId()
|
||||||
|
{
|
||||||
|
$user = new User(1, 'bill.gates', 'Bill', 'Gates');
|
||||||
|
|
||||||
|
$userRepository = new InMemoryUserRepository([1 => $user]);
|
||||||
|
|
||||||
|
$this->assertEquals($user, $userRepository->findUserOfId(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFindUserOfIdThrowsNotFoundException()
|
||||||
|
{
|
||||||
|
$userRepository = new InMemoryUserRepository([]);
|
||||||
|
$this->expectException(UserNotFoundException::class);
|
||||||
|
$userRepository->findUserOfId(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\OptLogin\Tests\unit;
|
||||||
|
|
||||||
|
use Codeception\Test\Unit;
|
||||||
|
use DI\ContainerBuilder;
|
||||||
|
use Exception;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Slim\App;
|
||||||
|
use Slim\Factory\AppFactory;
|
||||||
|
use Slim\Psr7\Factory\StreamFactory;
|
||||||
|
use Slim\Psr7\Headers;
|
||||||
|
use Slim\Psr7\Request as SlimRequest;
|
||||||
|
use Slim\Psr7\Uri;
|
||||||
|
|
||||||
|
class TestCase extends Unit
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return App
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
protected function getAppInstance(): App
|
||||||
|
{
|
||||||
|
// Instantiate PHP-DI ContainerBuilder
|
||||||
|
$containerBuilder = new ContainerBuilder();
|
||||||
|
|
||||||
|
$app_folder = __DIR__ . '/../../app';
|
||||||
|
|
||||||
|
// Container intentionally not compiled for tests.
|
||||||
|
|
||||||
|
// Set up settings
|
||||||
|
$settings = require $app_folder . '/settings.php';
|
||||||
|
$settings($containerBuilder);
|
||||||
|
|
||||||
|
// Set up dependencies
|
||||||
|
$dependencies = require $app_folder . '/dependencies.php';
|
||||||
|
$dependencies($containerBuilder);
|
||||||
|
|
||||||
|
// Set up repositories
|
||||||
|
$repositories = require $app_folder . '/repositories.php';
|
||||||
|
$repositories($containerBuilder);
|
||||||
|
|
||||||
|
// Build PHP-DI Container instance
|
||||||
|
$container = $containerBuilder->build();
|
||||||
|
|
||||||
|
// Instantiate the app
|
||||||
|
AppFactory::setContainer($container);
|
||||||
|
$app = AppFactory::create();
|
||||||
|
|
||||||
|
// Register middleware
|
||||||
|
$middleware = require $app_folder . '/middleware.php';
|
||||||
|
$middleware($app);
|
||||||
|
|
||||||
|
// Register routes
|
||||||
|
$routes = require $app_folder . '/routes.php';
|
||||||
|
$routes($app);
|
||||||
|
|
||||||
|
return $app;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $method
|
||||||
|
* @param string $path
|
||||||
|
* @param array $headers
|
||||||
|
* @param array $cookies
|
||||||
|
* @param array $serverParams
|
||||||
|
* @return Request
|
||||||
|
*/
|
||||||
|
protected function createRequest(
|
||||||
|
string $method,
|
||||||
|
string $path,
|
||||||
|
array $headers = ['HTTP_ACCEPT' => 'application/json'],
|
||||||
|
array $cookies = [],
|
||||||
|
array $serverParams = []
|
||||||
|
): Request {
|
||||||
|
$uri = new Uri('', '', 80, $path);
|
||||||
|
$handle = fopen('php://temp', 'w+');
|
||||||
|
$stream = (new StreamFactory())->createStreamFromResource($handle);
|
||||||
|
|
||||||
|
$h = new Headers();
|
||||||
|
foreach ($headers as $name => $value) {
|
||||||
|
$h->addHeader($name, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SlimRequest($method, $uri, $h, $cookies, $serverParams, $stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
Reference in New Issue
Block a user