Skip to content

Commit 648499e

Browse files
authored
Use native Doctrine mapping tools (#29)
1 parent b27c60f commit 648499e

19 files changed

+338
-229
lines changed

.github/codecov.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
coverage:
2+
status:
3+
patch: false
4+
project:
5+
default: false
6+
src:
7+
paths: src
8+
threshold: 5%

README.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,30 @@ A Doctrine mocking library for testing
1313
In your unit tests that need an Entity Manager, use a `new \Firehed\Mocktrine\InMemoryEntityManager`. Done!
1414

1515
Any object with Doctrine's entity annotations (`@Entity`, `@Id`, `@Column`, etc) should work without modification.
16-
Only annotation-based entity classes are supported; XML and Yaml are not.
1716

1817
This library aims to provide as much type information as possible, so that static analysis tools (such as PHPStan) work well without additional plugins.
1918

19+
### Mapping support
20+
21+
As of version 0.5, any mapping driver supported by Doctrine can be used with this library.
22+
The `InMemoryEntityManager` accepts the driver as an optional parameter.
23+
24+
```diff
25+
- $em = new Mocktrine\InMemoryEntityManager();
26+
+ $em = new Mocktrine\InMemoryEntityManager(
27+
+ \Doctrine\ORM\Mapping\Driver\AttributeDriver(['src/Model']),
28+
+ );
29+
```
30+
31+
You can also grab the value directly from your Doctrine config:
32+
```php
33+
$config = Setup::createAnnotationMetadataDriverConfiguration(...)
34+
$driver = $config->getMetadataDriverImpl();
35+
$em = new Mocktrine\InMemoryEntityManager($driver)
36+
```
37+
38+
If a driver is not provided, it will default to the `SimpleAnnotationReader` that's used via `Setup::createAnnotationMetadataConfiguration`.
39+
2040
## Supported features
2141

2242
The following methods on Doctrine's `EntityManagerInterface` should all work as expected:

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"type": "library",
55
"require": {
66
"php": "^7.4 || ^8.0",
7+
"doctrine/annotations": "^1.10",
78
"doctrine/collections": "^1.6",
89
"doctrine/orm": "^2.7",
910
"doctrine/persistence": "^1.3 || ^2.0",

src/CriteriaEvaluator.php

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@
1212
Expression,
1313
Value,
1414
};
15+
use Doctrine\Persistence\Mapping\ClassMetadata;
1516
use DomainException;
1617
use ReflectionClass;
1718
use UnexpectedValueException;
18-
use phpDocumentor\Reflection\DocBlockFactory;
1919

2020
use function array_filter;
2121
use function array_map;
@@ -44,37 +44,24 @@
4444
*/
4545
class CriteriaEvaluator
4646
{
47-
private static ?DocBlockFactory $docBlockFactory = null;
48-
49-
/** @var class-string<Entity> */
50-
private string $className;
47+
/** @var ClassMetadata<Entity> */
48+
private ClassMetadata $metadata;
5149

5250
/** @var \ReflectionProperty[] */
5351
private array $reflectionProperties;
5452

5553
/**
56-
* @param class-string<Entity> $className
54+
* @param ClassMetadata<Entity> $metadata
5755
*/
58-
public function __construct(string $className)
56+
public function __construct(ClassMetadata $metadata)
5957
{
58+
$this->metadata = $metadata;
59+
$className = $metadata->getName();
6060
$rc = new ReflectionClass($className);
61-
$this->className = $className;
62-
63-
if (!self::$docBlockFactory) {
64-
self::$docBlockFactory = DocBlockFactory::createInstance();
65-
}
66-
67-
foreach ($rc->getProperties() as $rp) {
68-
$docComment = $rp->getDocComment();
69-
assert($docComment !== false);
70-
71-
$docblock = self::$docBlockFactory->create($docComment);
72-
// TODO: support other relations
73-
// TODO: support PHP 8 annotations if Doctrine ORM ends up doing so
74-
if ($docblock->hasTag('Column')) {
75-
$rp->setAccessible(true);
76-
$this->reflectionProperties[$rp->getName()] = $rp;
77-
}
61+
foreach ($metadata->getFieldNames() as $fieldName) {
62+
$rp = $rc->getProperty($fieldName);
63+
$rp->setAccessible(true);
64+
$this->reflectionProperties[$fieldName] = $rp;
7865
}
7966
}
8067

@@ -243,7 +230,7 @@ private function getValueOfProperty(object $entity, string $property)
243230
throw new UnexpectedValueException(sprintf(
244231
'Property "%s" is not a mapped field on class "%s"',
245232
$property,
246-
$this->className,
233+
$this->metadata->getName(),
247234
));
248235
}
249236
return $this->reflectionProperties[$property]->getValue($entity);
@@ -264,7 +251,7 @@ private function sortResults(array $results, array $orderBy): array
264251
throw new UnexpectedValueException(sprintf(
265252
'Sort field "%s" is not a mapped field on class "%s"',
266253
$property,
267-
$this->className,
254+
$this->metadata->getName(),
268255
));
269256
}
270257
}

