Skip to content

Commit e7e1bb9

Browse files
authored
Merge pull request #4 from w3c/inverse
Allow monitoring inverse side of relationships
2 parents db8bafd + f35e940 commit e7e1bb9

16 files changed

+2220
-121
lines changed

Annotation/Change.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,9 @@ class Change
2323
*/
2424
public $class = LifecyclePropertyChangedEvent::class;
2525

26+
/**
27+
* @var bool
28+
* @deprecated to be removed in next major version and the class will always act as if it was set to true
29+
*/
30+
public $monitor_owning = false;
2631
}

Annotation/Update.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,6 @@ class Update
2727
* @var bool
2828
*/
2929
public $monitor_collections = true;
30+
31+
public $monitor_owning = false;
3032
}

EventListener/LifecycleEventsListener.php

Lines changed: 279 additions & 58 deletions
Large diffs are not rendered by default.

EventListener/LifecyclePropertyEventsListener.php

Lines changed: 12 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@
22

33
namespace W3C\LifecycleEventsBundle\EventListener;
44

5-
use Doctrine\Common\Annotations\Reader;
65
use Doctrine\Common\Util\ClassUtils;
76
use Doctrine\ORM\Event\PreUpdateEventArgs;
8-
use Doctrine\ORM\Mapping\ClassMetadata;
97
use Doctrine\ORM\PersistentCollection;
108
use W3C\LifecycleEventsBundle\Annotation\Change;
9+
use W3C\LifecycleEventsBundle\Services\AnnotationGetter;
1110
use W3C\LifecycleEventsBundle\Services\LifecycleEventsDispatcher;
1211

1312
/**
@@ -25,20 +24,20 @@ class LifecyclePropertyEventsListener
2524
private $dispatcher;
2625

2726
/**
28-
* @var Reader
27+
* @var AnnotationGetter
2928
*/
30-
private $reader;
29+
private $annotationGetter;
3130

3231
/**
3332
* Constructs a new instance
3433
*
35-
* @param LifecycleEventsDispatcher $dispatcher the dispatcher to fed
36-
* @param Reader $reader
34+
* @param LifecycleEventsDispatcher $dispatcher the dispatcher to feed
35+
* @param AnnotationGetter $annotationGetter
3736
*/
38-
public function __construct(LifecycleEventsDispatcher $dispatcher, Reader $reader)
37+
public function __construct(LifecycleEventsDispatcher $dispatcher, AnnotationGetter $annotationGetter)
3938
{
40-
$this->dispatcher = $dispatcher;
41-
$this->reader = $reader;
39+
$this->dispatcher = $dispatcher;
40+
$this->annotationGetter = $annotationGetter;
4241
}
4342

4443
public function preUpdate(PreUpdateEventArgs $args)
@@ -57,7 +56,8 @@ private function addPropertyChanges(PreUpdateEventArgs $args)
5756
$classMetadata = $args->getEntityManager()->getClassMetadata($realClass);
5857

