diff --git a/.gitignore b/.gitignore
index cf97dc4..e864b22 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,5 +3,6 @@
/composer.lock
/phpcs.xml
/phpunit.xml
+/tests/tmp
/var
/vendor
diff --git a/composer.json b/composer.json
index 862e60d..99ba6fc 100644
--- a/composer.json
+++ b/composer.json
@@ -3,11 +3,14 @@
"description": "Symfony MakerBundle for MongoDB ODM",
"keywords": ["mongodb", "maker", "odm", "dev"],
"type": "symfony-bundle",
+ "repositories": [
+ { "type": "github", "url": "https://github.com/GromNaN/symfony-maker-bundle" }
+ ],
"require": {
"php": "^8.4",
"ext-mongodb": "^2.1",
"doctrine/mongodb-odm-bundle": "^5.5",
- "symfony/maker-bundle": "^1.65",
+ "symfony/maker-bundle": "dev-develop",
"symfony/http-kernel": "^7.4|^8"
},
"require-dev": {
@@ -15,7 +18,8 @@
"phpstan/phpstan": "^2.1.30",
"phpstan/phpstan-phpunit": "^2.0.7",
"phpunit/phpunit": "^12.5",
- "symfony/framework-bundle": "^7.4|^8"
+ "symfony/framework-bundle": "^7.4|^8",
+ "twig/twig": "^3.22"
},
"license": "MIT",
"autoload": {
diff --git a/config/help/MakeDocument.txt b/config/help/MakeDocument.txt
new file mode 100644
index 0000000..2c00e42
--- /dev/null
+++ b/config/help/MakeDocument.txt
@@ -0,0 +1,3 @@
+The %command.name% command creates or updates a document and repository class.
+
+php %command.full_name% BlogPost
diff --git a/config/makers.php b/config/makers.php
index 9ae41b1..7218941 100644
--- a/config/makers.php
+++ b/config/makers.php
@@ -14,11 +14,29 @@
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use Doctrine\Bundle\MongoDBMakerBundle\Maker\MakeDocument;
+use Doctrine\Bundle\MongoDBMakerBundle\MongoDB\DocumentClassGenerator;
+use Doctrine\Bundle\MongoDBMakerBundle\MongoDB\MongoDBHelper;
return static function (ContainerConfigurator $container): void {
$services = $container->services();
+ $services->set('doctrine_mongodb_maker.mongodb_helper', MongoDBHelper::class)
+ ->args([
+ service('doctrine_mongodb'),
+ ]);
+
+ $services->set('doctrine_mongodb_maker.document_class_generator', DocumentClassGenerator::class)
+ ->args([
+ service('maker.generator'),
+ service('doctrine_mongodb_maker.mongodb_helper'),
+ ]);
+
$services->set('doctrine_mongodb_maker.maker.make_document', MakeDocument::class)
- ->args([])
+ ->args([
+ service('maker.file_manager'),
+ service('doctrine_mongodb_maker.mongodb_helper'),
+ service('maker.generator'),
+ service('doctrine_mongodb_maker.document_class_generator'),
+ ])
->tag('maker.command');
};
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
index b302b90..25e0b31 100644
--- a/phpcs.xml.dist
+++ b/phpcs.xml.dist
@@ -13,7 +13,9 @@
config
src
- tests
+ tests/TestKernel.php
+ tests/BundleTest.php
+ tests/Maker
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
index 347e7a5..2d2b268 100644
--- a/phpstan.neon.dist
+++ b/phpstan.neon.dist
@@ -4,6 +4,8 @@ parameters:
paths:
- src
- tests
+ excludePaths:
+ - tests/tmp
includes:
- vendor/phpstan/phpstan-phpunit/extension.neon
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index bebf77d..59969e8 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -6,6 +6,11 @@
failOnAllIssues="true"
displayDetailsOnAllIssues="true"
>
+
+
+
+
+
./tests/
diff --git a/src/Maker/MakeDocument.php b/src/Maker/MakeDocument.php
index af799bd..b0467b5 100644
--- a/src/Maker/MakeDocument.php
+++ b/src/Maker/MakeDocument.php
@@ -4,39 +4,407 @@
namespace Doctrine\Bundle\MongoDBMakerBundle\Maker;
+use Doctrine\Bundle\MongoDBBundle\DependencyInjection\Compiler\DoctrineMongoDBMappingsPass;
+use Doctrine\Bundle\MongoDBMakerBundle\MongoDB\DocumentClassGenerator;
+use Doctrine\Bundle\MongoDBMakerBundle\MongoDB\MongoDBHelper;
+use Doctrine\ODM\MongoDB\Types\Type;
+use InvalidArgumentException;
+use ReflectionClass;
+use ReflectionProperty;
use Symfony\Bundle\MakerBundle\ConsoleStyle;
use Symfony\Bundle\MakerBundle\DependencyBuilder;
+use Symfony\Bundle\MakerBundle\FileManager;
use Symfony\Bundle\MakerBundle\Generator;
use Symfony\Bundle\MakerBundle\InputAwareMakerInterface;
use Symfony\Bundle\MakerBundle\InputConfiguration;
use Symfony\Bundle\MakerBundle\Maker\AbstractMaker;
+use Symfony\Bundle\MakerBundle\Maker\Common\UidTrait;
+use Symfony\Bundle\MakerBundle\Str;
+use Symfony\Bundle\MakerBundle\Util\ClassDetails;
+use Symfony\Bundle\MakerBundle\Util\ClassSource\Model\ClassProperty;
+use Symfony\Bundle\MakerBundle\Util\ClassSourceManipulator;
+use Symfony\Bundle\MakerBundle\Validator;
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;
+
+use function array_key_exists;
+use function array_keys;
+use function array_map;
+use function class_exists;
+use function dirname;
+use function file_get_contents;
+use function implode;
+use function in_array;
+use function preg_match;
+use function sprintf;
+use function str_starts_with;
+use function substr;
final class MakeDocument extends AbstractMaker implements InputAwareMakerInterface
{
+ use UidTrait;
+
+ private Generator $generator;
+ private DocumentClassGenerator $documentClassGenerator;
+
+ public function __construct(
+ private FileManager $fileManager,
+ private MongoDBHelper $mongoDBHelper,
+ Generator|null $generator = null,
+ DocumentClassGenerator|null $documentClassGenerator = null,
+ ) {
+ if ($generator === null) {
+ $this->generator = new Generator($fileManager, 'App\\');
+ } else {
+ $this->generator = $generator;
+ }
+
+ if ($documentClassGenerator === null) {
+ $this->documentClassGenerator = new DocumentClassGenerator($this->generator, $this->mongoDBHelper);
+ } else {
+ $this->documentClassGenerator = $documentClassGenerator;
+ }
+ }
+
public static function getCommandName(): string
{
- return 'doctrine:mongodb:make:document';
+ return 'make:document';
}
public static function getCommandDescription(): string
{
- return 'Creates a new MongoDB ODM document class';
+ return 'Create or update a MongoDB ODM document class';
}
public function configureCommand(Command $command, InputConfiguration $inputConfig): void
{
- // TODO: Implement configureCommand() method.
+ $command
+ ->addArgument('name', InputArgument::OPTIONAL, sprintf('Class name of the document to create or update (e.g. %s>)', Str::asClassName(Str::getRandomTerm())))
+ ->addOption('regenerate', null, InputOption::VALUE_NONE, 'Instead of adding new fields, simply generate the methods (e.g. getter/setter) for existing fields')
+ ->addOption('overwrite', null, InputOption::VALUE_NONE, 'Overwrite any existing getter/setter methods')
+ ->setHelp(file_get_contents(dirname(__DIR__, 2) . '/config/help/MakeDocument.txt'));
+
+ $this->addWithUuidOption($command);
+
+ $inputConfig->setArgumentAsNonInteractive('name');
}
- public function configureDependencies(DependencyBuilder $dependencies, InputInterface|null $input = null): void
+ public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void
{
- // TODO: Implement configureDependencies() method.
+ $documentClassName = $input->getArgument('name');
+ if ($documentClassName && empty($this->verifyDocumentName($documentClassName))) {
+ return;
+ }
+
+ if ($input->getOption('regenerate')) {
+ $io->block([
+ 'This command will generate any missing methods (e.g. getters & setters) for a class or all classes in a namespace.',
+ 'To overwrite any existing methods, re-run this command with the --overwrite flag',
+ ], null, 'fg=yellow');
+ $classOrNamespace = $io->ask('Enter a class or namespace to regenerate', $this->getDocumentNamespace(), Validator::notBlank(...));
+
+ $input->setArgument('name', $classOrNamespace);
+
+ return;
+ }
+
+ $this->checkIsUsingUid($input);
+
+ $argument = $command->getDefinition()->getArgument('name');
+ $question = $this->createDocumentClassQuestion($argument->getDescription());
+ $documentClassName ??= $io->askQuestion($question);
+
+ while ($dangerous = $this->verifyDocumentName($documentClassName)) {
+ if ($io->confirm(sprintf('"%s" contains one or more non-ASCII characters, which are potentially problematic with some database. It is recommended to use only ASCII characters for document names. Continue anyway?', $documentClassName), false)) {
+ break;
+ }
+
+ $documentClassName = $io->askQuestion($question);
+ }
+
+ $input->setArgument('name', $documentClassName);
}
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void
{
- // TODO: Implement generate() method.
+ $overwrite = $input->getOption('overwrite');
+
+ // the regenerate option has entirely custom behavior
+ if ($input->getOption('regenerate')) {
+ $io->comment('Document regeneration is not yet implemented.');
+ $this->writeSuccessMessage($io);
+
+ return;
+ }
+
+ $documentClassDetails = $generator->createClassNameDetails(
+ $input->getArgument('name'),
+ 'Document\\',
+ );
+
+ $classExists = class_exists($documentClassDetails->getFullName());
+ if (! $classExists) {
+ $documentPath = $this->documentClassGenerator->generateDocumentClass(
+ documentClassDetails: $documentClassDetails,
+ idType: $this->getIdType(),
+ );
+
+ $generator->writeChanges();
+ }
+
+ if ($classExists) {
+ $documentPath = $this->getPathOfClass($documentClassDetails->getFullName());
+ $io->text('Your document already exists! So let\'s add some new fields!');
+ } else {
+ $io->text([
+ '',
+ 'Document generated! Now let\'s add some fields!',
+ 'You can always add more fields later manually or by re-running this command.',
+ ]);
+ }
+
+ $currentFields = $this->getPropertyNames($documentClassDetails->getFullName());
+ $manipulator = $this->createClassManipulator($documentPath, $io, $overwrite);
+
+ $isFirstField = true;
+ while (true) {
+ $newField = $this->askForNextField($io, $currentFields, $documentClassDetails->getFullName(), $isFirstField);
+ $isFirstField = false;
+
+ if ($newField === null) {
+ break;
+ }
+
+ if ($newField instanceof ClassProperty) {
+ $manipulator->addEntityField($newField);
+ $currentFields[] = $newField->propertyName;
+ }
+
+ $this->fileManager->dumpFile($documentPath, $manipulator->getSourceCode());
+ }
+
+ $this->writeSuccessMessage($io);
+ $io->text([
+ 'Next: Add more fields with the same command, or start using your document!',
+ '',
+ ]);
+ }
+
+ public function configureDependencies(DependencyBuilder $dependencies, InputInterface|null $input = null): void
+ {
+ $dependencies->addClassDependency(
+ DoctrineMongoDBMappingsPass::class,
+ 'mongodb-odm-bundle',
+ );
+ }
+
+ /** @param string[] $fields */
+ private function askForNextField(ConsoleStyle $io, array $fields, string $documentClass, bool $isFirstField): ClassProperty|null
+ {
+ $io->writeln('');
+
+ if ($isFirstField) {
+ $questionText = 'New property name (press to stop adding fields)';
+ } else {
+ $questionText = 'Add another property? Enter the property name (or press to stop adding fields)';
+ }
+
+ $fieldName = $io->ask($questionText, null, function ($name) use ($fields) {
+ // allow it to be empty
+ if (! $name) {
+ return $name;
+ }
+
+ if (in_array($name, $fields, true)) {
+ throw new InvalidArgumentException(sprintf('The "%s" property already exists.', $name));
+ }
+
+ return Validator::validateDoctrineFieldName($name, $this->mongoDBHelper->getRegistry());
+ });
+
+ if (! $fieldName) {
+ return null;
+ }
+
+ $defaultType = 'string';
+ // try to guess the type by the field name prefix/suffix
+ $snakeCasedField = Str::asSnakeCase($fieldName);
+
+ $suffix = substr($snakeCasedField, -3);
+ if ($suffix === '_at') {
+ $defaultType = 'date_immutable';
+ } elseif ($suffix === '_id') {
+ $defaultType = 'int';
+ } elseif (str_starts_with($snakeCasedField, 'is_')) {
+ $defaultType = 'bool';
+ } elseif (str_starts_with($snakeCasedField, 'has_')) {
+ $defaultType = 'bool';
+ }
+
+ $type = null;
+ $types = $this->getTypesMap();
+
+ $allValidTypes = array_keys($types);
+
+ while ($type === null) {
+ $question = new Question('Field type (enter ? to see all types)', $defaultType);
+ $question->setAutocompleterValues($allValidTypes);
+ $type = $io->askQuestion($question);
+
+ if ($type === '?') {
+ $this->printAvailableTypes($io);
+ $io->writeln('');
+
+ $type = null;
+ } elseif (! in_array($type, $allValidTypes, true)) {
+ $this->printAvailableTypes($io);
+ $io->error(sprintf('Invalid type "%s".', $type));
+ $io->writeln('');
+
+ $type = null;
+ }
+ }
+
+ // this is a normal field
+ $classProperty = new ClassProperty(propertyName: $fieldName, type: $type);
+
+ if ($type === 'string') {
+ // MongoDB doesn't have length constraints at the database level
+ // but we can still track it for validation purposes
+ $classProperty->length = null;
+ }
+
+ if ($io->confirm('Can this field be null in the database (nullable)', false)) {
+ $classProperty->nullable = true;
+ }
+
+ return $classProperty;
+ }
+
+ private function printAvailableTypes(ConsoleStyle $io): void
+ {
+ $allTypes = $this->getTypesMap();
+
+ $typesTable = [
+ 'main' => [
+ 'string' => [],
+ 'int' => [],
+ 'float' => [],
+ 'bool' => [],
+ ],
+ 'array_object' => [
+ 'hash' => [],
+ 'collection' => [],
+ 'object_id' => [],
+ ],
+ 'date_time' => [
+ 'date' => ['date_immutable'],
+ 'timestamp' => [],
+ ],
+ ];
+
+ $printSection = static function (array $sectionTypes) use ($io, &$allTypes): void {
+ foreach ($sectionTypes as $mainType => $subTypes) {
+ if (! array_key_exists($mainType, $allTypes)) {
+ continue;
+ }
+
+ foreach ($subTypes as $key => $potentialType) {
+ if (! array_key_exists($potentialType, $allTypes)) {
+ unset($subTypes[$key]);
+ }
+
+ unset($allTypes[$potentialType]);
+ }
+
+ unset($allTypes[$mainType]);
+
+ $line = sprintf(' * %s', $mainType);
+
+ if ($subTypes !== []) {
+ $line .= sprintf(' or %s', implode(' or ', array_map(
+ static fn ($subType) => sprintf('%s', $subType),
+ $subTypes,
+ )));
+ }
+
+ $io->writeln($line);
+ }
+
+ $io->writeln('');
+ };
+
+ $io->writeln('Main Types');
+ $printSection($typesTable['main']);
+
+ $io->writeln('Array/Object Types');
+ $printSection($typesTable['array_object']);
+
+ $io->writeln('Date/Time Types');
+ $printSection($typesTable['date_time']);
+
+ $io->writeln('Other Types');
+ $allTypes = array_map(static fn () => [], $allTypes);
+ $printSection($allTypes);
+ }
+
+ private function createDocumentClassQuestion(string $questionText): Question
+ {
+ $question = new Question($questionText);
+ $question->setValidator(Validator::notBlank(...));
+ $question->setAutocompleterValues($this->mongoDBHelper->getDocumentsForAutocomplete());
+
+ return $question;
+ }
+
+ /** @return string[] */
+ private function verifyDocumentName(string $documentName): array
+ {
+ preg_match('/([^\x00-\x7F]+)/u', $documentName, $matches);
+
+ return $matches;
+ }
+
+ private function createClassManipulator(string $path, ConsoleStyle $io, bool $overwrite): ClassSourceManipulator
+ {
+ $manipulator = new ClassSourceManipulator(
+ sourceCode: $this->fileManager->getFileContents($path),
+ overwrite: $overwrite,
+ );
+
+ $manipulator->setIo($io);
+
+ return $manipulator;
+ }
+
+ private function getPathOfClass(string $class): string
+ {
+ return (new ClassDetails($class))->getPath();
+ }
+
+ /** @return string[] */
+ private function getPropertyNames(string $class): array
+ {
+ if (! class_exists($class)) {
+ return [];
+ }
+
+ $reflClass = new ReflectionClass($class);
+
+ return array_map(static fn (ReflectionProperty $prop) => $prop->getName(), $reflClass->getProperties());
+ }
+
+ private function getDocumentNamespace(): string
+ {
+ return $this->mongoDBHelper->getDocumentNamespace();
+ }
+
+ /** @return array */
+ private function getTypesMap(): array
+ {
+ return Type::getTypesMap();
}
}
diff --git a/src/MongoDB/DocumentClassGenerator.php b/src/MongoDB/DocumentClassGenerator.php
new file mode 100644
index 0000000..ffaba12
--- /dev/null
+++ b/src/MongoDB/DocumentClassGenerator.php
@@ -0,0 +1,92 @@
+generator->createClassNameDetails(
+ $documentClassDetails->getRelativeName(),
+ 'Repository\\',
+ 'Repository',
+ );
+
+ $collectionName = $this->mongoDBHelper->getPotentialCollectionName($documentClassDetails->getFullName());
+
+ $useStatements = new UseStatementGenerator([
+ $repoClassDetails->getFullName(),
+ ['Doctrine\\ODM\\MongoDB\\Mapping\\Annotations' => 'ODM'],
+ ]);
+
+ $templatePath = dirname(__DIR__, 2) . '/templates/mongodb/Document.tpl.php';
+ $documentPath = $this->generator->generateClass(
+ $documentClassDetails->getFullName(),
+ $templatePath,
+ [
+ 'use_statements' => $useStatements,
+ 'repository_class_name' => $repoClassDetails->getShortName(),
+ 'should_escape_collection_name' => $this->mongoDBHelper->isKeyword($collectionName),
+ 'collection_name' => $collectionName,
+ 'id_type' => $idType,
+ ],
+ );
+
+ if ($generateRepositoryClass) {
+ $this->generateRepositoryClass(
+ $repoClassDetails->getFullName(),
+ $documentClassDetails->getFullName(),
+ true,
+ );
+ }
+
+ return $documentPath;
+ }
+
+ public function generateRepositoryClass(
+ string $repositoryClass,
+ string $documentClass,
+ bool $includeExampleComments = true,
+ ): void {
+ $shortDocumentClass = Str::getShortClassName($documentClass);
+
+ $useStatements = new UseStatementGenerator([
+ $documentClass,
+ DocumentManager::class,
+ DocumentRepository::class,
+ ]);
+
+ $templatePath = dirname(__DIR__, 2) . '/templates/mongodb/Repository.tpl.php';
+ $this->generator->generateClass(
+ $repositoryClass,
+ $templatePath,
+ [
+ 'use_statements' => $useStatements,
+ 'document_class_name' => $shortDocumentClass,
+ 'include_example_comments' => $includeExampleComments,
+ ],
+ );
+ }
+}
diff --git a/src/MongoDB/MongoDBHelper.php b/src/MongoDB/MongoDBHelper.php
new file mode 100644
index 0000000..20a3f32
--- /dev/null
+++ b/src/MongoDB/MongoDBHelper.php
@@ -0,0 +1,171 @@
+registry;
+ }
+
+ public function getPotentialCollectionName(string $className): string
+ {
+ $shortClassName = Str::getShortClassName($className);
+
+ return Str::asSnakeCase($shortClassName);
+ }
+
+ public function isKeyword(string $name): bool
+ {
+ return in_array(lcfirst($name), self::MONGODB_KEYWORDS, true);
+ }
+
+ public function getDocumentNamespace(): string
+ {
+ $documentManager = $this->registry->getManager();
+
+ if (! $documentManager instanceof DocumentManager) {
+ return 'App\\Document';
+ }
+
+ $configuration = $documentManager->getConfiguration();
+ $documentNamespaces = [];
+
+ foreach ($configuration->getMetadataDriverImpl()->getAllClassNames() as $className) {
+ $parts = explode('\\', $className);
+ if (count($parts) <= 1) {
+ continue;
+ }
+
+ array_pop($parts);
+ $namespace = implode('\\', $parts);
+ $documentNamespaces[$namespace] = true;
+ }
+
+ $namespaces = array_keys($documentNamespaces);
+
+ // Prefer 'Document' namespace if it exists
+ foreach ($namespaces as $namespace) {
+ if (str_contains($namespace, '\\Document')) {
+ return $namespace;
+ }
+ }
+
+ return $namespaces[0] ?? 'App\\Document';
+ }
+
+ /** @return string[] */
+ public function getDocumentsForAutocomplete(): array
+ {
+ $documentManager = $this->registry->getManager();
+
+ if (! $documentManager instanceof DocumentManager) {
+ return [];
+ }
+
+ $configuration = $documentManager->getConfiguration();
+ $allDocuments = [];
+
+ foreach ($configuration->getMetadataDriverImpl()->getAllClassNames() as $className) {
+ $allDocuments[] = $className;
+ $allDocuments[] = Str::getShortClassName($className);
+ }
+
+ return $allDocuments;
+ }
+}
diff --git a/templates/mongodb/Document.tpl.php b/templates/mongodb/Document.tpl.php
new file mode 100644
index 0000000..720bd6a
--- /dev/null
+++ b/templates/mongodb/Document.tpl.php
@@ -0,0 +1,43 @@
+
+= "
+
+namespace = $namespace ?>;
+
+= $use_statements; ?>
+
+#[ODM\Document(repositoryClass: = $repository_class_name ?>::class)]
+#[ODM\Collection(name: '= $collection_name ?>')]
+
+class = $class_name."\n" ?>
+{
+
+ #[ODM\Id(strategy: 'UUID')]
+ private ?string $id = null;
+
+ public function getId(): ?string
+ {
+ return $this->id;
+ }
+
+ #[ODM\Id(strategy: 'UUID')]
+ private ?string $id = null;
+
+ public function getId(): ?string
+ {
+ return $this->id;
+ }
+
+ #[ODM\Id]
+ private ?string $id = null;
+
+ public function getId(): ?string
+ {
+ return $this->id;
+ }
+
+}
+
diff --git a/templates/mongodb/Repository.tpl.php b/templates/mongodb/Repository.tpl.php
new file mode 100644
index 0000000..cb68f65
--- /dev/null
+++ b/templates/mongodb/Repository.tpl.php
@@ -0,0 +1,40 @@
+= "
+
+namespace = $namespace ?>;
+
+= $use_statements; ?>
+
+/**
+ * @extends DocumentRepository<= $document_class_name ?>>
+ */
+class = $class_name ?> extends DocumentRepository
+{
+ public function __construct(DocumentManager $dm)
+ {
+ parent::__construct($dm, $dm->getUnitOfWork(), $dm->getClassMetadata(= $document_class_name ?>::class));
+ }
+
+
+ /**
+ * @return = $document_class_name ?>[]
+ */
+ public function findByExampleField(mixed $value): array
+ {
+ return $this->createQueryBuilder()
+ ->field('exampleField')->equals($value)
+ ->getQuery()
+ ->execute()
+ ->toArray();
+ }
+
+ public function findOneBySomeField(mixed $value): ?= $document_class_name ?>
+
+ {
+ return $this->createQueryBuilder()
+ ->field('exampleField')->equals($value)
+ ->getQuery()
+ ->getSingleResult();
+ }
+
+}
+
diff --git a/tests/Maker/MakeDocumentTest.php b/tests/Maker/MakeDocumentTest.php
new file mode 100644
index 0000000..1e0198d
--- /dev/null
+++ b/tests/Maker/MakeDocumentTest.php
@@ -0,0 +1,128 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Doctrine\Bundle\MongoDBMakerBundle\Tests\Maker;
+
+use Doctrine\Bundle\MongoDBMakerBundle\Maker\MakeDocument;
+use Generator;
+use Symfony\Bundle\MakerBundle\Test\MakerTestCase;
+use Symfony\Bundle\MakerBundle\Test\MakerTestDetails;
+use Symfony\Bundle\MakerBundle\Test\MakerTestRunner;
+use Symfony\Component\HttpKernel\KernelInterface;
+
+use function getenv;
+use function sprintf;
+use function strpos;
+use function substr;
+
+class MakeDocumentTest extends MakerTestCase
+{
+ protected function getMakerClass(): string
+ {
+ return MakeDocument::class;
+ }
+
+ private static function createMakeDocumentTest(bool $withDatabase = true): MakerTestDetails
+ {
+ return self::buildMakerTest()
+ ->preRun(static function (MakerTestRunner $runner) use ($withDatabase): void {
+ // @todo We have to re-install the bundles to get the recipe-contrib applied
+ // https://github.com/symfony/flex/issues/1076
+ // https://github.com/symfony/recipes/pull/1509
+ $runner->runProcess('composer remove --no-scripts --dev doctrine/mongodb-maker-bundle');
+ $runner->runProcess('composer config extra.symfony.allow-contrib true');
+ $runner->runProcess('composer require --dev doctrine/mongodb-maker-bundle');
+
+ if (! $withDatabase) {
+ return;
+ }
+
+ $runner->replaceInFile(
+ '.env',
+ 'mongodb://localhost:27017',
+ (string) getenv('MONGODB_URI'),
+ );
+ });
+ }
+
+ protected function createKernel(): KernelInterface
+ {
+ return new MakerTestKernel('dev', true);
+ }
+
+ public static function getTestDetails(): Generator
+ {
+ yield 'it_creates_a_new_class_basic' => [
+ self::createMakeDocumentTest()
+ ->run(static function (MakerTestRunner $runner): void {
+ $runner->runMaker([
+ // document class name
+ 'User',
+ // add not additional fields
+ '',
+ ]);
+
+ self::runDocumentTest($runner);
+ }),
+ ];
+ }
+
+ /** @param array $data */
+ private static function runDocumentTest(MakerTestRunner $runner, array $data = []): void
+ {
+ $runner->renderTemplateFile(
+ 'make-document/GeneratedDocumentTest.php.twig',
+ 'tests/GeneratedDocumentTest.php',
+ ['data' => $data],
+ );
+
+ //$runner->updateSchema();
+ $runner->runTests();
+ }
+
+ private static function runCustomTest(MakerTestRunner $runner, string $filename, bool $withDatabase = true): void
+ {
+ $runner->copy(
+ 'make-document/tests/' . $filename,
+ 'tests/GeneratedDocumentTest.php',
+ );
+
+ if ($withDatabase) {
+ $runner->updateSchema();
+ }
+
+ $runner->runTests();
+ }
+
+ private static function copyDocument(MakerTestRunner $runner, string $filename): void
+ {
+ $documentClassName = substr(
+ $filename,
+ 0,
+ strpos($filename, '-'),
+ );
+
+ $runner->copy(
+ sprintf('make-document/documents/attributes/%s', $filename),
+ sprintf('src/Document/%s.php', $documentClassName),
+ );
+ }
+
+ private static function copyDocumentDirectory(MakerTestRunner $runner, string $directory): void
+ {
+ $runner->copy(
+ sprintf('make-document/%s/attributes', $directory),
+ '',
+ );
+ }
+}
diff --git a/tests/Maker/MakerTestKernel.php b/tests/Maker/MakerTestKernel.php
new file mode 100644
index 0000000..401df75
--- /dev/null
+++ b/tests/Maker/MakerTestKernel.php
@@ -0,0 +1,38 @@
+load(static function (ContainerBuilder $container): void {
+ $container->loadFromExtension('doctrine_mongodb', [
+ 'default_connection' => 'default',
+ 'connections' => [
+ 'default' => ['server' => 'mongodb://localhost:27017'],
+ ],
+ 'document_managers' => [
+ 'default' => ['auto_mapping' => true],
+ ],
+ ]);
+ });
+ }
+}
diff --git a/tests/TestKernel.php b/tests/TestKernel.php
new file mode 100644
index 0000000..a83fc6d
--- /dev/null
+++ b/tests/TestKernel.php
@@ -0,0 +1,62 @@
+load(static function (ContainerBuilder $container): void {
+ $container->loadFromExtension('framework', ['secret' => 'S0ME_SECRET']);
+ $container->loadFromExtension('doctrine_mongodb', [
+ 'default_connection' => 'default',
+ 'connections' => [
+ 'default' => ['server' => 'mongodb://localhost:27017'],
+ ],
+ 'document_managers' => [
+ 'default' => ['auto_mapping' => true],
+ ],
+ ]);
+ $container->loadFromExtension('maker', []);
+ $container->loadFromExtension('mongodb_maker', ['generate_final_documents' => true]);
+ });
+ }
+
+ public function process(ContainerBuilder $container): void
+ {
+ /**
+ * Makes all makers public to help the tests
+ *
+ * @see \Symfony\Bundle\MakerBundle\Test\MakerTestKernel::process()
+ */
+ foreach ($container->findTaggedServiceIds(MakeCommandRegistrationPass::MAKER_TAG) as $id => $tags) {
+ $defn = $container->getDefinition($id);
+ $defn->setPublic(true);
+ }
+ }
+}
diff --git a/tests/fixtures/make-document/GeneratedDocumentTest.php.twig b/tests/fixtures/make-document/GeneratedDocumentTest.php.twig
new file mode 100644
index 0000000..f457764
--- /dev/null
+++ b/tests/fixtures/make-document/GeneratedDocumentTest.php.twig
@@ -0,0 +1,35 @@
+getContainer()
+ ->get('doctrine_mongodb')
+ ->getManager();
+
+ $dm->createQueryBuilder(User::class)
+ ->remove()
+ ->getQuery()
+ ->execute();
+
+ $user = new User();
+ {% for field, value in data %}
+ $user->{{ field }} = '{{ value }}';
+ {% endfor %}
+ $dm->persist($user);
+ $dm->flush();
+
+ $actualUser = $dm->getRepository(User::class)->findAll();
+
+ $this->assertcount(1, $actualUser);
+ }
+}
diff --git a/tests/fixtures/make-document/documents/User-basic.php b/tests/fixtures/make-document/documents/User-basic.php
new file mode 100644
index 0000000..4ee4c77
--- /dev/null
+++ b/tests/fixtures/make-document/documents/User-basic.php
@@ -0,0 +1,17 @@
+