src/CriteriaEvaluatorFactory.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
namespace Firehed\Mocktrine;
66

7+
use Doctrine\Persistence\Mapping\ClassMetadata;
8+
79
use function array_key_exists;
810

911
/**
@@ -19,13 +21,14 @@ class CriteriaEvaluatorFactory
1921
private static array $instances = [];
2022

2123
/**
22-
* @param class-string<Entity> $className
24+
* @param ClassMetadata<Entity> $metadata
2325
* @return CriteriaEvaluator<Entity>
2426
*/
25-
public static function getInstance(string $className): CriteriaEvaluator
27+
public static function getInstance(ClassMetadata $metadata): CriteriaEvaluator
2628
{
29+
$className = $metadata->getName();
2730
if (!array_key_exists($className, self::$instances)) {
28-
self::$instances[$className] = new CriteriaEvaluator($className);
31+
self::$instances[$className] = new CriteriaEvaluator($metadata);
2932
}
3033
return self::$instances[$className];
3134
}

src/InMemoryEntityManager.php

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
namespace Firehed\Mocktrine;
66

7+
use Doctrine\Common\Annotations\AnnotationRegistry;
8+
use Doctrine\Common\Annotations\SimpleAnnotationReader;
79
use Doctrine\ORM\{
810
Configuration,
911
EntityManagerInterface,
@@ -16,12 +18,19 @@
1618
Query\ResultSetMapping,
1719
UnitOfWork,
1820
};
21+
use Doctrine\ORM\Mapping\{
22+
Driver\AnnotationDriver,
23+
Driver\DoctrineAnnotations,
24+
};
1925
use Doctrine\Persistence\Mapping\{
2026
ClassMetadata,
2127
ClassMetadataFactory,
28+
Driver\MappingDriver,
2229
};
2330
use RuntimeException;
2431

32+
use function class_exists;
33+
2534
class InMemoryEntityManager implements EntityManagerInterface
2635
{
2736
/**
@@ -33,6 +42,17 @@ class InMemoryEntityManager implements EntityManagerInterface
3342
*/
3443
private $repos = [];
3544

45+
/**
46+
* Default instance, for performance
47+
*/
48+
private static ?MappingDriver $defaultMappingDriver = null;
49+
50+
/**
51+
* The mapping driver used for reading the Doctrine ORM mappings from
52+
* entities.
53+
*/
54+
private MappingDriver $mappingDriver;
55+
3656
/**
3757
* @template Entity of object
3858
* @var array<class-string<Entity>, array<Entity>>
@@ -50,6 +70,20 @@ class InMemoryEntityManager implements EntityManagerInterface
5070
*/
5171
private array $onFlushCallbacks = [];
5272

73+
public function __construct(?MappingDriver $driver = null)
74+
{
75+
if ($driver === null) {
76+
// Doctrine's default
77+
// `createAnnotationMetadataDriverConfiguration()` uses the simple
78+
// annotation reader. This is configurable in Setup, but we will
79+
// emulate the default case.
80+
// If you would like different behavior, provide the driver
81+
// directly.
82+
$driver = self::getDefaultMappingDriver();
83+
}
84+
$this->mappingDriver = $driver;
85+
}
86+
5387
public function addOnFlushCallback(callable $callback): void
5488
{
5589
$this->onFlushCallbacks[] = $callback;
@@ -198,7 +232,6 @@ public function flush()
198232
}
199233
$idField = $repo->getIdField();
200234
$idType = $repo->getIdType();
201-
assert($idField !== null);
202235
$rp = new \ReflectionProperty($className, $idField);
203236
$rp->setAccessible(true);
204237
foreach ($entities as $entity) {
@@ -228,7 +261,7 @@ public function getRepository($className)
228261
{
229262
// https://github.com/phpstan/phpstan/issues/2761
230263
if (!isset($this->repos[$className])) {
231-
$this->repos[$className] = new InMemoryRepository($className);
264+
$this->repos[$className] = new InMemoryRepository($className, $this->mappingDriver);
232265
}
233266

234267
return $this->repos[$className];
@@ -631,4 +664,20 @@ public function hasFilters()
631664
{
632665
throw new RuntimeException(__METHOD__ . ' not yet implemented');
633666
}
667+
668+
private static function getDefaultMappingDriver(): MappingDriver
669+
{
670+
if (self::$defaultMappingDriver === null) {
671+
// Hack: reproduce the logic of AnnotationRegistry::registerFile,
672+
// which is a weird autoloader of sorts. By using class_exists
673+
// instead of AnnotationRegistry::registerFile(), we're able to hit
674+
// the file through normal Composer autoloading and avoid having to
675+
// worry about the relative path to the vendor/ directory.
676+
class_exists(DoctrineAnnotations::class);
677+
$reader = new SimpleAnnotationReader();
678+
$reader->addNamespace('Doctrine\ORM\Mapping');
679+
self::$defaultMappingDriver = new AnnotationDriver($reader);
680+
}
681+
return self::$defaultMappingDriver;
682+
}
634683
}

0 commit comments

Comments
 (0)