5958
foreach ($args->getEntityChangeSet() as $property => $change) {
60-
$annotation = $this->getChangeAnnotation($classMetadata, $property);
59+
/** @var Change $annotation */
60+
$annotation = $this->annotationGetter->getPropertyAnnotation($classMetadata, $property, Change::class);
6161

6262
if ($annotation) {
6363
$this->dispatcher->addPropertyChange(
@@ -89,7 +89,8 @@ private function addCollectionChanges(PreUpdateEventArgs $args)
8989
}
9090

9191
$property = $update->getMapping()['fieldName'];
92-
$annotation = $this->getChangeAnnotation($classMetadata, $property);
92+
/** @var Change $annotation */
93+
$annotation = $this->annotationGetter->getPropertyAnnotation($classMetadata, $property, Change::class);
9394

9495
// Make sure $u belongs to the entity we are working on
9596
if (!isset($annotation)) {
@@ -105,29 +106,4 @@ private function addCollectionChanges(PreUpdateEventArgs $args)
105106
);
106107
}
107108
}
108-
109-
/**
110-
* @param ClassMetadata $classMetadata
111-
* @param string $property
112-
*
113-
* @return Change
114-
* @throws \ReflectionException
115-
*/
116-
private function getChangeAnnotation(ClassMetadata $classMetadata, $property)
117-
{
118-
$reflProperty = $classMetadata->getReflectionProperty($property);
119-
120-
if ($reflProperty) {
121-
/** @var Change $annotation */
122-
$annotation = $this->reader->getPropertyAnnotation(
123-
$classMetadata->getReflectionProperty($property),
124-
Change::class
125-
);
126-
return $annotation;
127-
}
128-
129-
throw new \ReflectionException(
130-
$classMetadata->getName() . '.' . $property . ' not found. Could this be a private field of a parent class?'
131-
);
132-
}
133109
}

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ This Symfony bundle is meant to capture and dispatch events that happen througho
1313
Doctrine already provides such events, but using them directly has a few shortcomings:
1414
- You don't decide at which point in a action you want to dispatch events. Events are fired during a flush.
1515
- When Doctrine events are fired, you are not assured that the entities have actually been saved in the database.
16-
This is obvious for preUpdate (sent before persisting the changes), but postPersist and postRemove have the same issue:
16+
This is obvious for preUpdate (sent before persisting the changes), but postPersist and preRemove have the same issue:
1717
if you persist two new entities in a single transaction, the first insert could work (thus an event would be sent) but
1818
not the second, resulting in no entities being saved at all
1919

@@ -112,6 +112,7 @@ must have a constructor with the following signature:
112112
public function __construct($entity, array $propertiesChangeSet = null, array $collectionsChangeSet = null)
113113
```
114114
- `monitor_collections`: whether the annotation should monitor changes to collection fields. Defaults to true
115+
- `monitor_owning`: whether owning side relationship changes should be also monitored as inverse side changes. Defaults to false
115116

116117
#### `@On\Change`
117118

@@ -144,11 +145,16 @@ and for collections:
144145
*/
145146
public function __construct($entity, $property, $deletedElements = null, $insertedElements = null)
146147
```
148+
- `monitor_owning`: whether to record changes to this field when owning sides change (defaults to `false`). Using
149+
`@On\Change` on inverse side of relationships won't trigger any events unless this paramter is set to true. This
150+
parameter is likely to be removed in the next major version and act as if it was set to `true` since when the
151+
annotation is added to the inverse side of relationship, it is obvious it means that you want changes to owning side to
152+
be monitored here
147153

148154
#### `@On\IgnoreClassUpdates`
149155

