File: /var/www/payments-gateway/vendor/symfony/maker-bundle/src/Util/ClassSourceManipulator.php
<?php
/*
* This file is part of the Symfony MakerBundle package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\MakerBundle\Util;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Embedded;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToMany;
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\OneToMany;
use Doctrine\ORM\Mapping\OneToOne;
use PhpParser\Builder;
use PhpParser\BuilderHelpers;
use PhpParser\Lexer;
use PhpParser\Node;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor;
use PhpParser\Parser;
use PhpParser\PhpVersion;
use Symfony\Bundle\MakerBundle\ConsoleStyle;
use Symfony\Bundle\MakerBundle\Doctrine\BaseCollectionRelation;
use Symfony\Bundle\MakerBundle\Doctrine\BaseRelation;
use Symfony\Bundle\MakerBundle\Doctrine\DoctrineHelper;
use Symfony\Bundle\MakerBundle\Doctrine\RelationManyToMany;
use Symfony\Bundle\MakerBundle\Doctrine\RelationManyToOne;
use Symfony\Bundle\MakerBundle\Doctrine\RelationOneToMany;
use Symfony\Bundle\MakerBundle\Doctrine\RelationOneToOne;
use Symfony\Bundle\MakerBundle\Str;
use Symfony\Bundle\MakerBundle\Util\ClassSource\Model\ClassProperty;
/**
* @internal
*/
final class ClassSourceManipulator
{
private const CONTEXT_OUTSIDE_CLASS = 'outside_class';
private const CONTEXT_CLASS = 'class';
private const CONTEXT_CLASS_METHOD = 'class_method';
private const DEFAULT_VALUE_NONE = '__default_value_none';
private Parser $parser;
private Lexer\Emulative $lexer;
private PrettyPrinter $printer;
private ?ConsoleStyle $io = null;
private ?array $oldStmts = null;
private array $oldTokens = [];
private array $newStmts = [];
private array $pendingComments = [];
public function __construct(
private string $sourceCode,
private bool $overwrite = false,
private bool $useAttributesForDoctrineMapping = true,
) {
$this->lexer = new Lexer\Emulative(
PhpVersion::fromString('8.1'),
);
$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;
}
public function addEntityField(ClassProperty $mapping): void
{
$typeHint = DoctrineHelper::getPropertyTypeForColumn($mapping->type);
if ($typeHint && DoctrineHelper::canColumnTypeBeInferredByPropertyType($mapping->type, $typeHint)) {
$mapping->needsTypeHint = false;
}
if ($mapping->needsTypeHint) {
$typeConstant = DoctrineHelper::getTypeConstant($mapping->type);
if ($typeConstant) {
$this->addUseStatementIfNecessary(Types::class);
$mapping->type = $typeConstant;
}
}
// 2) USE property type on property below, nullable
// 3) If default value, then NOT nullable
$nullable = $mapping->nullable ?? false;
$attributes[] = $this->buildAttributeNode(Column::class, $mapping->getAttributes(), 'ORM');
$defaultValue = null;
$commentLines = [];
if (null !== $mapping->enumType) {
if ('array' === $typeHint) {
// still need to add the use statement
$this->addUseStatementIfNecessary($mapping->enumType);
$commentLines = [\sprintf('@return %s[]', Str::getShortClassName($mapping->enumType))];
if ($nullable) {
$commentLines[0] = \sprintf('%s|null', $commentLines[0]);
} else {
$defaultValue = new Node\Expr\Array_([], ['kind' => Node\Expr\Array_::KIND_SHORT]);
}
} else {
$typeHint = $this->addUseStatementIfNecessary($mapping->enumType);
}
} elseif ('array' === $typeHint && !$nullable) {
$defaultValue = new Node\Expr\Array_([], ['kind' => Node\Expr\Array_::KIND_SHORT]);
} elseif ($typeHint && '\\' === $typeHint[0] && false !== strpos($typeHint, '\\', 1)) {
$typeHint = $this->addUseStatementIfNecessary(substr($typeHint, 1));
}
$propertyType = $typeHint;
if ($propertyType && !$defaultValue && 'mixed' !== $propertyType) {
// all property types
$propertyType = '?'.$propertyType;
}
$this->addProperty(
name: $mapping->propertyName,
defaultValue: $defaultValue,
attributes: $attributes,
comments: $mapping->comments,
propertyType: $propertyType
);
$this->addGetter(
$mapping->propertyName,
$typeHint,
// getter methods always have nullable return values
// because even though these are required in the db, they may not be set yet
// unless there is a default value
null === $defaultValue && 'mixed' !== $propertyType,
$commentLines
);
// don't generate setters for id fields
if (!($mapping->id ?? false)) {
$this->addSetter($mapping->propertyName, $typeHint, $nullable && 'mixed' !== $propertyType);
}
}
public function addEmbeddedEntity(string $propertyName, string $className): void
{
$typeHint = $this->addUseStatementIfNecessary($className);
$attributes = [
$this->buildAttributeNode(
Embedded::class,
['class' => new ClassNameValue($className, $typeHint)],
'ORM'
),
];
$this->addProperty(
name: $propertyName,
attributes: $attributes,
propertyType: $typeHint,
);
// logic to avoid re-adding the same ArrayCollection line
$addEmbedded = true;
if ($this->getConstructorNode()) {
// We print the constructor to a string, then
// look for "$this->propertyName = "
$constructorString = $this->printer->prettyPrint([$this->getConstructorNode()]);
if (str_contains($constructorString, \sprintf('$this->%s = ', $propertyName))) {
$addEmbedded = false;
}
}
if ($addEmbedded) {
$this->addStatementToConstructor(
new Node\Stmt\Expression(new Node\Expr\Assign(
new Node\Expr\PropertyFetch(new Node\Expr\Variable('this'), $propertyName),
new Node\Expr\New_(new Node\Name($typeHint))
))
);
}
$this->addGetter($propertyName, $typeHint, false);
$this->addSetter($propertyName, $typeHint, false);
}
public function addManyToOneRelation(RelationManyToOne $manyToOne): void
{
$this->addSingularRelation($manyToOne);
}
public function addOneToOneRelation(RelationOneToOne $oneToOne): void
{
$this->addSingularRelation($oneToOne);
}
public function addOneToManyRelation(RelationOneToMany $oneToMany): void
{
$this->addCollectionRelation($oneToMany);
}
public function addManyToManyRelation(RelationManyToMany $manyToMany): void
{
$this->addCollectionRelation($manyToMany);
}
public function addInterface(string $interfaceName): void
{
$this->addUseStatementIfNecessary($interfaceName);
$this->getClassNode()->implements[] = new Node\Name(Str::getShortClassName($interfaceName));
$this->updateSourceCodeFromNewStmts();
}
/**
* @param string $trait the fully-qualified trait name
*/
public function addTrait(string $trait): void
{
$importedClassName = $this->addUseStatementIfNecessary($trait);
/** @var Node\Stmt\TraitUse[] $traitNodes */
$traitNodes = $this->findAllNodes(fn ($node) => $node instanceof Node\Stmt\TraitUse);
foreach ($traitNodes as $node) {
if ($node->traits[0]->toString() === $importedClassName) {
return;
}
}
$traitNodes[] = new Node\Stmt\TraitUse([new Node\Name($importedClassName)]);
$classNode = $this->getClassNode();
if (!empty($classNode->stmts) && 1 === \count($traitNodes)) {
$traitNodes[] = $this->createBlankLineNode(self::CONTEXT_CLASS);
}
// avoid all the use traits in class for unshift all the new UseTrait
// in the right order.
foreach ($classNode->stmts as $key => $node) {
if ($node instanceof Node\Stmt\TraitUse) {
unset($classNode->stmts[$key]);
}
}
array_unshift($classNode->stmts, ...$traitNodes);
$this->updateSourceCodeFromNewStmts();
}
public function addAccessorMethod(string $propertyName, string $methodName, $returnType, bool $isReturnTypeNullable, array $commentLines = [], $typeCast = null): void
{
$this->addCustomGetter($propertyName, $methodName, $returnType, $isReturnTypeNullable, $commentLines, $typeCast);
}
public function addGetter(string $propertyName, $returnType, bool $isReturnTypeNullable, array $commentLines = []): void
{
$methodName = $this->getGetterName($propertyName, $returnType);
$this->addCustomGetter($propertyName, $methodName, $returnType, $isReturnTypeNullable, $commentLines);
}
private function getGetterName(string $propertyName, $returnType): string
{
if ('bool' !== $returnType) {
return 'get'.Str::asCamelCase($propertyName);
}
// exclude is & has from getter definition if already in property name
if (0 !== strncasecmp($propertyName, 'is', 2) && 0 !== strncasecmp($propertyName, 'has', 3)) {
return 'is'.Str::asCamelCase($propertyName);
}
return Str::asLowerCamelCase($propertyName);
}
public function addSetter(string $propertyName, ?string $type, bool $isNullable, array $commentLines = []): void
{
$builder = $this->createSetterNodeBuilder($propertyName, $type, $isNullable, $commentLines);
$builder->addStmt(
new Node\Stmt\Expression(new Node\Expr\Assign(
new Node\Expr\PropertyFetch(new Node\Expr\Variable('this'), $propertyName),
new Node\Expr\Variable($propertyName)
))
);
$this->makeMethodFluent($builder);
$this->addMethod($builder->getNode());
}
/**
* @param Node[] $params
*/
public function addConstructor(array $params, string $methodBody): void
{
if (null !== $this->getConstructorNode()) {
throw new \LogicException('Constructor already exists.');
}
$methodBuilder = $this->createMethodBuilder('__construct', null, false);
$this->addMethodParams($methodBuilder, $params);
$this->addMethodBody($methodBuilder, $methodBody);
$this->addNodeAfterProperties($methodBuilder->getNode());
$this->updateSourceCodeFromNewStmts();
}
/**
* @param Node[] $params
*/
public function addMethodBuilder(Builder\Method $methodBuilder, array $params = [], ?string $methodBody = null): void
{
$this->addMethodParams($methodBuilder, $params);
if ($methodBody) {
$this->addMethodBody($methodBuilder, $methodBody);
}
$this->addMethod($methodBuilder->getNode());
}
public function addMethodBody(Builder\Method $methodBuilder, string $methodBody): void
{
$nodes = $this->parser->parse($methodBody);
$methodBuilder->addStmts($nodes);
}
public function createMethodBuilder(string $methodName, $returnType, bool $isReturnTypeNullable, array $commentLines = []): Builder\Method
{
$methodNodeBuilder = (new Builder\Method($methodName))
->makePublic();
if (null !== $returnType) {
if (class_exists($returnType) || interface_exists($returnType)) {
$returnType = $this->addUseStatementIfNecessary($returnType);
}
$methodNodeBuilder->setReturnType($isReturnTypeNullable ? new Node\NullableType(new Node\Identifier($returnType)) : $returnType);
}
if ($commentLines) {
$methodNodeBuilder->setDocComment($this->createDocBlock($commentLines));
}
return $methodNodeBuilder;
}
public function createMethodLevelCommentNode(string $comment)
{
return $this->createSingleLineCommentNode($comment, self::CONTEXT_CLASS_METHOD);
}
public function createMethodLevelBlankLine()
{
return $this->createBlankLineNode(self::CONTEXT_CLASS_METHOD);
}
/**
* @param array<Node\Attribute|Node\AttributeGroup> $attributes
*/
public function addProperty(string $name, $defaultValue = self::DEFAULT_VALUE_NONE, array $attributes = [], array $comments = [], ?string $propertyType = null): void
{
if ($this->propertyExists($name)) {
// we never overwrite properties
return;
}
$newPropertyBuilder = (new Builder\Property($name))->makePrivate();
if (null !== $propertyType) {
$newPropertyBuilder->setType($propertyType);
}
if ($this->useAttributesForDoctrineMapping) {
foreach ($attributes as $attribute) {
$newPropertyBuilder->addAttribute($attribute);
}
}
if ($comments) {
$newPropertyBuilder->setDocComment($this->createDocBlock($comments));
}
if (self::DEFAULT_VALUE_NONE !== $defaultValue) {
$newPropertyBuilder->setDefault($defaultValue);
}
$newPropertyNode = $newPropertyBuilder->getNode();
$this->addNodeAfterProperties($newPropertyNode);
}
public function addAttributeToClass(string $attributeClass, array $options): void
{
$this->addUseStatementIfNecessary($attributeClass);
$classNode = $this->getClassNode();
$attributePrefix = str_starts_with($attributeClass, 'ORM\\') ? 'ORM' : null;
$classNode->attrGroups[] = new Node\AttributeGroup([$this->buildAttributeNode($attributeClass, $options, $attributePrefix)]);
$this->updateSourceCodeFromNewStmts();
}
private function addCustomGetter(string $propertyName, string $methodName, $returnType, bool $isReturnTypeNullable, array $commentLines = [], $typeCast = null): void
{
$propertyFetch = new Node\Expr\PropertyFetch(new Node\Expr\Variable('this'), $propertyName);
if (null !== $typeCast) {
switch ($typeCast) {
case 'string':
$propertyFetch = new Node\Expr\Cast\String_($propertyFetch);
break;
default:
// implement other cases if/when the library needs them
throw new \Exception('Not implemented');
}
}
$getterNodeBuilder = (new Builder\Method($methodName))
->makePublic()
->addStmt(
new Node\Stmt\Return_($propertyFetch)
);
if (null !== $returnType) {
$getterNodeBuilder->setReturnType($isReturnTypeNullable ? new Node\NullableType(new Node\Identifier($returnType)) : $returnType);
}
if ($commentLines) {
$getterNodeBuilder->setDocComment($this->createDocBlock($commentLines));
}
$this->addMethod($getterNodeBuilder->getNode());
}
private function createSetterNodeBuilder(string $propertyName, $type, bool $isNullable, array $commentLines = []): Builder\Method
{
$methodName = $this->getSetterName($propertyName, $type);
$setterNodeBuilder = (new Builder\Method($methodName))->makePublic();
if ($commentLines) {
$setterNodeBuilder->setDocComment($this->createDocBlock($commentLines));
}
$paramBuilder = new Builder\Param($propertyName);
if (null !== $type) {
$paramBuilder->setType($isNullable ? new Node\NullableType(new Node\Identifier($type)) : $type);
}
$setterNodeBuilder->addParam($paramBuilder->getNode());
return $setterNodeBuilder;
}
private function getSetterName(string $propertyName, $type): string
{
return 'set'.Str::asCamelCase($propertyName);
}
private function addSingularRelation(BaseRelation $relation): void
{
$typeHint = $this->addUseStatementIfNecessary($relation->getTargetClassName());
if ($relation->getTargetClassName() === $this->getThisFullClassName()) {
$typeHint = 'self';
}
$annotationOptions = [];
if ($relation->isOwning()) {
// sometimes, we don't map the inverse relation
if ($relation->getMapInverseRelation()) {
$annotationOptions['inversedBy'] = $relation->getTargetPropertyName();
}
} else {
$annotationOptions['mappedBy'] = $relation->getTargetPropertyName();
}
if ('self' === $typeHint) {
// Doctrine does not currently resolve "self" correctly for targetEntity guessing
$annotationOptions['targetEntity'] = new ClassNameValue($typeHint, $relation->getTargetClassName());
}
if ($relation instanceof RelationOneToOne) {
$annotationOptions['cascade'] = ['persist', 'remove'];
}
$attributes = [
$this->buildAttributeNode(
$relation instanceof RelationManyToOne ? ManyToOne::class : OneToOne::class,
$annotationOptions,
'ORM'
),
];
if (!$relation->isNullable() && $relation->isOwning()) {
$attributes[] = $this->buildAttributeNode(JoinColumn::class, ['nullable' => false], 'ORM');
}
$this->addProperty(
name: $relation->getPropertyName(),
defaultValue: null,
attributes: $attributes,
propertyType: '?'.$typeHint,
);
$this->addGetter(
$relation->getPropertyName(),
$relation->getCustomReturnType() ?? $typeHint,
// getter methods always have nullable return values
// unless this has been customized explicitly
!$relation->getCustomReturnType() || $relation->isCustomReturnTypeNullable()
);
if ($relation->shouldAvoidSetter()) {
return;
}
$setterNodeBuilder = $this->createSetterNodeBuilder(
$relation->getPropertyName(),
$typeHint,
// make the type-hint nullable always for ManyToOne to allow the owning
// side to be set to null, which is needed for orphanRemoval
// (specifically: when you set the inverse side, the generated
// code will *also* set the owning side to null - so it needs to be allowed)
// e.g. $userAvatarPhoto->setUser(null);
$relation instanceof RelationOneToOne ? $relation->isNullable() : true
);
// set the *owning* side of the relation
// OneToOne is the only "singular" relation type that
// may be the inverse side
if ($relation instanceof RelationOneToOne && !$relation->isOwning()) {
$this->addNodesToSetOtherSideOfOneToOne($relation, $setterNodeBuilder);
}
$setterNodeBuilder->addStmt(
new Node\Stmt\Expression(new Node\Expr\Assign(
new Node\Expr\PropertyFetch(new Node\Expr\Variable('this'), $relation->getPropertyName()),
new Node\Expr\Variable($relation->getPropertyName())
))
);
$this->makeMethodFluent($setterNodeBuilder);
$this->addMethod($setterNodeBuilder->getNode());
}
private function addCollectionRelation(BaseCollectionRelation $relation): void
{
$typeHint = $relation->isSelfReferencing() ? 'self' : $this->addUseStatementIfNecessary($relation->getTargetClassName());
$arrayCollectionTypeHint = $this->addUseStatementIfNecessary(ArrayCollection::class);
$collectionTypeHint = $this->addUseStatementIfNecessary(Collection::class);
$annotationOptions = [
'targetEntity' => new ClassNameValue($typeHint, $relation->getTargetClassName()),
];
if ($relation->isOwning()) {
// sometimes, we don't map the inverse relation
if ($relation->getMapInverseRelation()) {
$annotationOptions['inversedBy'] = $relation->getTargetPropertyName();
}
} else {
$annotationOptions['mappedBy'] = $relation->getTargetPropertyName();
}
if ($relation->getOrphanRemoval()) {
$annotationOptions['orphanRemoval'] = true;
}
$attributes = [
$this->buildAttributeNode(
$relation instanceof RelationManyToMany ? ManyToMany::class : OneToMany::class,
$annotationOptions,
'ORM'
),
];
$this->addProperty(
name: $relation->getPropertyName(),
attributes: $attributes,
// add @var that advertises this as a collection of specific objects
comments: [\sprintf('@var %s<int, %s>', $collectionTypeHint, $typeHint)],
propertyType: $collectionTypeHint,
);
// logic to avoid re-adding the same ArrayCollection line
$addArrayCollection = true;
if ($this->getConstructorNode()) {
// We print the constructor to a string, then
// look for "$this->propertyName = "
$constructorString = $this->printer->prettyPrint([$this->getConstructorNode()]);
if (str_contains($constructorString, \sprintf('$this->%s = ', $relation->getPropertyName()))) {
$addArrayCollection = false;
}
}
if ($addArrayCollection) {
$this->addStatementToConstructor(
new Node\Stmt\Expression(new Node\Expr\Assign(
new Node\Expr\PropertyFetch(new Node\Expr\Variable('this'), $relation->getPropertyName()),
new Node\Expr\New_(new Node\Name($arrayCollectionTypeHint))
))
);
}
$this->addGetter(
$relation->getPropertyName(),
$collectionTypeHint,
false,
// add @return that advertises this as a collection of specific objects
[\sprintf('@return %s<int, %s>', $collectionTypeHint, $typeHint)]
);
$argName = Str::pluralCamelCaseToSingular($relation->getPropertyName());
// adder method
$adderNodeBuilder = (new Builder\Method($relation->getAdderMethodName()))->makePublic();
$paramBuilder = new Builder\Param($argName);
$paramBuilder->setType($typeHint);
$adderNodeBuilder->addParam($paramBuilder->getNode());
// if (!$this->avatars->contains($avatar))
$containsMethodCallNode = new Node\Expr\MethodCall(
new Node\Expr\PropertyFetch(new Node\Expr\Variable('this'), $relation->getPropertyName()),
'contains',
[new Node\Expr\Variable($argName)]
);
$ifNotContainsStmt = new Node\Stmt\If_(
new Node\Expr\BooleanNot($containsMethodCallNode)
);
$adderNodeBuilder->addStmt($ifNotContainsStmt);
// append the item
$ifNotContainsStmt->stmts[] = new Node\Stmt\Expression(
new Node\Expr\MethodCall(
new Node\Expr\PropertyFetch(new Node\Expr\Variable('this'), $relation->getPropertyName()),
'add',
[new Node\Expr\Variable($argName)]
));
// set the owning side of the relationship
if (!$relation->isOwning()) {
$ifNotContainsStmt->stmts[] = new Node\Stmt\Expression(
new Node\Expr\MethodCall(
new Node\Expr\Variable($argName),
$relation->getTargetSetterMethodName(),
[new Node\Expr\Variable('this')]
)
);
}
$this->makeMethodFluent($adderNodeBuilder);
$this->addMethod($adderNodeBuilder->getNode());
/*
* Remover
*/
$removerNodeBuilder = (new Builder\Method($relation->getRemoverMethodName()))->makePublic();
$paramBuilder = new Builder\Param($argName);
$paramBuilder->setType($typeHint);
$removerNodeBuilder->addParam($paramBuilder->getNode());
// $this->avatars->removeElement($avatar)
$removeElementCall = new Node\Expr\MethodCall(
new Node\Expr\PropertyFetch(new Node\Expr\Variable('this'), $relation->getPropertyName()),
'removeElement',
[new Node\Expr\Variable($argName)]
);
// set the owning side of the relationship
if ($relation->isOwning()) {
// $this->avatars->removeElement($avatar);
$removerNodeBuilder->addStmt(BuilderHelpers::normalizeStmt($removeElementCall));
} else {
// if ($this->avatars->removeElement($avatar))
$ifRemoveElementStmt = new Node\Stmt\If_($removeElementCall);
$removerNodeBuilder->addStmt($ifRemoveElementStmt);
if ($relation instanceof RelationOneToMany) {
// OneToMany: $student->setCourse(null);
/*
* // set the owning side to null (unless already changed)
* if ($student->getCourse() === $this) {
* $student->setCourse(null);
* }
*/
$ifRemoveElementStmt->stmts[] = $this->createSingleLineCommentNode(
'set the owning side to null (unless already changed)',
self::CONTEXT_CLASS_METHOD
);
// if ($student->getCourse() === $this) {
$ifNode = new Node\Stmt\If_(new Node\Expr\BinaryOp\Identical(
new Node\Expr\MethodCall(
new Node\Expr\Variable($argName),
$relation->getTargetGetterMethodName()
),
new Node\Expr\Variable('this')
));
// $student->setCourse(null);
$ifNode->stmts = [
new Node\Stmt\Expression(new Node\Expr\MethodCall(
new Node\Expr\Variable($argName),
$relation->getTargetSetterMethodName(),
[new Node\Arg($this->createNullConstant())]
)),
];
$ifRemoveElementStmt->stmts[] = $ifNode;
} elseif ($relation instanceof RelationManyToMany) {
// $student->removeCourse($this);
$ifRemoveElementStmt->stmts[] = new Node\Stmt\Expression(
new Node\Expr\MethodCall(
new Node\Expr\Variable($argName),
$relation->getTargetRemoverMethodName(),
[new Node\Expr\Variable('this')]
)
);
} else {
throw new \Exception('Unknown relation type');
}
}
$this->makeMethodFluent($removerNodeBuilder);
$this->addMethod($removerNodeBuilder->getNode());
}
private function addStatementToConstructor(Node\Stmt $stmt): void
{
if (!$this->getConstructorNode()) {
$constructorNode = (new Builder\Method('__construct'))->makePublic()->getNode();
// add call to parent::__construct() if there is a need to
try {
$ref = new \ReflectionClass($this->getThisFullClassName());
if ($ref->getParentClass() && $ref->getParentClass()->getConstructor()) {
$constructorNode->stmts[] = new Node\Stmt\Expression(
new Node\Expr\StaticCall(new Node\Name('parent'), new Node\Identifier('__construct'))
);
}
} catch (\ReflectionException) {
}
$this->addNodeAfterProperties($constructorNode);
}
$constructorNode = $this->getConstructorNode();
$constructorNode->stmts[] = $stmt;
$this->updateSourceCodeFromNewStmts();
}
/**
* @throws \Exception
*/
private function getConstructorNode(): ?Node\Stmt\ClassMethod
{
foreach ($this->getClassNode()->stmts as $classNode) {
if ($classNode instanceof Node\Stmt\ClassMethod && '__construct' == $classNode->name) {
return $classNode;
}
}
return null;
}
/**
* @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 (null === $targetIndex && Str::areClassesAlphabetical($class, (string) $stmt->uses[0]->name)) {
$targetIndex = $index;
}
$lastUseStmtIndex = $index;
} elseif ($stmt instanceof Node\Stmt\Class_) {
if (null !== $targetIndex) {
// 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 (null !== $lastUseStmtIndex) {
$targetIndex = $lastUseStmtIndex + 1;
} else {
$targetIndex = $index;
$addLineBreak = true;
}
break;
}
}
if (null === $targetIndex) {
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 $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()]
*/
public function buildAttributeNode(string $attributeClass, array $options, ?string $attributePrefix = null): Node\Attribute
{
$options = $this->sortOptionsByClassConstructorParameters($options, $attributeClass);
$context = $this;
$nodeArguments = array_map(static function (string $option, mixed $value) use ($context) {
if (null === $value) {
return new Node\NullableType(new Node\Identifier($option));
}
// Use the Doctrine Types constant
if ('type' === $option && str_starts_with($value, 'Types::')) {
return new Node\Arg(
new Node\Expr\ConstFetch(new Node\Name($value)),
false,
false,
[],
new Node\Identifier($option)
);
}
if ('enumType' === $option) {
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(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(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
{
$traverser = new NodeTraverser();
$visitor = new NodeVisitor\FirstFindingVisitor($filterCallback);
$traverser->addVisitor($visitor);
$traverser->traverse($this->newStmts);
return $visitor->getFoundNode();
}
private function findLastNode(callable $filterCallback, array $ast): ?Node
{
$traverser = new NodeTraverser();
$visitor = new NodeVisitor\FindingVisitor($filterCallback);
$traverser->addVisitor($visitor);
$traverser->traverse($ast);
$nodes = $visitor->getFoundNodes();
$node = end($nodes);
return false === $node ? null : $node;
}
/**
* @return Node[]
*/
private function findAllNodes(callable $filterCallback): array
{
$traverser = new NodeTraverser();
$visitor = new NodeVisitor\FindingVisitor($filterCallback);
$traverser->addVisitor($visitor);
$traverser->traverse($this->newStmts);
return $visitor->getFoundNodes();
}
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),
};
}
private function createSingleLineCommentNode(string $comment, string $context): Node\Stmt
{
$this->pendingComments[] = $comment;
switch ($context) {
case self::CONTEXT_OUTSIDE_CLASS:
// just not needed yet
throw new \Exception('not supported');
case self::CONTEXT_CLASS:
// just not needed yet
throw new \Exception('not supported');
case self::CONTEXT_CLASS_METHOD:
return BuilderHelpers::normalizeStmt(new Node\Expr\Variable(\sprintf('__COMMENT__VAR_%d', \count($this->pendingComments) - 1)));
default:
throw new \Exception('Unknown context: '.$context);
}
}
private function createDocBlock(array $commentLines): string
{
$docBlock = "/**\n";
foreach ($commentLines as $commentLine) {
if ($commentLine) {
$docBlock .= " * $commentLine\n";
} else {
// avoid the empty, extra space on blank lines
$docBlock .= " *\n";
}
}
$docBlock .= "\n */";
return $docBlock;
}
private function addMethod(Node\Stmt\ClassMethod $methodNode): void
{
$classNode = $this->getClassNode();
$methodName = $methodNode->name;
$existingIndex = null;
if ($this->methodExists($methodName)) {
if (!$this->overwrite) {
$this->writeNote(\sprintf(
'Not generating <info>%s::%s()</info>: method already exists',
Str::getShortClassName($this->getThisFullClassName()),
$methodName
));
return;
}
// record, so we can overwrite in the same place
$existingIndex = $this->getMethodIndex($methodName);
}
$newStatements = [];
// put new method always at the bottom
if (!empty($classNode->stmts)) {
$newStatements[] = $this->createBlankLineNode(self::CONTEXT_CLASS);
}
$newStatements[] = $methodNode;
if (null === $existingIndex) {
// add them to the end!
$classNode->stmts = array_merge($classNode->stmts, $newStatements);
} else {
array_splice(
$classNode->stmts,
$existingIndex,
1,
$newStatements
);
}
$this->updateSourceCodeFromNewStmts();
}
private function makeMethodFluent(Builder\Method $methodBuilder): void
{
$methodBuilder
->addStmt($this->createBlankLineNode(self::CONTEXT_CLASS_METHOD))
->addStmt(new Node\Stmt\Return_(new Node\Expr\Variable('this')));
$methodBuilder->setReturnType('static');
}
private function isInSameNamespace(string $class): bool
{
$namespace = substr($class, 0, strrpos($class, '\\'));
return $this->getNamespaceNode()->name->toCodeString() === $namespace;
}
private function getThisFullClassName(): string
{
return (string) $this->getClassNode()->namespacedName;
}
/**
* 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(fn ($node) => $node instanceof Node\Stmt\Property, [$classNode]);
// otherwise, try to add after the last constant
if (!$targetNode) {
$targetNode = $this->findLastNode(fn ($node) => $node instanceof Node\Stmt\ClassConst, [$classNode]);
}
// otherwise, try to add after the last trait
if (!$targetNode) {
$targetNode = $this->findLastNode(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 createNullConstant(): Node\Expr\ConstFetch
{
return new Node\Expr\ConstFetch(new Node\Name('null'));
}
private function addNodesToSetOtherSideOfOneToOne(RelationOneToOne $relation, Builder\Method $setterNodeBuilder): void
{
if (!$relation->isNullable()) {
$setterNodeBuilder->addStmt($this->createSingleLineCommentNode(
'set the owning side of the relation if necessary',
self::CONTEXT_CLASS_METHOD
));
// if ($user->getUserProfile() !== $this) {
$ifNode = new Node\Stmt\If_(new Node\Expr\BinaryOp\NotIdentical(
new Node\Expr\MethodCall(
new Node\Expr\Variable($relation->getPropertyName()),
$relation->getTargetGetterMethodName()
),
new Node\Expr\Variable('this')
));
// $user->setUserProfile($this);
$ifNode->stmts = [
new Node\Stmt\Expression(new Node\Expr\MethodCall(
new Node\Expr\Variable($relation->getPropertyName()),
$relation->getTargetSetterMethodName(),
[new Node\Arg(new Node\Expr\Variable('this'))]
)),
];
$setterNodeBuilder->addStmt($ifNode);
$setterNodeBuilder->addStmt($this->createBlankLineNode(self::CONTEXT_CLASS_METHOD));
return;
}
// at this point, we know the relation is nullable
$setterNodeBuilder->addStmt($this->createSingleLineCommentNode(
'unset the owning side of the relation if necessary',
self::CONTEXT_CLASS_METHOD
));
// if ($user !== null && $user->getUserProfile() !== $this)
$ifNode = new Node\Stmt\If_(new Node\Expr\BinaryOp\BooleanAnd(
new Node\Expr\BinaryOp\Identical(
new Node\Expr\Variable($relation->getPropertyName()),
$this->createNullConstant()
),
new Node\Expr\BinaryOp\NotIdentical(
new Node\Expr\PropertyFetch(
new Node\Expr\Variable('this'),
$relation->getPropertyName()
),
$this->createNullConstant()
)
));
$ifNode->stmts = [
// $this->user->setUserProfile(null)
new Node\Stmt\Expression(new Node\Expr\MethodCall(
new Node\Expr\PropertyFetch(
new Node\Expr\Variable('this'),
$relation->getPropertyName()
),
$relation->getTargetSetterMethodName(),
[new Node\Arg($this->createNullConstant())]
)),
];
$setterNodeBuilder->addStmt($ifNode);
$setterNodeBuilder->addStmt($this->createBlankLineNode(self::CONTEXT_CLASS_METHOD));
$setterNodeBuilder->addStmt($this->createSingleLineCommentNode(
'set the owning side of the relation if necessary',
self::CONTEXT_CLASS_METHOD
));
// if ($user === null && $this->user !== null)
$ifNode = new Node\Stmt\If_(new Node\Expr\BinaryOp\BooleanAnd(
new Node\Expr\BinaryOp\NotIdentical(
new Node\Expr\Variable($relation->getPropertyName()),
$this->createNullConstant()
),
new Node\Expr\BinaryOp\NotIdentical(
new Node\Expr\MethodCall(
new Node\Expr\Variable($relation->getPropertyName()),
$relation->getTargetGetterMethodName()
),
new Node\Expr\Variable('this')
)
));
$ifNode->stmts = [
new Node\Stmt\Expression(new Node\Expr\MethodCall(
new Node\Expr\Variable($relation->getPropertyName()),
$relation->getTargetSetterMethodName(),
[new Node\Arg(new Node\Expr\Variable('this'))]
)),
];
$setterNodeBuilder->addStmt($ifNode);
$setterNodeBuilder->addStmt($this->createBlankLineNode(self::CONTEXT_CLASS_METHOD));
}
private function methodExists(string $methodName): bool
{
return false !== $this->getMethodIndex($methodName);
}
private function getMethodIndex(string $methodName)
{
foreach ($this->getClassNode()->stmts as $i => $node) {
if ($node instanceof Node\Stmt\ClassMethod && strtolower($node->name->toString()) === strtolower($methodName)) {
return $i;
}
}
return false;
}
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;
}
private function writeNote(string $note): void
{
if (null !== $this->io) {
$this->io->text($note);
}
}
private function addMethodParams(Builder\Method $methodBuilder, array $params): void
{
foreach ($params as $param) {
$methodBuilder->addParam($param);
}
}
/**
* 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
{
switch (\gettype($value)) {
case 'string':
$nodeValue = new Node\Scalar\String_($value);
break;
case 'integer':
$nodeValue = new Node\Scalar\LNumber($value);
break;
case 'double':
$nodeValue = new Node\Scalar\DNumber($value);
break;
case 'boolean':
$nodeValue = new Node\Expr\ConstFetch(new Node\Name($value ? 'true' : 'false'));
break;
case '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));
$nodeValue = new Node\Expr\Array_($arrayItems, ['kind' => Node\Expr\Array_::KIND_SHORT]);
break;
default:
$nodeValue = null;
}
if (null === $nodeValue) {
if ($value instanceof ClassNameValue) {
$nodeValue = new Node\Expr\ConstFetch(
new Node\Name(
\sprintf('%s::class', $value->isSelf() ? 'self' : $value->getShortName())
)
);
} else {
throw new \Exception(\sprintf('Cannot build a node expr for value of type "%s"', \gettype($value)));
}
}
return $nodeValue;
}
/**
* 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
*/
private function sortOptionsByClassConstructorParameters(array $options, string $classString): array
{
if (str_starts_with($classString, 'ORM\\')) {
$classString = \sprintf('Doctrine\\ORM\\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)) {
$sorted[$name] = $options[$name];
unset($options[$name]);
}
}
return array_merge($sorted, $options);
}
}