diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php
index 1f1d74ce2c6..4791c0ce82b 100644
--- a/.php-cs-fixer.dist.php
+++ b/.php-cs-fixer.dist.php
@@ -14,6 +14,7 @@
$finder = PhpCsFixer\Finder::create()
->in(__DIR__)
->exclude([
+ 'src/Bridge/Symfony/Maker/Resources/skeleton',
'tests/Fixtures/app/var',
])
->notPath('src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php')
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 67347659e50..7efcd15fa42 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
* Security: **BC** Fix `ApiProperty` `security` attribute expression being passed a class string for the `object` variable on updates/creates - null is now passed instead if the object is not available (#4184)
* Security: `ApiProperty` now supports a `security_post_denormalize` attribute, which provides access to the `object` variable for the object being updated/created and `previous_object` for the object before it was updated (#4184)
+* Maker: Add `make:data-provider` and `make :data-persister` commands to generate a data provider / persister (#3850)
* JSON Schema: Add support for generating property schema with numeric constraint restrictions (#4225)
* JSON Schema: Add support for generating property schema with Collection restriction (#4182)
* JSON Schema: Add support for generating property schema format for Url and Hostname (#4185)
diff --git a/composer.json b/composer.json
index 1c0ef10df25..ae80aa92421 100644
--- a/composer.json
+++ b/composer.json
@@ -72,6 +72,7 @@
"symfony/form": "^3.4 || ^4.4 || ^5.1",
"symfony/framework-bundle": "^4.4 || ^5.1",
"symfony/http-client": "^4.4 || ^5.1",
+ "symfony/maker-bundle": "^1.24",
"symfony/mercure-bundle": "*",
"symfony/messenger": "^4.4 || ^5.1",
"symfony/phpunit-bridge": "^5.1.7",
@@ -114,7 +115,8 @@
},
"autoload-dev": {
"psr-4": {
- "ApiPlatform\\Core\\Tests\\": "tests/"
+ "ApiPlatform\\Core\\Tests\\": "tests/",
+ "App\\": "tests/Fixtures/app/var/tmp/src/"
}
},
"config": {
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
index 520746c369c..a35e7a951bc 100644
--- a/phpstan.neon.dist
+++ b/phpstan.neon.dist
@@ -36,6 +36,8 @@ parameters:
- tests/ProphecyTrait.php
- tests/Behat/CoverageContext.php
- tests/Fixtures/TestBundle/Security/AbstractSecurityUser.php
+ # Templates for Maker
+ - src/Bridge/Symfony/Maker/Resources/skeleton
earlyTerminatingMethodCalls:
PHPUnit\Framework\Constraint\Constraint:
- fail
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 6c8892bd6b5..c5df9424cdc 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -29,6 +29,7 @@
vendor
src/Bridge/NelmioApiDoc
src/Bridge/FosUser
+ src/Bridge/Symfony/Maker/Resources/skeleton
.php-cs-fixer.dist.php
src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php
src/Bridge/Symfony/Bundle/Test/Constraint/ArraySubsetLegacy.php
diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php
index d3f5594c2df..2cdd88f2051 100644
--- a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php
+++ b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php
@@ -127,6 +127,7 @@ public function load(array $configs, ContainerBuilder $container): void
$this->registerElasticsearchConfiguration($container, $config, $loader);
$this->registerDataTransformerConfiguration($container);
$this->registerSecurityConfiguration($container, $loader);
+ $this->registerMakerConfiguration($container, $config, $loader);
$container->registerForAutoconfiguration(DataPersisterInterface::class)
->addTag('api_platform.data_persister');
@@ -743,6 +744,15 @@ private function registerOpenApiConfiguration(ContainerBuilder $container, array
$container->setParameter('api_platform.openapi.license.url', $config['openapi']['license']['url']);
}
+ private function registerMakerConfiguration(ContainerBuilder $container, array $config, XmlFileLoader $loader): void
+ {
+ if (!$this->isConfigEnabled($container, $config['maker'])) {
+ return;
+ }
+
+ $loader->load('maker.xml');
+ }
+
private function buildDeprecationArgs(string $version, string $message): array
{
return method_exists(Definition::class, 'getDeprecation')
diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php b/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php
index d202cd727f1..1b1e2b5a48b 100644
--- a/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php
+++ b/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php
@@ -26,6 +26,7 @@
use FOS\UserBundle\FOSUserBundle;
use GraphQL\GraphQL;
use Symfony\Bundle\FullStack;
+use Symfony\Bundle\MakerBundle\MakerBundle;
use Symfony\Bundle\MercureBundle\MercureBundle;
use Symfony\Bundle\TwigBundle\TwigBundle;
use Symfony\Component\Config\Definition\BaseNode;
@@ -209,6 +210,7 @@ public function getConfigTreeBuilder()
$this->addMessengerSection($rootNode);
$this->addElasticsearchSection($rootNode);
$this->addOpenApiSection($rootNode);
+ $this->addMakerSection($rootNode);
$this->addExceptionToStatusSection($rootNode);
@@ -629,6 +631,16 @@ private function addDefaultsSection(ArrayNodeDefinition $rootNode): void
}
}
+ private function addMakerSection(ArrayNodeDefinition $rootNode): void
+ {
+ $rootNode
+ ->children()
+ ->arrayNode('maker')
+ ->{class_exists(MakerBundle::class) ? 'canBeDisabled' : 'canBeEnabled'}()
+ ->end()
+ ->end();
+ }
+
private function buildDeprecationArgs(string $version, string $message): array
{
return method_exists(BaseNode::class, 'getDeprecation')
diff --git a/src/Bridge/Symfony/Bundle/Resources/config/maker.xml b/src/Bridge/Symfony/Bundle/Resources/config/maker.xml
new file mode 100644
index 00000000000..f829cf3af09
--- /dev/null
+++ b/src/Bridge/Symfony/Bundle/Resources/config/maker.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Bridge/Symfony/Maker/MakeDataPersister.php b/src/Bridge/Symfony/Maker/MakeDataPersister.php
new file mode 100644
index 00000000000..0a40200c449
--- /dev/null
+++ b/src/Bridge/Symfony/Maker/MakeDataPersister.php
@@ -0,0 +1,116 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Core\Bridge\Symfony\Maker;
+
+use ApiPlatform\Core\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
+use Symfony\Bundle\MakerBundle\ConsoleStyle;
+use Symfony\Bundle\MakerBundle\DependencyBuilder;
+use Symfony\Bundle\MakerBundle\Generator;
+use Symfony\Bundle\MakerBundle\InputConfiguration;
+use Symfony\Bundle\MakerBundle\Maker\AbstractMaker;
+use Symfony\Bundle\MakerBundle\Str;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Question\Question;
+
+final class MakeDataPersister extends AbstractMaker
+{
+ private $resourceNameCollection;
+
+ public function __construct(ResourceNameCollectionFactoryInterface $resourceNameCollection)
+ {
+ $this->resourceNameCollection = $resourceNameCollection;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getCommandName(): string
+ {
+ return 'make:data-persister';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getCommandDescription(): string
+ {
+ return 'Creates an API Platform data persister';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function configureCommand(Command $command, InputConfiguration $inputConfig)
+ {
+ $command
+ ->addArgument('name', InputArgument::OPTIONAL, 'Choose a class name for your data persister (e.g. AwesomeDataPersister>)')
+ ->addArgument('resource-class', InputArgument::OPTIONAL, 'Choose a Resource class')
+ ->setHelp(file_get_contents(__DIR__.'/Resources/help/MakeDataPersister.txt'));
+
+ $inputConfig->setArgumentAsNonInteractive('resource-class');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function configureDependencies(DependencyBuilder $dependencies)
+ {
+ }
+
+ public function interact(InputInterface $input, ConsoleStyle $io, Command $command)
+ {
+ if (null === $input->getArgument('resource-class')) {
+ $argument = $command->getDefinition()->getArgument('resource-class');
+
+ $resourceClasses = $this->resourceNameCollection->create();
+
+ $question = new Question($argument->getDescription());
+ $question->setAutocompleterValues($resourceClasses);
+
+ $value = $io->askQuestion($question);
+
+ $input->setArgument('resource-class', $value);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator)
+ {
+ $dataPersisterClassNameDetails = $generator->createClassNameDetails(
+ $input->getArgument('name'),
+ 'DataPersister\\'
+ );
+ $resourceClass = $input->getArgument('resource-class');
+
+ $generator->generateClass(
+ $dataPersisterClassNameDetails->getFullName(),
+ __DIR__.'/Resources/skeleton/DataPersister.tpl.php',
+ [
+ 'resource_class' => null !== $resourceClass ? Str::getShortClassName($resourceClass) : null,
+ 'resource_full_class_name' => $resourceClass,
+ ]
+ );
+ $generator->writeChanges();
+
+ $this->writeSuccessMessage($io);
+ $io->text([
+ 'Next: Open your new data persister class and start customizing it.',
+ 'Find the documentation at https://api-platform.com/docs/core/data-persisters/>',
+ ]);
+ }
+}
diff --git a/src/Bridge/Symfony/Maker/MakeDataProvider.php b/src/Bridge/Symfony/Maker/MakeDataProvider.php
new file mode 100644
index 00000000000..3bd380d7579
--- /dev/null
+++ b/src/Bridge/Symfony/Maker/MakeDataProvider.php
@@ -0,0 +1,122 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Core\Bridge\Symfony\Maker;
+
+use ApiPlatform\Core\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
+use Symfony\Bundle\MakerBundle\ConsoleStyle;
+use Symfony\Bundle\MakerBundle\DependencyBuilder;
+use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException;
+use Symfony\Bundle\MakerBundle\Generator;
+use Symfony\Bundle\MakerBundle\InputConfiguration;
+use Symfony\Bundle\MakerBundle\Maker\AbstractMaker;
+use Symfony\Bundle\MakerBundle\Str;
+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 Symfony\Component\Console\Question\Question;
+
+class MakeDataProvider extends AbstractMaker
+{
+ private $resourceNameCollection;
+
+ public function __construct(ResourceNameCollectionFactoryInterface $resourceNameCollection)
+ {
+ $this->resourceNameCollection = $resourceNameCollection;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getCommandName(): string
+ {
+ return 'make:data-provider';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getCommandDescription(): string
+ {
+ return 'Creates an API Platform data provider';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function configureCommand(Command $command, InputConfiguration $inputConfig)
+ {
+ $command
+ ->addArgument('name', InputArgument::OPTIONAL, 'Choose a class name for your data provider (e.g. AwesomeDataProvider>)')
+ ->addArgument('resource-class', InputArgument::OPTIONAL, 'Choose a Resource class')
+ ->addOption('item-only', null, InputOption::VALUE_NONE, 'Generate only an item data provider')
+ ->addOption('collection-only', null, InputOption::VALUE_NONE, 'Generate only a collection data provider')
+ ->setHelp(file_get_contents(__DIR__.'/Resources/help/MakeDataProvider.txt'));
+
+ $inputConfig->setArgumentAsNonInteractive('resource-class');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function configureDependencies(DependencyBuilder $dependencies)
+ {
+ }
+
+ public function interact(InputInterface $input, ConsoleStyle $io, Command $command)
+ {
+ if ($input->getOption('item-only') && $input->getOption('collection-only')) {
+ throw new RuntimeCommandException('You should at least generate an item or a collection data provider');
+ }
+
+ if (null === $input->getArgument('resource-class')) {
+ $argument = $command->getDefinition()->getArgument('resource-class');
+
+ $question = new Question($argument->getDescription());
+ $question->setAutocompleterValues($this->resourceNameCollection->create());
+
+ $input->setArgument('resource-class', $io->askQuestion($question));
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator)
+ {
+ $dataProviderClassNameDetails = $generator->createClassNameDetails(
+ $input->getArgument('name'),
+ 'DataProvider\\'
+ );
+ $resourceClass = $input->getArgument('resource-class');
+
+ $generator->generateClass(
+ $dataProviderClassNameDetails->getFullName(),
+ __DIR__.'/Resources/skeleton/DataProvider.tpl.php',
+ [
+ 'resource_class' => null !== $resourceClass ? Str::getShortClassName($resourceClass) : null,
+ 'resource_full_class_name' => $resourceClass,
+ 'generate_collection' => !$input->getOption('item-only'),
+ 'generate_item' => !$input->getOption('collection-only'),
+ ]
+ );
+ $generator->writeChanges();
+
+ $this->writeSuccessMessage($io);
+ $io->text([
+ 'Next: Open your new data provider class and start customizing it.',
+ 'Find the documentation at https://api-platform.com/docs/core/data-providers/>',
+ ]);
+ }
+}
diff --git a/src/Bridge/Symfony/Maker/Resources/help/MakeDataPersister.txt b/src/Bridge/Symfony/Maker/Resources/help/MakeDataPersister.txt
new file mode 100644
index 00000000000..7afe500d462
--- /dev/null
+++ b/src/Bridge/Symfony/Maker/Resources/help/MakeDataPersister.txt
@@ -0,0 +1,5 @@
+The %command.name% command generates a new API Platform data persister class.
+
+php %command.full_name% AwesomeDataPersister
+
+If the argument is missing, the command will ask for the class name interactively.
diff --git a/src/Bridge/Symfony/Maker/Resources/help/MakeDataProvider.txt b/src/Bridge/Symfony/Maker/Resources/help/MakeDataProvider.txt
new file mode 100644
index 00000000000..73064481560
--- /dev/null
+++ b/src/Bridge/Symfony/Maker/Resources/help/MakeDataProvider.txt
@@ -0,0 +1,5 @@
+The %command.name% command generates a new API Platform data provider class.
+
+php %command.full_name% AwesomeDataProvider
+
+If the argument is missing, the command will ask for the class name interactively.
diff --git a/src/Bridge/Symfony/Maker/Resources/skeleton/DataPersister.tpl.php b/src/Bridge/Symfony/Maker/Resources/skeleton/DataPersister.tpl.php
new file mode 100644
index 00000000000..64b20ef7034
--- /dev/null
+++ b/src/Bridge/Symfony/Maker/Resources/skeleton/DataPersister.tpl.php
@@ -0,0 +1,56 @@
+= "
+
+namespace = $namespace ?>;
+
+use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
+use ApiPlatform\Core\DataPersister\ResumableDataPersisterInterface;
+
+use = $resource_full_class_name ?>;
+
+
+final class = $class_name ?> implements ContextAwareDataPersisterInterface, ResumableDataPersisterInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function supports($data, array $context = []): bool
+ {
+
+ return $data instanceof = $resource_class ?>::class; // Add your custom conditions here
+
+ return false; // Add your custom conditions here
+
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function resumable(array $context = []): bool
+ {
+ return false; // Set it to true if you want to call the other data persisters
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function persist($data, array $context = [])= 70200) {
+ echo ': object';
+ }?>
+
+ {
+ // Call your persistence layer to save $data
+
+ return $data;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function remove($data, array $context = []): void
+ {
+ // Call your persistence layer to delete $data
+ }
+}
diff --git a/src/Bridge/Symfony/Maker/Resources/skeleton/DataProvider.tpl.php b/src/Bridge/Symfony/Maker/Resources/skeleton/DataProvider.tpl.php
new file mode 100644
index 00000000000..b9edcba4021
--- /dev/null
+++ b/src/Bridge/Symfony/Maker/Resources/skeleton/DataProvider.tpl.php
@@ -0,0 +1,63 @@
+= "
+
+namespace = $namespace ?>;
+
+
+use ApiPlatform\Core\DataProvider\ContextAwareCollectionDataProviderInterface;
+
+
+use ApiPlatform\Core\DataProvider\ItemDataProviderInterface;
+
+use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface;
+
+use = $resource_full_class_name ?>;
+
+
+final class = $class_name ?> implements RestrictedDataProviderInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function supports(string $resourceClass, string $operationName = null, array $context = []): bool
+ {
+
+ return = $resource_class ?>::class === $resourceClass; // Add your custom conditions here
+
+ return false; // Add your custom conditions here
+
+ }
+
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCollection(string $resourceClass, string $operationName = null, array $context = []): iterable
+ {
+ // Retrieve the collection from somewhere
+ }
+
+
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getItem(string $resourceClass, $id, string $operationName = null, array $context = [])= 70200) {
+ echo ': ?object';
+ }?>
+
+ {
+ // Retrieve the item from somewhere then return it or null if not found
+ }
+
+}
diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
index cf2097fcf46..e5d6de48f87 100644
--- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
+++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
@@ -1356,6 +1356,8 @@ private function getBaseContainerBuilderProphecyWithoutDefaultMetadataLoading(ar
'api_platform.json_schema.schema_factory',
'api_platform.listener.view.validate',
'api_platform.listener.view.validate_query_parameters',
+ 'api_platform.maker.command.data_persister',
+ 'api_platform.maker.command.data_provider',
'api_platform.mercure.listener.response.add_link_header',
'api_platform.messenger.data_persister',
'api_platform.messenger.data_transformer',
diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php
index 71352749b39..acc1c276d88 100644
--- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php
+++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php
@@ -224,6 +224,9 @@ private function runDefaultConfigTests(array $doctrineIntegrationsToLoad = ['orm
'backward_compatibility_layer' => true,
'swagger_ui_extra_configuration' => [],
],
+ 'maker' => [
+ 'enabled' => true,
+ ],
], $config);
}
diff --git a/tests/Bridge/Symfony/Maker/MakeDataPersisterTest.php b/tests/Bridge/Symfony/Maker/MakeDataPersisterTest.php
new file mode 100644
index 00000000000..8ea178c7403
--- /dev/null
+++ b/tests/Bridge/Symfony/Maker/MakeDataPersisterTest.php
@@ -0,0 +1,186 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Question;
+use Symfony\Bundle\FrameworkBundle\Console\Application;
+use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
+use Symfony\Component\Console\Tester\CommandTester;
+use Symfony\Component\Filesystem\Filesystem;
+
+class MakeDataPersisterTest extends KernelTestCase
+{
+ protected function tearDown(): void
+ {
+ (new Filesystem())->remove(self::tempDir());
+ }
+
+ /** @dataProvider dataPersisterProvider */
+ public function testMakeDataPersister(array $commandInputs, array $userInputs, string $expected)
+ {
+ $this->assertFileDoesNotExist(self::tempFile('src/DataPersister/CustomDataPersister.php'));
+
+ $tester = new CommandTester((new Application(self::bootKernel()))->find('make:data-persister'));
+ $tester->setInputs($userInputs);
+ $tester->execute($commandInputs);
+
+ $this->assertFileExists(self::tempFile('src/DataPersister/CustomDataPersister.php'));
+
+ // Unify line endings
+ $expected = preg_replace('~\R~u', "\r\n", $expected);
+ $result = preg_replace('~\R~u', "\r\n", file_get_contents(self::tempFile('src/DataPersister/CustomDataPersister.php')));
+ $this->assertSame($expected, $result);
+
+ $display = $tester->getDisplay();
+ $this->assertStringContainsString('Success!', $display);
+
+ if (!isset($commandInputs['name'])) {
+ $this->assertStringContainsString('Choose a class name for your data persister (e.g. AwesomeDataPersister):', $display);
+ } else {
+ $this->assertStringNotContainsString('Choose a class name for your data persister (e.g. AwesomeDataPersister):', $display);
+ }
+ if (!isset($commandInputs['resource-class'])) {
+ $this->assertStringContainsString('Choose a Resource class:', $display);
+ } else {
+ $this->assertStringNotContainsString('Choose a Resource class:', $display);
+ }
+ $this->assertStringContainsString(<< [
+ [],
+ ['CustomDataPersister', ''],
+ \PHP_VERSION_ID >= 70200 ? $expected : str_replace(': object', '', $expected),
+ ];
+
+ $expected = <<<'EOF'
+ [
+ [],
+ ['CustomDataPersister', Question::class],
+ $expected,
+ ];
+
+ yield 'Generate data persister with resource class not interactively' => [
+ ['name' => 'CustomDataPersister', 'resource-class' => Question::class],
+ [],
+ $expected,
+ ];
+ }
+
+ private static function tempDir(): string
+ {
+ return __DIR__.'/../../../Fixtures/app/var/tmp';
+ }
+
+ private static function tempFile(string $path): string
+ {
+ return sprintf('%s/%s', self::tempDir(), $path);
+ }
+}
diff --git a/tests/Bridge/Symfony/Maker/MakeDataProviderTest.php b/tests/Bridge/Symfony/Maker/MakeDataProviderTest.php
new file mode 100644
index 00000000000..921db2e807d
--- /dev/null
+++ b/tests/Bridge/Symfony/Maker/MakeDataProviderTest.php
@@ -0,0 +1,331 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Question;
+use Symfony\Bundle\FrameworkBundle\Console\Application;
+use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
+use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException;
+use Symfony\Component\Console\Tester\CommandTester;
+use Symfony\Component\Filesystem\Filesystem;
+
+class MakeDataProviderTest extends KernelTestCase
+{
+ protected function tearDown(): void
+ {
+ (new Filesystem())->remove(self::tempDir());
+ }
+
+ /** @dataProvider dataProviderProvider */
+ public function testMakeDataProvider(array $commandInputs, array $userInputs, string $expected)
+ {
+ $this->assertFileDoesNotExist(self::tempFile('src/DataProvider/CustomDataProvider.php'));
+
+ $tester = new CommandTester((new Application(self::bootKernel()))->find('make:data-provider'));
+ $tester->setInputs($userInputs);
+ $tester->execute($commandInputs);
+
+ $this->assertFileExists(self::tempFile('src/DataProvider/CustomDataProvider.php'));
+
+ // Unify line endings
+ $expected = preg_replace('~\R~u', "\r\n", $expected);
+ $result = preg_replace('~\R~u', "\r\n", file_get_contents(self::tempFile('src/DataProvider/CustomDataProvider.php')));
+ $this->assertSame($expected, $result);
+
+ $display = $tester->getDisplay();
+ $this->assertStringContainsString('Success!', $display);
+
+ if (!isset($commandInputs['name'])) {
+ $this->assertStringContainsString('Choose a class name for your data provider (e.g. AwesomeDataProvider):', $display);
+ } else {
+ $this->assertStringNotContainsString('Choose a class name for your data provider (e.g. AwesomeDataProvider):', $display);
+ }
+ if (!isset($commandInputs['resource-class'])) {
+ $this->assertStringContainsString(' Choose a Resource class:', $display);
+ } else {
+ $this->assertStringNotContainsString('Choose a Resource class:', $display);
+ }
+
+ $this->assertStringContainsString(<< [
+ [],
+ ['CustomDataProvider', ''],
+ \PHP_VERSION_ID >= 70200 ? $expected : str_replace(': ?object', '', $expected),
+ ];
+
+ $expected = <<<'EOF'
+ [
+ [],
+ ['CustomDataProvider', Question::class],
+ $expected,
+ ];
+
+ yield 'Generate all with resource class not interactively' => [
+ ['name' => 'CustomDataProvider', 'resource-class' => Question::class],
+ [],
+ $expected,
+ ];
+
+ $expected = <<<'EOF'
+ [
+ ['--item-only' => true],
+ ['CustomDataProvider', ''],
+ \PHP_VERSION_ID >= 70200 ? $expected : str_replace(': ?object', '', $expected),
+ ];
+
+ $expected = <<<'EOF'
+ [
+ ['--item-only' => true],
+ ['CustomDataProvider', Question::class],
+ $expected,
+ ];
+
+ yield 'Generate an item data provider with a resource class not interactively' => [
+ ['name' => 'CustomDataProvider', 'resource-class' => Question::class, '--item-only' => true],
+ [],
+ $expected,
+ ];
+
+ $expected = <<<'EOF'
+ [
+ ['--collection-only' => true],
+ ['CustomDataProvider', ''],
+ $expected,
+ ];
+
+ $expected = <<<'EOF'
+ [
+ ['--collection-only' => true],
+ ['CustomDataProvider', Question::class],
+ $expected,
+ ];
+
+ yield 'Generate a collection data provider with a resource class not interactively' => [
+ ['name' => 'CustomDataProvider', 'resource-class' => Question::class, '--collection-only' => true],
+ [],
+ $expected,
+ ];
+ }
+
+ public function testMakeDataProviderThrows()
+ {
+ $tester = new CommandTester((new Application(self::bootKernel()))->find('make:data-provider'));
+ $this->expectException(RuntimeCommandException::class);
+ $this->expectExceptionMessage('You should at least generate an item or a collection data provider');
+
+ $tester->execute(['name' => 'CustomDataProvider', 'resource-class' => Question::class, '--collection-only' => true, '--item-only' => true]);
+ }
+
+ private static function tempDir(): string
+ {
+ return __DIR__.'/../../../Fixtures/app/var/tmp';
+ }
+
+ private static function tempFile(string $path): string
+ {
+ return sprintf('%s/%s', self::tempDir(), $path);
+ }
+}
diff --git a/tests/Fixtures/app/AppKernel.php b/tests/Fixtures/app/AppKernel.php
index 2eff16320d6..0c7ec78a311 100644
--- a/tests/Fixtures/app/AppKernel.php
+++ b/tests/Fixtures/app/AppKernel.php
@@ -25,6 +25,7 @@
use Symfony\Bridge\Doctrine\Types\UuidType;
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
+use Symfony\Bundle\MakerBundle\MakerBundle;
use Symfony\Bundle\MercureBundle\MercureBundle;
use Symfony\Bundle\SecurityBundle\SecurityBundle;
use Symfony\Bundle\TwigBundle\TwigBundle;
@@ -74,6 +75,7 @@ public function registerBundles(): array
new WebProfilerBundle(),
new FriendsOfBehatSymfonyExtensionBundle(),
new FrameworkBundle(),
+ new MakerBundle(),
];
if (class_exists(DoctrineMongoDBBundle::class)) {