Open
Show file tree
Hide file tree
Changes from all commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Failed to load files.
Original file line numberDiff line numberDiff line change
Expand Up@@ -64,9 +64,6 @@ public function processNode(Node $node, Scope $scope): array
}

$className = $class->getName();
if ($objectManager->getMetadataFactory()->isTransient($className)) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm trying to figure out internally why the embeddables are considered transient, but removal of this check causes no test failures

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also classes not managed by Doctrine will have an exception thrown a bit later while trying to get their metadata and will return early from the rule with no errors, so I believe the intent is preserved

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've created an issue in ORM but I think we can continue without waiting for its resolution doctrine/orm#8006

return [];
}

try {
$metadata = $objectManager->getClassMetadata($className);
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Doctrine\ORM;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MissingPropertyFromReflectionException;
use PHPStan\Rules\Rule;
use PHPStan\Type\Doctrine\ObjectMetadataResolver;
use PHPStan\Type\ObjectType;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\VerbosityLevel;
use function sprintf;

/**
* @implements Rule<Node\Stmt\PropertyProperty>
*/
class EntityEmbeddableRule implements Rule
{

/** @var \PHPStan\Type\Doctrine\ObjectMetadataResolver */
private $objectMetadataResolver;

public function __construct(ObjectMetadataResolver $objectMetadataResolver)
{
$this->objectMetadataResolver = $objectMetadataResolver;
}

public function getNodeType(): string
{
return Node\Stmt\PropertyProperty::class;
}

public function processNode(Node $node, Scope $scope): array
{
$class = $scope->getClassReflection();
if ($class === null) {
return [];
}

$objectManager = $this->objectMetadataResolver->getObjectManager();
if ($objectManager === null) {
return [];
}

$className = $class->getName();

try {
$metadata = $objectManager->getClassMetadata($className);
} catch (\Doctrine\ORM\Mapping\MappingException $e) {
return [];
}

$classMetadataInfo = 'Doctrine\ORM\Mapping\ClassMetadataInfo';
if (!$metadata instanceof $classMetadataInfo) {
return [];
}

$propertyName = (string) $node->name;
try {
$property = $class->getNativeProperty($propertyName);
} catch (MissingPropertyFromReflectionException $e) {
return [];
}

if (!isset($metadata->embeddedClasses[$propertyName])) {
return [];
}

$errors = [];
$embeddedClass = $metadata->embeddedClasses[$propertyName];
$propertyWritableType = $property->getWritableType();
$accordingToMapping = new ObjectType($embeddedClass['class']);
if (!TypeCombinator::removeNull($propertyWritableType)->equals($accordingToMapping)) {
$errors[] = sprintf(
'Property %s::$%s type mapping mismatch: mapping specifies %s but property expects %s.',
$class->getName(),
$propertyName,
$accordingToMapping->describe(VerbosityLevel::typeOnly()),
$propertyWritableType->describe(VerbosityLevel::typeOnly())
);
}

return $errors;
}

}
Original file line numberDiff line numberDiff line change
Expand Up@@ -153,6 +153,21 @@ public function testCustomType(): void
]);
}

public function testEmbeddable(): void
{
$this->analyse([__DIR__ . '/data/Embeddable.php'], []);
}

public function testBrokenEmbeddable(): void
{
$this->analyse([__DIR__ . '/data/BrokenEmbeddable.php'], [
[
'Property PHPStan\Rules\Doctrine\ORM\BrokenEmbeddable::$one type mapping mismatch: database can contain string|null but property expects string.',
16,
],
]);
}

public function testUnknownType(): void
{
$this->analyse([__DIR__ . '/data/EntityWithUnknownType.php'], [
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Doctrine\ORM;

use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use PHPStan\Type\Doctrine\ObjectMetadataResolver;

/**
* @extends RuleTestCase<EntityEmbeddableRule>
*/
class EntityEmbeddableRuleTest extends RuleTestCase
{

protected function getRule(): Rule
{
return new EntityEmbeddableRule(
new ObjectMetadataResolver(__DIR__ . '/entity-manager.php', null)
);
}

public function testEmbedded(): void
{
$this->analyse([__DIR__ . '/data/EntityWithEmbeddable.php'], []);
}

public function testEmbeddedWithWrongTypeHint(): void
{
$this->analyse([__DIR__ . '/data/EntityWithBrokenEmbeddable.php'], [
[
'Property PHPStan\Rules\Doctrine\ORM\EntityWithBrokenEmbeddable::$embedded type mapping mismatch: mapping specifies PHPStan\Rules\Doctrine\ORM\Embeddable but property expects int.',
24,
],
]);
}

}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Doctrine\ORM;

use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Embeddable()
*/
class BrokenEmbeddable
{
/**
* @ORM\Column(type="string", nullable=true)
* @var string
*/
private $one;
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Doctrine\ORM;

use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Entity()
*/
class EntityWithBrokenEmbeddable
{

/**
* @ORM\Id()
* @ORM\Column(type="integer")
* @var int
*/
private $id;

/**
* @ORM\Embedded(class=Embeddable::class)
* @var int
*/
private $embedded;
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Doctrine\ORM;

use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Entity()
*/
class EntityWithEmbeddable
{

/**
* @ORM\Id()
* @ORM\Column(type="integer")
* @var int
*/
private $id;

/**
* @ORM\Embedded(class=Embeddable::class)
* @var Embeddable
*/
private $embedded;

/**
* @ORM\Embedded(class=Embeddable::class)
* @var ?Embeddable
*/
private $nullable;
}