mirror of
https://github.com/doctrine/mongodb-maker-bundle.git
synced 2026-03-23 22:42:07 +01:00
Support adding fields in make command (#10)
This commit is contained in:
@@ -13,7 +13,8 @@
|
||||
"doctrine/mongodb-odm-bundle": "^5.6",
|
||||
"symfony/http-kernel": "^7.4|^8",
|
||||
"symfony/maker-bundle": "^1@dev",
|
||||
"symfony/polyfill-php85": "^1.33"
|
||||
"symfony/polyfill-php85": "^1.33",
|
||||
"symfony/uid": "^7.4|^8"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/coding-standard": "^14",
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Doctrine\Bundle\MongoDBMakerBundle\Maker;
|
||||
|
||||
use Doctrine\Bundle\MongoDBBundle\DoctrineMongoDBBundle;
|
||||
use Doctrine\Bundle\MongoDBMakerBundle\MongoDB\ClassSourceManipulator;
|
||||
use Doctrine\Bundle\MongoDBMakerBundle\MongoDB\DocumentClassGenerator;
|
||||
use Doctrine\Bundle\MongoDBMakerBundle\MongoDB\MongoDBHelper;
|
||||
use Doctrine\Bundle\MongoDBMakerBundle\MongoDB\Validator;
|
||||
@@ -23,7 +24,6 @@ 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\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
@@ -152,7 +152,7 @@ final class MakeDocument extends AbstractMaker implements InputAwareMakerInterfa
|
||||
}
|
||||
|
||||
$currentFields = $this->getPropertyNames($documentClassDetails->getFullName());
|
||||
$manipulator = $this->createClassManipulator($documentPath, $io, $overwrite);
|
||||
$manipulator = $this->createClassManipulator($documentPath, $io);
|
||||
|
||||
$isFirstField = true;
|
||||
while (true) {
|
||||
@@ -163,7 +163,7 @@ final class MakeDocument extends AbstractMaker implements InputAwareMakerInterfa
|
||||
break;
|
||||
}
|
||||
|
||||
$manipulator->addEntityField($newField);
|
||||
$manipulator->addDocumentField($newField);
|
||||
$currentFields[] = $newField->propertyName;
|
||||
|
||||
$this->fileManager->dumpFile($documentPath, $manipulator->getSourceCode());
|
||||
@@ -351,11 +351,10 @@ final class MakeDocument extends AbstractMaker implements InputAwareMakerInterfa
|
||||
return $matches;
|
||||
}
|
||||
|
||||
private function createClassManipulator(string $path, ConsoleStyle $io, bool $overwrite): ClassSourceManipulator
|
||||
private function createClassManipulator(string $path, ConsoleStyle $io): ClassSourceManipulator
|
||||
{
|
||||
$manipulator = new ClassSourceManipulator(
|
||||
sourceCode: $this->fileManager->getFileContents($path),
|
||||
overwrite: $overwrite,
|
||||
);
|
||||
|
||||
$manipulator->setIo($io);
|
||||
|
||||
587
src/MongoDB/ClassSourceManipulator.php
Normal file
587
src/MongoDB/ClassSourceManipulator.php
Normal file
@@ -0,0 +1,587 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Bundle\MongoDBMakerBundle\MongoDB;
|
||||
|
||||
use Doctrine\ODM\MongoDB\Mapping\Attribute\Field;
|
||||
use Doctrine\ODM\MongoDB\Types\Type;
|
||||
use Exception;
|
||||
use PhpParser\Builder;
|
||||
use PhpParser\Lexer;
|
||||
use PhpParser\Node;
|
||||
use PhpParser\NodeTraverser;
|
||||
use PhpParser\NodeVisitor;
|
||||
use PhpParser\Parser;
|
||||
use PhpParser\PhpVersion;
|
||||
use PhpParser\Token;
|
||||
use ReflectionClass;
|
||||
use ReflectionException;
|
||||
use ReflectionParameter;
|
||||
use Symfony\Bundle\MakerBundle\ConsoleStyle;
|
||||
use Symfony\Bundle\MakerBundle\Str;
|
||||
use Symfony\Bundle\MakerBundle\Util\ClassNameValue;
|
||||
use Symfony\Bundle\MakerBundle\Util\ClassSource\Model\ClassProperty;
|
||||
use Symfony\Bundle\MakerBundle\Util\PrettyPrinter;
|
||||
|
||||
use function array_key_exists;
|
||||
use function array_keys;
|
||||
use function array_map;
|
||||
use function array_merge;
|
||||
use function array_search;
|
||||
use function array_splice;
|
||||
use function array_unshift;
|
||||
use function array_values;
|
||||
use function end;
|
||||
use function get_debug_type;
|
||||
use function gettype;
|
||||
use function is_int;
|
||||
use function preg_replace;
|
||||
use function sprintf;
|
||||
use function str_contains;
|
||||
use function str_replace;
|
||||
use function str_starts_with;
|
||||
use function strpos;
|
||||
use function strrpos;
|
||||
use function substr;
|
||||
|
||||
/** @internal */
|
||||
final class ClassSourceManipulator
|
||||
{
|
||||
private const string CONTEXT_OUTSIDE_CLASS = 'outside_class';
|
||||
private const string CONTEXT_CLASS = 'class';
|
||||
private const string CONTEXT_CLASS_METHOD = 'class_method';
|
||||
|
||||
private Parser $parser;
|
||||
private Lexer\Emulative $lexer;
|
||||
private PrettyPrinter $printer;
|
||||
private ConsoleStyle|null $io = null;
|
||||
|
||||
private array|null $oldStmts = null;
|
||||
|
||||
/** @var Token[] $oldTokens */
|
||||
private array $oldTokens = [];
|
||||
|
||||
/** @var Node[] $newStmts */
|
||||
private array $newStmts = [];
|
||||
|
||||
/** @var string[] $pendingComments */
|
||||
private array $pendingComments = [];
|
||||
|
||||
public function __construct(
|
||||
private string $sourceCode,
|
||||
private readonly bool $useAttributesForDoctrineMapping = true,
|
||||
) {
|
||||
$this->lexer = new Lexer\Emulative(
|
||||
PhpVersion::fromString('8.4'),
|
||||
);
|
||||
$this->parser = new Parser\Php7($this->lexer);
|
||||
|
||||
$this->printer = new PrettyPrinter();
|
||||
|
||||
$this->setSourceCode($sourceCode);
|
||||
}
|
||||
|
||||
public function setIo(ConsoleStyle $io): void
|
||||
{
|
||||
$this->io = $io;
|
||||
}
|
||||
|
||||
public function getSourceCode(): string
|
||||
{
|
||||
return $this->sourceCode;
|
||||
}
|
||||
|
||||
/** @throws ReflectionException */
|
||||
public function addDocumentField(ClassProperty $mapping): void
|
||||
{
|
||||
$typeHint = MongoDBHelper::getPropertyTypeForField($mapping->type);
|
||||
if ($typeHint && MongoDBHelper::canFieldTypeBeInferredByPropertyType($mapping->type, $typeHint)) {
|
||||
$mapping->needsTypeHint = false;
|
||||
}
|
||||
|
||||
if ($mapping->needsTypeHint) {
|
||||
$typeConstant = MongoDBHelper::getTypeConstant($mapping->type);
|
||||
if ($typeConstant) {
|
||||
$this->addUseStatementIfNecessary(Type::class);
|
||||
$mapping->type = $typeConstant;
|
||||
}
|
||||
}
|
||||
|
||||
$nullable = $mapping->nullable ?? false;
|
||||
|
||||
$attributes[] = $this->buildAttributeNode(Field::class, $mapping->getAttributes(), 'ODM');
|
||||
|
||||
$defaultValue = null;
|
||||
|
||||
if ($mapping->enumType !== null) {
|
||||
if ($typeHint === 'array') {
|
||||
// still need to add the use statement
|
||||
$this->addUseStatementIfNecessary($mapping->enumType);
|
||||
|
||||
if (! $nullable) {
|
||||
$defaultValue = new Node\Expr\Array_([], ['kind' => Node\Expr\Array_::KIND_SHORT]);
|
||||
}
|
||||
} else {
|
||||
$typeHint = $this->addUseStatementIfNecessary($mapping->enumType);
|
||||
}
|
||||
} elseif ($typeHint === 'array' && ! $nullable) {
|
||||
$defaultValue = new Node\Expr\Array_([], ['kind' => Node\Expr\Array_::KIND_SHORT]);
|
||||
} elseif ($typeHint && $typeHint[0] === '\\' && strpos($typeHint, '\\', 1) !== false) {
|
||||
$typeHint = $this->addUseStatementIfNecessary(substr($typeHint, 1));
|
||||
}
|
||||
|
||||
$propertyType = $typeHint;
|
||||
if ($propertyType && ! $defaultValue && $propertyType !== 'mixed' && $nullable) {
|
||||
// all property types
|
||||
$propertyType .= '|null';
|
||||
}
|
||||
|
||||
$this->addProperty(
|
||||
mapping: $mapping,
|
||||
defaultValue: $defaultValue,
|
||||
attributes: $attributes,
|
||||
comments: $mapping->comments,
|
||||
propertyType: $propertyType,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<Node\Attribute|Node\AttributeGroup> $attributes
|
||||
* @param string[] $comments
|
||||
*/
|
||||
public function addProperty(ClassProperty $mapping, mixed $defaultValue, array $attributes = [], array $comments = [], string|null $propertyType = null): void
|
||||
{
|
||||
$name = $mapping->propertyName;
|
||||
if ($this->propertyExists($name)) {
|
||||
// we never overwrite properties
|
||||
return;
|
||||
}
|
||||
|
||||
$newPropertyBuilder = (new Builder\Property($name))->makePublic();
|
||||
|
||||
if ($propertyType !== null) {
|
||||
$newPropertyBuilder->setType($propertyType);
|
||||
}
|
||||
|
||||
if ($this->useAttributesForDoctrineMapping) {
|
||||
foreach ($attributes as $attribute) {
|
||||
$newPropertyBuilder->addAttribute($attribute);
|
||||
}
|
||||
}
|
||||
|
||||
if ($comments) {
|
||||
$newPropertyBuilder->setDocComment($this->createDocBlock($comments));
|
||||
}
|
||||
|
||||
if ($defaultValue !== null || $mapping->nullable) {
|
||||
$newPropertyBuilder->setDefault($defaultValue);
|
||||
}
|
||||
|
||||
$newPropertyNode = $newPropertyBuilder->getNode();
|
||||
|
||||
$this->addNodeAfterProperties($newPropertyNode);
|
||||
}
|
||||
|
||||
/** @return string The alias to use when referencing this class */
|
||||
public function addUseStatementIfNecessary(string $class): string
|
||||
{
|
||||
$shortClassName = Str::getShortClassName($class);
|
||||
if ($this->isInSameNamespace($class)) {
|
||||
return $shortClassName;
|
||||
}
|
||||
|
||||
$namespaceNode = $this->getNamespaceNode();
|
||||
|
||||
$targetIndex = null;
|
||||
$addLineBreak = false;
|
||||
$lastUseStmtIndex = null;
|
||||
foreach ($namespaceNode->stmts as $index => $stmt) {
|
||||
if ($stmt instanceof Node\Stmt\Use_) {
|
||||
// I believe this is an array to account for use statements with {}
|
||||
foreach ($stmt->uses as $use) {
|
||||
$alias = $use->alias ? $use->alias->name : $use->name->getLast();
|
||||
|
||||
// the use statement already exists? Don't add it again
|
||||
if ($class === (string) $use->name) {
|
||||
return $alias;
|
||||
}
|
||||
|
||||
if (str_starts_with($class, $alias)) {
|
||||
return $class;
|
||||
}
|
||||
|
||||
if ($alias === $shortClassName) {
|
||||
// we have a conflicting alias!
|
||||
// to be safe, use the fully-qualified class name
|
||||
// everywhere and do not add another use statement
|
||||
return '\\' . $class;
|
||||
}
|
||||
}
|
||||
|
||||
// if $class is alphabetically before this use statement, place it before
|
||||
// only set $targetIndex the first time you find it
|
||||
if ($targetIndex === null && Str::areClassesAlphabetical($class, (string) $stmt->uses[0]->name)) {
|
||||
$targetIndex = $index;
|
||||
}
|
||||
|
||||
$lastUseStmtIndex = $index;
|
||||
} elseif ($stmt instanceof Node\Stmt\Class_) {
|
||||
if ($targetIndex !== null) {
|
||||
// we already found where to place the use statement
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// we hit the class! If there were any use statements,
|
||||
// then put this at the bottom of the use statement list
|
||||
if ($lastUseStmtIndex !== null) {
|
||||
$targetIndex = $lastUseStmtIndex + 1;
|
||||
} else {
|
||||
$targetIndex = $index;
|
||||
$addLineBreak = true;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($targetIndex === null) {
|
||||
throw new Exception('Could not find a class!');
|
||||
}
|
||||
|
||||
$newUseNode = (new Builder\Use_($class, Node\Stmt\Use_::TYPE_NORMAL))->getNode();
|
||||
array_splice(
|
||||
$namespaceNode->stmts,
|
||||
$targetIndex,
|
||||
0,
|
||||
$addLineBreak ? [$newUseNode, $this->createBlankLineNode(self::CONTEXT_OUTSIDE_CLASS)] : [$newUseNode],
|
||||
);
|
||||
|
||||
$this->updateSourceCodeFromNewStmts();
|
||||
|
||||
return $shortClassName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a PHPParser attribute node.
|
||||
*
|
||||
* @param string $attributeClass The attribute class which should be used for the attribute E.g. #[Column()]
|
||||
* @param array<string, mixed> $options The named arguments for the attribute ($key = argument name, $value = argument value)
|
||||
* @param ?string $attributePrefix If a prefix is provided, the node is built using the prefix. E.g. #[ORM\Column()]
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function buildAttributeNode(string $attributeClass, array $options, string|null $attributePrefix = null): Node\Attribute
|
||||
{
|
||||
$options = $this->sortOptionsByClassConstructorParameters($options, $attributeClass);
|
||||
|
||||
$nodeArguments = array_map(function (string $option, mixed $value) {
|
||||
$context = $this;
|
||||
|
||||
if ($value === null) {
|
||||
return new Node\NullableType(new Node\Identifier($option));
|
||||
}
|
||||
|
||||
// Use the Doctrine Types constant
|
||||
if ($option === 'type' && str_starts_with($value, 'Type::')) {
|
||||
return new Node\Arg(
|
||||
new Node\Expr\ConstFetch(new Node\Name($value)),
|
||||
false,
|
||||
false,
|
||||
[],
|
||||
new Node\Identifier($option),
|
||||
);
|
||||
}
|
||||
|
||||
if ($option === 'enumType') {
|
||||
return new Node\Arg(
|
||||
new Node\Expr\ConstFetch(new Node\Name(Str::getShortClassName($value) . '::class')),
|
||||
false,
|
||||
false,
|
||||
[],
|
||||
new Node\Identifier($option),
|
||||
);
|
||||
}
|
||||
|
||||
return new Node\Arg($context->buildNodeExprByValue($value), false, false, [], new Node\Identifier($option));
|
||||
}, array_keys($options), array_values($options));
|
||||
|
||||
$class = $attributePrefix ? sprintf('%s\\%s', $attributePrefix, Str::getShortClassName($attributeClass)) : Str::getShortClassName($attributeClass);
|
||||
|
||||
return new Node\Attribute(
|
||||
new Node\Name($class),
|
||||
$nodeArguments,
|
||||
);
|
||||
}
|
||||
|
||||
private function updateSourceCodeFromNewStmts(): void
|
||||
{
|
||||
$newCode = $this->printer->printFormatPreserving(
|
||||
$this->newStmts,
|
||||
$this->oldStmts,
|
||||
$this->oldTokens,
|
||||
);
|
||||
|
||||
// replace the 3 "fake" items that may be in the code (allowing for different indentation)
|
||||
$newCode = preg_replace('/(\ |\t)*private\ \$__EXTRA__LINE;/', '', $newCode);
|
||||
$newCode = preg_replace('/use __EXTRA__LINE;/', '', $newCode);
|
||||
$newCode = preg_replace('/(\ |\t)*\$__EXTRA__LINE;/', '', $newCode);
|
||||
|
||||
// process comment lines
|
||||
foreach ($this->pendingComments as $i => $comment) {
|
||||
// sanity check
|
||||
$placeholder = sprintf('$__COMMENT__VAR_%d;', $i);
|
||||
if (! str_contains($newCode, $placeholder)) {
|
||||
// this can happen if a comment is createSingleLineCommentNode()
|
||||
// is called, but then that generated code is ultimately not added
|
||||
continue;
|
||||
}
|
||||
|
||||
$newCode = str_replace($placeholder, '// ' . $comment, $newCode);
|
||||
}
|
||||
|
||||
$this->pendingComments = [];
|
||||
|
||||
$this->setSourceCode($newCode);
|
||||
}
|
||||
|
||||
private function setSourceCode(string $sourceCode): void
|
||||
{
|
||||
$this->sourceCode = $sourceCode;
|
||||
$this->oldStmts = $this->parser->parse($sourceCode);
|
||||
|
||||
$this->oldTokens = $this->parser->getTokens();
|
||||
|
||||
$traverser = new NodeTraverser();
|
||||
$traverser->addVisitor(new NodeVisitor\CloningVisitor());
|
||||
$traverser->addVisitor(new NodeVisitor\NameResolver(null, ['replaceNodes' => false]));
|
||||
$this->newStmts = $traverser->traverse($this->oldStmts);
|
||||
}
|
||||
|
||||
private function getClassNode(): Node\Stmt\Class_
|
||||
{
|
||||
$node = $this->findFirstNode(static fn ($node) => $node instanceof Node\Stmt\Class_);
|
||||
|
||||
if (! $node) {
|
||||
throw new Exception('Could not find class node');
|
||||
}
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
private function getNamespaceNode(): Node\Stmt\Namespace_
|
||||
{
|
||||
$node = $this->findFirstNode(static fn ($node) => $node instanceof Node\Stmt\Namespace_);
|
||||
|
||||
if (! $node) {
|
||||
throw new Exception('Could not find namespace node');
|
||||
}
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
private function findFirstNode(callable $filterCallback): Node|null
|
||||
{
|
||||
$traverser = new NodeTraverser();
|
||||
$visitor = new NodeVisitor\FirstFindingVisitor($filterCallback);
|
||||
$traverser->addVisitor($visitor);
|
||||
$traverser->traverse($this->newStmts);
|
||||
|
||||
return $visitor->getFoundNode();
|
||||
}
|
||||
|
||||
/** @param Node[] $ast */
|
||||
private function findLastNode(callable $filterCallback, array $ast): Node|null
|
||||
{
|
||||
$traverser = new NodeTraverser();
|
||||
$visitor = new NodeVisitor\FindingVisitor($filterCallback);
|
||||
$traverser->addVisitor($visitor);
|
||||
$traverser->traverse($ast);
|
||||
|
||||
$nodes = $visitor->getFoundNodes();
|
||||
$node = end($nodes);
|
||||
|
||||
return $node === false ? null : $node;
|
||||
}
|
||||
|
||||
private function createBlankLineNode(string $context): Node\Stmt\Use_|Node|Node\Stmt\Property|Node\Expr\Variable
|
||||
{
|
||||
return match ($context) {
|
||||
self::CONTEXT_OUTSIDE_CLASS => new Builder\Use_(
|
||||
'__EXTRA__LINE',
|
||||
Node\Stmt\Use_::TYPE_NORMAL,
|
||||
)
|
||||
->getNode(),
|
||||
self::CONTEXT_CLASS => (new Builder\Property('__EXTRA__LINE'))
|
||||
->makePrivate()
|
||||
->getNode(),
|
||||
self::CONTEXT_CLASS_METHOD => new Node\Expr\Variable(
|
||||
'__EXTRA__LINE',
|
||||
),
|
||||
default => throw new Exception('Unknown context: ' . $context),
|
||||
};
|
||||
}
|
||||
|
||||
/** @param string[] $commentLines */
|
||||
private function createDocBlock(array $commentLines): string
|
||||
{
|
||||
$docBlock = "/**\n";
|
||||
foreach ($commentLines as $commentLine) {
|
||||
if ($commentLine) {
|
||||
$docBlock .= sprintf(" *%s\n", $commentLine);
|
||||
} else {
|
||||
// avoid the empty, extra space on blank lines
|
||||
$docBlock .= " *\n";
|
||||
}
|
||||
}
|
||||
|
||||
return $docBlock . "\n */";
|
||||
}
|
||||
|
||||
private function isInSameNamespace(string $class): bool
|
||||
{
|
||||
$namespace = substr($class, 0, strrpos($class, '\\'));
|
||||
|
||||
return $this->getNamespaceNode()->name->toCodeString() === $namespace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds this new node where a new property should go.
|
||||
*
|
||||
* Useful for adding properties, or adding a constructor.
|
||||
*/
|
||||
private function addNodeAfterProperties(Node $newNode): void
|
||||
{
|
||||
$classNode = $this->getClassNode();
|
||||
|
||||
// try to add after last property
|
||||
$targetNode = $this->findLastNode(static fn ($node) => $node instanceof Node\Stmt\Property, [$classNode]);
|
||||
|
||||
// otherwise, try to add after the last constant
|
||||
if (! $targetNode) {
|
||||
$targetNode = $this->findLastNode(static fn ($node) => $node instanceof Node\Stmt\ClassConst, [$classNode]);
|
||||
}
|
||||
|
||||
// otherwise, try to add after the last trait
|
||||
if (! $targetNode) {
|
||||
$targetNode = $this->findLastNode(static fn ($node) => $node instanceof Node\Stmt\TraitUse, [$classNode]);
|
||||
}
|
||||
|
||||
// add the new property after this node
|
||||
if ($targetNode) {
|
||||
$index = array_search($targetNode, $classNode->stmts);
|
||||
|
||||
array_splice(
|
||||
$classNode->stmts,
|
||||
$index + 1,
|
||||
0,
|
||||
[$this->createBlankLineNode(self::CONTEXT_CLASS), $newNode],
|
||||
);
|
||||
|
||||
$this->updateSourceCodeFromNewStmts();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// put right at the beginning of the class
|
||||
// add an empty line, unless the class is totally empty
|
||||
if (! empty($classNode->stmts)) {
|
||||
array_unshift($classNode->stmts, $this->createBlankLineNode(self::CONTEXT_CLASS));
|
||||
}
|
||||
|
||||
array_unshift($classNode->stmts, $newNode);
|
||||
$this->updateSourceCodeFromNewStmts();
|
||||
}
|
||||
|
||||
private function propertyExists(string $propertyName): bool
|
||||
{
|
||||
foreach ($this->getClassNode()->stmts as $i => $node) {
|
||||
if ($node instanceof Node\Stmt\Property && $node->props[0]->name->toString() === $propertyName) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $value
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
private function buildArrayNode(array $value): Node\Expr\Array_
|
||||
{
|
||||
$context = $this;
|
||||
$arrayItems = array_map(static fn ($key, $value) => new Node\Expr\ArrayItem(
|
||||
$context->buildNodeExprByValue($value),
|
||||
! is_int($key) ? $context->buildNodeExprByValue($key) : null,
|
||||
), array_keys($value), array_values($value));
|
||||
|
||||
return new Node\Expr\Array_($arrayItems, ['kind' => Node\Expr\Array_::KIND_SHORT]);
|
||||
}
|
||||
|
||||
private function buildClassNameValueNode(mixed $value): Node\Expr\ConstFetch
|
||||
{
|
||||
if (! ($value instanceof ClassNameValue)) {
|
||||
throw new Exception(sprintf('Cannot build a node expr for value of type "%s"', get_debug_type($value)));
|
||||
}
|
||||
|
||||
return new Node\Expr\ConstFetch(
|
||||
new Node\Name(
|
||||
sprintf('%s::class', $value->isSelf() ? 'self' : $value->getShortName()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* builds a PHPParser Expr Node based on the value given in $value
|
||||
* throws an Exception when the given $value is not resolvable by this method.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
private function buildNodeExprByValue(mixed $value): Node\Expr
|
||||
{
|
||||
return match (gettype($value)) {
|
||||
'string' => new Node\Scalar\String_($value),
|
||||
'integer' => new Node\Scalar\LNumber($value),
|
||||
'double' => new Node\Scalar\DNumber($value),
|
||||
'boolean' => new Node\Expr\ConstFetch(new Node\Name($value ? 'true' : 'false')),
|
||||
'array' => $this->buildArrayNode($value),
|
||||
default => $this->buildClassNameValueNode($value),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* sort the given options based on the constructor parameters for the given $classString
|
||||
* this prevents code inspections warnings for IDEs like intellij/phpstorm.
|
||||
*
|
||||
* option keys that are not found in the constructor will be added at the end of the sorted array
|
||||
*
|
||||
* @param array<string, mixed> $options
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
private function sortOptionsByClassConstructorParameters(array $options, string $classString): array
|
||||
{
|
||||
if (str_starts_with($classString, 'ODM\\')) {
|
||||
$classString = sprintf('Doctrine\\ODM\\MongoDB\\Mapping\\%s', substr($classString, 4));
|
||||
}
|
||||
|
||||
$constructorParameterNames = array_map(static fn (ReflectionParameter $reflectionParameter) => $reflectionParameter->getName(), (new ReflectionClass($classString))->getConstructor()->getParameters());
|
||||
|
||||
$sorted = [];
|
||||
foreach ($constructorParameterNames as $name) {
|
||||
if (! array_key_exists($name, $options)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sorted[$name] = $options[$name];
|
||||
unset($options[$name]);
|
||||
}
|
||||
|
||||
return array_merge($sorted, $options);
|
||||
}
|
||||
}
|
||||
@@ -4,16 +4,25 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Bundle\MongoDBMakerBundle\MongoDB;
|
||||
|
||||
use DateTime;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ODM\MongoDB\DocumentManager;
|
||||
use Doctrine\ODM\MongoDB\Types\Type;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use ReflectionClass;
|
||||
use ReflectionException;
|
||||
use ReflectionNamedType;
|
||||
use Symfony\Bundle\MakerBundle\Str;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
|
||||
use function array_first;
|
||||
use function array_flip;
|
||||
use function array_keys;
|
||||
use function array_pop;
|
||||
use function count;
|
||||
use function explode;
|
||||
use function implode;
|
||||
use function sprintf;
|
||||
use function str_contains;
|
||||
|
||||
/** @internal */
|
||||
@@ -98,4 +107,85 @@ final class MongoDBHelper
|
||||
|
||||
return $allDocuments;
|
||||
}
|
||||
|
||||
public static function canFieldTypeBeInferredByPropertyType(string $fieldType, string $propertyType): bool
|
||||
{
|
||||
return match ($propertyType) {
|
||||
'\\' . DateTime::class => $fieldType === Type::DATE,
|
||||
'\\' . DateTimeImmutable::class => $fieldType === Type::DATE_IMMUTABLE,
|
||||
Uuid::class => $fieldType === Type::UUID,
|
||||
'array' => $fieldType === Type::HASH,
|
||||
'bool' => $fieldType === Type::BOOL,
|
||||
'float' => $fieldType === Type::FLOAT,
|
||||
'int' => $fieldType === Type::INT,
|
||||
'string' => $fieldType === Type::STRING,
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
|
||||
/** @throws ReflectionException */
|
||||
public static function getPropertyTypeForField(string $fieldType): string|null
|
||||
{
|
||||
$propertyType = match ($fieldType) {
|
||||
Type::STRING,
|
||||
Type::BINDATA,
|
||||
Type::BINDATABYTEARRAY,
|
||||
Type::BINDATACUSTOM,
|
||||
Type::BINDATAFUNC,
|
||||
Type::BINDATAMD5,
|
||||
Type::BINDATAUUID,
|
||||
Type::BINDATAUUIDRFC4122,
|
||||
Type::DECIMAL128,
|
||||
Type::ID,
|
||||
Type::TIMESTAMP => 'string',
|
||||
Type::INT => 'int',
|
||||
Type::FLOAT => 'float',
|
||||
Type::BOOL => 'bool',
|
||||
Type::HASH,
|
||||
Type::COLLECTION,
|
||||
Type::OBJECTID,
|
||||
Type::VECTOR_FLOAT32,
|
||||
Type::VECTOR_INT8,
|
||||
Type::VECTOR_PACKED_BIT => 'array',
|
||||
Type::DATE => '\\' . DateTime::class,
|
||||
Type::DATE_IMMUTABLE => '\\' . DateTimeImmutable::class,
|
||||
Type::UUID => '\\' . Uuid::class,
|
||||
default => null,
|
||||
};
|
||||
|
||||
$typesMap = Type::getTypesMap();
|
||||
if ($propertyType !== null || ! isset($typesMap[$fieldType])) {
|
||||
return $propertyType;
|
||||
}
|
||||
|
||||
$reflection = new ReflectionClass($typesMap[$fieldType]);
|
||||
$returnType = $reflection->getMethod('convertToPHPValue')->getReturnType();
|
||||
|
||||
/*
|
||||
* we do not support union and intersection types
|
||||
*/
|
||||
if (! $returnType instanceof ReflectionNamedType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $returnType->isBuiltin() ? $returnType->getName() : '\\' . $returnType->getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the string "field type", this returns the "Types::STRING" constant.
|
||||
*
|
||||
* This is, effectively, a reverse lookup: given the final string, give us
|
||||
* the constant to be used in the generated code.
|
||||
*/
|
||||
public static function getTypeConstant(string $fieldType): string|null
|
||||
{
|
||||
$reflection = new ReflectionClass(Type::class);
|
||||
$constants = array_flip($reflection->getConstants());
|
||||
|
||||
if (! isset($constants[$fieldType])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return sprintf('Type::%s', $constants[$fieldType]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,27 +14,12 @@ class <?= $class_name."\n" ?>
|
||||
{
|
||||
<?php if (EntityIdTypeEnum::UUID === $id_type): ?>
|
||||
#[ODM\Id(strategy: 'UUID')]
|
||||
private ?string $id = null;
|
||||
|
||||
public function getId(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
public ?string $id = null;
|
||||
<?php elseif (EntityIdTypeEnum::ULID === $id_type): ?>
|
||||
#[ODM\Id(strategy: 'UUID')]
|
||||
private ?string $id = null;
|
||||
|
||||
public function getId(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
public ?string $id = null;
|
||||
<?php else: ?>
|
||||
#[ODM\Id]
|
||||
private ?string $id = null;
|
||||
|
||||
public function getId(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
public ?string $id = null;
|
||||
<?php endif ?>
|
||||
}
|
||||
|
||||
@@ -65,6 +65,36 @@ class MakeDocumentTest extends MakerTestCase
|
||||
self::runDocumentTest($runner);
|
||||
}),
|
||||
];
|
||||
|
||||
yield 'it_creates_a_new_class_with_fields' => [
|
||||
self::createMakeDocumentTest()
|
||||
->run(static function (MakerTestRunner $runner): void {
|
||||
$runner->runMaker([
|
||||
// document class name
|
||||
'User',
|
||||
// additional fields
|
||||
'some_id',
|
||||
'uuid',
|
||||
'y',
|
||||
'firstName',
|
||||
'string',
|
||||
'n',
|
||||
'isActive',
|
||||
'bool',
|
||||
'n',
|
||||
'friends',
|
||||
'collection',
|
||||
'n',
|
||||
'',
|
||||
]);
|
||||
|
||||
self::runDocumentTest($runner, [
|
||||
'firstName' => 'Pauline',
|
||||
'isActive' => true,
|
||||
'friends' => ['Alice', 'Bob'],
|
||||
]);
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $data */
|
||||
|
||||
@@ -13,16 +13,24 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Bundle\MongoDBMakerBundle\Tests\MongoDB;
|
||||
|
||||
use DateTime;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Bundle\MongoDBMakerBundle\MongoDB\MongoDBHelper;
|
||||
use Doctrine\ODM\MongoDB\Configuration;
|
||||
use Doctrine\ODM\MongoDB\DocumentManager;
|
||||
use Doctrine\ODM\MongoDB\Mapping\Driver\AttributeDriver;
|
||||
use Doctrine\ODM\MongoDB\Types\Type;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Generator;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\MockObject\Stub;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ReflectionException;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
|
||||
use function class_exists;
|
||||
use function get_debug_type;
|
||||
|
||||
class MongoDBHelperTest extends TestCase
|
||||
{
|
||||
@@ -141,4 +149,76 @@ class MongoDBHelperTest extends TestCase
|
||||
'App\\Document',
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('canFieldTypeBeInferredByPropertyTypeDataProvider')]
|
||||
public function testCanFieldTypeBeInferredByPropertyType(string $fieldType, string $propertyType, bool $expectedResult): void
|
||||
{
|
||||
$this->assertEquals($expectedResult, MongoDBHelper::canFieldTypeBeInferredByPropertyType($fieldType, $propertyType));
|
||||
}
|
||||
|
||||
public static function canFieldTypeBeInferredByPropertyTypeDataProvider(): Generator
|
||||
{
|
||||
yield 'string field type with string property type' => [Type::STRING, 'string', true];
|
||||
yield 'int field type with int property type' => [Type::INT, 'int', true];
|
||||
yield 'float field type with float property type' => [Type::FLOAT, 'float', true];
|
||||
yield 'bool field type with bool property type' => [Type::BOOL, 'bool', true];
|
||||
yield 'date field type with DateTime property type' => [Type::DATE, '\\' . DateTime::class, true];
|
||||
yield 'date_immutable field type with DateTimeImmutable property type' => [Type::DATE_IMMUTABLE, '\\' . DateTimeImmutable::class, true];
|
||||
yield 'has field type with array property type' => [Type::HASH, 'array', true];
|
||||
yield 'uuid field type with Uuid property type' => [Type::UUID, Uuid::class, true];
|
||||
|
||||
yield 'object ID with string property type' => [Type::OBJECTID, 'string', false];
|
||||
yield 'collection field type with array property type' => [Type::COLLECTION, 'array', false];
|
||||
}
|
||||
|
||||
/** @throws ReflectionException */
|
||||
#[DataProvider('getPropertyTypeForFieldDataProvider')]
|
||||
public function testGetPropertyTypeForField(string $fieldType, string|null $expectedPropertyType): void
|
||||
{
|
||||
$this->assertSame($expectedPropertyType, MongoDBHelper::getPropertyTypeForField($fieldType));
|
||||
}
|
||||
|
||||
public static function getPropertyTypeForFieldDataProvider(): Generator
|
||||
{
|
||||
Type::registerType('custom_type', CustomType::class);
|
||||
|
||||
yield [Type::STRING, 'string'];
|
||||
yield [Type::BINDATA, 'string'];
|
||||
yield [Type::BINDATABYTEARRAY, 'string'];
|
||||
yield [Type::BINDATACUSTOM, 'string'];
|
||||
yield [Type::BINDATAFUNC, 'string'];
|
||||
yield [Type::BINDATAUUID, 'string'];
|
||||
yield [Type::BINDATAUUIDRFC4122, 'string'];
|
||||
yield [Type::DECIMAL128, 'string'];
|
||||
yield [Type::ID, 'string'];
|
||||
yield [Type::TIMESTAMP, 'string'];
|
||||
yield [Type::INT, 'int'];
|
||||
yield [Type::FLOAT, 'float'];
|
||||
yield [Type::HASH, 'array'];
|
||||
yield [Type::COLLECTION, 'array'];
|
||||
yield [Type::OBJECTID, 'array'];
|
||||
yield [Type::VECTOR_FLOAT32, 'array'];
|
||||
yield [Type::VECTOR_INT8, 'array'];
|
||||
yield [Type::VECTOR_PACKED_BIT, 'array'];
|
||||
yield [Type::DATE, '\\' . DateTime::class];
|
||||
yield [Type::DATE_IMMUTABLE, '\\' . DateTimeImmutable::class];
|
||||
yield [Type::UUID, '\\' . Uuid::class];
|
||||
yield ['unknown_type', null];
|
||||
yield [Type::RAW, null];
|
||||
yield ['custom_type', 'string'];
|
||||
}
|
||||
|
||||
public function testGetTypeConstant(): void
|
||||
{
|
||||
$this->assertSame('Type::STRING', MongoDBHelper::getTypeConstant(Type::STRING));
|
||||
$this->assertNull(MongoDBHelper::getTypeConstant('unknown_type'));
|
||||
}
|
||||
}
|
||||
|
||||
class CustomType extends Type
|
||||
{
|
||||
public function convertToPHPValue(mixed $value): string
|
||||
{
|
||||
return 'foo';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ class GeneratedDocumentTest extends KernelTestCase
|
||||
|
||||
$user = new User();
|
||||
{% for field, value in data %}
|
||||
$user->{{ field }} = '{{ value }}';
|
||||
$user->{{ field }} = {{ value|json_encode|raw }};
|
||||
{% endfor %}
|
||||
$dm->persist($user);
|
||||
$dm->flush();
|
||||
|
||||
Reference in New Issue
Block a user