first version, pre PR

This commit is contained in:
flo 2025-01-01 23:34:33 +01:00
parent 1df825db2a
commit cc989e96f7
74 changed files with 2810 additions and 198 deletions

2
.gitignore vendored
View File

@ -11,6 +11,8 @@
/var/ /var/
/vendor/ /vendor/
docker/docker-compose.yml
*.env *.env
composer.lock composer.lock
composer.development.json composer.development.json

View File

@ -201,8 +201,8 @@ use Psr\\Http\\Server\\RequestHandlerInterface;
class {$apiHandlerName} implements RequestHandlerInterface class {$apiHandlerName} implements RequestHandlerInterface
{ {
public function __construct( public function __construct(
private readonly {$cqrsHandlerName} \${$cqrsHandlerVariableName}, private readonly {$cqrsHandlerName} \$handler,
private readonly {$cqrsBuilderName} \${$cqrsBuilderVariableName}, private readonly {$cqrsBuilderName} \$builder,
private readonly {$apiResponseFormatterName} \$responseFormatter, private readonly {$apiResponseFormatterName} \$responseFormatter,
) { ) {
} }
@ -211,10 +211,10 @@ class {$apiHandlerName} implements RequestHandlerInterface
{ {
\$data = \$request->getAttribute(AnalyzeBodyMiddleware::JSON_DATA); \$data = \$request->getAttribute(AnalyzeBodyMiddleware::JSON_DATA);
\${$cqrsVariableName} = \$this->{$cqrsBuilderVariableName}->build( \${$cqrsVariableName} = \$this->builder->build(
\$data \$data
); );
\$result = \$this->{$cqrsHandlerVariableName}->execute(\${$cqrsVariableName}); \$result = \$this->handler->execute(\${$cqrsVariableName});
return new SuccessResponse(\$this->responseFormatter->format(\$result)); return new SuccessResponse(\$this->responseFormatter->format(\$result));
} }
@ -275,9 +275,7 @@ class {$cqrsHandlerName}
public function execute({$cqrsName} \${$cqrsVariableName}): {$cqrsResultName} public function execute({$cqrsName} \${$cqrsVariableName}): {$cqrsResultName}
{ {
return new {$cqrsResultName}( return new {$cqrsResultName}();
);
} }
} }
"; ";

30
bin/script/firstRun Executable file
View File

@ -0,0 +1,30 @@
#!/bin/bash
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
PROJECT_DIR=$(realpath $SCRIPT_DIR/../../)
ENV_DIR=$(realpath $PROJECT_DIR/../../../)
source $ENV_DIR/bin/denv_msg
source $ENV_DIR/bin/drun
# Install PHP packages
denv_echo_msg "[backend]: Composer install"
drun backend composer install
denv_success_msg "[backend]: Composer install done"
# Dump autoload
denv_echo_msg "[backend]: Dump autoload"
drun backend composer da
denv_success_msg "[backend]: Dump autoload done"
# Migrate databases to current version
denv_echo_msg "[backend]: Migrate db"
drun backend composer dmm
drun backend composer dmlm
denv_success_msg "[backend]: Migrate db done"
# Insert setup for project after this line
denv_echo_msg "[backend]: Initialize data"
drun backend composer console rbac:update
drun backend composer console init:data
denv_success_msg "[backend]: Initialize data done"

View File

@ -3,54 +3,16 @@
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
PROJECT_DIR=$(realpath $SCRIPT_DIR/../../) PROJECT_DIR=$(realpath $SCRIPT_DIR/../../)
ENV_DIR=$(realpath $PROJECT_DIR/../../../) ENV_DIR=$(realpath $PROJECT_DIR/../../../)
source $ENV_DIR/bin/denv_msg
EXIT=0
# Check .env file # Check .env file
if [ ! -f "$PROJECT_DIR/.env" ] if [ ! -f "$PROJECT_DIR/.env" ] ; then
then
cp "$PROJECT_DIR/.env.example" "$PROJECT_DIR/.env" cp "$PROJECT_DIR/.env.example" "$PROJECT_DIR/.env"
EXIT=1 denv_info_msg "[backend] Created .env"
fi fi
# Check docker-compose.yml file # Check docker-compose.yml file
if [ ! -f "$PROJECT_DIR/docker/docker-compose.yml" ] if [ ! -f "$PROJECT_DIR/docker/docker-compose.yml" ] ; then
then
cp "$PROJECT_DIR/docker/docker-compose.yml.dist" "$PROJECT_DIR/docker/docker-compose.yml" cp "$PROJECT_DIR/docker/docker-compose.yml.dist" "$PROJECT_DIR/docker/docker-compose.yml"
EXIT=1 denv_info_msg "[backend] Created docker-compose.yml"
fi fi
# Check docker-compose-mac.yml file
if [ ! -f "$PROJECT_DIR/docker/docker-compose-mac.yml" ]
then
cp "$PROJECT_DIR/docker/docker-compose-mac.yml.dist" "$PROJECT_DIR/docker/docker-compose-mac.yml"
EXIT=1
fi
if [ $EXIT -eq 1 ]
then
echo "docker-compose or env files created, please change variables and call init again"
exit 1
fi
# Source key-scripts
source $ENV_DIR/bin/drun
source $ENV_DIR/bin/dexec
# Build and start docker containers
dexec template-backend build
dexec template-backend up -d
# Install PHP packages
drun template-backend composer install
# Dump autoload
drun template-backend composer da
# Migrate databases to current version
drun template-backend composer dmm
drun template-backend composer dmlm
# Insert setup for project after this line
drun template-backend composer console rbac:update
drun template-backend composer console init:data

39
bin/script/update Executable file
View File

@ -0,0 +1,39 @@
#!/bin/bash
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
PROJECT_DIR=$(realpath $SCRIPT_DIR/../../)
ENV_DIR=$(realpath $PROJECT_DIR/../../../)
SYSTEM_ENV_FILE="$PROJECT_DIR/.env"
CWD=$(pwd)
source $ENV_DIR/bin/denv_msg
source $ENV_DIR/bin/drun
# Pull branch in project directory
denv_echo_msg "[backend]: Git pull"
cd "$PROJECT_DIR"
git pull
denv_success_msg "[backend]: Git pull done"
# Install PHP packages
denv_echo_msg "[backend]: Composer install"
drun backend composer install
denv_success_msg "[backend]: Composer install done"
# Dump autoload
denv_echo_msg "[backend]: Dump autoload"
drun backend composer da
denv_success_msg "[backend]: Dump autoload done"
# Migrate databases to current version
denv_echo_msg "[backend]: Migrate db"
drun backend composer dmm
drun backend composer dmlm
denv_success_msg "[backend]: Migrate db done"
# Insert setup for project after this line
denv_echo_msg "[backend]: Update roles and permissions"
drun backend composer console rbac:update
denv_success_msg "[backend]: Update roles and permissions done"
# Switch back to current working directory
cd "$CWD"

View File

@ -10,7 +10,8 @@ return [
'user' => [ 'user' => [
], ],
'admin' => [ 'admin' => [
'user.create-user', 'user.create',
'user.read-list',
], ],
] ]
] ]

View File

@ -0,0 +1,17 @@
<?php
return [
'mail' => [
'default' => [
'sender' => $_ENV['MAIL_DEFAULT_SENDER'],
'senderName' => $_ENV['MAIL_DEFAULT_SENDER_NAME']
],
'smtp-server' => [
'host' => $_ENV['SMTP_HOST'],
'port' => (int)$_ENV['SMTP_PORT'] ?? 25,
'encryption' => $_ENV['SMTP_ENCRYPTION'],
'username' => $_ENV['SMTP_USERNAME'],
'password' => $_ENV['SMTP_PASSWORD'],
]
]
];

View File

@ -0,0 +1,9 @@
<?php
return [
'notification' => [
'defaultTitle' => $_ENV['NOTIFICATION_DEFAULT_TITLE'] ?? 'Template',
'host' => $_ENV['NOTIFICATION_HOST'],
'id' => $_ENV['NOTIFICATION_ID'],
]
];

View File

