Skip to content

Resolve entity metadata without objectManagerLoader #253

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"doctrine/lexer": "^1.2.1",
"doctrine/mongodb-odm": "^1.3 || ^2.1",
"doctrine/orm": "^2.11.0",
"doctrine/persistence": "^1.1 || ^2.0",
"doctrine/persistence": "^1.3.8 || ^2.2.1",
"nesbot/carbon": "^2.49",
"nikic/php-parser": "^4.13.2",
"php-parallel-lint/php-parallel-lint": "^1.2",
Expand Down
3 changes: 3 additions & 0 deletions phpcs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
</properties>
<exclude name="SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingTraversableParameterTypeHintSpecification"/>
<exclude name="SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingTraversableReturnTypeHintSpecification"/>
<exclude name="SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingAnyTypeHint"/>
<exclude name="SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint"/>
</rule>
<rule ref="SlevomatCodingStandard.TypeHints.PropertyTypeHint">
<properties>
Expand All @@ -49,6 +51,7 @@
<property name="enableObjectTypeHint" value="false"/>
</properties>
<exclude name="SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingTraversableTypeHintSpecification"/>
<exclude name="SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingAnyTypeHint"/>
</rule>
<rule ref="SlevomatCodingStandard.ControlStructures.AssignmentInCondition"/>
<rule ref="SlevomatCodingStandard.Operators.DisallowEqualOperators"/>
Expand Down
32 changes: 32 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
parameters:
ignoreErrors:
-
message: "#^Parameter \\#1 \\$drivers of class PHPStan\\\\Doctrine\\\\Mapping\\\\MappingDriverChain constructor expects array\\<Doctrine\\\\Persistence\\\\Mapping\\\\Driver\\\\MappingDriver\\>, array\\<int, Doctrine\\\\ORM\\\\Mapping\\\\Driver\\\\AnnotationDriver\\|Doctrine\\\\ORM\\\\Mapping\\\\Driver\\\\AttributeDriver\\> given\\.$#"
count: 1
path: src/Doctrine/Mapping/ClassMetadataFactory.php

-
message: "#^Parameter \\#1 \\$entityName of class Doctrine\\\\ORM\\\\Mapping\\\\ClassMetadata constructor expects class\\-string\\<T of object\\>, string given\\.$#"
count: 1
path: src/Doctrine/Mapping/ClassMetadataFactory.php

-
message: "#^Parameter \\#1 \\$className \\(class\\-string\\) of method PHPStan\\\\Doctrine\\\\Mapping\\\\MappingDriverChain\\:\\:isTransient\\(\\) should be contravariant with parameter \\$className \\(string\\) of method Doctrine\\\\Persistence\\\\Mapping\\\\Driver\\\\MappingDriver\\:\\:isTransient\\(\\)$#"
count: 1
path: src/Doctrine/Mapping/MappingDriverChain.php

-
message: "#^Parameter \\#1 \\$className \\(class\\-string\\) of method PHPStan\\\\Doctrine\\\\Mapping\\\\MappingDriverChain\\:\\:loadMetadataForClass\\(\\) should be contravariant with parameter \\$className \\(string\\) of method Doctrine\\\\Persistence\\\\Mapping\\\\Driver\\\\MappingDriver\\:\\:loadMetadataForClass\\(\\)$#"
count: 1
path: src/Doctrine/Mapping/MappingDriverChain.php

-
message: "#^PHPDoc tag @return contains generic type Doctrine\\\\Persistence\\\\Mapping\\\\ClassMetadataFactory\\<Doctrine\\\\ORM\\\\Mapping\\\\ClassMetadata\\> but interface Doctrine\\\\Persistence\\\\Mapping\\\\ClassMetadataFactory is not generic\\.$#"
count: 1
path: src/Type/Doctrine/ObjectMetadataResolver.php

