[PHP 7.4] Add null on conditional type of property type (#4394)

Co-authored-by: rector-bot <tomas@getrector.org>
This commit is contained in:
Tomas Votruba 2020-10-12 16:34:28 +02:00 committed by GitHub
parent 50f75c9325
commit a31837679d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 960 additions and 376 deletions

View File

@ -48,6 +48,7 @@ expectedArguments(
\Rector\NodeTypeResolver\Node\AttributeKey::CLOSURE_NODE,
\Rector\NodeTypeResolver\Node\AttributeKey::PARAMETER_POSITION,
\Rector\NodeTypeResolver\Node\AttributeKey::ARGUMENT_POSITION,
\Rector\NodeTypeResolver\Node\AttributeKey::IS_FIRST_LEVEL_STATEMENT,
);
expectedArguments(
@ -81,6 +82,7 @@ expectedArguments(
\Rector\NodeTypeResolver\Node\AttributeKey::CLOSURE_NODE,
\Rector\NodeTypeResolver\Node\AttributeKey::PARAMETER_POSITION,
\Rector\NodeTypeResolver\Node\AttributeKey::ARGUMENT_POSITION,
\Rector\NodeTypeResolver\Node\AttributeKey::IS_FIRST_LEVEL_STATEMENT,
);
expectedArguments(

View File

@ -8,7 +8,8 @@ use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Expr\StaticCall;
use PHPStan\Type\ObjectType;
use PhpParser\Node\Expr\StaticPropertyFetch;
use PHPStan\Type\Type;
use PHPStan\Type\TypeWithClassName;
use PHPStan\Type\UnionType;
use Rector\NodeNameResolver\NodeNameResolver;
@ -50,11 +51,11 @@ final class ParsedPropertyFetchNodeCollector
public function collect(Node $node): void
{
if (! $node instanceof PropertyFetch) {
if (! $node instanceof PropertyFetch && ! $node instanceof StaticPropertyFetch) {
return;
}
$propertyType = $this->nodeTypeResolver->getStaticType($node->var);
$propertyType = $this->resolvePropertyCallerType($node);
// make sure name is valid
if ($node->name instanceof StaticCall || $node->name instanceof MethodCall) {
@ -62,20 +63,11 @@ final class ParsedPropertyFetchNodeCollector
}
$propertyName = $this->nodeNameResolver->getName($node->name);
if ($propertyType instanceof TypeWithClassName) {
$this->propertyFetchesByTypeAndName[$propertyType->getClassName()][$propertyName][] = $node;
if ($propertyName === null) {
return;
}
if ($propertyType instanceof UnionType) {
foreach ($propertyType->getTypes() as $unionedType) {
if (! $unionedType instanceof ObjectType) {
continue;
}
$this->propertyFetchesByTypeAndName[$unionedType->getClassName()][$propertyName][] = $node;
}
}
$this->addPropertyFetchWithTypeAndName($propertyType, $node, $propertyName);
}
/**
@ -85,4 +77,35 @@ final class ParsedPropertyFetchNodeCollector
{
return $this->propertyFetchesByTypeAndName[$className][$propertyName] ?? [];
}
/**
* @param PropertyFetch|StaticPropertyFetch $node
*/
private function resolvePropertyCallerType(Node $node): Type
{
if ($node instanceof PropertyFetch) {
return $this->nodeTypeResolver->getStaticType($node->var);
}
return $this->nodeTypeResolver->getStaticType($node->class);
}
/**
* @param PropertyFetch|StaticPropertyFetch $propertyFetchNode
*/
private function addPropertyFetchWithTypeAndName(
Type $propertyType,
Node $propertyFetchNode,
string $propertyName
): void {
if ($propertyType instanceof TypeWithClassName) {
$this->propertyFetchesByTypeAndName[$propertyType->getClassName()][$propertyName][] = $propertyFetchNode;
}
if ($propertyType instanceof UnionType) {
foreach ($propertyType->getTypes() as $unionedType) {
$this->addPropertyFetchWithTypeAndName($unionedType, $propertyFetchNode, $propertyName);
}
}
}
}

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Rector\NodeNestingScope;
use PhpParser\Node;
use PhpParser\Node\FunctionLike;
use Rector\Core\PhpParser\Node\BetterNodeFinder;
use Rector\NodeNestingScope\ValueObject\ControlStructure;
@ -28,6 +29,20 @@ final class ScopeNestingComparator
return $firstNodeScopeNode === $secondNodeScopeNode;
}
public function isNodeConditionallyScoped(Node $node): bool
{
$foundParentType = $this->betterNodeFinder->findFirstParentInstanceOf(
$node,
ControlStructure::CONDITIONAL_NODE_SCOPE_TYPES + [FunctionLike::class]
);
if ($foundParentType === null) {
return false;
}
return ! $foundParentType instanceof FunctionLike;
}
private function findParentControlStructure(Node $node): ?Node
{
return $this->betterNodeFinder->findFirstParentInstanceOf($node, ControlStructure::BREAKING_SCOPE_NODE_TYPES);

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Rector\NodeNestingScope\ValueObject;
use PhpParser\Node\Expr\Match_;
use PhpParser\Node\FunctionLike;
use PhpParser\Node\Stmt\Case_;
use PhpParser\Node\Stmt\Catch_;
@ -13,6 +14,7 @@ use PhpParser\Node\Stmt\ElseIf_;
use PhpParser\Node\Stmt\For_;
use PhpParser\Node\Stmt\Foreach_;
use PhpParser\Node\Stmt\If_;
use PhpParser\Node\Stmt\Switch_;
use PhpParser\Node\Stmt\While_;
final class ControlStructure
@ -32,4 +34,20 @@ final class ControlStructure
Case_::class,
FunctionLike::class,
];
/**
* These situations happens only if condition is met
* @var class-string[]
*/
public const CONDITIONAL_NODE_SCOPE_TYPES = [
If_::class,
While_::class,
Do_::class,
Else_::class,
ElseIf_::class,
Catch_::class,
Case_::class,
Match_::class,
Switch_::class,
];
}

View File

@ -204,4 +204,9 @@ final class AttributeKey
* @var string
*/
public const ARGUMENT_POSITION = 'argument_position';
/**
* @var string
*/
public const IS_FIRST_LEVEL_STATEMENT = 'is_first_level_statement';
}

View File

@ -13,6 +13,7 @@ use PhpParser\NodeVisitor\NodeConnectingVisitor;
use Rector\Core\Configuration\Configuration;
use Rector\NodeCollector\NodeVisitor\NodeCollectorNodeVisitor;
use Rector\NodeTypeResolver\NodeVisitor\FileInfoNodeVisitor;
use Rector\NodeTypeResolver\NodeVisitor\FirstLevelNodeVisitor;
use Rector\NodeTypeResolver\NodeVisitor\FunctionLikeParamArgPositionNodeVisitor;
use Rector\NodeTypeResolver\NodeVisitor\FunctionMethodAndClassNodeVisitor;
use Rector\NodeTypeResolver\NodeVisitor\NamespaceNodeVisitor;
@ -78,6 +79,11 @@ final class NodeScopeAndMetadataDecorator
*/
private $functionLikeParamArgPositionNodeVisitor;
/**
* @var FirstLevelNodeVisitor
*/
private $firstLevelNodeVisitor;
public function __construct(
CloningVisitor $cloningVisitor,
Configuration $configuration,
@ -89,7 +95,8 @@ final class NodeScopeAndMetadataDecorator
PhpDocInfoNodeVisitor $phpDocInfoNodeVisitor,
StatementNodeVisitor $statementNodeVisitor,
NodeConnectingVisitor $nodeConnectingVisitor,
FunctionLikeParamArgPositionNodeVisitor $functionLikeParamArgPositionNodeVisitor
FunctionLikeParamArgPositionNodeVisitor $functionLikeParamArgPositionNodeVisitor,
FirstLevelNodeVisitor $firstLevelNodeVisitor
) {
$this->phpStanNodeScopeResolver = $phpStanNodeScopeResolver;
$this->cloningVisitor = $cloningVisitor;
@ -102,6 +109,7 @@ final class NodeScopeAndMetadataDecorator
$this->phpDocInfoNodeVisitor = $phpDocInfoNodeVisitor;
$this->nodeConnectingVisitor = $nodeConnectingVisitor;
$this->functionLikeParamArgPositionNodeVisitor = $functionLikeParamArgPositionNodeVisitor;
$this->firstLevelNodeVisitor = $firstLevelNodeVisitor;
}
/**
@ -141,6 +149,7 @@ final class NodeScopeAndMetadataDecorator
$nodeTraverser->addVisitor($this->functionMethodAndClassNodeVisitor);
$nodeTraverser->addVisitor($this->namespaceNodeVisitor);
$nodeTraverser->addVisitor($this->phpDocInfoNodeVisitor);
$nodeTraverser->addVisitor($this->firstLevelNodeVisitor);
$nodeTraverser->addVisitor($this->functionLikeParamArgPositionNodeVisitor);
$nodes = $nodeTraverser->traverse($nodes);

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Rector\NodeTypeResolver\NodeVisitor;
use PhpParser\Node;
use PhpParser\Node\FunctionLike;
use PhpParser\Node\Stmt\Expression;
use PhpParser\NodeVisitorAbstract;
use Rector\NodeTypeResolver\Node\AttributeKey;
final class FirstLevelNodeVisitor extends NodeVisitorAbstract
{
/**
* @return Node
*/
public function enterNode(Node $node): ?Node
{
if ($node instanceof FunctionLike) {
foreach ((array) $node->getStmts() as $stmt) {
$stmt->setAttribute(AttributeKey::IS_FIRST_LEVEL_STATEMENT, true);
if ($stmt instanceof Expression) {
$stmt->expr->setAttribute(AttributeKey::IS_FIRST_LEVEL_STATEMENT, true);
}
}
}
return null;
}
}

