diff --git a/README.md b/README.md index 8960441..fcd3912 100644 --- a/README.md +++ b/README.md @@ -229,3 +229,23 @@ Options: --include-query-bus Add a query bus dependency --include-command-bus Add a command bus dependency ``` + +### Resource + +This command can be used to generate an [Api Platform](https://api-platform.com/) resource. Minimum required version is [2.7](https://api-platform.com/docs/core/upgrade-guide/#api-platform-2730) for the PHP attributes support. + +#### Command Output + +```bash +Description: + Creates a new API Platform resource + +Usage: + make:ddd:resource [options] [--] [] + +Arguments: + name The name of the model class to create the resource for (e.g. Customer). Model must exist already. + +Options: + --config Config flavor to create (attribute|xml). +``` diff --git a/config/services.yaml b/config/services.yaml index 542a618..0ea7601 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -42,6 +42,18 @@ services: tags: - { name: maker.command } + GeekCell\DddBundle\Maker\MakeResource: + class: GeekCell\DddBundle\Maker\MakeResource + arguments: + - '@maker.file_manager' + - '@GeekCell\DddBundle\Maker\Doctrine\ApiPlatformConfigUpdator' + tags: + - { name: maker.command } + GeekCell\DddBundle\Maker\Doctrine\DoctrineConfigUpdater: class: GeekCell\DddBundle\Maker\Doctrine\DoctrineConfigUpdater public: false + + GeekCell\DddBundle\Maker\Doctrine\ApiPlatformConfigUpdator: + class: GeekCell\DddBundle\Maker\ApiPlatform\ApiPlatformConfigUpdater + public: false diff --git a/src/Maker/AbstractBaseConfigUpdater.php b/src/Maker/AbstractBaseConfigUpdater.php new file mode 100644 index 0000000..8ccb923 --- /dev/null +++ b/src/Maker/AbstractBaseConfigUpdater.php @@ -0,0 +1,41 @@ + + */ + protected function read(string $yamlSource): array + { + $this->manipulator = new YamlSourceManipulator($yamlSource); + return $this->manipulator->getData(); + } + + /** + * Returns the updated YAML contents for the given data. + * + * @param array $yamlData + * @return string + * @throws AssertionFailedException + */ + protected function write(array $yamlData): string + { + Assert\Assertion::notNull($this->manipulator); + $this->manipulator->setData($yamlData); + + return $this->manipulator->getContents(); + } +} diff --git a/src/Maker/ApiPlatform/ApiPlatformConfigUpdater.php b/src/Maker/ApiPlatform/ApiPlatformConfigUpdater.php new file mode 100644 index 0000000..4b0e8d3 --- /dev/null +++ b/src/Maker/ApiPlatform/ApiPlatformConfigUpdater.php @@ -0,0 +1,27 @@ +read($yamlSource); + + /** @var array|null $currentPaths */ + $currentPaths = $data['api_platform']['mapping']['paths']; + $data['api_platform']['mapping']['paths'] = array_unique(array_merge($currentPaths, [$path])); + + return $this->write($data); + } +} diff --git a/src/Maker/Doctrine/DoctrineConfigUpdater.php b/src/Maker/Doctrine/DoctrineConfigUpdater.php index a2404f4..37ce45e 100644 --- a/src/Maker/Doctrine/DoctrineConfigUpdater.php +++ b/src/Maker/Doctrine/DoctrineConfigUpdater.php @@ -5,15 +5,10 @@ namespace GeekCell\DddBundle\Maker\Doctrine; use Assert; -use Symfony\Bundle\MakerBundle\Util\YamlSourceManipulator; +use GeekCell\DddBundle\Maker\AbstractBaseConfigUpdater; -class DoctrineConfigUpdater +class DoctrineConfigUpdater extends AbstractBaseConfigUpdater { - /** - * @var null|YamlSourceManipulator - */ - private ?YamlSourceManipulator $manipulator; - /** * Registers a custom DBAL mapping type. * @@ -25,10 +20,10 @@ class DoctrineConfigUpdater */ public function addCustomDBALMappingType(string $yamlSource, string $identifier, string $mappingClass): string { - $data = $this->createYamlSourceManipulator($yamlSource); + $data = $this->read($yamlSource); $data['doctrine']['dbal']['types'][$identifier] = $mappingClass; - return $this->getYamlContentsFromData($data); + return $this->write($data); } /** @@ -44,39 +39,13 @@ public function updateORMDefaultEntityMapping(string $yamlSource, string $mappin { Assert\Assertion::inArray($mappingType, ['xml', 'attribute'], 'Invalid mapping type: %s'); - $data = $this->createYamlSourceManipulator($yamlSource); + $data = $this->read($yamlSource); $data['doctrine']['orm']['mappings']['App']['type'] = $mappingType; $data['doctrine']['orm']['mappings']['App']['dir'] = $directory; $data['doctrine']['orm']['mappings']['App']['prefix'] = 'App\Domain\Model'; $data['doctrine']['orm']['mappings']['App']['alias'] = 'App'; $data['doctrine']['orm']['mappings']['App']['is_bundle'] = false; - return $this->getYamlContentsFromData($data); - } - - /** - * Creates a YamlSourceManipulator from a YAML source. - * - * @param string $yamlSource - * @return array - */ - private function createYamlSourceManipulator(string $yamlSource): array - { - $this->manipulator = new YamlSourceManipulator($yamlSource); - return $this->manipulator->getData(); - } - - /** - * Returns the updated YAML contents for the given data. - * - * @param array $yamlData - * @return string - */ - private function getYamlContentsFromData(array $yamlData): string - { - Assert\Assertion::notNull($this->manipulator); - $this->manipulator->setData($yamlData); - - return $this->manipulator->getContents(); + return $this->write($data); } } diff --git a/src/Maker/MakeModel.php b/src/Maker/MakeModel.php index 2725eb0..e820577 100644 --- a/src/Maker/MakeModel.php +++ b/src/Maker/MakeModel.php @@ -46,16 +46,13 @@ final class MakeModel extends AbstractMaker implements InputAwareMakerInterface */ private $templateVariables = []; - /** - * Constructor. - * * @param DoctrineConfigUpdater $doctrineUpdater * @param FileManager $fileManager */ public function __construct( - private DoctrineConfigUpdater $doctrineUpdater, - private FileManager $fileManager, + private readonly DoctrineConfigUpdater $doctrineUpdater, + private readonly FileManager $fileManager, ) {} /** diff --git a/src/Maker/MakeResource.php b/src/Maker/MakeResource.php new file mode 100644 index 0000000..5e31319 --- /dev/null +++ b/src/Maker/MakeResource.php @@ -0,0 +1,276 @@ +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).', + 'attribute' + ) + ; + } + + /** + * @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', + ], + ); + $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()}!"); + } + + $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[] = $providerClassNameDetails->getFullName(); + $classesToImport[] = $processorClassNameDetails->getFullName(); + } + + $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 + ]; + + $generator->generateClass( + $classNameDetails->getFullName(), + __DIR__.'/../Resources/skeleton/resource/Resource.tpl.php', + $templateVars, + ); + + if ($configFlavor === self::CONFIG_FLAVOR_XML) { + $targetPath = self::CONFIG_PATH_XML . $classNameDetails->getShortName() . '.xml'; + $generator->generateFile( + $targetPath, + __DIR__.'/../Resources/skeleton/resource/ResourceXmlConfig.tpl.php', + [ + 'entity_full_class_name' => $modelClassNameDetails->getFullName(), + 'provider_class_name' => $providerClassNameDetails->getFullName(), + 'processor_class_name' => $processorClassNameDetails->getFullName(), + ] + ); + } + + $generator->writeChanges(); + + $this->writeSuccessMessage($io); + } + + /** + * ensure custom resource path(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(); + } +} 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/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..4e6744f --- /dev/null +++ b/src/Resources/skeleton/resource/Resource.tpl.php @@ -0,0 +1,26 @@ + + +namespace ; + + + + +#[ApiResource( + provider: ::class, + processor: ::class, +)] + +final class +{ + /** + * Convenience factory method to create the resource from an instance of the model + * + * @param $model + * + * @return static + */ + public static function create( $model): static + { + return new static(); + } +} diff --git a/src/Resources/skeleton/resource/ResourceXmlConfig.tpl.php b/src/Resources/skeleton/resource/ResourceXmlConfig.tpl.php new file mode 100644 index 0000000..f7949c2 --- /dev/null +++ b/src/Resources/skeleton/resource/ResourceXmlConfig.tpl.php @@ -0,0 +1,11 @@ + + + +