From f0f31190811f6302a1b900bd1911a9ce338271da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milan=20Felix=20=C5=A0ulc?= Date: Tue, 1 Oct 2024 15:16:17 +0200 Subject: [PATCH] Kick off --- .docs/README.md | 43 +++- composer.json | 4 +- examples/d01/.mate.neon | 9 +- .../Api/User/Create/CreateUserController.php | 28 +++ .../app/Api/User/Create/CreateUserRequest.php | 16 ++ .../Api/User/Create/CreateUserRequestBody.php | 38 +++ .../Api/User/Create/CreateUserResponse.php | 20 ++ .../Api/User/Delete/DeleteUserController.php | 28 +++ .../Api/User/Delete/DeleteUserResponse.php | 19 ++ .../app/Api/User/Get/GetUserController.php | 28 +++ .../d01/app/Api/User/Get/GetUserRequest.php | 15 ++ .../d01/app/Api/User/Get/GetUserResponse.php | 20 ++ .../app/Api/User/List/ListUserController.php | 28 +++ .../d01/app/Api/User/List/ListUserRequest.php | 15 ++ .../Api/User/List/ListUserRequestFilter.php | 26 ++ .../app/Api/User/List/ListUserResponse.php | 30 +++ .../Api/User/Update/UpdateUserController.php | 28 +++ .../app/Api/User/Update/UpdateUserRequest.php | 16 ++ .../Api/User/Update/UpdateUserRequestBody.php | 38 +++ .../Api/User/Update/UpdateUserResponse.php | 20 ++ .../d01/app/Domain/User/CreateUserCommand.php | 13 +- .../d01/app/Domain/User/CreateUserHandler.php | 49 ++-- .../d01/app/Domain/User/Database/User.php | 42 ++++ .../Domain/User/Database/UserRepository.php | 23 ++ .../d01/app/Domain/User/DeleteUserCommand.php | 15 ++ .../d01/app/Domain/User/DeleteUserHandler.php | 21 ++ .../d01/app/Domain/User/GetUserCommand.php | 15 ++ .../d01/app/Domain/User/GetUserHandler.php | 21 ++ .../d01/app/Domain/User/ListUserCommand.php | 15 ++ .../d01/app/Domain/User/ListUserHandler.php | 21 ++ .../d01/app/Domain/User/UpdateUserCommand.php | 20 ++ .../d01/app/Domain/User/UpdateUserHandler.php | 58 +++++ examples/d02/.mate.neon | 79 +++--- phpstan.neon | 29 +++ resources/presets/default/config.neon | 5 + resources/presets/moderntv/config.neon | 228 ++++++++++++++++++ .../templates/api/controller_create.latte | 28 +++ .../templates/api/controller_delete.latte | 28 +++ .../templates/api/controller_get.latte | 28 +++ .../templates/api/controller_list.latte | 28 +++ .../templates/api/controller_update.latte | 28 +++ .../templates/api/request_body_create.latte | 27 +++ .../templates/api/request_body_update.latte | 27 +++ .../templates/api/request_create.latte | 16 ++ .../templates/api/request_delete.latte | 15 ++ .../templates/api/request_filter_list.latte | 26 ++ .../moderntv/templates/api/request_get.latte | 15 ++ .../moderntv/templates/api/request_list.latte | 15 ++ .../templates/api/request_update.latte | 16 ++ .../templates/api/response_create.latte | 20 ++ .../templates/api/response_delete.latte | 19 ++ .../moderntv/templates/api/response_get.latte | 20 ++ .../templates/api/response_list.latte | 30 +++ .../templates/api/response_update.latte | 20 ++ .../templates/bus/command_create.latte | 17 ++ .../templates/bus/command_delete.latte | 15 ++ .../moderntv/templates/bus/command_get.latte | 15 ++ .../moderntv/templates/bus/command_list.latte | 15 ++ .../templates/bus/command_update.latte | 17 ++ .../templates/bus/handler_create.latte | 44 ++++ .../templates/bus/handler_delete.latte | 21 ++ .../moderntv/templates/bus/handler_get.latte | 21 ++ .../moderntv/templates/bus/handler_list.latte | 21 ++ .../templates/bus/handler_update.latte | 45 ++++ .../moderntv/templates/database/entity.latte | 29 +++ .../templates/database/repository.latte | 23 ++ .../_defaults/LICENSE.latte | 0 .../_defaults/Makefile.latte | 0 .../_defaults/README.latte | 0 .../_defaults/editorconfig.latte | 0 .../_defaults/github/codesniffer.latte | 0 .../_defaults/github/dependabot.latte | 0 .../_defaults/github/kodiak.latte | 0 .../_defaults/github/phpstan.latte | 0 .../_defaults/github/tests.latte | 0 .../_defaults/gitignore.latte | 0 .../_defaults/phpstan.latte | 0 .../_defaults/ruleset.latte | 0 .../_defaults/var/gitignore.latte | 0 .../_defaults/www/htaccess.latte | 0 .../demo/app/Bootstrap.php | 0 .../demo/app/UI/@Templates/@layout.latte | 0 .../demo/app/UI/BasePresenter.php | 0 .../demo/app/UI/Home/HomePresenter.php | 0 .../demo/app/UI/Home/Templates/default.latte | 0 .../demo/config/config.latte | 0 .../demo/config/local.latte | 0 .../demo/config/local.neon.latte | 0 .../demo/www/index.php | 0 src/Bootstrap.php | 13 +- src/Command/BaseCommand.php | 27 +++ src/Command/CraftCommand.php | 118 +++++---- src/Command/InitCommand.php | 27 +++ src/Command/MakeCommand.php | 27 +++ src/Config/AppConfig.php | 20 ++ src/Config/CrafterConfig.php | 22 ++ src/Config/CraftersConfig.php | 17 ++ src/Config/DataConfig.php | 11 - src/Config/InputConfig.php | 17 ++ src/Config/Loader/ConfigLoader.php | 95 ++++++++ src/Config/MateConfig.php | 16 +- src/Config/ProcessConfig.php | 14 ++ src/Config/StructConfig.php | 18 ++ .../{DataField.php => StructFieldConfig.php} | 2 +- src/Config/StructsConfig.php | 22 ++ src/Crafter/Template/ClassContext.php | 17 ++ src/Crafter/Template/HelperContext.php | 16 ++ src/Crafter/Template/StructContext.php | 18 ++ src/Crafter/Template/StructFieldContext.php | 16 ++ src/Crafter/Template/TemplateContext.php | 16 ++ src/Crafter/Worker/CrafterResult.php | 28 +++ src/Crafter/Worker/CrafterWorker.php | 103 ++++++++ src/Crafter/Worker/WorkerContext.php | 16 ++ src/Crafter/Worker/WorkerContextFactory.php | 34 +++ src/DI/BetterContainer.php | 18 ++ src/Exception/ConfigIOException.php | 8 + src/Exception/LogicalException.php | 10 + src/Exception/RuntimeException.php | 8 + src/Generator/CommandGenerator.php | 42 ---- src/Generator/ControllerGenerator.php | 19 -- src/Generator/EntityGenerator.php | 19 -- src/Generator/HandlerGenerator.php | 65 ----- src/Loader/MateLoader.php | 8 - src/Template/TemplateRenderer.php | 40 +++ src/Utils/Classes.php | 23 ++ 125 files changed, 2514 insertions(+), 288 deletions(-) create mode 100644 examples/d01/app/Api/User/Create/CreateUserController.php create mode 100644 examples/d01/app/Api/User/Create/CreateUserRequest.php create mode 100644 examples/d01/app/Api/User/Create/CreateUserRequestBody.php create mode 100644 examples/d01/app/Api/User/Create/CreateUserResponse.php create mode 100644 examples/d01/app/Api/User/Delete/DeleteUserController.php create mode 100644 examples/d01/app/Api/User/Delete/DeleteUserResponse.php create mode 100644 examples/d01/app/Api/User/Get/GetUserController.php create mode 100644 examples/d01/app/Api/User/Get/GetUserRequest.php create mode 100644 examples/d01/app/Api/User/Get/GetUserResponse.php create mode 100644 examples/d01/app/Api/User/List/ListUserController.php create mode 100644 examples/d01/app/Api/User/List/ListUserRequest.php create mode 100644 examples/d01/app/Api/User/List/ListUserRequestFilter.php create mode 100644 examples/d01/app/Api/User/List/ListUserResponse.php create mode 100644 examples/d01/app/Api/User/Update/UpdateUserController.php create mode 100644 examples/d01/app/Api/User/Update/UpdateUserRequest.php create mode 100644 examples/d01/app/Api/User/Update/UpdateUserRequestBody.php create mode 100644 examples/d01/app/Api/User/Update/UpdateUserResponse.php create mode 100644 examples/d01/app/Domain/User/Database/User.php create mode 100644 examples/d01/app/Domain/User/Database/UserRepository.php create mode 100644 examples/d01/app/Domain/User/DeleteUserCommand.php create mode 100644 examples/d01/app/Domain/User/DeleteUserHandler.php create mode 100644 examples/d01/app/Domain/User/GetUserCommand.php create mode 100644 examples/d01/app/Domain/User/GetUserHandler.php create mode 100644 examples/d01/app/Domain/User/ListUserCommand.php create mode 100644 examples/d01/app/Domain/User/ListUserHandler.php create mode 100644 examples/d01/app/Domain/User/UpdateUserCommand.php create mode 100644 examples/d01/app/Domain/User/UpdateUserHandler.php create mode 100644 resources/presets/default/config.neon create mode 100644 resources/presets/moderntv/config.neon create mode 100644 resources/presets/moderntv/templates/api/controller_create.latte create mode 100644 resources/presets/moderntv/templates/api/controller_delete.latte create mode 100644 resources/presets/moderntv/templates/api/controller_get.latte create mode 100644 resources/presets/moderntv/templates/api/controller_list.latte create mode 100644 resources/presets/moderntv/templates/api/controller_update.latte create mode 100644 resources/presets/moderntv/templates/api/request_body_create.latte create mode 100644 resources/presets/moderntv/templates/api/request_body_update.latte create mode 100644 resources/presets/moderntv/templates/api/request_create.latte create mode 100644 resources/presets/moderntv/templates/api/request_delete.latte create mode 100644 resources/presets/moderntv/templates/api/request_filter_list.latte create mode 100644 resources/presets/moderntv/templates/api/request_get.latte create mode 100644 resources/presets/moderntv/templates/api/request_list.latte create mode 100644 resources/presets/moderntv/templates/api/request_update.latte create mode 100644 resources/presets/moderntv/templates/api/response_create.latte create mode 100644 resources/presets/moderntv/templates/api/response_delete.latte create mode 100644 resources/presets/moderntv/templates/api/response_get.latte create mode 100644 resources/presets/moderntv/templates/api/response_list.latte create mode 100644 resources/presets/moderntv/templates/api/response_update.latte create mode 100644 resources/presets/moderntv/templates/bus/command_create.latte create mode 100644 resources/presets/moderntv/templates/bus/command_delete.latte create mode 100644 resources/presets/moderntv/templates/bus/command_get.latte create mode 100644 resources/presets/moderntv/templates/bus/command_list.latte create mode 100644 resources/presets/moderntv/templates/bus/command_update.latte create mode 100644 resources/presets/moderntv/templates/bus/handler_create.latte create mode 100644 resources/presets/moderntv/templates/bus/handler_delete.latte create mode 100644 resources/presets/moderntv/templates/bus/handler_get.latte create mode 100644 resources/presets/moderntv/templates/bus/handler_list.latte create mode 100644 resources/presets/moderntv/templates/bus/handler_update.latte create mode 100644 resources/presets/moderntv/templates/database/entity.latte create mode 100644 resources/presets/moderntv/templates/database/repository.latte rename resources/{templates => projects}/_defaults/LICENSE.latte (100%) rename resources/{templates => projects}/_defaults/Makefile.latte (100%) rename resources/{templates => projects}/_defaults/README.latte (100%) rename resources/{templates => projects}/_defaults/editorconfig.latte (100%) rename resources/{templates => projects}/_defaults/github/codesniffer.latte (100%) rename resources/{templates => projects}/_defaults/github/dependabot.latte (100%) rename resources/{templates => projects}/_defaults/github/kodiak.latte (100%) rename resources/{templates => projects}/_defaults/github/phpstan.latte (100%) rename resources/{templates => projects}/_defaults/github/tests.latte (100%) rename resources/{templates => projects}/_defaults/gitignore.latte (100%) rename resources/{templates => projects}/_defaults/phpstan.latte (100%) rename resources/{templates => projects}/_defaults/ruleset.latte (100%) rename resources/{templates => projects}/_defaults/var/gitignore.latte (100%) rename resources/{templates => projects}/_defaults/www/htaccess.latte (100%) rename resources/{templates => projects}/demo/app/Bootstrap.php (100%) rename resources/{templates => projects}/demo/app/UI/@Templates/@layout.latte (100%) rename resources/{templates => projects}/demo/app/UI/BasePresenter.php (100%) rename resources/{templates => projects}/demo/app/UI/Home/HomePresenter.php (100%) rename resources/{templates => projects}/demo/app/UI/Home/Templates/default.latte (100%) rename resources/{templates => projects}/demo/config/config.latte (100%) rename resources/{templates => projects}/demo/config/local.latte (100%) rename resources/{templates => projects}/demo/config/local.neon.latte (100%) rename resources/{templates => projects}/demo/www/index.php (100%) create mode 100644 src/Command/BaseCommand.php create mode 100644 src/Command/InitCommand.php create mode 100644 src/Command/MakeCommand.php create mode 100644 src/Config/AppConfig.php create mode 100644 src/Config/CrafterConfig.php create mode 100644 src/Config/CraftersConfig.php delete mode 100644 src/Config/DataConfig.php create mode 100644 src/Config/InputConfig.php create mode 100644 src/Config/Loader/ConfigLoader.php create mode 100644 src/Config/ProcessConfig.php create mode 100644 src/Config/StructConfig.php rename src/Config/{DataField.php => StructFieldConfig.php} (86%) create mode 100644 src/Config/StructsConfig.php create mode 100644 src/Crafter/Template/ClassContext.php create mode 100644 src/Crafter/Template/HelperContext.php create mode 100644 src/Crafter/Template/StructContext.php create mode 100644 src/Crafter/Template/StructFieldContext.php create mode 100644 src/Crafter/Template/TemplateContext.php create mode 100644 src/Crafter/Worker/CrafterResult.php create mode 100644 src/Crafter/Worker/CrafterWorker.php create mode 100644 src/Crafter/Worker/WorkerContext.php create mode 100644 src/Crafter/Worker/WorkerContextFactory.php create mode 100644 src/DI/BetterContainer.php create mode 100644 src/Exception/ConfigIOException.php create mode 100644 src/Exception/LogicalException.php create mode 100644 src/Exception/RuntimeException.php delete mode 100644 src/Generator/CommandGenerator.php delete mode 100644 src/Generator/ControllerGenerator.php delete mode 100644 src/Generator/EntityGenerator.php delete mode 100644 src/Generator/HandlerGenerator.php delete mode 100644 src/Loader/MateLoader.php create mode 100644 src/Template/TemplateRenderer.php create mode 100644 src/Utils/Classes.php diff --git a/.docs/README.md b/.docs/README.md index 398d2d5..a2f5d4b 100644 --- a/.docs/README.md +++ b/.docs/README.md @@ -12,6 +12,8 @@ composer require contributte/mate --dev 1. Create `.mate.neon` in your project root. +You can initialize it by running `vendor/bin/mate init`. Or you can create it manually. + ```neon data: user: @@ -26,9 +28,48 @@ data: 2. Run `vendor/bin/mate` or `php mate.phar` in your project root. ``` -php mate.phar craft +vendor/bin/mate craft ``` ## Configuration Under construction. + +## Usage + +### `mate init` + +Create `.mate.neon` in your project. + +### `mate craft` + +Generate files based on `.mate.neon`. + +```bash +vendor/bin/mate craft --struct user +``` + +```bash +vendor/bin/mate craft --struct user --crafter=entity +vendor/bin/mate craft --struct user --crafter=repository + +vendor/bin/mate craft --struct user --crafter=bus --mode=create +vendor/bin/mate craft --struct user --crafter=bus --mode=update +vendor/bin/mate craft --struct user --crafter=bus --mode=delete +vendor/bin/mate craft --struct user --crafter=bus --mode=list +vendor/bin/mate craft --struct user --crafter=bus --mode=get + +vendor/bin/mate craft --struct user --crafter=api --mode=create +vendor/bin/mate craft --struct user --crafter=api --mode=update +vendor/bin/mate craft --struct user --crafter=api --mode=delete +vendor/bin/mate craft --struct user --crafter=api --mode=list +vendor/bin/mate craft --struct user --crafter=api --mode=get +``` + +### `mate generate` + +Generate whole project based on `.mate.neon`. + +```bash +vendor/bin/mate generate --template api +``` diff --git a/composer.json b/composer.json index d2119f6..9ba1bcf 100644 --- a/composer.json +++ b/composer.json @@ -19,12 +19,14 @@ ], "require": { "php": ">=8.2", + "nette/di": "^3.2.2", "nette/php-generator": "^4.1.6", "nette/utils": "^4.0.5", "nette/neon": "^3.4.3", "nette/schema": "^1.3.0", "latte/latte": "^3.0.18", - "symfony/console": "^7.1.5" + "symfony/console": "^7.1.5", + "nette/safe": "^1.0.0" }, "require-dev": { "contributte/qa": "^0.4.0", diff --git a/examples/d01/.mate.neon b/examples/d01/.mate.neon index e96f693..fc88379 100644 --- a/examples/d01/.mate.neon +++ b/examples/d01/.mate.neon @@ -1,4 +1,10 @@ -data: +mate: + presets: [moderntv] + +app: + scopes: [database, bus, api] + +structs: user: fields: username: {type: string} @@ -6,3 +12,4 @@ data: password: {type: string} createdAt: {type: Nette\Utils\DateTime} updatedAt: {type: Nette\Utils\DateTime} + roles: {type: array} diff --git a/examples/d01/app/Api/User/Create/CreateUserController.php b/examples/d01/app/Api/User/Create/CreateUserController.php new file mode 100644 index 0000000..84a4198 --- /dev/null +++ b/examples/d01/app/Api/User/Create/CreateUserController.php @@ -0,0 +1,28 @@ + Expect::string()->required(), + 'email' => Expect::string()->required(), + 'password' => Expect::string()->required(), + 'createdAt' => Expect::string()->required(), + 'updatedAt' => Expect::string()->required(), + 'roles' => Expect::string()->required(), + ])->castTo(self::class); + } + +} diff --git a/examples/d01/app/Api/User/Create/CreateUserResponse.php b/examples/d01/app/Api/User/Create/CreateUserResponse.php new file mode 100644 index 0000000..9866399 --- /dev/null +++ b/examples/d01/app/Api/User/Create/CreateUserResponse.php @@ -0,0 +1,20 @@ +payload = $dto; + + return $self; + } + +} diff --git a/examples/d01/app/Api/User/Delete/DeleteUserController.php b/examples/d01/app/Api/User/Delete/DeleteUserController.php new file mode 100644 index 0000000..8560754 --- /dev/null +++ b/examples/d01/app/Api/User/Delete/DeleteUserController.php @@ -0,0 +1,28 @@ +payload = $dto; + + return $self; + } + +} diff --git a/examples/d01/app/Api/User/List/ListUserController.php b/examples/d01/app/Api/User/List/ListUserController.php new file mode 100644 index 0000000..a57daf3 --- /dev/null +++ b/examples/d01/app/Api/User/List/ListUserController.php @@ -0,0 +1,28 @@ + Expect::structure([ + 'username' => Expect::anyOf('asc', 'desc'), + ])->required(false)->castTo('array'), + 'q' => Expect::structure([ + 'username' => Expect::string(), + ])->required(false)->castTo('array'), + ])->castTo(self::class); + } + +} diff --git a/examples/d01/app/Api/User/List/ListUserResponse.php b/examples/d01/app/Api/User/List/ListUserResponse.php new file mode 100644 index 0000000..2996302 --- /dev/null +++ b/examples/d01/app/Api/User/List/ListUserResponse.php @@ -0,0 +1,30 @@ +entities as $entity) { + $payload[] = $entity->toArray(); + } + + $self->entities = $payload; + $self->count = $result->count; + $self->limit = $result->limit; + $self->page = $result->page; + + return $self; + } + +} diff --git a/examples/d01/app/Api/User/Update/UpdateUserController.php b/examples/d01/app/Api/User/Update/UpdateUserController.php new file mode 100644 index 0000000..9e06008 --- /dev/null +++ b/examples/d01/app/Api/User/Update/UpdateUserController.php @@ -0,0 +1,28 @@ + Expect::string(), + 'email' => Expect::string(), + 'password' => Expect::string(), + 'createdAt' => Expect::string(), + 'updatedAt' => Expect::string(), + 'roles' => Expect::string(), + ])->castTo(self::class); + } + +} diff --git a/examples/d01/app/Api/User/Update/UpdateUserResponse.php b/examples/d01/app/Api/User/Update/UpdateUserResponse.php new file mode 100644 index 0000000..1db51e0 --- /dev/null +++ b/examples/d01/app/Api/User/Update/UpdateUserResponse.php @@ -0,0 +1,20 @@ +payload = $dto; + + return $self; + } + +} diff --git a/examples/d01/app/Domain/User/CreateUserCommand.php b/examples/d01/app/Domain/User/CreateUserCommand.php index c933f3f..e904859 100644 --- a/examples/d01/app/Domain/User/CreateUserCommand.php +++ b/examples/d01/app/Domain/User/CreateUserCommand.php @@ -6,12 +6,15 @@ readonly class CreateUserCommand { + public function __construct( - public string $username, - public string $email, - public string $password, - public \Nette\Utils\DateTime $createdAt, - public \Nette\Utils\DateTime $updatedAt, + public string $username; + public string $email; + public string $password; + public Nette\Utils\DateTime $createdAt; + public Nette\Utils\DateTime $updatedAt; + public array $roles; ) { } + } diff --git a/examples/d01/app/Domain/User/CreateUserHandler.php b/examples/d01/app/Domain/User/CreateUserHandler.php index 829d926..cf488b5 100644 --- a/examples/d01/app/Domain/User/CreateUserHandler.php +++ b/examples/d01/app/Domain/User/CreateUserHandler.php @@ -4,29 +4,44 @@ namespace App\Domain\User; +use App\Model\Bus\Result\Result; +use App\Model\Exception\Runtime\EntityExistsException; +use App\Model\Exception\Runtime\EntityNotFoundException; +use Doctrine\DBAL\Exception\UniqueConstraintViolationException; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Messenger\Attribute\AsMessageHandler; + +#[AsMessageHandler] readonly class CreateUserHandler { + public function __construct( - private \Doctrine\ORM\EntityManagerInterface $em, + private EntityManagerInterface $em, + private EventDispatcherInterface $ed, ) { } - - public function __invoke(CreateUserCommand $command): object + public function __invoke(CreateUserCommand $command): Result { - $entity = new User( - username: $command->username, - email: $command->email, - password: $command->password, - createdAt: $command->createdAt, - updatedAt: $command->updatedAt, - ); - - - $this->em->persist($entity); - $this->em->flush(); - - - return $entity; + $user = new User(); + $user->username = $command->username; + $user->email = $command->email; + $user->password = $command->password; + $user->createdAt = $command->createdAt; + $user->updatedAt = $command->updatedAt; + $user->roles = $command->roles; + + try { + $this->em->persist($user); + $this->em->flush(); + } catch (UniqueConstraintViolationException $e) { + throw EntityExistsException::alreadyExists(User::class, $e); + } + + // $this->ed->dispatch(new UserCreatedEvent($user)); + + return Result::from($user); } + } diff --git a/examples/d01/app/Domain/User/Database/User.php b/examples/d01/app/Domain/User/Database/User.php new file mode 100644 index 0000000..816f04d --- /dev/null +++ b/examples/d01/app/Domain/User/Database/User.php @@ -0,0 +1,42 @@ + + */ +class UserRepository extends AbstractRepository +{ + + public function __construct( + EntityManagerInterface $em + ) + { + parent::__construct($em->getRepository(User::class)); + } + +} diff --git a/examples/d01/app/Domain/User/DeleteUserCommand.php b/examples/d01/app/Domain/User/DeleteUserCommand.php new file mode 100644 index 0000000..de3fca9 --- /dev/null +++ b/examples/d01/app/Domain/User/DeleteUserCommand.php @@ -0,0 +1,15 @@ +em->getRepository(User::class)->findOneBy(['id' => $command->id]); + + if ($user === null) { + throw EntityNotFoundException::notFoundByUiid(User::class, $command->id); + } + + if ($command->username !== null) { + $user->username = $command->username; + } + if ($command->email !== null) { + $user->email = $command->email; + } + if ($command->password !== null) { + $user->password = $command->password; + } + if ($command->createdAt !== null) { + $user->createdAt = $command->createdAt; + } + if ($command->updatedAt !== null) { + $user->updatedAt = $command->updatedAt; + } + if ($command->roles !== null) { + $user->roles = $command->roles; + } + + $this->em->persist($user); + $this->em->flush(); + + // $this->ed->dispatch(new UserUpdatedEvent($user)); + + return Result::from($user); + } + +} diff --git a/examples/d02/.mate.neon b/examples/d02/.mate.neon index f03e385..04bec7a 100644 --- a/examples/d02/.mate.neon +++ b/examples/d02/.mate.neon @@ -1,50 +1,51 @@ app: namespace: App - class: - entity: - baseClass: App\Model\Database\Entity\AbstractEntity - repository: - baseClass: App\Model\Database\Repository\AbstractRepository - handler: - dependencies: - em: Doctrine\ORM\EntityManagerInterface + craft: + # Database + - { category: database, type: entity, class: "App\Domain\${name|ucfirst}\Database\${name|ucfirst}", baseClass: App\Model\Database\Entity\AbstractEntity } + - { category: database, type: repository, class: "App\Domain\${name|ucfirst}\Database\${name|ucfirst}Repository", baseClass: App\Model\Database\Repository\AbstractRepository } + # Domain + - { category: domain, type: command, flavor: create, class: "App\Domain\${name|ucfirst}\Create${name|ucfirst}Command" } + - { category: domain, type: handler, flavor: create, class: "App\Domain\${name|ucfirst}\Create${name|ucfirst}Handler", dependencies: { em: Doctrine\ORM\EntityManagerInterface } } + - { category: domain, type: command, flavor: update, class: "App\Domain\${name|ucfirst}\Update${name|ucfirst}Command" } + - { category: domain, type: handler, flavor: update, class: "App\Domain\${name|ucfirst}\Update${name|ucfirst}Handler", dependencies: { em: Doctrine\ORM\EntityManagerInterface } } + - { category: domain, type: command, flavor: get, class: "App\Domain\${name|ucfirst}\Get${name|ucfirst}Command" } + - { category: domain, type: handler, flavor: get, class: "App\Domain\${name|ucfirst}\Get${name|ucfirst}Handler", dependencies: { em: Doctrine\ORM\EntityManagerInterface } } + - { category: domain, type: command, flavor: list, class: "App\Domain\${name|ucfirst}\List${name|ucfirst}Command" } + - { category: domain, type: handler, flavor: list, class: "App\Domain\${name|ucfirst}\List${name|ucfirst}Handler", dependencies: { em: Doctrine\ORM\EntityManagerInterface } } + - { category: domain, type: command, flavor: delete, class: "App\Domain\${name|ucfirst}\Delete${name|ucfirst}Command" } + - { category: domain, type: handler, flavor: delete, class: "App\Domain\${name|ucfirst}\Delete${name|ucfirst}Handler", dependencies: { em: Doctrine\ORM\EntityManagerInterface } } + # API (create) + - { category: api, type: controller, flavor: create, class: "App\Api\${name|ucfirst}\Create\Create${name|ucfirst}Controller" } + - { category: api, type: request, flavor: create, class: "App\Api\${name|ucfirst}\Create\Create${name|ucfirst}Request" } + - { category: api, type: request_body, flavor: create, class: "App\Api\${name|ucfirst}\Create\Create${name|ucfirst}Body" } + - { category: api, type: response, flavor: create, class: "App\Api\${name|ucfirst}\Create\Create${name|ucfirst}Response" } + # API (update) + - { category: api, type: controller, flavor: update, class: "App\Api\${name|ucfirst}\Update\Update${name|ucfirst}Controller" } + - { category: api, type: request, flavor: update, class: "App\Api\${name|ucfirst}\Update\Update${name|ucfirst}Request" } + - { category: api, type: request_body, flavor: update, class: "App\Api\${name|ucfirst}\Update\Update${name|ucfirst}Body" } + - { category: api, type: response, flavor: update, class: "App\Api\${name|ucfirst}\Update\Update${name|ucfirst}Response" } + # API (get) + - { category: api, type: controller, flavor: get, class: "App\Api\${name|ucfirst}\Get\Get${name|ucfirst}Controller" } + - { category: api, type: request, flavor: get, class: "App\Api\${name|ucfirst}\Get\Get${name|ucfirst}Request" } + - { category: api, type: response, flavor: get, class: "App\Api\${name|ucfirst}\Get\Get${name|ucfirst}Response" } + # API (list) + - { category: api, type: controller, flavor: list, class: "App\Api\${name|ucfirst}\List\List${name|ucfirst}Controller" } + - { category: api, type: request, flavor: list,class: "App\Api\${name|ucfirst}\List\List${name|ucfirst}Request" } + - { category: api, type: request_filter, flavor: list,class: "App\Api\${name|ucfirst}\List\List${name|ucfirst}Filter" } + - { category: api, type: response, flavor: list,class: "App\Api\${name|ucfirst}\List\List${name|ucfirst}Response" } + # API (delete) + - { category: api, type: controller, flavor: delete, class: "App\Api\${name|ucfirst}\Delete\Delete${name|ucfirst}Controller" } + - { category: api, type: response, flavor: delete,class: "App\Api\${name|ucfirst}\Delete\Delete${name|ucfirst}Response" } data: user: - class: - # Database - - { type: entity, class: App\Domain\User\Database\User } - - { type: repository, class: App\Domain\User\Database\UserRepository } - # CQRS - - { type: command, class: App\Domain\User\CreateUserCommand } - - { type: handler, class: App\Domain\User\CreateUserHandler } - # API (create) - - { type: controller, flavor: create, class: App\Api\User\Create\CreateUserController } - - { type: request, flavor: create, class: App\Api\User\Create\CreateUserRequest } - - { type: request_body, flavor: create, class: App\Api\User\Create\CreateUserBody } - - { type: response, flavor: create, class: App\Api\User\Create\CreateUserResponse } - # API (update) - - { type: controller, flavor: update, class: App\Api\User\Update\UpdateUserController } - - { type: request, flavor: update, class: App\Api\User\Update\UpdateUserRequest } - - { type: request_body, flavor: update, class: App\Api\User\Update\UpdateUserBody } - - { type: response, flavor: update, class: App\Api\User\Update\UpdateUserResponse } - # API (get) - - { type: controller, flavor: get, class: App\Api\User\Get\GetUserController } - - { type: request, flavor: get, class: App\Api\User\Get\GetUserRequest } - - { type: response, flavor: get, class: App\Api\User\Get\GetUserResponse } - # API (list) - - { type: controller, flavor: list, class: App\Api\User\List\ListUserController } - - { type: request, flavor: list,class: App\Api\User\List\ListUserRequest } - - { type: request_filter, flavor: list,class: App\Api\User\List\ListUserFilter } - - { type: response, flavor: list,class: App\Api\User\List\ListUserResponse } - # API (delete) - - { type: controller, flavor: delete, class: App\Api\User\Delete\DeleteUserController } - - { type: response, flavor: delete,class: App\Api\User\Delete\DeleteUserResponse } - + name: user + craft: [api, domain, database] fields: username: {type: string} email: {type: string} password: {type: string} - createdAt: {type: Nette\Utils\DateTime} + createdAt: {type: Nette\Utils\DateTime } updatedAt: {type: Nette\Utils\DateTime} diff --git a/phpstan.neon b/phpstan.neon index cbfddc9..2c30fdf 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -16,5 +16,34 @@ parameters: paths: - src + - bin ignoreErrors: + + typeAliases: + ConfigShape: """ + array{ + app: array{ + appDir: string, + namespace: string, + scopes: array|null, + crafters: array|null, + baseClass: string|null + }>|null + }, + mate: array{ + presets: array|null + }, + structs: array|null + }>|null + } + """ diff --git a/resources/presets/default/config.neon b/resources/presets/default/config.neon new file mode 100644 index 0000000..d9342c3 --- /dev/null +++ b/resources/presets/default/config.neon @@ -0,0 +1,5 @@ +app: + namespace: App + appDir: app + + crafters: [] diff --git a/resources/presets/moderntv/config.neon b/resources/presets/moderntv/config.neon new file mode 100644 index 0000000..73d726b --- /dev/null +++ b/resources/presets/moderntv/config.neon @@ -0,0 +1,228 @@ +app: + namespace: App + appDir: app + + crafters: + # Database ================================ + database_entity: + crafter: entity + scopes: [database] + template: templates/database/entity.latte + class: 'App\Domain\{$name|firstUpper}\Database\{$name|firstUpper}' + baseClass: App\Model\Database\Entity\AbstractEntity + + database_repository: + crafter: repository + scopes: [database] + template: templates/database/repository.latte + class: 'App\Domain\{$name|firstUpper}\Database\{$name|firstUpper}Repository' + baseClass: App\Model\Database\Repository\AbstractRepository + + # Bus ==================================== + # Bus (create) + bus_command_create: + crafter: command + mode: create + scopes: [bus] + template: templates/bus/command_create.latte + class: 'App\Domain\{$name|firstUpper}\Create{$name|firstUpper}Command' + bus_handler_create: + crafter: handler + mode: create + scopes: [bus] + template: templates/bus/handler_create.latte + class: 'App\Domain\{$name|firstUpper}\Create{$name|firstUpper}Handler' + dependencies: + em: Doctrine\ORM\EntityManagerInterface + # Bus (update) + bus_command_update: + crafter: command + mode: update + scopes: [bus] + template: templates/bus/command_update.latte + class: 'App\Domain\{$name|firstUpper}\Update{$name|firstUpper}Command' + bus_handler_update: + crafter: handler + mode: update + scopes: [bus] + template: templates/bus/handler_update.latte + class: 'App\Domain\{$name|firstUpper}\Update{$name|firstUpper}Handler' + dependencies: + em: Doctrine\ORM\EntityManagerInterface + # Bus (get) + bus_command_get: + crafter: command + mode: get + scopes: [bus] + template: templates/bus/command_get.latte + class: 'App\Domain\{$name|firstUpper}\Get{$name|firstUpper}Command' + bus_handler_get: + crafter: handler + mode: get + scopes: [bus] + template: templates/bus/handler_get.latte + class: 'App\Domain\{$name|firstUpper}\Get{$name|firstUpper}Handler' + dependencies: + em: Doctrine\ORM\EntityManagerInterface + # Bus (list) + bus_command_list: + crafter: command + mode: update + scopes: [bus] + template: templates/bus/command_list.latte + class: 'App\Domain\{$name|firstUpper}\List{$name|firstUpper}Command' + bus_handler_list: + crafter: handler + mode: create + scopes: [bus] + template: templates/bus/handler_list.latte + class: 'App\Domain\{$name|firstUpper}\List{$name|firstUpper}Handler' + dependencies: + em: Doctrine\ORM\EntityManagerInterface + # Bus (delete) + bus_command_delete: + crafter: command + mode: delete + scopes: [bus] + template: templates/bus/command_delete.latte + class: 'App\Domain\{$name|firstUpper}\Delete{$name|firstUpper}Command' + bus_handler_delete: + crafter: handler + mode: delete + scopes: [bus] + template: templates/bus/handler_delete.latte + class: 'App\Domain\{$name|firstUpper}\Delete{$name|firstUpper}Handler' + dependencies: + em: Doctrine\ORM\EntityManagerInterface + + # API ==================================== + # API (create) + api_controller_create: + crafter: controller + mode: create + scopes: [api] + template: templates/api/controller_create.latte + class: 'App\Api\{$name|firstUpper}\Create\Create{$name|firstUpper}Controller' + + api_request_create: + crafter: request + mode: create + scopes: [api] + template: templates/api/request_create.latte + class: 'App\Api\{$name|firstUpper}\Create\Create{$name|firstUpper}Request' + + api_request_body_create: + crafter: request_body + mode: create + scopes: [api] + template: templates/api/request_body_create.latte + class: 'App\Api\{$name|firstUpper}\Create\Create{$name|firstUpper}RequestBody' + + api_response_create: + crafter: response + mode: create + scopes: [api] + template: templates/api/response_create.latte + class: 'App\Api\{$name|firstUpper}\Create\Create{$name|firstUpper}Response' + + # API (update) + api_controller_update: + crafter: controller + mode: update + scopes: [api] + template: templates/api/controller_update.latte + class: 'App\Api\{$name|firstUpper}\Update\Update{$name|firstUpper}Controller' + + api_request_update: + crafter: request + mode: update + scopes: [api] + template: templates/api/request_update.latte + class: 'App\Api\{$name|firstUpper}\Update\Update{$name|firstUpper}Request' + + api_request_body_update: + crafter: request_body + mode: update + scopes: [api] + template: templates/api/request_body_update.latte + class: 'App\Api\{$name|firstUpper}\Update\Update{$name|firstUpper}RequestBody' + + api_response_update: + crafter: response + mode: update + scopes: [api] + template: templates/api/response_update.latte + class: 'App\Api\{$name|firstUpper}\Update\Update{$name|firstUpper}Response' + + # API (get) + api_controller_get: + crafter: controller + mode: get + scopes: [api] + template: templates/api/controller_get.latte + class: 'App\Api\{$name|firstUpper}\Get\Get{$name|firstUpper}Controller' + + api_request_get: + crafter: request + mode: get + scopes: [api] + template: templates/api/request_get.latte + class: 'App\Api\{$name|firstUpper}\Get\Get{$name|firstUpper}Request' + + api_response_get: + crafter: response + mode: get + scopes: [api] + template: templates/api/response_get.latte + class: 'App\Api\{$name|firstUpper}\Get\Get{$name|firstUpper}Response' + + # API (list) + api_controller_list: + crafter: controller + mode: list + scopes: [api] + template: templates/api/controller_list.latte + class: 'App\Api\{$name|firstUpper}\List\List{$name|firstUpper}Controller' + + api_request_list: + crafter: request + mode: list + scopes: [api] + template: templates/api/request_list.latte + class: 'App\Api\{$name|firstUpper}\List\List{$name|firstUpper}Request' + + api_request_filter_list: + crafter: request_filter + mode: list + scopes: [api] + template: templates/api/request_filter_list.latte + class: 'App\Api\{$name|firstUpper}\List\List{$name|firstUpper}RequestFilter' + + api_response_list: + crafter: response + mode: list + scopes: [api] + template: templates/api/response_list.latte + class: 'App\Api\{$name|firstUpper}\List\List{$name|firstUpper}Response' + + # API (delete) + api_controller_delete: + crafter: controller + mode: delete + scopes: [api] + template: templates/api/controller_delete.latte + class: 'App\Api\{$name|firstUpper}\Delete\Delete{$name|firstUpper}Controller' + + api_request_delete: + crafter: request + mode: delete + scopes: [api] + template: templates/api/request_delete.latte + class: 'App\Api\{$name|firstUpper}\Delete\Delete{$name|firstUpper}Response' + + api_response_delete: + crafter: response + mode: delete + scopes: [api] + template: templates/api/response_delete.latte + class: 'App\Api\{$name|firstUpper}\Delete\Delete{$name|firstUpper}Response' diff --git a/resources/presets/moderntv/templates/api/controller_create.latte b/resources/presets/moderntv/templates/api/controller_create.latte new file mode 100644 index 0000000..92f25c0 --- /dev/null +++ b/resources/presets/moderntv/templates/api/controller_create.latte @@ -0,0 +1,28 @@ +class->namespace}; + +use App\Api\AbstractController; +use App\Model\Api\Request\RequestFactory; +use Contributte\FrameX\Http\IResponse; +use Moderntv\Messenger\Bus\CommandBus; +use Psr\Http\Message\ServerRequestInterface; + +final class {$ctx->class->className} extends AbstractController +{ + + public function __construct( + private readonly CommandBus $bus, + private readonly RequestFactory $requestFactory, + ) + { + } + + public function __invoke(ServerRequestInterface $serverRequest): IResponse + { + // TODO + } + +} diff --git a/resources/presets/moderntv/templates/api/controller_delete.latte b/resources/presets/moderntv/templates/api/controller_delete.latte new file mode 100644 index 0000000..92f25c0 --- /dev/null +++ b/resources/presets/moderntv/templates/api/controller_delete.latte @@ -0,0 +1,28 @@ +class->namespace}; + +use App\Api\AbstractController; +use App\Model\Api\Request\RequestFactory; +use Contributte\FrameX\Http\IResponse; +use Moderntv\Messenger\Bus\CommandBus; +use Psr\Http\Message\ServerRequestInterface; + +final class {$ctx->class->className} extends AbstractController +{ + + public function __construct( + private readonly CommandBus $bus, + private readonly RequestFactory $requestFactory, + ) + { + } + + public function __invoke(ServerRequestInterface $serverRequest): IResponse + { + // TODO + } + +} diff --git a/resources/presets/moderntv/templates/api/controller_get.latte b/resources/presets/moderntv/templates/api/controller_get.latte new file mode 100644 index 0000000..92f25c0 --- /dev/null +++ b/resources/presets/moderntv/templates/api/controller_get.latte @@ -0,0 +1,28 @@ +class->namespace}; + +use App\Api\AbstractController; +use App\Model\Api\Request\RequestFactory; +use Contributte\FrameX\Http\IResponse; +use Moderntv\Messenger\Bus\CommandBus; +use Psr\Http\Message\ServerRequestInterface; + +final class {$ctx->class->className} extends AbstractController +{ + + public function __construct( + private readonly CommandBus $bus, + private readonly RequestFactory $requestFactory, + ) + { + } + + public function __invoke(ServerRequestInterface $serverRequest): IResponse + { + // TODO + } + +} diff --git a/resources/presets/moderntv/templates/api/controller_list.latte b/resources/presets/moderntv/templates/api/controller_list.latte new file mode 100644 index 0000000..92f25c0 --- /dev/null +++ b/resources/presets/moderntv/templates/api/controller_list.latte @@ -0,0 +1,28 @@ +class->namespace}; + +use App\Api\AbstractController; +use App\Model\Api\Request\RequestFactory; +use Contributte\FrameX\Http\IResponse; +use Moderntv\Messenger\Bus\CommandBus; +use Psr\Http\Message\ServerRequestInterface; + +final class {$ctx->class->className} extends AbstractController +{ + + public function __construct( + private readonly CommandBus $bus, + private readonly RequestFactory $requestFactory, + ) + { + } + + public function __invoke(ServerRequestInterface $serverRequest): IResponse + { + // TODO + } + +} diff --git a/resources/presets/moderntv/templates/api/controller_update.latte b/resources/presets/moderntv/templates/api/controller_update.latte new file mode 100644 index 0000000..92f25c0 --- /dev/null +++ b/resources/presets/moderntv/templates/api/controller_update.latte @@ -0,0 +1,28 @@ +class->namespace}; + +use App\Api\AbstractController; +use App\Model\Api\Request\RequestFactory; +use Contributte\FrameX\Http\IResponse; +use Moderntv\Messenger\Bus\CommandBus; +use Psr\Http\Message\ServerRequestInterface; + +final class {$ctx->class->className} extends AbstractController +{ + + public function __construct( + private readonly CommandBus $bus, + private readonly RequestFactory $requestFactory, + ) + { + } + + public function __invoke(ServerRequestInterface $serverRequest): IResponse + { + // TODO + } + +} diff --git a/resources/presets/moderntv/templates/api/request_body_create.latte b/resources/presets/moderntv/templates/api/request_body_create.latte new file mode 100644 index 0000000..a5ca728 --- /dev/null +++ b/resources/presets/moderntv/templates/api/request_body_create.latte @@ -0,0 +1,27 @@ +class->namespace}; + +use App\Domain\User\Database\User; +use Nette\Schema\Expect; +use Nette\Schema\Schema; + +final class {$ctx->class->className} +{ + + {foreach $ctx->struct->fields as $field} + public {$field->type} ${$field->name}; + + {/foreach} + public static function schema(): Schema + { + return Expect::structure([ + {foreach $ctx->struct->fields as $field} + '{$field->name}' => Expect::string()->required(), + {/foreach} + ])->castTo(self::class); + } + +} diff --git a/resources/presets/moderntv/templates/api/request_body_update.latte b/resources/presets/moderntv/templates/api/request_body_update.latte new file mode 100644 index 0000000..943f533 --- /dev/null +++ b/resources/presets/moderntv/templates/api/request_body_update.latte @@ -0,0 +1,27 @@ +class->namespace}; + +use App\Domain\User\Database\User; +use Nette\Schema\Expect; +use Nette\Schema\Schema; + +final class {$ctx->class->className} +{ + + {foreach $ctx->struct->fields as $field} + public {$field->type}|null ${$field->name}; + + {/foreach} + public static function schema(): Schema + { + return Expect::structure([ + {foreach $ctx->struct->fields as $field} + '{$field->name}' => Expect::string(), + {/foreach} + ])->castTo(self::class); + } + +} diff --git a/resources/presets/moderntv/templates/api/request_create.latte b/resources/presets/moderntv/templates/api/request_create.latte new file mode 100644 index 0000000..8191ab2 --- /dev/null +++ b/resources/presets/moderntv/templates/api/request_create.latte @@ -0,0 +1,16 @@ +class->namespace}; + +final readonly class {$ctx->class->className} +{ + + public function __construct( + public {$ctx->class->className}Body $body, + ) + { + } + +} diff --git a/resources/presets/moderntv/templates/api/request_delete.latte b/resources/presets/moderntv/templates/api/request_delete.latte new file mode 100644 index 0000000..ed2290d --- /dev/null +++ b/resources/presets/moderntv/templates/api/request_delete.latte @@ -0,0 +1,15 @@ +class->namespace}; + +final readonly class {$ctx->class->className} +{ + + public function __construct( + ) + { + } + +} diff --git a/resources/presets/moderntv/templates/api/request_filter_list.latte b/resources/presets/moderntv/templates/api/request_filter_list.latte new file mode 100644 index 0000000..c2498dc --- /dev/null +++ b/resources/presets/moderntv/templates/api/request_filter_list.latte @@ -0,0 +1,26 @@ +class->namespace}; + +use App\Model\Api\Request\RequestFilter; +use Nette\Schema\Expect; +use Nette\Schema\Schema; + +class {$ctx->class->className} extends RequestFilter +{ + + public static function schema(): Schema + { + return RequestFilter::extend([ + 'o' => Expect::structure([ + 'username' => Expect::anyOf('asc', 'desc'), + ])->required(false)->castTo('array'), + 'q' => Expect::structure([ + 'username' => Expect::string(), + ])->required(false)->castTo('array'), + ])->castTo(self::class); + } + +} diff --git a/resources/presets/moderntv/templates/api/request_get.latte b/resources/presets/moderntv/templates/api/request_get.latte new file mode 100644 index 0000000..ed2290d --- /dev/null +++ b/resources/presets/moderntv/templates/api/request_get.latte @@ -0,0 +1,15 @@ +class->namespace}; + +final readonly class {$ctx->class->className} +{ + + public function __construct( + ) + { + } + +} diff --git a/resources/presets/moderntv/templates/api/request_list.latte b/resources/presets/moderntv/templates/api/request_list.latte new file mode 100644 index 0000000..ed2290d --- /dev/null +++ b/resources/presets/moderntv/templates/api/request_list.latte @@ -0,0 +1,15 @@ +class->namespace}; + +final readonly class {$ctx->class->className} +{ + + public function __construct( + ) + { + } + +} diff --git a/resources/presets/moderntv/templates/api/request_update.latte b/resources/presets/moderntv/templates/api/request_update.latte new file mode 100644 index 0000000..8191ab2 --- /dev/null +++ b/resources/presets/moderntv/templates/api/request_update.latte @@ -0,0 +1,16 @@ +class->namespace}; + +final readonly class {$ctx->class->className} +{ + + public function __construct( + public {$ctx->class->className}Body $body, + ) + { + } + +} diff --git a/resources/presets/moderntv/templates/api/response_create.latte b/resources/presets/moderntv/templates/api/response_create.latte new file mode 100644 index 0000000..1b5c6de --- /dev/null +++ b/resources/presets/moderntv/templates/api/response_create.latte @@ -0,0 +1,20 @@ +class->namespace}; + +use Contributte\FrameX\Http\EntityResponse; + +final class {$ctx->class->className} extends EntityResponse +{ + + public static function of({$ctx->helper->dtoClassName} $dto): self + { + $self = self::create(); + $self->payload = $dto; + + return $self; + } + +} diff --git a/resources/presets/moderntv/templates/api/response_delete.latte b/resources/presets/moderntv/templates/api/response_delete.latte new file mode 100644 index 0000000..ca8eaab --- /dev/null +++ b/resources/presets/moderntv/templates/api/response_delete.latte @@ -0,0 +1,19 @@ +class->namespace}; + +use Contributte\FrameX\Http\DataResponse; + +final class {$ctx->class->className} extends DataResponse +{ + + public static function of(): self + { + $self = self::create(); + + return $self; + } + +} diff --git a/resources/presets/moderntv/templates/api/response_get.latte b/resources/presets/moderntv/templates/api/response_get.latte new file mode 100644 index 0000000..1b5c6de --- /dev/null +++ b/resources/presets/moderntv/templates/api/response_get.latte @@ -0,0 +1,20 @@ +class->namespace}; + +use Contributte\FrameX\Http\EntityResponse; + +final class {$ctx->class->className} extends EntityResponse +{ + + public static function of({$ctx->helper->dtoClassName} $dto): self + { + $self = self::create(); + $self->payload = $dto; + + return $self; + } + +} diff --git a/resources/presets/moderntv/templates/api/response_list.latte b/resources/presets/moderntv/templates/api/response_list.latte new file mode 100644 index 0000000..22479ed --- /dev/null +++ b/resources/presets/moderntv/templates/api/response_list.latte @@ -0,0 +1,30 @@ +class->namespace}; + +use Contributte\FrameX\Http\EntityListResponse; + +final class {$ctx->class->className} extends EntityListResponse +{ + + public static function of({$ctx->class->className|replace('Response', 'Result')} $result): self + { + $self = self::create(); + + $payload = []; + + foreach ($result->entities as $entity) { + $payload[] = $entity->toArray(); + } + + $self->entities = $payload; + $self->count = $result->count; + $self->limit = $result->limit; + $self->page = $result->page; + + return $self; + } + +} diff --git a/resources/presets/moderntv/templates/api/response_update.latte b/resources/presets/moderntv/templates/api/response_update.latte new file mode 100644 index 0000000..1b5c6de --- /dev/null +++ b/resources/presets/moderntv/templates/api/response_update.latte @@ -0,0 +1,20 @@ +class->namespace}; + +use Contributte\FrameX\Http\EntityResponse; + +final class {$ctx->class->className} extends EntityResponse +{ + + public static function of({$ctx->helper->dtoClassName} $dto): self + { + $self = self::create(); + $self->payload = $dto; + + return $self; + } + +} diff --git a/resources/presets/moderntv/templates/bus/command_create.latte b/resources/presets/moderntv/templates/bus/command_create.latte new file mode 100644 index 0000000..cefb6b5 --- /dev/null +++ b/resources/presets/moderntv/templates/bus/command_create.latte @@ -0,0 +1,17 @@ +class->namespace}; + +readonly class {$ctx->class->className} +{ + + public function __construct( + {foreach $ctx->struct->fields as $field} + public {$field->type} ${$field->name}; + {/foreach} + ) { + } + +} diff --git a/resources/presets/moderntv/templates/bus/command_delete.latte b/resources/presets/moderntv/templates/bus/command_delete.latte new file mode 100644 index 0000000..7707b44 --- /dev/null +++ b/resources/presets/moderntv/templates/bus/command_delete.latte @@ -0,0 +1,15 @@ +class->namespace}; + +readonly class {$ctx->class->className} +{ + + public function __construct( + public string $id + ) { + } + +} diff --git a/resources/presets/moderntv/templates/bus/command_get.latte b/resources/presets/moderntv/templates/bus/command_get.latte new file mode 100644 index 0000000..7707b44 --- /dev/null +++ b/resources/presets/moderntv/templates/bus/command_get.latte @@ -0,0 +1,15 @@ +class->namespace}; + +readonly class {$ctx->class->className} +{ + + public function __construct( + public string $id + ) { + } + +} diff --git a/resources/presets/moderntv/templates/bus/command_list.latte b/resources/presets/moderntv/templates/bus/command_list.latte new file mode 100644 index 0000000..d13d32f --- /dev/null +++ b/resources/presets/moderntv/templates/bus/command_list.latte @@ -0,0 +1,15 @@ +class->namespace}; + +readonly class {$ctx->class->className} +{ + + public function __construct( + public {$ctx->class->className}Filter $filter, + ) { + } + +} diff --git a/resources/presets/moderntv/templates/bus/command_update.latte b/resources/presets/moderntv/templates/bus/command_update.latte new file mode 100644 index 0000000..d9f95aa --- /dev/null +++ b/resources/presets/moderntv/templates/bus/command_update.latte @@ -0,0 +1,17 @@ +class->namespace}; + +readonly class {$ctx->class->className} +{ + + public function __construct( + {foreach $ctx->struct->fields as $field} + public {$field->type}|null ${$field->name}; + {/foreach} + ) { + } + +} diff --git a/resources/presets/moderntv/templates/bus/handler_create.latte b/resources/presets/moderntv/templates/bus/handler_create.latte new file mode 100644 index 0000000..f377e57 --- /dev/null +++ b/resources/presets/moderntv/templates/bus/handler_create.latte @@ -0,0 +1,44 @@ +class->namespace}; + +use App\Model\Bus\Result\Result; +use App\Model\Exception\Runtime\EntityExistsException; +use App\Model\Exception\Runtime\EntityNotFoundException; +use Doctrine\DBAL\Exception\UniqueConstraintViolationException; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Messenger\Attribute\AsMessageHandler; + +#[AsMessageHandler] +readonly class {$ctx->class->className} +{ + + public function __construct( + private EntityManagerInterface $em, + private EventDispatcherInterface $ed, + ) { + } + + public function __invoke({$ctx->class->className|replace('Handler', 'Command')} $command): Result + { + {$ctx->helper->structVar} = new {$ctx->helper->entityClassName}(); + {foreach $ctx->struct->fields as $field} + {$ctx->helper->structVar}->{$field->name} = $command->{$field->name}; + {/foreach} + + try { + $this->em->persist({$ctx->helper->structVar}); + $this->em->flush(); + } catch (UniqueConstraintViolationException $e) { + throw EntityExistsException::alreadyExists({$ctx->helper->entityClassName}::class, $e); + } + + // $this->ed->dispatch(new {$ctx->helper->entityClassName}CreatedEvent({$ctx->helper->structVar})); + + return Result::from({$ctx->helper->structVar}); + } + +} diff --git a/resources/presets/moderntv/templates/bus/handler_delete.latte b/resources/presets/moderntv/templates/bus/handler_delete.latte new file mode 100644 index 0000000..6ec4286 --- /dev/null +++ b/resources/presets/moderntv/templates/bus/handler_delete.latte @@ -0,0 +1,21 @@ +class->namespace}; + +#[AsMessageHandler] +readonly class {$ctx->class->className} +{ + + public function __construct( + private EntityManagerInterface $em, + private EventDispatcherInterface $ed, + ) { + } + + public function __invoke({$ctx->class->className|replace('Handler', 'Command')} $command): object + { + } + +} diff --git a/resources/presets/moderntv/templates/bus/handler_get.latte b/resources/presets/moderntv/templates/bus/handler_get.latte new file mode 100644 index 0000000..6ec4286 --- /dev/null +++ b/resources/presets/moderntv/templates/bus/handler_get.latte @@ -0,0 +1,21 @@ +class->namespace}; + +#[AsMessageHandler] +readonly class {$ctx->class->className} +{ + + public function __construct( + private EntityManagerInterface $em, + private EventDispatcherInterface $ed, + ) { + } + + public function __invoke({$ctx->class->className|replace('Handler', 'Command')} $command): object + { + } + +} diff --git a/resources/presets/moderntv/templates/bus/handler_list.latte b/resources/presets/moderntv/templates/bus/handler_list.latte new file mode 100644 index 0000000..6ec4286 --- /dev/null +++ b/resources/presets/moderntv/templates/bus/handler_list.latte @@ -0,0 +1,21 @@ +class->namespace}; + +#[AsMessageHandler] +readonly class {$ctx->class->className} +{ + + public function __construct( + private EntityManagerInterface $em, + private EventDispatcherInterface $ed, + ) { + } + + public function __invoke({$ctx->class->className|replace('Handler', 'Command')} $command): object + { + } + +} diff --git a/resources/presets/moderntv/templates/bus/handler_update.latte b/resources/presets/moderntv/templates/bus/handler_update.latte new file mode 100644 index 0000000..47e16f7 --- /dev/null +++ b/resources/presets/moderntv/templates/bus/handler_update.latte @@ -0,0 +1,45 @@ +class->namespace}; + +use App\Model\Bus\Result\Result; +use App\Model\Exception\Runtime\EntityNotFoundException; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Messenger\Attribute\AsMessageHandler; + +#[AsMessageHandler] +readonly class {$ctx->class->className} +{ + + public function __construct( + private EntityManagerInterface $em, + private EventDispatcherInterface $ed, + ) { + } + + public function __invoke({$ctx->class->className|replace('Handler', 'Command')} $command): Result + { + {$ctx->helper->structVar} = $this->em->getRepository({$ctx->helper->entityClassName}::class)->findOneBy(['id' => $command->id]); + + if ({$ctx->helper->structVar} === null) { + throw EntityNotFoundException::notFoundByUiid({$ctx->helper->entityClassName}::class, $command->id); + } + + {foreach $ctx->struct->fields as $field} + if ($command->{$field->name} !== null) { + {$ctx->helper->structVar}->{$field->name} = $command->{$field->name}; + } + {/foreach} + + $this->em->persist({$ctx->helper->structVar}); + $this->em->flush(); + + // $this->ed->dispatch(new {$ctx->helper->entityClassName}UpdatedEvent({$ctx->helper->structVar})); + + return Result::from({$ctx->helper->structVar}); + } + +} diff --git a/resources/presets/moderntv/templates/database/entity.latte b/resources/presets/moderntv/templates/database/entity.latte new file mode 100644 index 0000000..3bb545c --- /dev/null +++ b/resources/presets/moderntv/templates/database/entity.latte @@ -0,0 +1,29 @@ +class->namespace}; + +use App\Model\Database\Entity\AbstractEntity; +use Doctrine\ORM\Mapping\Column; +use Doctrine\ORM\Mapping\Entity; +use Doctrine\ORM\Mapping\Table; + +#[Entity] +#[Table(name: '{$ctx->class->className|lower}')] +class {$ctx->class->className} extends {$ctx->class->extends} +{ + + {foreach $ctx->struct->fields as $field} + #[Column(type: '{$field->type}')] + private {$field->type} ${$field->name}; + + {/foreach} + public function __construct( + // TODO - generated code + ) + { + // TODO - generated code + } + +} diff --git a/resources/presets/moderntv/templates/database/repository.latte b/resources/presets/moderntv/templates/database/repository.latte new file mode 100644 index 0000000..3cd10cd --- /dev/null +++ b/resources/presets/moderntv/templates/database/repository.latte @@ -0,0 +1,23 @@ +class->namespace}; + +use App\Model\Database\Repository\AbstractRepository; +use Doctrine\ORM\EntityManagerInterface; + +/** + * @extends AbstractRepository<{$ctx->class->className}> + */ +class {$ctx->class->className} extends AbstractRepository +{ + + public function __construct( + EntityManagerInterface $em + ) + { + parent::__construct($em->getRepository({$ctx->class->className|replace('Repository', '')}::class)); + } + +} diff --git a/resources/templates/_defaults/LICENSE.latte b/resources/projects/_defaults/LICENSE.latte similarity index 100% rename from resources/templates/_defaults/LICENSE.latte rename to resources/projects/_defaults/LICENSE.latte diff --git a/resources/templates/_defaults/Makefile.latte b/resources/projects/_defaults/Makefile.latte similarity index 100% rename from resources/templates/_defaults/Makefile.latte rename to resources/projects/_defaults/Makefile.latte diff --git a/resources/templates/_defaults/README.latte b/resources/projects/_defaults/README.latte similarity index 100% rename from resources/templates/_defaults/README.latte rename to resources/projects/_defaults/README.latte diff --git a/resources/templates/_defaults/editorconfig.latte b/resources/projects/_defaults/editorconfig.latte similarity index 100% rename from resources/templates/_defaults/editorconfig.latte rename to resources/projects/_defaults/editorconfig.latte diff --git a/resources/templates/_defaults/github/codesniffer.latte b/resources/projects/_defaults/github/codesniffer.latte similarity index 100% rename from resources/templates/_defaults/github/codesniffer.latte rename to resources/projects/_defaults/github/codesniffer.latte diff --git a/resources/templates/_defaults/github/dependabot.latte b/resources/projects/_defaults/github/dependabot.latte similarity index 100% rename from resources/templates/_defaults/github/dependabot.latte rename to resources/projects/_defaults/github/dependabot.latte diff --git a/resources/templates/_defaults/github/kodiak.latte b/resources/projects/_defaults/github/kodiak.latte similarity index 100% rename from resources/templates/_defaults/github/kodiak.latte rename to resources/projects/_defaults/github/kodiak.latte diff --git a/resources/templates/_defaults/github/phpstan.latte b/resources/projects/_defaults/github/phpstan.latte similarity index 100% rename from resources/templates/_defaults/github/phpstan.latte rename to resources/projects/_defaults/github/phpstan.latte diff --git a/resources/templates/_defaults/github/tests.latte b/resources/projects/_defaults/github/tests.latte similarity index 100% rename from resources/templates/_defaults/github/tests.latte rename to resources/projects/_defaults/github/tests.latte diff --git a/resources/templates/_defaults/gitignore.latte b/resources/projects/_defaults/gitignore.latte similarity index 100% rename from resources/templates/_defaults/gitignore.latte rename to resources/projects/_defaults/gitignore.latte diff --git a/resources/templates/_defaults/phpstan.latte b/resources/projects/_defaults/phpstan.latte similarity index 100% rename from resources/templates/_defaults/phpstan.latte rename to resources/projects/_defaults/phpstan.latte diff --git a/resources/templates/_defaults/ruleset.latte b/resources/projects/_defaults/ruleset.latte similarity index 100% rename from resources/templates/_defaults/ruleset.latte rename to resources/projects/_defaults/ruleset.latte diff --git a/resources/templates/_defaults/var/gitignore.latte b/resources/projects/_defaults/var/gitignore.latte similarity index 100% rename from resources/templates/_defaults/var/gitignore.latte rename to resources/projects/_defaults/var/gitignore.latte diff --git a/resources/templates/_defaults/www/htaccess.latte b/resources/projects/_defaults/www/htaccess.latte similarity index 100% rename from resources/templates/_defaults/www/htaccess.latte rename to resources/projects/_defaults/www/htaccess.latte diff --git a/resources/templates/demo/app/Bootstrap.php b/resources/projects/demo/app/Bootstrap.php similarity index 100% rename from resources/templates/demo/app/Bootstrap.php rename to resources/projects/demo/app/Bootstrap.php diff --git a/resources/templates/demo/app/UI/@Templates/@layout.latte b/resources/projects/demo/app/UI/@Templates/@layout.latte similarity index 100% rename from resources/templates/demo/app/UI/@Templates/@layout.latte rename to resources/projects/demo/app/UI/@Templates/@layout.latte diff --git a/resources/templates/demo/app/UI/BasePresenter.php b/resources/projects/demo/app/UI/BasePresenter.php similarity index 100% rename from resources/templates/demo/app/UI/BasePresenter.php rename to resources/projects/demo/app/UI/BasePresenter.php diff --git a/resources/templates/demo/app/UI/Home/HomePresenter.php b/resources/projects/demo/app/UI/Home/HomePresenter.php similarity index 100% rename from resources/templates/demo/app/UI/Home/HomePresenter.php rename to resources/projects/demo/app/UI/Home/HomePresenter.php diff --git a/resources/templates/demo/app/UI/Home/Templates/default.latte b/resources/projects/demo/app/UI/Home/Templates/default.latte similarity index 100% rename from resources/templates/demo/app/UI/Home/Templates/default.latte rename to resources/projects/demo/app/UI/Home/Templates/default.latte diff --git a/resources/templates/demo/config/config.latte b/resources/projects/demo/config/config.latte similarity index 100% rename from resources/templates/demo/config/config.latte rename to resources/projects/demo/config/config.latte diff --git a/resources/templates/demo/config/local.latte b/resources/projects/demo/config/local.latte similarity index 100% rename from resources/templates/demo/config/local.latte rename to resources/projects/demo/config/local.latte diff --git a/resources/templates/demo/config/local.neon.latte b/resources/projects/demo/config/local.neon.latte similarity index 100% rename from resources/templates/demo/config/local.neon.latte rename to resources/projects/demo/config/local.neon.latte diff --git a/resources/templates/demo/www/index.php b/resources/projects/demo/www/index.php similarity index 100% rename from resources/templates/demo/www/index.php rename to resources/projects/demo/www/index.php diff --git a/src/Bootstrap.php b/src/Bootstrap.php index 3e10b1d..ab1dad0 100644 --- a/src/Bootstrap.php +++ b/src/Bootstrap.php @@ -3,6 +3,10 @@ namespace Contributte\Mate; use Contributte\Mate\Command\CraftCommand; +use Contributte\Mate\Config\Loader\ConfigLoader; +use Contributte\Mate\Crafter\Worker\CrafterWorker; +use Contributte\Mate\DI\BetterContainer; +use Contributte\Mate\Template\TemplateRenderer; use Symfony\Component\Console\Application; final class Bootstrap @@ -10,8 +14,13 @@ final class Bootstrap public static function boot(): Application { - $application = new Application('Mate', '1.0.0'); - $application->add(new CraftCommand()); + $container = new BetterContainer(); + $container->service(ConfigLoader::class, fn (): ConfigLoader => new ConfigLoader()); + $container->service(TemplateRenderer::class, fn (): TemplateRenderer => new TemplateRenderer()); + $container->service(CrafterWorker::class, fn (TemplateRenderer $templateRenderer): CrafterWorker => new CrafterWorker($templateRenderer)); + + $application = new Application('Mate', '0.1.0'); + $application->add($container->createInstance(CraftCommand::class)); return $application; } diff --git a/src/Command/BaseCommand.php b/src/Command/BaseCommand.php new file mode 100644 index 0000000..7391312 --- /dev/null +++ b/src/Command/BaseCommand.php @@ -0,0 +1,27 @@ + OutputInterface::VERBOSITY_NORMAL, + LogLevel::ALERT => OutputInterface::VERBOSITY_NORMAL, + LogLevel::CRITICAL => OutputInterface::VERBOSITY_NORMAL, + LogLevel::ERROR => OutputInterface::VERBOSITY_NORMAL, + LogLevel::WARNING => OutputInterface::VERBOSITY_NORMAL, + LogLevel::NOTICE => OutputInterface::VERBOSITY_NORMAL, + LogLevel::INFO => OutputInterface::VERBOSITY_NORMAL, + LogLevel::DEBUG => OutputInterface::VERBOSITY_NORMAL, + ]); + } + +} diff --git a/src/Command/CraftCommand.php b/src/Command/CraftCommand.php index 6317b4e..c7f5834 100644 --- a/src/Command/CraftCommand.php +++ b/src/Command/CraftCommand.php @@ -2,82 +2,106 @@ namespace Contributte\Mate\Command; -use Contributte\Mate\Generator\CommandGenerator; -use Contributte\Mate\Generator\HandlerGenerator; -use Nette\Neon\Neon; -use Nette\Utils\FileSystem; +use Contributte\Mate\Config\Loader\ConfigLoader; +use Contributte\Mate\Crafter\Worker\CrafterWorker; +use Contributte\Mate\Crafter\Worker\WorkerContextFactory; +use Nette\Safe; +use Nette\Schema\Expect; +use Nette\Schema\Processor; +use Nette\Schema\ValidationException; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; #[AsCommand( name: 'craft', description: 'Craft classes by defined config' )] -final class CraftCommand extends Command +final class CraftCommand extends BaseCommand { + public function __construct( + private ConfigLoader $configLoader, + private CrafterWorker $crafterWorker, + ) + { + parent::__construct(); + } + protected function configure(): void { - $this->addOption('data', 'd', InputOption::VALUE_REQUIRED, 'Data structure reference'); + $this->addOption('struct', 's', InputOption::VALUE_REQUIRED, 'Structure definition'); + $this->addOption('scope', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Scope definition'); } protected function execute(InputInterface $input, OutputInterface $output): int { - $cwd = getcwd(); - - // Load config - $file = FileSystem::read($cwd . '/.mate.neon'); - - /** @var array{data: array}>} $config */ - $config = Neon::decode($file); - - // Validation - /** @var string|false|null $dataKey */ - $dataKey = $input->getOption('data'); - - if ($dataKey === null || $dataKey === false) { - $output->writeln('Missing --data option'); + $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); + + try { + /** @var object{ struct: string, scope: string[] } $options */ + $options = (new Processor())->process(Expect::structure([ + 'struct' => Expect::mixed()->assert(fn ($v) => $v === null || $v === '', 'Option --struct|-s must be filled'), + 'scope' => Expect::arrayOf('string')->default([]), + ])->otherItems(), $input->getOptions()); + } catch (ValidationException $e) { + foreach ($e->getMessageObjects() as $message) { + $ui->error($message->variables['assertion']); + } return Command::FAILURE; } - if (!isset($config['data'][$dataKey])) { - $output->writeln(sprintf('Unknown data reference "%s" in .mate.neon', $dataKey)); + // Input + $struct = $options->struct; + $cwd = Safe::getcwd(); + $configFile = $cwd . '/.mate.neon'; + + // Config + $config = $this->configLoader->load($cwd, $cwd . '/.mate.neon'); + + // HUD + $ui->title('Input'); + $ui->table([], [ + ['CWD', $cwd], + ['Config', $configFile], + ['Struct', $struct], + ['Presets', implode(',', $config->mate->presets)], + ['Scopes', implode(',', $options->scope)], + ]); + + // Context + $workerContextFactory = new WorkerContextFactory(); + $workerContextFactory->withScopes($options->scope); + $workerContext = $workerContextFactory->from($config); + + // Worker validation + if (!$config->structs->has($struct)) { + $ui->error(sprintf('Unknown struct reference "%s" in .mate.neon', $struct)); return Command::FAILURE; } - // Entity - $dataClass = ucfirst($dataKey); + // Worker + $result = $this->crafterWorker->execute($workerContext, $this->createLogger($output)); - // Generators - $commandGenerator = new CommandGenerator(); + // HUD + $ui->title('Crafted'); - foreach ($config['data'] as $data) { - $filename = $cwd . sprintf('/app/Domain/%s/Create%sCommand.php', $dataClass, $dataClass); - $generatedClass = $commandGenerator->generate( - namespace: sprintf('App\Domain\%s', $dataClass), - commandClass: sprintf('App\Domain\%s\Create%sCommand', $dataClass, $dataClass), - fields: $data['fields'] - ); - FileSystem::write($filename, $generatedClass); - } + foreach ($result->items as $item) { + if ($item['state'] === 'skipped') { + continue; + } - $handlerGenerator = new HandlerGenerator(); - - foreach ($config['data'] as $data) { - $filename = $cwd . sprintf('/app/Domain/%s/Create%sHandler.php', $dataClass, $dataClass); - $generatedClass = $handlerGenerator->generate( - namespace: sprintf('App\Domain\%s', $dataClass), - handlerClass: sprintf('App\Domain\%s\Create%sHandler', $dataClass, $dataClass), - commandClass: sprintf('App\Domain\%s\Create%sCommand', $dataClass, $dataClass), - entityClass: $dataClass, - fields: $data['fields'] - ); - FileSystem::write($filename, $generatedClass); + $ui->block($item['crafter']); + $ui->table([], [ + ['Input', $item['input']], + ['Output', $item['output']], + ['Note', $item['note']], + ]); } return Command::SUCCESS; diff --git a/src/Command/InitCommand.php b/src/Command/InitCommand.php new file mode 100644 index 0000000..c238d05 --- /dev/null +++ b/src/Command/InitCommand.php @@ -0,0 +1,27 @@ + $scopes + */ + public function __construct( + public string $appDir, + public string $namespace, + public array $scopes, + public CraftersConfig $crafters, + ) + { + } + +} diff --git a/src/Config/CrafterConfig.php b/src/Config/CrafterConfig.php new file mode 100644 index 0000000..f887108 --- /dev/null +++ b/src/Config/CrafterConfig.php @@ -0,0 +1,22 @@ + $scopes + */ + public function __construct( + public string $crafter, + public string $template, + public string $class, + public string|null $mode = null, + public array $scopes = [], + public string|null $baseClass = null, + ) + { + } + +} diff --git a/src/Config/CraftersConfig.php b/src/Config/CraftersConfig.php new file mode 100644 index 0000000..6b5e20b --- /dev/null +++ b/src/Config/CraftersConfig.php @@ -0,0 +1,17 @@ + $items + */ + public function __construct( + public array $items, + ) + { + } + +} diff --git a/src/Config/DataConfig.php b/src/Config/DataConfig.php deleted file mode 100644 index 95df142..0000000 --- a/src/Config/DataConfig.php +++ /dev/null @@ -1,11 +0,0 @@ -loadFile($file); + + // Merge presets + $presets = $config['mate']['presets'] ?? ['default']; + + foreach ($presets as $preset) { + $presetFile = $this->loadFile(__DIR__ . '/../../../resources/presets/' . $preset . '/config.neon'); + + /** @phpstan-var ConfigShape $config */ + $config = SchemaHelpers::merge($presetFile, $config); + } + + return new InputConfig( + mate: new MateConfig( + presets: $presets, + ), + process: new ProcessConfig( + cwd: $cwd, + ), + app: new AppConfig( + appDir: $config['app']['appDir'], + namespace: $config['app']['namespace'], + scopes: $config['app']['scopes'] ?? [], + crafters: new CraftersConfig( + items: Arrays::map( + $config['app']['crafters'] ?? [], + fn (array $crafter): CrafterConfig => new CrafterConfig( + crafter: $crafter['crafter'], + template: $crafter['template'], + class: $crafter['class'], + mode: $crafter['mode'] ?? null, + scopes: $crafter['scopes'] ?? [], + baseClass: $crafter['baseClass'] ?? null + ) + ) + ) + ), + structs: new StructsConfig( + items: Arrays::map( + $config['structs'] ?? [], + fn (array $struct, string $structKey): StructConfig => new StructConfig( + name: $structKey, + fields: Arrays::map( + $struct['fields'] ?? [], + fn (array $field, string $fieldKey): StructFieldConfig => new StructFieldConfig( + name: $fieldKey, + type: $field['type'], + nullable: $field['nullable'] ?? false + ) + ) + ) + ) + ) + ); + } + + /** + * @phpstan-return ConfigShape + */ + private function loadFile(string $file): array + { + // Read file + $file = FileSystem::read($file); + + /** @phpstan-var ConfigShape $config */ + $config = Neon::decode($file); + + return $config; + } + +} diff --git a/src/Config/MateConfig.php b/src/Config/MateConfig.php index 20520e1..9bf4df6 100644 --- a/src/Config/MateConfig.php +++ b/src/Config/MateConfig.php @@ -2,20 +2,16 @@ namespace Contributte\Mate\Config; -use Nette\Schema\Expect; -use Nette\Schema\Schema; - final class MateConfig { - public static function schema(): Schema + /** + * @param array $presets + */ + public function __construct( + public array $presets, + ) { - return Expect::structure([ - 'username' => Expect::string()->required(), - 'email' => Expect::string()->required(), - 'password' => Expect::string()->required(), - ]) - ->castTo(self::class); } } diff --git a/src/Config/ProcessConfig.php b/src/Config/ProcessConfig.php new file mode 100644 index 0000000..5ff120b --- /dev/null +++ b/src/Config/ProcessConfig.php @@ -0,0 +1,14 @@ + $items + */ + public function __construct( + public array $items, + ) + { + } + + public function has(string $key): bool + { + return isset($this->items[$key]); + } + +} diff --git a/src/Crafter/Template/ClassContext.php b/src/Crafter/Template/ClassContext.php new file mode 100644 index 0000000..c533872 --- /dev/null +++ b/src/Crafter/Template/ClassContext.php @@ -0,0 +1,17 @@ + */ + public array $items = []; + + public function add( + string $crafter, + string $state, + string|null $note = null, + string|null $input = null, + string|null $output = null, + ): void + { + $this->items[] = [ + 'crafter' => $crafter, + 'state' => $state, + 'note' => $note, + 'input' => $input, + 'output' => $output, + ]; + } + +} diff --git a/src/Crafter/Worker/CrafterWorker.php b/src/Crafter/Worker/CrafterWorker.php new file mode 100644 index 0000000..5177df3 --- /dev/null +++ b/src/Crafter/Worker/CrafterWorker.php @@ -0,0 +1,103 @@ +mate->app->scopes === []) { + throw new LogicalException('No scope defined'); + } + + $result = new CrafterResult(); + + foreach ($workerContext->mate->app->crafters->items as $crafterKey => $crafter) { + // Skip crafter if not in scope + if (array_intersect($crafter->scopes, $workerContext->mate->app->scopes) === []) { + $result->add( + crafter: $crafterKey, + state: 'skipped', + note: 'Scope mismatch', + ); + + continue; + } + + foreach ($workerContext->mate->structs->items as $struct) { + // Prepare context + $resolvedClass = $this->templateRenderer->render($crafter->class, ['name' => $struct->name]); + $resolvedClassName = Classes::getClassName($resolvedClass); + $resolvedNamespace = Classes::getNamespace($resolvedClass); + $resolvedFilename = Strings::replace($resolvedClass, '#\\\#', '/'); + + $context = new TemplateContext( + class: new ClassContext( + class: $resolvedClass, + className: $resolvedClassName, + namespace: $resolvedNamespace, + extends: $crafter->baseClass, + ), + helper: new HelperContext( + structVar: sprintf('$%s', $struct->name), + entityClassName: sprintf('%s', ucfirst($struct->name)), + dtoClassName: sprintf('%sDto', ucfirst($struct->name)), + ), + struct: new StructContext( + name: $struct->name, + fields: Arrays::map( + $struct->fields, + fn (StructFieldConfig $field): StructFieldContext => new StructFieldContext( + name: $field->name, + type: $field->type, + nullable: $field->nullable, + ) + ) + ) + ); + + // Craft (input & output) + $intputFile = __DIR__ . '/../../../resources/presets/moderntv/' . $crafter->template; + $outputFile = $workerContext->mate->process->cwd . '/' . lcfirst($resolvedFilename) . '.php'; + + $outputContent = $this->templateRenderer->renderFile( + file: $intputFile, + params: ['ctx' => $context] + ); + + FileSystem::write($outputFile, $outputContent); + + $result->add( + crafter: $crafterKey, + state: 'crafted', + input: $intputFile, + output: $outputFile, + ); + } + } + + return $result; + } + +} diff --git a/src/Crafter/Worker/WorkerContext.php b/src/Crafter/Worker/WorkerContext.php new file mode 100644 index 0000000..6863d7a --- /dev/null +++ b/src/Crafter/Worker/WorkerContext.php @@ -0,0 +1,16 @@ + */ + private array $scopes = []; + + /** + * @param array $scopes + */ + public function withScopes(array $scopes): self + { + $this->scopes = $scopes; + + return $this; + } + + public function from(InputConfig $mateConfig): WorkerContext + { + if ($this->scopes !== []) { + $mateConfig->app->scopes = $this->scopes; + } + + return new WorkerContext( + mate: $mateConfig, + ); + } + +} diff --git a/src/DI/BetterContainer.php b/src/DI/BetterContainer.php new file mode 100644 index 0000000..5820510 --- /dev/null +++ b/src/DI/BetterContainer.php @@ -0,0 +1,18 @@ +addService($class, fn () => $this->callMethod($factory)); + + $this->wiring[$class] = [0 => [$class]]; + } + +} diff --git a/src/Exception/ConfigIOException.php b/src/Exception/ConfigIOException.php new file mode 100644 index 0000000..6da5d9e --- /dev/null +++ b/src/Exception/ConfigIOException.php @@ -0,0 +1,8 @@ + $fields - */ - public function generate( - string $namespace, - string $commandClass, - array $fields - ): string - { - $file = new PhpFile(); - $file->setStrictTypes(); - - $classNamespace = $file->addNamespace($namespace); - - $class = $classNamespace->addClass($classNamespace->simplifyType($commandClass)); - $class->setReadOnly(); - - $constructor = $class->addMethod('__construct'); - - foreach ($fields as $fieldName => $field) { - $parameter = $constructor->addPromotedParameter($fieldName); - $parameter->setPublic(); - $parameter->setType($field['type']); - $parameter->setReadOnly(); - } - - $printer = new Printer(); - - return $printer->printFile($file); - } - -} diff --git a/src/Generator/ControllerGenerator.php b/src/Generator/ControllerGenerator.php deleted file mode 100644 index dea472f..0000000 --- a/src/Generator/ControllerGenerator.php +++ /dev/null @@ -1,19 +0,0 @@ -printClass($class); - } - -} diff --git a/src/Generator/EntityGenerator.php b/src/Generator/EntityGenerator.php deleted file mode 100644 index 7581c57..0000000 --- a/src/Generator/EntityGenerator.php +++ /dev/null @@ -1,19 +0,0 @@ -printClass($class); - } - -} diff --git a/src/Generator/HandlerGenerator.php b/src/Generator/HandlerGenerator.php deleted file mode 100644 index 3c78a1c..0000000 --- a/src/Generator/HandlerGenerator.php +++ /dev/null @@ -1,65 +0,0 @@ - $fields - */ - public function generate( - string $namespace, - string $handlerClass, - string $commandClass, - string $entityClass, - array $fields - ): string - { - $dumper = new Dumper(); - $file = new PhpFile(); - $file->setStrictTypes(); - - $classNamespace = $file->addNamespace($namespace); - - $class = $classNamespace->addClass($classNamespace->simplifyType($handlerClass)); - $class->setReadOnly(); - - $constructor = $class->addMethod('__construct'); - $paramEm = $constructor->addPromotedParameter('em'); - $paramEm->setPrivate(); - $paramEm->setType($classNamespace->simplifyType('Doctrine\ORM\EntityManagerInterface')); - - $invoke = $class->addMethod('__invoke'); - $invoke->setPublic(); - $invoke->setReturnType('object'); - $invoke->addParameter('command') - ->setType($commandClass); - - // Inner - $arguments = []; - - foreach ($fields as $fieldName => $field) { - $arguments[$fieldName] = new Literal('$command->' . $fieldName); - } - - $invoke->addBody('$entity = ?;', [ - new Literal($dumper->format('new ' . $entityClass . '(...?:)', $arguments)), - ]); - $invoke->addBody("\n"); - $invoke->addBody('$this->em->persist($entity);'); - $invoke->addBody('$this->em->flush();'); - $invoke->addBody("\n"); - $invoke->addBody('return $entity;'); - - $printer = new Printer(); - - return $printer->printFile($file); - } - -} diff --git a/src/Loader/MateLoader.php b/src/Loader/MateLoader.php deleted file mode 100644 index 052f78d..0000000 --- a/src/Loader/MateLoader.php +++ /dev/null @@ -1,8 +0,0 @@ -latte = new Engine(); + $this->latte->setLoader(new StringLoader()); + $this->latte->setStrictTypes(); + $this->latte->setStrictParsing(); + $this->latte->setContentType(ContentType::Text); + } + + /** + * @param array $params + */ + public function render(string $string, array $params = []): string + { + return $this->latte->renderToString($string, $params); + } + + /** + * @param array $params + */ + public function renderFile(string $file, array $params = []): string + { + return $this->render(FileSystem::read($file), $params); + } + +} diff --git a/src/Utils/Classes.php b/src/Utils/Classes.php new file mode 100644 index 0000000..fc78af4 --- /dev/null +++ b/src/Utils/Classes.php @@ -0,0 +1,23 @@ +