From 0c7f247ca92ca10bd58fa1b5a740b78fc00a7caf Mon Sep 17 00:00:00 2001 From: Pascal Cremer Date: Sat, 21 Jan 2023 01:22:54 +0100 Subject: [PATCH 01/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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 3c3fb35c9dc92fc0b99c1efd005c13980895f38e Mon Sep 17 00:00:00 2001 From: janvt Date: Fri, 10 Mar 2023 11:40:29 +0100 Subject: [PATCH 22/22] 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')