-
message: "#^PHPDoc tag @var for property PHPStan\\\\Type\\\\Doctrine\\\\ObjectMetadataResolver\\:\\:\\$metadataFactory contains generic type Doctrine\\\\Persistence\\\\Mapping\\\\ClassMetadataFactory\\<Doctrine\\\\ORM\\\\Mapping\\\\ClassMetadata\\> but interface Doctrine\\\\Persistence\\\\Mapping\\\\ClassMetadataFactory is not generic\\.$#"
count: 1
path: src/Type/Doctrine/ObjectMetadataResolver.php

3 changes: 3 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
includes:
- extension.neon
- rules.neon
- phpstan-baseline.neon
- vendor/phpstan/phpstan-strict-rules/rules.neon
- vendor/phpstan/phpstan-phpunit/extension.neon
- vendor/phpstan/phpstan-phpunit/rules.neon
Expand All @@ -13,6 +14,8 @@ parameters:
- tests/*/data-php-*/*
- tests/Rules/Doctrine/ORM/entity-manager.php

reportUnmatchedIgnoredErrors: false

ignoreErrors:
-
message: '~^Variable method call on Doctrine\\ORM\\QueryBuilder~'
Expand Down
46 changes: 46 additions & 0 deletions src/Doctrine/Mapping/ClassMetadataFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php declare(strict_types = 1);

namespace PHPStan\Doctrine\Mapping;

use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\EventManager;
use Doctrine\DBAL\Platforms\MySqlPlatform;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\Driver\AnnotationDriver;
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
use function class_exists;

class ClassMetadataFactory extends \Doctrine\ORM\Mapping\ClassMetadataFactory
{

protected function initialize(): void
{
$parentReflection = new \ReflectionClass(parent::class);
$driverProperty = $parentReflection->getProperty('driver');
$driverProperty->setAccessible(true);

$drivers = [
new AnnotationDriver(new AnnotationReader()),
];
if (class_exists(AttributeDriver::class) && PHP_VERSION_ID >= 80000) {
$drivers[] = new AttributeDriver([]);
}

$driverProperty->setValue($this, count($drivers) === 1 ? $drivers[0] : new MappingDriverChain($drivers));

$evmProperty = $parentReflection->getProperty('evm');
$evmProperty->setAccessible(true);
$evmProperty->setValue($this, new EventManager());
$this->initialized = true;

$targetPlatformProperty = $parentReflection->getProperty('targetPlatform');
$targetPlatformProperty->setAccessible(true);
$targetPlatformProperty->setValue($this, new MySqlPlatform());
}

protected function newClassMetadataInstance($className)
{
return new ClassMetadata($className);
}

}
65 changes: 65 additions & 0 deletions src/Doctrine/Mapping/MappingDriverChain.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php declare(strict_types = 1);

namespace PHPStan\Doctrine\Mapping;

use Doctrine\Persistence\Mapping\ClassMetadata;
use Doctrine\Persistence\Mapping\Driver\MappingDriver;

class MappingDriverChain implements MappingDriver
{

/** @var MappingDriver[] */
private $drivers;

/**
* @param MappingDriver[] $drivers
*/
public function __construct(array $drivers)
{
$this->drivers = $drivers;
}

/**
* @param class-string $className
*/
public function loadMetadataForClass($className, ClassMetadata $metadata): void
{
foreach ($this->drivers as $driver) {
if ($driver->isTransient($className)) {
continue;
}

$driver->loadMetadataForClass($className, $metadata);
return;
}
}

public function getAllClassNames()
{
$all = [];
foreach ($this->drivers as $driver) {
foreach ($driver->getAllClassNames() as $className) {
$all[] = $className;
}
}

return $all;
}

/**
* @param class-string $className
*/
public function isTransient($className)
{
foreach ($this->drivers as $driver) {
if ($driver->isTransient($className)) {
continue;
}

return false;
}

return true;
}

}
39 changes: 30 additions & 9 deletions src/Type/Doctrine/ObjectMetadataResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

namespace PHPStan\Type\Doctrine;

