diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..fb115d5 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,14 @@ +name: Tests + +on: [push] + +jobs: + unit-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: php-actions/composer@v6 + - name: PHPUnit Tests + uses: php-actions/phpunit@v9 + with: + configuration: tests/phpunit.xml 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/README.md b/README.md index e9129bf..fcd3912 100644 --- a/README.md +++ b/README.md @@ -153,3 +153,99 @@ 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 +``` + +### 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) +``` + +### 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 +``` + +### 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). +``` diff --git a/composer.json b/composer.json index 35cd08f..0175feb 100644 --- a/composer.json +++ b/composer.json @@ -29,11 +29,12 @@ "phpunit/phpunit": "^9.5", "symfony/framework-bundle": "^6.0", "symfony/yaml": "^6.0", - "symfony/filesystem": "^6.0" + "symfony/filesystem": "^6.0", + "symfony/maker-bundle": "^1.48" }, "autoload": { "psr-4": { - "GeekCell\\DddBundle\\": "src" + "GeekCell\\DddBundle\\": "src/" } }, "autoload-dev": { diff --git a/composer.lock b/composer.lock index 7e2f900..9d58003 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": "691a2195655795e1b1ac96f61ef6ade8", + "content-hash": "8b5e75343626ca72110baba2e8256803", "packages": [ { "name": "beberlei/assert", @@ -3590,16 +3590,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.14.4", + "version": "v3.15.1", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "1b3d9dba63d93b8a202c31e824748218781eae6b" + "reference": "d48755372a113bddb99f749e34805d83f3acfe04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/1b3d9dba63d93b8a202c31e824748218781eae6b", - "reference": "1b3d9dba63d93b8a202c31e824748218781eae6b", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/d48755372a113bddb99f749e34805d83f3acfe04", + "reference": "d48755372a113bddb99f749e34805d83f3acfe04", "shasum": "" }, "require": { @@ -3666,9 +3666,15 @@ } ], "description": "A tool to automatically fix PHP code style", + "keywords": [ + "Static code analysis", + "fixer", + "standards", + "static analysis" + ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.14.4" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.15.1" }, "funding": [ { @@ -3676,7 +3682,7 @@ "type": "github" } ], - "time": "2023-02-09T21:49:13+00:00" + "time": "2023-03-13T23:26:30+00:00" }, { "name": "hamcrest/hamcrest-php", @@ -5910,6 +5916,99 @@ ], "time": "2023-02-24T10:42:00+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.7", diff --git a/config/services.yaml b/config/services.yaml index 461b5d4..5fc81c3 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -10,3 +10,44 @@ services: arguments: - '@Symfony\Component\Messenger\MessageBusInterface' 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\MakeQuery: + class: GeekCell\DddBundle\Maker\MakeQuery + tags: + - { name: maker.command } + + GeekCell\DddBundle\Maker\MakeCommand: + class: GeekCell\DddBundle\Maker\MakeCommand + tags: + - { name: maker.command } + + GeekCell\DddBundle\Maker\MakeController: + class: GeekCell\DddBundle\Maker\MakeController + arguments: + - '@maker.file_manager' + 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/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/ 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/AbstractBaseMakerCQRS.php b/src/Maker/AbstractBaseMakerCQRS.php new file mode 100644 index 0000000..077f111 --- /dev/null +++ b/src/Maker/AbstractBaseMakerCQRS.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/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 new file mode 100644 index 0000000..37ce45e --- /dev/null +++ b/src/Maker/Doctrine/DoctrineConfigUpdater.php @@ -0,0 +1,51 @@ +read($yamlSource); + $data['doctrine']['dbal']['types'][$identifier] = $mappingClass; + + return $this->write($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', 'attribute'], 'Invalid mapping type: %s'); + + $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->write($data); + } +} diff --git a/src/Maker/MakeCommand.php b/src/Maker/MakeCommand.php new file mode 100644 index 0000000..d778092 --- /dev/null +++ b/src/Maker/MakeCommand.php @@ -0,0 +1,60 @@ +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.', + null + ) + ->addOption( + 'include-command-bus', + null, + InputOption::VALUE_REQUIRED, + 'Add a command bus dependency.', + null + ) + ; + } + + /** + * @inheritDoc + */ + public function configureDependencies(DependencyBuilder $dependencies, InputInterface $input = null): void + { + } + + /** + * @inheritDoc + */ + public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void + { + if (null === $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 (null === $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' => u($routeName)->snake()->lower(), + '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 new file mode 100644 index 0000000..a26d8e3 --- /dev/null +++ b/src/Maker/MakeModel.php @@ -0,0 +1,637 @@ +> + */ + private $classesToImport = []; + + /** + * @var array + */ + private $templateVariables = []; + + /** + * @param DoctrineConfigUpdater $doctrineUpdater + * @param FileManager $fileManager + */ + public function __construct( + private readonly DoctrineConfigUpdater $doctrineUpdater, + private readonly 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 + ->addArgument( + 'name', + InputArgument::REQUIRED, + 'The name of the model class (e.g. Customer)', + ) + ->addOption( + 'aggregate-root', + 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 + ) + ; + } + + /** + * @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 + { + 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 = $input->getOption('with-suffix'); + if (null === $useSuffix) { + $useSuffix = $io->confirm( + sprintf( + 'Do you want to suffix the model class name? (%sModel)', + $modelName, + ), + false, + ); + $input->setOption('with-suffix', $useSuffix); + } + + if (null === $input->getOption('aggregate-root')) { + $asAggregateRoot = $io->confirm( + sprintf( + 'Do you want create %s%s as aggregate root?', + $modelName, + $useSuffix ? 'Model' : '', + ), + ); + $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, + ), + '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' : ''; + + $modelClassNameDetails = $generator->createClassNameDetails( + $modelName, + 'Domain\\Model\\', + $suffix, + ); + + $this->templateVariables['class_name'] = $modelClassNameDetails->getShortName(); + + $identityClassNameDetails = $this->generateIdentity($modelName, $input, $io, $generator); + $this->generateEntityMappings($modelClassNameDetails, $input, $io, $generator); + $this->generateEntity($modelClassNameDetails, $input, $generator); + $this->generateRepository($modelClassNameDetails, $identityClassNameDetails, $input, $generator); + + $this->writeSuccessMessage($io); + } + + /** + * Optionally, generate the identity value object for the model. + * + * @param string $modelName + * @param InputInterface $input + * @param ConsoleStyle $io + * @param Generator $generator + * @return ClassNameDetails|null + */ + private function generateIdentity( + string $modelName, + InputInterface $input, + ConsoleStyle $io, + Generator $generator + ): ?ClassNameDetails { + if (!$this->shouldGenerateIdentity($input)) { + return null; + } + + // 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 null; + } + + // 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 null; + } + + // 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->classesToImport[] = $mappingTypeClassNameDetails->getFullName(); + $this->templateVariables['type_class'] = $mappingTypeClassNameDetails->getShortName(); + $this->templateVariables['type_name'] = $typeName; + + // Write out the changes. + $generator->writeChanges(); + + return $identityClassNameDetails; + } + + /** + * Optionally, generate entity mappings for the model. + * + * @param ClassNameDetails $modelClassNameDetails + * @param InputInterface $input + * @param ConsoleStyle $io + * @param Generator $generator + */ + private function generateEntityMappings( + ClassNameDetails $modelClassNameDetails, + InputInterface $input, + ConsoleStyle $io, + Generator $generator + ): void { + if (!$this->shouldGenerateEntity($input)) { + return; + } + + $modelName = $modelClassNameDetails->getShortName(); + + if ($this->shouldGenerateEntityAttributes($input)) { + try { + $newYaml = $this->doctrineUpdater->updateORMDefaultEntityMapping( + $this->fileManager->getFileContents(DOCTRINE_CONFIG_PATH), + 'attribute', + '%kernel.project_dir%/src/Domain/Model', + ); + $generator->dumpFile(DOCTRINE_CONFIG_PATH, $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(DOCTRINE_CONFIG_PATH), + 'xml', + '%kernel.project_dir%'.$mappingsDirectory, + ); + $generator->dumpFile(DOCTRINE_CONFIG_PATH, $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(); + } + + /** + * Generate model entity + * + * @param ClassNameDetails $modelClassNameDetails + * @param InputInterface $input + * @param Generator $generator + */ + private function generateEntity( + ClassNameDetails $modelClassNameDetails, + InputInterface $input, + 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 ClassNameDetails $modelClassNameDetails + * @param ClassNameDetails $identityClassNameDetails + * @param InputInterface $input + * @param Generator $generator + * @throws \Exception + */ + private function generateRepository( + ClassNameDetails $modelClassNameDetails, + ClassNameDetails $identityClassNameDetails, + InputInterface $input, + Generator $generator + ): void { + $interfaceNameDetails = $generator->createClassNameDetails( + $input->getArgument('name'), + '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(), + $identityClassNameDetails->getFullName(), + ManagerRegistry::class, + QueryBuilder::class, + [ OrmRepository::class => 'OrmRepository' ], + [ $interfaceNameDetails->getFullName() => $interfaceClassName ], + ]), + '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, + $templateVars, + ); + + $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/Maker/MakeQuery.php b/src/Maker/MakeQuery.php new file mode 100644 index 0000000..4963553 --- /dev/null +++ b/src/Maker/MakeQuery.php @@ -0,0 +1,61 @@ +addArgument( + 'name', + 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).', + null + ) + ; + } + + /** + * @inheritDoc + */ + public function configureDependencies(DependencyBuilder $dependencies, InputInterface $input = null): void + { + } + + /** + * @inheritDoc + */ + public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void + { + // 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.'); + } + + if (false === $input->getOption('config')) { + $configFlavor = $io->choice( + 'Config flavor to create (attribute|xml). (%sModel)', + [ + 'attribute' => 'PHP attributes', + 'xml' => 'XML mapping', + ], + 'attribute' + ); + $input->setOption('config', $configFlavor); + } + } + + /** + * @inheritDoc + */ + public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void + { + $baseName = $input->getArgument('name'); + $configFlavor = $input->getOption('config'); + + $modelClassNameDetails = $generator->createClassNameDetails( + $baseName, + 'Domain\\Model\\', + '', + ); + + if (!class_exists($modelClassNameDetails->getFullName())) { + throw new RuntimeCommandException("Could not find model {$modelClassNameDetails->getFullName()}!"); + } + + $identityClassNameDetails = $this->ensureIdentity($modelClassNameDetails, $generator); + + $classNameDetails = $generator->createClassNameDetails( + $baseName, + self::NAMESPACE_PREFIX . 'Resource', + 'Resource', + ); + + $this->ensureConfig($generator, $configFlavor); + + $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); + + $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_uuid' => $configureWithUuid, + ]; + + $generator->generateClass( + $classNameDetails->getFullName(), + __DIR__.'/../Resources/skeleton/resource/Resource.tpl.php', + $templateVars, + ); + + if ($configFlavor === self::CONFIG_FLAVOR_XML) { + $targetPathResourceConfig = self::CONFIG_PATH_XML . '/' . $classNameDetails->getShortName() . '.xml'; + $generator->generateFile( + $targetPathResourceConfig, + __DIR__.'/../Resources/skeleton/resource/ResourceXmlConfig.tpl.php', + [ + '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', + ] + ); + } + + $generator->writeChanges(); + + $this->writeSuccessMessage($io); + } + + /** + * ensure custom resource path(s) are added to config + * + * @param Generator $generator + * @param string $configFlavor + * @return void + */ + private function ensureConfig(Generator $generator, string $configFlavor): void + { + $customResourcePath = '%kernel.project_dir%/src/Infrastructure/ApiPlatform/Resource'; + $customConfigPath = '%kernel.project_dir%/' . self::CONFIG_PATH_XML; + + if (!$this->fileManager->fileExists(self::CONFIG_PATH)) { + $generator->generateFile( + self::CONFIG_PATH, + __DIR__ . '/../Resources/skeleton/resource/ApiPlatformConfig.tpl.php', + [ + '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(); + } + + /** + * @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(); + } + + /** + * @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/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..eed3eed --- /dev/null +++ b/src/Resources/skeleton/command/CommandHandler.tpl.php @@ -0,0 +1,13 @@ + + +namespace ; + + + +#[AsMessageHandler] +class implements CommandHandler +{ + public function __invoke( $query): void + { + } +} 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 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..b8fbf60 --- /dev/null +++ b/src/Resources/skeleton/doctrine/Mapping.tpl.xml.php @@ -0,0 +1,13 @@ + + + + + + + + + + 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/model/Identity.tpl.php b/src/Resources/skeleton/model/Identity.tpl.php new file mode 100644 index 0000000..d416ee4 --- /dev/null +++ b/src/Resources/skeleton/model/Identity.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..ce94ab3 --- /dev/null +++ b/src/Resources/skeleton/model/Model.tpl.php @@ -0,0 +1,43 @@ + + +namespace ; + + + + +#[ORM\Entity] + +class extends AggregateRoot +{ + + /** + * @var + */ + + #[ORM\Id] + #[ORM\Column(type: ::NAME)] + + private $; + + + public function __construct() + { + + $this->id = new (1); + + $this->uuid = ::random(); + + } + + + /** + * Get + * + * @return + */ + public function get(): + { + return $this->; + } + +} diff --git a/src/Resources/skeleton/model/Repository.tpl.php b/src/Resources/skeleton/model/Repository.tpl.php new file mode 100644 index 0000000..56b2ddd --- /dev/null +++ b/src/Resources/skeleton/model/Repository.tpl.php @@ -0,0 +1,26 @@ + + +namespace ; + + + +class extends OrmRepository implements +{ + public function findById( $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') + // ; + // }); + // } +} 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): ?; +} 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..b049bdb --- /dev/null +++ b/src/Resources/skeleton/query/QueryHandler.tpl.php @@ -0,0 +1,13 @@ + + +namespace ; + + + +#[AsMessageHandler] +class implements QueryHandler +{ + public function __invoke( $query): Collection + { + } +} 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..d5e29e0 --- /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 = []): void + { + } +} 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/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..8b81f84 --- /dev/null +++ b/src/Resources/skeleton/resource/Resource.tpl.php @@ -0,0 +1,45 @@ + + +namespace ; + + + + +#[ApiResource( + provider: ::class, + processor: ::class, +)] + +final class +{ + public function __construct( + + #[ApiProperty(identifier: true)] + + + public string $uuid, + + public int $id, + + // TODO: Add more properties ... + ) {} + + /** + * 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( + + strval($model->getUuid()), + + intval($model->getId()), + + // TODO: Initialize further ... + ); + } +} diff --git a/src/Resources/skeleton/resource/ResourceXmlConfig.tpl.php b/src/Resources/skeleton/resource/ResourceXmlConfig.tpl.php new file mode 100644 index 0000000..3209b8d --- /dev/null +++ b/src/Resources/skeleton/resource/ResourceXmlConfig.tpl.php @@ -0,0 +1,12 @@ + + + + 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(); 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 @@ + + + + + . + + + +