Skip to content

Commit 6e2a0fe

Browse files
TheCelavilisachenko
authored andcommitted
Implement workaround for Doctrine entities with metadata listener #327
1 parent 5d96ceb commit 6e2a0fe

File tree

4 files changed

+253
-3
lines changed

4 files changed

+253
-3
lines changed

README.md

+22-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Features
2626
* Ability to change the return value of any methods/functions via `Around` type of advice.
2727
* Rich pointcut grammar syntax for defining pointcuts in the source code.
2828
* Native debugging for AOP with XDebug. The code with weaved aspects is fully readable and native. You can put a breakpoint in the original class or in the aspect and it will work (for debug mode)!
29-
* Can be integrated with any existing PHP frameworks and libraries.
29+
* Can be integrated with any existing PHP frameworks and libraries (with or without additional configuration).
3030
* Highly optimized for production use: support of opcode cachers, lazy loading of advices and aspects, joinpoints caching, no runtime checks of pointcuts, no runtime annotations parsing, no evals and `__call` methods, no slow proxies and `call_user_func_array()`. Fast bootstraping process (2-20ms) and advice invocation.
3131

3232

@@ -194,7 +194,27 @@ use Aspect\MonitorAspect;
194194

195195
Now you are ready to use the power of aspects! Feel free to change anything everywhere. If you like this project, you could support it <a href="https://flattr.com/submit/auto?fid=83r77w&url=https%3A%2F%2Fgithub.com%2Fgoaop%2Fframework" target="_blank"><img src="https://button.flattr.com/flattr-badge-large.png" alt="Flattr this" title="Flattr this" border="0"></a> [![Gratipay](https://img.shields.io/gratipay/lisachenko.svg)](https://gratipay.com/lisachenko/)
196196

197-
### 6. Contribution
197+
### 6. Optional configurations
198+
199+
#### 6.1 Support for weaving Doctrine entities
200+
201+
Weaving Doctrine entities can not be supported out of the box due to the fact
202+
that Go! AOP generates two sets of classes for each weaved entity, a concrete class and
203+
proxy with pointcuts. Doctrine will interpret both of those classes as concrete entities
204+
and assign for both of them same metadata, which would mess up the database and relations
205+
(see [https://github.com/goaop/framework/issues/327](https://github.com/goaop/framework/issues/327)).
206+
207+
Therefore, a workaround is provided with this library which will sort out
208+
mapping issue in Doctrine. Workaround is in form of event subscriber,
209+
`Go\Bridge\Doctrine\MetadataLoadInterceptor` which has to be registered
210+
when Doctrine is bootstraped in your project. For details how to do that,
211+
see [http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html](http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html).
212+
213+
Event subscriber will modify metadata entity definition for generated Go! Aop proxies
214+
as mapped superclass. That would sort out issues on which you may stumble upon when
215+
weaving Doctrine entities.
216+
217+
### 7. Contribution
198218

199219
To contribute changes see the [Contribute Readme](CONTRIBUTE.md)
200220

composer.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
"require-dev": {
1818
"symfony/console": "^2.7|^3.0",
1919
"adlawson/vfs": "^0.12",
20-
"phpunit/phpunit": "^4.8"
20+
"phpunit/phpunit": "^4.8",
21+
"doctrine/orm": "^2.5"
2122
},
2223

2324
"suggest": {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
<?php
2+
/*
3+
* Go! AOP framework
4+
*
5+
* @copyright Copyright 2012, Lisachenko Alexander <[email protected]>
6+
*
7+
* This source file is subject to the license that is bundled
8+
* with this source code in the file LICENSE.
9+
*/
10+
namespace Go\Bridge\Doctrine;
11+
12+
use Doctrine\Common\EventSubscriber;
13+
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
14+
use Doctrine\ORM\Events;
15+
use Doctrine\ORM\Mapping\ClassMetadata;
16+
use Go\Core\AspectContainer;
17+
18+
/**
19+
* Class MetadataLoadInterceptor
20+
*
21+
* Support for weaving Doctrine entities.
22+
*/
23+
final class MetadataLoadInterceptor implements EventSubscriber
24+
{
25+
/**
26+
* {@inheritdoc}
27+
*/
28+
public function getSubscribedEvents()
29+
{
30+
return [
31+
Events::loadClassMetadata
32+
];
33+
}
34+
35+
/**
36+
* Handles \Doctrine\ORM\Events::loadClassMetadata event by modifying metadata of Go! AOP proxied classes.
37+
*
38+
* This method intercepts loaded metadata of Doctrine's entities which are weaved by Go! AOP,
39+
* and denotes them as mapped superclass. If weaved entities uses mappings from traits
40+
* (such as Timestampable, Blameable, etc... from https://github.com/Atlantic18/DoctrineExtensions),
41+
* it will remove all mappings from proxied class for fields inherited from traits in order to prevent
42+
* collision with concrete subclass of weaved entity. Fields from trait will be present in concrete subclass
43+
* of weaved entitites.
44+
*
45+
* @see http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/inheritance-mapping.html#mapped-superclasses
46+
* @see https://github.com/Atlantic18/DoctrineExtensions
47+
*
48+
* @param LoadClassMetadataEventArgs $args
49+
*/
50+
public function loadClassMetadata(LoadClassMetadataEventArgs $args)
51+
{
52+
/**
53+
* @var ClassMetadata $metadata
54+
*/
55+
$metadata = $args->getClassMetadata();
56+
57+
if (1 === preg_match(sprintf('/.+(%s)$/', AspectContainer::AOP_PROXIED_SUFFIX), $metadata->name)) {
58+
$metadata->isMappedSuperclass = true;
59+
$metadata->isEmbeddedClass = false;
60+
$metadata->table = [];
61+
$metadata->customRepositoryClassName = null;
62+
63+
$this->removeMappingsFromTraits($metadata);
64+
}
65+
}
66+
67+
/**
68+
* Remove fields in Go! AOP proxied class metadata that are inherited
69+
* from traits.
70+
*
71+
* @param ClassMetadata $metadata
72+
*/
73+
private function removeMappingsFromTraits(ClassMetadata $metadata)
74+
{
75+
$traits = $this->getTraits($metadata->name);
76+
77+
foreach ($traits as $trait) {
78+
$trait = new \ReflectionClass($trait);
79+
80+
/**
81+
* @var \ReflectionProperty $property
82+
*/
83+
foreach ($trait->getProperties() as $property) {
84+
$name = $property->getName();
85+
86+
if (isset($metadata->fieldMappings[$name])) {
87+
$mapping = $metadata->fieldMappings[$name];
88+
89+
unset(
90+
$metadata->fieldMappings[$name],
91+
$metadata->fieldNames[$mapping['columnName']],
92+
$metadata->columnNames[$name]
93+
);
94+
}
95+
}
96+
}
97+
}
98+
99+
/**
100+
* Get ALL traits used by one class.
101+
*
102+
* This method is copied from https://github.com/RunOpenCode/traitor-bundle/blob/master/src/RunOpenCode/Bundle/Traitor/Utils/ClassUtils.php
103+
*
104+
* @param object|string $objectOrClass Instance of class or FQCN
105+
* @param bool $autoload Weather to autoload class.
106+
*
107+
* @throws \InvalidArgumentException
108+
* @throws \RuntimeException
109+
*
110+
* @return array Used traits.
111+
*/
112+
private function getTraits($objectOrClass, $autoload = true)
113+
{
114+
if (is_object($objectOrClass)) {
115+
$objectOrClass = get_class($objectOrClass);
116+
}
117+
118+
if (!is_string($objectOrClass)) {
119+
throw new \InvalidArgumentException(sprintf('Full qualified class name expected, got: "%s".', gettype($objectOrClass)));
120+
}
121+
122+
if (!class_exists($objectOrClass)) {
123+
throw new \RuntimeException(sprintf('Class "%s" does not exists or it can not be autoloaded.', $objectOrClass));
124+
}
125+
126+
$traits = [];
127+
// Get traits of all parent classes
128+
do {
129+
$traits = array_merge(class_uses($objectOrClass, $autoload), $traits);
130+
} while ($objectOrClass = get_parent_class($objectOrClass));
131+
132+
$traitsToSearch = $traits;
133+
134+
while (count($traitsToSearch) > 0) {
135+
$newTraits = class_uses(array_pop($traitsToSearch), $autoload);
136+
$traits = array_merge($newTraits, $traits);
137+
$traitsToSearch = array_merge($newTraits, $traitsToSearch);
138+
}
139+
140+
foreach ($traits as $trait => $same) {
141+
$traits = array_merge(class_uses($trait, $autoload), $traits);
142+
}
143+
144+
return array_unique(array_map(function ($fqcn) {
145+
return ltrim($fqcn, '\\');
146+
}, $traits));
147+
}
148+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
/*
3+
* Go! AOP framework
4+
*
5+
* @copyright Copyright 2014, Lisachenko Alexander <[email protected]>
6+
*
7+
* This source file is subject to the license that is bundled
8+
* with this source code in the file LICENSE.
9+
*/
10+
namespace Go\Aop\Bridge\Doctrine;
11+
12+
use Doctrine\ORM\EntityManager;
13+
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
14+
use Doctrine\ORM\Mapping\ClassMetadata;
15+
use Go\Bridge\Doctrine\MetadataLoadInterceptor;
16+
use Go\Core\AspectContainer;
17+
18+
class MetadataLoadInterceptorTest extends \PHPUnit_Framework_TestCase
19+
{
20+
public function testItWillNotModifyClassMetadataForNonProxiedClasses()
21+
{
22+
$metadatas = [
23+
new ClassMetadata('\\Some\\Class\\Name'),
24+
new ClassMetadata(sprintf('%s\\Some\\Class\\Name', AspectContainer::AOP_PROXIED_SUFFIX)),
25+
new ClassMetadata(AspectContainer::AOP_PROXIED_SUFFIX),
26+
];
27+
28+
$metadataInterceptor = new MetadataLoadInterceptor();
29+
$entityManager = $this->getMockBuilder(EntityManager::class)->disableOriginalConstructor()->getMock();
30+
31+
/**
32+
* @var ClassMetadata $metadata
33+
*/
34+
foreach ($metadatas as $metadata) {
35+
$metadata->isMappedSuperclass = false;
36+
$metadataInterceptor->loadClassMetadata(new LoadClassMetadataEventArgs($metadata, $entityManager));
37+
38+
$this->assertFalse($metadata->isMappedSuperclass);
39+
}
40+
}
41+
42+
public function testItWillModifyClassMetadataForNonProxiedClasses()
43+
{
44+
$metadata = new ClassMetadata(Entity__AopProxied::class);
45+
$metadataInterceptor = new MetadataLoadInterceptor();
46+
$entityManager = $this->getMockBuilder(EntityManager::class)->disableOriginalConstructor()->getMock();
47+
48+
$metadata->isMappedSuperclass = false;
49+
$metadata->isEmbeddedClass = true;
50+
$metadata->table = ['table_name'];
51+
$metadata->customRepositoryClassName = 'CustomRepositoryClass';
52+
53+
$metadata->fieldMappings['mappedField'] = [
54+
'columnName' => 'mapped_field',
55+
'fieldName' => 'mappedField'
56+
];
57+
$metadata->fieldNames['mapped_field'] = 'mappedField';
58+
$metadata->columnNames['mappedField'] = 'mapped_field';
59+
60+
$metadataInterceptor->loadClassMetadata(new LoadClassMetadataEventArgs($metadata, $entityManager));
61+
62+
$this->assertTrue($metadata->isMappedSuperclass);
63+
$this->assertFalse($metadata->isEmbeddedClass);
64+
$this->assertEquals(0, count($metadata->table));
65+
$this->assertNull($metadata->customRepositoryClassName);
66+
67+
$this->assertFalse(isset($metadata->fieldMappings['mappedField']));
68+
$this->assertFalse(isset($metadata->fieldNames['mapped_field']));
69+
$this->assertFalse(isset($metadata->columnNames['mappedField']));
70+
}
71+
}
72+
73+
trait SimpleTrait
74+
{
75+
private $mappedField;
76+
}
77+
78+
class Entity__AopProxied
79+
{
80+
use SimpleTrait;
81+
}

0 commit comments

Comments
 (0)