use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\Persistence\Mapping\ClassMetadataFactory;
use Doctrine\Persistence\ObjectManager;
use PHPStan\Reflection\ReflectionProvider;
use function is_file;
Expand All @@ -26,6 +28,9 @@ final class ObjectMetadataResolver
/** @var string|null */
private $resolvedRepositoryClass;

/** @var ClassMetadataFactory<ClassMetadata>|null */
private $metadataFactory;

public function __construct(
ReflectionProvider $reflectionProvider,
?string $objectManagerLoader,
Expand Down Expand Up @@ -66,35 +71,51 @@ public function getObjectManager(): ?ObjectManager
public function isTransient(string $className): bool
{
$objectManager = $this->getObjectManager();
if ($objectManager === null) {
return true;
}

try {
if ($objectManager === null) {
return $this->getMetadataFactory()->isTransient($className);
}

return $objectManager->getMetadataFactory()->isTransient($className);
} catch (\ReflectionException $e) {
return true;
}
}

/**
* @return ClassMetadataFactory<ClassMetadata>
*/
private function getMetadataFactory(): ClassMetadataFactory
{
if ($this->metadataFactory !== null) {
return $this->metadataFactory;
}

$metadataFactory = new \PHPStan\Doctrine\Mapping\ClassMetadataFactory();

return $this->metadataFactory = $metadataFactory;
}

/**
* @template T of object
* @param class-string<T> $className
* @return ClassMetadataInfo<T>|null
*/
public function getClassMetadata(string $className): ?ClassMetadataInfo
{
$objectManager = $this->getObjectManager();
if ($objectManager === null) {
return null;
}

if ($this->isTransient($className)) {
return null;
}

$objectManager = $this->getObjectManager();

try {
$metadata = $objectManager->getClassMetadata($className);
if ($objectManager === null) {
$metadata = $this->getMetadataFactory()->getMetadataFor($className);
} else {
$metadata = $objectManager->getClassMetadata($className);
}
} catch (\Doctrine\ORM\Mapping\MappingException $e) {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php declare(strict_types = 1);

namespace PHPStan\DoctrineIntegration\ORM;

use PHPStan\Testing\LevelsTestCase;

final class EntityManagerWithoutObjectManagerLoaderIntegrationTest extends LevelsTestCase
{

/**
* @return string[][]
*/
public function dataTopics(): array
{
return [
['entityManagerDynamicReturn'],
['entityManagerMergeReturn'],
['customRepositoryUsage'],
['dbalQueryBuilderExecuteDynamicReturn'],
];
}

public function getDataPath(): string
{
return __DIR__ . '/data';
}

public function getPhpStanExecutablePath(): string
{
return __DIR__ . '/../../../vendor/phpstan/phpstan/phpstan';
}

public function getPhpStanConfigPath(): string
{
return __DIR__ . '/phpstan-without-object-manager-loader.neon';
}

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

namespace PHPStan\DoctrineIntegration\ORM;

use PHPStan\Testing\LevelsTestCase;

final class EntityRepositoryWithoutObjectManagerLoaderDynamicReturnIntegrationTest extends LevelsTestCase
{

/**
* @return string[][]
*/
public function dataTopics(): array
{
return [
['entityRepositoryDynamicReturn'],
];
}

public function getDataPath(): string
{
if (PHP_MAJOR_VERSION === 7 && PHP_MINOR_VERSION === 1) {
return __DIR__ . '/data-php-7.1';
}

return __DIR__ . '/data';
}

public function getPhpStanExecutablePath(): string
{
return __DIR__ . '/../../../vendor/phpstan/phpstan/phpstan';
}

public function getPhpStanConfigPath(): string
{
return __DIR__ . '/phpstan-without-object-manager-loader.neon';
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
includes:
- ../../../extension.neon
- ../../../rules.neon
- phar://phpstan.phar/conf/bleedingEdge.neon

parameters:
doctrine:
reportDynamicQueryBuilders: true
queryBuilderClass: PHPStan\DoctrineIntegration\ORM\QueryBuilder\CustomQueryBuilder

This file was deleted.

Loading