From cc989e96f7e60694872904b9c34bf52f221f6a45 Mon Sep 17 00:00:00 2001 From: flo Date: Wed, 1 Jan 2025 23:34:33 +0100 Subject: [PATCH] first version, pre PR --- .gitignore | 2 + bin/createApi.php | 22 +- bin/script/firstRun | 30 + bin/script/init | 50 +- bin/script/update | 39 ++ config/autoload/authorization.global.php | 3 +- config/autoload/mail.global.php | 17 + config/autoload/notification.global.php | 9 + config/config.php | 5 +- config/pipeline.php | 4 +- data/mails/registration/assets/icon.png | Bin 0 -> 1766 bytes data/mails/registration/template.latte | 66 ++ data/mails/reset-password/assets/icon.png | Bin 0 -> 1766 bytes data/mails/reset-password/template.latte | 63 ++ .../Version20230922085011.php | 0 .../Version20230922092351.php | 0 .../Version20230922092754.php | 0 .../Version20230922101354.php | 0 .../Version20230922101355.php | 0 .../Version20230924113403.php | 0 .../template/Version20240827155813.php | 37 ++ .../template/Version20240911191301.php | 41 ++ .../template/Version20241130201314.php | 31 + .../template/Version20241130202949.php | 77 +++ docker/docker-compose-mac.yml.dist | 52 -- src/DataDomain/Business/config/doctrine.php | 2 +- .../src/AutowireRepositoryFactory.php | 33 + .../src/Client/EncryptionClient.php | 16 +- .../Exception/config/service_manager.php | 4 +- .../Exception/src/ErrorCode.php | 11 +- .../Exception/src/ErrorDomain.php | 7 +- .../{Exception.php => TemplateException.php} | 3 +- .../Middleware/ExceptionHandlerMiddleware.php | 37 -- .../TemplateExceptionHandlerMiddleware.php | 71 +++ .../Logging/src/Logger/Logger.php | 6 +- .../Mail/config/service_manager.php | 12 + .../Mail/src/ConfigProvider.php | 15 + .../src/Exception/SendMailFailedException.php | 32 + .../Mail/src/Service/MailService.php | 176 ++++++ .../Notification/config/service_manager.php | 12 + .../Notification/src/ConfigProvider.php | 15 + .../src/Exception/SendNotificationFailed.php | 26 + .../src/Service/NotificationService.php | 55 ++ .../EnsureAuthorizationMiddleware.php | 3 +- .../Request/config/service_manager.php | 4 +- .../src/Factory/RequestServiceFactory.php | 2 +- .../src/Middleware/AnalyzeBodyMiddleware.php | 8 +- .../Middleware/AnalyzeHeaderMiddleware.php | 39 +- .../Response/src/SuccessResponse.php | 6 +- .../Schema/config/service_manager.php | 41 ++ .../Schema/src/ConfigProvider.php | 15 + src/Infrastructure/Schema/src/Enum/Method.php | 8 + .../Schema/src/Enum/SchemaType.php | 13 + .../src/Exception/FileNotFoundException.php | 10 + .../UnsupportedContentTypeException.php | 10 + .../Backend/Builder/ClassFileBuilder.php | 37 ++ .../Backend/Builder/PropertyBuilder.php | 112 ++++ .../Exporter/Backend/Builder/TypeBuilder.php | 80 +++ .../src/Exporter/Backend/Model/ClassFile.php | 87 +++ .../src/Exporter/Backend/Model/Property.php | 54 ++ .../src/Exporter/Backend/Model/Type.php | 22 + .../Schema/src/Exporter/BackendExporter.php | 581 ++++++++++++++++++ .../Frontend/Builder/PropertyBuilder.php | 82 +++ .../Frontend/Builder/ServiceBuilder.php | 76 +++ .../Exporter/Frontend/Builder/TypeBuilder.php | 39 ++ .../src/Exporter/Frontend/Model/Property.php | 28 + .../src/Exporter/Frontend/Model/Service.php | 16 + .../src/Exporter/Frontend/Model/Type.php | 22 + .../Schema/src/Exporter/FrontendExporter.php | 91 +++ .../Schema/src/Factory/OpenApiFactory.php | 71 +++ .../Middleware/SchemaValidationMiddleware.php | 38 ++ src/Infrastructure/Schema/src/Model/Api.php | 98 +++ src/Infrastructure/Schema/src/Model/Type.php | 93 +++ .../Schema/src/Reader/ApiDefinitionReader.php | 141 +++++ 74 files changed, 2810 insertions(+), 198 deletions(-) create mode 100755 bin/script/firstRun create mode 100755 bin/script/update create mode 100644 config/autoload/mail.global.php create mode 100644 config/autoload/notification.global.php create mode 100644 data/mails/registration/assets/icon.png create mode 100644 data/mails/registration/template.latte create mode 100644 data/mails/reset-password/assets/icon.png create mode 100644 data/mails/reset-password/template.latte rename data/migrations/{business => template}/Version20230922085011.php (100%) rename data/migrations/{business => template}/Version20230922092351.php (100%) rename data/migrations/{business => template}/Version20230922092754.php (100%) rename data/migrations/{business => template}/Version20230922101354.php (100%) rename data/migrations/{business => template}/Version20230922101355.php (100%) rename data/migrations/{business => template}/Version20230924113403.php (100%) create mode 100644 data/migrations/template/Version20240827155813.php create mode 100644 data/migrations/template/Version20240911191301.php create mode 100644 data/migrations/template/Version20241130201314.php create mode 100644 data/migrations/template/Version20241130202949.php delete mode 100644 docker/docker-compose-mac.yml.dist create mode 100644 src/Infrastructure/Database/src/AutowireRepositoryFactory.php rename src/Infrastructure/Exception/src/Exception/{Exception.php => TemplateException.php} (93%) delete mode 100644 src/Infrastructure/Exception/src/Middleware/ExceptionHandlerMiddleware.php create mode 100644 src/Infrastructure/Exception/src/Middleware/TemplateExceptionHandlerMiddleware.php create mode 100644 src/Infrastructure/Mail/config/service_manager.php create mode 100644 src/Infrastructure/Mail/src/ConfigProvider.php create mode 100644 src/Infrastructure/Mail/src/Exception/SendMailFailedException.php create mode 100644 src/Infrastructure/Mail/src/Service/MailService.php create mode 100644 src/Infrastructure/Notification/config/service_manager.php create mode 100644 src/Infrastructure/Notification/src/ConfigProvider.php create mode 100644 src/Infrastructure/Notification/src/Exception/SendNotificationFailed.php create mode 100644 src/Infrastructure/Notification/src/Service/NotificationService.php create mode 100644 src/Infrastructure/Schema/config/service_manager.php create mode 100644 src/Infrastructure/Schema/src/ConfigProvider.php create mode 100644 src/Infrastructure/Schema/src/Enum/Method.php create mode 100644 src/Infrastructure/Schema/src/Enum/SchemaType.php create mode 100644 src/Infrastructure/Schema/src/Exception/FileNotFoundException.php create mode 100644 src/Infrastructure/Schema/src/Exception/UnsupportedContentTypeException.php create mode 100644 src/Infrastructure/Schema/src/Exporter/Backend/Builder/ClassFileBuilder.php create mode 100644 src/Infrastructure/Schema/src/Exporter/Backend/Builder/PropertyBuilder.php create mode 100644 src/Infrastructure/Schema/src/Exporter/Backend/Builder/TypeBuilder.php create mode 100644 src/Infrastructure/Schema/src/Exporter/Backend/Model/ClassFile.php create mode 100644 src/Infrastructure/Schema/src/Exporter/Backend/Model/Property.php create mode 100644 src/Infrastructure/Schema/src/Exporter/Backend/Model/Type.php create mode 100644 src/Infrastructure/Schema/src/Exporter/BackendExporter.php create mode 100644 src/Infrastructure/Schema/src/Exporter/Frontend/Builder/PropertyBuilder.php create mode 100644 src/Infrastructure/Schema/src/Exporter/Frontend/Builder/ServiceBuilder.php create mode 100644 src/Infrastructure/Schema/src/Exporter/Frontend/Builder/TypeBuilder.php create mode 100644 src/Infrastructure/Schema/src/Exporter/Frontend/Model/Property.php create mode 100644 src/Infrastructure/Schema/src/Exporter/Frontend/Model/Service.php create mode 100644 src/Infrastructure/Schema/src/Exporter/Frontend/Model/Type.php create mode 100644 src/Infrastructure/Schema/src/Exporter/FrontendExporter.php create mode 100644 src/Infrastructure/Schema/src/Factory/OpenApiFactory.php create mode 100644 src/Infrastructure/Schema/src/Middleware/SchemaValidationMiddleware.php create mode 100644 src/Infrastructure/Schema/src/Model/Api.php create mode 100644 src/Infrastructure/Schema/src/Model/Type.php create mode 100644 src/Infrastructure/Schema/src/Reader/ApiDefinitionReader.php 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 0000000000000000000000000000000000000000..07e8b9d9cba7a4dc27cd800357a7ca8e4b45ec2c GIT binary patch literal 1766 zcmVPx#1ZP1_K>z@;j|==^1poj532;bRa{vHNyZ``ByaAytitqpc02y>eSaefwW^{L9 za%BK_cXuvnZfkR6VQ^(GZ*pgw?mQX*00ve`L_t(|UhSM`sPse>hOc7p4SW0S3Mwd8 z1jXJfqJk)*_`@G|YzT^q3N}Ph5qm+z&w{;yU@!PbR8;KPd++Ui*aULpNivg}l)IY; z9vYtO4wiiYuG4rCfk7;422iKP>zKGze+2PtMU(d zqolwmh8^XL(khTTQivDHMdjGCq`XyH1rWmz6rd0kCmOU zygW+M>3&~k+oLb#Bl38;mULxr`J2ps$)-J5w#K^hCi%OxK8)on@+7&EEaDK!-C(`H zX-}7pLUlYkEM_vGSw@x8RJLf$K7Q|u>yl-7BDB!tRF0>Fdh8ENgG zm+I}sThcmE-cKY$eKA>EUL$=HtyLZ^v(V66`&k|)y`UOd2eKri96De=kK_wIDx1jb zy)08W7mdBOpXKh-i({m9pdufTWHNl1SMDn5Lw+HxMk=XL3G zM-k!QC7+dySCAh1K>}Bshrz0nFTs!>@gXDqZE>+A3-g+EO;geKFD@xWFYb}~t1y%O zOx~CKTb1;*YC#Zd9dMFlkg7Ix_^pFZf;2-*$l8#8j}dFlMCm#PLmJyU_&7_ihpB5( zI7eD1>5H*z$AvN~N6MH{e=NC6_R0M%#J6QSQ8FrLO6!DOnhw+uTU!73kqhCIpY)<{ zLy>gl)|9|hF>4=JIaXtHRlq01+=k=;Q^=ff=UDqyN%Ge_0pJ7efOX<7-B)I*Q!|*c z746Ad1cS`h$-9#7RzAhPleG3N@-i7^6+l%IV7X-57f3=d5i-kTk^ym780)1FgQ+S! z?{sv-khsXhAovZVmrjtY3!Ojh#MUx@+jk|YuA)&fQeQp^)oo=kRd*hfwfh3oI`UOE zg=+iw=%o`(AP=Uh@ZL?feSpE6GOkOuF3>xP^3bT_Q)%7$ZOh178Dy?~CL1xaDF~O8 zdv+kDOIY(!2D3w1J><$>$+l6*tXfyDZI_l+tS_ziTGB6W9J)YVEi%dKy6Dbl!5=>< zQX7|Q`;{a$-!;r>apBrz>%!*AwozDG(8R#BkFK-xSvg7IY#k%-39>dWcOEdFd&}r$ zXSfcqe=SLFx)3ym>{@tmoMfI#S8SMUU2vVrMR-#ipTc9(Ad{%M-MheNk+JK;rIkQA z%ZW1IcC;Y_J}y<4C#Y-t%>10zxl~#orfKh&WwnKB_@K1jYk7cd9lpR7()vJ6%hCas z<>w*n@Lettv{W>-&YzLkcH2Ql`CiEbjGo6fl5olgrtx + + + 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 0000000000000000000000000000000000000000..07e8b9d9cba7a4dc27cd800357a7ca8e4b45ec2c GIT binary patch literal 1766 zcmVPx#1ZP1_K>z@;j|==^1poj532;bRa{vHNyZ``ByaAytitqpc02y>eSaefwW^{L9 za%BK_cXuvnZfkR6VQ^(GZ*pgw?mQX*00ve`L_t(|UhSM`sPse>hOc7p4SW0S3Mwd8 z1jXJfqJk)*_`@G|YzT^q3N}Ph5qm+z&w{;yU@!PbR8;KPd++Ui*aULpNivg}l)IY; z9vYtO4wiiYuG4rCfk7;422iKP>zKGze+2PtMU(d zqolwmh8^XL(khTTQivDHMdjGCq`XyH1rWmz6rd0kCmOU zygW+M>3&~k+oLb#Bl38;mULxr`J2ps$)-J5w#K^hCi%OxK8)on@+7&EEaDK!-C(`H zX-}7pLUlYkEM_vGSw@x8RJLf$K7Q|u>yl-7BDB!tRF0>Fdh8ENgG zm+I}sThcmE-cKY$eKA>EUL$=HtyLZ^v(V66`&k|)y`UOd2eKri96De=kK_wIDx1jb zy)08W7mdBOpXKh-i({m9pdufTWHNl1SMDn5Lw+HxMk=XL3G zM-k!QC7+dySCAh1K>}Bshrz0nFTs!>@gXDqZE>+A3-g+EO;geKFD@xWFYb}~t1y%O zOx~CKTb1;*YC#Zd9dMFlkg7Ix_^pFZf;2-*$l8#8j}dFlMCm#PLmJyU_&7_ihpB5( zI7eD1>5H*z$AvN~N6MH{e=NC6_R0M%#J6QSQ8FrLO6!DOnhw+uTU!73kqhCIpY)<{ zLy>gl)|9|hF>4=JIaXtHRlq01+=k=;Q^=ff=UDqyN%Ge_0pJ7efOX<7-B)I*Q!|*c z746Ad1cS`h$-9#7RzAhPleG3N@-i7^6+l%IV7X-57f3=d5i-kTk^ym780)1FgQ+S! z?{sv-khsXhAovZVmrjtY3!Ojh#MUx@+jk|YuA)&fQeQp^)oo=kRd*hfwfh3oI`UOE zg=+iw=%o`(AP=Uh@ZL?feSpE6GOkOuF3>xP^3bT_Q)%7$ZOh178Dy?~CL1xaDF~O8 zdv+kDOIY(!2D3w1J><$>$+l6*tXfyDZI_l+tS_ziTGB6W9J)YVEi%dKy6Dbl!5=>< zQX7|Q`;{a$-!;r>apBrz>%!*AwozDG(8R#BkFK-xSvg7IY#k%-39>dWcOEdFd&}r$ zXSfcqe=SLFx)3ym>{@tmoMfI#S8SMUU2vVrMR-#ipTc9(Ad{%M-MheNk+JK;rIkQA z%ZW1IcC;Y_J}y<4C#Y-t%>10zxl~#orfKh&WwnKB_@K1jYk7cd9lpR7()vJ6%hCas z<>w*n@Lettv{W>-&YzLkcH2Ql`CiEbjGo6fl5olgrtx + + + 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