[DoctrineCodeQuality] Initialize collectoins in constructor

This commit is contained in:
Tomas Votruba 2019-09-28 15:23:08 +02:00
parent 8e442f9fb6
commit 6fbdf5461a
9 changed files with 337 additions and 2 deletions

View File

@ -55,6 +55,7 @@
"Rector\\ConsoleDiffer\\": "packages/ConsoleDiffer/src",
"Rector\\DeadCode\\": "packages/DeadCode/src",
"Rector\\Doctrine\\": "packages/Doctrine/src",
"Rector\\DoctrineCodeQuality\\": "packages/DoctrineCodeQuality/src",
"Rector\\ElasticSearchDSL\\": "packages/ElasticSearchDSL/src",
"Rector\\FileSystemRector\\": "packages/FileSystemRector/src",
"Rector\\Guzzle\\": "packages/Guzzle/src",
@ -111,6 +112,7 @@
"Rector\\CodingStyle\\Tests\\": "packages/CodingStyle/tests",
"Rector\\DeadCode\\Tests\\": "packages/DeadCode/tests",
"Rector\\Doctrine\\Tests\\": "packages/Doctrine/tests",
"Rector\\DoctrineCodeQuality\\Tests\\": "packages/DoctrineCodeQuality/tests",
"Rector\\ElasticSearchDSL\\Tests\\": "packages/ElasticSearchDSL/tests",
"Rector\\Guzzle\\Tests\\": "packages/Guzzle/tests",
"Rector\\Laravel\\Tests\\": "packages/Laravel/tests",

View File

@ -1,2 +1,3 @@
services:
Rector\Doctrine\Rector\Class_\ManagerRegistryGetManagerToEntityManagerRector: ~
Rector\DoctrineCodeQuality\Rector\Class_\InitializeDefaultEntityCollectionRector: ~

View File