150156
This annotation is a bit different. When placed on a field (property or collection), it prevents `@On\Update` from
151-
firing events related to this field. `@On\Change' ones will still work. This annotation does not allow any parameters.
157+
firing events related to this field. `@On\Change` ones will still work. This annotation does not allow any parameters.
152158

153159
#### Example class
154160

Resources/config/services.yml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,28 @@ parameters:
44
w3c_lifecycle_events.dispatcher.class: W3C\LifecycleEventsBundle\Services\LifecycleEventsDispatcher
55
w3c_lifecycle_events.post_flush_listener.class: W3C\LifecycleEventsBundle\EventListener\PostFlushListener
66
w3c_lifecycle_events.auto_dispatch: ''
7+
w3c_lifecycle_events.annotation_getter.class: W3C\LifecycleEventsBundle\Services\AnnotationGetter
78

89
services:
10+
w3c_lifecycle_events.annotation_getter:
11+
class: "%w3c_lifecycle_events.annotation_getter.class%"
12+
arguments: ["@annotation_reader"]
913
w3c_lifecycle_events.dispatcher:
1014
class: "%w3c_lifecycle_events.dispatcher.class%"
1115
arguments: ['@event_dispatcher', '%w3c_lifecycle_events.auto_dispatch%']
1216
w3c_lifecycle_events.listener:
1317
class: "%w3c_lifecycle_events.listener.class%"
1418
tags:
1519
- { name: doctrine.event_listener, event: postPersist }
16-
- { name: doctrine.event_listener, event: postRemove }
17-
- { name: doctrine.event_listener, event: postSoftDelete }
20+
- { name: doctrine.event_listener, event: preRemove }
21+
- { name: doctrine.event_listener, event: preSoftDelete }
1822
- { name: doctrine.event_listener, event: preUpdate }
19-
arguments: ['@w3c_lifecycle_events.dispatcher', '@annotation_reader']
23+
arguments: ['@w3c_lifecycle_events.dispatcher', '@w3c_lifecycle_events.annotation_getter']
2024
w3c_lifecycle_events.property_listener:
2125
class: "%w3c_lifecycle_events.property_listener.class%"
2226
tags:
2327
- { name: doctrine.event_listener, event: preUpdate }
24-
arguments: ['@w3c_lifecycle_events.dispatcher', '@annotation_reader']
28+
arguments: ['@w3c_lifecycle_events.dispatcher', '@w3c_lifecycle_events.annotation_getter']
2529
w3c_lifecycle_events.post_flush_listener:
2630
class: "%w3c_lifecycle_events.post_flush_listener.class%"
2731
tags:

Services/AnnotationGetter.php

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
namespace W3C\LifecycleEventsBundle\Services;
4+
5+
use Doctrine\Common\Annotations\Reader;
6+
use Doctrine\ORM\Mapping\ClassMetadata;
7+
8+
/**
9+
* Convenient class to get lifecycle annotations more easily
10+
*
11+
* @author Jean-Guilhem Rouel <jean-gui@w3.org>
12+
*/
13+
class AnnotationGetter
14+
{
15+
/**
16+
* @var Reader
17+
*/
18+
private $reader;
19+
20+
public function __construct(Reader $reader)
21+
{
22+
$this->reader = $reader;
23+
}
24+
25+
/**
26+
* Get a class-level annotation
27+
*
28+
* @param string $class Class to get annotation of
29+
* @param string $annotationClass Class of the annotation to get
30+
*
31+
* @return object|null object of same class as $annotationClass or null if no annotation is found
32+
*/
33+
public function getAnnotation($class, $annotationClass)
34+
{
35+
$annotation = $this->reader->getClassAnnotation(
36+
new \ReflectionClass($class),
37+
$annotationClass
38+
);
39+
return $annotation;
40+
}
41+
42+
/**
43+
* Get a field-level annotation
44+
*
45+
* @param ClassMetadata $classMetadata Metadata of the class to get annotation of
46+
* @param string $field Name of the field to get annotation of
47+
* @param string $annotationClass Class of the annotation to get
48+
*
49+
* @return object|null object of same class as $annotationClass or null if no annotation is found
50+
* @throws \ReflectionException if the field does not exist
51+
*/
52+
public function getPropertyAnnotation(ClassMetadata $classMetadata, $field, $annotationClass)
53+
{
54+
$reflProperty = $classMetadata->getReflectionProperty($field);
55+
56+
if ($reflProperty) {
57+
return $this->reader->getPropertyAnnotation($reflProperty, $annotationClass);
58+
}
59+
60+
throw new \ReflectionException(
61+
$classMetadata->getName() . '.' . $field . ' not found. Could this be a private field of a parent class?'
62+
);
63+
}
64+
}

Services/LifecycleEventsDispatcher.php

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,24 @@ public function getUpdates()
234234

235235
public function addUpdate(Update $annotation, $entity, array $propertyChangeSet = null, array $collectionChangeSet = null)
236236
{
237-
$this->updates[] = [$annotation, $entity, $propertyChangeSet, $collectionChangeSet];
237+
if (list($key, $update) = $this->getUpdate($entity)) {
238+
$update[2] = array_merge_recursive((array)$update[2], (array)$propertyChangeSet);
239+
$update[3] = array_merge_recursive((array)$update[3], (array)$collectionChangeSet);
240+
$this->updates[$key] = $update;
241+
} else {
242+
$this->updates[] = [$annotation, $entity, $propertyChangeSet, $collectionChangeSet];
243+
}
244+
}
245+
246+
public function getUpdate($entity)
247+
{
248+
foreach ($this->updates as $key => $update) {
249+
if ($update[1] === $entity) {
250+
return [$key, $update];
251+
}
252+
}
253+
254+
return null;
238255
}
239256

240257
public function getPropertyChanges()
@@ -252,9 +269,26 @@ public function getCollectionChanges()
252269
return $this->collectionChanges;
253270
}
254271

255-
public function addCollectionChange(Change $annotation, $entity, $property, $deletedElements = null, $insertedElements = null)
272+
public function addCollectionChange(Change $annotation, $entity, $property, $deletedElements = [], $insertedElements = [])
256273
{
257-
$this->collectionChanges[] = [$annotation, $entity, $property, $deletedElements, $insertedElements];
274+
if (list($key, $change) = $this->getCollectionChange($entity, $property)) {
275+
$change[3] = array_merge_recursive((array)$change[3], (array)$deletedElements);
276+
$change[4] = array_merge_recursive((array)$change[4], (array)$insertedElements);
277+
$this->collectionChanges[$key] = $change;
278+
} else {
279+
$this->collectionChanges[] = [$annotation, $entity, $property, $deletedElements, $insertedElements];
280+
}
281+
}
282+
283+
public function getCollectionChange($entity, $property)
284+
{
285+
foreach ($this->collectionChanges as $key => $update) {
286+
if ($update[1] === $entity && $update[2] === $property) {
287+
return [$key, $update];
288+
}
289+
}
290+
291+
return null;
258292
}
259293

260294
/**
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
namespace W3C\LifecycleEventsBundle\Tests\Annotation\Fixtures;
4+
5+
use W3C\LifecycleEventsBundle\Annotation as On;
6+
7+
/**
8+
* @On\Update(monitor_owning=true)
9+
*
10+
* @author Jean-Guilhem Rouel <jean-gui@w3.org>
11+
*/
12+
class Person
13+
{
14+
public $name;
15+
16+
/**
17+
* @ ORM\ManyToMany(targetEntity="Person", inversedBy="friendOf")
18+
*/
19+
public $friends;
20+
21+
/**
22+
* @ ORM\ManyToMany(targetEntity="Person", mappedBy="friends")
23+
* @On\Change(monitor_owning=true)
24+
*/
25+
public $friendOf;
26+
27+
/**
28+
* @ ORM\ManyToOne(targetEntity="Person", inversedBy="sons")
29+
*/
30+
public $father;
31+
32+
/**
33+
* @ ORM\OneToMany(targetEntity="Person", mappedBy="father")
34+
* @On\Change(monitor_owning=true)
35+
*/
36+
public $sons;
37+
38+
/**
39+
* @ ORM\OneToOne(targetEntity="Person", inversedBy="mentoring")
40+
*/
41+
public $mentor;
42+
43+
/**
44+
* @ ORM\OneToOne(targetEntity="Person", mappedBy="mentor")
45+
* @On\Change(monitor_owning=true)
46+
*/
47+
public $mentoring;
48+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
namespace W3C\LifecycleEventsBundle\Tests\Annotation\Fixtures;
4+
5+
use W3C\LifecycleEventsBundle\Annotation as On;
6+
7+
/**
8+
* @On\Update()
9+
*
10+
* @author Jean-Guilhem Rouel <jean-gui@w3.org>
11+
*/
12+
class PersonNoMonitor
13+
{
14+
public $name;
15+
16+
/**
17+
* @ ORM\ManyToMany(targetEntity="Person", inversedBy="friendOf")
18+
*/
19+
public $friends;
20+
21+
/**
22+
* @ ORM\ManyToMany(targetEntity="Person", mappedBy="friends")
23+
*/
24+
public $friendOf;
25+
26+
/**
27+
* @ ORM\ManyToOne(targetEntity="Person", inversedBy="sons")
28+
* @On\Change()
29+
*/
30+
public $father;
31+
32+
/**
33+
* @ ORM\OneToMany(targetEntity="Person", mappedBy="father")
34+
*/
35+
public $sons;
36+
37+
/**
38+
* @ ORM\OneToOne(targetEntity="Person", inversedBy="mentoring")
39+
*/
40+
public $mentor;
41+
42+
/**
43+
* @ ORM\OneToOne(targetEntity="Person", mappedBy="mentor")
44+
*/
45+
public $mentoring;
46+
}

0 commit comments

Comments
 (0)