@ -52,6 +52,9 @@ $aggregator = new ConfigAggregator([
\Template\Infrastructure\Rbac\ConfigProvider::class, \Template\Infrastructure\Rbac\ConfigProvider::class,
\Template\Infrastructure\Request\ConfigProvider::class, \Template\Infrastructure\Request\ConfigProvider::class,
\Template\Infrastructure\Session\ConfigProvider::class, \Template\Infrastructure\Session\ConfigProvider::class,
\Template\Infrastructure\Mail\ConfigProvider::class,
\Template\Infrastructure\Notification\ConfigProvider::class,
\Template\Infrastructure\Schema\ConfigProvider::class,
// HandlingDomain // HandlingDomain
\Template\Handling\User\ConfigProvider::class, \Template\Handling\User\ConfigProvider::class,
@ -63,7 +66,7 @@ $aggregator = new ConfigAggregator([
\Template\API\Console\ConfigProvider::class, \Template\API\Console\ConfigProvider::class,
/// External /// External
\Template\API\External\Health\ConfigProvider::class, \Template\API\External\Api\ConfigProvider::class,
\Template\API\External\User\ConfigProvider::class, \Template\API\External\User\ConfigProvider::class,
\Template\API\External\Authentication\ConfigProvider::class, \Template\API\External\Authentication\ConfigProvider::class,

View File

@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
use Template\Infrastructure\Exception\Middleware\ExceptionHandlerMiddleware; use Template\Infrastructure\Exception\Middleware\TemplateExceptionHandlerMiddleware;
use Template\Infrastructure\Request\Middleware\AnalyzeBodyMiddleware; use Template\Infrastructure\Request\Middleware\AnalyzeBodyMiddleware;
use Template\Infrastructure\Request\Middleware\AnalyzeHeaderMiddleware; use Template\Infrastructure\Request\Middleware\AnalyzeHeaderMiddleware;
use Template\Infrastructure\Session\Middleware\SessionMiddleware; use Template\Infrastructure\Session\Middleware\SessionMiddleware;
@ -68,7 +68,7 @@ return function (Application $app, MiddlewareFactory $factory, ContainerInterfac
//// Pre Template Space //// Pre Template Space
$app->pipe(ExceptionHandlerMiddleware::class); $app->pipe(TemplateExceptionHandlerMiddleware::class);
$app->pipe(AnalyzeHeaderMiddleware::class); $app->pipe(AnalyzeHeaderMiddleware::class);
$app->pipe(AnalyzeBodyMiddleware::class); $app->pipe(AnalyzeBodyMiddleware::class);

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,66 @@
<html>
<head>
<meta charset="utf-8">
<title>Willkommen bei Template!</title>
<style>
body {
}
.message-block {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 0 auto;
margin-top: 20px;
padding: 20px;
}
.header {
background-color: rgb(21 128 61);
width: 100%;
height: 15%;
text-align: center;
}
.title {
display: flex;
flex-wrap: wrap;
}
.wrapper {
display: inline-block;
}
.quote {
font-style: italic;
font-size: x-small;
color: slategray;
}
img {
margin: 1rem;
}
h1 {
margin-top: auto;
margin-bottom: auto;
}
</style>
</head>
<body>
<div id="header" class="header">
<div class="wrapper">
<div id="title" class="title">
<img src="assets/icon.png" />
<h1>Template</h1>
</div>
</div>
</div>
<div id="message-block" class="message-block">
<p>Hallo {$username},</p>
<br />
<p>Herzlich willkommen beim Template!</p>
<br />
<p>Ich konnte deine Anmeldung nun bestätigen.</p>
<p>Bitte klicke auf <a href="{$confirmationLink}">diesen Link</a> um dein Passwort festzulegen.</p>
<p>Danach ist deine Registierung abgeschlossen und du kannst loslegen!</p>
<br />
<p>Mit grünen Grüßen,</p>
<p>{$growerName}</p>
<br />
<p class="quote">"Smoke weed everyday" - Snoop Dogg</p>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,63 @@
<html>
<head>
<meta charset="utf-8">
<title>Passwort zurücksetzen</title>
<style>
body {
}
.message-block {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 0 auto;
margin-top: 20px;
padding: 20px;
}
.header {
background-color: rgb(255, 199, 44);
width: 100%;
height: 15%;
text-align: center;
}
.title {
display: flex;
flex-wrap: wrap;
}
.wrapper {
display: inline-block;
}
.quote {
font-style: italic;
font-size: x-small;
color: slategray;
}
img {
margin: 1rem;
}
h1 {
margin-top: auto;
margin-bottom: auto;
}
</style>
</head>
<body>
<div id="header" class="header">
<div class="wrapper">
<div id="title" class="title">
<img src="assets/icon.png" />
<h1>Beekeeper</h1>
</div>
</div>
</div>
<div id="message-block" class="message-block">
<p>Hallo {$username},</p>
<br />
<p>Dein Passwort wurde zurückgesetzt.</p>
<p>Bitte klicke auf <a href="{$passwordResetLink}">diesen Link</a> um ein neues Passwort zu vergeben.</p>
<br />
<p>Wenn du dein Passwort nicht zurückgesetzt hast, kannst du diese Nachricht ignorieren!</p>
<br />
<p>Mit fleißigen Grüßen,</p>
<p>Der Beekeeper</p>
</div>
</body>
</html>

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Template\Migrations\Bee;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20240827155813 extends AbstractMigration
{
public function getDescription(): string
{
return "Create Table 'user_password_token'";
}
public function up(Schema $schema): void
{
$sql = "CREATE TABLE user_password_token (
id binary(16) NOT NULL,
user_id binary(16) NOT NULL,
updated_at datetime NOT NULL,
created_at datetime NOT NULL,
PRIMARY KEY (id)
);";
$this->addSql($sql);
}
public function down(Schema $schema): void
{
$this->addSql("DROP TABLE user_password_token;");
}
}

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Bee\Migrations\Bee;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20240911191301 extends AbstractMigration
{
public function getDescription(): string
{
return "Create Table 'mail'";
}
public function up(Schema $schema): void
{
$sql = "CREATE TABLE mail (
id binary(16) NOT NULL,
template varchar(255) NOT NULL,
data json NOT NULL,
recipient varchar(255) NOT NULL,
sender varchar(255) NULL,
sender_name varchar(255) NULL,
failed_at datetime NULL,
created_at datetime NOT NULL,
PRIMARY KEY (id)
);";
$this->addSql($sql);
}
public function down(Schema $schema): void
{
$this->addSql("DROP TABLE mail;");
}
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Template\Migrations\Template;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20241130201314 extends AbstractMigration
{
public function getDescription(): string
{
return 'rename status and thermometer to reference location';
}
public function up(Schema $schema): void
{
$this->addSql("RENAME TABLE `thermometer` TO `location_thermometer`");
$this->addSql("RENAME TABLE `status` TO `location_status`");
}
public function down(Schema $schema): void
{
$this->addSql("RENAME TABLE `location_thermometer` TO `thermometer`");
$this->addSql("RENAME TABLE `location_status` TO `status`");
}
}

View File

@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace Template\Migrations\Template;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20241130202949 extends AbstractMigration
{
public function getDescription(): string
{
return 'update procedure to last migration';
}
public function up(Schema $schema): void
{
$this->addSql("DROP PROCEDURE pS_LOCATION_ThermometerHistoryGraphList");
$this->addSql("
CREATE PROCEDURE pS_LOCATION_ThermometerHistoryGraphList (
IN locaiton_id BINARY(16),
IN start_at DATETIME,
IN end_at DATETIME,
IN `interval` int
)
BEGIN
SELECT
FROM_UNIXTIME(FLOOR(UNIX_TIMESTAMP(created_at) / (`interval` * 60)) * (`interval` * 60)) AS time_interval,
ROUND(AVG(temperature),1) AS avg_temperature,
ROUND(AVG(humidity)) AS avg_humidity
FROM
location_thermometer
WHERE
locaiton_id = locaiton_id AND
(created_at >= COALESCE(start_at, '1970-01-01 00:00:00')) AND
(created_at <= COALESCE(end_at, NOW()))
GROUP BY
time_interval
ORDER BY
time_interval ASC;
END;
");
}
public function down(Schema $schema): void
{
$this->addSql("DROP PROCEDURE pS_LOCATION_ThermometerHistoryGraphList");
$this->addSql("
CREATE PROCEDURE pS_LOCATION_ThermometerHistoryGraphList (
IN locaiton_id BINARY(16),
IN start_at DATETIME,
IN end_at DATETIME,
IN `interval` int
)
BEGIN
SELECT
FROM_UNIXTIME(FLOOR(UNIX_TIMESTAMP(created_at) / (`interval` * 60)) * (`interval` * 60)) AS time_interval,
ROUND(AVG(temperature),1) AS avg_temperature,
ROUND(AVG(humidity)) AS avg_humidity
FROM
thermometer
WHERE
locaiton_id = locaiton_id AND
(created_at >= COALESCE(start_at, '1970-01-01 00:00:00')) AND
(created_at <= COALESCE(end_at, NOW()))
GROUP BY
time_interval
ORDER BY
time_interval ASC;
END;
");
}
}

View File

@ -1,52 +0,0 @@
networks:
template:
external: true
services:
template-backend-mysql:
image: template-backend-mysql
networks:
- template
build:
context: ./../
dockerfile: ./docker/mysql/dockerfile
volumes:
- /Users/flo/dev/backend/template/var/db:/var/lib/mysql:z
environment:
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASSWORD}
ports:
- 3306:3306
healthcheck:
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
timeout: 20s
retries: 10
template-backend-app:
image: template-backend-app
networks:
- template
build:
context: ./../
dockerfile: ./docker/php/dockerfile
volumes:
- /Users/flo/dev/backend/template/:/var/www/html:z
ports:
- 9000:9000
depends_on:
template-backend-mysql:
condition: service_healthy
template-backend-nginx:
image: template-backend-nginx
networks:
- template
build:
context: ./../
dockerfile: ./docker/nginx/dockerfile
labels:
- "traefik.http.routers.backend.rule=Host(`template.local`) && PathPrefix(`/api`)"
- "traefik.http.routers.backend.entrypoints=websecure"
- "traefik.http.routers.backend.tls.certresolver=le"
depends_on:
- template-backend-app

View File

@ -46,7 +46,7 @@ return [
'migrations_configuration' => [ 'migrations_configuration' => [
'orm_template' => [ 'orm_template' => [
'directory' => 'data/migrations/business', 'directory' => 'data/migrations/template',
'name' => 'Doctrine Database Migrations for Template', 'name' => 'Doctrine Database Migrations for Template',
'namespace' => 'Template\Migrations\Template', 'namespace' => 'Template\Migrations\Template',
'table' => 'migrations', 'table' => 'migrations',

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Template\Infrastructure\Database;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository;
use Laminas\ServiceManager\Factory\FactoryInterface;
use Psr\Container\ContainerInterface;
class AutowireRepositoryFactory implements FactoryInterface
{
public function __construct(
private readonly string $entityManagerClass,
private readonly string $entityClass,
) {
var_dump($this->entityClass, $this->entityClass);
}
public function __invoke(
ContainerInterface $container,
$requestedName,
?array $options = null
): ObjectRepository|EntityRepository
{
/** @var EntityManager $em */
$em = $container->get($this->entityManagerClass);
return $em->getRepository($this->entityClass);
}
}

View File

@ -4,23 +4,15 @@ declare(strict_types=1);
namespace Template\Infrastructure\Encryption\Client; namespace Template\Infrastructure\Encryption\Client;
use Laminas\Crypt\Password\Bcrypt;
class EncryptionClient class EncryptionClient
{ {
private readonly Bcrypt $bcrypt;
public function __construct(
) {
$this->bcrypt = new Bcrypt();
$this->bcrypt->setCost(10);
}
public function encrypt(string $value): string { public function encrypt(string $value): string {
return $this->bcrypt->create($value); return password_hash($value, PASSWORD_BCRYPT, [
'cost' => 10
]);
} }
public function verify(string $value, string $encryptedValue): bool { public function verify(string $value, string $encryptedValue): bool {
return $this->bcrypt->verify($value, $encryptedValue); return hash_equals($encryptedValue, crypt($value, $encryptedValue));
} }
} }

View File

@ -2,11 +2,11 @@
declare(strict_types=1); declare(strict_types=1);
use Template\Infrastructure\Exception\Middleware\ExceptionHandlerMiddleware; use Template\Infrastructure\Exception\Middleware\TemplateExceptionHandlerMiddleware;
use Reinfi\DependencyInjection\Factory\AutoWiringFactory; use Reinfi\DependencyInjection\Factory\AutoWiringFactory;
return [ return [
'factories' => [ 'factories' => [
ExceptionHandlerMiddleware::class => AutoWiringFactory::class, TemplateExceptionHandlerMiddleware::class => AutoWiringFactory::class,
], ],
]; ];

View File

@ -3,10 +3,13 @@
namespace Template\Infrastructure\Exception; namespace Template\Infrastructure\Exception;
enum ErrorCode : string { enum ErrorCode : string {
case SomethingWentWrong = 'SomethingWentWrong';
case NotFound = 'NotFound';
case AlreadyExists = 'AlreadyExists'; case AlreadyExists = 'AlreadyExists';
case WrongCredentials = 'WrongCredentials'; case Failed = 'Failed';
case Invalid = 'Invalid';
case Maintenance = 'Maintenance';
case Mismatch = 'Mismatch'; case Mismatch = 'Mismatch';
case NotFound = 'NotFound';
case SomethingWentWrong = 'SomethingWentWrong';
case WrongCredentials = 'WrongCredentials';
case ValidationFailed = 'ValidationFailed';
} }

View File

@ -3,11 +3,12 @@
namespace Template\Infrastructure\Exception; namespace Template\Infrastructure\Exception;
enum ErrorDomain : string { enum ErrorDomain : string {
case Schema = 'Schema';
case Generic = 'Generic'; case Generic = 'Generic';
case Mail = 'Mail';
case Notification = 'Notification';
case Registration = 'Registration';
case Role = 'Role'; case Role = 'Role';
case User = 'User'; case User = 'User';
case UserPassword = 'UserPassword'; case UserPassword = 'UserPassword';
case Registration = 'Registration';
case Product = 'Product';
} }

View File

@ -2,10 +2,11 @@
namespace Template\Infrastructure\Exception\Exception; namespace Template\Infrastructure\Exception\Exception;
use Exception;
use Template\Infrastructure\Exception\ErrorCode; use Template\Infrastructure\Exception\ErrorCode;
use Template\Infrastructure\Exception\ErrorDomain; use Template\Infrastructure\Exception\ErrorDomain;
class Exception extends \Exception { class TemplateException extends Exception {
private readonly ErrorDomain $errorDomain; private readonly ErrorDomain $errorDomain;
private readonly ErrorCode $errorCode; private readonly ErrorCode $errorCode;

View File

@ -1,37 +0,0 @@
<?php
namespace Template\Infrastructure\Exception\Middleware;
use Template\Infrastructure\Exception\Exception\Exception;
use Template\Infrastructure\Logging\Logger\Logger;
use Template\Infrastructure\Response\ErrorResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class ExceptionHandlerMiddleware implements MiddlewareInterface
{
public function __construct(
private readonly Logger $logger
) {
}
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface
{
try {
return $handler->handle($request);
} catch (Exception $exception) {
$this->logger->exception($exception);
return new ErrorResponse(
$exception->getErrorDomain(),
$exception->getErrorCode(),
$exception->getMessage()
);
}
}
}

View File

@ -0,0 +1,71 @@
<?php
namespace Template\Infrastructure\Exception\Middleware;
use League\OpenAPIValidation\PSR15\Exception\InvalidResponseMessage;
use League\OpenAPIValidation\PSR15\Exception\InvalidServerRequestMessage;
use Template\Infrastructure\Exception\ErrorCode;
use Template\Infrastructure\Exception\ErrorDomain;
use Template\Infrastructure\Exception\Exception\TemplateException;
use Template\Infrastructure\Logging\Logger\Logger;
use Template\Infrastructure\Response\ErrorResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class TemplateExceptionHandlerMiddleware implements MiddlewareInterface
{
public function __construct(
private readonly Logger $logger
) {
}
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface
{
try {
return $handler->handle($request);
} catch (TemplateException $exception) {
$this->logger->exception($exception);
return new ErrorResponse(
$exception->getErrorDomain(),
$exception->getErrorCode(),
$exception->getMessage()
);
} catch (InvalidResponseMessage $exception) {
$this->logger->exception($exception);
$lines = [];
do {
$lines[] = $exception->getMessage();
$exception = $exception->getPrevious();
} while ($exception->getPrevious() !== null);
return new ErrorResponse(
ErrorDomain::Schema,
ErrorCode::ValidationFailed,
implode(PHP_EOL, $lines)
);
} catch (InvalidServerRequestMessage $exception) {
$this->logger->exception($exception);
$lines = [];
do {
$lines[] = $exception->getMessage();
$exception = $exception->getPrevious();
} while ($exception->getPrevious() !== null);
return new ErrorResponse(
ErrorDomain::Schema,
ErrorCode::ValidationFailed,
implode(PHP_EOL, $lines)
);
}
}
}

View File

@ -2,8 +2,10 @@
namespace Template\Infrastructure\Logging\Logger; namespace Template\Infrastructure\Logging\Logger;
use Template\Infrastructure\Exception\Exception\Exception; use Template\Infrastructure\Exception\Exception\TemplateException;
use Template\Infrastructure\Logging\Handler\FileStreamHandler;
use Monolog\ErrorHandler; use Monolog\ErrorHandler;
use Monolog\Handler\StreamHandler;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Stringable; use Stringable;
use Throwable; use Throwable;
@ -24,7 +26,7 @@ class Logger implements LoggerInterface
'trace' => $exception->getTraceAsString() 'trace' => $exception->getTraceAsString()
]*/; ]*/;
if ($exception instanceof Exception) { if ($exception instanceof TemplateException) {
$exceptionContext = array_merge([ $exceptionContext = array_merge([
'errorDomain' => $exception->getErrorDomain()->value, 'errorDomain' => $exception->getErrorDomain()->value,
'errorCode' => $exception->getErrorCode()->value, 'errorCode' => $exception->getErrorCode()->value,

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
use Reinfi\DependencyInjection\Factory\AutoWiringFactory;
use Template\Infrastructure\Mail\Service\MailService;
return [
'factories' => [
MailService::class => AutoWiringFactory::class,
],
];

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Template\Infrastructure\Mail;
class ConfigProvider
{
public function __invoke(): array
{
return [
'dependencies' => require __DIR__ . './../config/service_manager.php',
];
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace Template\Infrastructure\Mail\Exception;
use Throwable;
use Template\Infrastructure\Exception\ErrorCode;
use Template\Infrastructure\Exception\ErrorDomain;
use Template\Infrastructure\Exception\Exception\TemplateException;
class SendMailFailedException extends TemplateException
{
private const MESSAGE = 'Das Mail template %s konnte mit den Daten {%s} nicht an %s gesendet werden. Grund: %s';
public function __construct(
string $templateIdentification,
array $templateData,
string $recipient,
Throwable $reason,
) {
parent::__construct(
sprintf(
self::MESSAGE,
$templateIdentification,
json_encode($templateData),
$recipient,
$reason->getMessage(),
),
ErrorDomain::Mail,
ErrorCode::Failed
);
}
}

View File

@ -0,0 +1,176 @@
<?php
declare(strict_types=1);
namespace Template\Infrastructure\Mail\Service;
use Template\Data\Business\Entity\Mail;
use Template\Data\Business\Manager\EntityManager;
use Template\Data\Business\Repository\MailRepository;
use DateTime;
use Exception;
use Latte\Engine;
use Nette\Mail\Mailer;
use Nette\Mail\Message;
use Nette\Mail\SmtpMailer;
use Reinfi\DependencyInjection\Service\ConfigService;
use Throwable;
use Template\Infrastructure\Logging\Logger\Logger;
use Template\Infrastructure\Mail\Exception\SendMailFailedException;
class MailService
{
private const TEMPLATE_PATH = APP_ROOT . '/data/mails/';
private readonly Engine $engine;
private readonly MailRepository $mailRepository;
public function __construct(
private readonly EntityManager $entityManager,
private readonly ConfigService $configService,
private readonly Logger $logger,
) {
$this->engine = new Engine();
$this->mailRepository = $this->entityManager->getRepository(Mail::class);
}
public function enqueue(
string $template,
array $templateData,
string $recipient,
?string $sender = null,
?string $senderName = null
): void {
$mail = new Mail();
$mail->setTemplate($template);
$mail->setData($templateData);
$mail->setRecipient($recipient);
$mail->setSender($sender);
$mail->setSenderName($senderName);
$this->entityManager->persist($mail);
$this->entityManager->flush();
}
public function send(?int $count = null): void {
$qb = $this->mailRepository->createQueryBuilder('m');
$qb->orderBy('m.createdAt', 'asc');
if ($count !== null) {
$qb->setMaxResults($count);
}
$mailsToSend = $qb->getQuery()->execute();
/** @var Mail $mailToSend */
foreach ($mailsToSend as $mailToSend) {
try {
$this->sendMail(
$mailToSend->getTemplate(),
$mailToSend->getData(),
$mailToSend->getRecipient(),
$mailToSend->getSender(),
$mailToSend->getSenderName(),
);
$this->entityManager->remove($mailToSend);
} catch (SendMailFailedException $e) {
// log is done withing sendMail
$mailToSend->setFailedAt(new DateTime());
$this->entityManager->persist($mailToSend);
}
$this->entityManager->flush();
}
}
/**
* @throws SendMailFailedException
*/
private function sendMail(
string $template,
array $templateData,
string $recipient,
?string $sender = null,
?string $senderName = null
): void {
try {
$mail = $this->getMail(
template: $template,
templateData: $templateData,
recipient: $recipient,
sender: $sender,
senderName: $senderName
);
$mailer = $this->getMailer();
$mailer->send($mail);
} catch (Throwable $e) {
$this->logger->exception($e);
throw new SendMailFailedException(
$template,
$templateData,
$recipient,
$e
);
}
}
private function getMailer(): Mailer
{
$smtpConfig = $this->configService->resolve('mail.smtp-server');
return new SmtpMailer(
host: $smtpConfig['host'],
username: $smtpConfig['username'],
password: $smtpConfig['password'],
encryption: $smtpConfig['encryption'],
port: $smtpConfig['port'],
);
}
private function getMail(
string $template,
array $templateData,
string $recipient,
?string $sender = null,
?string $senderName = null
): Message {
$templatePath = self::TEMPLATE_PATH . $template . '/template.latte';
$assetsPath = self::TEMPLATE_PATH . $template . "/";
if (!file_exists($templatePath)) {
throw new Exception("Template File does not exist");
}
if (!is_dir($assetsPath)) {
$assetsPath = null;
}
$mail = new Message();
$mail->setFrom($this->getSenderName($sender, $senderName));
$mail->addTo($recipient);
$mail->setHtmlBody(
$this->engine->renderToString(
$templatePath,
$templateData,
),
$assetsPath
);
return $mail;
}
private function getSenderName(
?string $sender,
?string $senderName
): string {
$defaultConfig = $this->configService->resolve('mail.default');
$from = $sender ?? $defaultConfig['sender'] ?? throw new Exception('Could not determine Sender');
$name =$sender ?? $defaultConfig['senderName'] ?? throw new Exception('Could not determine Sender Name');
return sprintf('%s <%s>', $name, $from);
}
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
use Reinfi\DependencyInjection\Factory\AutoWiringFactory;
use Template\Infrastructure\Notification\Service\NotificationService;
return [
'factories' => [
NotificationService::class => AutoWiringFactory::class,
],
];

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Template\Infrastructure\Notification;
class ConfigProvider
{
public function __invoke(): array
{
return [
'dependencies' => require __DIR__ . './../config/service_manager.php',
];
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace Template\Infrastructure\Notification\Exception;
use Throwable;
use Template\Infrastructure\Exception\ErrorCode;
use Template\Infrastructure\Exception\ErrorDomain;
use Template\Infrastructure\Exception\Exception\TemplateException;
class SendNotificationFailed extends TemplateException
{
private const MESSAGE = 'Notification could not be sent. Reason: %s';
public function __construct(
Throwable $reason,
) {
parent::__construct(
sprintf(
self::MESSAGE,
$reason->getMessage(),
),
ErrorDomain::Notification,
ErrorCode::Failed
);
}
}

View File

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Template\Infrastructure\Notification\Service;
use Exception;
use Reinfi\DependencyInjection\Service\ConfigService;
use Throwable;
use Template\Infrastructure\Notification\Exception\SendNotificationFailed;
class NotificationService
{
private string $defaultTitle;
private ?string $host;
private ?string $id;
public function __construct(
private readonly ConfigService $configService,
) {
$config = $this->configService->resolve('notification');
$this->defaultTitle = $config['defaultTitle'];
$this->host = $config['host'] ?? '';
$this->id = $config['id'] ?? '';
}
/**
* @throws SendNotificationFailed
*/
public function push(
string $message,
string $title = null,
): void {
try {
if (($this->id ?? '') === '') {
throw new Exception('Notification ID is not set.');
}
if (($this->host ?? '') === '') {
throw new Exception('Notification Host is not set.');
}
$command = sprintf(
'curl -H "Title: %s" -H "Tags: broccoli" -d "%s" %s/%s',
$title ?? $this->defaultTitle,
$message,
$this->host,
$this->id
);
shell_exec($command);
} catch (Throwable $e) {
throw new SendNotificationFailed($e);
}
}
}

View File

@ -56,7 +56,8 @@ class EnsureAuthorizationMiddleware implements MiddlewareInterface
): bool { ): bool {
$role = $user->getRole(); $role = $user->getRole();
$permissions = $role->getPermissions(); $permissions = $role->getPermissions();
$targetApi = $this->routes[$targetPath];
$targetApi = $this->routes[$targetPath] ?? null;
/** @var Permission $permission */ /** @var Permission $permission */
foreach($permissions as $permission) { foreach($permissions as $permission) {

View File

@ -2,12 +2,14 @@
declare(strict_types=1); declare(strict_types=1);
use Reinfi\DependencyInjection\Factory\AutoWiringFactory; use Template\Infrastructure\Rbac\Middleware\EnsureAuthorizationMiddleware;
use Template\Infrastructure\Request\Factory\RequestServiceFactory; use Template\Infrastructure\Request\Factory\RequestServiceFactory;
use Template\Infrastructure\Request\Middleware\AnalyzeBodyMiddleware; use Template\Infrastructure\Request\Middleware\AnalyzeBodyMiddleware;
use Template\Infrastructure\Request\Middleware\AnalyzeHeaderMiddleware; use Template\Infrastructure\Request\Middleware\AnalyzeHeaderMiddleware;
use Template\Infrastructure\Request\Middleware\InternalRequestMiddleware; use Template\Infrastructure\Request\Middleware\InternalRequestMiddleware;
use Template\Infrastructure\Request\Service\RequestService; use Template\Infrastructure\Request\Service\RequestService;
use Reinfi\DependencyInjection\Factory\AutoWiringFactory;
use Reinfi\DependencyInjection\Factory\InjectionFactory;
return [ return [
'factories' => [ 'factories' => [

View File

@ -18,7 +18,7 @@ class RequestServiceFactory implements FactoryInterface
{ {
/** @var ConfigService $configService */ /** @var ConfigService $configService */
$configService = $container->get(ConfigService::class); $configService = $container->get(ConfigService::class);
$apiKey = $configService->resolve("api.keys.template"); $apiKey = $configService->resolve("api.keys.Template");
return new RequestService( return new RequestService(
$apiKey, $apiKey,

View File

@ -2,16 +2,10 @@
namespace Template\Infrastructure\Request\Middleware; namespace Template\Infrastructure\Request\Middleware;
use Template\Data\Business\Entity\Permission;
use Template\Data\Business\Entity\User;
use Template\Infrastructure\Response\ForbiddenResponse;
use Template\Infrastructure\Session\Middleware\LoggedInUserMiddleware;
use Laminas\Config\Config;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface; use Psr\Http\Server\RequestHandlerInterface;
use Reinfi\DependencyInjection\Annotation\InjectConfig;
class AnalyzeBodyMiddleware implements MiddlewareInterface class AnalyzeBodyMiddleware implements MiddlewareInterface
{ {
@ -33,6 +27,8 @@ class AnalyzeBodyMiddleware implements MiddlewareInterface
$request->getBody()->getContents(), $request->getBody()->getContents(),
true true
); );
$request->getBody()->rewind();
} }
return $handler->handle($request->withAttribute( return $handler->handle($request->withAttribute(

View File

@ -2,36 +2,47 @@
namespace Template\Infrastructure\Request\Middleware; namespace Template\Infrastructure\Request\Middleware;
use Template\Data\Business\Entity\Permission;
use Template\Data\Business\Entity\User;
use Template\Infrastructure\Response\ForbiddenResponse;
use Template\Infrastructure\Session\Middleware\LoggedInUserMiddleware;
use Laminas\Config\Config;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface; use Psr\Http\Server\RequestHandlerInterface;
use Reinfi\DependencyInjection\Annotation\InjectConfig;
class AnalyzeHeaderMiddleware implements MiddlewareInterface class AnalyzeHeaderMiddleware implements MiddlewareInterface
{ {
const HOST_ATTRIBUTE = 'host_attribute'; public const HOST_ATTRIBUTE = 'host_attribute';
public const CONTENT_TYPE_ATTRIBUTE = 'content_type_attribute';
private const CONTENT_TYPE_HEADER_KEY = 'Content-Type';
private const HOST_HEADER_KEY = 'host';
private const FORWARDED_HOST_HEADER_KEY = 'x-forwarded-host';
public function process( public function process(
ServerRequestInterface $request, ServerRequestInterface $request,
RequestHandlerInterface $handler RequestHandlerInterface $handler
): ResponseInterface ): ResponseInterface
{ {
$headers = $request->getHeaders();
$contentType = $this->readHeader($headers, self::CONTENT_TYPE_HEADER_KEY) ?? null;
$host = explode( $host = explode(
':', ':',
$request->getHeaders()['host'][0] $this->readHeader($headers, self::HOST_HEADER_KEY) ??
?? $request->getHeaders()['x-forwarded-host'][0] $this->readHeader($headers, self::FORWARDED_HOST_HEADER_KEY) ??
?? 'UNKNOWN' 'UNKNOWN'
)[0]; )[0];
return $handler->handle($request->withAttribute( return $handler->handle(
self::HOST_ATTRIBUTE, $request
$host ->withAttribute(self::HOST_ATTRIBUTE, $host)
)); ->withAttribute(self::CONTENT_TYPE_ATTRIBUTE, $contentType)
);
}
private function readHeader(array $header, string $key): mixed
{
return
($header[$key] ??
$header[strtolower($key)] ??
$header[strtoupper($key)] ?? [])[0] ?? null;
} }
} }

View File

@ -7,9 +7,13 @@ use Laminas\Diactoros\Response\JsonResponse;
class SuccessResponse extends JsonResponse class SuccessResponse extends JsonResponse
{ {
public function __construct( public function __construct(
mixed $data = 'OK' mixed $data = 'Success'
) )
{ {
if ($data === []) {
$data = 'Success';
}
parent::__construct( parent::__construct(
$data $data
); );

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
use cebe\openapi\spec\OpenApi;
use Reinfi\DependencyInjection\Factory\AutoWiringFactory;
use Template\Infrastructure\Schema\Exporter\Backend\Builder\ClassFileBuilder;
use Template\Infrastructure\Schema\Exporter\BackendExporter;
use Template\Infrastructure\Schema\Exporter\Frontend\Builder\PropertyBuilder as FrontendPropertyBuilder;
use Template\Infrastructure\Schema\Exporter\Backend\Builder\PropertyBuilder as BackendPropertyBuilder;
use Template\Infrastructure\Schema\Exporter\Frontend\Builder\ServiceBuilder;
use Template\Infrastructure\Schema\Exporter\Frontend\Builder\TypeBuilder as FrontendTypeBuilder;
use Template\Infrastructure\Schema\Exporter\Backend\Builder\TypeBuilder as BackendTypeBuilder;;
use Template\Infrastructure\Schema\Exporter\FrontendExporter;
use Template\Infrastructure\Schema\Factory\OpenApiFactory;
use Template\Infrastructure\Schema\Middleware\SchemaValidationMiddleware;
use Template\Infrastructure\Schema\Reader\ApiDefinitionReader;
return [
'factories' => [
OpenApi::class => OpenApiFactory::class,
/// Reader
ApiDefinitionReader::class => AutoWiringFactory::class,
/// Middleware
SchemaValidationMiddleware::class => AutoWiringFactory::class,
/// Exporter
// Backend
BackendExporter::class => AutoWiringFactory::class,
ClassFileBuilder::class => AutoWiringFactory::class,
BackendPropertyBuilder::class => AutoWiringFactory::class,
BackendTypeBuilder::class => AutoWiringFactory::class,
// Frontend
FrontendExporter::class => AutoWiringFactory::class,
ServiceBuilder::class => AutoWiringFactory::class,
FrontendTypeBuilder::class => AutoWiringFactory::class,
FrontendPropertyBuilder::class => AutoWiringFactory::class,
],
];

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Template\Infrastructure\Schema;
class ConfigProvider
{
public function __invoke(): array
{
return [
'dependencies' => require __DIR__ . './../config/service_manager.php',
];
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace Template\Infrastructure\Schema\Enum;
enum Method : string {
case post = 'post';
case get = 'get';
}

View File

@ -0,0 +1,13 @@
<?php
namespace Template\Infrastructure\Schema\Enum;
enum SchemaType : string {
case object = 'object';
case array = 'array';
case string = 'string';
case number = 'number';
case integer = 'integer';
case boolean = 'boolean';
case any = 'any';
}

View File

@ -0,0 +1,10 @@
<?php
namespace Template\Infrastructure\Schema\Exception;
use Exception;
class FileNotFoundException extends Exception
{
}

View File

@ -0,0 +1,10 @@
<?php
namespace Template\Infrastructure\Schema\Exception;
use Exception;
class UnsupportedContentTypeException extends Exception
{
}

View File

@ -0,0 +1,37 @@
<?php
namespace Template\Infrastructure\Schema\Exporter\Backend\Builder;
use Template\Infrastructure\Schema\Exporter\Backend\Model\ClassFile;
use Template\Infrastructure\Schema\Model\Api;
class ClassFileBuilder
{
public function build(Api $api, string $domain, string $type, string $namespace, string $directory): ClassFile
{
if(in_array($type, ["UseCase", "UseCaseResult", "UseCaseHandler", "UseCaseBuilder", "Request", "Response", "ResponseFormatter", "RequestHandler"])) {
$name = $api->getUseCase() . $type;
} else {
$name = $type;
}
if (!file_exists($directory)) {
mkdir(
directory: $directory,
recursive: true
);
}
return new ClassFile(
$api,
$domain,
$name,
$namespace,
$directory,
$directory . $name . '.php',
$type,
[],
null
);
}
}

View File

@ -0,0 +1,112 @@
<?php
namespace Template\Infrastructure\Schema\Exporter\Backend\Builder;
use Laminas\ServiceManager\ServiceManager;
use Template\Infrastructure\Schema\Enum\SchemaType;
use Template\Infrastructure\Schema\Exporter\Backend\Model\ClassFile;
use Template\Infrastructure\Schema\Exporter\Backend\Model\Property;
use Template\Infrastructure\Schema\Model\Type;
class PropertyBuilder
{
public function __construct(
private readonly ServiceManager $serviceManager,
) {
}
public function build(ClassFile $parent, string $name, Type $type): Property {
$nullableFlag = ($type->getIsNullable() ?? false) ? '?' : '';
$normalizedType = $this->normalizeBackendType($parent, $name, $type);
$types = [$normalizedType['normalizedType']];
/*
if ($type->getEnum() !== null) {
$format = match ($type->getType()) {
SchemaType::string => '"%s"',
default => "%s"
};
$types = [];
foreach ($type->getEnum() as $enumValue) {
$types[] = sprintf($format, $enumValue);
}
}
*/
return new Property(
$type,
$name,
sprintf("%s%s", $nullableFlag, implode('|', $types)),
$normalizedType['backendType'],
$normalizedType['useUsings'],
$normalizedType['createUsings'],
$normalizedType['format']
);
}
private function normalizeBackendType(ClassFile $parent, string $name, Type $type) : array
{
$definition = null;
$createUsings = [];
$useUsings = [];
switch ($type->getType()) {
case SchemaType::boolean:
$normalizedType = 'bool';
break;
case SchemaType::integer:
$normalizedType = 'int';
break;
case SchemaType::number:
switch ($type->getFormat()) {
case 'float': $normalizedType = 'float'; break;
default: $normalizedType = 'NUMBER';
}
break;
case SchemaType::string:
switch ($type->getFormat()) {
case 'uuid':
$normalizedType = 'UuidInterface';
$createUsings = ['Ramsey\Uuid\Uuid'];
$useUsings = ['Ramsey\Uuid\UuidInterface'];
$format = "Uuid::fromString(%s)";
break;
case 'date-time':
$normalizedType = 'DateTimeImmutable';
$createUsings = ['DateTimeImmutable'];
$useUsings = ['DateTimeImmutable'];
$format = "new DateTimeImmutable(%s)";
break;
default: $normalizedType = 'string';
}
break;
case SchemaType::object:
/** @var TypeBuilder $typeBuilder */
$typeBuilder = $this->serviceManager->get(TypeBuilder::class);
//$backendType = $typeBuilder->build($parent, ucfirst($name), $type);
$normalizedType = "any";//$backendType->getName();
break;
case SchemaType::array:
$singular = ucfirst(substr($name, 0, strlen($name) - 1));
/** @var TypeBuilder $typeBuilder */
//$typeBuilder = $this->serviceManager->get(TypeBuilder::class);
//$backendType = $typeBuilder->build($parent, $singular, $type->getItemType());
$normalizedType = "array";
break;
default:
$normalizedType = $type->getType()->value;
break;
};
return [
'normalizedType' => $normalizedType,
'backendType' => $backendType ?? null,
'format' => $format ?? '%s',
'useUsings' => $useUsings ?? [],
'createUsings' => $createUsings ?? [],
];
}
}

View File

@ -0,0 +1,80 @@
<?php
namespace Template\Infrastructure\Schema\Exporter\Backend\Builder;
use Template\Infrastructure\Schema\Exporter\Backend\Model\ClassFile;
use Template\Infrastructure\Schema\Exporter\Backend\Model\Property;
use Template\Infrastructure\Schema\Exporter\Backend\Model\Type;
use Template\Infrastructure\Schema\Model\Type as SchemaType;
class TypeBuilder
{
public function __construct(
private readonly PropertyBuilder $propertyBuilder,
private readonly ClassFileBuilder $classFileBuilder,
) {
}
public function build(ClassFile $parent, string $name, SchemaType $type, ?ClassFile $root = null): Type
{
if ($root === null) {
$name = implode([$name]);
} else {
$name = implode([$parent->getName(), $name]);
}
$propertyDefinitions = [];
$newClassFile = $this->classFileBuilder->build(
$parent->getApi(),
$parent->getDomain(),
$name,
$parent->getNamespace() . (($root === null) ? ($parent->getType() . '\\') : ''),
$parent->getDirectory() . (($root === null) ? ($parent->getType() . '/') : ''),
);
$usings = [];
/**
* @var string $propertyName
* @var SchemaType $type
*/
foreach (($type->getProperties() ?? []) as $propertyName => $type) {
$propertyDefinition = $this->propertyBuilder->build($newClassFile, $propertyName, $type);
$usings = array_merge($usings, $propertyDefinition->getUseUsings());
$propertyDefinitions[] = $propertyDefinition;
}
$lines = [];
$lines[] = "public function __construct(";
/** @var Property $propertyDefinition */
foreach ($propertyDefinitions as $propertyDefinition) {
$lines[] = "\tprivate readonly " . $propertyDefinition->getType() . " $" . $propertyDefinition->getName() . ',';
}
$lines[] = ") {";
$lines[] = "}";
/** @var Property $propertyDefinition */
foreach ($propertyDefinitions as $propertyDefinition) {
$lines[] = null;
$lines[] = "public function get" . ucfirst($propertyDefinition->getName()) . '(): ' .$propertyDefinition->getType();
$lines[] = "{";
$lines[] = "\treturn \$this->{$propertyDefinition->getName()};";
$lines[] = "}";
}
$newClassFile->setDefinition(
implode(
PHP_EOL,
array_map(
function (?string $line) { return $line === null ? '' : "\t" . $line; },
$lines
)
)
);
$newClassFile->setPredefinedUsings($usings);
return new Type(
$name,
$newClassFile,
);
}
}

View File

@ -0,0 +1,87 @@
<?php
namespace Template\Infrastructure\Schema\Exporter\Backend\Model;
use Template\Infrastructure\Schema\Model\Api;
class ClassFile
{
private bool $finalized = false;
public function __construct(
private readonly Api $api,
private readonly string $domain,
private readonly string $name,
private readonly string $namespace,
private readonly string $directory,
private readonly string $path,
private readonly string $type,
private array $predefinedUsings,
private ?string $definition,
) {
}
public function isFinalized(): bool
{
return $this->finalized;
}
public function setFinalized(bool $finalized): void
{
$this->finalized = $finalized;
}
public function getApi(): Api
{
return $this->api;
}
public function getDomain(): string
{
return $this->domain;
}
public function getType(): string
{
return $this->type;
}
public function getName(): string
{
return $this->name;
}
public function getNamespace(): string
{
return $this->namespace;
}
public function getDirectory(): string
{
return $this->directory;
}
public function getPath(): string
{
return $this->path;
}
public function getPredefinedUsings(): array
{
return $this->predefinedUsings;
}
public function setPredefinedUsings(array $pred): void
{
$this->predefinedUsings = $pred;
}
public function getDefinition(): ?string
{
return $this->definition;
}
public function setDefinition(?string $definition): void
{
$this->definition = $definition;
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace Template\Infrastructure\Schema\Exporter\Backend\Model;
use Template\Infrastructure\Schema\Model\Type as SchemaType;
class Property
{
public function __construct(
private readonly SchemaType $schemaType,
private readonly string $name,
private readonly string $type,
private readonly ?Type $subtypeDefinition,
private readonly array $useUsings,
private readonly array $createUsings,
private readonly string $format,
) {
}
public function getSchemaType(): SchemaType
{
return $this->schemaType;
}
public function getName(): string
{
return $this->name;
}
public function getType(): string
{
return $this->type;
}
public function getSubtype(): ?Type
{
return $this->subtypeDefinition;
}
public function getUseUsings(): array
{
return $this->useUsings;
}
public function getCreateUsings(): array
{
return $this->createUsings;
}
public function getFormat(): string
{
return $this->format;
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace Template\Infrastructure\Schema\Exporter\Backend\Model;
class Type
{
public function __construct(
private readonly string $name,
private readonly ClassFile $file
) {
}
public function getName(): string
{
return $this->name;
}
public function getFile(): ClassFile
{
return $this->file;
}
}

View File

@ -0,0 +1,581 @@
<?php
namespace Template\Infrastructure\Schema\Exporter;
use Template\Infrastructure\Schema\Exporter\Backend\Builder\ClassFileBuilder;
use Template\Infrastructure\Schema\Exporter\Backend\Builder\PropertyBuilder;
use Template\Infrastructure\Schema\Exporter\Backend\Builder\TypeBuilder;
use Template\Infrastructure\Schema\Exporter\Backend\Model\ClassFile;
use Template\Infrastructure\Schema\Exporter\Backend\Model\Property;
use Template\Infrastructure\Schema\Model\Api;
use Template\Infrastructure\Schema\Model\Type as SchemaType;
use Template\Infrastructure\Schema\Reader\ApiDefinitionReader;
class BackendExporter
{
private const BASE_NAMESPACE = 'Template';
private const AUTO_WIRING_FACTORY = 'Reinfi\DependencyInjection\Factory\AutoWiringFactory';
private const INJECTION_FACTORY = 'Reinfi\DependencyInjection\Factory\InjectionFactory';
private const ENSURE_AUTHORIZATION_MIDDLEWARE = 'Template\Infrastructure\Rbac\Middleware\EnsureAuthorizationMiddleware';
private const SCHEMA_VALIDATION_MIDDLEWARE = 'Template\Infrastructure\Schema\Middleware\SchemaValidationMiddleware';
private const LOGGED_IN_USER_MIDDLEWARE = 'Template\Infrastructure\Session\Middleware\LoggedInUserMiddleware';
private const SERVICE_MANAGER_USINGS = [
self::AUTO_WIRING_FACTORY,
self::INJECTION_FACTORY,
];
private const ROUTER_USINGS = [
self::ENSURE_AUTHORIZATION_MIDDLEWARE,
self::SCHEMA_VALIDATION_MIDDLEWARE,
self::LOGGED_IN_USER_MIDDLEWARE,
];
private const DOMAIN_TYPE_API = 'Api';
private const DOMAIN_TYPE_HANDLING = 'Handling';
public function __construct(
private readonly ApiDefinitionReader $apiDefinitionReader,
private readonly ClassFileBuilder $fileBuilder,
private readonly PropertyBuilder $propertyBuilder,
) {
}
public function export(string $exportRootDirectory = APP_ROOT . "/var/export/schema/be-models/"): void
{
$namespaces = $this->apiDefinitionReader->read("/api/location");
$files = [];
foreach ($namespaces as $namespace => $apis) {
$namespaceFiles = [];
$apiDomainNamespace = self::BASE_NAMESPACE . '\\API\\External\\' . ucfirst($namespace) . '\\';
$apiDomainDirectory = $exportRootDirectory . 'ApiDomain/External/' . ucfirst($namespace) . '/';
$apiDomainDirectorySource = $apiDomainDirectory . 'src/';
$apiDomainDirectoryConfig = $apiDomainDirectory . 'config/';
$handlingDomainNamespace = self::BASE_NAMESPACE . '\\Handling\\' . ucfirst($namespace) . '\\';
$handlingDomainDirectory = $exportRootDirectory . 'HandlingDomain/' . ucfirst($namespace) . '/';
$handlingDomainDirectorySource = $handlingDomainDirectory . 'src/';
$handlingDomainDirectoryConfig = $handlingDomainDirectory . 'config/';
/** @var Api $api */
foreach ($apis as $api) {
$useCaseFiles = [];
$useCase = $api->getUseCase();
$useCaseHandlingNamespace = $handlingDomainNamespace . 'UseCase\\' . $useCase . '\\';
$useCaseHandlingDirectory = $handlingDomainDirectorySource . 'UseCase/' . $useCase . '/';
$useCaseFiles[] = $this->fileBuilder->build($api, self::DOMAIN_TYPE_HANDLING, 'UseCase', $useCaseHandlingNamespace, $useCaseHandlingDirectory);
$useCaseFiles[] = $this->fileBuilder->build($api, self::DOMAIN_TYPE_HANDLING, 'UseCaseResult', $useCaseHandlingNamespace, $useCaseHandlingDirectory);
$useCaseFiles[] = $this->fileBuilder->build($api, self::DOMAIN_TYPE_HANDLING, 'UseCaseHandler', $useCaseHandlingNamespace, $useCaseHandlingDirectory);
$useCaseFiles[] = $this->fileBuilder->build($api, self::DOMAIN_TYPE_HANDLING, 'UseCaseBuilder', $useCaseHandlingNamespace, $useCaseHandlingDirectory);
$useCaseSeparatedNamespace = $apiDomainNamespace . $useCase . '\\';
$useCaseSeparatedDirectory = $apiDomainDirectorySource . $useCase . '/';
$apisAreUseCaseSeparated = false;
$responseFormatterNamespace = $apisAreUseCaseSeparated ? $useCaseSeparatedNamespace : $apiDomainNamespace . 'ResponseFormatter\\';
$responseFormatterDirectory = $apisAreUseCaseSeparated ? $useCaseSeparatedDirectory : $apiDomainDirectorySource . 'ResponseFormatter/';
$handlerNamespace = $apisAreUseCaseSeparated ? $useCaseSeparatedNamespace : $apiDomainNamespace . 'Handler\\';
$handlerDirectory = $apisAreUseCaseSeparated ? $useCaseSeparatedDirectory : $apiDomainDirectorySource . 'Handler/';
$useCaseFiles[] = $this->fileBuilder->build($api, self::DOMAIN_TYPE_API, 'ResponseFormatter', $responseFormatterNamespace, $responseFormatterDirectory);
$useCaseFiles[] = $this->fileBuilder->build($api, self::DOMAIN_TYPE_API, 'RequestHandler', $handlerNamespace, $handlerDirectory);
foreach ($useCaseFiles as $useCaseFile) {
$this->generateFileContent($useCaseFile, $useCaseFiles);
$namespaceFiles[] = $useCaseFile;
}
}
$namespaceFiles[] = $this->fileBuilder->build($api, self::DOMAIN_TYPE_HANDLING, 'ConfigProvider', $handlingDomainNamespace, $handlingDomainDirectorySource);
$namespaceFiles[] = $this->fileBuilder->build($api, self::DOMAIN_TYPE_HANDLING, 'service_manager', $handlingDomainNamespace, $handlingDomainDirectoryConfig);
$namespaceFiles[] = $this->fileBuilder->build($api, self::DOMAIN_TYPE_API,'ConfigProvider', $apiDomainNamespace, $apiDomainDirectorySource);
$namespaceFiles[] = $this->fileBuilder->build($api, self::DOMAIN_TYPE_API,'service_manager', $apiDomainNamespace, $apiDomainDirectoryConfig);
$namespaceFiles[] = $this->fileBuilder->build($api, self::DOMAIN_TYPE_API,'routes', $apiDomainNamespace, $apiDomainDirectoryConfig);
foreach ($namespaceFiles as $namespaceFile) {
$this->generateFileContent($namespaceFile, $namespaceFiles);
$files[] = $namespaceFile;
}
}
foreach ($files as $file) {
file_put_contents(
$file->getPath(),
$file->getDefinition()
);
}
}
private function getFilesOfType(array $files, array $types) : array
{
$return = [];
/** @var ClassFile $file */
foreach ($files as $file) {
if (in_array($file->getType(), $types)) {
$return[] = $file;
}
}
return $return;
}
private function generateFileContent(ClassFile $file, array $files): void
{
if ($file->isFinalized())
return;
$fileDefinition = $this->getFileDefinition($file, $files);
$usings = $fileDefinition['usings'];
$isClass = $fileDefinition['isClass'];
$inheritance = $fileDefinition['inheritance'];
$content = $fileDefinition['content'];
$namespace = substr($file->getNamespace(), 0, strlen($file->getNamespace())-1);
$definition = "<?php" . PHP_EOL . PHP_EOL;
$definition .= "declare(strict_types=1);" . PHP_EOL . PHP_EOL;
$definition .= "namespace {$namespace};" . PHP_EOL . PHP_EOL;
foreach (array_unique($usings) as $using) {
$definition .= $using . PHP_EOL;
}
if (count($usings) > 0){
$definition .= PHP_EOL;
}
if ($isClass) {
$definition .= "class {$file->getName()}";
if($inheritance) {
$definition .= ' ' . $inheritance;
}
$definition .= PHP_EOL;
$definition .= "{" . PHP_EOL;
}
$definition .= $content . PHP_EOL;
if ($isClass) {
$definition .= "}";
}
$file->setDefinition($definition);
$file->setFinalized(true);
}
private function getFileDefinition(ClassFile $file, array $files): array
{
$usingsDefault = [];
$usingsFiles = [];
$isClass = true;
$inheritance = null;
switch ($file->getType()) {
// HandlingDomain:
case 'UseCase':
$responseContent = $this->getUseCaseContent($file);
$content = $responseContent['definition'];
break;
case 'UseCaseBuilder':
$usingsFiles = ['Request'];
$content = $this->getUseCaseBuilderContent($files);
break;
case 'UseCaseHandler':
$content = $this->getUseCaseHandlerContent($files);
break;
case 'UseCaseResult':
$content = $this->getUseCaseResultContent($files);
break;
// ApiDomain:
case 'RequestHandler':
$usingsDefault = [
"Psr\Http\Message\ResponseInterface",
"Psr\Http\Message\ServerRequestInterface",
"Psr\Http\Server\RequestHandlerInterface",
"Template\Infrastructure\Response\SuccessResponse",
"Template\Infrastructure\Request\Middleware\AnalyzeBodyMiddleware",
];
$inheritance = "implements RequestHandlerInterface";
$usingsFiles = ['UseCaseBuilder', 'UseCaseHandler', 'ResponseFormatter'];
$content = $this->getRequestHandlerContent($files);
break;
case 'ResponseFormatter':
$usingsFiles = ['UseCaseResult'];
$content = $this->getResponseFormatterContent($files);
break;
case 'routes':
$isClass = false;
$usingsDefault = self::ROUTER_USINGS;
$usingsFiles = ['RequestHandler'];
$content = $this->getRouterContent($files);
break;
// General:
case 'ConfigProvider':
$content = $this->getContentProviderContent($file->getDomain());
break;
case 'service_manager':
$isClass = false;
$usingsDefault = self::SERVICE_MANAGER_USINGS;
if ($file->getDomain() === 'Api') {
$usingsFiles = ['RequestHandler', 'ResponseFormatter'];
}
if ($file->getDomain() === 'Handling') {
$usingsFiles = ['UseCaseHandler', 'UseCaseBuilder'];
}
$content = $this->getServiceManagerContent(
$this->getFilesOfType($files, $usingsFiles)
);
break;
// Request / Response Types
default:
$isClass = true;
$content = $file->getDefinition();
break;
}
return [
'isClass' => $isClass,
'content' => $content,
'inheritance' => $inheritance,
'usings' => array_map(
function ($using) { return "use {$using};";},
array_merge(
$file->getPredefinedUsings(),
$usingsDefault,
array_map(
function (ClassFile $file) { return $file->getNamespace() . $file->getName(); },
$this->getFilesOfType($files, $usingsFiles)
)
)
)
];
}
private function getUseCaseContent(ClassFile $file): array
{
$usings = [];
$properties = ($file->getApi()->getRequest()?->getProperties() ?? []);
$propertyDefinitions = [];
$propertyUsings = [];
/**
* @var string $name
* @var SchemaType $type
*/
foreach ($properties as $name => $type) {
$propertyDefinition = $this->propertyBuilder->build($file, $name, $type);
$propertyDefinitions[] = $propertyDefinition;
$propertyUsings = array_merge($propertyUsings, $propertyDefinition->getUseUsings());
}
$lines = [];
$lines[] = "public function __construct(";
/** @var Property $propertyDefinition */
foreach ($propertyDefinitions as $propertyDefinition) {
$lines[] = "\tprivate readonly " . $propertyDefinition->getType() . " $" . $propertyDefinition->getName() . ',';
}
$lines[] = ") {";
$lines[] = "}";
/** @var Property $propertyDefinition */
foreach ($propertyDefinitions as $propertyDefinition) {
$lines[] = null;
$lines[] = "public function get" . ucfirst($propertyDefinition->getName()) . '(): ' .$propertyDefinition->getType();
$lines[] = "{";
$lines[] = "\treturn \$this->{$propertyDefinition->getName()};";
$lines[] = "}";
}
$file->setPredefinedUsings($propertyUsings);
return [
'definition' => implode(
PHP_EOL,
array_map(
function (?string $line) { return $line === null ? '' : "\t" . $line; },
$lines
)
),
'usings' => $usings,
];
}
private function getUseCaseBuilderContent(array $files): string
{
/** @var ClassFile $builder */
$builder = $this->getFilesOfType($files, ['UseCaseBuilder'])[0];
/** @var ClassFile $cqrs */
$cqrs = $this->getFilesOfType($files, ['UseCase'])[0];
$properties = ($cqrs->getApi()->getRequest()?->getProperties() ?? []);
$propertyDefinitions = [];
$usings = [];
/**
* @var string $name
* @var SchemaType $type
*/
foreach ($properties as $name => $type) {
$propertyDefinition = $this->propertyBuilder->build($cqrs, $name, $type);
$propertyDefinitions[] = $propertyDefinition;
$usings = array_merge($usings, $propertyDefinition->getUseUsings());
}
$lines = [];
$lines[] = "public function build(";
foreach ($propertyDefinitions as $propertyDefinition) {
$lines[] = "\t" . $propertyDefinition->getType() . " $" . $propertyDefinition->getName() . ',';
}
$lines[] = "): {$cqrs->getName()} {";
$lines[] = "\treturn new {$cqrs->getName()}(";
foreach ($propertyDefinitions as $propertyDefinition) {
$lines[] = "\t\t\${$propertyDefinition->getName()},";
}
$lines[] = "\t);";
$lines[] = "}";
$builder->setPredefinedUsings($usings);
return implode(
PHP_EOL,
array_map(
function (?string $line) { return $line === null ? '' : "\t" . $line; },
$lines
)
);
}
private function getUseCaseResultContent(array $files): string
{
$lines[] = "public function __construct(";
$lines[] = ") {";
$lines[] = "}";
return implode(
PHP_EOL,
array_map(
function (?string $line) { return $line === null ? '' : "\t" . $line; },
$lines
)
);
}
private function getUseCaseHandlerContent(array $files): string
{
/** @var ClassFile $result */
$result = $this->getFilesOfType($files, ['UseCaseResult'])[0];
/** @var ClassFile $cqrs */
$cqrs = $this->getFilesOfType($files, ['UseCase'])[0];
$lines = [];
$lines[] = "public function __construct(";
$lines[] = ") {";
$lines[] = "}";
$lines[] = null;
$lines[] = "public function handle({$cqrs->getName()} \$useCase): {$result->getName()}";
$lines[] = "{";
$lines[] = "\treturn new {$result->getName()}();";
$lines[] = "}";
return implode(
PHP_EOL,
array_map(
function (?string $line) { return $line === null ? '' : "\t" . $line; },
$lines
)
);
}
private function getResponseFormatterContent(array $files): string
{
/** @var ClassFile $result */
$result = $this->getFilesOfType($files, ['UseCaseResult'])[0];
/** @var ClassFile $responseFormatter */
$responseFormatter = $this->getFilesOfType($files, ['ResponseFormatter'])[0];
$propertyDefinitions = [];
$propertyUsings = [];
/**
* @var string $name
* @var SchemaType $type
*/
foreach (($result->getApi()->getResponse()?->getProperties() ?? []) as $name => $type) {
$propertyDefinition = $this->propertyBuilder->build($result, $name, $type);
$propertyDefinitions[] = $propertyDefinition;
$propertyUsings = array_merge($propertyUsings, $propertyDefinition->getCreateUsings());
}
$lines = [];
$lines[] = "public function format({$result->getName()} \$result): array";
$lines[] = "{";
$lines[] = "\treturn [";
foreach ($propertyDefinitions as $propertyDefinition) {
$lines[] = "\t\t'{$propertyDefinition->getName()}' => ,";
}
$lines[] = "\t];";
$lines[] = "}";
$responseFormatter->setPredefinedUsings($propertyUsings);
return implode(
PHP_EOL,
array_map(
function (?string $line) { return $line === null ? '' : "\t" . $line; },
$lines
)
);
}
private function getRequestHandlerContent(array $files): string
{
/** @var ClassFile $requestHandler */
$requestHandler = $this->getFilesOfType($files, ['RequestHandler'])[0];
/** @var ClassFile $useCaseHandler */
$useCaseHandler = $this->getFilesOfType($files, ['UseCaseHandler'])[0];
/** @var ClassFile $useCaseBuilder */
$useCaseBuilder = $this->getFilesOfType($files, ['UseCaseBuilder'])[0];
/** @var ClassFile $responseFormatter */
$responseFormatter = $this->getFilesOfType($files, ['ResponseFormatter'])[0];
$propertyDefinitions = [];
$propertyUsings = [];
/**
* @var string $name
* @var SchemaType $type
*/
foreach (($useCaseHandler->getApi()->getRequest()?->getProperties() ?? []) as $name => $type) {
$propertyDefinition = $this->propertyBuilder->build($useCaseHandler, $name, $type);
$propertyDefinitions[] = $propertyDefinition;
$propertyUsings = array_merge($propertyUsings, $propertyDefinition->getCreateUsings());
}
$lines = [];
$lines[] = "public function __construct(";
$lines[] = "\tprivate readonly {$useCaseBuilder->getName()} \$builder,";
$lines[] = "\tprivate readonly {$useCaseHandler->getName()} \$handler,";
$lines[] = "\tprivate readonly {$responseFormatter->getName()} \$responseFormatter,";
$lines[] = ") {";
$lines[] = "}";
$lines[] = null;
$lines[] = "public function handle(ServerRequestInterface \$request): ResponseInterface";
$lines[] = "{";
$lines[] = "\t\$data = \$request->getAttribute(AnalyzeBodyMiddleware::JSON_DATA);";
$lines[] = null;
$lines[] = "\t\$useCase = \$this->builder->build(";
/** @var Property $propertyDefinition */
foreach ($propertyDefinitions as $propertyDefinition) {
$value = "\$data['{$propertyDefinition->getName()}']";
$lines[] = "\t\t" .sprintf($propertyDefinition->getFormat(), $value) . ',';
}
$lines[] = "\t);";
$lines[] = "\t\$result = \$this->handler->handle(\$useCase);";
$lines[] = null;
$lines[] = "\t\$response = \$this->responseFormatter->format(\$result);";
$lines[] = "\treturn new SuccessResponse(\$response);";
$lines[] = "}";
$requestHandler->setPredefinedUsings($propertyUsings);
return implode(
PHP_EOL,
array_map(
function (?string $line) { return $line === null ? '' : "\t" . $line; },
$lines
)
);
}
private function getContentProviderContent(string $domain): string
{
$lines = [];
$lines[] = "public function __invoke(): array";
$lines[] = "{";
$lines[] = "\treturn [";
$lines[] = "\t\t'dependencies' => require __DIR__ . '/./../config/service_manager.php',";
if ($domain === 'Api') {
$lines[] = "\t\t'routes' => require __DIR__ . '/./../config/routes.php',";
}
$lines[] = "\t];";
$lines[] = "}";
return implode(
PHP_EOL,
array_map(
function (?string $line) { return $line === null ? '' : "\t" . $line; },
$lines
)
);
}
private function getServiceManagerContent(array $files): string
{
$definition = "return [" . PHP_EOL;
$definition .= "\t'factories' => [" . PHP_EOL;
$lastUseCase = '';
/** @var ClassFile $file */
foreach ($files as $file) {
if ($lastUseCase !== $file->getApi()->getUseCase()) {
$lastUseCase = $file->getApi()->getUseCase();
$definition .= "\t\t/// " . $lastUseCase . PHP_EOL;
}
$definition .= "\t\t" . $file->getName() . '::class => AutoWiringFactory::class,' . PHP_EOL;
}
$definition .= "\t]" . PHP_EOL;
$definition .= "];";
return $definition;
}
private function getRouterContent(array $files) : string
{
$apiHandlerFiles = $this->getFilesOfType($files, ['RequestHandler']);
$definition = "return [" . PHP_EOL;
/** @var ClassFile $apiHandlerFile */
foreach ($apiHandlerFiles as $apiHandlerFile) {
$namespace = lcfirst($apiHandlerFile->getApi()->getNamespace());
$method = strtoupper($apiHandlerFile->getApi()->getMethod()->value);
$apiHandler = $apiHandlerFile->getName();
$definition .= "\t[" . PHP_EOL;
$definition .= "\t\t'name' => '{$namespace}.{$apiHandlerFile->getApi()->getEndpoint()}'," . PHP_EOL;
$definition .= "\t\t'path' => '{$apiHandlerFile->getApi()->getPath()}'," . PHP_EOL;
$definition .= "\t\t'allowed_methods' => ['{$method}']," . PHP_EOL;
$definition .= "\t\t'middleware' => [" . PHP_EOL;
$definition .= "\t\t\tSchemaValidationMiddleware::class," . PHP_EOL;
$definition .= "\t\t\tLoggedInUserMiddleware::class," . PHP_EOL;
$definition .= "\t\t\tEnsureAuthorizationMiddleware::class," . PHP_EOL;
$definition .= "\t\t\t{$apiHandler}::class," . PHP_EOL;
$definition .= "\t\t]," . PHP_EOL;
$definition .= "\t]," . PHP_EOL;
}
$definition .= "];";
return $definition;
}
}

View File

@ -0,0 +1,82 @@
<?php
namespace Template\Infrastructure\Schema\Exporter\Frontend\Builder;
use Laminas\ServiceManager\ServiceManager;
use Template\Infrastructure\Schema\Enum\SchemaType;
use Template\Infrastructure\Schema\Exporter\Frontend\Model\Property;
use Template\Infrastructure\Schema\Model\Type;
class PropertyBuilder
{
public function __construct(
private readonly ServiceManager $serviceManager,
) {
}
public function build(string $parentType, string $name, Type $type): Property {
$requiredFlag = ($type->getIsRequired() ?? true) ? '' : '?';
$normalizedType = $this->normalizeFrontendType($parentType, $name, $type);
$types = [$normalizedType['normalizedType']];
if ($type->getEnum() !== null) {
$format = match ($type->getType()) {
SchemaType::string => '"%s"',
default => "%s"
};
$types = [];
foreach ($type->getEnum() as $enumValue) {
$types[] = sprintf($format, $enumValue);
}
}
if($type->getIsNullable() ?? false) {
$types = array_merge($types, ['null']);
}
if(!($type->getIsRequired() ?? true)) {
$types = array_merge($types, ['undefined']);
}
return new Property(
$name,
sprintf("%s%s: %s;", $name, $requiredFlag, implode('|', $types)),
$normalizedType['definition']
);
}
private function normalizeFrontendType(string $parent, string $name, Type $type) : array
{
$definition = null;
switch ($type->getType()) {
case SchemaType::integer:
$normalizedType = 'number';
break;
case SchemaType::object:
/** @var TypeBuilder $typeBuilder */
$typeBuilder = $this->serviceManager->get(TypeBuilder::class);
$frontendType = $typeBuilder->build($parent, ucfirst($name), $type->getProperties());
$normalizedType = $frontendType->getName();
$definition = $frontendType->getDefinition();
break;
case SchemaType::array:
$singular = ucfirst(substr($name, 0, strlen($name) - 1));
$normalizedArrayItemType = $this->normalizeFrontendType($parent, $singular, $type->getItemType());
$normalizedType = $normalizedArrayItemType['normalizedType'] . '[]';
$definition = $normalizedArrayItemType['definition'];
break;
default:
$normalizedType = $type->getType()->value;
break;
};
return [
'normalizedType' => $normalizedType,
'definition' => $definition
];
}
}

View File

@ -0,0 +1,76 @@
<?php
namespace Template\Infrastructure\Schema\Exporter\Frontend\Builder;
use Template\Infrastructure\Schema\Exporter\Frontend\Model\Service;
use Template\Infrastructure\Schema\Model\Api;
class ServiceBuilder
{
public function __construct(
) {
}
public function build(string $namespace, array $apis): Service {
$definition =
'import { Injectable } from "@angular/core";' . PHP_EOL .
'import { Observable } from "rxjs";' . PHP_EOL .
'import { RequestService } from "src/app/core/services/request.service";';
// build imports
/** @var Api $api */
foreach ($apis as $api) {
$definition = $definition. PHP_EOL . $this->buildFrontendServiceImport($api);
}
$definition = $definition . PHP_EOL . PHP_EOL .
"@Injectable()" . PHP_EOL .
sprintf("export class %sService {", $namespace) . PHP_EOL .
"\tconstructor(private requestService: RequestService) {" . PHP_EOL .
"\t}" . PHP_EOL;
// build methods
foreach ($apis as $api) {
$definition = $definition. PHP_EOL . $this->buildFrontendServiceMethod($api);
}
$definition = $definition . '}';
return new Service($definition);
}
private function buildFrontendServiceImport(Api $api): string {
$endpoint = $api->getEndpoint();
$endpointName = implode(
array_map(
function ($item) {
return ucfirst($item);
},
explode('-', $endpoint)
)
);
return "import { {$endpointName}Request, {$endpointName}Response } from '../models/{$endpoint}.model';";
}
private function buildFrontendServiceMethod(Api $api): string {
$endpointName = implode(
array_map(
function ($item) {
return ucfirst($item);
},
explode('-', $api->getEndpoint())
)
);
$methodName = lcfirst($endpointName);
$path = str_replace('/api/', '', $api->getPath());
$apiMethod = $api->getMethod()->value;
$method = "\t" . "$methodName(body: {$endpointName}Request): Observable<{$endpointName}Response> {" . PHP_EOL;
$method = $method . "\t\t" . "return this.requestService.call<{$endpointName}Response>(" . PHP_EOL;
$method = $method . "\t\t\t" . "'{$apiMethod}'," . PHP_EOL;
$method = $method . "\t\t\t" . "'{$path}'," . PHP_EOL;
$method = $method . "\t\t\t" . "body" . PHP_EOL;
$method = $method . "\t\t" . ");" . PHP_EOL;
return $method . "\t" . "}" . PHP_EOL;
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace Template\Infrastructure\Schema\Exporter\Frontend\Builder;
use Template\Infrastructure\Schema\Exporter\Frontend\Model\Type;
use Template\Infrastructure\Schema\Model\Type as SchemaType;
class TypeBuilder
{
public function __construct(
private readonly PropertyBuilder $propertyBuilder
) {
}
public function build(string $parent, string $name, array $properties): Type
{
$typeName = implode([$parent, $name]);
$typeDefinition = "export interface " . $typeName . " {" . PHP_EOL;
$subTypeDefinitions = [];
/**
* @var string $name
* @var SchemaType $type
*/
foreach ($properties as $name => $type) {
$frontendProperty = $this->propertyBuilder->build($typeName, $name, $type);
$typeDefinition = $typeDefinition . ' ' . $frontendProperty->getDefinition() . PHP_EOL;
if ($frontendProperty->getSubtypeDefinition() !== null)
$subTypeDefinitions[] = $frontendProperty->getSubtypeDefinition();
}
$typeDefinition = $typeDefinition . '}' . PHP_EOL;
return new Type(
$typeName,
implode(PHP_EOL, array_merge([$typeDefinition], $subTypeDefinitions))
);
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace Template\Infrastructure\Schema\Exporter\Frontend\Model;
class Property
{
public function __construct(
private readonly string $name,
private readonly string $definition,
private readonly ?string $subtypeDefinition,
) {
}
public function getName(): string
{
return $this->name;
}
public function getDefinition(): string
{
return $this->definition;
}
public function getSubtypeDefinition(): ?string
{
return $this->subtypeDefinition;
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace Template\Infrastructure\Schema\Exporter\Frontend\Model;
class Service
{
public function __construct(
private readonly string $definition
) {
}
public function getDefinition(): string
{
return $this->definition;
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace Template\Infrastructure\Schema\Exporter\Frontend\Model;
class Type
{
public function __construct(
private readonly string $name,
private readonly string $definition,
) {
}
public function getName(): string
{
return $this->name;
}
public function getDefinition(): string
{
return $this->definition;
}
}

View File

@ -0,0 +1,91 @@
<?php
namespace Template\Infrastructure\Schema\Exporter;
use Template\Infrastructure\Schema\Enum\SchemaType;
use Template\Infrastructure\Schema\Exporter\Frontend\Builder\ServiceBuilder;
use Template\Infrastructure\Schema\Exporter\Frontend\Builder\TypeBuilder;
use Template\Infrastructure\Schema\Exporter\Frontend\Model\Type;
use Template\Infrastructure\Schema\Model\Api;
use Template\Infrastructure\Schema\Model\Type as SchemaTypeModel;
use Template\Infrastructure\Schema\Reader\ApiDefinitionReader;
class FrontendExporter
{
public function __construct(
private readonly ApiDefinitionReader $apiDefinitionReader,
private readonly TypeBuilder $typeBuilder,
private readonly ServiceBuilder $serviceBuilder,
) {
}
public function export(string $exportRootDirectory = APP_ROOT . "/var/export/schema/fe-models/"): void
{
$namespaces = $this->apiDefinitionReader->read("");
foreach ($namespaces as $namespace => $apis) {
$namespaceDirectory = $exportRootDirectory . lcfirst($namespace);
$modelDirectory = $namespaceDirectory . '/models/';
$serviceDirectory = $namespaceDirectory . '/services/';
$this->assureDirectoryExists($namespaceDirectory);
$this->assureDirectoryExists($modelDirectory);
$this->assureDirectoryExists($serviceDirectory);
/** @var Api $api */
foreach ($apis as $api) {
$apiModels = [];
$apiModels[] = $this->buildFrontendModel($api, $api->getRequest(), "request");
$apiModels[] = $this->buildFrontendModel($api, $api->getResponse(), "response");
$modelFilePath = $modelDirectory . $api->getEndpoint() . '.model.ts';
file_put_contents(
$modelFilePath,
implode(
PHP_EOL,
array_map(
function (Type $apiModel) {
return $apiModel->getDefinition();
},
$apiModels
)
)
);
}
$serviceFilePath = $serviceDirectory . lcfirst($namespace) . '.service.ts';
file_put_contents(
$serviceFilePath,
$this->serviceBuilder->build(
$namespace,
$apis
)->getDefinition()
);
}
}
private function buildFrontendModel(Api $api, ?SchemaTypeModel $type, string $name): Type
{
$endpoint = $api->getUseCase();
$name = (ucfirst($name));
if ($type === null) {
return $this->typeBuilder->build($endpoint, $name, []);
}
return match ($type->getType()) {
SchemaType::object => $this->typeBuilder->build($endpoint, $name, $type->getProperties()),
default => $this->typeBuilder->build($endpoint, $name, [lcfirst($name) => $type])
};
}
private function assureDirectoryExists(string $path): void {
if (!file_exists($path)) {
mkdir(
directory: $path,
recursive: true
);
}
}
}

View File

@ -0,0 +1,71 @@
<?php
namespace Template\Infrastructure\Schema\Factory;
use cebe\openapi\exceptions\UnresolvableReferenceException;
use cebe\openapi\Reader;
use cebe\openapi\ReferenceContext;
use cebe\openapi\spec\OpenApi;
use Laminas\ServiceManager\Factory\FactoryInterface;
use Psr\Container\ContainerInterface;
use RuntimeException;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;
use Throwable;
use Template\Infrastructure\Schema\Exception\FileNotFoundException;
use Template\Infrastructure\Schema\Exception\UnsupportedContentTypeException;
class OpenApiFactory implements FactoryInterface
{
private const SCHEMA_PATH = APP_ROOT . '/src/ApiDomain/Schema';
private const SCHEMA_FILE_NAME = 'api.schema.json';
/**
* @throws UnresolvableReferenceException
* @throws RuntimeException
* @throws FileNotFoundException
* @throws UnsupportedContentTypeException
*/
public function __invoke(
ContainerInterface $container,
$requestedName,
?array $options = null
): OpenApi
{
$schemaFile = $this->findSchemaDefinitionFile();
try {
$schema = Reader::readFromJson($schemaFile->getContents());
} catch (Throwable) {
throw new UnsupportedContentTypeException(sprintf(
'File %s does not contain valid JSON',
$schemaFile->getRealPath(),
));
}
$schema->resolveReferences(new ReferenceContext($schema, self::SCHEMA_PATH . '/' . self::SCHEMA_FILE_NAME));
return $schema;
}
/**
* @throws FileNotFoundException
*/
private function findSchemaDefinitionFile(): SplFileInfo
{
try {
$finder = (new Finder())->files()->in(self::SCHEMA_PATH)->name(self::SCHEMA_FILE_NAME);
$iterator = $finder->getIterator();
$iterator->rewind();
return $iterator->current();
} catch (Throwable) {
throw new FileNotFoundException(sprintf(
'File with name "%s" not found in path: %s',
self::SCHEMA_FILE_NAME,
self::SCHEMA_PATH
));
}
}
}

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Template\Infrastructure\Schema\Middleware;
use cebe\openapi\spec\OpenApi;
use League\OpenAPIValidation\PSR15\Exception\InvalidResponseMessage;
use League\OpenAPIValidation\PSR15\Exception\InvalidServerRequestMessage;
use League\OpenAPIValidation\PSR15\ValidationMiddleware;
use League\OpenAPIValidation\PSR15\ValidationMiddlewareBuilder;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class SchemaValidationMiddleware implements MiddlewareInterface
{
public function __construct(
private readonly OpenApi $schema
) {
}
/**
* @throws InvalidServerRequestMessage
* @throws InvalidResponseMessage
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
/** @var ValidationMiddleware $validationMiddleware */
$validationMiddleware = (new ValidationMiddlewareBuilder())
->fromSchema($this->schema)
->getValidationMiddleware();
return $validationMiddleware->process($request, $handler);
}
}

View File

@ -0,0 +1,98 @@
<?php
namespace Template\Infrastructure\Schema\Model;
use Template\Infrastructure\Schema\Enum\Method;
class Api
{
private Method $method;
private ?Type $request = null;
private ?Type $response = null;
public function __construct(
private readonly string $path
) {
$this->method = Method::get;
}
public function getPath(): string
{
return $this->path;
}
public function getMethod(): Method
{
return $this->method;
}
public function setMethod(Method $method): void
{
$this->method = $method;
}
public function getRequest(): ?Type
{
return $this->request;
}
public function setRequest(?Type $request): void
{
$this->request = $request;
}
public function getResponse(): ?Type
{
return $this->response;
}
public function setResponse(?Type $response): void
{
$this->response = $response;
}
//
// Getter for path substrings
//
private function getPathParts(): array {
return array_map(
function (string $name) {
if ($name === 'api') // remove '/api'
return '';
if (str_starts_with($name, '{') || str_ends_with($name, '}')) // remove e.g. '{locationId}'
return '';
return $name;
},
explode("/", $this->path)
);
}
public function getNamespace(): string {
$pathParts = $this->getPathParts();
$namespace = implode(
'',
array_map(
function (string $name) {
return ucfirst($name);
},
array_slice($pathParts, 0, count($pathParts) - 1)
)
);
return $namespace === '' ? 'Api' : $namespace;
}
public function getUseCase(): string {
return implode(
array_map(
function (string $name) {
return ucfirst($name);
},
explode('-', $this->getEndpoint())
)
);
}
public function getEndpoint(): string {
$pathParts = $this->getPathParts();
return end($pathParts);
}
}

View File

@ -0,0 +1,93 @@
<?php
namespace Template\Infrastructure\Schema\Model;
use Template\Infrastructure\Schema\Enum\SchemaType;
class Type
{
private ?bool $isRequired = null;
private ?bool $isNullable = null;
private ?array $enum = null;
private ?string $pattern = null;
private ?string $format = null;
// schemaType === object
private ?array $properties = null;
// schemaType === array
private ?Type $itemType = null;
public function __construct(
private readonly SchemaType $type
) {
}
public function getType(): SchemaType
{
return $this->type;
}
public function getIsRequired(): ?bool
{
return $this->isRequired;
}
public function setIsRequired(?bool $isRequired): void
{
$this->isRequired = $isRequired;
}
public function getIsNullable(): ?bool
{
return $this->isNullable;
}
public function setIsNullable(?bool $isNullable): void
{
$this->isNullable = $isNullable;
}
public function getEnum(): ?array
{
return $this->enum;
}
public function setEnum(?array $enum): void
{
$this->enum = $enum;
}
public function getPattern(): ?string
{
return $this->pattern;
}
public function setPattern(?string $pattern): void
{
$this->pattern = $pattern;
}
public function getFormat(): ?string
{
return $this->format;
}
public function setFormat(?string $format): void
{
$this->format = $format;
}
public function getProperties(): ?array
{
return $this->properties;
}
public function setProperties(?array $properties): void
{
$this->properties = $properties;
}
public function getItemType(): ?Type
{
return $this->itemType;
}
public function setItemType(?Type $itemType): void
{
$this->itemType = $itemType;
}
}

View File

@ -0,0 +1,141 @@
<?php
namespace Template\Infrastructure\Schema\Reader;
use cebe\openapi\exceptions\UnresolvableReferenceException;
use cebe\openapi\Reader;
use cebe\openapi\ReferenceContext;
use cebe\openapi\spec\OpenApi;
use Exception;
use Laminas\ServiceManager\Factory\FactoryInterface;
use Psr\Container\ContainerInterface;
use RuntimeException;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;
use Throwable;
use Template\Infrastructure\Schema\Enum\Method;
use Template\Infrastructure\Schema\Enum\SchemaType;
use Template\Infrastructure\Schema\Exception\FileNotFoundException;
use Template\Infrastructure\Schema\Exception\UnsupportedContentTypeException;
use Template\Infrastructure\Schema\Model\Api;
use Template\Infrastructure\Schema\Model\Type;
class ApiDefinitionReader
{
public function __construct(
private readonly OpenApi $schema
) {
}
public function read(?string $pathRegex): array
{
$namespaces = [];
$schemaDefinition = json_decode(
json_encode($this->schema->getSerializableData()),
true
);
foreach ($schemaDefinition['paths'] as $path => $definition) {
if (!$this->regexMatches($pathRegex, $path)) {
continue;
}
$api = new Api($path);
if (key_exists('get', $definition)) {
$api->setMethod(Method::get);
$api->setRequest(null);
}
if (key_exists('post', $definition)) {
$requestSchema = $definition['post']['requestBody']['content']['application/json']['schema'] ?? [];
$api->setMethod(Method::post);
$api->setRequest($this->resolveSchema($requestSchema));
}
$responseSchema = $definition[$api->getMethod()->value]['responses']['200']['content']['application/json']['schema'] ?? [];
$api->setResponse($this->resolveSchema($responseSchema));
$namespaces[$api->getNamespace()][] = $api;
}
return $namespaces;
}
private function regexMatches(?string $pathRegex, $path): bool {
if($pathRegex === null) {
return true;
}
return preg_match(
sprintf(
"~%s~",
$pathRegex
),
$path
) !== false;
}
private function resolveSchema(array $definition): ?Type {
if (empty($definition)) {
return null;
}
if (!key_exists('type', $definition)) {
return null;
}
switch ($definition['type']) {
case 'string':
$type = new Type(SchemaType::string);
break;
case 'number':
$type = new Type(SchemaType::number);
break;
case 'integer':
$type = new Type(SchemaType::integer);
break;
case 'boolean':
$type = new Type(SchemaType::boolean);
break;
case 'array':
$type = new Type(SchemaType::array);
$type->setItemType($this->resolveSchema($definition['items']));
break;
case 'object':
$type = new Type(SchemaType::object);
$properties = [];
foreach (($definition['properties'] ?? []) as $name => $definedProperty) {
$property = $this->resolveSchema($definedProperty);
$property->setIsRequired(in_array($name, $definition['required']));
$properties[$name] = $property;
}
foreach ($definition['required'] as $name) {
if (!key_exists($name, $properties)) {
$property = new Type(SchemaType::any);
$property->setIsRequired(true);
$property->setIsNullable(true);
$properties[$name] = $property;
}
}
$type->setProperties($properties);
break;
default:
$type = new Type(SchemaType::any);
break;
}
$type->setIsNullable($definition['nullable'] ?? false);
$type->setEnum($definition['enum'] ?? null);
$type->setPattern($definition['pattern'] ?? null);
$type->setFormat($definition['format'] ?? null);
return $type;
}
}