@ -3808,7 +3808,7 @@ Remove 0 from break and continue
- class: `Rector\Php55\Rector\FuncCall\PregReplaceEModifierRector`
The /e modifier is no longer supported, use preg_replace_callback instead
The /e modifier is no longer supported, use preg_replace_callback instead
```diff
class SomeClass

View File

@ -917,7 +917,7 @@ if (true) {
```php
?>
<strong>feel</strong><?php
<strong>feel</strong><?php
```
<br>

View File

@ -0,0 +1,190 @@
<?php declare(strict_types=1);
namespace Rector\DoctrineCodeQuality\Rector\Class_;
use PhpParser\Node;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\New_;
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\Expression;
use Rector\BetterPhpDocParser\Contract\Doctrine\ToManyTagNodeInterface;
use Rector\BetterPhpDocParser\PhpDocNode\Doctrine\Class_\EntityTagValueNode;
use Rector\Doctrine\ValueObject\DoctrineClass;
use Rector\Rector\AbstractRector;
use Rector\RectorDefinition\CodeSample;
use Rector\RectorDefinition\RectorDefinition;
/**
* @see https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/reference/best-practices.html#initialize-collections-in-the-constructor
*
* @see \Rector\DoctrineCodeQuality\Tests\Rector\Class_\InitializeDefaultEntityCollectionRector\InitializeDefaultEntityCollectionRectorTest
*/
final class InitializeDefaultEntityCollectionRector extends AbstractRector
{
public function getDefinition(): RectorDefinition
{
return new RectorDefinition('Initialize collection property in Entity constructor', [
new CodeSample(
<<<'PHP'
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class SomeClass
{
/**
* @ORM\OneToMany(targetEntity="MarketingEvent")
*/
private $marketingEvents = [];
}
PHP
,
<<<'PHP'
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class SomeClass
{
/**
* @ORM\OneToMany(targetEntity="MarketingEvent")
*/
private $marketingEvents = [];
public function __construct()
{
$this->marketingEvents = new ArrayCollection();
}
}
PHP
),
]);
}
/**
* @return string[]
*/
public function getNodeTypes(): array
{
return [Class_::class];
}
/**
* @param Class_ $node
*/
public function refactor(Node $node): ?Node
{
$classPhpDocInfo = $this->getPhpDocInfo($node);
if ($classPhpDocInfo === null) {
return null;
}
if (! $classPhpDocInfo->getByType(EntityTagValueNode::class)) {
return null;
}
$toManyPropertyNames = $this->resolveToManyPropertyNames($node);
if ($toManyPropertyNames === []) {
return null;
}
$constructClassMethod = $node->getMethod('__construct');
$assigns = $this->createAssignsOfArrayCollectionsForPropertyNames($toManyPropertyNames);
if ($constructClassMethod === null) {
$constructClassMethod = $this->nodeFactory->createPublicMethod('__construct');
$constructClassMethod->stmts = $assigns;
$node->stmts = array_merge((array) $node->stmts, [$constructClassMethod]);
} else {
$assigns = $this->filterOutExistingAssigns($constructClassMethod, $assigns);
// all properties are initialized → skip
if ($assigns === []) {
return null;
}
$constructClassMethod->stmts = array_merge($assigns, (array) $constructClassMethod->stmts);
}
return $node;
}
/**
* @return string[]
*/
private function resolveToManyPropertyNames(Class_ $class): array
{
$collectionPropertyNames = [];
foreach ($class->getProperties() as $property) {
if (count($property->props) !== 1) {
continue;
}
$propertyPhpDocInfo = $this->getPhpDocInfo($property);
if ($propertyPhpDocInfo === null) {
continue;
}
if (! $propertyPhpDocInfo->getByType(ToManyTagNodeInterface::class)) {
continue;
}
/** @var string $propertyName */
$propertyName = $this->getName($property);
$collectionPropertyNames[] = $propertyName;
}
return $collectionPropertyNames;
}
/**
* @param string[] $propertyNames
* @return Expression[]
*/
private function createAssignsOfArrayCollectionsForPropertyNames(array $propertyNames): array
{
$assigns = [];
foreach ($propertyNames as $propertyName) {
$assigns[] = $this->createPropertyArrayCollectionAssign($propertyName);
}
return $assigns;
}
private function createPropertyArrayCollectionAssign(string $toManyPropertyName): Expression
{
$propertyFetch = $this->createPropertyFetch('this', $toManyPropertyName);
$newCollection = new New_(new FullyQualified(DoctrineClass::ARRAY_COLLECTION));
$assign = new Assign($propertyFetch, $newCollection);
return new Expression($assign);
}
/**
* @param Expression[] $assigns
* @return Expression[]
*/
private function filterOutExistingAssigns(Node\Stmt\ClassMethod $constructClassMethod, array $assigns): array
{
$this->traverseNodesWithCallable((array) $constructClassMethod->stmts, function (Node $node) use (&$assigns) {
foreach ($assigns as $key => $assign) {
if (! $this->areNodesEqual($node, $assign)) {
continue;
}
unset($assigns[$key]);
}
return null;
});
return $assigns;
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace Rector\DoctrineCodeQuality\Tests\Rector\Class_\InitializeDefaultEntityCollectionRector\Fixture;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class ExistingConstructor
{
/**
* @ORM\OneToMany(targetEntity="MarketingEvent")
*/
private $marketingEvents = [];
public function __construct()
{
$value = 5;
}
}
?>
-----
<?php
namespace Rector\DoctrineCodeQuality\Tests\Rector\Class_\InitializeDefaultEntityCollectionRector\Fixture;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class ExistingConstructor
{
/**
* @ORM\OneToMany(targetEntity="MarketingEvent")
*/
private $marketingEvents = [];
public function __construct()
{
$this->marketingEvents = new \Doctrine\Common\Collections\ArrayCollection();
$value = 5;
}
}
?>

View File

@ -0,0 +1,41 @@
<?php
namespace Rector\DoctrineCodeQuality\Tests\Rector\Class_\InitializeDefaultEntityCollectionRector\Fixture;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class SomeClass
{
/**
* @ORM\OneToMany(targetEntity="MarketingEvent")
*/
private $marketingEvents = [];
}
?>
-----
<?php
namespace Rector\DoctrineCodeQuality\Tests\Rector\Class_\InitializeDefaultEntityCollectionRector\Fixture;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class SomeClass
{
/**
* @ORM\OneToMany(targetEntity="MarketingEvent")
*/
private $marketingEvents = [];
public function __construct()
{
$this->marketingEvents = new \Doctrine\Common\Collections\ArrayCollection();
}
}
?>

View File

@ -0,0 +1,23 @@
<?php
namespace Rector\DoctrineCodeQuality\Tests\Rector\Class_\InitializeDefaultEntityCollectionRector\Fixture;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* @ORM\Entity
*/
class SkipAdded
{
/**
* @ORM\OneToMany(targetEntity="MarketingEvent")
*/
private $marketingEvents = [];
public function __construct()
{
$this->marketingEvents = new ArrayCollection();
$value = 5;
}
}

View File

@ -0,0 +1,30 @@
<?php declare(strict_types=1);
namespace Rector\DoctrineCodeQuality\Tests\Rector\Class_\InitializeDefaultEntityCollectionRector;
use Iterator;
use Rector\DoctrineCodeQuality\Rector\Class_\InitializeDefaultEntityCollectionRector;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
final class InitializeDefaultEntityCollectionRectorTest extends AbstractRectorTestCase
{
/**
* @dataProvider provideDataForTest()
*/
public function test(string $file): void
{
$this->doTestFile($file);
}
public function provideDataForTest(): Iterator
{
yield [__DIR__ . '/Fixture/fixture.php.inc'];
yield [__DIR__ . '/Fixture/existing_constructor.php.inc'];
yield [__DIR__ . '/Fixture/skip_added.php.inc'];
}
protected function getRectorClass(): string
{
return InitializeDefaultEntityCollectionRector::class;
}
}