View File

@ -18,6 +18,8 @@ use PHPStan\Type\Type;
use PHPStan\Type\UnionType;
use PHPStan\Type\VerbosityLevel;
use Rector\Core\Exception\ShouldNotHappenException;
use Rector\PHPStan\Type\FullyQualifiedObjectType;
use Rector\PHPStan\Type\ShortenedObjectType;
use Rector\PHPStan\TypeFactoryStaticHelper;
final class TypeFactory
@ -74,6 +76,10 @@ final class TypeFactory
$type = $this->removeValueFromConstantType($type);
}
if ($type instanceof ShortenedObjectType) {
$type = new FullyQualifiedObjectType($type->getFullyQualifiedName());
}
$typeHash = md5($type->describe(VerbosityLevel::cache()));
$uniqueTypes[$typeHash] = $type;
}

View File

@ -26,6 +26,11 @@ final class PHPStanStaticTypeMapper
*/
public const KIND_PROPERTY = 'property';
/**
* @var string
*/
public const KIND_RETURN = 'return';
/**
* @var TypeMapperInterface[]
*/

View File

@ -5,12 +5,14 @@ declare(strict_types=1);
namespace Rector\PHPStanStaticTypeMapper\TypeMapper;
use PhpParser\Node;
use PhpParser\Node\Name;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\Type\NullType;
use PHPStan\Type\Type;
use PHPStan\Type\VerbosityLevel;
use Rector\AttributeAwarePhpDoc\Ast\Type\AttributeAwareIdentifierTypeNode;
use Rector\PHPStanStaticTypeMapper\Contract\TypeMapperInterface;
use Rector\PHPStanStaticTypeMapper\PHPStanStaticTypeMapper;
final class NullTypeMapper implements TypeMapperInterface
{
@ -32,7 +34,11 @@ final class NullTypeMapper implements TypeMapperInterface
*/
public function mapToPhpParserNode(Type $type, ?string $kind = null): ?Node
{
return null;
if ($kind !== PHPStanStaticTypeMapper::KIND_PROPERTY) {
return null;
}
return new Name('null');
}
public function mapToDocString(Type $type, ?Type $parentType = null): string

View File

@ -204,11 +204,16 @@ final class UnionTypeMapper implements TypeMapperInterface
{
$phpParserUnionType = $this->matchPhpParserUnionType($unionType);
if ($phpParserUnionType !== null) {
if (! $this->phpVersionProvider->isAtLeast(PhpVersionFeature::UNION_TYPES)) {
return null;
}
return $phpParserUnionType;
}
// the type should be compatible with all other types, e.g. A extends B, B
$compatibleObjectCandidate = $this->resolveCompatibleObjectCandidate($unionType);
if ($compatibleObjectCandidate === null) {
return null;
}

View File

@ -3,7 +3,6 @@
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="tests/bootstrap.php"
colors="true"
verbose="true"
>
<php>
<const name="RECTOR_REPOSITORY" value="true"/>

View File

@ -254,6 +254,12 @@ CODE_SAMPLE
}
$onlyProperty = $property->props[0];
// skip is already has value
if ($onlyProperty->default !== null) {
return;
}
$onlyProperty->default = $this->createNull();
}

