Skip to content

Resolve custom repository classes #47

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

Closed
wants to merge 4 commits into from
Closed
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
15 changes: 7 additions & 8 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@
"require": {
"php": "~7.1",
"phpstan/phpstan": "^0.11",
"nikic/php-parser": "^4.0"
"nikic/php-parser": "^4.0",
"doctrine/orm": "^2.5",
"doctrine/common": "^2.7",
"doctrine/annotations": "^1.5",
"doctrine/persistence": "^1.1",
"doctrine/dbal": "^2.5",
"doctrine/event-manager": "^1.0"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm quite unconvinced of the need for this - composer-require-checker demands this, but actually if you dont configure 'mapping' you wont need any of this, and if you do, you should have it installed anyway

Copy link
Member

Choose a reason for hiding this comment

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

I don't understand why any of the changes in composer.json were needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Its because of the code that loads/creates the entity manager, but if we use the approach in #40 this wont be needed.

},
"require-dev": {
"consistence/coding-standard": "^3.0.1",
Expand All @@ -23,15 +29,8 @@
"phpstan/phpstan-strict-rules": "^0.11",
"phpunit/phpunit": "^7.0",
"slevomat/coding-standard": "^4.5.2",
"doctrine/common": "^2.7",
"doctrine/orm": "^2.5",
"doctrine/collections": "^1.0"
},
"conflict": {
"doctrine/common": "<2.7",
"doctrine/orm": "<2.5",
"doctrine/collections": "<1.0"
},
"autoload": {
"psr-4": {
"PHPStan\\": "src/"
Expand Down
9 changes: 8 additions & 1 deletion extension.neon
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
parameters:
doctrine:
repositoryClass: Doctrine\ORM\EntityRepository
metadata: []
allCollectionsSelectable: true

conditionalTags:
Expand All @@ -21,7 +22,7 @@ services:
-
class: PHPStan\Type\Doctrine\ObjectManagerGetRepositoryDynamicReturnTypeExtension
arguments:
repositoryClass: %doctrine.repositoryClass%
metadataProvider: @PHPStan\DoctrineClassMetadataProvider
tags:
- phpstan.broker.dynamicMethodReturnTypeExtension
-
Expand All @@ -32,3 +33,9 @@ services:
class: PHPStan\Type\Doctrine\ObjectRepositoryDynamicReturnTypeExtension
tags:
- phpstan.broker.dynamicMethodReturnTypeExtension
-
class: PHPStan\DoctrineClassMetadataProvider
arguments:
repositoryClass: %doctrine.repositoryClass%
mapping: %doctrine.metadata%

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

namespace PHPStan;

use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\Annotations\AnnotationRegistry;
use Doctrine\Common\EventManager;
use Doctrine\Common\Persistence\Mapping\Driver\MappingDriver;
use Doctrine\Common\Persistence\Mapping\Driver\MappingDriverChain;
use Doctrine\Common\Persistence\Mapping\MappingException;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Mapping;
use Doctrine\ORM\Proxy\ProxyFactory;

class DoctrineClassMetadataProvider
{

/** @var ?EntityManager */
private $em;

/** @var string */
private $repositoryClass;

/**
* @param string $repositoryClass
* @param mixed[] $mapping
*/
public function __construct(string $repositoryClass, array $mapping)
{
if (count($mapping) > 0) {
$configuration = new Configuration();
$configuration->setDefaultRepositoryClassName($repositoryClass);
$configuration->setMetadataDriverImpl($this->setupMappingDriver($mapping));
$configuration->setProxyDir('/dev/null');
$configuration->setProxyNamespace('__DP__');
$configuration->setAutoGenerateProxyClasses(ProxyFactory::AUTOGENERATE_EVAL);
$evm = new EventManager();
$this->em = EntityManager::create(
\Doctrine\DBAL\DriverManager::getConnection(['host' => '/:memory:', 'driver' => 'pdo_sqlite'], $configuration, $evm),
$configuration,
$evm
);
}
$this->repositoryClass = $repositoryClass;
}

/**
* @param mixed[] $mapping
*/
private function setupMappingDriver(array $mapping): MappingDriver
{
$driver = new MappingDriverChain();
foreach ($mapping as $namespace => $config) {
switch ($config['type']) {
case 'annotation':
AnnotationRegistry::registerUniqueLoader('class_exists');
$nested = new Mapping\Driver\AnnotationDriver(new AnnotationReader(), $config['paths']);
break;
case 'yml':
case 'yaml':
$nested = new Mapping\Driver\YamlDriver($config['paths']);
break;
case 'xml':
$nested = new Mapping\Driver\XmlDriver($config['paths']);
break;
default:
throw new \InvalidArgumentException('Unknown mapping type: ' . $config['type']);
}
$driver->addDriver($nested, $namespace);
}
return $driver;
}

public function getBaseRepositoryClass(): string
{
return $this->repositoryClass;
}

public function getRepositoryClass(string $className): string
{
if ($this->em === null) {
return $this->getBaseRepositoryClass();
}

try {
return $this->em->getClassMetadata($className)->customRepositoryClassName ?: $this->getBaseRepositoryClass();
} catch (MappingException $e) {
return $this->getBaseRepositoryClass();
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\DoctrineClassMetadataProvider;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\Constant\ConstantStringType;
Expand All @@ -13,12 +14,12 @@
class ObjectManagerGetRepositoryDynamicReturnTypeExtension implements \PHPStan\Type\DynamicMethodReturnTypeExtension
{

/** @var string */
private $repositoryClass;
/** @var DoctrineClassMetadataProvider */
private $metadataProvider;

public function __construct(string $repositoryClass)
public function __construct(DoctrineClassMetadataProvider $metadataProvider)
{
$this->repositoryClass = $repositoryClass;
$this->metadataProvider = $metadataProvider;
}

public function getClass(): string
Expand Down Expand Up @@ -47,7 +48,7 @@ public function getTypeFromMethodCall(
return new MixedType();
}

return new ObjectRepositoryType($argType->getValue(), $this->repositoryClass);
return new ObjectRepositoryType($argType->getValue(), $this->metadataProvider->getRepositoryClass($argType->getValue()));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ public function dataTopics(): array
{
return [
['entityManagerDynamicReturn'],
['entityManagerDynamicReturnCustomRepositoryAnnotations'],
['entityManagerDynamicReturnCustomRepositoryXml'],
['entityManagerDynamicReturnCustomRepositoryYml'],
['entityRepositoryDynamicReturn'],
['entityManagerMergeReturn'],
];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{
"message": "Call to an undefined method PHPStan\\DoctrineIntegration\\ORM\\EntityManagerDynamicReturnCustomRepositoryAnnotations\\MyEntityRepository<PHPStan\\DoctrineIntegration\\ORM\\EntityManagerDynamicReturnCustomRepositoryAnnotations\\MyEntity>::nonexistant().",
"line": 35,
"ignorable": true
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php declare(strict_types = 1);

namespace PHPStan\DoctrineIntegration\ORM\EntityManagerDynamicReturnCustomRepositoryAnnotations;

use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Mapping as ORM;
use RuntimeException;

class Example
{
/**
* @var EntityManagerInterface
*/
private $entityManager;

public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}

public function findDynamicType(): void
{
$test = $this->entityManager->getRepository(MyEntity::class)->findMyEntity(1);

if ($test === null) {
throw new RuntimeException('Sorry, but no...');
}

$test->doSomething();
}

public function errorDynamicType(): void
{
$this->entityManager->getRepository(MyEntity::class)->nonexistant();
}
}

/**
* @ORM\Entity(repositoryClass="MyEntityRepository")
*/
class MyEntity
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*
* @var int
*/
private $id;

public function doSomething(): void
{
}
}

class MyEntityRepository extends EntityRepository
{
public function findMyEntity($id): ?MyEntity
{
return $this->findOneBy([
'id' => $id
]);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{
"message": "Call to an undefined method PHPStan\\DoctrineIntegration\\ORM\\EntityManagerDynamicReturnCustomRepositoryXml\\MyEntityRepository<PHPStan\\DoctrineIntegration\\ORM\\EntityManagerDynamicReturnCustomRepositoryXml\\MyEntity>::nonexistant().",
"line": 35,
"ignorable": true
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php declare(strict_types = 1);

namespace PHPStan\DoctrineIntegration\ORM\EntityManagerDynamicReturnCustomRepositoryXml;

use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Mapping as ORM;
use RuntimeException;

class Example
{
/**
* @var EntityManagerInterface
*/
private $entityManager;

public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}

public function findDynamicType(): void
{
$test = $this->entityManager->getRepository(MyEntity::class)->findMyEntity(1);

if ($test === null) {
throw new RuntimeException('Sorry, but no...');
}

$test->doSomething();
}

public function errorDynamicType(): void
{
$this->entityManager->getRepository(MyEntity::class)->nonexistant();
}
}

class MyEntity
{
private $id;

public function doSomething(): void
{
}
}

class MyEntityRepository extends EntityRepository
{
public function findMyEntity($id): ?MyEntity
{
return $this->findOneBy([
'id' => $id
]);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{
"message": "Call to an undefined method PHPStan\\DoctrineIntegration\\ORM\\EntityManagerDynamicReturnCustomRepositoryYml\\MyEntityRepository<PHPStan\\DoctrineIntegration\\ORM\\EntityManagerDynamicReturnCustomRepositoryYml\\MyEntity>::nonexistant().",
"line": 35,
"ignorable": true
}
]
Loading