From 0c7f247ca92ca10bd58fa1b5a740b78fc00a7caf Mon Sep 17 00:00:00 2001 From: Pascal Cremer Date: Sat, 21 Jan 2023 01:22:54 +0100 Subject: [PATCH 01/44] feat: Add initial domain model maker WIP! refs: #13 --- composer.json | 5 +- composer.lock | 509 ++++++++++++++++++- config/services.yaml | 4 + src/Maker/MakeModel.php | 190 +++++++ src/Resources/skeleton/identity/Id.tpl.php | 9 + src/Resources/skeleton/identity/Uuid.tpl.php | 9 + src/Resources/skeleton/model/Model.tpl.php | 36 ++ 7 files changed, 759 insertions(+), 3 deletions(-) create mode 100644 src/Maker/MakeModel.php create mode 100644 src/Resources/skeleton/identity/Id.tpl.php create mode 100644 src/Resources/skeleton/identity/Uuid.tpl.php create mode 100644 src/Resources/skeleton/model/Model.tpl.php diff --git a/composer.json b/composer.json index 8cb7fac..227f054 100644 --- a/composer.json +++ b/composer.json @@ -36,7 +36,8 @@ "mockery/mockery": "^1.5", "phpstan/phpstan": "^1.9", "phpstan/phpstan-mockery": "^1.1", - "phpunit/phpunit": "^9.5" + "phpunit/phpunit": "^9.5", + "symfony/maker-bundle": "^1.48" }, "scripts": { "gc:tests": "phpunit --testdox --colors=always", @@ -44,4 +45,4 @@ "gc:cs-fix": "php-cs-fixer fix --config .php-cs-fixer.php -vvv || true", "gc:phpstan": "phpstan analyse --memory-limit=2G --no-progress --level=8" } -} \ No newline at end of file +} diff --git a/composer.lock b/composer.lock index 4374b71..a5dbc6e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "46885e9569c3d37adfb7198a73bbcb7b", + "content-hash": "93cb43bd0e2b4d469005556b1c18fa2c", "packages": [ { "name": "beberlei/assert", @@ -5463,6 +5463,181 @@ ], "time": "2020-09-28T06:39:44+00:00" }, + { + "name": "symfony/cache", + "version": "v6.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache.git", + "reference": "ddd1a70bfdf4ed19facad0db689c7bca979d322e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache/zipball/ddd1a70bfdf4ed19facad0db689c7bca979d322e", + "reference": "ddd1a70bfdf4ed19facad0db689c7bca979d322e", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/cache": "^2.0|^3.0", + "psr/log": "^1.1|^2|^3", + "symfony/cache-contracts": "^1.1.7|^2|^3", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/var-exporter": "^6.2" + }, + "conflict": { + "doctrine/dbal": "<2.13.1", + "symfony/dependency-injection": "<5.4", + "symfony/http-kernel": "<5.4", + "symfony/var-dumper": "<5.4" + }, + "provide": { + "psr/cache-implementation": "2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0", + "symfony/cache-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "cache/integration-tests": "dev-master", + "doctrine/dbal": "^2.13.1|^3.0", + "predis/predis": "^1.1", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "symfony/config": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/filesystem": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/messenger": "^5.4|^6.0", + "symfony/var-dumper": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Cache\\": "" + }, + "classmap": [ + "Traits/ValueWrapper.php" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides extended PSR-6, PSR-16 (and tags) implementations", + "homepage": "https://symfony.com", + "keywords": [ + "caching", + "psr6" + ], + "support": { + "source": "https://github.com/symfony/cache/tree/v6.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-12-29T16:29:13+00:00" + }, + { + "name": "symfony/cache-contracts", + "version": "v3.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache-contracts.git", + "reference": "e8d1a5fc43534063204b74c080ebe36307d12271" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/e8d1a5fc43534063204b74c080ebe36307d12271", + "reference": "e8d1a5fc43534063204b74c080ebe36307d12271", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/cache": "^3.0" + }, + "suggest": { + "symfony/cache-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.3-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Cache\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to caching", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/cache-contracts/tree/v3.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-11-25T10:21:52+00:00" + }, { "name": "symfony/finder", "version": "v6.2.3", @@ -5527,6 +5702,250 @@ ], "time": "2022-12-22T17:55:15+00:00" }, + { + "name": "symfony/framework-bundle", + "version": "v6.2.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/framework-bundle.git", + "reference": "c26ddbb2c2d8e5eaebbb1297b833be0967725fbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/c26ddbb2c2d8e5eaebbb1297b833be0967725fbc", + "reference": "c26ddbb2c2d8e5eaebbb1297b833be0967725fbc", + "shasum": "" + }, + "require": { + "composer-runtime-api": ">=2.1", + "ext-xml": "*", + "php": ">=8.1", + "symfony/cache": "^5.4|^6.0", + "symfony/config": "^6.1", + "symfony/dependency-injection": "^6.2", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/error-handler": "^6.1", + "symfony/event-dispatcher": "^5.4|^6.0", + "symfony/filesystem": "^5.4|^6.0", + "symfony/finder": "^5.4|^6.0", + "symfony/http-foundation": "^6.2", + "symfony/http-kernel": "^6.2.1", + "symfony/polyfill-mbstring": "~1.0", + "symfony/routing": "^5.4|^6.0" + }, + "conflict": { + "doctrine/annotations": "<1.13.1", + "doctrine/persistence": "<1.3", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "phpunit/phpunit": "<5.4.3", + "symfony/asset": "<5.4", + "symfony/console": "<5.4", + "symfony/dom-crawler": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/form": "<5.4", + "symfony/http-client": "<5.4", + "symfony/lock": "<5.4", + "symfony/mailer": "<5.4", + "symfony/messenger": "<6.2", + "symfony/mime": "<6.2", + "symfony/property-access": "<5.4", + "symfony/property-info": "<5.4", + "symfony/security-core": "<5.4", + "symfony/security-csrf": "<5.4", + "symfony/serializer": "<6.1", + "symfony/stopwatch": "<5.4", + "symfony/translation": "<5.4", + "symfony/twig-bridge": "<5.4", + "symfony/twig-bundle": "<5.4", + "symfony/validator": "<5.4", + "symfony/web-profiler-bundle": "<5.4", + "symfony/workflow": "<5.4" + }, + "require-dev": { + "doctrine/annotations": "^1.13.1|^2", + "doctrine/persistence": "^1.3|^2|^3", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/asset": "^5.4|^6.0", + "symfony/browser-kit": "^5.4|^6.0", + "symfony/console": "^5.4.9|^6.0.9", + "symfony/css-selector": "^5.4|^6.0", + "symfony/dom-crawler": "^5.4|^6.0", + "symfony/dotenv": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/form": "^5.4|^6.0", + "symfony/html-sanitizer": "^6.1", + "symfony/http-client": "^5.4|^6.0", + "symfony/lock": "^5.4|^6.0", + "symfony/mailer": "^5.4|^6.0", + "symfony/messenger": "^6.2", + "symfony/mime": "^6.2", + "symfony/notifier": "^5.4|^6.0", + "symfony/polyfill-intl-icu": "~1.0", + "symfony/process": "^5.4|^6.0", + "symfony/property-info": "^5.4|^6.0", + "symfony/rate-limiter": "^5.4|^6.0", + "symfony/security-bundle": "^5.4|^6.0", + "symfony/semaphore": "^5.4|^6.0", + "symfony/serializer": "^6.1", + "symfony/stopwatch": "^5.4|^6.0", + "symfony/string": "^5.4|^6.0", + "symfony/translation": "^5.4|^6.0", + "symfony/twig-bundle": "^5.4|^6.0", + "symfony/uid": "^5.4|^6.0", + "symfony/validator": "^5.4|^6.0", + "symfony/web-link": "^5.4|^6.0", + "symfony/workflow": "^5.4|^6.0", + "symfony/yaml": "^5.4|^6.0", + "twig/twig": "^2.10|^3.0" + }, + "suggest": { + "ext-apcu": "For best performance of the system caches", + "symfony/console": "For using the console commands", + "symfony/form": "For using forms", + "symfony/property-info": "For using the property_info service", + "symfony/serializer": "For using the serializer service", + "symfony/validator": "For using validation", + "symfony/web-link": "For using web links, features such as preloading, prefetching or prerendering", + "symfony/yaml": "For using the debug:config and lint:yaml commands" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\FrameworkBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/framework-bundle/tree/v6.2.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-12-20T16:41:15+00:00" + }, + { + "name": "symfony/maker-bundle", + "version": "v1.48.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/maker-bundle.git", + "reference": "2e428e8432e9879187672fe08f1cc335e2a31dd6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/2e428e8432e9879187672fe08f1cc335e2a31dd6", + "reference": "2e428e8432e9879187672fe08f1cc335e2a31dd6", + "shasum": "" + }, + "require": { + "doctrine/inflector": "^2.0", + "nikic/php-parser": "^4.11", + "php": ">=8.0", + "symfony/config": "^5.4.7|^6.0", + "symfony/console": "^5.4.7|^6.0", + "symfony/dependency-injection": "^5.4.7|^6.0", + "symfony/deprecation-contracts": "^2.2|^3", + "symfony/filesystem": "^5.4.7|^6.0", + "symfony/finder": "^5.4.3|^6.0", + "symfony/framework-bundle": "^5.4.7|^6.0", + "symfony/http-kernel": "^5.4.7|^6.0" + }, + "conflict": { + "doctrine/doctrine-bundle": "<2.4", + "doctrine/orm": "<2.10", + "symfony/doctrine-bridge": "<5.4" + }, + "require-dev": { + "composer/semver": "^3.0", + "doctrine/doctrine-bundle": "^2.4", + "doctrine/orm": "^2.10.0", + "symfony/http-client": "^5.4.7|^6.0", + "symfony/phpunit-bridge": "^5.4.7|^6.0", + "symfony/polyfill-php80": "^1.16.0", + "symfony/process": "^5.4.7|^6.0", + "symfony/security-core": "^5.4.7|^6.0", + "symfony/yaml": "^5.4.3|^6.0", + "twig/twig": "^2.0|^3.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-main": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Bundle\\MakerBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Maker helps you create empty commands, controllers, form classes, tests and more so you can forget about writing boilerplate code.", + "homepage": "https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html", + "keywords": [ + "code generator", + "generator", + "scaffold", + "scaffolding" + ], + "support": { + "issues": "https://github.com/symfony/maker-bundle/issues", + "source": "https://github.com/symfony/maker-bundle/tree/v1.48.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-11-14T10:48:46+00:00" + }, { "name": "symfony/options-resolver", "version": "v6.2.0", @@ -5734,6 +6153,94 @@ ], "time": "2022-11-02T09:08:04+00:00" }, + { + "name": "symfony/routing", + "version": "v6.2.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "35fec764f3e2c8c08fb340d275c84bc78ca7e0c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/35fec764f3e2c8c08fb340d275c84bc78ca7e0c9", + "reference": "35fec764f3e2c8c08fb340d275c84bc78ca7e0c9", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "conflict": { + "doctrine/annotations": "<1.12", + "symfony/config": "<6.2", + "symfony/dependency-injection": "<5.4", + "symfony/yaml": "<5.4" + }, + "require-dev": { + "doctrine/annotations": "^1.12|^2", + "psr/log": "^1|^2|^3", + "symfony/config": "^6.2", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/yaml": "^5.4|^6.0" + }, + "suggest": { + "symfony/config": "For using the all-in-one router or any loader", + "symfony/expression-language": "For using expression matching", + "symfony/http-foundation": "For using a Symfony Request object", + "symfony/yaml": "For using the YAML loader" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v6.2.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-12-20T16:41:15+00:00" + }, { "name": "symfony/stopwatch", "version": "v6.2.0", diff --git a/config/services.yaml b/config/services.yaml index ee31b39..63cc427 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -16,3 +16,7 @@ services: arguments: - '@Symfony\Component\Messenger\MessageBusInterface' public: true + + GeekCell\DddBundle\Maker\MakeModel: + tags: + - { name: maker.command } diff --git a/src/Maker/MakeModel.php b/src/Maker/MakeModel.php new file mode 100644 index 0000000..cce1e82 --- /dev/null +++ b/src/Maker/MakeModel.php @@ -0,0 +1,190 @@ +addArgument( + 'name', + InputArgument::OPTIONAL, + 'The name of the model class (e.g. Customer)', + ) + ->addOption( + 'aggregate-root', + null, + InputOption::VALUE_NONE, + 'Marks the model as aggregate root', + ) + ->addOption( + 'with-identity', + null, + InputOption::VALUE_REQUIRED, + 'Whether an identity value object should be created', + ) + ->addOption( + 'with-suffix', + null, + InputOption::VALUE_NONE, + 'Adds the suffix "Model" to the model class name', + ) + ; + } + + public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void + { + /** @var string $modelName */ + $modelName = $input->getArgument('name'); + $useSuffix = $io->confirm( + sprintf( + 'Do you want to suffix the model class name? (%sModel)', + $modelName, + ), + false, + ); + $input->setOption('with-suffix', $useSuffix); + + if (false === $input->getOption('aggregate-root')) { + $asAggregateRoot = $io->confirm( + sprintf( + 'Do you want create %s%s as aggregate root?', + $modelName, + $useSuffix ? 'Model' : '', + ), + true, + ); + $input->setOption('aggregate-root', $asAggregateRoot); + } + + if (null === $input->getOption('with-identity')) { + $withIdentity = $io->choice( + sprintf( + 'How do you want to identify %s%s?', + $modelName, + $useSuffix ? 'Model' : '', + ), + [ + 'id' => sprintf( + 'Numeric identity representation (%sId)', + $modelName, + ), + 'uuid' => sprintf( + 'UUID representation (%sUuid)', + $modelName, + ), + 'none' => 'I\'ll add it later myself', + ], + ); + $input->setOption('with-identity', $withIdentity); + } + } + + public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void + { + /** @var string $modelName */ + $modelName = $input->getArgument('name'); + $suffix = $input->getOption('with-suffix') ? 'Model' : ''; + + $classesToImport = []; + + /** @var string $withIdentity */ + $withIdentity = $input->getOption('with-identity'); + if ('none' !== $withIdentity) { + $identityClassNameDetails = $generator->createClassNameDetails( + $modelName, + 'Domain\\Model\\ValueObject\\Identity\\', + ucfirst($withIdentity), + ); + + $templatePath = sprintf( + '%s/../Resources/skeleton/identity/%s.tpl.php', + __DIR__, + ucfirst($withIdentity), + ); + + $extendsAlias = match ($withIdentity) { + 'id' => 'AbstractId', + 'uuid' => 'AbstractUuid', + default => throw new \InvalidArgumentException('Invalid identity type'), + }; + + $baseClass = match ($withIdentity) { + 'id' => [Id::class => $extendsAlias], + 'uuid' => [Uuid::class => $extendsAlias], + default => throw new \InvalidArgumentException('Invalid identity type'), + }; + + $generator->generateClass( + $identityClassNameDetails->getFullName(), + $templatePath, + [ + 'extends_alias' => $extendsAlias, + // @phpstan-ignore-next-line + 'use_statements' => new UseStatementGenerator([$baseClass]), + ] + ); + + $classesToImport[] = $identityClassNameDetails->getFullName(); + } + + $modelClassNameDetails = $generator->createClassNameDetails( + $modelName, + 'Domain\\Model\\', + $suffix, + ); + + if ($input->getOption('aggregate-root')) { + $classesToImport[] = AggregateRoot::class; + } + + $templatePath = __DIR__.'/../Resources/skeleton/model/Model.tpl.php'; + $generator->generateClass( + $modelClassNameDetails->getFullName(), + $templatePath, + [ + 'aggregate_root' => $input->getOption('aggregate-root'), + 'entity' => $input->getOption('entity'), + 'use_statements' => new UseStatementGenerator($classesToImport), + 'with_identity' => 'none' !== $withIdentity ? $withIdentity : null, + ] + ); + + $generator->writeChanges(); + + $this->writeSuccessMessage($io); + } + + public function configureDependencies(DependencyBuilder $dependencies, InputInterface $input = null): void + { + // TODO: Implement configureDependencies() method. + } +} diff --git a/src/Resources/skeleton/identity/Id.tpl.php b/src/Resources/skeleton/identity/Id.tpl.php new file mode 100644 index 0000000..11a8316 --- /dev/null +++ b/src/Resources/skeleton/identity/Id.tpl.php @@ -0,0 +1,9 @@ + + +namespace ; + + + +class extends +{ +} diff --git a/src/Resources/skeleton/identity/Uuid.tpl.php b/src/Resources/skeleton/identity/Uuid.tpl.php new file mode 100644 index 0000000..11a8316 --- /dev/null +++ b/src/Resources/skeleton/identity/Uuid.tpl.php @@ -0,0 +1,9 @@ + + +namespace ; + + + +class extends +{ +} diff --git a/src/Resources/skeleton/model/Model.tpl.php b/src/Resources/skeleton/model/Model.tpl.php new file mode 100644 index 0000000..7d7a6b1 --- /dev/null +++ b/src/Resources/skeleton/model/Model.tpl.php @@ -0,0 +1,36 @@ + + +namespace ; + + + +class extends AggregateRoot +{ + + /** + * @var + */ + private $; + + + public function __construct() + { + + $this->id = new Id(1); + + $this->uuid = Uuid::random(); + + } + + + /** + * Get + * + * @return + */ + public function get(): + { + return $this->; + } + +} From 3a9c485b17ed53a740870d11b48bfb7312f1482b Mon Sep 17 00:00:00 2001 From: Pascal Cremer Date: Sat, 21 Jan 2023 15:55:36 +0100 Subject: [PATCH 02/44] feat: Add entity mapping capabilities to the model maker. WIP! refs: #13 --- config/services.yaml | 8 + src/Maker/Doctrine/DoctrineConfigUpdater.php | 82 ++++ src/Maker/MakeModel.php | 430 +++++++++++++++--- .../skeleton/doctrine/Mapping.tpl.xml.php | 13 + src/Resources/skeleton/identity/Uuid.tpl.php | 9 - .../model/DoctrineMappingType.tpl.php | 20 + .../Id.tpl.php => model/Identity.tpl.php} | 2 +- src/Resources/skeleton/model/Model.tpl.php | 33 +- 8 files changed, 522 insertions(+), 75 deletions(-) create mode 100644 src/Maker/Doctrine/DoctrineConfigUpdater.php create mode 100644 src/Resources/skeleton/doctrine/Mapping.tpl.xml.php delete mode 100644 src/Resources/skeleton/identity/Uuid.tpl.php create mode 100644 src/Resources/skeleton/model/DoctrineMappingType.tpl.php rename src/Resources/skeleton/{identity/Id.tpl.php => model/Identity.tpl.php} (53%) diff --git a/config/services.yaml b/config/services.yaml index 63cc427..6b3b0e9 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -18,5 +18,13 @@ services: public: true GeekCell\DddBundle\Maker\MakeModel: + class: GeekCell\DddBundle\Maker\MakeModel + arguments: + - '@GeekCell\DddBundle\Maker\Doctrine\DoctrineConfigUpdater' + - '@maker.file_manager' tags: - { name: maker.command } + + GeekCell\DddBundle\Maker\Doctrine\DoctrineConfigUpdater: + class: GeekCell\DddBundle\Maker\Doctrine\DoctrineConfigUpdater + public: false diff --git a/src/Maker/Doctrine/DoctrineConfigUpdater.php b/src/Maker/Doctrine/DoctrineConfigUpdater.php new file mode 100644 index 0000000..f681935 --- /dev/null +++ b/src/Maker/Doctrine/DoctrineConfigUpdater.php @@ -0,0 +1,82 @@ +createYamlSourceManipulator($yamlSource); + $data['doctrine']['dbal']['types'][$identifier] = $mappingClass; + + return $this->getYamlContentsFromData($data); + } + + /** + * Updates the default entity mapping configuration. + * + * @param string $yamlSource The contents of current doctrine.yaml + * @param string $mappingType The type of the mapping (xml or annotation) + * @param string $directory The directory where the mapping files are located + * + * @return string The updated doctrine.yaml contents + */ + public function updateORMDefaultEntityMapping(string $yamlSource, string $mappingType, string $directory): string + { + Assert\Assertion::inArray($mappingType, ['xml', 'annotation'], 'Invalid mapping type: %s'); + + $data = $this->createYamlSourceManipulator($yamlSource); + $data['doctrine']['orm']['mappings']['App']['type'] = $mappingType; + $data['doctrine']['orm']['mappings']['App']['dir'] = $directory; + $data['doctrine']['orm']['mappings']['App']['prefix'] = 'App\Domain\Model'; + $data['doctrine']['orm']['mappings']['App']['alias'] = 'App'; + $data['doctrine']['orm']['mappings']['App']['is_bundle'] = false; + + return $this->getYamlContentsFromData($data); + } + + /** + * Creates a YamlSourceManipulator from a YAML source. + * + * @param string $yamlSource + * @return array + */ + private function createYamlSourceManipulator(string $yamlSource): array + { + $this->manipulator = new YamlSourceManipulator($yamlSource); + return $this->manipulator->getData(); + } + + /** + * Returns the updated YAML contents for the given data. + * + * @param array $yamlData + * @return string + */ + private function getYamlContentsFromData(array $yamlData): string + { + Assert\Assertion::notNull($this->manipulator); + $this->manipulator->setData($yamlData); + + return $this->manipulator->getContents(); + } +} diff --git a/src/Maker/MakeModel.php b/src/Maker/MakeModel.php index cce1e82..515ce52 100644 --- a/src/Maker/MakeModel.php +++ b/src/Maker/MakeModel.php @@ -7,30 +7,71 @@ use GeekCell\Ddd\Domain\ValueObject\Id; use GeekCell\Ddd\Domain\ValueObject\Uuid; use GeekCell\DddBundle\Domain\AggregateRoot; +use GeekCell\DddBundle\Infrastructure\Doctrine\Type\AbstractIdType; +use GeekCell\DddBundle\Infrastructure\Doctrine\Type\AbstractUuidType; +use GeekCell\DddBundle\Maker\Doctrine\DoctrineConfigUpdater; use Symfony\Bundle\MakerBundle\InputConfiguration; use Symfony\Bundle\MakerBundle\DependencyBuilder; use Symfony\Bundle\MakerBundle\ConsoleStyle; +use Symfony\Bundle\MakerBundle\Doctrine\ORMDependencyBuilder; +use Symfony\Bundle\MakerBundle\FileManager; use Symfony\Bundle\MakerBundle\Generator; use Symfony\Bundle\MakerBundle\InputAwareMakerInterface; use Symfony\Bundle\MakerBundle\Maker\AbstractMaker; +use Symfony\Bundle\MakerBundle\Util\ClassNameDetails; use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator; +use Symfony\Bundle\MakerBundle\Util\YamlManipulationFailedException; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; +use function Symfony\Component\String\u; + final class MakeModel extends AbstractMaker implements InputAwareMakerInterface { + /** + * @var array> + */ + private $classesToImport = []; + + /** + * @var array + */ + private $templateVariables = []; + + + /** + * Constructor. + * + * @param DoctrineConfigUpdater $doctrineUpdater + * @param FileManager $fileManager + */ + public function __construct( + private DoctrineConfigUpdater $doctrineUpdater, + private FileManager $fileManager, + ) { + } + + /** + * @inheritDoc + */ public static function getCommandName(): string { return 'make:ddd:model'; } + /** + * @inheritDoc + */ public static function getCommandDescription(): string { return 'Creates a new domain model class'; } + /** + * @inheritDoc + */ public function configureCommand(Command $command, InputConfiguration $inputConfig): void { $command @@ -45,6 +86,12 @@ public function configureCommand(Command $command, InputConfiguration $inputConf InputOption::VALUE_NONE, 'Marks the model as aggregate root', ) + ->addOption( + 'entity', + null, + InputOption::VALUE_REQUIRED, + 'Use this model as Doctrine entity', + ) ->addOption( 'with-identity', null, @@ -60,6 +107,21 @@ public function configureCommand(Command $command, InputConfiguration $inputConf ; } + /** + * @inheritDoc + */ + public function configureDependencies(DependencyBuilder $dependencies, InputInterface $input = null): void + { + if (null === $input || !$this->shouldGenerateEntity($input)) { + return; + } + + ORMDependencyBuilder::buildDependencies($dependencies); + } + + /** + * @inheritDoc + */ public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void { /** @var string $modelName */ @@ -101,81 +163,62 @@ public function interact(InputInterface $input, ConsoleStyle $io, Command $comma 'UUID representation (%sUuid)', $modelName, ), - 'none' => 'I\'ll add it later myself', + 'n/a' => 'I\'ll take care later myself', ], ); $input->setOption('with-identity', $withIdentity); } + + if (null === $input->getOption('entity')) { + $asEntity = $io->choice( + sprintf( + 'Do you want %s%s to be a (Doctrine) database entity?', + $modelName, + $useSuffix ? 'Model' : '', + ), + [ + 'attributes' => 'Yes, via PHP attributes', + 'xml' => 'Yes, via XML mapping', + 'n/a' => 'No, I\'ll handle it separately', + ], + ); + $input->setOption('entity', $asEntity); + } } + /** + * @inheritDoc + */ public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void { /** @var string $modelName */ $modelName = $input->getArgument('name'); $suffix = $input->getOption('with-suffix') ? 'Model' : ''; - $classesToImport = []; - - /** @var string $withIdentity */ - $withIdentity = $input->getOption('with-identity'); - if ('none' !== $withIdentity) { - $identityClassNameDetails = $generator->createClassNameDetails( - $modelName, - 'Domain\\Model\\ValueObject\\Identity\\', - ucfirst($withIdentity), - ); - - $templatePath = sprintf( - '%s/../Resources/skeleton/identity/%s.tpl.php', - __DIR__, - ucfirst($withIdentity), - ); - - $extendsAlias = match ($withIdentity) { - 'id' => 'AbstractId', - 'uuid' => 'AbstractUuid', - default => throw new \InvalidArgumentException('Invalid identity type'), - }; - - $baseClass = match ($withIdentity) { - 'id' => [Id::class => $extendsAlias], - 'uuid' => [Uuid::class => $extendsAlias], - default => throw new \InvalidArgumentException('Invalid identity type'), - }; - - $generator->generateClass( - $identityClassNameDetails->getFullName(), - $templatePath, - [ - 'extends_alias' => $extendsAlias, - // @phpstan-ignore-next-line - 'use_statements' => new UseStatementGenerator([$baseClass]), - ] - ); - - $classesToImport[] = $identityClassNameDetails->getFullName(); - } - $modelClassNameDetails = $generator->createClassNameDetails( $modelName, 'Domain\\Model\\', $suffix, ); + $this->templateVariables['class_name'] = $modelClassNameDetails->getShortName(); + + $this->generateIdentity($modelName, $input, $io, $generator); + $this->generateEntity($modelClassNameDetails, $input, $io, $generator); + if ($input->getOption('aggregate-root')) { - $classesToImport[] = AggregateRoot::class; + $this->classesToImport[] = AggregateRoot::class; + $this->templateVariables['extends_aggregate_root'] = true; } + // @phpstan-ignore-next-line + $this->templateVariables['use_statements'] = new UseStatementGenerator($this->classesToImport); + $templatePath = __DIR__.'/../Resources/skeleton/model/Model.tpl.php'; $generator->generateClass( $modelClassNameDetails->getFullName(), $templatePath, - [ - 'aggregate_root' => $input->getOption('aggregate-root'), - 'entity' => $input->getOption('entity'), - 'use_statements' => new UseStatementGenerator($classesToImport), - 'with_identity' => 'none' !== $withIdentity ? $withIdentity : null, - ] + $this->templateVariables, ); $generator->writeChanges(); @@ -183,8 +226,291 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen $this->writeSuccessMessage($io); } - public function configureDependencies(DependencyBuilder $dependencies, InputInterface $input = null): void + /** + * Optionally, generate the identity value object for the model. + * + * @param string $modelName + * @param InputInterface $input + * @param ConsoleStyle $io + * @param Generator $generator + */ + private function generateIdentity( + string $modelName, + InputInterface $input, + ConsoleStyle $io, + Generator $generator + ): void + { + if (!$this->shouldGenerateIdentity($input)) { + return; + } + + // 1. Generate the identity value object. + + /** @var string $identityType */ + $identityType = $input->getOption('with-identity'); + $identityClassNameDetails = $generator->createClassNameDetails( + $modelName, + 'Domain\\Model\\ValueObject\\Identity\\', + ucfirst($identityType), + ); + + $extendsAlias = match ($identityType) { + 'id' => 'AbstractId', + 'uuid' => 'AbstractUuid', + default => null, + }; + + $baseClass = match ($identityType) { + 'id' => [Id::class => $extendsAlias], + 'uuid' => [Uuid::class => $extendsAlias], + default => null, + }; + + if (!$extendsAlias || !$baseClass) { + throw new \InvalidArgumentException(sprintf('Unknown identity type "%s"', $identityType)); + } + + // @phpstan-ignore-next-line + $useStatements = new UseStatementGenerator([$baseClass]); + + $generator->generateClass( + $identityClassNameDetails->getFullName(), + __DIR__.'/../Resources/skeleton/model/Identity.tpl.php', + [ + 'identity_class' => $identityClassNameDetails->getShortName(), + 'extends_alias' => $extendsAlias, + 'use_statements' => $useStatements, + ], + ); + + $this->classesToImport[] = $identityClassNameDetails->getFullName(); + $this->templateVariables['identity_type'] = $identityType; + $this->templateVariables['identity_class'] = $identityClassNameDetails->getShortName(); + + if (!$this->shouldGenerateEntity($input)) { + return; + } + + // 2. Generate custom Doctrine mapping type for the identity. + + $mappingTypeClassNameDetails = $generator->createClassNameDetails( + $modelName.ucfirst($identityType), + 'Infrastructure\\Doctrine\\DBAL\\Type\\', + 'Type', + ); + + $baseTypeClass = match ($identityType) { + 'id' => AbstractIdType::class, + 'uuid' => AbstractUuidType::class, + default => null, + }; + + if (!$baseTypeClass) { + throw new \InvalidArgumentException(sprintf('Unknown identity type "%s"', $identityType)); + } + + $useStatements = new UseStatementGenerator([ + $identityClassNameDetails->getFullName(), + $baseTypeClass + ]); + + $typeName = u($identityClassNameDetails->getShortName())->snake()->toString(); + $generator->generateClass( + $mappingTypeClassNameDetails->getFullName(), + __DIR__.'/../Resources/skeleton/model/DoctrineMappingType.tpl.php', + [ + 'type_name' => $typeName, + 'type_class' => $mappingTypeClassNameDetails->getShortName(), + 'extends_type_class' => sprintf('Abstract%sType', ucfirst($identityType)), + 'identity_class' => $identityClassNameDetails->getShortName(), + 'use_statements' => $useStatements, + ], + ); + + $configPath = 'config/packages/doctrine.yaml'; + if (!$this->fileManager->fileExists($configPath)) { + $io->error(sprintf('Doctrine configuration at path "%s" does not exist.', $configPath)); + return; + } + + // 2.1 Add the custom mapping type to the Doctrine configuration. + + $newYaml = $this->doctrineUpdater->addCustomDBALMappingType( + $this->fileManager->getFileContents($configPath), + $typeName, + $mappingTypeClassNameDetails->getFullName(), + ); + $generator->dumpFile($configPath, $newYaml); + + $this->templateVariables['type_class'] = $mappingTypeClassNameDetails->getShortName(); + $this->templateVariables['type_name'] = $typeName; + + // Write out the changes. + $generator->writeChanges(); + } + + /** + * Optionally, generate entity mappings for the model. + * + * @param ClassNameDetails $modelClassNameDetails + * @param InputInterface $input + * @param ConsoleStyle $io + * @param Generator $generator + */ + private function generateEntity( + ClassNameDetails $modelClassNameDetails, + InputInterface $input, + ConsoleStyle $io, + Generator $generator + ): void { - // TODO: Implement configureDependencies() method. + if (!$this->shouldGenerateEntity($input)) { + return; + } + + $modelName = $modelClassNameDetails->getShortName(); + $configPath = 'config/packages/doctrine.yaml'; + if (!$this->fileManager->fileExists($configPath)) { + $io->error(sprintf('Doctrine configuration at path "%s" does not exist.', $configPath)); + return; + } + + if ($this->shouldGenerateEntityAttributes($input)) { + try { + $newYaml = $this->doctrineUpdater->updateORMDefaultEntityMapping( + $this->fileManager->getFileContents($configPath), + 'attribute', + '%kernel.project_dir%/src/Domain/Model', + ); + $generator->dumpFile($configPath, $newYaml); + $this->classesToImport[] = ['Doctrine\\ORM\\Mapping' => 'ORM']; + $this->templateVariables['as_entity'] = true; + } catch (YamlManipulationFailedException $e) { + $io->error($e->getMessage()); + $this->templateVariables['as_entity'] = false; + } + + return; + } + + if ($this->shouldGenerateEntityXml($input)) { + $tableName = u($modelClassNameDetails->getShortName())->before('Model')->snake()->toString(); + $hasIdentity = $this->shouldGenerateIdentity($input); + if ($hasIdentity && !isset($this->templateVariables['type_name'])) { + throw new \LogicException( + 'Cannot generate entity XML mapping without identity type (which should have been generated).' + ); + } + + $this->templateVariables['as_entity'] = false; + + try { + $mappingsDirectory = '/src/Infrastructure/Doctrine/ORM/Mapping'; + $newYaml = $this->doctrineUpdater->updateORMDefaultEntityMapping( + $this->fileManager->getFileContents($configPath), + 'xml', + '%kernel.project_dir%'.$mappingsDirectory, + ); + $generator->dumpFile($configPath, $newYaml); + + $targetPath = sprintf( + '%s%s/%s.orm.xml', + $this->fileManager->getRootDirectory(), + $mappingsDirectory, + $modelName + ); + $generator->generateFile( + $targetPath, + __DIR__.'/../Resources/skeleton/doctrine/Mapping.tpl.xml.php', + [ + 'model_class' => $modelClassNameDetails->getFullName(), + 'has_identity' => $hasIdentity, + 'type_name' => $this->templateVariables['type_name'], + 'table_name' => $tableName, + 'identity_column_name' => $this->templateVariables['identity_type'], + ], + ); + } catch (YamlManipulationFailedException $e) { + $io->error($e->getMessage()); + } + } + + // Write out the changes. + $generator->writeChanges(); + } + + // Helper methods + + /** + * Returns whether the user wants to generate entity mappings as PHP attributes. + * + * @param InputInterface $input + * @return bool + */ + private function shouldGenerateEntityAttributes(InputInterface $input): bool + { + return 'attributes' === $input->getOption('entity'); + } + + /** + * Returns whether the user wants to generate entity mappings as XML. + * + * @param InputInterface $input + * @return bool + */ + private function shouldGenerateEntityXml(InputInterface $input): bool + { + return 'xml' === $input->getOption('entity'); + } + + /** + * Returns whether the user wants to generate entity mappings. + * + * @param InputInterface $input + * @return bool + */ + private function shouldGenerateEntity(InputInterface $input): bool + { + return ( + $this->shouldGenerateEntityAttributes($input) || + $this->shouldGenerateEntityXml($input) + ); + } + + /** + * Returns whether the user wants to generate an identity value object for the model. + * + * @param InputInterface $input + * @return bool + */ + private function shouldGenerateId(InputInterface $input): bool + { + return 'id' === $input->getOption('with-identity'); + } + + /** + * Returns whether the user wants to generate a UUID value object for the model. + * + * @param InputInterface $input + * @return bool + */ + private function shouldGenerateUuid(InputInterface $input): bool + { + return 'uuid' === $input->getOption('with-identity'); + } + + /** + * Returns whether the user wants to generate an identity value object for the model. + * + * @param InputInterface $input + * @return bool + */ + private function shouldGenerateIdentity(InputInterface $input): bool + { + return ( + $this->shouldGenerateId($input) || + $this->shouldGenerateUuid($input) + ); } } diff --git a/src/Resources/skeleton/doctrine/Mapping.tpl.xml.php b/src/Resources/skeleton/doctrine/Mapping.tpl.xml.php new file mode 100644 index 0000000..e1a1e6d --- /dev/null +++ b/src/Resources/skeleton/doctrine/Mapping.tpl.xml.php @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/src/Resources/skeleton/identity/Uuid.tpl.php b/src/Resources/skeleton/identity/Uuid.tpl.php deleted file mode 100644 index 11a8316..0000000 --- a/src/Resources/skeleton/identity/Uuid.tpl.php +++ /dev/null @@ -1,9 +0,0 @@ - - -namespace ; - - - -class extends -{ -} diff --git a/src/Resources/skeleton/model/DoctrineMappingType.tpl.php b/src/Resources/skeleton/model/DoctrineMappingType.tpl.php new file mode 100644 index 0000000..2e8dd0f --- /dev/null +++ b/src/Resources/skeleton/model/DoctrineMappingType.tpl.php @@ -0,0 +1,20 @@ + + +namespace ; + + + +class extends +{ + public const NAME = ''; + + public function getName(): string + { + return self::NAME; + } + + protected function getIdType(): string + { + return ::class; + } +} diff --git a/src/Resources/skeleton/identity/Id.tpl.php b/src/Resources/skeleton/model/Identity.tpl.php similarity index 53% rename from src/Resources/skeleton/identity/Id.tpl.php rename to src/Resources/skeleton/model/Identity.tpl.php index 11a8316..d416ee4 100644 --- a/src/Resources/skeleton/identity/Id.tpl.php +++ b/src/Resources/skeleton/model/Identity.tpl.php @@ -4,6 +4,6 @@ -class extends +class extends { } diff --git a/src/Resources/skeleton/model/Model.tpl.php b/src/Resources/skeleton/model/Model.tpl.php index 7d7a6b1..ce94ab3 100644 --- a/src/Resources/skeleton/model/Model.tpl.php +++ b/src/Resources/skeleton/model/Model.tpl.php @@ -4,33 +4,40 @@ -class extends AggregateRoot + +#[ORM\Entity] + +class extends AggregateRoot { - + /** - * @var + * @var */ - private $; + + #[ORM\Id] + #[ORM\Column(type: ::NAME)] + + private $; public function __construct() { - - $this->id = new Id(1); - - $this->uuid = Uuid::random(); + + $this->id = new (1); + + $this->uuid = ::random(); } - + /** - * Get + * Get * - * @return + * @return */ - public function get(): + public function get(): { - return $this->; + return $this->; } } From e0f72fad8bbb7c29ae5710856ac2821aa11a9fbd Mon Sep 17 00:00:00 2001 From: Pascal Cremer Date: Sat, 21 Jan 2023 16:23:25 +0100 Subject: [PATCH 03/44] fix: Use `attribute` instead of `annotation`. --- src/Maker/Doctrine/DoctrineConfigUpdater.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Maker/Doctrine/DoctrineConfigUpdater.php b/src/Maker/Doctrine/DoctrineConfigUpdater.php index f681935..a2404f4 100644 --- a/src/Maker/Doctrine/DoctrineConfigUpdater.php +++ b/src/Maker/Doctrine/DoctrineConfigUpdater.php @@ -42,7 +42,7 @@ public function addCustomDBALMappingType(string $yamlSource, string $identifier, */ public function updateORMDefaultEntityMapping(string $yamlSource, string $mappingType, string $directory): string { - Assert\Assertion::inArray($mappingType, ['xml', 'annotation'], 'Invalid mapping type: %s'); + Assert\Assertion::inArray($mappingType, ['xml', 'attribute'], 'Invalid mapping type: %s'); $data = $this->createYamlSourceManipulator($yamlSource); $data['doctrine']['orm']['mappings']['App']['type'] = $mappingType; From 94ffb490c59ccc4be8d88bb16534b0d076a3e42a Mon Sep 17 00:00:00 2001 From: Pascal Cremer Date: Sat, 21 Jan 2023 16:23:58 +0100 Subject: [PATCH 04/44] fix: Add missing import for identity type class. --- src/Maker/MakeModel.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Maker/MakeModel.php b/src/Maker/MakeModel.php index 515ce52..a7ff474 100644 --- a/src/Maker/MakeModel.php +++ b/src/Maker/MakeModel.php @@ -239,8 +239,7 @@ private function generateIdentity( InputInterface $input, ConsoleStyle $io, Generator $generator - ): void - { + ): void { if (!$this->shouldGenerateIdentity($input)) { return; } @@ -343,6 +342,7 @@ private function generateIdentity( ); $generator->dumpFile($configPath, $newYaml); + $this->classesToImport[] = $mappingTypeClassNameDetails->getFullName(); $this->templateVariables['type_class'] = $mappingTypeClassNameDetails->getShortName(); $this->templateVariables['type_name'] = $typeName; @@ -363,8 +363,7 @@ private function generateEntity( InputInterface $input, ConsoleStyle $io, Generator $generator - ): void - { + ): void { if (!$this->shouldGenerateEntity($input)) { return; } From cdb533a4725251929cf112f3daa8e5959506d69c Mon Sep 17 00:00:00 2001 From: Pascal Cremer Date: Sat, 21 Jan 2023 16:24:52 +0100 Subject: [PATCH 05/44] chore: Exclude `src/Resources/skeleton` from linters. --- .php-cs-fixer.php | 5 ++++- phpstan.neon | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 0f734fb..2d363d1 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -1,7 +1,10 @@ exclude(['var', 'vendor', 'migrations', 'fixtures']) + ->exclude([ + 'src/Resources/skeleton', + 'vendor' + ]) ->in(__DIR__) ; diff --git a/phpstan.neon b/phpstan.neon index db38942..d2370f1 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -8,4 +8,5 @@ parameters: - src - tests excludePaths: + - src/Resources/skeleton - vendor/ From 6b4437889fae1218ac36691b3fd3955860df84be Mon Sep 17 00:00:00 2001 From: janvt Date: Mon, 6 Mar 2023 17:33:49 +0100 Subject: [PATCH 06/44] fix hard-coded 'identity' attribute name --- src/Resources/skeleton/doctrine/Mapping.tpl.xml.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Resources/skeleton/doctrine/Mapping.tpl.xml.php b/src/Resources/skeleton/doctrine/Mapping.tpl.xml.php index e1a1e6d..b8fbf60 100644 --- a/src/Resources/skeleton/doctrine/Mapping.tpl.xml.php +++ b/src/Resources/skeleton/doctrine/Mapping.tpl.xml.php @@ -5,7 +5,7 @@ - + From 0060e39368144a886a3b946fa58f63454fd94df6 Mon Sep 17 00:00:00 2001 From: janvt Date: Mon, 6 Mar 2023 18:13:13 +0100 Subject: [PATCH 07/44] move Doctrine config check to interact step --- src/Maker/MakeModel.php | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/Maker/MakeModel.php b/src/Maker/MakeModel.php index a7ff474..cf133a1 100644 --- a/src/Maker/MakeModel.php +++ b/src/Maker/MakeModel.php @@ -10,6 +10,7 @@ use GeekCell\DddBundle\Infrastructure\Doctrine\Type\AbstractIdType; use GeekCell\DddBundle\Infrastructure\Doctrine\Type\AbstractUuidType; use GeekCell\DddBundle\Maker\Doctrine\DoctrineConfigUpdater; +use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException; use Symfony\Bundle\MakerBundle\InputConfiguration; use Symfony\Bundle\MakerBundle\DependencyBuilder; use Symfony\Bundle\MakerBundle\ConsoleStyle; @@ -21,6 +22,7 @@ use Symfony\Bundle\MakerBundle\Util\ClassNameDetails; use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator; use Symfony\Bundle\MakerBundle\Util\YamlManipulationFailedException; +use Symfony\Bundle\MakerBundle\Util\YamlSourceManipulator; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -28,6 +30,8 @@ use function Symfony\Component\String\u; +const DOCTRINE_CONFIG_PATH = 'config/packages/doctrine.yaml'; + final class MakeModel extends AbstractMaker implements InputAwareMakerInterface { /** @@ -124,6 +128,10 @@ public function configureDependencies(DependencyBuilder $dependencies, InputInte */ public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void { + if (!$this->fileManager->fileExists(DOCTRINE_CONFIG_PATH)) { + throw new RuntimeCommandException('The file "' . DOCTRINE_CONFIG_PATH . '" does not exist. This command requires that file to exist so that it can be updated.'); + } + /** @var string $modelName */ $modelName = $input->getArgument('name'); $useSuffix = $io->confirm( @@ -369,20 +377,15 @@ private function generateEntity( } $modelName = $modelClassNameDetails->getShortName(); - $configPath = 'config/packages/doctrine.yaml'; - if (!$this->fileManager->fileExists($configPath)) { - $io->error(sprintf('Doctrine configuration at path "%s" does not exist.', $configPath)); - return; - } if ($this->shouldGenerateEntityAttributes($input)) { try { $newYaml = $this->doctrineUpdater->updateORMDefaultEntityMapping( - $this->fileManager->getFileContents($configPath), + $this->fileManager->getFileContents(DOCTRINE_CONFIG_PATH), 'attribute', '%kernel.project_dir%/src/Domain/Model', ); - $generator->dumpFile($configPath, $newYaml); + $generator->dumpFile(DOCTRINE_CONFIG_PATH, $newYaml); $this->classesToImport[] = ['Doctrine\\ORM\\Mapping' => 'ORM']; $this->templateVariables['as_entity'] = true; } catch (YamlManipulationFailedException $e) { @@ -407,11 +410,11 @@ private function generateEntity( try { $mappingsDirectory = '/src/Infrastructure/Doctrine/ORM/Mapping'; $newYaml = $this->doctrineUpdater->updateORMDefaultEntityMapping( - $this->fileManager->getFileContents($configPath), + $this->fileManager->getFileContents(DOCTRINE_CONFIG_PATH), 'xml', '%kernel.project_dir%'.$mappingsDirectory, ); - $generator->dumpFile($configPath, $newYaml); + $generator->dumpFile(DOCTRINE_CONFIG_PATH, $newYaml); $targetPath = sprintf( '%s%s/%s.orm.xml', From 2bbafd2cb9acf76b85a90dbbc71f6dac904f2068 Mon Sep 17 00:00:00 2001 From: janvt Date: Tue, 7 Mar 2023 13:46:31 +0100 Subject: [PATCH 08/44] fix test class name --- tests/Unit/Infrastructure/Doctrine/PaginatorTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Unit/Infrastructure/Doctrine/PaginatorTest.php b/tests/Unit/Infrastructure/Doctrine/PaginatorTest.php index 1ebd63d..c10c266 100644 --- a/tests/Unit/Infrastructure/Doctrine/PaginatorTest.php +++ b/tests/Unit/Infrastructure/Doctrine/PaginatorTest.php @@ -9,7 +9,7 @@ use Mockery; use PHPUnit\Framework\TestCase; -class DoctrinePaginatorTest extends TestCase +class PaginatorTest extends TestCase { /** @var OrmPaginator|Mockery\MockInterface */ private mixed $ormPaginatorMock; @@ -21,7 +21,7 @@ public function setUp(): void $this->ormPaginatorMock = Mockery::mock(OrmPaginator::class); } - /** + /**g * @dataProvider provideCurrentPageData * * @param int $first From 4505860246e7761618bda419206e2543663401e1 Mon Sep 17 00:00:00 2001 From: janvt Date: Tue, 7 Mar 2023 13:52:47 +0100 Subject: [PATCH 09/44] add unit tests to github action --- .github/workflows/test.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..58c6daf --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,15 @@ +name: Tests + +on: [push] + +jobs: + unit-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: php-actions/composer@v6 + - uses: php-actions/phpunit@v3 + - name: PHPUnit Tests + uses: php-actions/phpunit@9.6 + with: + bootstrap: vendor/autoload.php From acfd03058a0558cc958537edf7c229ae927f4693 Mon Sep 17 00:00:00 2001 From: janvt Date: Tue, 7 Mar 2023 13:54:05 +0100 Subject: [PATCH 10/44] fix phpunit version --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 58c6daf..cefd8db 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,6 +10,6 @@ jobs: - uses: php-actions/composer@v6 - uses: php-actions/phpunit@v3 - name: PHPUnit Tests - uses: php-actions/phpunit@9.6 + uses: php-actions/phpunit@9 with: bootstrap: vendor/autoload.php From e059bd11b84b7116a850664654907a57796f762c Mon Sep 17 00:00:00 2001 From: janvt Date: Tue, 7 Mar 2023 13:55:05 +0100 Subject: [PATCH 11/44] fix phpunit version, again --- .github/workflows/test.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cefd8db..709c92e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,8 +8,7 @@ jobs: steps: - uses: actions/checkout@v3 - uses: php-actions/composer@v6 - - uses: php-actions/phpunit@v3 - name: PHPUnit Tests - uses: php-actions/phpunit@9 + uses: php-actions/phpunit@v9 with: bootstrap: vendor/autoload.php From fc9607d0a8ce2c2fd98d8f2208527492eaf321cd Mon Sep 17 00:00:00 2001 From: janvt Date: Tue, 7 Mar 2023 14:02:06 +0100 Subject: [PATCH 12/44] fix phpunit config --- .github/workflows/test.yml | 3 ++- tests/phpunit.xml | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 tests/phpunit.xml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 709c92e..7c66059 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,4 +11,5 @@ jobs: - name: PHPUnit Tests uses: php-actions/phpunit@v9 with: - bootstrap: vendor/autoload.php + configuration: test/phpunit.xml + version: 9.6 diff --git a/tests/phpunit.xml b/tests/phpunit.xml new file mode 100644 index 0000000..d121ef7 --- /dev/null +++ b/tests/phpunit.xml @@ -0,0 +1,9 @@ + + + + + . + + + + From 25fca64798b1e7374bdb438d2c49b2ea3029997c Mon Sep 17 00:00:00 2001 From: janvt Date: Tue, 7 Mar 2023 14:04:12 +0100 Subject: [PATCH 13/44] remove invalid version parameter --- .github/workflows/test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7c66059..d84ace7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,4 +12,3 @@ jobs: uses: php-actions/phpunit@v9 with: configuration: test/phpunit.xml - version: 9.6 From cf8cb722b3fbcf34c621c5a75648d4fa4b4f80fb Mon Sep 17 00:00:00 2001 From: janvt Date: Tue, 7 Mar 2023 14:05:38 +0100 Subject: [PATCH 14/44] fix path to phpunit config --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d84ace7..fb115d5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,4 +11,4 @@ jobs: - name: PHPUnit Tests uses: php-actions/phpunit@v9 with: - configuration: test/phpunit.xml + configuration: tests/phpunit.xml From f4898c3815a399f727b778862af56cf023e916a6 Mon Sep 17 00:00:00 2001 From: janvt Date: Tue, 7 Mar 2023 14:13:24 +0100 Subject: [PATCH 15/44] add php version --- .github/workflows/test.yml | 1 + tests/.phpunit.result.cache | 1 + 2 files changed, 2 insertions(+) create mode 100644 tests/.phpunit.result.cache diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fb115d5..6dceaaf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,4 +11,5 @@ jobs: - name: PHPUnit Tests uses: php-actions/phpunit@v9 with: + php_version: 8.1 configuration: tests/phpunit.xml diff --git a/tests/.phpunit.result.cache b/tests/.phpunit.result.cache new file mode 100644 index 0000000..a4e9905 --- /dev/null +++ b/tests/.phpunit.result.cache @@ -0,0 +1 @@ +{"version":1,"defects":{"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Doctrine\\PaginatorTest::testGetCurrentPage":4},"times":{"GeekCell\\DddBundle\\Tests\\Unit\\Domain\\AggregateRootTest::testRecordAndCommit":0.009,"GeekCell\\DddBundle\\Tests\\Unit\\Domain\\AggregateRootTest::testRecordWithoutCommit":0,"GeekCell\\DddBundle\\Tests\\Unit\\Domain\\AggregateRootTest::testDispatch":0,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Doctrine\\PaginatorTest::testGetCurrentPage":0.005,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Doctrine\\PaginatorTest::testGetTotalPages with data set #0":0.002,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Doctrine\\PaginatorTest::testGetTotalPages with data set #1":0,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Doctrine\\PaginatorTest::testGetTotalPages with data set #2":0,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Doctrine\\PaginatorTest::testGetTotalPages with data set #3":0,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Doctrine\\PaginatorTest::testGetTotalPages with data set #4":0,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Doctrine\\PaginatorTest::testGetTotalPages with data set #5":0,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Doctrine\\PaginatorTest::testGetTotalPages with data set #6":0,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Doctrine\\PaginatorTest::testGetItemsPerPage":0,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Doctrine\\PaginatorTest::testGetTotalItems":0,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Doctrine\\PaginatorTest::testGetIterator":0,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Doctrine\\Type\\AbstractIdTypeTest::testConvertToDatabaseValue":0.005,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Doctrine\\Type\\AbstractIdTypeTest::testConvertToDatabaseValueScalar":0,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Doctrine\\Type\\AbstractIdTypeTest::testConvertToDatabaseValueInvalidType":0.001,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Doctrine\\Type\\AbstractIdTypeTest::testConvertToPhpValue":0,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Doctrine\\Type\\AbstractUuidTypeTest::testConvertToDatabaseValue":0,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Doctrine\\Type\\AbstractUuidTypeTest::testConvertToDatabaseValueScalar":0,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Doctrine\\Type\\AbstractUuidTypeTest::testConvertToDatabaseValueInvalidType":0,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Doctrine\\Type\\AbstractUuidTypeTest::testConvertToPhpValue":0,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Messenger\\CommandBusTest::testDispatch":0.003,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Messenger\\QueryBusTest::testDispatch":0,"GeekCell\\DddBundle\\Tests\\Unit\\Support\\Facades\\FacadeTest::testGetFacadeRoot":0.004,"GeekCell\\DddBundle\\Tests\\Unit\\Support\\Facades\\FacadeTest::testGetFacadeRootWithCachedInstance":0,"GeekCell\\DddBundle\\Tests\\Unit\\Support\\Facades\\FacadeTest::testGetFacadeRootWithoutKernel":0,"GeekCell\\DddBundle\\Tests\\Unit\\Support\\Facades\\FacadeTest::testGetFacadeRootWithoutMatchingAccessor":0.001,"GeekCell\\DddBundle\\Tests\\Unit\\Support\\Facades\\FacadeTest::testCallStatic":0,"GeekCell\\DddBundle\\Tests\\Unit\\Support\\Facades\\FacadeTest::testCallStaticWithoutKernel":0,"GeekCell\\DddBundle\\Tests\\Unit\\Support\\Facades\\FacadeTest::testCallStaticWithUnknownMethod":0,"GeekCell\\DddBundle\\Tests\\Unit\\Support\\Facades\\FacadeTest::testSwap":0,"GeekCell\\DddBundle\\Tests\\Unit\\Support\\Facades\\FacadeTest::testCreateMock":0,"GeekCell\\DddBundle\\Tests\\Unit\\Support\\Facades\\FacadeTest::testCreateFreshMock":0,"Tests\\Unit\\Support\\Traits\\DispatchableTraitTest::testGetDefaultEventDispatcher":0}} \ No newline at end of file From 9f60b5c7e395f29dfe9ca6f89eb678316f18b93d Mon Sep 17 00:00:00 2001 From: janvt Date: Tue, 7 Mar 2023 14:20:19 +0100 Subject: [PATCH 16/44] why is there a 'g' there? --- tests/Unit/Infrastructure/Doctrine/PaginatorTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Unit/Infrastructure/Doctrine/PaginatorTest.php b/tests/Unit/Infrastructure/Doctrine/PaginatorTest.php index c10c266..d77cccf 100644 --- a/tests/Unit/Infrastructure/Doctrine/PaginatorTest.php +++ b/tests/Unit/Infrastructure/Doctrine/PaginatorTest.php @@ -21,7 +21,7 @@ public function setUp(): void $this->ormPaginatorMock = Mockery::mock(OrmPaginator::class); } - /**g + /** * @dataProvider provideCurrentPageData * * @param int $first From 856cb00b4b4a4518167997525100d6d7badb9e4a Mon Sep 17 00:00:00 2001 From: janvt Date: Tue, 7 Mar 2023 14:23:11 +0100 Subject: [PATCH 17/44] remove php_version --- .github/workflows/test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6dceaaf..fb115d5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,5 +11,4 @@ jobs: - name: PHPUnit Tests uses: php-actions/phpunit@v9 with: - php_version: 8.1 configuration: tests/phpunit.xml From 7fec342f0e48ceb772866671157a0d89c4ce9e7a Mon Sep 17 00:00:00 2001 From: janvt Date: Wed, 8 Mar 2023 17:51:10 +0100 Subject: [PATCH 18/44] add integration test --- composer.json | 2 +- config/services.yaml | 4 ++ src/Maker/MakeModel.php | 2 +- tests/.phpunit.result.cache | 1 - tests/Integration/GeekCellMakerTestKernel.php | 17 ++++++++ tests/Integration/Maker/MakeModelTest.php | 42 +++++++++++++++++++ tests/phpunit.xml | 8 +++- 7 files changed, 72 insertions(+), 4 deletions(-) delete mode 100644 tests/.phpunit.result.cache create mode 100644 tests/Integration/GeekCellMakerTestKernel.php create mode 100644 tests/Integration/Maker/MakeModelTest.php diff --git a/composer.json b/composer.json index 227f054..e1c6c85 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ }, "autoload": { "psr-4": { - "GeekCell\\DddBundle\\": "src" + "GeekCell\\DddBundle\\": "src/" } }, "autoload-dev": { diff --git a/config/services.yaml b/config/services.yaml index 6b3b0e9..ea0884e 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -25,6 +25,10 @@ services: tags: - { name: maker.command } + maker.maker.make_model: + alias: GeekCell\DddBundle\Maker\MakeModel + public: true + GeekCell\DddBundle\Maker\Doctrine\DoctrineConfigUpdater: class: GeekCell\DddBundle\Maker\Doctrine\DoctrineConfigUpdater public: false diff --git a/src/Maker/MakeModel.php b/src/Maker/MakeModel.php index cf133a1..87d4c43 100644 --- a/src/Maker/MakeModel.php +++ b/src/Maker/MakeModel.php @@ -81,7 +81,7 @@ public function configureCommand(Command $command, InputConfiguration $inputConf $command ->addArgument( 'name', - InputArgument::OPTIONAL, + InputArgument::REQUIRED, 'The name of the model class (e.g. Customer)', ) ->addOption( diff --git a/tests/.phpunit.result.cache b/tests/.phpunit.result.cache deleted file mode 100644 index a4e9905..0000000 --- a/tests/.phpunit.result.cache +++ /dev/null @@ -1 +0,0 @@ -{"version":1,"defects":{"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Doctrine\\PaginatorTest::testGetCurrentPage":4},"times":{"GeekCell\\DddBundle\\Tests\\Unit\\Domain\\AggregateRootTest::testRecordAndCommit":0.009,"GeekCell\\DddBundle\\Tests\\Unit\\Domain\\AggregateRootTest::testRecordWithoutCommit":0,"GeekCell\\DddBundle\\Tests\\Unit\\Domain\\AggregateRootTest::testDispatch":0,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Doctrine\\PaginatorTest::testGetCurrentPage":0.005,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Doctrine\\PaginatorTest::testGetTotalPages with data set #0":0.002,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Doctrine\\PaginatorTest::testGetTotalPages with data set #1":0,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Doctrine\\PaginatorTest::testGetTotalPages with data set #2":0,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Doctrine\\PaginatorTest::testGetTotalPages with data set #3":0,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Doctrine\\PaginatorTest::testGetTotalPages with data set #4":0,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Doctrine\\PaginatorTest::testGetTotalPages with data set #5":0,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Doctrine\\PaginatorTest::testGetTotalPages with data set #6":0,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Doctrine\\PaginatorTest::testGetItemsPerPage":0,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Doctrine\\PaginatorTest::testGetTotalItems":0,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Doctrine\\PaginatorTest::testGetIterator":0,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Doctrine\\Type\\AbstractIdTypeTest::testConvertToDatabaseValue":0.005,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Doctrine\\Type\\AbstractIdTypeTest::testConvertToDatabaseValueScalar":0,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Doctrine\\Type\\AbstractIdTypeTest::testConvertToDatabaseValueInvalidType":0.001,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Doctrine\\Type\\AbstractIdTypeTest::testConvertToPhpValue":0,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Doctrine\\Type\\AbstractUuidTypeTest::testConvertToDatabaseValue":0,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Doctrine\\Type\\AbstractUuidTypeTest::testConvertToDatabaseValueScalar":0,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Doctrine\\Type\\AbstractUuidTypeTest::testConvertToDatabaseValueInvalidType":0,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Doctrine\\Type\\AbstractUuidTypeTest::testConvertToPhpValue":0,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Messenger\\CommandBusTest::testDispatch":0.003,"GeekCell\\DddBundle\\Tests\\Unit\\Infrastructure\\Messenger\\QueryBusTest::testDispatch":0,"GeekCell\\DddBundle\\Tests\\Unit\\Support\\Facades\\FacadeTest::testGetFacadeRoot":0.004,"GeekCell\\DddBundle\\Tests\\Unit\\Support\\Facades\\FacadeTest::testGetFacadeRootWithCachedInstance":0,"GeekCell\\DddBundle\\Tests\\Unit\\Support\\Facades\\FacadeTest::testGetFacadeRootWithoutKernel":0,"GeekCell\\DddBundle\\Tests\\Unit\\Support\\Facades\\FacadeTest::testGetFacadeRootWithoutMatchingAccessor":0.001,"GeekCell\\DddBundle\\Tests\\Unit\\Support\\Facades\\FacadeTest::testCallStatic":0,"GeekCell\\DddBundle\\Tests\\Unit\\Support\\Facades\\FacadeTest::testCallStaticWithoutKernel":0,"GeekCell\\DddBundle\\Tests\\Unit\\Support\\Facades\\FacadeTest::testCallStaticWithUnknownMethod":0,"GeekCell\\DddBundle\\Tests\\Unit\\Support\\Facades\\FacadeTest::testSwap":0,"GeekCell\\DddBundle\\Tests\\Unit\\Support\\Facades\\FacadeTest::testCreateMock":0,"GeekCell\\DddBundle\\Tests\\Unit\\Support\\Facades\\FacadeTest::testCreateFreshMock":0,"Tests\\Unit\\Support\\Traits\\DispatchableTraitTest::testGetDefaultEventDispatcher":0}} \ No newline at end of file diff --git a/tests/Integration/GeekCellMakerTestKernel.php b/tests/Integration/GeekCellMakerTestKernel.php new file mode 100644 index 0000000..cb43772 --- /dev/null +++ b/tests/Integration/GeekCellMakerTestKernel.php @@ -0,0 +1,17 @@ + [$this->createMakerTest() + ->run(function (MakerTestRunner $runner) { + $runner->runMaker( + [ + 'Horse', // entity name + true, // aggregate root + true, // use as Doctrine entity + true, // create identity value object + false, // add "Model" suffix + ] + ); + + $runner->runTests(); + }), + ]; + } +} diff --git a/tests/phpunit.xml b/tests/phpunit.xml index d121ef7..f200e04 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -1,5 +1,11 @@ - + . From 108535e9a05f56465ef0fe931477e38884086e1c Mon Sep 17 00:00:00 2001 From: janvt Date: Thu, 9 Mar 2023 12:57:28 +0100 Subject: [PATCH 19/44] generate Repository along with entity --- src/Maker/MakeModel.php | 120 +++++++++++++----- .../skeleton/model/Repository.tpl.php | 64 ++++++++++ 2 files changed, 153 insertions(+), 31 deletions(-) create mode 100644 src/Resources/skeleton/model/Repository.tpl.php diff --git a/src/Maker/MakeModel.php b/src/Maker/MakeModel.php index 87d4c43..d685fab 100644 --- a/src/Maker/MakeModel.php +++ b/src/Maker/MakeModel.php @@ -4,6 +4,8 @@ namespace GeekCell\DddBundle\Maker; +use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\Persistence\ManagerRegistry; use GeekCell\Ddd\Domain\ValueObject\Id; use GeekCell\Ddd\Domain\ValueObject\Uuid; use GeekCell\DddBundle\Domain\AggregateRoot; @@ -22,7 +24,6 @@ use Symfony\Bundle\MakerBundle\Util\ClassNameDetails; use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator; use Symfony\Bundle\MakerBundle\Util\YamlManipulationFailedException; -use Symfony\Bundle\MakerBundle\Util\YamlSourceManipulator; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -54,8 +55,7 @@ final class MakeModel extends AbstractMaker implements InputAwareMakerInterface public function __construct( private DoctrineConfigUpdater $doctrineUpdater, private FileManager $fileManager, - ) { - } + ) {} /** * @inheritDoc @@ -87,7 +87,7 @@ public function configureCommand(Command $command, InputConfiguration $inputConf ->addOption( 'aggregate-root', null, - InputOption::VALUE_NONE, + InputOption::VALUE_REQUIRED, 'Marks the model as aggregate root', ) ->addOption( @@ -105,7 +105,7 @@ public function configureCommand(Command $command, InputConfiguration $inputConf ->addOption( 'with-suffix', null, - InputOption::VALUE_NONE, + InputOption::VALUE_REQUIRED, 'Adds the suffix "Model" to the model class name', ) ; @@ -134,14 +134,17 @@ public function interact(InputInterface $input, ConsoleStyle $io, Command $comma /** @var string $modelName */ $modelName = $input->getArgument('name'); - $useSuffix = $io->confirm( - sprintf( - 'Do you want to suffix the model class name? (%sModel)', - $modelName, - ), - false, - ); - $input->setOption('with-suffix', $useSuffix); + + if (false === $input->getOption('with-suffix')) { + $useSuffix = $io->confirm( + sprintf( + 'Do you want to suffix the model class name? (%sModel)', + $modelName, + ), + false, + ); + $input->setOption('with-suffix', $useSuffix); + } if (false === $input->getOption('aggregate-root')) { $asAggregateRoot = $io->confirm( @@ -212,24 +215,9 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen $this->templateVariables['class_name'] = $modelClassNameDetails->getShortName(); $this->generateIdentity($modelName, $input, $io, $generator); + $this->generateEntityMappings($modelClassNameDetails, $input, $io, $generator); $this->generateEntity($modelClassNameDetails, $input, $io, $generator); - - if ($input->getOption('aggregate-root')) { - $this->classesToImport[] = AggregateRoot::class; - $this->templateVariables['extends_aggregate_root'] = true; - } - - // @phpstan-ignore-next-line - $this->templateVariables['use_statements'] = new UseStatementGenerator($this->classesToImport); - - $templatePath = __DIR__.'/../Resources/skeleton/model/Model.tpl.php'; - $generator->generateClass( - $modelClassNameDetails->getFullName(), - $templatePath, - $this->templateVariables, - ); - - $generator->writeChanges(); + $this->generateRepository($modelClassNameDetails, $input, $io, $generator); $this->writeSuccessMessage($io); } @@ -366,7 +354,7 @@ private function generateIdentity( * @param ConsoleStyle $io * @param Generator $generator */ - private function generateEntity( + private function generateEntityMappings( ClassNameDetails $modelClassNameDetails, InputInterface $input, ConsoleStyle $io, @@ -442,6 +430,76 @@ private function generateEntity( $generator->writeChanges(); } + /** + * Generate model entity + * + * @param ClassNameDetails $modelClassNameDetails + * @param InputInterface $input + * @param ConsoleStyle $io + * @param Generator $generator + */ + private function generateEntity( + ClassNameDetails $modelClassNameDetails, + InputInterface $input, + ConsoleStyle $io, + Generator $generator + ): void { + if ($input->getOption('aggregate-root')) { + $this->classesToImport[] = AggregateRoot::class; + $this->templateVariables['extends_aggregate_root'] = true; + } + + // @phpstan-ignore-next-line + $this->templateVariables['use_statements'] = new UseStatementGenerator($this->classesToImport); + + $templatePath = __DIR__.'/../Resources/skeleton/model/Model.tpl.php'; + $generator->generateClass( + $modelClassNameDetails->getFullName(), + $templatePath, + $this->templateVariables, + ); + + $generator->writeChanges(); + } + + /** + * Generate model repository + * + * @param InputInterface $input + * @param ConsoleStyle $io + * @param Generator $generator + */ + private function generateRepository( + ClassNameDetails $modelClassNameDetails, + InputInterface $input, + ConsoleStyle $io, + Generator $generator + ): void { + $classNameDetails = $generator->createClassNameDetails( + $input->getArgument('name'), + 'Repository\\', + 'Repository', + ); + + $templateVars = [ + 'use_statements' => new UseStatementGenerator([ + $modelClassNameDetails->getFullName(), + ServiceEntityRepository::class, + ManagerRegistry::class + ]), + 'entity_class_name' => $modelClassNameDetails->getShortName() + ]; + + $templatePath = __DIR__.'/../Resources/skeleton/model/Repository.tpl.php'; + $generator->generateClass( + $classNameDetails->getFullName(), + $templatePath, + $templateVars, + ); + + $generator->writeChanges(); + } + // Helper methods /** diff --git a/src/Resources/skeleton/model/Repository.tpl.php b/src/Resources/skeleton/model/Repository.tpl.php new file mode 100644 index 0000000..a8c8337 --- /dev/null +++ b/src/Resources/skeleton/model/Repository.tpl.php @@ -0,0 +1,64 @@ + + +namespace ; + + + +class extends ServiceEntityRepository +{ + /** + * @extends ServiceEntityRepository<> + * + * @method |null find($id, $lockMode = null, $lockVersion = null) + * @method |null findOneBy(array $criteria, array $orderBy = null) + * @method [] findAll() + * @method [] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, ::class); + } + + public function save( $entity, bool $flush = false): void + { + $this->getEntityManager()->persist($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function remove( $entity, bool $flush = false): void + { + $this->getEntityManager()->remove($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + +// /** +// * @return [] Returns an array of objects +// */ +// public function findByExampleField($value): array +// { +// return $this->createQueryBuilder('t') +// ->andWhere('t.exampleField = :val') +// ->setParameter('val', $value) +// ->orderBy('t.id', 'ASC') +// ->setMaxResults(10) +// ->getQuery() +// ->getResult() +// ; +// } + +// public function findOneBySomeField($value): ? +// { +// return $this->createQueryBuilder('t') +// ->andWhere('t.exampleField = :val') +// ->setParameter('val', $value) +// ->getQuery() +// ->getOneOrNullResult() +// ; +// } +} From fc83b60f08d1d34ba14cafcb169192b96fae4062 Mon Sep 17 00:00:00 2001 From: janvt Date: Thu, 9 Mar 2023 13:03:22 +0100 Subject: [PATCH 20/44] remove non-functioning integration test --- tests/Integration/Maker/MakeModelTest.php | 42 ----------------------- 1 file changed, 42 deletions(-) delete mode 100644 tests/Integration/Maker/MakeModelTest.php diff --git a/tests/Integration/Maker/MakeModelTest.php b/tests/Integration/Maker/MakeModelTest.php deleted file mode 100644 index 0651b16..0000000 --- a/tests/Integration/Maker/MakeModelTest.php +++ /dev/null @@ -1,42 +0,0 @@ - [$this->createMakerTest() - ->run(function (MakerTestRunner $runner) { - $runner->runMaker( - [ - 'Horse', // entity name - true, // aggregate root - true, // use as Doctrine entity - true, // create identity value object - false, // add "Model" suffix - ] - ); - - $runner->runTests(); - }), - ]; - } -} From d743d66480007990057efce0f0002ab48575c0d4 Mon Sep 17 00:00:00 2001 From: janvt Date: Thu, 9 Mar 2023 13:11:08 +0100 Subject: [PATCH 21/44] add docs --- README.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/README.md b/README.md index e9129bf..150d62f 100644 --- a/README.md +++ b/README.md @@ -153,3 +153,36 @@ class Logger extends Facade ``` Although facades are better testable than regular singletons, it is highly recommended to only use them sparringly and always prefer normal dependency injection when possible. + +## Generator Commands + +This bundle adds several [maker bundle](https://symfony.com/bundles/SymfonyMakerBundle/current/index.html) commands to generate commonly used components. + +### Model / Repository + +This command can be used to generate: + +- The domain model class. +- A repository class for the model. +- The model's identity class as value object (optional). +- A Doctrine database entity configuration, either as annotation or separate config file (optional). +- A custom Doctrine type for the model's identity class (optional). + +#### Command Output + +```bash +Description: + Creates a new domain model class + +Usage: + make:ddd:model [options] [--] [] + +Arguments: + name The name of the model class (e.g. Customer) + +Options: + --aggregate-root Marks the model as aggregate root + --entity=ENTITY Use this model as Doctrine entity + --with-identity=WITH-IDENTITY Whether an identity value object should be created + --with-suffix Adds the suffix "Model" to the model class name +``` From ddceaeafb761bde2b5068ca8110eab949944fbe0 Mon Sep 17 00:00:00 2001 From: janvt Date: Thu, 9 Mar 2023 16:48:43 +0100 Subject: [PATCH 22/44] query maker command --- config/services.yaml | 7 +- src/Maker/MakeQuery.php | 134 ++++++++++++++++++ src/Resources/skeleton/query/Query.tpl.php | 9 ++ .../skeleton/query/QueryHandler.tpl.php | 20 +++ 4 files changed, 167 insertions(+), 3 deletions(-) create mode 100644 src/Maker/MakeQuery.php create mode 100644 src/Resources/skeleton/query/Query.tpl.php create mode 100644 src/Resources/skeleton/query/QueryHandler.tpl.php diff --git a/config/services.yaml b/config/services.yaml index ea0884e..9873c17 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -25,9 +25,10 @@ services: tags: - { name: maker.command } - maker.maker.make_model: - alias: GeekCell\DddBundle\Maker\MakeModel - public: true + GeekCell\DddBundle\Maker\MakeQuery: + class: GeekCell\DddBundle\Maker\MakeQuery + tags: + - { name: maker.command } GeekCell\DddBundle\Maker\Doctrine\DoctrineConfigUpdater: class: GeekCell\DddBundle\Maker\Doctrine\DoctrineConfigUpdater diff --git a/src/Maker/MakeQuery.php b/src/Maker/MakeQuery.php new file mode 100644 index 0000000..ddb61c3 --- /dev/null +++ b/src/Maker/MakeQuery.php @@ -0,0 +1,134 @@ +addArgument( + 'name', + InputArgument::REQUIRED, + 'The name of the query class (e.g. Customer)', + ) + ; + } + + /** + * @inheritDoc + */ + public function configureDependencies(DependencyBuilder $dependencies, InputInterface $input = null): void + { + } + + /** + * @inheritDoc + */ + public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void + { + } + + /** + * @inheritDoc + */ + public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void + { + $queryClassNameDetails = $generator->createClassNameDetails( + $input->getArgument('name'), + NAMESPACE_PREFIX, + 'Query', + ); + + $this->generateQuery($queryClassNameDetails, $generator); + $this->generateQueryHandler($queryClassNameDetails, $generator); + + $this->writeSuccessMessage($io); + } + + private function generateQuery(ClassNameDetails $queryClassNameDetails, Generator $generator) + { + $templateVars = [ + 'use_statements' => new UseStatementGenerator([ + Query::class, + ]), + ]; + + $templatePath = __DIR__.'/../Resources/skeleton/query/Query.tpl.php'; + $generator->generateClass( + $queryClassNameDetails->getFullName(), + $templatePath, + $templateVars, + ); + + $generator->writeChanges(); + } + + private function generateQueryHandler(ClassNameDetails $queryClassNameDetails, Generator $generator) + { + $classNameDetails = $generator->createClassNameDetails( + $queryClassNameDetails->getShortName(), + NAMESPACE_PREFIX, + 'Handler', + ); + + $templateVars = [ + 'use_statements' => new UseStatementGenerator([ + QueryHandler::class, + Collection::class, + AsMessageHandler::class + ]), + 'query_class_name' => $queryClassNameDetails->getShortName() + ]; + + $templatePath = __DIR__.'/../Resources/skeleton/query/QueryHandler.tpl.php'; + $generator->generateClass( + $classNameDetails->getFullName(), + $templatePath, + $templateVars, + ); + + $generator->writeChanges(); + } +} diff --git a/src/Resources/skeleton/query/Query.tpl.php b/src/Resources/skeleton/query/Query.tpl.php new file mode 100644 index 0000000..35c802e --- /dev/null +++ b/src/Resources/skeleton/query/Query.tpl.php @@ -0,0 +1,9 @@ + + +namespace ; + + + +class implements Query +{ +} diff --git a/src/Resources/skeleton/query/QueryHandler.tpl.php b/src/Resources/skeleton/query/QueryHandler.tpl.php new file mode 100644 index 0000000..d36afe2 --- /dev/null +++ b/src/Resources/skeleton/query/QueryHandler.tpl.php @@ -0,0 +1,20 @@ + + +namespace ; + + + +#[AsMessageHandler] +class implements QueryHandler +{ + /** + */ + public function __construct() + { + } + + public function __invoke( $query): Collection + { + return new Collection([]); + } +} From 3c3fb35c9dc92fc0b99c1efd005c13980895f38e Mon Sep 17 00:00:00 2001 From: janvt Date: Fri, 10 Mar 2023 11:40:29 +0100 Subject: [PATCH 23/44] change repository example --- src/Maker/MakeModel.php | 4 +++- .../skeleton/model/Repository.tpl.php | 21 +++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/Maker/MakeModel.php b/src/Maker/MakeModel.php index d685fab..3682056 100644 --- a/src/Maker/MakeModel.php +++ b/src/Maker/MakeModel.php @@ -5,6 +5,7 @@ namespace GeekCell\DddBundle\Maker; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ManagerRegistry; use GeekCell\Ddd\Domain\ValueObject\Id; use GeekCell\Ddd\Domain\ValueObject\Uuid; @@ -485,7 +486,8 @@ private function generateRepository( 'use_statements' => new UseStatementGenerator([ $modelClassNameDetails->getFullName(), ServiceEntityRepository::class, - ManagerRegistry::class + ManagerRegistry::class, + QueryBuilder::class ]), 'entity_class_name' => $modelClassNameDetails->getShortName() ]; diff --git a/src/Resources/skeleton/model/Repository.tpl.php b/src/Resources/skeleton/model/Repository.tpl.php index a8c8337..4a3c9d7 100644 --- a/src/Resources/skeleton/model/Repository.tpl.php +++ b/src/Resources/skeleton/model/Repository.tpl.php @@ -38,21 +38,20 @@ public function remove( $entity, bool $flush = false): } // /** -// * @return [] Returns an array of objects -// */ +// * @return [] Returns an array of objects +// */ // public function findByExampleField($value): array // { -// return $this->createQueryBuilder('t') -// ->andWhere('t.exampleField = :val') -// ->setParameter('val', $value) -// ->orderBy('t.id', 'ASC') -// ->setMaxResults(10) -// ->getQuery() -// ->getResult() -// ; +// return $this->filter(function(QueryBuilder $queryBuilder) use ($value) { +// $queryBuilder +// ->andWhere('t.exampleField = :val') +// ->setParamter('val', $value) +// ->orderBy('t.id', 'ASC') +// ; +// }); // } -// public function findOneBySomeField($value): ? +// public function findOneBySomeField($value): ? // { // return $this->createQueryBuilder('t') // ->andWhere('t.exampleField = :val') From 4bf8e6e4070ccc7883f52dfa3a1e0abfa34664b0 Mon Sep 17 00:00:00 2001 From: janvt Date: Fri, 10 Mar 2023 11:52:32 +0100 Subject: [PATCH 24/44] minor template improvements --- src/Maker/MakeQuery.php | 16 +++++++++++++--- .../skeleton/query/QueryHandler.tpl.php | 7 ------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/Maker/MakeQuery.php b/src/Maker/MakeQuery.php index ddb61c3..1e41f30 100644 --- a/src/Maker/MakeQuery.php +++ b/src/Maker/MakeQuery.php @@ -39,7 +39,7 @@ public static function getCommandName(): string */ public static function getCommandDescription(): string { - return 'Creates a new query class'; + return 'Creates a new query class and handler'; } /** @@ -87,7 +87,12 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen $this->writeSuccessMessage($io); } - private function generateQuery(ClassNameDetails $queryClassNameDetails, Generator $generator) + /** + * @param ClassNameDetails $queryClassNameDetails + * @param Generator $generator + * @return void + */ + private function generateQuery(ClassNameDetails $queryClassNameDetails, Generator $generator): void { $templateVars = [ 'use_statements' => new UseStatementGenerator([ @@ -105,7 +110,12 @@ private function generateQuery(ClassNameDetails $queryClassNameDetails, Generato $generator->writeChanges(); } - private function generateQueryHandler(ClassNameDetails $queryClassNameDetails, Generator $generator) + /** + * @param ClassNameDetails $queryClassNameDetails + * @param Generator $generator + * @return void + */ + private function generateQueryHandler(ClassNameDetails $queryClassNameDetails, Generator $generator): void { $classNameDetails = $generator->createClassNameDetails( $queryClassNameDetails->getShortName(), diff --git a/src/Resources/skeleton/query/QueryHandler.tpl.php b/src/Resources/skeleton/query/QueryHandler.tpl.php index d36afe2..b049bdb 100644 --- a/src/Resources/skeleton/query/QueryHandler.tpl.php +++ b/src/Resources/skeleton/query/QueryHandler.tpl.php @@ -7,14 +7,7 @@ #[AsMessageHandler] class implements QueryHandler { - /** - */ - public function __construct() - { - } - public function __invoke( $query): Collection { - return new Collection([]); } } From 425a9c14dc6d811b48782253c5a41d2faaac491e Mon Sep 17 00:00:00 2001 From: janvt Date: Fri, 10 Mar 2023 11:56:23 +0100 Subject: [PATCH 25/44] add docs --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index 150d62f..9126091 100644 --- a/README.md +++ b/README.md @@ -186,3 +186,25 @@ Options: --with-identity=WITH-IDENTITY Whether an identity value object should be created --with-suffix Adds the suffix "Model" to the model class name ``` + +### Query / Command + +These commands can be used to generate: + +- A query and query handler class. +- A command and command handler class. + +The query / command generated is just an empty class. The handler class is registered as a message handler for the configured [Symfony Messenger](https://symfony.com/doc/current/messenger.html). + +#### Command Output + +```bash +Description: + Creates a new query|command class and handler + +Usage: + make:ddd:query|command [] + +Arguments: + name The name of the query|command class (e.g. Customer) +``` From 5d7813036d9953bdbea7ff6ba225b75e05d1cbaf Mon Sep 17 00:00:00 2001 From: janvt Date: Fri, 10 Mar 2023 12:26:23 +0100 Subject: [PATCH 26/44] add maker command for command entity / handler --- config/services.yaml | 5 + src/Maker/AbstractBaseMakeQueryCommand.php | 148 ++++++++++++++++++ src/Maker/MakeCommand.php | 61 ++++++++ src/Maker/MakeQuery.php | 113 ++----------- .../skeleton/command/Command.tpl.php | 9 ++ .../skeleton/command/CommandHandler.tpl.php | 13 ++ 6 files changed, 251 insertions(+), 98 deletions(-) create mode 100644 src/Maker/AbstractBaseMakeQueryCommand.php create mode 100644 src/Maker/MakeCommand.php create mode 100644 src/Resources/skeleton/command/Command.tpl.php create mode 100644 src/Resources/skeleton/command/CommandHandler.tpl.php diff --git a/config/services.yaml b/config/services.yaml index 9873c17..48126f2 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -30,6 +30,11 @@ services: tags: - { name: maker.command } + GeekCell\DddBundle\Maker\MakeCommand: + class: GeekCell\DddBundle\Maker\MakeCommand + tags: + - { name: maker.command } + GeekCell\DddBundle\Maker\Doctrine\DoctrineConfigUpdater: class: GeekCell\DddBundle\Maker\Doctrine\DoctrineConfigUpdater public: false diff --git a/src/Maker/AbstractBaseMakeQueryCommand.php b/src/Maker/AbstractBaseMakeQueryCommand.php new file mode 100644 index 0000000..44767dd --- /dev/null +++ b/src/Maker/AbstractBaseMakeQueryCommand.php @@ -0,0 +1,148 @@ +getTarget()); + } + + /** + * @return string + */ + function getNamespacePrefix(): string + { + return 'Application\\' . $this->getClassSuffix() . '\\'; + } + + /** + * @inheritDoc + */ + public function configureCommand(Command $command, InputConfiguration $inputConfig): void + { + $command + ->addArgument( + 'name', + InputArgument::REQUIRED, + 'The name of the ' . $this->getTarget() . ' class (e.g. Customer)', + ) + ; + } + + /** + * @inheritDoc + */ + public function configureDependencies(DependencyBuilder $dependencies, InputInterface $input = null): void + { + } + + /** + * @inheritDoc + */ + public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void + { + } + + /** + * @inheritDoc + */ + public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void + { + $entityClassNameDetails = $generator->createClassNameDetails( + $input->getArgument('name'), + $this->getNamespacePrefix(), + $this->getClassSuffix(), + ); + + $this->generateEntity($entityClassNameDetails, $generator); + $this->generateHandler($entityClassNameDetails, $generator); + + $this->writeSuccessMessage($io); + } + + /** + * @param ClassNameDetails $queryClassNameDetails + * @param Generator $generator + * @return void + */ + private function generateEntity(ClassNameDetails $queryClassNameDetails, Generator $generator): void + { + $templateVars = [ + 'use_statements' => new UseStatementGenerator($this->getEntityUseStatements()), + ]; + + $templatePath = __DIR__."/../Resources/skeleton/{$this->getTarget()}/{$this->getClassSuffix()}.tpl.php"; + $generator->generateClass( + $queryClassNameDetails->getFullName(), + $templatePath, + $templateVars, + ); + + $generator->writeChanges(); + } + + /** + * @param ClassNameDetails $queryClassNameDetails + * @param Generator $generator + * @return void + */ + private function generateHandler(ClassNameDetails $queryClassNameDetails, Generator $generator): void + { + $classNameDetails = $generator->createClassNameDetails( + $queryClassNameDetails->getShortName(), + $this->getNamespacePrefix(), + 'Handler', + ); + + $templateVars = [ + 'use_statements' => new UseStatementGenerator($this->getEntityHandlerUseStatements()), + 'query_class_name' => $queryClassNameDetails->getShortName() + ]; + + $templatePath = __DIR__."/../Resources/skeleton/{$this->getTarget()}/{$this->getClassSuffix()}Handler.tpl.php"; + $generator->generateClass( + $classNameDetails->getFullName(), + $templatePath, + $templateVars, + ); + + $generator->writeChanges(); + } +} diff --git a/src/Maker/MakeCommand.php b/src/Maker/MakeCommand.php new file mode 100644 index 0000000..0ff7204 --- /dev/null +++ b/src/Maker/MakeCommand.php @@ -0,0 +1,61 @@ +addArgument( - 'name', - InputArgument::REQUIRED, - 'The name of the query class (e.g. Customer)', - ) - ; - } - - /** - * @inheritDoc - */ - public function configureDependencies(DependencyBuilder $dependencies, InputInterface $input = null): void - { + return 'Creates a new ' . self::TARGET . ' class and handler'; } /** * @inheritDoc */ - public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void + function getTarget(): string { + return self::TARGET; } /** * @inheritDoc */ - public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void + function getEntityUseStatements(): array { - $queryClassNameDetails = $generator->createClassNameDetails( - $input->getArgument('name'), - NAMESPACE_PREFIX, - 'Query', - ); - - $this->generateQuery($queryClassNameDetails, $generator); - $this->generateQueryHandler($queryClassNameDetails, $generator); - - $this->writeSuccessMessage($io); - } - - /** - * @param ClassNameDetails $queryClassNameDetails - * @param Generator $generator - * @return void - */ - private function generateQuery(ClassNameDetails $queryClassNameDetails, Generator $generator): void - { - $templateVars = [ - 'use_statements' => new UseStatementGenerator([ - Query::class, - ]), + return [ + Query::class ]; - - $templatePath = __DIR__.'/../Resources/skeleton/query/Query.tpl.php'; - $generator->generateClass( - $queryClassNameDetails->getFullName(), - $templatePath, - $templateVars, - ); - - $generator->writeChanges(); } /** - * @param ClassNameDetails $queryClassNameDetails - * @param Generator $generator - * @return void + * @inheritDoc */ - private function generateQueryHandler(ClassNameDetails $queryClassNameDetails, Generator $generator): void + function getEntityHandlerUseStatements(): array { - $classNameDetails = $generator->createClassNameDetails( - $queryClassNameDetails->getShortName(), - NAMESPACE_PREFIX, - 'Handler', - ); - - $templateVars = [ - 'use_statements' => new UseStatementGenerator([ - QueryHandler::class, - Collection::class, - AsMessageHandler::class - ]), - 'query_class_name' => $queryClassNameDetails->getShortName() + return [ + QueryHandler::class, + Collection::class, + AsMessageHandler::class ]; - - $templatePath = __DIR__.'/../Resources/skeleton/query/QueryHandler.tpl.php'; - $generator->generateClass( - $classNameDetails->getFullName(), - $templatePath, - $templateVars, - ); - - $generator->writeChanges(); } } diff --git a/src/Resources/skeleton/command/Command.tpl.php b/src/Resources/skeleton/command/Command.tpl.php new file mode 100644 index 0000000..1ee97fe --- /dev/null +++ b/src/Resources/skeleton/command/Command.tpl.php @@ -0,0 +1,9 @@ + + +namespace ; + + + +class implements Command +{ +} diff --git a/src/Resources/skeleton/command/CommandHandler.tpl.php b/src/Resources/skeleton/command/CommandHandler.tpl.php new file mode 100644 index 0000000..8725a84 --- /dev/null +++ b/src/Resources/skeleton/command/CommandHandler.tpl.php @@ -0,0 +1,13 @@ + + +namespace ; + + + +#[AsMessageHandler] +class implements CommandHandler +{ + public function __invoke( $query): Collection + { + } +} From 6f2e293047ef1fa4744817b8bd8970779e44efa1 Mon Sep 17 00:00:00 2001 From: janvt Date: Fri, 10 Mar 2023 15:12:58 +0100 Subject: [PATCH 27/44] change command return type to void --- src/Maker/MakeCommand.php | 1 - src/Resources/skeleton/command/CommandHandler.tpl.php | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Maker/MakeCommand.php b/src/Maker/MakeCommand.php index 0ff7204..bdaa81f 100644 --- a/src/Maker/MakeCommand.php +++ b/src/Maker/MakeCommand.php @@ -54,7 +54,6 @@ function getEntityHandlerUseStatements(): array { return [ CommandHandler::class, - Collection::class, AsMessageHandler::class ]; } diff --git a/src/Resources/skeleton/command/CommandHandler.tpl.php b/src/Resources/skeleton/command/CommandHandler.tpl.php index 8725a84..eed3eed 100644 --- a/src/Resources/skeleton/command/CommandHandler.tpl.php +++ b/src/Resources/skeleton/command/CommandHandler.tpl.php @@ -7,7 +7,7 @@ #[AsMessageHandler] class implements CommandHandler { - public function __invoke( $query): Collection + public function __invoke( $query): void { } } From aaecb4fec6e78c6daf450b7b87186c421a8141bc Mon Sep 17 00:00:00 2001 From: janvt Date: Fri, 10 Mar 2023 15:30:05 +0100 Subject: [PATCH 28/44] add maker command for controller --- config/services.yaml | 7 + src/Maker/MakeController.php | 173 ++++++++++++++++++ src/Maker/MakeModel.php | 1 - .../skeleton/controller/Controller.tpl.php | 19 ++ .../skeleton/controller/RouteConfig.tpl.php | 5 + 5 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 src/Maker/MakeController.php create mode 100644 src/Resources/skeleton/controller/Controller.tpl.php create mode 100644 src/Resources/skeleton/controller/RouteConfig.tpl.php diff --git a/config/services.yaml b/config/services.yaml index 48126f2..542a618 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -35,6 +35,13 @@ services: tags: - { name: maker.command } + GeekCell\DddBundle\Maker\MakeController: + class: GeekCell\DddBundle\Maker\MakeController + arguments: + - '@maker.file_manager' + tags: + - { name: maker.command } + GeekCell\DddBundle\Maker\Doctrine\DoctrineConfigUpdater: class: GeekCell\DddBundle\Maker\Doctrine\DoctrineConfigUpdater public: false diff --git a/src/Maker/MakeController.php b/src/Maker/MakeController.php new file mode 100644 index 0000000..2b744e2 --- /dev/null +++ b/src/Maker/MakeController.php @@ -0,0 +1,173 @@ +addArgument( + 'name', + InputArgument::REQUIRED, + 'The name of the controller class (e.g. Customer)', + ) + ->addOption( + 'include-query-bus', + null, + InputOption::VALUE_REQUIRED, + 'Add a query bus dependency.', + false + ) + ->addOption( + 'include-command-bus', + null, + InputOption::VALUE_REQUIRED, + 'Add a command bus dependency.', + false + ) + ; + } + + /** + * @inheritDoc + */ + public function configureDependencies(DependencyBuilder $dependencies, InputInterface $input = null): void + { + } + + /** + * @inheritDoc + */ + public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void + { + if (false === $input->getOption('include-query-bus')) { + $includeQueryBus = $io->confirm( + 'Do you want to add a query bus dependency?', + false, + ); + $input->setOption('include-query-bus', $includeQueryBus); + } + + if (false === $input->getOption('include-command-bus')) { + $includeCommandBus = $io->confirm( + 'Do you want to add a command bus dependency?', + false, + ); + $input->setOption('include-command-bus', $includeCommandBus); + } + } + + /** + * @inheritDoc + */ + public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void + { + $classNameDetails = $generator->createClassNameDetails( + $input->getArgument('name'), + self::NAMESPACE_PREFIX, + 'Controller', + ); + + $classesToImport = [ + AbstractController::class, + Route::class, + Response::class + ]; + + $routeName = lcfirst($input->getArgument('name')); + $templateVars = [ + 'route_name' => $routeName, + 'route_name_snake' => strtolower( + preg_replace( + ["/([A-Z]+)/", "/_([A-Z]+)([A-Z][a-z])/"], + ["_$1", "_$1_$2"], + $routeName + ) + ), + 'dependencies' => [] + ]; + + if ($input->getOption('include-query-bus')) { + $templateVars['dependencies'][] = 'private QueryBus $queryBus'; + $classesToImport[] = QueryBus::class; + } + + if ($input->getOption('include-command-bus')) { + $templateVars['dependencies'][] = 'private CommandBus $commandBus'; + $classesToImport[] = CommandBus::class; + } + + $templateVars['use_statements'] = new UseStatementGenerator($classesToImport); + + $templatePath = __DIR__.'/../Resources/skeleton/controller/Controller.tpl.php'; + $generator->generateClass( + $classNameDetails->getFullName(), + $templatePath, + $templateVars, + ); + + // ensure controller config has been created + if (!$this->fileManager->fileExists(self::CONFIG_PATH)) { + $templatePathConfig = __DIR__ . '/../Resources/skeleton/controller/RouteConfig.tpl.php'; + $generator->generateFile( + self::CONFIG_PATH, + $templatePathConfig, + [ + 'path' => '../../src/Infrastructure/Http/Controller/', + 'namespace' => str_replace('\\' . $classNameDetails->getShortName(), '', $classNameDetails->getFullName()) + ] + ); + } + + $generator->writeChanges(); + + $this->writeSuccessMessage($io); + } +} diff --git a/src/Maker/MakeModel.php b/src/Maker/MakeModel.php index 3682056..2725eb0 100644 --- a/src/Maker/MakeModel.php +++ b/src/Maker/MakeModel.php @@ -154,7 +154,6 @@ public function interact(InputInterface $input, ConsoleStyle $io, Command $comma $modelName, $useSuffix ? 'Model' : '', ), - true, ); $input->setOption('aggregate-root', $asAggregateRoot); } diff --git a/src/Resources/skeleton/controller/Controller.tpl.php b/src/Resources/skeleton/controller/Controller.tpl.php new file mode 100644 index 0000000..eca9319 --- /dev/null +++ b/src/Resources/skeleton/controller/Controller.tpl.php @@ -0,0 +1,19 @@ + + +namespace ; + + + +class extends AbstractController +{ + 0): ?> + public function __construct() + {} + + + #[Route('/', name: '')] + public function (): Response + { + return new Response(); + } +} diff --git a/src/Resources/skeleton/controller/RouteConfig.tpl.php b/src/Resources/skeleton/controller/RouteConfig.tpl.php new file mode 100644 index 0000000..78b0e96 --- /dev/null +++ b/src/Resources/skeleton/controller/RouteConfig.tpl.php @@ -0,0 +1,5 @@ +controllers: + resource: + path: + namespace: + type: attribute From 3e52790dfad2ad3279eafad7146214124b16e786 Mon Sep 17 00:00:00 2001 From: janvt Date: Fri, 10 Mar 2023 15:41:15 +0100 Subject: [PATCH 29/44] add docs --- README.md | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9126091..8960441 100644 --- a/README.md +++ b/README.md @@ -206,5 +206,26 @@ Usage: make:ddd:query|command [] Arguments: - name The name of the query|command class (e.g. Customer) + name The name of the query|command class (e.g. Customer) +``` + +### Controller + +This command can be used to generate a controller with optional `QueryBus` and `CommandBus` dependencies. + +#### Command Output + +```bash +Description: + Creates a new controller class + +Usage: + make:ddd:controller [options] [--] [] + +Arguments: + name The name of the model class (e.g. Customer) + +Options: + --include-query-bus Add a query bus dependency + --include-command-bus Add a command bus dependency ``` From 679e03ac148d00d74b8233e2ab5fe68b9ce2f603 Mon Sep 17 00:00:00 2001 From: janvt Date: Mon, 13 Mar 2023 16:44:10 +0100 Subject: [PATCH 30/44] add command for creating API Platform resource --- config/services.yaml | 12 + src/Maker/AbstractBaseConfigUpdater.php | 41 ++++ .../ApiPlatform/ApiPlatformConfigUpdater.php | 27 +++ src/Maker/Doctrine/DoctrineConfigUpdater.php | 43 +--- src/Maker/MakeModel.php | 7 +- src/Maker/MakeResource.php | 220 ++++++++++++++++++ .../resource/ApiPlatformConfig.tpl.php | 4 + .../skeleton/resource/Processor.tpl.php | 19 ++ .../skeleton/resource/Provider.tpl.php | 20 ++ .../skeleton/resource/Resource.tpl.php | 24 ++ 10 files changed, 375 insertions(+), 42 deletions(-) create mode 100644 src/Maker/AbstractBaseConfigUpdater.php create mode 100644 src/Maker/ApiPlatform/ApiPlatformConfigUpdater.php create mode 100644 src/Maker/MakeResource.php create mode 100644 src/Resources/skeleton/resource/ApiPlatformConfig.tpl.php create mode 100644 src/Resources/skeleton/resource/Processor.tpl.php create mode 100644 src/Resources/skeleton/resource/Provider.tpl.php create mode 100644 src/Resources/skeleton/resource/Resource.tpl.php diff --git a/config/services.yaml b/config/services.yaml index 542a618..0ea7601 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -42,6 +42,18 @@ services: tags: - { name: maker.command } + GeekCell\DddBundle\Maker\MakeResource: + class: GeekCell\DddBundle\Maker\MakeResource + arguments: + - '@maker.file_manager' + - '@GeekCell\DddBundle\Maker\Doctrine\ApiPlatformConfigUpdator' + tags: + - { name: maker.command } + GeekCell\DddBundle\Maker\Doctrine\DoctrineConfigUpdater: class: GeekCell\DddBundle\Maker\Doctrine\DoctrineConfigUpdater public: false + + GeekCell\DddBundle\Maker\Doctrine\ApiPlatformConfigUpdator: + class: GeekCell\DddBundle\Maker\ApiPlatform\ApiPlatformConfigUpdater + public: false diff --git a/src/Maker/AbstractBaseConfigUpdater.php b/src/Maker/AbstractBaseConfigUpdater.php new file mode 100644 index 0000000..8ccb923 --- /dev/null +++ b/src/Maker/AbstractBaseConfigUpdater.php @@ -0,0 +1,41 @@ + + */ + protected function read(string $yamlSource): array + { + $this->manipulator = new YamlSourceManipulator($yamlSource); + return $this->manipulator->getData(); + } + + /** + * Returns the updated YAML contents for the given data. + * + * @param array $yamlData + * @return string + * @throws AssertionFailedException + */ + protected function write(array $yamlData): string + { + Assert\Assertion::notNull($this->manipulator); + $this->manipulator->setData($yamlData); + + return $this->manipulator->getContents(); + } +} diff --git a/src/Maker/ApiPlatform/ApiPlatformConfigUpdater.php b/src/Maker/ApiPlatform/ApiPlatformConfigUpdater.php new file mode 100644 index 0000000..4b0e8d3 --- /dev/null +++ b/src/Maker/ApiPlatform/ApiPlatformConfigUpdater.php @@ -0,0 +1,27 @@ +read($yamlSource); + + /** @var array|null $currentPaths */ + $currentPaths = $data['api_platform']['mapping']['paths']; + $data['api_platform']['mapping']['paths'] = array_unique(array_merge($currentPaths, [$path])); + + return $this->write($data); + } +} diff --git a/src/Maker/Doctrine/DoctrineConfigUpdater.php b/src/Maker/Doctrine/DoctrineConfigUpdater.php index a2404f4..37ce45e 100644 --- a/src/Maker/Doctrine/DoctrineConfigUpdater.php +++ b/src/Maker/Doctrine/DoctrineConfigUpdater.php @@ -5,15 +5,10 @@ namespace GeekCell\DddBundle\Maker\Doctrine; use Assert; -use Symfony\Bundle\MakerBundle\Util\YamlSourceManipulator; +use GeekCell\DddBundle\Maker\AbstractBaseConfigUpdater; -class DoctrineConfigUpdater +class DoctrineConfigUpdater extends AbstractBaseConfigUpdater { - /** - * @var null|YamlSourceManipulator - */ - private ?YamlSourceManipulator $manipulator; - /** * Registers a custom DBAL mapping type. * @@ -25,10 +20,10 @@ class DoctrineConfigUpdater */ public function addCustomDBALMappingType(string $yamlSource, string $identifier, string $mappingClass): string { - $data = $this->createYamlSourceManipulator($yamlSource); + $data = $this->read($yamlSource); $data['doctrine']['dbal']['types'][$identifier] = $mappingClass; - return $this->getYamlContentsFromData($data); + return $this->write($data); } /** @@ -44,39 +39,13 @@ public function updateORMDefaultEntityMapping(string $yamlSource, string $mappin { Assert\Assertion::inArray($mappingType, ['xml', 'attribute'], 'Invalid mapping type: %s'); - $data = $this->createYamlSourceManipulator($yamlSource); + $data = $this->read($yamlSource); $data['doctrine']['orm']['mappings']['App']['type'] = $mappingType; $data['doctrine']['orm']['mappings']['App']['dir'] = $directory; $data['doctrine']['orm']['mappings']['App']['prefix'] = 'App\Domain\Model'; $data['doctrine']['orm']['mappings']['App']['alias'] = 'App'; $data['doctrine']['orm']['mappings']['App']['is_bundle'] = false; - return $this->getYamlContentsFromData($data); - } - - /** - * Creates a YamlSourceManipulator from a YAML source. - * - * @param string $yamlSource - * @return array - */ - private function createYamlSourceManipulator(string $yamlSource): array - { - $this->manipulator = new YamlSourceManipulator($yamlSource); - return $this->manipulator->getData(); - } - - /** - * Returns the updated YAML contents for the given data. - * - * @param array $yamlData - * @return string - */ - private function getYamlContentsFromData(array $yamlData): string - { - Assert\Assertion::notNull($this->manipulator); - $this->manipulator->setData($yamlData); - - return $this->manipulator->getContents(); + return $this->write($data); } } diff --git a/src/Maker/MakeModel.php b/src/Maker/MakeModel.php index 2725eb0..e820577 100644 --- a/src/Maker/MakeModel.php +++ b/src/Maker/MakeModel.php @@ -46,16 +46,13 @@ final class MakeModel extends AbstractMaker implements InputAwareMakerInterface */ private $templateVariables = []; - /** - * Constructor. - * * @param DoctrineConfigUpdater $doctrineUpdater * @param FileManager $fileManager */ public function __construct( - private DoctrineConfigUpdater $doctrineUpdater, - private FileManager $fileManager, + private readonly DoctrineConfigUpdater $doctrineUpdater, + private readonly FileManager $fileManager, ) {} /** diff --git a/src/Maker/MakeResource.php b/src/Maker/MakeResource.php new file mode 100644 index 0000000..32fd700 --- /dev/null +++ b/src/Maker/MakeResource.php @@ -0,0 +1,220 @@ +addArgument( + 'name', + InputArgument::REQUIRED, + 'The name of the model class to create the resource for (e.g. Customer). Model must exist already.', + ) + ; + } + + /** + * @inheritDoc + */ + public function configureDependencies(DependencyBuilder $dependencies, InputInterface $input = null): void + { + } + + /** + * @inheritDoc + */ + public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void + { + } + + /** + * @inheritDoc + */ + public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void + { + $baseName = $input->getArgument('name'); + + $modelClassNameDetails = $generator->createClassNameDetails( + $baseName, + 'Domain\\Model\\', + '', + ); + + if (!class_exists($modelClassNameDetails->getFullName())) { + throw new RuntimeCommandException("Could not find model {$modelClassNameDetails->getFullName()}!"); + } + + $classNameDetails = $generator->createClassNameDetails( + $baseName, + self::NAMESPACE_PREFIX . 'Resource', + 'Resource', + ); + + $this->ensureConfig($generator); + + $providerClassNameDetails = $generator->createClassNameDetails( + $baseName, + self::NAMESPACE_PREFIX . 'State', + 'Provider', + ); + $this->generateProvider($providerClassNameDetails, $generator); + + $processorClassNameDetails = $generator->createClassNameDetails( + $baseName, + self::NAMESPACE_PREFIX . 'State', + 'Processor', + ); + $this->generateProcessor($processorClassNameDetails, $generator); + + $templateVars = [ + 'use_statements' => new UseStatementGenerator([ + ApiResource::class, + $modelClassNameDetails->getFullName(), + $providerClassNameDetails->getFullName(), + $processorClassNameDetails->getFullName(), + ]), + 'entity_class_name' => $modelClassNameDetails->getShortName(), + 'provider_class_name' => $providerClassNameDetails->getShortName(), + 'processor_class_name' => $processorClassNameDetails->getShortName(), + ]; + + $generator->generateClass( + $classNameDetails->getFullName(), + __DIR__.'/../Resources/skeleton/resource/Resource.tpl.php', + $templateVars, + ); + + $generator->writeChanges(); + + $this->writeSuccessMessage($io); + } + + /** + * ensure custom resource path is added to config + * + * @param Generator $generator + * @return void + */ + private function ensureConfig(Generator $generator): void + { + $customResourcePath = '%kernel.project_dir%/src/Infrastructure/ApiPlatform/Resource'; + if ($this->fileManager->fileExists(self::CONFIG_PATH)) { + $newYaml = $this->configUpdater->addCustomPath( + $this->fileManager->getFileContents(self::CONFIG_PATH), + $customResourcePath + ); + + $generator->dumpFile(self::CONFIG_PATH, $newYaml); + } else { + $generator->generateFile( + self::CONFIG_PATH, + __DIR__ . '/../Resources/skeleton/resource/ApiPlatformConfig.tpl.php', + [ + 'path' => $customResourcePath, + ] + ); + } + + $generator->writeChanges(); + } + + /** + * @param ClassNameDetails $providerClassNameDetails + * @param Generator $generator + * @return void + */ + private function generateProvider(ClassNameDetails $providerClassNameDetails, Generator $generator) + { + $templateVars = [ + 'use_statements' => new UseStatementGenerator([ + ProviderInterface::class, + QueryBus::class, + Operation::class + ]), + ]; + + $generator->generateClass( + $providerClassNameDetails->getFullName(), + __DIR__.'/../Resources/skeleton/resource/Provider.tpl.php', + $templateVars, + ); + + $generator->writeChanges(); + } + + /** + * @param ClassNameDetails $processorClassNameDetails + * @param Generator $generator + * @return void + */ + private function generateProcessor(ClassNameDetails $processorClassNameDetails, Generator $generator) + { + $templateVars = [ + 'use_statements' => new UseStatementGenerator([ + ProcessorInterface::class, + CommandBus::class, + Operation::class + ]), + ]; + + $generator->generateClass( + $processorClassNameDetails->getFullName(), + __DIR__.'/../Resources/skeleton/resource/Processor.tpl.php', + $templateVars, + ); + + $generator->writeChanges(); + } +} diff --git a/src/Resources/skeleton/resource/ApiPlatformConfig.tpl.php b/src/Resources/skeleton/resource/ApiPlatformConfig.tpl.php new file mode 100644 index 0000000..28922c6 --- /dev/null +++ b/src/Resources/skeleton/resource/ApiPlatformConfig.tpl.php @@ -0,0 +1,4 @@ +api_platform: + mapping: + paths: + - '' diff --git a/src/Resources/skeleton/resource/Processor.tpl.php b/src/Resources/skeleton/resource/Processor.tpl.php new file mode 100644 index 0000000..1a4a9f8 --- /dev/null +++ b/src/Resources/skeleton/resource/Processor.tpl.php @@ -0,0 +1,19 @@ + + +namespace ; + + + +class implements ProcessorInterface +{ + public function __construct( + private readonly CommandBus $commandBus + ) {} + + /** + * @inheritDoc + */ + public function process($data, Operation $operation, array $uriVariables = [], array $context = []) + { + } +} diff --git a/src/Resources/skeleton/resource/Provider.tpl.php b/src/Resources/skeleton/resource/Provider.tpl.php new file mode 100644 index 0000000..a21481f --- /dev/null +++ b/src/Resources/skeleton/resource/Provider.tpl.php @@ -0,0 +1,20 @@ + + +namespace ; + + + +class implements ProviderInterface +{ + public function __construct( + private readonly QueryBus $queryBus + ) {} + + /** + * @inheritDoc + */ + public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null + { + return null; + } +} diff --git a/src/Resources/skeleton/resource/Resource.tpl.php b/src/Resources/skeleton/resource/Resource.tpl.php new file mode 100644 index 0000000..2c917c8 --- /dev/null +++ b/src/Resources/skeleton/resource/Resource.tpl.php @@ -0,0 +1,24 @@ + + +namespace ; + + + +#[ApiResource( + provider: ::class, + processor: ::class, +)] +final class +{ + /** + * Convenience factory method to create the resource from an instance of the model + * + * @param $model + * + * @return static + */ + public static function create( $model): static + { + return new static(); + } +} From 883316eb5bb430596c75407a15e14283bffbc4c2 Mon Sep 17 00:00:00 2001 From: janvt Date: Mon, 13 Mar 2023 17:31:23 +0100 Subject: [PATCH 31/44] check for API Platform --- src/Maker/MakeResource.php | 4 ++++ src/Resources/skeleton/resource/Processor.tpl.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Maker/MakeResource.php b/src/Maker/MakeResource.php index 32fd700..d8eeb76 100644 --- a/src/Maker/MakeResource.php +++ b/src/Maker/MakeResource.php @@ -8,6 +8,7 @@ use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProcessorInterface; use ApiPlatform\State\ProviderInterface; +use ApiPlatform\Symfony\Bundle\ApiPlatformBundle; use GeekCell\Ddd\Contracts\Application\CommandBus; use GeekCell\Ddd\Contracts\Application\QueryBus; use GeekCell\DddBundle\Maker\ApiPlatform\ApiPlatformConfigUpdater; @@ -77,6 +78,9 @@ public function configureDependencies(DependencyBuilder $dependencies, InputInte */ public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void { + if (!class_exists(ApiPlatformBundle::class)) { + throw new RuntimeCommandException('This command requires Api Platform >2.7 to be installed.'); + } } /** diff --git a/src/Resources/skeleton/resource/Processor.tpl.php b/src/Resources/skeleton/resource/Processor.tpl.php index 1a4a9f8..d5e29e0 100644 --- a/src/Resources/skeleton/resource/Processor.tpl.php +++ b/src/Resources/skeleton/resource/Processor.tpl.php @@ -13,7 +13,7 @@ public function __construct( /** * @inheritDoc */ - public function process($data, Operation $operation, array $uriVariables = [], array $context = []) + public function process($data, Operation $operation, array $uriVariables = [], array $context = []): void { } } From 4c79cf023d2a7d60dfb786ec9cfc32a4f4f31505 Mon Sep 17 00:00:00 2001 From: janvt Date: Tue, 14 Mar 2023 14:37:51 +0100 Subject: [PATCH 32/44] generate optional XML config instead of PHP attributes --- src/Maker/MakeResource.php | 81 +++++++++++++++---- .../skeleton/resource/Resource.tpl.php | 2 + .../resource/ResourceXmlConfig.tpl.php | 11 +++ 3 files changed, 78 insertions(+), 16 deletions(-) create mode 100644 src/Resources/skeleton/resource/ResourceXmlConfig.tpl.php diff --git a/src/Maker/MakeResource.php b/src/Maker/MakeResource.php index d8eeb76..bffd763 100644 --- a/src/Maker/MakeResource.php +++ b/src/Maker/MakeResource.php @@ -25,11 +25,16 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; final class MakeResource extends AbstractMaker implements InputAwareMakerInterface { const NAMESPACE_PREFIX = 'Infrastructure\\ApiPlatform\\'; const CONFIG_PATH = 'config/packages/api_platform.yaml'; + const CONFIG_PATH_XML = 'config/api_platform/'; + + const CONFIG_FLAVOR_ATTRIBUTE = 'attribute'; + const CONFIG_FLAVOR_XML = 'xml'; public function __construct( private FileManager $fileManager, @@ -63,6 +68,13 @@ public function configureCommand(Command $command, InputConfiguration $inputConf InputArgument::REQUIRED, 'The name of the model class to create the resource for (e.g. Customer). Model must exist already.', ) + ->addOption( + 'config', + null, + InputOption::VALUE_REQUIRED, + 'Config flavor to create (attribute|xml).', + 'attribute' + ) ; } @@ -81,6 +93,17 @@ public function interact(InputInterface $input, ConsoleStyle $io, Command $comma if (!class_exists(ApiPlatformBundle::class)) { throw new RuntimeCommandException('This command requires Api Platform >2.7 to be installed.'); } + + if (false === $input->getOption('config')) { + $configFlavor = $io->choice( + 'Config flavor to create (attribute|xml). (%sModel)', + [ + 'attribute' => 'PHP attributes', + 'xml' => 'XML mapping', + ], + ); + $input->setOption('config', $configFlavor); + } } /** @@ -89,6 +112,7 @@ public function interact(InputInterface $input, ConsoleStyle $io, Command $comma public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void { $baseName = $input->getArgument('name'); + $configFlavor = $input->getOption('config'); $modelClassNameDetails = $generator->createClassNameDetails( $baseName, @@ -106,7 +130,7 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen 'Resource', ); - $this->ensureConfig($generator); + $this->ensureConfig($generator, $configFlavor); $providerClassNameDetails = $generator->createClassNameDetails( $baseName, @@ -122,16 +146,19 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen ); $this->generateProcessor($processorClassNameDetails, $generator); + $classesToImport = [$modelClassNameDetails->getFullName()]; + if ($configFlavor === self::CONFIG_FLAVOR_ATTRIBUTE) { + $classesToImport[] = ApiResource::class; + $classesToImport[] = $providerClassNameDetails->getFullName(); + $classesToImport[] = $processorClassNameDetails->getFullName(); + } + $templateVars = [ - 'use_statements' => new UseStatementGenerator([ - ApiResource::class, - $modelClassNameDetails->getFullName(), - $providerClassNameDetails->getFullName(), - $processorClassNameDetails->getFullName(), - ]), + 'use_statements' => new UseStatementGenerator($classesToImport), 'entity_class_name' => $modelClassNameDetails->getShortName(), 'provider_class_name' => $providerClassNameDetails->getShortName(), 'processor_class_name' => $processorClassNameDetails->getShortName(), + 'configure_with_attributes' => $configFlavor === self::CONFIG_FLAVOR_ATTRIBUTE ]; $generator->generateClass( @@ -140,28 +167,37 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen $templateVars, ); + if ($configFlavor === self::CONFIG_FLAVOR_XML) { + $targetPath = self::CONFIG_PATH_XML . $classNameDetails->getShortName() . '.xml'; + $generator->generateFile( + $targetPath, + __DIR__.'/../Resources/skeleton/resource/ResourceXmlConfig.tpl.php', + [ + 'entity_full_class_name' => $modelClassNameDetails->getFullName(), + 'provider_class_name' => $providerClassNameDetails->getFullName(), + 'processor_class_name' => $processorClassNameDetails->getFullName(), + ] + ); + } + $generator->writeChanges(); $this->writeSuccessMessage($io); } /** - * ensure custom resource path is added to config + * ensure custom resource path(s) are added to config * * @param Generator $generator + * @param string $configFlavor * @return void */ - private function ensureConfig(Generator $generator): void + private function ensureConfig(Generator $generator, string $configFlavor): void { $customResourcePath = '%kernel.project_dir%/src/Infrastructure/ApiPlatform/Resource'; - if ($this->fileManager->fileExists(self::CONFIG_PATH)) { - $newYaml = $this->configUpdater->addCustomPath( - $this->fileManager->getFileContents(self::CONFIG_PATH), - $customResourcePath - ); + $customConfigPath = '%kernel.project_dir%/' . self::CONFIG_PATH_XML; - $generator->dumpFile(self::CONFIG_PATH, $newYaml); - } else { + if (!$this->fileManager->fileExists(self::CONFIG_PATH)) { $generator->generateFile( self::CONFIG_PATH, __DIR__ . '/../Resources/skeleton/resource/ApiPlatformConfig.tpl.php', @@ -169,8 +205,21 @@ private function ensureConfig(Generator $generator): void 'path' => $customResourcePath, ] ); + + $generator->writeChanges(); } + $newYaml = $this->configUpdater->addCustomPath( + $this->fileManager->getFileContents(self::CONFIG_PATH), + $customResourcePath + ); + + if ($configFlavor === self::CONFIG_FLAVOR_XML) { + $newYaml = $this->configUpdater->addCustomPath($newYaml, $customConfigPath); + } + + $generator->dumpFile(self::CONFIG_PATH, $newYaml); + $generator->writeChanges(); } diff --git a/src/Resources/skeleton/resource/Resource.tpl.php b/src/Resources/skeleton/resource/Resource.tpl.php index 2c917c8..4e6744f 100644 --- a/src/Resources/skeleton/resource/Resource.tpl.php +++ b/src/Resources/skeleton/resource/Resource.tpl.php @@ -4,10 +4,12 @@ + #[ApiResource( provider: ::class, processor: ::class, )] + final class { /** diff --git a/src/Resources/skeleton/resource/ResourceXmlConfig.tpl.php b/src/Resources/skeleton/resource/ResourceXmlConfig.tpl.php new file mode 100644 index 0000000..f7949c2 --- /dev/null +++ b/src/Resources/skeleton/resource/ResourceXmlConfig.tpl.php @@ -0,0 +1,11 @@ + + + + From 8c653f07aa7a16fd40f5b5a1b8850c9572e7d912 Mon Sep 17 00:00:00 2001 From: janvt Date: Tue, 14 Mar 2023 14:47:49 +0100 Subject: [PATCH 33/44] adjust Api Platform version check to check for >2.7 --- src/Maker/MakeResource.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Maker/MakeResource.php b/src/Maker/MakeResource.php index bffd763..5e31319 100644 --- a/src/Maker/MakeResource.php +++ b/src/Maker/MakeResource.php @@ -90,7 +90,10 @@ public function configureDependencies(DependencyBuilder $dependencies, InputInte */ public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void { - if (!class_exists(ApiPlatformBundle::class)) { + // Check for bundle to make sure API Platform package is installed. + // Then check if the new ApiResource class in the Metadata namespace exists. + // -> Was only introduced in v2.7. + if (!class_exists(ApiPlatformBundle::class) || !class_exists(ApiResource::class)) { throw new RuntimeCommandException('This command requires Api Platform >2.7 to be installed.'); } From 870a036da20617164f1394f8aed4482500135675 Mon Sep 17 00:00:00 2001 From: janvt Date: Tue, 14 Mar 2023 14:47:57 +0100 Subject: [PATCH 34/44] add docs --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index 8960441..fcd3912 100644 --- a/README.md +++ b/README.md @@ -229,3 +229,23 @@ Options: --include-query-bus Add a query bus dependency --include-command-bus Add a command bus dependency ``` + +### Resource + +This command can be used to generate an [Api Platform](https://api-platform.com/) resource. Minimum required version is [2.7](https://api-platform.com/docs/core/upgrade-guide/#api-platform-2730) for the PHP attributes support. + +#### Command Output + +```bash +Description: + Creates a new API Platform resource + +Usage: + make:ddd:resource [options] [--] [] + +Arguments: + name The name of the model class to create the resource for (e.g. Customer). Model must exist already. + +Options: + --config Config flavor to create (attribute|xml). +``` From 05ba7466081d1e069150581883b75f198221ebcf Mon Sep 17 00:00:00 2001 From: janvt Date: Thu, 16 Mar 2023 11:18:33 +0100 Subject: [PATCH 35/44] use String component for snake casing --- src/Maker/MakeController.php | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Maker/MakeController.php b/src/Maker/MakeController.php index 2b744e2..7404c95 100644 --- a/src/Maker/MakeController.php +++ b/src/Maker/MakeController.php @@ -21,6 +21,7 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; +use function Symfony\Component\String\u; final class MakeController extends AbstractMaker implements InputAwareMakerInterface { @@ -124,13 +125,7 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen $routeName = lcfirst($input->getArgument('name')); $templateVars = [ 'route_name' => $routeName, - 'route_name_snake' => strtolower( - preg_replace( - ["/([A-Z]+)/", "/_([A-Z]+)([A-Z][a-z])/"], - ["_$1", "_$1_$2"], - $routeName - ) - ), + 'route_name_snake' => u($routeName)->snake()->lower(), 'dependencies' => [] ]; From 826ea99bcddb4c6f9771dda2190747202529c6b8 Mon Sep 17 00:00:00 2001 From: janvt Date: Fri, 17 Mar 2023 13:14:32 +0100 Subject: [PATCH 36/44] adjust repository generation --- src/Maker/MakeModel.php | 89 +++++++++++++++---- .../skeleton/model/Repository.tpl.php | 61 +++---------- .../model/RepositoryInterface.tpl.php | 10 +++ 3 files changed, 94 insertions(+), 66 deletions(-) create mode 100644 src/Resources/skeleton/model/RepositoryInterface.tpl.php diff --git a/src/Maker/MakeModel.php b/src/Maker/MakeModel.php index e820577..bc4f386 100644 --- a/src/Maker/MakeModel.php +++ b/src/Maker/MakeModel.php @@ -7,6 +7,7 @@ use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ManagerRegistry; +use GeekCell\Ddd\Contracts\Domain\Repository; use GeekCell\Ddd\Domain\ValueObject\Id; use GeekCell\Ddd\Domain\ValueObject\Uuid; use GeekCell\DddBundle\Domain\AggregateRoot; @@ -29,6 +30,7 @@ use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; +use GeekCell\DddBundle\Infrastructure\Doctrine\Repository as OrmRepository; use function Symfony\Component\String\u; @@ -211,10 +213,10 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen $this->templateVariables['class_name'] = $modelClassNameDetails->getShortName(); - $this->generateIdentity($modelName, $input, $io, $generator); + $identityClassNameDetails = $this->generateIdentity($modelName, $input, $io, $generator); $this->generateEntityMappings($modelClassNameDetails, $input, $io, $generator); - $this->generateEntity($modelClassNameDetails, $input, $io, $generator); - $this->generateRepository($modelClassNameDetails, $input, $io, $generator); + $this->generateEntity($modelClassNameDetails, $input, $generator); + $this->generateRepository($modelClassNameDetails, $identityClassNameDetails, $input, $generator); $this->writeSuccessMessage($io); } @@ -226,15 +228,16 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen * @param InputInterface $input * @param ConsoleStyle $io * @param Generator $generator + * @return ClassNameDetails|null */ private function generateIdentity( string $modelName, InputInterface $input, ConsoleStyle $io, Generator $generator - ): void { + ): ?ClassNameDetails { if (!$this->shouldGenerateIdentity($input)) { - return; + return null; } // 1. Generate the identity value object. @@ -281,7 +284,7 @@ private function generateIdentity( $this->templateVariables['identity_class'] = $identityClassNameDetails->getShortName(); if (!$this->shouldGenerateEntity($input)) { - return; + return null; } // 2. Generate custom Doctrine mapping type for the identity. @@ -323,7 +326,7 @@ private function generateIdentity( $configPath = 'config/packages/doctrine.yaml'; if (!$this->fileManager->fileExists($configPath)) { $io->error(sprintf('Doctrine configuration at path "%s" does not exist.', $configPath)); - return; + return null; } // 2.1 Add the custom mapping type to the Doctrine configuration. @@ -341,6 +344,8 @@ private function generateIdentity( // Write out the changes. $generator->writeChanges(); + + return $identityClassNameDetails; } /** @@ -432,13 +437,11 @@ private function generateEntityMappings( * * @param ClassNameDetails $modelClassNameDetails * @param InputInterface $input - * @param ConsoleStyle $io * @param Generator $generator */ private function generateEntity( ClassNameDetails $modelClassNameDetails, InputInterface $input, - ConsoleStyle $io, Generator $generator ): void { if ($input->getOption('aggregate-root')) { @@ -462,33 +465,87 @@ private function generateEntity( /** * Generate model repository * + * @param ClassNameDetails $modelClassNameDetails + * @param ClassNameDetails $identityClassNameDetails * @param InputInterface $input - * @param ConsoleStyle $io * @param Generator $generator + * @throws \Exception */ private function generateRepository( ClassNameDetails $modelClassNameDetails, + ClassNameDetails $identityClassNameDetails, InputInterface $input, - ConsoleStyle $io, Generator $generator ): void { - $classNameDetails = $generator->createClassNameDetails( + $interfaceNameDetails = $generator->createClassNameDetails( $input->getArgument('name'), - 'Repository\\', + 'Domain\\Repository\\', 'Repository', ); + $this->generateRepositoryInterface( + $interfaceNameDetails, + $modelClassNameDetails, + $identityClassNameDetails, + $generator + ); + + $implementationNameDetails = $generator->createClassNameDetails( + $input->getArgument('name'), + 'Infrastructure\\Doctrine\\ORM\\Repository\\', + 'Repository', + ); + + $interfaceClassName = $interfaceNameDetails->getShortName() . 'Interface'; $templateVars = [ 'use_statements' => new UseStatementGenerator([ $modelClassNameDetails->getFullName(), - ServiceEntityRepository::class, + $identityClassNameDetails->getFullName(), ManagerRegistry::class, - QueryBuilder::class + [ OrmRepository::class => 'OrmRepository' ], + [ $interfaceNameDetails->getFullName() => $interfaceClassName ], ]), - 'entity_class_name' => $modelClassNameDetails->getShortName() + 'interface_class_name' => $interfaceClassName, + 'model_class_name' => $modelClassNameDetails->getShortName(), + 'identity_class_name' => $identityClassNameDetails->getShortName() ]; $templatePath = __DIR__.'/../Resources/skeleton/model/Repository.tpl.php'; + $generator->generateClass( + $implementationNameDetails->getFullName(), + $templatePath, + $templateVars, + ); + + $generator->writeChanges(); + } + + /** + * Generate model repository + * + * @param ClassNameDetails $classNameDetails + * @param ClassNameDetails $entityClassNameDetails + * @param ClassNameDetails $identityClassNameDetails + * @param Generator $generator + * @throws \Exception + */ + private function generateRepositoryInterface( + ClassNameDetails $classNameDetails, + ClassNameDetails $modelClassNameDetails, + ClassNameDetails $identityClassNameDetails, + Generator $generator + ): void { + $templateVars = [ + 'use_statements' => new UseStatementGenerator([ + $modelClassNameDetails->getFullName(), + $identityClassNameDetails->getFullName(), + Repository::class, + ]), + 'model_class_name' => $modelClassNameDetails->getShortName(), + 'identity_class_name' => $identityClassNameDetails->getShortName() + ]; + + $templatePath = __DIR__.'/../Resources/skeleton/model/RepositoryInterface.tpl.php'; $generator->generateClass( $classNameDetails->getFullName(), $templatePath, diff --git a/src/Resources/skeleton/model/Repository.tpl.php b/src/Resources/skeleton/model/Repository.tpl.php index 4a3c9d7..422a941 100644 --- a/src/Resources/skeleton/model/Repository.tpl.php +++ b/src/Resources/skeleton/model/Repository.tpl.php @@ -4,60 +4,21 @@ -class extends ServiceEntityRepository +class extends OrmRepository implements { - /** - * @extends ServiceEntityRepository<> - * - * @method |null find($id, $lockMode = null, $lockVersion = null) - * @method |null findOneBy(array $criteria, array $orderBy = null) - * @method [] findAll() - * @method [] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) - */ - public function __construct(ManagerRegistry $registry) + public function findById( $id): ? { - parent::__construct($registry, ::class); + return null; } - public function save( $entity, bool $flush = false): void + public function findByExampleField($value): self { - $this->getEntityManager()->persist($entity); - - if ($flush) { - $this->getEntityManager()->flush(); - } + return $this->filter(function(QueryBuilder $queryBuilder) use ($value) { + $queryBuilder + ->andWhere('t.exampleField = :val') + ->setParameter('val', $value) + ->orderBy('t.id', 'ASC') + ; + }); } - - public function remove( $entity, bool $flush = false): void - { - $this->getEntityManager()->remove($entity); - - if ($flush) { - $this->getEntityManager()->flush(); - } - } - -// /** -// * @return [] Returns an array of objects -// */ -// public function findByExampleField($value): array -// { -// return $this->filter(function(QueryBuilder $queryBuilder) use ($value) { -// $queryBuilder -// ->andWhere('t.exampleField = :val') -// ->setParamter('val', $value) -// ->orderBy('t.id', 'ASC') -// ; -// }); -// } - -// public function findOneBySomeField($value): ? -// { -// return $this->createQueryBuilder('t') -// ->andWhere('t.exampleField = :val') -// ->setParameter('val', $value) -// ->getQuery() -// ->getOneOrNullResult() -// ; -// } } diff --git a/src/Resources/skeleton/model/RepositoryInterface.tpl.php b/src/Resources/skeleton/model/RepositoryInterface.tpl.php new file mode 100644 index 0000000..7acbb9f --- /dev/null +++ b/src/Resources/skeleton/model/RepositoryInterface.tpl.php @@ -0,0 +1,10 @@ + + +namespace ; + + + +interface extends Repository +{ + public function findById( $id): ?; +} From e8a51e8ce9fba2c32281d304a7d79f8c26382bf5 Mon Sep 17 00:00:00 2001 From: janvt Date: Fri, 17 Mar 2023 14:11:46 +0100 Subject: [PATCH 37/44] add QueryBuilder dependency to use classes --- src/Maker/MakeModel.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Maker/MakeModel.php b/src/Maker/MakeModel.php index bc4f386..b503710 100644 --- a/src/Maker/MakeModel.php +++ b/src/Maker/MakeModel.php @@ -502,6 +502,7 @@ private function generateRepository( $modelClassNameDetails->getFullName(), $identityClassNameDetails->getFullName(), ManagerRegistry::class, + QueryBuilder::class, [ OrmRepository::class => 'OrmRepository' ], [ $interfaceNameDetails->getFullName() => $interfaceClassName ], ]), From ebd9100035915afdacf11478bdb1270bec850cf0 Mon Sep 17 00:00:00 2001 From: janvt Date: Mon, 20 Mar 2023 16:49:42 +0100 Subject: [PATCH 38/44] add resource identifier --- src/Maker/MakeResource.php | 43 +++++++++++++++++-- .../skeleton/resource/Resource.tpl.php | 9 ++++ 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/src/Maker/MakeResource.php b/src/Maker/MakeResource.php index 5e31319..2fbabd4 100644 --- a/src/Maker/MakeResource.php +++ b/src/Maker/MakeResource.php @@ -4,6 +4,7 @@ namespace GeekCell\DddBundle\Maker; +use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProcessorInterface; @@ -31,7 +32,7 @@ final class MakeResource extends AbstractMaker implements InputAwareMakerInterfa { const NAMESPACE_PREFIX = 'Infrastructure\\ApiPlatform\\'; const CONFIG_PATH = 'config/packages/api_platform.yaml'; - const CONFIG_PATH_XML = 'config/api_platform/'; + const CONFIG_PATH_XML = 'src/Infrastructure/ApiPlatform/Config'; const CONFIG_FLAVOR_ATTRIBUTE = 'attribute'; const CONFIG_FLAVOR_XML = 'xml'; @@ -127,6 +128,8 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen throw new RuntimeCommandException("Could not find model {$modelClassNameDetails->getFullName()}!"); } + $identityClassNameDetails = $this->ensureIdentity($modelClassNameDetails, $generator); + $classNameDetails = $generator->createClassNameDetails( $baseName, self::NAMESPACE_PREFIX . 'Resource', @@ -152,16 +155,19 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen $classesToImport = [$modelClassNameDetails->getFullName()]; if ($configFlavor === self::CONFIG_FLAVOR_ATTRIBUTE) { $classesToImport[] = ApiResource::class; + $classesToImport[] = ApiProperty::class; $classesToImport[] = $providerClassNameDetails->getFullName(); $classesToImport[] = $processorClassNameDetails->getFullName(); } + $configureWithUuid = str_contains(strtolower($identityClassNameDetails->getShortName()), 'uuid'); $templateVars = [ 'use_statements' => new UseStatementGenerator($classesToImport), 'entity_class_name' => $modelClassNameDetails->getShortName(), 'provider_class_name' => $providerClassNameDetails->getShortName(), 'processor_class_name' => $processorClassNameDetails->getShortName(), - 'configure_with_attributes' => $configFlavor === self::CONFIG_FLAVOR_ATTRIBUTE + 'configure_with_attributes' => $configFlavor === self::CONFIG_FLAVOR_ATTRIBUTE, + 'configure_with_uuid' => $configureWithUuid, ]; $generator->generateClass( @@ -171,7 +177,7 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen ); if ($configFlavor === self::CONFIG_FLAVOR_XML) { - $targetPath = self::CONFIG_PATH_XML . $classNameDetails->getShortName() . '.xml'; + $targetPath = self::CONFIG_PATH_XML . '/' . $classNameDetails->getShortName() . '.xml'; $generator->generateFile( $targetPath, __DIR__.'/../Resources/skeleton/resource/ResourceXmlConfig.tpl.php', @@ -179,6 +185,7 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen 'entity_full_class_name' => $modelClassNameDetails->getFullName(), 'provider_class_name' => $providerClassNameDetails->getFullName(), 'processor_class_name' => $processorClassNameDetails->getFullName(), + 'identifier_field_name' => $configureWithUuid ? 'uuid' : 'id', ] ); } @@ -273,4 +280,34 @@ private function generateProcessor(ClassNameDetails $processorClassNameDetails, $generator->writeChanges(); } + + /** + * @param ClassNameDetails $modelClassNameDetails + * @param Generator $generator + * @return ClassNameDetails + */ + private function ensureIdentity(ClassNameDetails $modelClassNameDetails, Generator $generator): ClassNameDetails + { + $idEntity = $generator->createClassNameDetails( + $modelClassNameDetails->getShortName(), + 'Domain\\Model\\ValueObject\\Identity', + 'Id', + ); + + if (class_exists($idEntity->getFullName())) { + return $idEntity; + } + + $uuidEntity = $generator->createClassNameDetails( + $modelClassNameDetails->getShortName(), + 'Domain\\Model\\ValueObject\\Identity', + 'Uuid', + ); + + if (class_exists($uuidEntity->getFullName())) { + return $uuidEntity; + } + + throw new RuntimeCommandException("Could not find model identity for {$modelClassNameDetails->getFullName()}. Checked for id class ({$idEntity->getFullName()}) and uuid class ({$uuidEntity->getFullName()})!"); + } } diff --git a/src/Resources/skeleton/resource/Resource.tpl.php b/src/Resources/skeleton/resource/Resource.tpl.php index 4e6744f..4257443 100644 --- a/src/Resources/skeleton/resource/Resource.tpl.php +++ b/src/Resources/skeleton/resource/Resource.tpl.php @@ -12,6 +12,15 @@ final class { + + #[ApiProperty(identifier: true)] + + + public string $uuid; + + public int $id; + + /** * Convenience factory method to create the resource from an instance of the model * From eb695d5b170c9cc8f1309c1372af98fa2e14c09d Mon Sep 17 00:00:00 2001 From: janvt Date: Tue, 21 Mar 2023 13:10:26 +0100 Subject: [PATCH 39/44] fix xml config generation --- src/Maker/MakeResource.php | 16 +++++++++++++--- .../resource/PropertiesXmlConfig.tpl.php | 4 ++++ src/Resources/skeleton/resource/Resource.tpl.php | 9 ++++++--- .../skeleton/resource/ResourceXmlConfig.tpl.php | 3 ++- 4 files changed, 25 insertions(+), 7 deletions(-) create mode 100644 src/Resources/skeleton/resource/PropertiesXmlConfig.tpl.php diff --git a/src/Maker/MakeResource.php b/src/Maker/MakeResource.php index 2fbabd4..517a9e1 100644 --- a/src/Maker/MakeResource.php +++ b/src/Maker/MakeResource.php @@ -177,14 +177,24 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen ); if ($configFlavor === self::CONFIG_FLAVOR_XML) { - $targetPath = self::CONFIG_PATH_XML . '/' . $classNameDetails->getShortName() . '.xml'; + $targetPathResourceConfig = self::CONFIG_PATH_XML . '/' . $classNameDetails->getShortName() . '.xml'; $generator->generateFile( - $targetPath, + $targetPathResourceConfig, __DIR__.'/../Resources/skeleton/resource/ResourceXmlConfig.tpl.php', [ - 'entity_full_class_name' => $modelClassNameDetails->getFullName(), + 'class_name' => $classNameDetails->getFullName(), + 'entity_short_class_name' => $modelClassNameDetails->getShortName(), 'provider_class_name' => $providerClassNameDetails->getFullName(), 'processor_class_name' => $processorClassNameDetails->getFullName(), + ] + ); + + $targetPathPropertiesConfig = self::CONFIG_PATH_XML . '/' . $classNameDetails->getShortName() . 'Properties.xml'; + $generator->generateFile( + $targetPathPropertiesConfig, + __DIR__.'/../Resources/skeleton/resource/PropertiesXmlConfig.tpl.php', + [ + 'class_name' => $classNameDetails->getFullName(), 'identifier_field_name' => $configureWithUuid ? 'uuid' : 'id', ] ); diff --git a/src/Resources/skeleton/resource/PropertiesXmlConfig.tpl.php b/src/Resources/skeleton/resource/PropertiesXmlConfig.tpl.php new file mode 100644 index 0000000..3e71f18 --- /dev/null +++ b/src/Resources/skeleton/resource/PropertiesXmlConfig.tpl.php @@ -0,0 +1,4 @@ + + + + diff --git a/src/Resources/skeleton/resource/Resource.tpl.php b/src/Resources/skeleton/resource/Resource.tpl.php index 4257443..348cc51 100644 --- a/src/Resources/skeleton/resource/Resource.tpl.php +++ b/src/Resources/skeleton/resource/Resource.tpl.php @@ -12,14 +12,17 @@ final class { + public function __construct( - #[ApiProperty(identifier: true)] + #[ApiProperty(identifier: true)] - public string $uuid; + public string $uuid, - public int $id; + public int $id, + // TODO: Add more properties ... + ) {} /** * Convenience factory method to create the resource from an instance of the model diff --git a/src/Resources/skeleton/resource/ResourceXmlConfig.tpl.php b/src/Resources/skeleton/resource/ResourceXmlConfig.tpl.php index f7949c2..3209b8d 100644 --- a/src/Resources/skeleton/resource/ResourceXmlConfig.tpl.php +++ b/src/Resources/skeleton/resource/ResourceXmlConfig.tpl.php @@ -4,7 +4,8 @@ xsi:schemaLocation="https://api-platform.com/schema/metadata/resources-3.0 https://api-platform.com/schema/metadata/resources-3.0.xsd"> From 35b5ceb90341fe5cfde5d58bb812f0bc05cd620d Mon Sep 17 00:00:00 2001 From: janvt Date: Tue, 21 Mar 2023 13:11:53 +0100 Subject: [PATCH 40/44] rename base CQRS command --- ...ractBaseMakeQueryCommand.php => AbstractBaseMakerCQRS.php} | 4 ++-- src/Maker/MakeCommand.php | 2 +- src/Maker/MakeQuery.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename src/Maker/{AbstractBaseMakeQueryCommand.php => AbstractBaseMakerCQRS.php} (97%) diff --git a/src/Maker/AbstractBaseMakeQueryCommand.php b/src/Maker/AbstractBaseMakerCQRS.php similarity index 97% rename from src/Maker/AbstractBaseMakeQueryCommand.php rename to src/Maker/AbstractBaseMakerCQRS.php index 44767dd..7b99eb5 100644 --- a/src/Maker/AbstractBaseMakeQueryCommand.php +++ b/src/Maker/AbstractBaseMakerCQRS.php @@ -16,7 +16,7 @@ use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; -abstract class AbstractBaseMakeQueryCommand extends AbstractMaker implements InputAwareMakerInterface +abstract class AbstractBaseMakerCQRS extends AbstractMaker implements InputAwareMakerInterface { /** * Should return the target for the extending command (query|command) @@ -27,7 +27,7 @@ abstract function getTarget(): string; /** * Should return an array of classes to import when generating the entity * @return string[] - */ + */g abstract function getEntityUseStatements(): array; /** diff --git a/src/Maker/MakeCommand.php b/src/Maker/MakeCommand.php index bdaa81f..d778092 100644 --- a/src/Maker/MakeCommand.php +++ b/src/Maker/MakeCommand.php @@ -9,7 +9,7 @@ use GeekCell\Ddd\Domain\Collection; use Symfony\Component\Messenger\Attribute\AsMessageHandler; -final class MakeCommand extends AbstractBaseMakeQueryCommand +final class MakeCommand extends AbstractBaseMakerCQRS { const TARGET = 'command'; diff --git a/src/Maker/MakeQuery.php b/src/Maker/MakeQuery.php index 97e9b6d..4963553 100644 --- a/src/Maker/MakeQuery.php +++ b/src/Maker/MakeQuery.php @@ -9,7 +9,7 @@ use GeekCell\Ddd\Domain\Collection; use Symfony\Component\Messenger\Attribute\AsMessageHandler; -final class MakeQuery extends AbstractBaseMakeQueryCommand +final class MakeQuery extends AbstractBaseMakerCQRS { const TARGET = 'query'; From 35c17b8847cecbb3037c1ddb04b4010299a6e489 Mon Sep 17 00:00:00 2001 From: Pascal Cremer Date: Tue, 21 Mar 2023 14:02:13 +0100 Subject: [PATCH 41/44] fix: Remove unused character. --- src/Maker/AbstractBaseMakerCQRS.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Maker/AbstractBaseMakerCQRS.php b/src/Maker/AbstractBaseMakerCQRS.php index 7b99eb5..077f111 100644 --- a/src/Maker/AbstractBaseMakerCQRS.php +++ b/src/Maker/AbstractBaseMakerCQRS.php @@ -27,7 +27,7 @@ abstract function getTarget(): string; /** * Should return an array of classes to import when generating the entity * @return string[] - */g + */ abstract function getEntityUseStatements(): array; /** From 5c5e38c326e604006fa0e33ca9948365fe6b930a Mon Sep 17 00:00:00 2001 From: Pascal Cremer Date: Tue, 21 Mar 2023 15:02:40 +0100 Subject: [PATCH 42/44] chore: Add small changes to generated code. --- .../skeleton/model/Repository.tpl.php | 22 ++++++++++--------- .../skeleton/resource/Resource.tpl.php | 9 +++++++- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/Resources/skeleton/model/Repository.tpl.php b/src/Resources/skeleton/model/Repository.tpl.php index 422a941..56b2ddd 100644 --- a/src/Resources/skeleton/model/Repository.tpl.php +++ b/src/Resources/skeleton/model/Repository.tpl.php @@ -8,17 +8,19 @@ class extends OrmRepository implements $id): ? { + // TODO: Implement me! + return null; } - public function findByExampleField($value): self - { - return $this->filter(function(QueryBuilder $queryBuilder) use ($value) { - $queryBuilder - ->andWhere('t.exampleField = :val') - ->setParameter('val', $value) - ->orderBy('t.id', 'ASC') - ; - }); - } + // public function findByExampleField($value): self + // { + // return $this->filter(function(QueryBuilder $queryBuilder) use ($value) { + // $queryBuilder + // ->andWhere('t.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('t.id', 'ASC') + // ; + // }); + // } } diff --git a/src/Resources/skeleton/resource/Resource.tpl.php b/src/Resources/skeleton/resource/Resource.tpl.php index 348cc51..8b81f84 100644 --- a/src/Resources/skeleton/resource/Resource.tpl.php +++ b/src/Resources/skeleton/resource/Resource.tpl.php @@ -33,6 +33,13 @@ public function __construct( */ public static function create( $model): static { - return new static(); + return new static( + + strval($model->getUuid()), + + intval($model->getId()), + + // TODO: Initialize further ... + ); } } From 09a5436cc7d0be3ba1e7fc2f98178caae4ff77d0 Mon Sep 17 00:00:00 2001 From: janvt Date: Mon, 27 Mar 2023 14:15:25 +0200 Subject: [PATCH 43/44] fix command option defaults --- src/Maker/MakeController.php | 8 ++++---- src/Maker/MakeModel.php | 9 +++++++-- src/Maker/MakeResource.php | 3 ++- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/Maker/MakeController.php b/src/Maker/MakeController.php index 7404c95..c52caa8 100644 --- a/src/Maker/MakeController.php +++ b/src/Maker/MakeController.php @@ -64,14 +64,14 @@ public function configureCommand(Command $command, InputConfiguration $inputConf null, InputOption::VALUE_REQUIRED, 'Add a query bus dependency.', - false + null ) ->addOption( 'include-command-bus', null, InputOption::VALUE_REQUIRED, 'Add a command bus dependency.', - false + null ) ; } @@ -88,7 +88,7 @@ public function configureDependencies(DependencyBuilder $dependencies, InputInte */ public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void { - if (false === $input->getOption('include-query-bus')) { + if (null === $input->getOption('include-query-bus')) { $includeQueryBus = $io->confirm( 'Do you want to add a query bus dependency?', false, @@ -96,7 +96,7 @@ public function interact(InputInterface $input, ConsoleStyle $io, Command $comma $input->setOption('include-query-bus', $includeQueryBus); } - if (false === $input->getOption('include-command-bus')) { + if (null === $input->getOption('include-command-bus')) { $includeCommandBus = $io->confirm( 'Do you want to add a command bus dependency?', false, diff --git a/src/Maker/MakeModel.php b/src/Maker/MakeModel.php index b503710..a26d8e3 100644 --- a/src/Maker/MakeModel.php +++ b/src/Maker/MakeModel.php @@ -89,24 +89,28 @@ public function configureCommand(Command $command, InputConfiguration $inputConf null, InputOption::VALUE_REQUIRED, 'Marks the model as aggregate root', + null ) ->addOption( 'entity', null, InputOption::VALUE_REQUIRED, 'Use this model as Doctrine entity', + null ) ->addOption( 'with-identity', null, InputOption::VALUE_REQUIRED, 'Whether an identity value object should be created', + null ) ->addOption( 'with-suffix', null, InputOption::VALUE_REQUIRED, 'Adds the suffix "Model" to the model class name', + null ) ; } @@ -135,7 +139,8 @@ public function interact(InputInterface $input, ConsoleStyle $io, Command $comma /** @var string $modelName */ $modelName = $input->getArgument('name'); - if (false === $input->getOption('with-suffix')) { + $useSuffix = $input->getOption('with-suffix'); + if (null === $useSuffix) { $useSuffix = $io->confirm( sprintf( 'Do you want to suffix the model class name? (%sModel)', @@ -146,7 +151,7 @@ public function interact(InputInterface $input, ConsoleStyle $io, Command $comma $input->setOption('with-suffix', $useSuffix); } - if (false === $input->getOption('aggregate-root')) { + if (null === $input->getOption('aggregate-root')) { $asAggregateRoot = $io->confirm( sprintf( 'Do you want create %s%s as aggregate root?', diff --git a/src/Maker/MakeResource.php b/src/Maker/MakeResource.php index 517a9e1..d17f02f 100644 --- a/src/Maker/MakeResource.php +++ b/src/Maker/MakeResource.php @@ -74,7 +74,7 @@ public function configureCommand(Command $command, InputConfiguration $inputConf null, InputOption::VALUE_REQUIRED, 'Config flavor to create (attribute|xml).', - 'attribute' + null ) ; } @@ -105,6 +105,7 @@ public function interact(InputInterface $input, ConsoleStyle $io, Command $comma 'attribute' => 'PHP attributes', 'xml' => 'XML mapping', ], + 'attribute' ); $input->setOption('config', $configFlavor); } From 6c6c3c88cfecd4448cd09e82e50c760afbdd5469 Mon Sep 17 00:00:00 2001 From: janvt Date: Mon, 27 Mar 2023 15:36:04 +0200 Subject: [PATCH 44/44] pass back test kernel in integration test --- tests/Integration/Domain/AggregateRootTest.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/Integration/Domain/AggregateRootTest.php b/tests/Integration/Domain/AggregateRootTest.php index 7408297..753c6c2 100644 --- a/tests/Integration/Domain/AggregateRootTest.php +++ b/tests/Integration/Domain/AggregateRootTest.php @@ -7,12 +7,19 @@ use GeekCell\DddBundle\Tests\Integration\Fixtures\Domain\Event\UserStateChangedEvent; use GeekCell\DddBundle\Tests\Integration\Fixtures\Domain\Event\UserUpdatedEvent; use GeekCell\DddBundle\Tests\Integration\Fixtures\Domain\Model\User; +use GeekCell\DddBundle\Tests\Integration\Fixtures\TestKernel; use GeekCell\Facade\Facade; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpKernel\KernelInterface; class AggregateRootTest extends KernelTestCase { + protected static function createKernel(array $options = []): KernelInterface + { + return new TestKernel('test', true); + } + public function setUp(): void { parent::setUp();