View File

@ -4,49 +4,17 @@ namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Fixture;
final class DefaultValues
{
/**
* @var bool
*/
private $name = 'not_a_bool';
/**
* @var bool
*/
private $isItRealName = false;
/**
* @var bool
*/
private $isItRealNameNull = null;
/**
* @var string
*/
private $size = false;
/**
* @var float
*/
private $a = 42.42;
/**
* @var float
*/
private $b = 42;
/**
* @var float
*/
private $c = 'hey';
/**
* @var int
*/
private $e = 42.42;
/**
* @var int
*/
private $f = 42;
/**
@ -68,14 +36,6 @@ namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Fixture;
final class DefaultValues
{
private string $name = 'not_a_bool';
private bool $isItRealName = false;
private ?bool $isItRealNameNull = null;
private bool $size = false;
private float $a = 42.42;
private int $b = 42;
@ -88,7 +48,7 @@ final class DefaultValues
private array $g = [1, 2, 3];
private array $h = [1, 2, 3];
private iterable $h = [1, 2, 3];
}
?>

View File

@ -0,0 +1,21 @@
<?php
namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Fixture;
final class DefaultValuesBool
{
private $size = false;
}
?>
-----
<?php
namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Fixture;
final class DefaultValuesBool
{
private bool $size = false;
}
?>

View File

@ -0,0 +1,34 @@
<?php
namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Fixture;
final class DefaultValuesString
{
/**
* @var bool
*/
private $name = 'not_a_bool';
public function getName(): string
{
return $this->name;
}
}
?>
-----
<?php
namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Fixture;
final class DefaultValuesString
{
private string $name = 'not_a_bool';
public function getName(): string
{
return $this->name;
}
}
?>

View File

@ -0,0 +1,24 @@
<?php
namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Fixture;
final class DefaultValuesWithNullable
{
/**
* @var bool
*/
private $isItRealNameNull = null;
}
?>
-----
<?php
namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Fixture;
final class DefaultValuesWithNullable
{
private ?bool $isItRealNameNull = null;
}
?>

View File

@ -0,0 +1,40 @@
<?php
namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Fixture;
use Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Source\ReturnString;
class IfConditionalUnionNullable
{
/** @var string|null */
private $nullOrString;
public function __construct(?ReturnString $returnString = null)
{
if ($returnString !== null) {
$this->nullOrString = $returnString->getName();
}
}
}
?>
-----
<?php
namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Fixture;
use Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Source\ReturnString;
class IfConditionalUnionNullable
{
private ?string $nullOrString = null;
public function __construct(?ReturnString $returnString = null)
{
if ($returnString !== null) {
$this->nullOrString = $returnString->getName();
}
}
}
?>

View File

@ -0,0 +1,36 @@
<?php
namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Fixture;
use Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Source\ReturnString;
class NoConditionUnionNullable
{
/** @var string|null */
private $nullOrString;
public function __construct(?ReturnString $returnString = null)
{
$this->nullOrString = $returnString->getNameOrNull();
}
}
?>
-----
<?php
namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Fixture;
use Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Source\ReturnString;
class NoConditionUnionNullable
{
private ?string $nullOrString = null;
public function __construct(?ReturnString $returnString = null)
{
$this->nullOrString = $returnString->getNameOrNull();
}
}
?>

View File

