diff --git a/.gitignore b/.gitignore
index 23a615a..8148750 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,6 +11,8 @@
/var/
/vendor/
+docker/docker-compose.yml
+
*.env
composer.lock
composer.development.json
diff --git a/bin/createApi.php b/bin/createApi.php
index 8c52026..9f7c360 100644
--- a/bin/createApi.php
+++ b/bin/createApi.php
@@ -201,8 +201,8 @@ use Psr\\Http\\Server\\RequestHandlerInterface;
class {$apiHandlerName} implements RequestHandlerInterface
{
public function __construct(
- private readonly {$cqrsHandlerName} \${$cqrsHandlerVariableName},
- private readonly {$cqrsBuilderName} \${$cqrsBuilderVariableName},
+ private readonly {$cqrsHandlerName} \$handler,
+ private readonly {$cqrsBuilderName} \$builder,
private readonly {$apiResponseFormatterName} \$responseFormatter,
) {
}
@@ -211,10 +211,10 @@ class {$apiHandlerName} implements RequestHandlerInterface
{
\$data = \$request->getAttribute(AnalyzeBodyMiddleware::JSON_DATA);
- \${$cqrsVariableName} = \$this->{$cqrsBuilderVariableName}->build(
+ \${$cqrsVariableName} = \$this->builder->build(
\$data
);
- \$result = \$this->{$cqrsHandlerVariableName}->execute(\${$cqrsVariableName});
+ \$result = \$this->handler->execute(\${$cqrsVariableName});
return new SuccessResponse(\$this->responseFormatter->format(\$result));
}
@@ -251,10 +251,10 @@ namespace {$cqrsHandlerNamespace};
class {$cqrsName}
{
public function __construct(
- #TODO
- ) {
+ #TODO
+ ) {
}
-
+
#TODO
}
";
@@ -270,14 +270,12 @@ class {$cqrsHandlerName}
{
public function __construct(
#TODO
- ) {
+ ) {
}
-
+
public function execute({$cqrsName} \${$cqrsVariableName}): {$cqrsResultName}
{
- return new {$cqrsResultName}(
-
- );
+ return new {$cqrsResultName}();
}
}
";
diff --git a/bin/script/firstRun b/bin/script/firstRun
new file mode 100755
index 0000000..caf4293
--- /dev/null
+++ b/bin/script/firstRun
@@ -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"
diff --git a/bin/script/init b/bin/script/init
index 508c7cc..de4c059 100755
--- a/bin/script/init
+++ b/bin/script/init
@@ -3,54 +3,16 @@
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
PROJECT_DIR=$(realpath $SCRIPT_DIR/../../)
ENV_DIR=$(realpath $PROJECT_DIR/../../../)
-
-EXIT=0
+source $ENV_DIR/bin/denv_msg
# Check .env file
-if [ ! -f "$PROJECT_DIR/.env" ]
-then
+if [ ! -f "$PROJECT_DIR/.env" ] ; then
cp "$PROJECT_DIR/.env.example" "$PROJECT_DIR/.env"
- EXIT=1
+ denv_info_msg "[backend] Created .env"
fi
# Check docker-compose.yml file
-if [ ! -f "$PROJECT_DIR/docker/docker-compose.yml" ]
-then
- cp "$PROJECT_DIR/docker/docker-compose.yml.dist" "$PROJECT_DIR/docker/docker-compose.yml"
- EXIT=1
+if [ ! -f "$PROJECT_DIR/docker/docker-compose.yml" ] ; then
+ cp "$PROJECT_DIR/docker/docker-compose.yml.dist" "$PROJECT_DIR/docker/docker-compose.yml"
+ denv_info_msg "[backend] Created docker-compose.yml"
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
\ No newline at end of file
diff --git a/bin/script/update b/bin/script/update
new file mode 100755
index 0000000..8402710
--- /dev/null
+++ b/bin/script/update
@@ -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"
\ No newline at end of file
diff --git a/config/autoload/authorization.global.php b/config/autoload/authorization.global.php
index 1ad1311..0c6dfa8 100644
--- a/config/autoload/authorization.global.php
+++ b/config/autoload/authorization.global.php
@@ -10,7 +10,8 @@ return [
'user' => [
],
'admin' => [
- 'user.create-user',
+ 'user.create',
+ 'user.read-list',
],
]
]
diff --git a/config/autoload/mail.global.php b/config/autoload/mail.global.php
new file mode 100644
index 0000000..a5b855f
--- /dev/null
+++ b/config/autoload/mail.global.php
@@ -0,0 +1,17 @@
+ [
+ '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'],
+ ]
+ ]
+];
\ No newline at end of file
diff --git a/config/autoload/notification.global.php b/config/autoload/notification.global.php
new file mode 100644
index 0000000..2f74213
--- /dev/null
+++ b/config/autoload/notification.global.php
@@ -0,0 +1,9 @@
+ [
+ 'defaultTitle' => $_ENV['NOTIFICATION_DEFAULT_TITLE'] ?? 'Template',
+ 'host' => $_ENV['NOTIFICATION_HOST'],
+ 'id' => $_ENV['NOTIFICATION_ID'],
+ ]
+];
\ No newline at end of file
diff --git a/config/config.php b/config/config.php
index 814b150..35d96fb 100644
--- a/config/config.php
+++ b/config/config.php
@@ -52,6 +52,9 @@ $aggregator = new ConfigAggregator([
\Template\Infrastructure\Rbac\ConfigProvider::class,
\Template\Infrastructure\Request\ConfigProvider::class,
\Template\Infrastructure\Session\ConfigProvider::class,
+ \Template\Infrastructure\Mail\ConfigProvider::class,
+ \Template\Infrastructure\Notification\ConfigProvider::class,
+ \Template\Infrastructure\Schema\ConfigProvider::class,
// HandlingDomain
\Template\Handling\User\ConfigProvider::class,
@@ -63,7 +66,7 @@ $aggregator = new ConfigAggregator([
\Template\API\Console\ConfigProvider::class,
/// External
- \Template\API\External\Health\ConfigProvider::class,
+ \Template\API\External\Api\ConfigProvider::class,
\Template\API\External\User\ConfigProvider::class,
\Template\API\External\Authentication\ConfigProvider::class,
diff --git a/config/pipeline.php b/config/pipeline.php
index 4d19afa..aeaaeb4 100644
--- a/config/pipeline.php
+++ b/config/pipeline.php
@@ -2,7 +2,7 @@
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\AnalyzeHeaderMiddleware;
use Template\Infrastructure\Session\Middleware\SessionMiddleware;
@@ -68,7 +68,7 @@ return function (Application $app, MiddlewareFactory $factory, ContainerInterfac
//// Pre Template Space
- $app->pipe(ExceptionHandlerMiddleware::class);
+ $app->pipe(TemplateExceptionHandlerMiddleware::class);
$app->pipe(AnalyzeHeaderMiddleware::class);
$app->pipe(AnalyzeBodyMiddleware::class);
diff --git a/data/mails/registration/assets/icon.png b/data/mails/registration/assets/icon.png
new file mode 100644
index 0000000..07e8b9d
Binary files /dev/null and b/data/mails/registration/assets/icon.png differ
diff --git a/data/mails/registration/template.latte b/data/mails/registration/template.latte
new file mode 100644
index 0000000..aba291e
--- /dev/null
+++ b/data/mails/registration/template.latte
@@ -0,0 +1,66 @@
+
+
+
+ Willkommen bei Template!
+
+
+
+
+
+
Hallo {$username},
+
+
Herzlich willkommen beim Template!
+
+
Ich konnte deine Anmeldung nun bestätigen.
+
Bitte klicke auf diesen Link um dein Passwort festzulegen.
+
Danach ist deine Registierung abgeschlossen und du kannst loslegen!
+
+
Mit grünen Grüßen,
+
{$growerName}
+
+
"Smoke weed everyday" - Snoop Dogg
+
+
+
diff --git a/data/mails/reset-password/assets/icon.png b/data/mails/reset-password/assets/icon.png
new file mode 100644
index 0000000..07e8b9d
Binary files /dev/null and b/data/mails/reset-password/assets/icon.png differ
diff --git a/data/mails/reset-password/template.latte b/data/mails/reset-password/template.latte
new file mode 100644
index 0000000..606d6b0
--- /dev/null
+++ b/data/mails/reset-password/template.latte
@@ -0,0 +1,63 @@
+
+
+
+ Passwort zurücksetzen
+
+
+
+
+
+
Hallo {$username},
+
+
Dein Passwort wurde zurückgesetzt.
+
Bitte klicke auf diesen Link um ein neues Passwort zu vergeben.
+
+
Wenn du dein Passwort nicht zurückgesetzt hast, kannst du diese Nachricht ignorieren!
+
+
Mit fleißigen Grüßen,
+
Der Beekeeper
+
+
+
diff --git a/data/migrations/business/Version20230922085011.php b/data/migrations/template/Version20230922085011.php
similarity index 100%
rename from data/migrations/business/Version20230922085011.php
rename to data/migrations/template/Version20230922085011.php
diff --git a/data/migrations/business/Version20230922092351.php b/data/migrations/template/Version20230922092351.php
similarity index 100%
rename from data/migrations/business/Version20230922092351.php
rename to data/migrations/template/Version20230922092351.php
diff --git a/data/migrations/business/Version20230922092754.php b/data/migrations/template/Version20230922092754.php
similarity index 100%
rename from data/migrations/business/Version20230922092754.php
rename to data/migrations/template/Version20230922092754.php
diff --git a/data/migrations/business/Version20230922101354.php b/data/migrations/template/Version20230922101354.php
similarity index 100%
rename from data/migrations/business/Version20230922101354.php
rename to data/migrations/template/Version20230922101354.php
diff --git a/data/migrations/business/Version20230922101355.php b/data/migrations/template/Version20230922101355.php
similarity index 100%
rename from data/migrations/business/Version20230922101355.php
rename to data/migrations/template/Version20230922101355.php
diff --git a/data/migrations/business/Version20230924113403.php b/data/migrations/template/Version20230924113403.php
similarity index 100%
rename from data/migrations/business/Version20230924113403.php
rename to data/migrations/template/Version20230924113403.php
diff --git a/data/migrations/template/Version20240827155813.php b/data/migrations/template/Version20240827155813.php
new file mode 100644
index 0000000..98c287b
--- /dev/null
+++ b/data/migrations/template/Version20240827155813.php
@@ -0,0 +1,37 @@
+addSql($sql);
+ }
+
+ public function down(Schema $schema): void
+ {
+ $this->addSql("DROP TABLE user_password_token;");
+ }
+}
diff --git a/data/migrations/template/Version20240911191301.php b/data/migrations/template/Version20240911191301.php
new file mode 100644
index 0000000..98f0102
--- /dev/null
+++ b/data/migrations/template/Version20240911191301.php
@@ -0,0 +1,41 @@
+addSql($sql);
+ }
+
+ public function down(Schema $schema): void
+ {
+ $this->addSql("DROP TABLE mail;");
+ }
+}
diff --git a/data/migrations/template/Version20241130201314.php b/data/migrations/template/Version20241130201314.php
new file mode 100644
index 0000000..2f927aa
--- /dev/null
+++ b/data/migrations/template/Version20241130201314.php
@@ -0,0 +1,31 @@
+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`");
+ }
+}
diff --git a/data/migrations/template/Version20241130202949.php b/data/migrations/template/Version20241130202949.php
new file mode 100644
index 0000000..c840d05
--- /dev/null
+++ b/data/migrations/template/Version20241130202949.php
@@ -0,0 +1,77 @@
+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;
+ ");
+ }
+}
diff --git a/docker/docker-compose-mac.yml.dist b/docker/docker-compose-mac.yml.dist
deleted file mode 100644
index 8fc3d8a..0000000
--- a/docker/docker-compose-mac.yml.dist
+++ /dev/null
@@ -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
\ No newline at end of file
diff --git a/src/DataDomain/Business/config/doctrine.php b/src/DataDomain/Business/config/doctrine.php
index 07bff44..9e24b55 100644
--- a/src/DataDomain/Business/config/doctrine.php
+++ b/src/DataDomain/Business/config/doctrine.php
@@ -46,7 +46,7 @@ return [
'migrations_configuration' => [
'orm_template' => [
- 'directory' => 'data/migrations/business',
+ 'directory' => 'data/migrations/template',
'name' => 'Doctrine Database Migrations for Template',
'namespace' => 'Template\Migrations\Template',
'table' => 'migrations',
diff --git a/src/Infrastructure/Database/src/AutowireRepositoryFactory.php b/src/Infrastructure/Database/src/AutowireRepositoryFactory.php
new file mode 100644
index 0000000..fa4caa9
--- /dev/null
+++ b/src/Infrastructure/Database/src/AutowireRepositoryFactory.php
@@ -0,0 +1,33 @@
+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);
+ }
+}
diff --git a/src/Infrastructure/Encryption/src/Client/EncryptionClient.php b/src/Infrastructure/Encryption/src/Client/EncryptionClient.php
index ef73e3a..baeb403 100644
--- a/src/Infrastructure/Encryption/src/Client/EncryptionClient.php
+++ b/src/Infrastructure/Encryption/src/Client/EncryptionClient.php
@@ -4,23 +4,15 @@ declare(strict_types=1);
namespace Template\Infrastructure\Encryption\Client;
-use Laminas\Crypt\Password\Bcrypt;
-
class EncryptionClient
{
- private readonly Bcrypt $bcrypt;
-
- public function __construct(
- ) {
- $this->bcrypt = new Bcrypt();
- $this->bcrypt->setCost(10);
- }
-
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 {
- return $this->bcrypt->verify($value, $encryptedValue);
+ return hash_equals($encryptedValue, crypt($value, $encryptedValue));
}
}
diff --git a/src/Infrastructure/Exception/config/service_manager.php b/src/Infrastructure/Exception/config/service_manager.php
index 060c7c0..99fb496 100644
--- a/src/Infrastructure/Exception/config/service_manager.php
+++ b/src/Infrastructure/Exception/config/service_manager.php
@@ -2,11 +2,11 @@
declare(strict_types=1);
-use Template\Infrastructure\Exception\Middleware\ExceptionHandlerMiddleware;
+use Template\Infrastructure\Exception\Middleware\TemplateExceptionHandlerMiddleware;
use Reinfi\DependencyInjection\Factory\AutoWiringFactory;
return [
'factories' => [
- ExceptionHandlerMiddleware::class => AutoWiringFactory::class,
+ TemplateExceptionHandlerMiddleware::class => AutoWiringFactory::class,
],
];
diff --git a/src/Infrastructure/Exception/src/ErrorCode.php b/src/Infrastructure/Exception/src/ErrorCode.php
index 72ad4d4..37af924 100644
--- a/src/Infrastructure/Exception/src/ErrorCode.php
+++ b/src/Infrastructure/Exception/src/ErrorCode.php
@@ -3,10 +3,13 @@
namespace Template\Infrastructure\Exception;
enum ErrorCode : string {
- case SomethingWentWrong = 'SomethingWentWrong';
-
- case NotFound = 'NotFound';
case AlreadyExists = 'AlreadyExists';
- case WrongCredentials = 'WrongCredentials';
+ case Failed = 'Failed';
+ case Invalid = 'Invalid';
+ case Maintenance = 'Maintenance';
case Mismatch = 'Mismatch';
+ case NotFound = 'NotFound';
+ case SomethingWentWrong = 'SomethingWentWrong';
+ case WrongCredentials = 'WrongCredentials';
+ case ValidationFailed = 'ValidationFailed';
}
\ No newline at end of file
diff --git a/src/Infrastructure/Exception/src/ErrorDomain.php b/src/Infrastructure/Exception/src/ErrorDomain.php
index d1af7a2..93232ef 100644
--- a/src/Infrastructure/Exception/src/ErrorDomain.php
+++ b/src/Infrastructure/Exception/src/ErrorDomain.php
@@ -3,11 +3,12 @@
namespace Template\Infrastructure\Exception;
enum ErrorDomain : string {
+ case Schema = 'Schema';
case Generic = 'Generic';
-
+ case Mail = 'Mail';
+ case Notification = 'Notification';
+ case Registration = 'Registration';
case Role = 'Role';
case User = 'User';
case UserPassword = 'UserPassword';
- case Registration = 'Registration';
- case Product = 'Product';
}
\ No newline at end of file
diff --git a/src/Infrastructure/Exception/src/Exception/Exception.php b/src/Infrastructure/Exception/src/Exception/TemplateException.php
similarity index 93%
rename from src/Infrastructure/Exception/src/Exception/Exception.php
rename to src/Infrastructure/Exception/src/Exception/TemplateException.php
index ff11d6a..30c44a1 100644
--- a/src/Infrastructure/Exception/src/Exception/Exception.php
+++ b/src/Infrastructure/Exception/src/Exception/TemplateException.php
@@ -2,10 +2,11 @@
namespace Template\Infrastructure\Exception\Exception;
+use Exception;
use Template\Infrastructure\Exception\ErrorCode;
use Template\Infrastructure\Exception\ErrorDomain;
-class Exception extends \Exception {
+class TemplateException extends Exception {
private readonly ErrorDomain $errorDomain;
private readonly ErrorCode $errorCode;
diff --git a/src/Infrastructure/Exception/src/Middleware/ExceptionHandlerMiddleware.php b/src/Infrastructure/Exception/src/Middleware/ExceptionHandlerMiddleware.php
deleted file mode 100644
index a3c715f..0000000
--- a/src/Infrastructure/Exception/src/Middleware/ExceptionHandlerMiddleware.php
+++ /dev/null
@@ -1,37 +0,0 @@
-handle($request);
- } catch (Exception $exception) {
- $this->logger->exception($exception);
-
- return new ErrorResponse(
- $exception->getErrorDomain(),
- $exception->getErrorCode(),
- $exception->getMessage()
- );
- }
- }
-}
diff --git a/src/Infrastructure/Exception/src/Middleware/TemplateExceptionHandlerMiddleware.php b/src/Infrastructure/Exception/src/Middleware/TemplateExceptionHandlerMiddleware.php
new file mode 100644
index 0000000..75f7ee8
--- /dev/null
+++ b/src/Infrastructure/Exception/src/Middleware/TemplateExceptionHandlerMiddleware.php
@@ -0,0 +1,71 @@
+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)
+ );
+ }
+ }
+}
diff --git a/src/Infrastructure/Logging/src/Logger/Logger.php b/src/Infrastructure/Logging/src/Logger/Logger.php
index a34c867..ac4e55a 100644
--- a/src/Infrastructure/Logging/src/Logger/Logger.php
+++ b/src/Infrastructure/Logging/src/Logger/Logger.php
@@ -2,8 +2,10 @@
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\Handler\StreamHandler;
use Psr\Log\LoggerInterface;
use Stringable;
use Throwable;
@@ -24,7 +26,7 @@ class Logger implements LoggerInterface
'trace' => $exception->getTraceAsString()
]*/;
- if ($exception instanceof Exception) {
+ if ($exception instanceof TemplateException) {
$exceptionContext = array_merge([
'errorDomain' => $exception->getErrorDomain()->value,
'errorCode' => $exception->getErrorCode()->value,
diff --git a/src/Infrastructure/Mail/config/service_manager.php b/src/Infrastructure/Mail/config/service_manager.php
new file mode 100644
index 0000000..ee178a2
--- /dev/null
+++ b/src/Infrastructure/Mail/config/service_manager.php
@@ -0,0 +1,12 @@
+ [
+ MailService::class => AutoWiringFactory::class,
+ ],
+];
diff --git a/src/Infrastructure/Mail/src/ConfigProvider.php b/src/Infrastructure/Mail/src/ConfigProvider.php
new file mode 100644
index 0000000..d86f527
--- /dev/null
+++ b/src/Infrastructure/Mail/src/ConfigProvider.php
@@ -0,0 +1,15 @@
+ require __DIR__ . './../config/service_manager.php',
+ ];
+ }
+}
diff --git a/src/Infrastructure/Mail/src/Exception/SendMailFailedException.php b/src/Infrastructure/Mail/src/Exception/SendMailFailedException.php
new file mode 100644
index 0000000..b1f8876
--- /dev/null
+++ b/src/Infrastructure/Mail/src/Exception/SendMailFailedException.php
@@ -0,0 +1,32 @@
+getMessage(),
+ ),
+ ErrorDomain::Mail,
+ ErrorCode::Failed
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/Infrastructure/Mail/src/Service/MailService.php b/src/Infrastructure/Mail/src/Service/MailService.php
new file mode 100644
index 0000000..54b0a53
--- /dev/null
+++ b/src/Infrastructure/Mail/src/Service/MailService.php
@@ -0,0 +1,176 @@
+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);
+ }
+}
\ No newline at end of file
diff --git a/src/Infrastructure/Notification/config/service_manager.php b/src/Infrastructure/Notification/config/service_manager.php
new file mode 100644
index 0000000..3288e09
--- /dev/null
+++ b/src/Infrastructure/Notification/config/service_manager.php
@@ -0,0 +1,12 @@
+ [
+ NotificationService::class => AutoWiringFactory::class,
+ ],
+];
diff --git a/src/Infrastructure/Notification/src/ConfigProvider.php b/src/Infrastructure/Notification/src/ConfigProvider.php
new file mode 100644
index 0000000..9cf0f29
--- /dev/null
+++ b/src/Infrastructure/Notification/src/ConfigProvider.php
@@ -0,0 +1,15 @@
+ require __DIR__ . './../config/service_manager.php',
+ ];
+ }
+}
diff --git a/src/Infrastructure/Notification/src/Exception/SendNotificationFailed.php b/src/Infrastructure/Notification/src/Exception/SendNotificationFailed.php
new file mode 100644
index 0000000..a2c17b6
--- /dev/null
+++ b/src/Infrastructure/Notification/src/Exception/SendNotificationFailed.php
@@ -0,0 +1,26 @@
+getMessage(),
+ ),
+ ErrorDomain::Notification,
+ ErrorCode::Failed
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/Infrastructure/Notification/src/Service/NotificationService.php b/src/Infrastructure/Notification/src/Service/NotificationService.php
new file mode 100644
index 0000000..34326bd
--- /dev/null
+++ b/src/Infrastructure/Notification/src/Service/NotificationService.php
@@ -0,0 +1,55 @@
+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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Infrastructure/Rbac/src/Middleware/EnsureAuthorizationMiddleware.php b/src/Infrastructure/Rbac/src/Middleware/EnsureAuthorizationMiddleware.php
index d3b9581..181ff0e 100644
--- a/src/Infrastructure/Rbac/src/Middleware/EnsureAuthorizationMiddleware.php
+++ b/src/Infrastructure/Rbac/src/Middleware/EnsureAuthorizationMiddleware.php
@@ -56,7 +56,8 @@ class EnsureAuthorizationMiddleware implements MiddlewareInterface
): bool {
$role = $user->getRole();
$permissions = $role->getPermissions();
- $targetApi = $this->routes[$targetPath];
+
+ $targetApi = $this->routes[$targetPath] ?? null;
/** @var Permission $permission */
foreach($permissions as $permission) {
diff --git a/src/Infrastructure/Request/config/service_manager.php b/src/Infrastructure/Request/config/service_manager.php
index 92b91fa..cafa66f 100644
--- a/src/Infrastructure/Request/config/service_manager.php
+++ b/src/Infrastructure/Request/config/service_manager.php
@@ -2,12 +2,14 @@
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\Middleware\AnalyzeBodyMiddleware;
use Template\Infrastructure\Request\Middleware\AnalyzeHeaderMiddleware;
use Template\Infrastructure\Request\Middleware\InternalRequestMiddleware;
use Template\Infrastructure\Request\Service\RequestService;
+use Reinfi\DependencyInjection\Factory\AutoWiringFactory;
+use Reinfi\DependencyInjection\Factory\InjectionFactory;
return [
'factories' => [
diff --git a/src/Infrastructure/Request/src/Factory/RequestServiceFactory.php b/src/Infrastructure/Request/src/Factory/RequestServiceFactory.php
index c28553e..f1e4396 100644
--- a/src/Infrastructure/Request/src/Factory/RequestServiceFactory.php
+++ b/src/Infrastructure/Request/src/Factory/RequestServiceFactory.php
@@ -18,7 +18,7 @@ class RequestServiceFactory implements FactoryInterface
{
/** @var ConfigService $configService */
$configService = $container->get(ConfigService::class);
- $apiKey = $configService->resolve("api.keys.template");
+ $apiKey = $configService->resolve("api.keys.Template");
return new RequestService(
$apiKey,
diff --git a/src/Infrastructure/Request/src/Middleware/AnalyzeBodyMiddleware.php b/src/Infrastructure/Request/src/Middleware/AnalyzeBodyMiddleware.php
index a732d08..ee973f7 100644
--- a/src/Infrastructure/Request/src/Middleware/AnalyzeBodyMiddleware.php
+++ b/src/Infrastructure/Request/src/Middleware/AnalyzeBodyMiddleware.php
@@ -2,16 +2,10 @@
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\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
-use Reinfi\DependencyInjection\Annotation\InjectConfig;
class AnalyzeBodyMiddleware implements MiddlewareInterface
{
@@ -33,6 +27,8 @@ class AnalyzeBodyMiddleware implements MiddlewareInterface
$request->getBody()->getContents(),
true
);
+
+ $request->getBody()->rewind();
}
return $handler->handle($request->withAttribute(
diff --git a/src/Infrastructure/Request/src/Middleware/AnalyzeHeaderMiddleware.php b/src/Infrastructure/Request/src/Middleware/AnalyzeHeaderMiddleware.php
index 129efc5..9d7fd47 100644
--- a/src/Infrastructure/Request/src/Middleware/AnalyzeHeaderMiddleware.php
+++ b/src/Infrastructure/Request/src/Middleware/AnalyzeHeaderMiddleware.php
@@ -2,36 +2,47 @@
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\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
-use Reinfi\DependencyInjection\Annotation\InjectConfig;
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(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface
{
+ $headers = $request->getHeaders();
+
+ $contentType = $this->readHeader($headers, self::CONTENT_TYPE_HEADER_KEY) ?? null;
$host = explode(
':',
- $request->getHeaders()['host'][0]
- ?? $request->getHeaders()['x-forwarded-host'][0]
- ?? 'UNKNOWN'
+ $this->readHeader($headers, self::HOST_HEADER_KEY) ??
+ $this->readHeader($headers, self::FORWARDED_HOST_HEADER_KEY) ??
+ 'UNKNOWN'
)[0];
- return $handler->handle($request->withAttribute(
- self::HOST_ATTRIBUTE,
- $host
- ));
+ return $handler->handle(
+ $request
+ ->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;
}
}
\ No newline at end of file
diff --git a/src/Infrastructure/Response/src/SuccessResponse.php b/src/Infrastructure/Response/src/SuccessResponse.php
index 004c101..17e40c1 100644
--- a/src/Infrastructure/Response/src/SuccessResponse.php
+++ b/src/Infrastructure/Response/src/SuccessResponse.php
@@ -7,9 +7,13 @@ use Laminas\Diactoros\Response\JsonResponse;
class SuccessResponse extends JsonResponse
{
public function __construct(
- mixed $data = 'OK'
+ mixed $data = 'Success'
)
{
+ if ($data === []) {
+ $data = 'Success';
+ }
+
parent::__construct(
$data
);
diff --git a/src/Infrastructure/Schema/config/service_manager.php b/src/Infrastructure/Schema/config/service_manager.php
new file mode 100644
index 0000000..336df68
--- /dev/null
+++ b/src/Infrastructure/Schema/config/service_manager.php
@@ -0,0 +1,41 @@
+ [
+ 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,
+ ],
+];
diff --git a/src/Infrastructure/Schema/src/ConfigProvider.php b/src/Infrastructure/Schema/src/ConfigProvider.php
new file mode 100644
index 0000000..62ba04f
--- /dev/null
+++ b/src/Infrastructure/Schema/src/ConfigProvider.php
@@ -0,0 +1,15 @@
+ require __DIR__ . './../config/service_manager.php',
+ ];
+ }
+}
diff --git a/src/Infrastructure/Schema/src/Enum/Method.php b/src/Infrastructure/Schema/src/Enum/Method.php
new file mode 100644
index 0000000..5b8bb5a
--- /dev/null
+++ b/src/Infrastructure/Schema/src/Enum/Method.php
@@ -0,0 +1,8 @@
+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
+ );
+ }
+}
diff --git a/src/Infrastructure/Schema/src/Exporter/Backend/Builder/PropertyBuilder.php b/src/Infrastructure/Schema/src/Exporter/Backend/Builder/PropertyBuilder.php
new file mode 100644
index 0000000..9d12eca
--- /dev/null
+++ b/src/Infrastructure/Schema/src/Exporter/Backend/Builder/PropertyBuilder.php
@@ -0,0 +1,112 @@
+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 ?? [],
+ ];
+ }
+}
diff --git a/src/Infrastructure/Schema/src/Exporter/Backend/Builder/TypeBuilder.php b/src/Infrastructure/Schema/src/Exporter/Backend/Builder/TypeBuilder.php
new file mode 100644
index 0000000..bd22b36
--- /dev/null
+++ b/src/Infrastructure/Schema/src/Exporter/Backend/Builder/TypeBuilder.php
@@ -0,0 +1,80 @@
+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,
+ );
+ }
+}
diff --git a/src/Infrastructure/Schema/src/Exporter/Backend/Model/ClassFile.php b/src/Infrastructure/Schema/src/Exporter/Backend/Model/ClassFile.php
new file mode 100644
index 0000000..915ec2d
--- /dev/null
+++ b/src/Infrastructure/Schema/src/Exporter/Backend/Model/ClassFile.php
@@ -0,0 +1,87 @@
+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;
+ }
+}
diff --git a/src/Infrastructure/Schema/src/Exporter/Backend/Model/Property.php b/src/Infrastructure/Schema/src/Exporter/Backend/Model/Property.php
new file mode 100644
index 0000000..71dbc2b
--- /dev/null
+++ b/src/Infrastructure/Schema/src/Exporter/Backend/Model/Property.php
@@ -0,0 +1,54 @@
+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;
+ }
+}
diff --git a/src/Infrastructure/Schema/src/Exporter/Backend/Model/Type.php b/src/Infrastructure/Schema/src/Exporter/Backend/Model/Type.php
new file mode 100644
index 0000000..1953e0d
--- /dev/null
+++ b/src/Infrastructure/Schema/src/Exporter/Backend/Model/Type.php
@@ -0,0 +1,22 @@
+name;
+ }
+
+ public function getFile(): ClassFile
+ {
+ return $this->file;
+ }
+}
diff --git a/src/Infrastructure/Schema/src/Exporter/BackendExporter.php b/src/Infrastructure/Schema/src/Exporter/BackendExporter.php
new file mode 100644
index 0000000..5c2b108
--- /dev/null
+++ b/src/Infrastructure/Schema/src/Exporter/BackendExporter.php
@@ -0,0 +1,581 @@
+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 = " 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;
+ }
+}
\ No newline at end of file
diff --git a/src/Infrastructure/Schema/src/Exporter/Frontend/Builder/PropertyBuilder.php b/src/Infrastructure/Schema/src/Exporter/Frontend/Builder/PropertyBuilder.php
new file mode 100644
index 0000000..720add6
--- /dev/null
+++ b/src/Infrastructure/Schema/src/Exporter/Frontend/Builder/PropertyBuilder.php
@@ -0,0 +1,82 @@
+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
+ ];
+ }
+}
diff --git a/src/Infrastructure/Schema/src/Exporter/Frontend/Builder/ServiceBuilder.php b/src/Infrastructure/Schema/src/Exporter/Frontend/Builder/ServiceBuilder.php
new file mode 100644
index 0000000..64ad6d8
--- /dev/null
+++ b/src/Infrastructure/Schema/src/Exporter/Frontend/Builder/ServiceBuilder.php
@@ -0,0 +1,76 @@
+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;
+ }
+}
diff --git a/src/Infrastructure/Schema/src/Exporter/Frontend/Builder/TypeBuilder.php b/src/Infrastructure/Schema/src/Exporter/Frontend/Builder/TypeBuilder.php
new file mode 100644
index 0000000..90018aa
--- /dev/null
+++ b/src/Infrastructure/Schema/src/Exporter/Frontend/Builder/TypeBuilder.php
@@ -0,0 +1,39 @@
+ $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))
+ );
+ }
+}
diff --git a/src/Infrastructure/Schema/src/Exporter/Frontend/Model/Property.php b/src/Infrastructure/Schema/src/Exporter/Frontend/Model/Property.php
new file mode 100644
index 0000000..c064c46
--- /dev/null
+++ b/src/Infrastructure/Schema/src/Exporter/Frontend/Model/Property.php
@@ -0,0 +1,28 @@
+name;
+ }
+
+ public function getDefinition(): string
+ {
+ return $this->definition;
+ }
+
+ public function getSubtypeDefinition(): ?string
+ {
+ return $this->subtypeDefinition;
+ }
+}
diff --git a/src/Infrastructure/Schema/src/Exporter/Frontend/Model/Service.php b/src/Infrastructure/Schema/src/Exporter/Frontend/Model/Service.php
new file mode 100644
index 0000000..dd6e700
--- /dev/null
+++ b/src/Infrastructure/Schema/src/Exporter/Frontend/Model/Service.php
@@ -0,0 +1,16 @@
+definition;
+ }
+}
diff --git a/src/Infrastructure/Schema/src/Exporter/Frontend/Model/Type.php b/src/Infrastructure/Schema/src/Exporter/Frontend/Model/Type.php
new file mode 100644
index 0000000..2c46d27
--- /dev/null
+++ b/src/Infrastructure/Schema/src/Exporter/Frontend/Model/Type.php
@@ -0,0 +1,22 @@
+name;
+ }
+
+ public function getDefinition(): string
+ {
+ return $this->definition;
+ }
+}
diff --git a/src/Infrastructure/Schema/src/Exporter/FrontendExporter.php b/src/Infrastructure/Schema/src/Exporter/FrontendExporter.php
new file mode 100644
index 0000000..23bd228
--- /dev/null
+++ b/src/Infrastructure/Schema/src/Exporter/FrontendExporter.php
@@ -0,0 +1,91 @@
+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
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Infrastructure/Schema/src/Factory/OpenApiFactory.php b/src/Infrastructure/Schema/src/Factory/OpenApiFactory.php
new file mode 100644
index 0000000..4c27ebb
--- /dev/null
+++ b/src/Infrastructure/Schema/src/Factory/OpenApiFactory.php
@@ -0,0 +1,71 @@
+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
+ ));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Infrastructure/Schema/src/Middleware/SchemaValidationMiddleware.php b/src/Infrastructure/Schema/src/Middleware/SchemaValidationMiddleware.php
new file mode 100644
index 0000000..afa39f5
--- /dev/null
+++ b/src/Infrastructure/Schema/src/Middleware/SchemaValidationMiddleware.php
@@ -0,0 +1,38 @@
+fromSchema($this->schema)
+ ->getValidationMiddleware();
+
+ return $validationMiddleware->process($request, $handler);
+ }
+
+}
diff --git a/src/Infrastructure/Schema/src/Model/Api.php b/src/Infrastructure/Schema/src/Model/Api.php
new file mode 100644
index 0000000..efb9c17
--- /dev/null
+++ b/src/Infrastructure/Schema/src/Model/Api.php
@@ -0,0 +1,98 @@
+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);
+ }
+}
diff --git a/src/Infrastructure/Schema/src/Model/Type.php b/src/Infrastructure/Schema/src/Model/Type.php
new file mode 100644
index 0000000..4f84aa8
--- /dev/null
+++ b/src/Infrastructure/Schema/src/Model/Type.php
@@ -0,0 +1,93 @@
+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;
+ }
+}
diff --git a/src/Infrastructure/Schema/src/Reader/ApiDefinitionReader.php b/src/Infrastructure/Schema/src/Reader/ApiDefinitionReader.php
new file mode 100644
index 0000000..c3e39ec
--- /dev/null
+++ b/src/Infrastructure/Schema/src/Reader/ApiDefinitionReader.php
@@ -0,0 +1,141 @@
+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;
+ }
+}
\ No newline at end of file