@ -0,0 +1,24 @@
<?php
namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Fixture;
class SkipUnionType
{
/**
* @var bool|int
*/
public $cantTouchThis = true;
public function setNumber()
{
$this->cantTouchThis = 100;
}
/**
* @return bool|int
*/
public function getCantTouchThis()
{
return $this->cantTouchThis;
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector\FixtureUnionTypes;
class IncludeUnionedType
{
/**
* @var bool|int
*/
public $cantTouchThis = true;
public function setNumber()
{
$this->cantTouchThis = 100;
}
/**
* @return bool|int
*/
public function getCantTouchThis()
{
return $this->cantTouchThis;
}
}
?>
-----
<?php
namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector\FixtureUnionTypes;
class IncludeUnionedType
{
public bool|int $cantTouchThis = true;
public function setNumber()
{
$this->cantTouchThis = 100;
}
/**
* @return bool|int
*/
public function getCantTouchThis()
{
return $this->cantTouchThis;
}
}
?>

View File

@ -0,0 +1,39 @@
<?php
namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector\FixtureUnionTypes;
class SetIfElse
{
public $stringOrInteger = 'hi';
public function setNumber()
{
if (mt_rand(0, 100)) {
$this->stringOrInteger = 'hey';
} else {
$this->stringOrInteger = 1000;
}
}
}
?>
-----
<?php
namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector\FixtureUnionTypes;
class SetIfElse
{
public string|int $stringOrInteger = 'hi';
public function setNumber()
{
if (mt_rand(0, 100)) {
$this->stringOrInteger = 'hey';
} else {
$this->stringOrInteger = 1000;
}
}
}
?>

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Source;
final class ReturnString
{
public function getName(): string
{
return 'name';
}
public function getNameOrNull(): ?string
{
if (mt_rand(0, 100)) {
return null;
}
return 'name';
}
}

View File

@ -6,6 +6,7 @@ namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector;
use Iterator;
use Rector\Core\Testing\PHPUnit\AbstractRectorTestCase;
use Rector\Core\ValueObject\PhpVersionFeature;
use Rector\Php74\Rector\Property\TypedPropertyRector;
use Symplify\SmartFileSystem\SmartFileInfo;
@ -24,6 +25,11 @@ final class TypedPropertyRectorTest extends AbstractRectorTestCase
return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture');
}
protected function getPhpVersion(): string
{
return PhpVersionFeature::BEFORE_UNION_TYPES;
}
/**
* @return mixed[]
*/

View File

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Rector\TypeDeclaration\AlreadyAssignDetector;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Assign;
use Rector\Core\PhpParser\NodeTraverser\CallableNodeTraverser;
use Rector\TypeDeclaration\Matcher\PropertyAssignMatcher;
abstract class AbstractAssignDetector
{
/**
* @var CallableNodeTraverser
*/
protected $callableNodeTraverser;
/**
* @var PropertyAssignMatcher
*/
private $propertyAssignMatcher;
/**
* @required
*/
public function autowireAbstractAssignDetector(
PropertyAssignMatcher $propertyAssignMatcher,
CallableNodeTraverser $callableNodeTraverser
): void {
$this->propertyAssignMatcher = $propertyAssignMatcher;
$this->callableNodeTraverser = $callableNodeTraverser;
}
protected function matchAssignExprToPropertyName(Node $node, string $propertyName): ?Expr
{
if (! $node instanceof Assign) {
return null;
}
return $this->propertyAssignMatcher->matchPropertyAssignExpr($node, $propertyName);
}
}

View File

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Rector\TypeDeclaration\AlreadyAssignDetector;
use PhpParser\Node;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Stmt\ClassLike;
use PhpParser\NodeTraverser;
use Rector\Core\ValueObject\MethodName;
use Rector\NodeTypeResolver\Node\AttributeKey;
final class ConstructorAssignDetector extends AbstractAssignDetector
{
public function detect(ClassLike $classLike, string $propertyName): bool
{
$isAssignedInConstructor = false;
$this->callableNodeTraverser->traverseNodesWithCallable($classLike->stmts, function (Node $node) use (
$propertyName, &$isAssignedInConstructor
): ?int {
$expr = $this->matchAssignExprToPropertyName($node, $propertyName);
if ($expr === null) {
return null;
}
// is in constructor?
$methodName = $node->getAttribute(AttributeKey::METHOD_NAME);
if ($methodName !== MethodName::CONSTRUCT) {
return null;
}
/** @var Assign $assign */
$assign = $node;
// cannot be nested
if ($assign->getAttribute(AttributeKey::IS_FIRST_LEVEL_STATEMENT) !== true) {
return null;
}
$isAssignedInConstructor = true;
return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN;
});
return $isAssignedInConstructor;
}
}

View File

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Rector\TypeDeclaration\AlreadyAssignDetector;
use PhpParser\Node;
use PhpParser\Node\Stmt\ClassLike;
use PhpParser\NodeTraverser;
use Rector\NodeNestingScope\ScopeNestingComparator;
use Rector\NodeTypeResolver\NodeTypeResolver;
use Rector\PHPStanStaticTypeMapper\DoctrineTypeAnalyzer;
/**
* Should add extra null type
*/
final class NullTypeAssignDetector extends AbstractAssignDetector
{
/**
* @var ScopeNestingComparator
*/
private $scopeNestingComparator;
/**
* @var DoctrineTypeAnalyzer
*/
private $doctrineTypeAnalyzer;
/**
* @var NodeTypeResolver
*/
private $nodeTypeResolver;
public function __construct(
ScopeNestingComparator $scopeNestingComparator,
DoctrineTypeAnalyzer $doctrineTypeAnalyzer,
NodeTypeResolver $nodeTypeResolver
) {
$this->scopeNestingComparator = $scopeNestingComparator;
$this->doctrineTypeAnalyzer = $doctrineTypeAnalyzer;
$this->nodeTypeResolver = $nodeTypeResolver;
}
public function detect(ClassLike $classLike, string $propertyName): ?bool
{
$needsNullType = null;
$this->callableNodeTraverser->traverseNodesWithCallable($classLike->stmts, function (Node $node) use (
$propertyName, &$needsNullType
): ?int {
$expr = $this->matchAssignExprToPropertyName($node, $propertyName);
if ($expr === null) {
return null;
}
if ($this->scopeNestingComparator->isNodeConditionallyScoped($expr)) {
$needsNullType = true;
return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN;
}
// not in doctrine property
$staticType = $this->nodeTypeResolver->getStaticType($expr);
if ($this->doctrineTypeAnalyzer->isDoctrineCollectionWithIterableUnionType($staticType)) {
$needsNullType = false;
return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN;
}
return null;
});
return $needsNullType;
}
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Rector\TypeDeclaration\AlreadyAssignDetector;
use PhpParser\Node\Stmt\ClassLike;
final class PropertyDefaultAssignDetector
{
public function detect(ClassLike $classLike, string $propertyName): bool
{
$property = $classLike->getProperty($propertyName);
if ($property === null) {
return false;
}
return $property->props[0]->default !== null;
}
}

View File

@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Rector\TypeDeclaration\Matcher;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\ArrayDimFetch;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Expr\StaticPropertyFetch;
use Rector\NodeNameResolver\NodeNameResolver;
final class PropertyAssignMatcher
{
/**
* @var NodeNameResolver
*/
private $nodeNameResolver;
public function __construct(NodeNameResolver $nodeNameResolver)
{
$this->nodeNameResolver = $nodeNameResolver;
}
/**
* Covers:
* - $this->propertyName = $expr;
* - $this->propertyName[] = $expr;
*/
public function matchPropertyAssignExpr(Assign $assign, string $propertyName): ?Expr
{
if ($this->isPropertyFetch($assign->var)) {
if (! $this->nodeNameResolver->isName($assign->var, $propertyName)) {
return null;
}
return $assign->expr;
}
if ($assign->var instanceof ArrayDimFetch && $this->isPropertyFetch($assign->var->var)) {
if (! $this->nodeNameResolver->isName($assign->var->var, $propertyName)) {
return null;
}
return $assign->expr;
}
return null;
}
private function isPropertyFetch(Node $node): bool
{
if ($node instanceof PropertyFetch) {
return true;
}
return $node instanceof StaticPropertyFetch;
}
}

View File

@ -10,8 +10,8 @@ use PhpParser\Node\Param;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Function_;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeWithClassName;
use Rector\Core\RectorDefinition\CodeSample;
use Rector\Core\RectorDefinition\RectorDefinition;
use Rector\Core\ValueObject\PhpVersionFeature;
@ -146,14 +146,8 @@ CODE_SAMPLE
return;
}
if ($inferedType instanceof ObjectType) {
$fqcn = $inferedType instanceof ShortenedObjectType
? $inferedType->getFullyQualifiedName()
: $inferedType->getClassName();
$reflectionClass = new ReflectionClass($fqcn);
if ($reflectionClass->isTrait()) {
return;
}
if ($this->isTraitType($inferedType)) {
return;
}
$paramTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode(
@ -187,4 +181,25 @@ CODE_SAMPLE
// already set → skip
return ! $param->type->getAttribute(NewType::HAS_NEW_INHERITED_TYPE, false);
}
private function isTraitType(Type $type): bool
{
if (! $type instanceof TypeWithClassName) {
return false;
}
$fullyQualifiedName = $this->getFullyQualifiedName($type);
$reflectionClass = new ReflectionClass($fullyQualifiedName);
return $reflectionClass->isTrait();
}
private function getFullyQualifiedName(TypeWithClassName $typeWithClassName): string
{
if ($typeWithClassName instanceof ShortenedObjectType) {
return $typeWithClassName->getFullyQualifiedName();
}
return $typeWithClassName->getClassName();
}
}

View File

@ -20,6 +20,7 @@ use Rector\Core\RectorDefinition\RectorDefinition;
use Rector\Core\ValueObject\MethodName;
use Rector\Core\ValueObject\PhpVersionFeature;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\PHPStanStaticTypeMapper\PHPStanStaticTypeMapper;
use Rector\TypeDeclaration\ChildPopulator\ChildReturnPopulator;
use Rector\TypeDeclaration\OverrideGuard\ClassMethodReturnTypeOverrideGuard;
use Rector\TypeDeclaration\PhpDocParser\NonInformativeReturnTagRemover;
@ -142,7 +143,11 @@ CODE_SAMPLE
return null;
}
$inferredReturnNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($inferedType);
$inferredReturnNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode(
$inferedType,
PHPStanStaticTypeMapper::KIND_RETURN
);
if ($this->shouldSkipInferredReturnNode($node, $inferredReturnNode)) {
return null;
}

View File

@ -8,6 +8,7 @@ use PhpParser\Node;
use PhpParser\Node\Stmt\Property;
use PHPStan\Type\MixedType;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo;
use Rector\Core\Exception\ShouldNotHappenException;
use Rector\Core\Rector\AbstractRector;
use Rector\Core\RectorDefinition\CodeSample;
use Rector\Core\RectorDefinition\RectorDefinition;
@ -81,8 +82,12 @@ CODE_SAMPLE
return null;
}
/** @var PhpDocInfo $phpDocInfo */
/** @var PhpDocInfo|null $phpDocInfo */
$phpDocInfo = $node->getAttribute(AttributeKey::PHP_DOC_INFO);
if ($phpDocInfo === null) {
throw new ShouldNotHappenException();
}
$phpDocInfo->changeVarType($propertyType);
return $node;

View File

@ -5,100 +5,116 @@ declare(strict_types=1);
namespace Rector\TypeDeclaration\TypeInferer;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\ArrayDimFetch;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Expr\StaticPropertyFetch;
use PhpParser\Node\Stmt\ClassLike;
use PHPStan\Type\ArrayType;
use PHPStan\Type\MixedType;
use PHPStan\Type\NullType;
use PHPStan\Type\Type;
use Rector\Core\ValueObject\MethodName;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\TypeDeclaration\AlreadyAssignDetector\ConstructorAssignDetector;
use Rector\TypeDeclaration\AlreadyAssignDetector\NullTypeAssignDetector;
use Rector\TypeDeclaration\AlreadyAssignDetector\PropertyDefaultAssignDetector;
use Rector\TypeDeclaration\Matcher\PropertyAssignMatcher;
final class AssignToPropertyTypeInferer extends AbstractTypeInferer
{
/**
* @var bool
* @var ConstructorAssignDetector
*/
private $isAssignedInConstructor = false;
private $constructorAssignDetector;
/**
* @var PropertyAssignMatcher
*/
private $propertyAssignMatcher;
/**
* @var PropertyDefaultAssignDetector
*/
private $propertyDefaultAssignDetector;
/**
* @var NullTypeAssignDetector
*/
private $nullTypeAssignDetector;
public function __construct(
ConstructorAssignDetector $constructorAssignDetector,
PropertyAssignMatcher $propertyAssignMatcher,
PropertyDefaultAssignDetector $propertyDefaultAssignDetector,
NullTypeAssignDetector $nullTypeAssignDetector
) {
$this->constructorAssignDetector = $constructorAssignDetector;
$this->propertyAssignMatcher = $propertyAssignMatcher;
$this->propertyDefaultAssignDetector = $propertyDefaultAssignDetector;
$this->nullTypeAssignDetector = $nullTypeAssignDetector;
}
public function inferPropertyInClassLike(string $propertyName, ClassLike $classLike): Type
{
$assignedExprStaticTypes = [];
$this->isAssignedInConstructor = false;
$assignedExprTypes = [];
$this->callableNodeTraverser->traverseNodesWithCallable($classLike->stmts, function (Node $node) use (
$propertyName,
&$assignedExprStaticTypes
&$assignedExprTypes
) {
if (! $node instanceof Assign) {
return null;
}
$expr = $this->matchPropertyAssignExpr($node, $propertyName);
$expr = $this->propertyAssignMatcher->matchPropertyAssignExpr($node, $propertyName);
if ($expr === null) {
return null;
}
$exprStaticType = $this->nodeTypeResolver->getStaticType($node->expr);
if ($exprStaticType instanceof MixedType) {
$exprStaticType = $this->resolveExprStaticTypeIncludingDimFetch($node);
if ($exprStaticType === null) {
return null;
}
if ($node->var instanceof ArrayDimFetch) {
$exprStaticType = new ArrayType(new MixedType(), $exprStaticType);
}
// is in constructor?
$methodName = $node->getAttribute(AttributeKey::METHOD_NAME);
if ($methodName === MethodName::CONSTRUCT) {
$this->isAssignedInConstructor = true;
}
$assignedExprStaticTypes[] = $exprStaticType;
$assignedExprTypes[] = $exprStaticType;
return null;
});
// add default type, as not initialized in the constructor
if (count($assignedExprStaticTypes) && ! $this->isAssignedInConstructor) {
$assignedExprStaticTypes[] = new NullType();
if ($this->shouldAddNullType($classLike, $propertyName, $assignedExprTypes)) {
$assignedExprTypes[] = new NullType();
}
return $this->typeFactory->createMixedPassedOrUnionType($assignedExprStaticTypes);
return $this->typeFactory->createMixedPassedOrUnionType($assignedExprTypes);
}
private function resolveExprStaticTypeIncludingDimFetch(Assign $assign): ?Type
{
$exprStaticType = $this->nodeTypeResolver->getStaticType($assign->expr);
if ($exprStaticType instanceof MixedType) {
return null;
}
if ($assign->var instanceof ArrayDimFetch) {
return new ArrayType(new MixedType(), $exprStaticType);
}
return $exprStaticType;
}
/**
* Covers:
* - $this->propertyName = $expr;
* - $this->propertyName[] = $expr;
* @param Type[] $assignedExprTypes
*/
private function matchPropertyAssignExpr(Assign $assign, string $propertyName): ?Expr
private function shouldAddNullType(ClassLike $classLike, string $propertyName, array $assignedExprTypes): bool
{
if ($this->isPropertyFetch($assign->var)) {
if (! $this->nodeNameResolver->isName($assign->var, $propertyName)) {
return null;
}
$hasPropertyDefaultValue = $this->propertyDefaultAssignDetector->detect($classLike, $propertyName);
$isAssignedInConstructor = $this->constructorAssignDetector->detect($classLike, $propertyName);
$shouldAddNullType = $this->nullTypeAssignDetector->detect($classLike, $propertyName);
return $assign->expr;
if ((count($assignedExprTypes) === 0) && ($isAssignedInConstructor || $hasPropertyDefaultValue)) {
return false;
}
if ($assign->var instanceof ArrayDimFetch && $this->isPropertyFetch($assign->var->var)) {
if (! $this->nodeNameResolver->isName($assign->var->var, $propertyName)) {
return null;
}
return $assign->expr;
if ($shouldAddNullType === true) {
return ! $isAssignedInConstructor && ! $hasPropertyDefaultValue;
}
return null;
}
private function isPropertyFetch(Node $node): bool
{
return $node instanceof PropertyFetch || $node instanceof StaticPropertyFetch;
return (count($assignedExprTypes) > 0) && (! $isAssignedInConstructor && ! $hasPropertyDefaultValue);
}
}

View File

@ -5,14 +5,16 @@ declare(strict_types=1);
namespace Rector\TypeDeclaration\TypeInferer;
use PhpParser\Node\Stmt\Property;
use PHPStan\Type\ArrayType;
use PHPStan\Type\MixedType;
use PHPStan\Type\NullType;
use PHPStan\Type\NeverType;
use PHPStan\Type\Type;
use PHPStan\Type\VoidType;
use Rector\NodeTypeResolver\PHPStan\Type\TypeFactory;
use Rector\PHPStanStaticTypeMapper\DoctrineTypeAnalyzer;
use Rector\TypeDeclaration\Contract\TypeInferer\PropertyTypeInfererInterface;
use Rector\TypeDeclaration\TypeInferer\PropertyTypeInferer\DefaultValuePropertyTypeInferer;
use Rector\TypeDeclaration\TypeInferer\PropertyTypeInferer\VarDocPropertyTypeInferer;
final class PropertyTypeInferer extends AbstractPriorityAwareTypeInferer
{
@ -36,12 +38,18 @@ final class PropertyTypeInferer extends AbstractPriorityAwareTypeInferer
*/
private $doctrineTypeAnalyzer;
/**
* @var VarDocPropertyTypeInferer
*/
private $varDocPropertyTypeInferer;
/**
* @param PropertyTypeInfererInterface[] $propertyTypeInferers
*/
public function __construct(
array $propertyTypeInferers,
DefaultValuePropertyTypeInferer $defaultValuePropertyTypeInferer,
VarDocPropertyTypeInferer $varDocPropertyTypeInferer,
TypeFactory $typeFactory,
DoctrineTypeAnalyzer $doctrineTypeAnalyzer
) {
@ -49,45 +57,73 @@ final class PropertyTypeInferer extends AbstractPriorityAwareTypeInferer
$this->defaultValuePropertyTypeInferer = $defaultValuePropertyTypeInferer;
$this->typeFactory = $typeFactory;
$this->doctrineTypeAnalyzer = $doctrineTypeAnalyzer;
$this->varDocPropertyTypeInferer = $varDocPropertyTypeInferer;
}
public function inferProperty(Property $property): Type
{
$resolvedTypes = [];
foreach ($this->propertyTypeInferers as $propertyTypeInferer) {
$type = $propertyTypeInferer->inferProperty($property);
if ($type instanceof VoidType || $type instanceof MixedType) {
continue;
}
// default value type must be added to each resolved type
$defaultValueType = $this->defaultValuePropertyTypeInferer->inferProperty($property);
if ($this->shouldUnionWithDefaultValue($defaultValueType, $type)) {
return $this->unionWithDefaultValueType($type, $defaultValueType);
}
return $type;
$resolvedTypes[] = $type;
}
return new MixedType();
// if nothing is clear from variable use, we use @var doc as fallback
if (count($resolvedTypes) > 0) {
$resolvedType = $this->typeFactory->createMixedPassedOrUnionType($resolvedTypes);
} else {
$resolvedType = $this->varDocPropertyTypeInferer->inferProperty($property);
}
// default value type must be added to each resolved type if set
// @todo include in one of inferrers above
$propertyDefaultValue = $property->props[0]->default;
if ($propertyDefaultValue !== null) {
$defaultValueType = $this->defaultValuePropertyTypeInferer->inferProperty($property);
if ($this->shouldUnionWithDefaultValue($defaultValueType, $resolvedType)) {
return $this->unionWithDefaultValueType($defaultValueType, $resolvedType);
}
}
if ($resolvedType === null) {
return new MixedType();
}
return $resolvedType;
}
private function shouldUnionWithDefaultValue(Type $defaultValueType, Type $type): bool
private function shouldUnionWithDefaultValue(Type $defaultValueType, ?Type $type = null): bool
{
if ($defaultValueType instanceof MixedType) {
return false;
}
// skip empty array type (mixed[])
if ($defaultValueType instanceof ArrayType && $defaultValueType->getItemType() instanceof NeverType && $type !== null) {
return false;
}
if ($type === null) {
return true;
}
return ! $this->doctrineTypeAnalyzer->isDoctrineCollectionWithIterableUnionType($type);
}
private function unionWithDefaultValueType(Type $type, Type $defaultValueType): Type
private function unionWithDefaultValueType(Type $defaultValueType, ?Type $resolvedType): Type
{
// default type has bigger priority than @var type, if not nullable type
if (! $defaultValueType instanceof NullType) {
return $defaultValueType;
}
$types[] = $defaultValueType;
$types = [$type, $defaultValueType];
if ($resolvedType !== null) {
$types[] = $resolvedType;
}
return $this->typeFactory->createMixedPassedOrUnionType($types);
}

View File

@ -13,7 +13,8 @@ use Rector\TypeDeclaration\Contract\TypeInferer\PropertyTypeInfererInterface;
/**
* Special case of type inferer - it is always added in the end of the resolved types
*/
final class DefaultValuePropertyTypeInferer implements PropertyTypeInfererInterface
//implements PropertyTypeInfererInterface
final class DefaultValuePropertyTypeInferer
{
/**
* @var NodeTypeResolver

View File

@ -5,27 +5,25 @@ declare(strict_types=1);
namespace Rector\TypeDeclaration\TypeInferer\PropertyTypeInferer;
use PhpParser\Node\Stmt\Property;
use PHPStan\Type\MixedType;
use PHPStan\Type\Type;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\TypeDeclaration\Contract\TypeInferer\PropertyTypeInfererInterface;
final class VarDocPropertyTypeInferer implements PropertyTypeInfererInterface
final class VarDocPropertyTypeInferer
{
public function inferProperty(Property $property): Type
public function inferProperty(Property $property): ?Type
{
/** @var PhpDocInfo|null $phpDocInfo */
$phpDocInfo = $property->getAttribute(AttributeKey::PHP_DOC_INFO);
if ($phpDocInfo === null) {
return new MixedType();
return null;
}
// is empty
if ($phpDocInfo->getPhpDocNode()->children === []) {
return null;
}
return $phpDocInfo->getVarType();
}
public function getPriority(): int
{
return 150;
}
}

View File

@ -5,9 +5,11 @@ namespace Rector\TypeDeclaration\Tests\Rector\Property\CompleteVarDocTypePropert
final class DefaultValue
{
private $number = 5;
private $maybe = false;
private $dreams = [];
private $name = 'John';
private $longName = 'Elton' . 'John';
}
@ -23,18 +25,17 @@ final class DefaultValue
* @var int
*/
private $number = 5;
/**
* @var bool
*/
private $maybe = false;
/**
* @var mixed[]
*/
private $dreams = [];
/**
* @var string
*/
private $name = 'John';
/**
* @var string
*/

View File

@ -0,0 +1,24 @@
<?php
namespace Rector\TypeDeclaration\Tests\Rector\Property\CompleteVarDocTypePropertyRector\Fixture;
final class DefaultValueArrayMixed
{
private $dreams = [];
}
?>
-----
<?php
namespace Rector\TypeDeclaration\Tests\Rector\Property\CompleteVarDocTypePropertyRector\Fixture;
final class DefaultValueArrayMixed
{
/**
* @var mixed[]
*/
private $dreams = [];
}
?>

View File

@ -10,18 +10,16 @@ class SkipMoreSpecific
* @var SomeService[]
*/
private static $registry = [];
/**
* register an SomeService for using.
*
* @param SomeService $service
*/
public static function register(SomeService $service)
{
self::$registry[$service->getLabel()] = $service;
}
/**
* find registered SomeService.
*
* @return SomeService[]
*/
public static function getRegisteredSomeServices()

View File

@ -583,11 +583,11 @@ class Command
*/
protected static $defaultName;
/**
* @var Application
* @var \Symfony\Component\Console\Application|null
*/
private $application;
/**
* @var string
* @var string|null
*/
private $name;
/**
@ -603,11 +603,11 @@ class Command
*/
private $hidden = false;
/**
* @var string
* @var string|null
*/
private $help;
/**
* @var string
* @var string|null
*/
private $description;
/**
@ -627,7 +627,7 @@ class Command
*/
private $code;
/**
* @var HelperSet
* @var \Symfony\Component\Console\Helper\HelperSet|null
*/
private $helperSet;
/**

View File

@ -0,0 +1,62 @@
<?php
namespace Rector\TypeDeclaration\Tests\Rector\Property\CompleteVarDocTypePropertyRector\Fixture;
use Symfony\Component\Console\Input\InputDefinition;
final class CommandDefinedInConstructor
{
private $definition;
public function __construct()
{
$this->definition = new InputDefinition();
}
/**
* @param array|InputDefinition $definition An array of argument and option instances or a definition instance
*/
public function setDefinition($definition)
{
if ($definition instanceof InputDefinition) {
$this->definition = $definition;
} else {
$this->definition->setDefinition($definition);
}
}
}
?>
-----
<?php
namespace Rector\TypeDeclaration\Tests\Rector\Property\CompleteVarDocTypePropertyRector\Fixture;
use Symfony\Component\Console\Input\InputDefinition;
final class CommandDefinedInConstructor
{
/**
* @var \Symfony\Component\Console\Input\InputDefinition
*/
private $definition;
public function __construct()
{
$this->definition = new InputDefinition();
}
/**
* @param array|InputDefinition $definition An array of argument and option instances or a definition instance
*/
public function setDefinition($definition)
{
if ($definition instanceof InputDefinition) {
$this->definition = $definition;
} else {
$this->definition->setDefinition($definition);
}
}
}
?>

View File

@ -45,7 +45,7 @@ use Symfony\Component\Console\Helper\HelperSet;
class SymfonyConsoleHelperSet
{
/**
* @var HelperSet
* @var \Symfony\Component\Console\Helper\HelperSet|null
*/
private $helperSet;

View File

@ -1,211 +0,0 @@
<?php
namespace Rector\TypeDeclaration\Tests\Rector\Property\PropertyTypeDeclarationRector\Fixture;
use App\Api\Poll\Entity\Poll;
use App\Api\User\Entity\User;
use App\Api\User\Entity\UserList;
use App\Entity\LocalizedString;
use App\Entity\LocalizedStringList;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Table(name="answer")
* @ORM\Entity
*/
class Complex
{
/**
* @ORM\OneToMany(targetEntity="App\Entity\LocalizedString", mappedBy="answerText", cascade={"all"})
* @ORM\JoinColumn(nullable=false)
* @ORM\OrderBy({"version" = "DESC"})
*/
private $answer;
/**
* @ORM\ManyToMany(targetEntity="App\Api\User\Entity\User", inversedBy="answers", cascade={"persist", "merge"})
*/
private $voters;
public function getAnswer(): LocalizedStringList
{
if ($this->answer instanceof Collection) {
return new LocalizedStringList($this->answer->getValues());
}
if ($this->answer instanceof LocalizedStringList) {
return $this->answer;
}
return new LocalizedStringList($this->answer);
}
/**
* @param LocalizedStringList|LocalizedString[]|ArrayCollection $answer
*/
public function setAnswer($answer): void
{
if ($answer instanceof LocalizedStringList) {
$this->answer = $answer->getLocalizedStrings();
return;
}
if ($answer instanceof ArrayCollection) {
$this->answer = $answer->getValues();
return;
}
$this->answer = $answer;
}
public function getVoters(): UserList
{
if ($this->voters instanceof Collection) {
return new UserList($this->voters->getValues());
}
if ($this->voters instanceof UserList) {
return $this->voters;
}
return new UserList($this->voters);
}
/**
* @param UserList|User[]|ArrayCollection $voters
*/
public function setVoters($voters): void
{
if ($voters instanceof UserList) {
$this->voters = $voters->getUsers();
return;
}
if ($voters instanceof ArrayCollection) {
$this->voters = $voters->getValues();
return;
}
$this->voters = $voters;
}
public function addVoter(User $user): void
{
$this->voters[] = $user;
}
}
?>
-----
<?php
namespace Rector\TypeDeclaration\Tests\Rector\Property\PropertyTypeDeclarationRector\Fixture;
use App\Api\Poll\Entity\Poll;
use App\Api\User\Entity\User;
use App\Api\User\Entity\UserList;
use App\Entity\LocalizedString;
use App\Entity\LocalizedStringList;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Table(name="answer")
* @ORM\Entity
*/
class Complex
{
/**
* @ORM\OneToMany(targetEntity="App\Entity\LocalizedString", mappedBy="answerText", cascade={"all"})
* @ORM\JoinColumn(nullable=false)
* @ORM\OrderBy({"version" = "DESC"})
* @var \App\Entity\LocalizedString[]|\Doctrine\Common\Collections\Collection
*/
private $answer;
/**
* @ORM\ManyToMany(targetEntity="App\Api\User\Entity\User", inversedBy="answers", cascade={"persist", "merge"})
* @var \App\Api\User\Entity\User[]|\Doctrine\Common\Collections\Collection
*/
private $voters;
public function getAnswer(): LocalizedStringList
{
if ($this->answer instanceof Collection) {
return new LocalizedStringList($this->answer->getValues());
}
if ($this->answer instanceof LocalizedStringList) {
return $this->answer;
}
return new LocalizedStringList($this->answer);
}
/**
* @param LocalizedStringList|LocalizedString[]|ArrayCollection $answer
*/
public function setAnswer($answer): void
{
if ($answer instanceof LocalizedStringList) {
$this->answer = $answer->getLocalizedStrings();
return;
}
if ($answer instanceof ArrayCollection) {
$this->answer = $answer->getValues();
return;
}
$this->answer = $answer;
}
public function getVoters(): UserList
{
if ($this->voters instanceof Collection) {
return new UserList($this->voters->getValues());
}
if ($this->voters instanceof UserList) {
return $this->voters;
}
return new UserList($this->voters);
}
/**
* @param UserList|User[]|ArrayCollection $voters
*/
public function setVoters($voters): void
{
if ($voters instanceof UserList) {
$this->voters = $voters->getUsers();
return;
}
if ($voters instanceof ArrayCollection) {
$this->voters = $voters->getValues();
return;
}
$this->voters = $voters;
}
public function addVoter(User $user): void
{
$this->voters[] = $user;
}
}
?>

View File

@ -34,7 +34,7 @@ class ConstructorParam
*/
private $stringValue;
/**
* @var stdClass
* @var \stdClass
*/
private $docBlockService;

View File

@ -73,13 +73,13 @@ final class GetterType
{
/**
* @Serializer\Type("string")
* @var string
* @var string|null
*/
private $email;
/**
* @Serializer\Type("string")
* @var string
* @var string|null
*/
private $password;