├── .github └── workflows │ └── symfony.yml ├── .gitignore ├── .gitlab-ci.yml ├── Attribute ├── Change.php ├── Create.php ├── Delete.php ├── IgnoreClassUpdates.php └── Update.php ├── DependencyInjection ├── Configuration.php └── W3CLifecycleEventsExtension.php ├── Event ├── Definitions │ └── LifecycleEvents.php ├── LifecycleCollectionChangedEvent.php ├── LifecycleDeletionEvent.php ├── LifecycleEvent.php ├── LifecyclePropertyChangedEvent.php ├── LifecycleUpdateEvent.php └── PreAutoDispatchEvent.php ├── EventListener ├── LifecycleEventsListener.php ├── LifecyclePropertyEventsListener.php └── PostFlushListener.php ├── LICENSE.md ├── README.md ├── Resources └── config │ └── services.yml ├── Services ├── AttributeGetter.php └── LifecycleEventsDispatcher.php ├── Tests ├── Attribute │ ├── ChangeTest.php │ ├── CreateTest.php │ ├── DeleteTest.php │ ├── Fixtures │ │ ├── Person.php │ │ ├── PersonNoMonitor.php │ │ ├── User.php │ │ ├── UserClass.php │ │ ├── UserErrorChange.php │ │ ├── UserErrorCreate.php │ │ ├── UserErrorDelete.php │ │ ├── UserErrorIgnore.php │ │ ├── UserErrorUpdate.php │ │ ├── UserEvent.php │ │ └── UserEvent2.php │ ├── IgnoreTest.php │ └── UpdateTest.php ├── Event │ ├── LifecycleCollectionChangedEventTest.php │ ├── LifecycleDeletionEventTest.php │ ├── LifecycleEventTest.php │ ├── LifecyclePropertyChangedEventTest.php │ ├── LifecycleUpdateEventTest.php │ └── PreAutoDispatchEventTest.php ├── EventListener │ ├── Fixtures │ │ ├── OtherEntity.php │ │ ├── UserChange.php │ │ ├── UserClassUpdateCollection.php │ │ ├── UserClassUpdateIgnoreCollection.php │ │ ├── UserClassUpdateIgnoreNoCollection.php │ │ ├── UserClassUpdateNoCollection.php │ │ └── UserNoAnnotation.php │ ├── LifecycleEventsListenerInverseNoMonitorTest.php │ ├── LifecycleEventsListenerInverseTest.php │ ├── LifecycleEventsListenerTest.php │ ├── LifecyclePropertyEventsListenerTest.php │ └── PostFlushListenerTest.php └── Services │ ├── AttributeGetterTest.php │ ├── Events │ ├── MyCollectionChangedEvent.php │ ├── MyLifecycleEvent.php │ ├── MyPropertyChangedEvent.php │ └── MyUpdatedEvent.php │ ├── Fixtures │ └── MySubscriber.php │ └── LifecycleEventsDispatcherTest.php ├── W3CLifecycleEventsBundle.php ├── composer.json ├── phpunit.xml └── w3c.json /.github/workflows/symfony.yml: -------------------------------------------------------------------------------- 1 | name: W3CLifecycleEventsBundle tests 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | php-version: [8.2] 16 | deps: ["low","default"] 17 | env: 18 | XDEBUG_MODE: coverage 19 | SYMFONY_DEPRECATIONS_HELPER: weak 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: Use PHP version ${{ matrix.php-version }} 23 | uses: shivammathur/setup-php@v2 24 | with: 25 | php-version: ${{ matrix.php-version }} 26 | coverage: xdebug 27 | - run: | 28 | if [[ "${{ matrix.deps }}" = "low" ]]; then 29 | composer update --prefer-dist --prefer-lowest 30 | else 31 | composer install --prefer-dist 32 | fi 33 | - run: mkdir -p build/logs 34 | - run: vendor/bin/phpunit --coverage-clover build/logs/clover.xml 35 | - name: Upload coverage results to Coveralls 36 | env: 37 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | run: | 39 | composer global require php-coveralls/php-coveralls 40 | php-coveralls --coverage_clover=build/logs/clover.xml -v 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/* 2 | composer.lock 3 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | before_script: 2 | - composer update 3 | 4 | stages: 5 | - test 6 | 7 | phpunit: 8 | stage: test 9 | script: 10 | - php -d zend_extension=xdebug.so ./vendor/bin/phpunit -c phpunit.xml --coverage-text --colors=never 11 | -------------------------------------------------------------------------------- /Attribute/Change.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | #[\Attribute(\Attribute::TARGET_PROPERTY)] 12 | class Change 13 | { 14 | public function __construct( 15 | public string $event = LifecycleEvents::PROPERTY_CHANGED, 16 | public string $class = LifecyclePropertyChangedEvent::class, 17 | /** 18 | * @deprecated to be removed in next major version and the class will always act as if it was set to true 19 | */ 20 | public bool $monitor_owning = false 21 | ){} 22 | } 23 | -------------------------------------------------------------------------------- /Attribute/Create.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | #[\Attribute(\Attribute::TARGET_CLASS)] 12 | class Create 13 | { 14 | public function __construct( 15 | public string $event = LifecycleEvents::CREATED, 16 | public string $class = LifecycleEvent::class, 17 | ){} 18 | } 19 | -------------------------------------------------------------------------------- /Attribute/Delete.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | #[\Attribute(\Attribute::TARGET_CLASS)] 12 | class Delete 13 | { 14 | public function __construct( 15 | public string $event = LifecycleEvents::DELETED, 16 | public string $class = LifecycleDeletionEvent::class, 17 | ){} 18 | } 19 | -------------------------------------------------------------------------------- /Attribute/IgnoreClassUpdates.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | #[\Attribute(\Attribute::TARGET_PROPERTY)] 9 | class IgnoreClassUpdates 10 | { 11 | } 12 | -------------------------------------------------------------------------------- /Attribute/Update.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | #[\Attribute(\Attribute::TARGET_CLASS)] 12 | class Update 13 | { 14 | public function __construct( 15 | public string $event = LifecycleEvents::UPDATED, 16 | public string $class = LifecycleUpdateEvent::class, 17 | public bool $monitor_collections = true, 18 | public bool $monitor_owning = false, 19 | ){} 20 | } 21 | -------------------------------------------------------------------------------- /DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | getRootNode(); 22 | 23 | $rootNode 24 | ->children() 25 | ->booleanNode('auto_dispatch') 26 | ->defaultValue(true) 27 | ->info('Automatically dispatch all lifecycle events.') 28 | ->end() 29 | ->end(); 30 | 31 | return $treeBuilder; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /DependencyInjection/W3CLifecycleEventsExtension.php: -------------------------------------------------------------------------------- 1 | processConfiguration($configuration, $configs); 24 | 25 | $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); 26 | $loader->load('services.yml'); 27 | 28 | $container->setParameter('w3c_lifecycle_events.auto_dispatch', $config['auto_dispatch']); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Event/Definitions/LifecycleEvents.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | final class LifecycleEvents { 9 | /** 10 | * Thrown each time an entity is created 11 | * 12 | * The event listener receives an 13 | * W3C\LifecycleEventsBundle\Event\LifecycleEvent instance. 14 | * 15 | * @var string 16 | */ 17 | const CREATED = 'w3c.lifecycle.created'; 18 | 19 | /** 20 | * Thrown each time a entity is updated 21 | * 22 | * The event listener receives an 23 | * W3C\LifecycleEventsBundle\Event\LifecycleUpdateEvent instance. 24 | * 25 | * @var string 26 | */ 27 | const UPDATED = 'w3c.lifecycle.updated'; 28 | 29 | /** 30 | * Thrown each time a property of an entity is changed 31 | * 32 | * The event listener receives an 33 | * W3C/LifecycleBundle\Event\LifecyclePropertyChangedEvent instance. 34 | * 35 | * @var string 36 | */ 37 | const PROPERTY_CHANGED = 'w3c.lifecycle.property_changed'; 38 | 39 | /** 40 | * Thrown each time a collection of an entity is changed 41 | * 42 | * The event listener receives an 43 | * W3C/LifecycleBundle\Event\LifecycleCollectionChangedEvent instance. 44 | * 45 | * @var string 46 | */ 47 | const COLLECTION_CHANGED = 'w3c.lifecycle.collection_changed'; 48 | 49 | /** 50 | * Thrown each time an entity is deleted 51 | * 52 | * The event listener receives an 53 | * W3C\LifecycleEventsBundle\Event\LifecycleEvent. 54 | * 55 | * @var string 56 | */ 57 | const DELETED = 'w3c.lifecycle.deleted'; 58 | } 59 | -------------------------------------------------------------------------------- /Event/LifecycleCollectionChangedEvent.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class LifecycleCollectionChangedEvent extends LifecycleEvent 11 | { 12 | private string $property; 13 | private ?array $deletedElements; 14 | private ?array $insertedElements; 15 | 16 | /** 17 | * Constructor. 18 | * 19 | * @param object $entity 20 | * @param string $property 21 | * @param array|null $deletedElements 22 | * @param array|null $insertedElements 23 | */ 24 | public function __construct(object $entity, string $property, array $deletedElements = null, array $insertedElements = null) 25 | { 26 | parent::__construct($entity); 27 | 28 | $this->property = $property; 29 | $this->deletedElements = $deletedElements; 30 | $this->insertedElements = $insertedElements; 31 | } 32 | 33 | public function getProperty(): string 34 | { 35 | return $this->property; 36 | } 37 | 38 | public function getDeletedElements(): ?array 39 | { 40 | return $this->deletedElements; 41 | } 42 | 43 | public function getInsertedElements(): ?array 44 | { 45 | return $this->insertedElements; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Event/LifecycleDeletionEvent.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class LifecycleDeletionEvent extends LifecycleEvent 12 | { 13 | protected array $identifier; 14 | 15 | public function __construct(object $entity, array $identifier = null) 16 | { 17 | parent::__construct($entity); 18 | $this->identifier = $identifier; 19 | } 20 | 21 | public function getIdentifier(): array 22 | { 23 | return $this->identifier; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Event/LifecycleEvent.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class LifecycleEvent extends Event 13 | { 14 | protected object $entity; 15 | 16 | public function __construct(object $entity) 17 | { 18 | $this->entity = $entity; 19 | } 20 | 21 | public function getEntity(): object 22 | { 23 | return $this->entity; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Event/LifecyclePropertyChangedEvent.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class LifecyclePropertyChangedEvent extends LifecycleEvent 11 | { 12 | /** 13 | * @var string 14 | */ 15 | private string $property; 16 | 17 | /** 18 | * @var mixed 19 | */ 20 | private $oldValue; 21 | 22 | /** 23 | * @var mixed 24 | */ 25 | private $newValue; 26 | 27 | /** 28 | * Constructor. 29 | * 30 | * @param object $entity 31 | * @param string $property 32 | * @param mixed $oldValue 33 | * @param mixed $newValue 34 | */ 35 | public function __construct(object $entity, string $property, $oldValue = null, $newValue = null) 36 | { 37 | parent::__construct($entity); 38 | 39 | $this->property = $property; 40 | $this->oldValue = $oldValue; 41 | $this->newValue = $newValue; 42 | } 43 | 44 | /** 45 | * @return string 46 | */ 47 | public function getProperty(): string 48 | { 49 | return $this->property; 50 | } 51 | 52 | /** 53 | * @return mixed 54 | */ 55 | public function getOldValue() 56 | { 57 | return $this->oldValue; 58 | } 59 | 60 | /** 61 | * @return mixed 62 | */ 63 | public function getNewValue() 64 | { 65 | return $this->newValue; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Event/LifecycleUpdateEvent.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class LifecycleUpdateEvent extends LifecycleEvent 13 | { 14 | private array $propertiesChangeSet; 15 | private array $collectionsChangeSet; 16 | 17 | /** 18 | * @param object $entity the entity being modified 19 | * @param array $propertiesChangeSet list of changes to properties 20 | * @param array $collectionsChangeSet list of changes to collections 21 | */ 22 | public function __construct(object $entity, array $propertiesChangeSet = [], array $collectionsChangeSet = []) 23 | { 24 | parent::__construct($entity); 25 | 26 | $this->propertiesChangeSet = $propertiesChangeSet; 27 | $this->collectionsChangeSet = $collectionsChangeSet; 28 | } 29 | 30 | /** 31 | * Return the list of properties that have changed 32 | * 33 | * @return array 34 | */ 35 | public function getChangedProperties(): array 36 | { 37 | return array_keys($this->propertiesChangeSet); 38 | } 39 | 40 | /** 41 | * Return whether some properties have changed 42 | * 43 | * @return bool 44 | */ 45 | public function havePropertiesChanged(): bool 46 | { 47 | return $this->propertiesChangeSet && count($this->propertiesChangeSet) > 0; 48 | } 49 | 50 | /** 51 | * Return the list of collections that have changed 52 | * 53 | * @return array 54 | */ 55 | public function getChangedCollections(): array 56 | { 57 | return array_keys($this->collectionsChangeSet); 58 | } 59 | 60 | /** 61 | * Return whether some collections have changed 62 | * 63 | * @return bool 64 | */ 65 | public function haveCollectionsChanged(): bool 66 | { 67 | return $this->collectionsChangeSet && count($this->collectionsChangeSet) > 0; 68 | } 69 | 70 | /** 71 | * Check if field has a changeset. 72 | * 73 | * @param string $field 74 | * 75 | * @return boolean 76 | */ 77 | public function hasChangedField(string $field): bool 78 | { 79 | return isset($this->propertiesChangeSet[$field]) || isset($this->collectionsChangeSet[$field]); 80 | } 81 | 82 | /** 83 | * Get the old value of the changeset of the changed field. 84 | * 85 | * @param string $field 86 | * 87 | * @return mixed 88 | */ 89 | public function getOldValue(string $field) 90 | { 91 | $this->assertValidProperty($field); 92 | 93 | return $this->propertiesChangeSet[$field]['old']; 94 | } 95 | 96 | /** 97 | * Get the new value of the changeset of the changed field. 98 | * 99 | * @param string $field 100 | * 101 | * @return mixed 102 | */ 103 | public function getNewValue(string $field) 104 | { 105 | $this->assertValidProperty($field); 106 | 107 | return $this->propertiesChangeSet[$field]['new']; 108 | } 109 | 110 | /** 111 | * Get the list of elements deleted from the collection $field 112 | * 113 | * @param string $field 114 | * 115 | * @return array 116 | */ 117 | public function getDeletedElements(string $field): array 118 | { 119 | $this->assertValidCollection($field); 120 | 121 | return $this->collectionsChangeSet[$field]['deleted']; 122 | } 123 | 124 | /** 125 | * Get the list of elements inserted to the collection $field 126 | * 127 | * @param string $field 128 | * 129 | * @return array 130 | */ 131 | public function getInsertedElements(string $field): array 132 | { 133 | $this->assertValidCollection($field); 134 | 135 | return $this->collectionsChangeSet[$field]['inserted']; 136 | } 137 | 138 | /** 139 | * Assert if the field exists in changeset. 140 | * 141 | * @param string $field 142 | * 143 | * @return void 144 | * 145 | * @throws InvalidArgumentException 146 | */ 147 | private function assertValidProperty(string $field) 148 | { 149 | if (!isset($this->propertiesChangeSet[$field])) { 150 | throw new InvalidArgumentException(sprintf( 151 | 'Field "%s" is not a valid field of the entity "%s" org has not changed.', 152 | $field, 153 | get_class($this->getEntity()) 154 | )); 155 | } 156 | } 157 | 158 | /** 159 | * Assert if the field exists in changeset. 160 | * 161 | * @param string $field 162 | * 163 | * @return void 164 | * 165 | * @throws InvalidArgumentException 166 | */ 167 | private function assertValidCollection(string $field) 168 | { 169 | if (!isset($this->collectionsChangeSet[$field])) { 170 | throw new InvalidArgumentException(sprintf( 171 | 'Field "%s" is not a valid collection of the entity "%s" org has not changed.', 172 | $field, 173 | get_class($this->getEntity()) 174 | )); 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /Event/PreAutoDispatchEvent.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class PreAutoDispatchEvent extends Event 14 | { 15 | protected LifecycleEventsDispatcher $dispatcher; 16 | 17 | public function __construct(LifecycleEventsDispatcher $dispatcher) 18 | { 19 | $this->dispatcher = $dispatcher; 20 | } 21 | 22 | public function getDispatcher(): LifecycleEventsDispatcher 23 | { 24 | return $this->dispatcher; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /EventListener/LifecycleEventsListener.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | class LifecycleEventsListener 26 | { 27 | private LifecycleEventsDispatcher $dispatcher; 28 | private AttributeGetter $attributeGetter; 29 | 30 | /** 31 | * Constructs a new instance 32 | * 33 | * @param LifecycleEventsDispatcher $dispatcher the dispatcher to feed 34 | * @param AttributeGetter $attributeGetter 35 | */ 36 | public function __construct(LifecycleEventsDispatcher $dispatcher, AttributeGetter $attributeGetter) 37 | { 38 | $this->dispatcher = $dispatcher; 39 | $this->attributeGetter = $attributeGetter; 40 | } 41 | 42 | /** 43 | * Called upon receiving postPersist events 44 | * 45 | * @param PostPersistEventArgs $args event to feed the dispatcher with 46 | * 47 | * @throws MappingException 48 | * @throws ReflectionException 49 | */ 50 | public function postPersist(PostPersistEventArgs $args): void 51 | { 52 | $entity = $args->getObject(); 53 | 54 | $classMetadata = $args->getObjectManager()->getClassMetadata($entity::class); 55 | $class = $classMetadata->getName(); 56 | 57 | /** @var Create $attribute */ 58 | $attribute = $this->attributeGetter->getAttribute($class, Create::class); 59 | if ($attribute) { 60 | $this->dispatcher->addCreation($attribute, $args); 61 | } 62 | 63 | foreach ($classMetadata->getAssociationMappings() as $property => $associationMapping) { 64 | if (!$classMetadata->isAssociationInverseSide($property)) { 65 | if ($classMetadata->isSingleValuedAssociation($property)) { 66 | $inverse = $classMetadata->reflFields[$property]->getValue($entity); 67 | $change = ['old' => null, 'new' => $inverse]; 68 | $this->propertyUpdateInverse($args, $class, $property, $change, $entity); 69 | } elseif ($classMetadata->isCollectionValuedAssociation($property)) { 70 | $inverse = $classMetadata->reflFields[$property]->getValue($entity); 71 | if ($inverse) { 72 | $change = ['deleted' => [], 'inserted' => $inverse->toArray()]; 73 | $this->collectionUpdateInverse($args, $class, $property, $change, $entity); 74 | } 75 | } 76 | } 77 | } 78 | } 79 | 80 | /** 81 | * @param LifecycleEventArgs $args 82 | * 83 | * @return void 84 | * @throws MappingException 85 | * @throws ReflectionException 86 | */ 87 | public function preSoftDelete(LifecycleEventArgs $args): void 88 | { 89 | $this->preRemove($args); 90 | } 91 | 92 | /** 93 | * Called upon receiving preRemove events. Better than postRemove as we still have information about associated 94 | * objects 95 | * 96 | * @param LifecycleEventArgs $args event to feed the dispatcher with 97 | * 98 | * @throws MappingException 99 | * @throws ReflectionException 100 | */ 101 | public function preRemove(LifecycleEventArgs $args): void 102 | { 103 | $entity = $args->getObject(); 104 | $classMetadata = $args->getObjectManager()->getClassMetadata($entity::class); 105 | $class = $classMetadata->getName(); 106 | 107 | /** @var Delete $attribute */ 108 | $attribute = $this->attributeGetter->getAttribute($class, Delete::class); 109 | if ($attribute) { 110 | $this->dispatcher->addDeletion($attribute, $args); 111 | } 112 | 113 | foreach ($classMetadata->getAssociationMappings() as $property => $associationMapping) { 114 | if (!$classMetadata->isAssociationInverseSide($property)) { 115 | if ($classMetadata->isSingleValuedAssociation($property)) { 116 | $inverse = $classMetadata->reflFields[$property]->getValue($entity); 117 | $change = ['old' => $inverse, 'new' => null]; 118 | $this->propertyUpdateInverse($args, $class, $property, $change, $entity); 119 | } elseif ($classMetadata->isCollectionValuedAssociation($property)) { 120 | $inverse = $classMetadata->reflFields[$property]->getValue($entity); 121 | if ($inverse) { 122 | $change = ['deleted' => $inverse->toArray(), 'inserted' => []]; 123 | $this->collectionUpdateInverse($args, $class, $property, $change, $entity); 124 | } 125 | } 126 | } 127 | } 128 | } 129 | 130 | /** 131 | * Called upon receiving preUpdate events 132 | * 133 | * @param PreUpdateEventArgs $args event to feed the dispatcher with 134 | * 135 | * @throws MappingException 136 | * @throws ReflectionException 137 | */ 138 | public function preUpdate(PreUpdateEventArgs $args): void 139 | { 140 | $entity = $args->getObject(); 141 | $classMetadata = $args->getObjectManager()->getClassMetadata($entity::class); 142 | $class = $classMetadata->getName(); 143 | 144 | /** @var Update $attribute */ 145 | $attribute = $this->attributeGetter->getAttribute($class, Update::class); 146 | 147 | // Build properties and collections changes, also take care of inverse side 148 | $changeSet = $this->buildChangeSet($args, $entity); 149 | 150 | $collectionChanges = $attribute && $attribute->monitor_collections ? $this->buildCollectionChanges($args, $entity) : []; 151 | 152 | if ($attribute) { 153 | // Add changes to the entity 154 | $this->dispatcher->addUpdate( 155 | $attribute, 156 | $entity, 157 | $changeSet, 158 | $collectionChanges 159 | ); 160 | } 161 | } 162 | 163 | /** 164 | * Return an array of collection changes belonging to $entity ignoring those marked with @IgnoreclassUpdates 165 | * 166 | * @param PreUpdateEventArgs $args 167 | * @param object $entity 168 | * 169 | * @return array 170 | * @throws MappingException 171 | * @throws ReflectionException 172 | */ 173 | private function buildCollectionChanges(PreUpdateEventArgs $args, object $entity): array 174 | { 175 | $classMetadata = $args->getObjectManager()->getClassMetadata($entity::class); 176 | $realClass = $classMetadata->getName(); 177 | $collectionsChanges = []; 178 | 179 | /** @var PersistentCollection $u */ 180 | foreach ($args->getObjectManager()->getUnitOfWork()->getScheduledCollectionUpdates() as $u) { 181 | $property = $u->getMapping()['fieldName']; 182 | 183 | // Make sure $u and the field belong to the entity we are working on 184 | if ($u->getOwner() !== $entity) { 185 | continue; 186 | } 187 | 188 | $ignoreAnnotation = $this->attributeGetter->getPropertyAttribute( 189 | $classMetadata, 190 | $property, 191 | IgnoreClassUpdates::class 192 | ); 193 | $change = [ 194 | 'deleted' => $u->getDeleteDiff(), 195 | 'inserted' => $u->getInsertDiff() 196 | ]; 197 | if (!$ignoreAnnotation) { 198 | $collectionsChanges[$property] = $change; 199 | } 200 | 201 | $this->collectionUpdateInverse($args, $realClass, $property, $change, $entity); 202 | } 203 | return $collectionsChanges; 204 | } 205 | 206 | /** 207 | * Return an array of changes to properties (not including collections) ignoring those marked with @IgnoreclassUpdates 208 | * 209 | * @param PreUpdateEventArgs $args 210 | * @param mixed $entity 211 | * 212 | * @return array 213 | * @throws MappingException 214 | * @throws ReflectionException 215 | */ 216 | private function buildChangeSet(PreUpdateEventArgs $args, $entity): array 217 | { 218 | $classMetadata = $args->getObjectManager()->getClassMetadata($entity::class); 219 | $realClass = $classMetadata->getName(); 220 | 221 | $changes = []; 222 | foreach (array_keys($args->getEntityChangeSet()) as $property) { 223 | $ignoreAnnotation = $this->attributeGetter->getPropertyAttribute( 224 | $classMetadata, 225 | $property, 226 | IgnoreClassUpdates::class 227 | ); 228 | $change = ['old' => $args->getOldValue($property), 'new' => $args->getNewValue($property)]; 229 | if (!$ignoreAnnotation) { 230 | $changes[$property] = $change; 231 | } 232 | 233 | if ($classMetadata->hasAssociation($property)) { 234 | $this->propertyUpdateInverse($args, $realClass, $property, $change, $entity); 235 | } 236 | } 237 | return $changes; 238 | } 239 | 240 | /** 241 | * @param LifecycleEventArgs $args 242 | * @param string $class 243 | * @param string $property 244 | * @param array $change 245 | * @param object $entity 246 | * 247 | * @throws MappingException 248 | * @throws ReflectionException 249 | */ 250 | private function collectionUpdateInverse( 251 | LifecycleEventArgs $args, 252 | string $class, 253 | string $property, 254 | array $change, 255 | object $entity 256 | ): void { 257 | $em = $args->getObjectManager(); 258 | $classMetadata = $em->getClassMetadata($class); 259 | 260 | // it is indeed an association with a potential inverse side 261 | if ($classMetadata->hasAssociation($property)) { 262 | $mapping = $classMetadata->getAssociationMapping($property); 263 | 264 | foreach ($change['deleted'] as $deletion) { 265 | $this->updateDeletedInverse($deletion, $entity, $args, $mapping); 266 | } 267 | 268 | foreach ($change['inserted'] as $insertion) { 269 | $this->updateInsertedInverse($insertion, $entity, $args, $mapping); 270 | } 271 | } 272 | } 273 | 274 | /** 275 | * @param LifecycleEventArgs $args 276 | * @param string $class 277 | * @param string $property 278 | * @param array $change 279 | * @param object $entity 280 | * 281 | * @throws MappingException 282 | * @throws ReflectionException 283 | */ 284 | private function propertyUpdateInverse( 285 | LifecycleEventArgs $args, 286 | string $class, 287 | string $property, 288 | array $change, 289 | object $entity 290 | ): void 291 | { 292 | $em = $args->getObjectManager(); 293 | $classMetadata = $em->getClassMetadata($class); 294 | 295 | $mapping = $classMetadata->getAssociationMapping($property); 296 | 297 | if (isset($change['new']) && $change['new']) { 298 | $newInverseMetadata = $em->getClassMetadata($change['new']::class); 299 | } else { 300 | $newInverseMetadata = $em->getClassMetadata($mapping['targetEntity']); 301 | } 302 | 303 | if (isset($change['old']) && $change['old']) { 304 | $oldInverseMetadata = $em->getClassMetadata($change['old']::class); 305 | } else { 306 | $oldInverseMetadata = $em->getClassMetadata($mapping['targetEntity']); 307 | } 308 | 309 | // Inverse side should always be similar for old and new entities, but in case that's not the case (because of 310 | // some weird inheritance, we consider old and new metadata 311 | if (!isset($mapping['inversedBy'])) { 312 | return; 313 | } 314 | 315 | // Old Inverse side is also single-valued (one-to-one) 316 | if ($oldInverseMetadata->isSingleValuedAssociation($mapping['inversedBy'])) { 317 | $this->updateOldInverse($change['old'], $entity, $args,$mapping); 318 | } // Old Inverse side is multi-valued (one-to-many) 319 | elseif ($oldInverseMetadata->isCollectionValuedAssociation($mapping['inversedBy'])) { 320 | $this->updateDeletedInverse($change['old'], $entity, $args,$mapping); 321 | } 322 | 323 | // New Inverse side is also single-valued (one-to-one) 324 | if ($newInverseMetadata->isSingleValuedAssociation($mapping['inversedBy'])) { 325 | $this->updateNewInverse($change['new'], $entity, $args, $mapping); 326 | } // New Inverse side is multi-valued (one-to-many) 327 | elseif ($newInverseMetadata->isCollectionValuedAssociation($mapping['inversedBy'])) { 328 | $this->updateInsertedInverse($change['new'], $entity, $args, $mapping); 329 | } 330 | } 331 | 332 | /** 333 | * @param object|null $oldEntity 334 | * @param object $owningEntity 335 | * @param LifecycleEventArgs $args 336 | * @param array $mapping 337 | * 338 | * @throws ReflectionException 339 | */ 340 | private function updateOldInverse( 341 | ?object $oldEntity, 342 | object $owningEntity, 343 | LifecycleEventArgs $args, 344 | AssociationMapping $mapping 345 | ): void { 346 | $inverseField = $mapping['inversedBy'] ?? null; 347 | if ($inverseField && $oldEntity) { 348 | $em = $args->getObjectManager(); 349 | 350 | $inverseMetadata = $em->getClassMetadata($oldEntity::class); 351 | $oldClass = $inverseMetadata->getName(); 352 | 353 | /** @var Update $targetAnnotation */ 354 | $targetAnnotation = $this->attributeGetter->getAttribute($oldClass, Update::class); 355 | /** @var Change $targetChangeAnnotation */ 356 | $targetChangeAnnotation = $this->attributeGetter->getPropertyAttribute($inverseMetadata, 357 | $inverseField, Change::class); 358 | 359 | $inverseMonitoredGlobal = $targetAnnotation && $targetAnnotation->monitor_owning 360 | && !$this->attributeGetter->getPropertyAttribute($inverseMetadata, 361 | $inverseField, IgnoreClassUpdates::class); 362 | 363 | $inverseMonitoredField = $targetChangeAnnotation && $targetChangeAnnotation->monitor_owning; 364 | 365 | $em->initializeObject($oldEntity); 366 | 367 | if ($inverseMonitoredGlobal) { 368 | $this->dispatcher->addUpdate( 369 | $targetAnnotation, 370 | $oldEntity, 371 | [$inverseField => ['old' => $owningEntity, 'new' => null]], 372 | [] 373 | ); 374 | } 375 | 376 | if ($inverseMonitoredField) { 377 | $this->dispatcher->addPropertyChange( 378 | $targetChangeAnnotation, 379 | $oldEntity, 380 | $mapping['inversedBy'], 381 | $owningEntity 382 | ); 383 | } 384 | } 385 | } 386 | 387 | /** 388 | * @param object|null $newEntity 389 | * @param object $owningEntity 390 | * @param LifecycleEventArgs $args 391 | * @param array $mapping 392 | * 393 | * @return void 394 | * @throws ReflectionException 395 | */ 396 | private function updateNewInverse( 397 | ?object $newEntity, 398 | object $owningEntity, 399 | LifecycleEventArgs $args, 400 | AssociationMapping $mapping, 401 | ): void { 402 | $inverseField = $mapping['inversedBy'] ?? null; 403 | if ($inverseField && $newEntity) { 404 | $em = $args->getObjectManager(); 405 | 406 | $inverseMetadata = $em->getClassMetadata($newEntity::class); 407 | $newClass = $inverseMetadata->getName(); 408 | 409 | /** @var Update $targetAnnotation */ 410 | $targetAnnotation = $this->attributeGetter->getAttribute($newClass, Update::class); 411 | /** @var Change $targetChangeAnnotation */ 412 | $targetChangeAnnotation = $this->attributeGetter->getPropertyAttribute($inverseMetadata, 413 | $inverseField, Change::class); 414 | 415 | $inverseMonitoredGlobal = $targetAnnotation && $targetAnnotation->monitor_owning 416 | && !$this->attributeGetter->getPropertyAttribute($inverseMetadata, 417 | $inverseField, IgnoreClassUpdates::class); 418 | 419 | $inverseMonitoredField = $targetChangeAnnotation && $targetChangeAnnotation->monitor_owning; 420 | 421 | $em->initializeObject($newEntity); 422 | 423 | if ($inverseMonitoredGlobal) { 424 | $this->dispatcher->addUpdate( 425 | $targetAnnotation, 426 | $newEntity, 427 | [$inverseField => ['old' => null, 'new' => $owningEntity]], 428 | [] 429 | ); 430 | } 431 | 432 | if ($inverseMonitoredField) { 433 | $this->dispatcher->addPropertyChange( 434 | $targetChangeAnnotation, 435 | $newEntity, 436 | $mapping['inversedBy'], 437 | null, 438 | $owningEntity 439 | ); 440 | } 441 | } 442 | } 443 | 444 | /** 445 | * @param object|null $deletedEntity 446 | * @param object $owningEntity 447 | * @param LifecycleEventArgs $args 448 | * @param array $mapping 449 | * 450 | * @return void 451 | * @throws ReflectionException 452 | */ 453 | private function updateDeletedInverse( 454 | ?object $deletedEntity, 455 | object $owningEntity, 456 | LifecycleEventArgs $args, 457 | AssociationMapping $mapping 458 | ): void { 459 | $inverseField = $mapping['inversedBy'] ?? null; 460 | if ($inverseField && $deletedEntity) { 461 | $em = $args->getObjectManager(); 462 | 463 | $inverseMetadata = $em->getClassMetadata($deletedEntity::class); 464 | $deletedClass = $inverseMetadata->getName(); 465 | 466 | /** @var Update $targetAnnotation */ 467 | $targetAnnotation = $this->attributeGetter->getAttribute($deletedClass, Update::class); 468 | /** @var Change $targetChangeAnnotation */ 469 | $targetChangeAnnotation = $this->attributeGetter->getPropertyAttribute($inverseMetadata, 470 | $inverseField, Change::class); 471 | 472 | $inverseMonitoredGlobal = $targetAnnotation && $targetAnnotation->monitor_owning 473 | && !$this->attributeGetter->getPropertyAttribute($inverseMetadata, 474 | $inverseField, IgnoreClassUpdates::class); 475 | 476 | $inverseMonitoredField = $targetChangeAnnotation && $targetChangeAnnotation->monitor_owning; 477 | 478 | $em->initializeObject($deletedEntity); 479 | 480 | if ($inverseMonitoredGlobal) { 481 | $this->dispatcher->addUpdate( 482 | $targetAnnotation, 483 | $deletedEntity, 484 | [], 485 | [$mapping['inversedBy'] => ['deleted' => [$owningEntity], 'inserted' => []]] 486 | ); 487 | } 488 | 489 | if ($inverseMonitoredField) { 490 | $this->dispatcher->addCollectionChange( 491 | $targetChangeAnnotation, 492 | $deletedEntity, 493 | $mapping['inversedBy'], 494 | [$owningEntity], 495 | [] 496 | ); 497 | } 498 | } 499 | } 500 | 501 | /** 502 | * @param object|null $insertedEntity 503 | * @param object $owningEntity 504 | * @param LifecycleEventArgs $args 505 | * @param array $mapping 506 | * 507 | * @return void 508 | * @throws ReflectionException 509 | */ 510 | private function updateInsertedInverse( 511 | ?object $insertedEntity, 512 | object $owningEntity, 513 | LifecycleEventArgs $args, 514 | AssociationMapping $mapping, 515 | ): void { 516 | $inverseField = $mapping['inversedBy'] ?? null; 517 | if ($inverseField && $insertedEntity) { 518 | $em = $args->getObjectManager(); 519 | 520 | $inverseMetadata = $em->getClassMetadata($insertedEntity::class); 521 | $deletedClass = $inverseMetadata->getName(); 522 | 523 | /** @var Update $targetAnnotation */ 524 | $targetAnnotation = $this->attributeGetter->getAttribute($deletedClass, Update::class); 525 | /** @var Change $targetChangeAnnotation */ 526 | $targetChangeAnnotation = $this->attributeGetter->getPropertyAttribute($inverseMetadata, 527 | $inverseField, Change::class); 528 | 529 | $inverseMonitoredGlobal = $targetAnnotation && $targetAnnotation->monitor_owning 530 | && !$this->attributeGetter->getPropertyAttribute($inverseMetadata, 531 | $inverseField, IgnoreClassUpdates::class); 532 | 533 | $inverseMonitoredField = $targetChangeAnnotation && $targetChangeAnnotation->monitor_owning; 534 | 535 | $em->initializeObject($insertedEntity); 536 | 537 | if ($inverseMonitoredGlobal) { 538 | $this->dispatcher->addUpdate( 539 | $targetAnnotation, 540 | $insertedEntity, 541 | [], 542 | [$mapping['inversedBy'] => ['deleted' => [], 'inserted' => [$owningEntity]]] 543 | ); 544 | } 545 | 546 | if ($inverseMonitoredField) { 547 | $this->dispatcher->addCollectionChange( 548 | $targetChangeAnnotation, 549 | $insertedEntity, 550 | $mapping['inversedBy'], 551 | [], 552 | [$owningEntity] 553 | ); 554 | } 555 | } 556 | } 557 | } 558 | -------------------------------------------------------------------------------- /EventListener/LifecyclePropertyEventsListener.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class LifecyclePropertyEventsListener 19 | { 20 | private LifecycleEventsDispatcher $dispatcher; 21 | private AttributeGetter $attributeGetter; 22 | 23 | /** 24 | * Constructs a new instance 25 | * 26 | * @param LifecycleEventsDispatcher $dispatcher the dispatcher to feed 27 | * @param AttributeGetter $attributeGetter 28 | */ 29 | public function __construct(LifecycleEventsDispatcher $dispatcher, AttributeGetter $attributeGetter) 30 | { 31 | $this->dispatcher = $dispatcher; 32 | $this->attributeGetter = $attributeGetter; 33 | } 34 | 35 | /** 36 | * @param PreUpdateEventArgs $args 37 | * 38 | * @return void 39 | * @throws ReflectionException 40 | */ 41 | public function preUpdate(PreUpdateEventArgs $args): void 42 | { 43 | $this->addPropertyChanges($args); 44 | $this->addCollectionChanges($args); 45 | } 46 | 47 | /** 48 | * @param PreUpdateEventArgs $args 49 | * 50 | * @throws ReflectionException 51 | */ 52 | private function addPropertyChanges(PreUpdateEventArgs $args): void 53 | { 54 | $entity = $args->getObject(); 55 | $classMetadata = $args->getObjectManager()->getClassMetadata($entity::class); 56 | 57 | foreach ($args->getEntityChangeSet() as $property => $change) { 58 | /** @var Change $attribute */ 59 | $attribute = $this->attributeGetter->getPropertyAttribute($classMetadata, $property, Change::class); 60 | 61 | if ($attribute) { 62 | $this->dispatcher->addPropertyChange( 63 | $attribute, 64 | $args->getObject(), 65 | $property, 66 | $change[0], 67 | $change[1] 68 | ); 69 | } 70 | } 71 | } 72 | 73 | /** 74 | * @param PreUpdateEventArgs $args 75 | * 76 | * @throws ReflectionException 77 | */ 78 | private function addCollectionChanges(PreUpdateEventArgs $args): void 79 | { 80 | $entity = $args->getObject(); 81 | $classMetadata = $args->getObjectManager()->getClassMetadata($entity::class); 82 | 83 | /** @var PersistentCollection $update */ 84 | foreach ($args->getObjectManager()->getUnitOfWork()->getScheduledCollectionUpdates() as $update) { 85 | if ($update->getOwner() !== $entity) { 86 | continue; 87 | } 88 | 89 | $property = $update->getMapping()['fieldName']; 90 | /** @var Change $attribute */ 91 | $attribute = $this->attributeGetter->getPropertyAttribute($classMetadata, $property, Change::class); 92 | 93 | // Make sure $u belongs to the entity we are working on 94 | if (!$attribute) { 95 | continue; 96 | } 97 | 98 | $this->dispatcher->addCollectionChange( 99 | $attribute, 100 | $args->getObject(), 101 | $property, 102 | $update->getDeleteDiff(), 103 | $update->getInsertDiff() 104 | ); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /EventListener/PostFlushListener.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class PostFlushListener 14 | { 15 | private LifecycleEventsDispatcher $dispatcher; 16 | 17 | public function __construct(LifecycleEventsDispatcher $dispatcher) 18 | { 19 | $this->dispatcher = $dispatcher; 20 | } 21 | 22 | /** 23 | * Called upon receiving postFlush events 24 | * Dispatches all gathered events 25 | * 26 | * @param PostFlushEventArgs $args post flush event 27 | */ 28 | public function postFlush(PostFlushEventArgs $args): void 29 | { 30 | if ($this->dispatcher->getAutoDispatch()) { 31 | $this->dispatcher->preAutoDispatch(); 32 | $this->dispatcher->dispatchEvents(); 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | W3C Software and Document Notice and License 2 | ============================================ 3 | 4 | This work is being provided by the copyright holders under the following license. 5 | 6 | License 7 | ------- 8 | 9 | By obtaining and/or copying this work, you (the licensee) agree that you have read, understood, and will comply with 10 | the following terms and conditions. 11 | 12 | Permission to copy, modify, and distribute this work, with or without modification, for any purpose and without fee or 13 | royalty is hereby granted, provided that you include the following on ALL copies of the work or portions thereof, 14 | including modifications: 15 | - The full text of this NOTICE in a location viewable to users of the redistributed or derivative work. 16 | - Any pre-existing intellectual property disclaimers, notices, or terms and conditions. If none exist, the W3C Software 17 | and Document Short Notice should be included. 18 | - Notice of any changes or modifications, through a copyright statement on the new code or document such as 19 | "This software or document includes material copied from or derived from 20 | [W3CLifecycleEventsBundle](https://www.github.com/w3c/W3CLifecycleEventsBundle). 21 | Copyright © 2017 W3C® (MIT, ERCIM, Keio, Beihang)." 22 | 23 | Disclaimers 24 | ----------- 25 | 26 | THIS WORK IS PROVIDED "AS IS," AND COPYRIGHT HOLDERS MAKE NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, 27 | INCLUDING BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF 28 | THE SOFTWARE OR DOCUMENT WILL NOT INFRINGE ANY THIRD PARTY PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS. 29 | 30 | COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE 31 | OF THE SOFTWARE OR DOCUMENT. 32 | 33 | The name and trademarks of copyright holders may NOT be used in advertising or publicity pertaining to the work without 34 | specific, written prior permission. Title to copyright in this work will at all times remain with copyright holders. 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://github.com/w3c/W3CLifecycleEventsBundle/actions/workflows/symfony.yml/badge.svg)](https://github.com/w3c/W3CLifecycleEventsBundle/actions/workflows/symfony.yml) 2 | [![Coverage Status](https://coveralls.io/repos/github/w3c/W3CLifecycleEventsBundle/badge.svg?branch=master)](https://coveralls.io/github/w3c/W3CLifecycleEventsBundle?branch=master) 3 | [![SensioLabsInsight](https://insight.symfony.com/projects/b0d1493c-6de8-4c18-87ad-a12d2487fd59/mini.svg)](https://insight.symfony.com/account/widget?project=b0d1493c-6de8-4c18-87ad-a12d2487fd59) 4 | 5 | lifecycle-events-bundle 6 | ======================= 7 | 8 | This Symfony bundle is meant to capture and dispatch events that happen throughout the lifecycle of entities: 9 | - creation 10 | - deletion 11 | - updates 12 | 13 | Doctrine already provides such events, but using them directly has a few shortcomings: 14 | - You don't decide at which point in a action you want to dispatch events. Events are fired during a flush. 15 | - 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 preRemove have the same issue: 17 | if you persist two new entities in a single transaction, the first insert could work (thus an event would be sent) but 18 | not the second, resulting in no entities being saved at all 19 | 20 | This bundle aims at circumventing these issues by providing means to fire entity creation, deletion and update events 21 | after a successful flush or whenever needed. 22 | 23 | It also provides a set of attributes to configure what events should be sent and when. 24 | 25 | This bundle was partially inspired by @kriswallsmith's talk 26 | ["Matters of State"](https://www.youtube.com/watch?v=lEiwP4w6mf4). 27 | 28 | Installation 29 | ------------ 30 | 31 | Simply run assuming you have installed composer.phar or composer binary: 32 | 33 | ``` bash 34 | $ php composer.phar require w3c/lifecycle-events-bundle 1.0.* 35 | ``` 36 | 37 | Finally, enable the bundle in the kernel: 38 | 39 | ``` php 40 | setAutoDispatch(false); 232 | [...] 233 | } 234 | ``` 235 | 236 | Events can then be dispatched manually using the following: 237 | ``` php 238 | dispatchEvents(); // manually dispatch all events 240 | ``` 241 | 242 | Special case: inheritance 243 | ------------------------- 244 | 245 | If you use inheritance in your entities, make sure to set fields of the parent class(es) protected (or public) so that 246 | changes to those can be monitored as belonging to subclasses. 247 | 248 | Failing to do so may lead to `\ReflectionException` exceptions such as: 249 | ``` 250 | Property W3CGroup::$updated does not exist. Could this be a private field of a parent class? 251 | ``` 252 | Even if those fields are not monitored! 253 | -------------------------------------------------------------------------------- /Resources/config/services.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | w3c_lifecycle_events.auto_dispatch: '' 3 | 4 | services: 5 | W3C\LifecycleEventsBundle\Services\AttributeGetter: 6 | W3C\LifecycleEventsBundle\Services\LifecycleEventsDispatcher: 7 | arguments: [ '@event_dispatcher', '%w3c_lifecycle_events.auto_dispatch%' ] 8 | W3C\LifecycleEventsBundle\EventListener\LifecycleEventsListener: 9 | arguments: [ '@w3c_lifecycle_events.dispatcher', '@w3c_lifecycle_events.attribute_getter' ] 10 | tags: 11 | - { name: doctrine.event_listener, event: postPersist } 12 | - { name: doctrine.event_listener, event: preRemove } 13 | - { name: doctrine.event_listener, event: preSoftDelete } 14 | - { name: doctrine.event_listener, event: preUpdate } 15 | W3C\LifecycleEventsBundle\EventListener\LifecyclePropertyEventsListener: 16 | arguments: [ '@w3c_lifecycle_events.dispatcher', '@w3c_lifecycle_events.attribute_getter' ] 17 | tags: 18 | - { name: doctrine.event_listener, event: preUpdate } 19 | W3C\LifecycleEventsBundle\EventListener\PostFlushListener: 20 | arguments: [ '@w3c_lifecycle_events.dispatcher' ] 21 | tags: 22 | - { name: doctrine.event_listener, event: postFlush } 23 | 24 | w3c_lifecycle_events.attribute_getter: 25 | alias: W3C\LifecycleEventsBundle\Services\AttributeGetter 26 | w3c_lifecycle_events.dispatcher: 27 | alias: W3C\LifecycleEventsBundle\Services\LifecycleEventsDispatcher 28 | w3c_lifecycle_events.listener: 29 | alias: W3C\LifecycleEventsBundle\EventListener\LifecycleEventsListener 30 | w3c_lifecycle_events.property_listener: 31 | alias: W3C\LifecycleEventsBundle\EventListener\LifecyclePropertyEventsListener 32 | w3c_lifecycle_events.post_flush_listener: 33 | alias: W3C\LifecycleEventsBundle\EventListener\PostFlushListener 34 | -------------------------------------------------------------------------------- /Services/AttributeGetter.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class AttributeGetter 14 | { 15 | /** 16 | * Get a class-level attribute 17 | * 18 | * @param string $class Class to get attribute of 19 | * @param string $attributeClass Class of the attribute to get 20 | * 21 | * @return object|null object of same class as $attributeClass or null if no attribute is found 22 | * @throws ReflectionException 23 | */ 24 | public function getAttribute(string $class, string $attributeClass): ?object 25 | { 26 | $reflection = new \ReflectionClass($class); 27 | $attributes = $reflection->getAttributes($attributeClass); 28 | 29 | if (\count($attributes) === 0) { 30 | return null; 31 | } 32 | 33 | return $attributes[0]->newInstance(); 34 | } 35 | 36 | /** 37 | * Get a field-level attribute 38 | * 39 | * @param ClassMetadata $classMetadata Metadata of the class to get attribute of 40 | * @param string $field Name of the field to get attribute of 41 | * @param string $attributeClass Class of the attribute to get 42 | * 43 | * @return object|null object of same class as $attributeClass or null if no attribute is found 44 | * @throws ReflectionException if the field does not exist 45 | */ 46 | public function getPropertyAttribute(ClassMetadata $classMetadata, string $field, string $attributeClass): ?object 47 | { 48 | 49 | $reflProperty = $classMetadata->getReflectionProperty($field); 50 | 51 | if ($reflProperty) { 52 | $attributes = $reflProperty->getAttributes($attributeClass); 53 | 54 | if (\count($attributes) === 0) { 55 | return null; 56 | } 57 | 58 | return $attributes[0]->newInstance(); 59 | } 60 | 61 | throw new ReflectionException( 62 | $classMetadata->getName() . '.' . $field . ' not found. Could this be a private field of a parent class?' 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Services/LifecycleEventsDispatcher.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | class LifecycleEventsDispatcher 25 | { 26 | /** 27 | * List of creation events 28 | * 29 | * @var LifecycleEventArgs[] 30 | */ 31 | private array $creations = []; 32 | 33 | /** 34 | * List of update events 35 | * 36 | * @var PreUpdateEventArgs[] 37 | */ 38 | private array $updates = []; 39 | 40 | /** 41 | * List of deletion events 42 | * 43 | * @var LifecycleEventArgs[] 44 | */ 45 | private array $deletions = []; 46 | 47 | /** 48 | * List of property change events 49 | * 50 | * @var array 51 | */ 52 | private array $propertyChanges = []; 53 | 54 | /** 55 | * List of collection change events 56 | * 57 | * @var array 58 | */ 59 | private array $collectionChanges = []; 60 | 61 | /** 62 | * Symfony's event dispatcher 63 | * 64 | * @var EventDispatcherInterface 65 | */ 66 | private EventDispatcherInterface $dispatcher; 67 | 68 | /** 69 | * Records whether events should be fired automatically after a successful flush 70 | * 71 | * @var boolean 72 | */ 73 | private bool $autoDispatch; 74 | 75 | /** 76 | * Create a new instance 77 | * 78 | * @param EventDispatcherInterface $dispatcher a Symfony's event dispatcher 79 | * @param bool $autoDispatch 80 | */ 81 | public function __construct(EventDispatcherInterface $dispatcher, bool $autoDispatch) 82 | { 83 | $this->dispatcher = $dispatcher; 84 | $this->autoDispatch = $autoDispatch; 85 | } 86 | 87 | public function getDispatcher(): EventDispatcherInterface 88 | { 89 | return $this->dispatcher; 90 | } 91 | 92 | /** 93 | * Dispatch all types of events to their listeners 94 | */ 95 | public function dispatchEvents(): void 96 | { 97 | $this->dispatchCreationEvents(); 98 | $this->dispatchDeletionEvents(); 99 | $this->dispatchUpdateEvents(); 100 | $this->dispatchPropertyChangeEvents(); 101 | $this->dispatchCollectionChangeEvents(); 102 | } 103 | 104 | /** 105 | * Dispatch creation events to listeners of w3c.lifecycle.created (or custom event name) 106 | */ 107 | private function dispatchCreationEvents(): void 108 | { 109 | $creations = $this->creations; 110 | $this->creations = []; 111 | 112 | foreach ($creations as $creation) { 113 | $attribute = $creation[0]; 114 | /** @var LifecycleEventArgs $eventArgs */ 115 | $eventArgs = $creation[1]; 116 | $entity = $eventArgs->getObject(); 117 | 118 | $this->dispatcher->dispatch(new $attribute->class($entity), $attribute->event); 119 | } 120 | } 121 | 122 | /** 123 | * Dispatch deletion events to listeners of w3c.lifecycle.deleted (or custom event name) 124 | */ 125 | private function dispatchDeletionEvents(): void 126 | { 127 | $deletions = $this->deletions; 128 | $this->deletions = []; 129 | 130 | foreach ($deletions as $deletion) { 131 | $attribute = $deletion[0]; 132 | /** @var LifecycleEventArgs $eventArgs */ 133 | $eventArgs = $deletion[1]; 134 | $identifier = $deletion[2]; 135 | $entity = $eventArgs->getObject(); 136 | 137 | $this->dispatcher->dispatch(new $attribute->class($entity, $identifier), $attribute->event); 138 | } 139 | } 140 | 141 | /** 142 | * Dispatch update events to listeners of w3c.lifecycle.updated (or custom event name) 143 | */ 144 | private function dispatchUpdateEvents(): void 145 | { 146 | $updates = $this->updates; 147 | $this->updates = []; 148 | 149 | foreach ($updates as $update) { 150 | [$attribute, $entity, $propertiesChanges, $collectionsChanges] = $update; 151 | 152 | $this->dispatcher->dispatch( 153 | new $attribute->class($entity, $propertiesChanges, $collectionsChanges), 154 | $attribute->event, 155 | ); 156 | } 157 | } 158 | 159 | /** 160 | * Dispatch property change events to listeners of w3c.lifecycle.property_changed (or custom event name) 161 | */ 162 | private function dispatchPropertyChangeEvents(): void 163 | { 164 | $propertyChanges = $this->propertyChanges; 165 | $this->propertyChanges = []; 166 | 167 | foreach ($propertyChanges as $propertyChange) { 168 | [$attribute, $entity, $property, $oldValue, $newValue] = $propertyChange; 169 | 170 | $this->dispatcher->dispatch( 171 | new $attribute->class($entity, $property, $oldValue, $newValue), 172 | $attribute->event, 173 | ); 174 | } 175 | } 176 | 177 | /** 178 | * Dispatch collection change events to listeners of w3c.lifecycle.collection_changed (or custom event name) 179 | */ 180 | private function dispatchCollectionChangeEvents(): void 181 | { 182 | $collectionChanges = $this->collectionChanges; 183 | $this->collectionChanges = []; 184 | 185 | foreach ($collectionChanges as $collectionChange) { 186 | [$attribute, $entity, $property, $deleted, $added] = $collectionChange; 187 | if ($attribute->event === LifecycleEvents::PROPERTY_CHANGED) { 188 | $attribute->event = LifecycleEvents::COLLECTION_CHANGED; 189 | } 190 | if($attribute->class === LifecyclePropertyChangedEvent::class) { 191 | $attribute->class = LifecycleCollectionChangedEvent::class; 192 | } 193 | 194 | $this->dispatcher->dispatch( 195 | new $attribute->class($entity, $property, $deleted, $added), 196 | $attribute->event, 197 | ); 198 | } 199 | } 200 | 201 | /** 202 | * Get the list of intercepted creation events 203 | * 204 | * @return LifecycleEventArgs[] a list of LifecycleEventArgs events 205 | */ 206 | public function getCreations(): array 207 | { 208 | return $this->creations; 209 | } 210 | 211 | public function addCreation(Create $attribute, LifecycleEventArgs $args): void 212 | { 213 | $this->creations[] = [$attribute, $args]; 214 | } 215 | 216 | /** 217 | * Get the list of intercepted deletion events 218 | * 219 | * @return LifecycleEventArgs[] a list of LifecycleEventArgs events 220 | */ 221 | public function getDeletions(): array 222 | { 223 | return $this->deletions; 224 | } 225 | 226 | public function addDeletion(Delete $attribute, LifecycleEventArgs $args): void 227 | { 228 | $classMetadata = $args->getObjectManager()->getClassMetadata($args->getObject()::class); 229 | $this->deletions[] = [ 230 | $attribute, 231 | $args, 232 | array_combine( 233 | $classMetadata->getIdentifierFieldNames(), 234 | $classMetadata->getIdentifierValues($args->getObject()) 235 | ) 236 | ]; 237 | } 238 | 239 | /** 240 | * Get the list of intercepted update events 241 | * 242 | * @return PreUpdateEventArgs[] a list of PreUpdateEventArgs events 243 | */ 244 | public function getUpdates(): array 245 | { 246 | return $this->updates; 247 | } 248 | 249 | public function addUpdate( 250 | Update $attribute, 251 | object $entity, 252 | array $propertyChangeSet = null, 253 | array $collectionChangeSet = null 254 | ): void { 255 | if (list($key, $update) = $this->getUpdate($entity)) { 256 | $update[2] = array_merge_recursive((array)$update[2], (array)$propertyChangeSet); 257 | $update[3] = array_merge_recursive((array)$update[3], (array)$collectionChangeSet); 258 | $this->updates[$key] = $update; 259 | } else { 260 | $this->updates[] = [$attribute, $entity, $propertyChangeSet, $collectionChangeSet]; 261 | } 262 | } 263 | 264 | public function getUpdate(object $entity): ?array 265 | { 266 | foreach ($this->updates as $key => $update) { 267 | if ($update[1] === $entity) { 268 | return [$key, $update]; 269 | } 270 | } 271 | 272 | return null; 273 | } 274 | 275 | public function getPropertyChanges(): array 276 | { 277 | return $this->propertyChanges; 278 | } 279 | 280 | public function addPropertyChange( 281 | Change $attribute, 282 | object $entity, 283 | string $property, 284 | $oldValue = null, 285 | $newValue = null 286 | ): void 287 | { 288 | $this->propertyChanges[] = [$attribute, $entity, $property, $oldValue, $newValue]; 289 | } 290 | 291 | public function getCollectionChanges(): array 292 | { 293 | return $this->collectionChanges; 294 | } 295 | 296 | public function addCollectionChange( 297 | Change $attribute, 298 | object $entity, 299 | string $property, 300 | array $deletedElements = [], 301 | array $insertedElements = [] 302 | ): void { 303 | if (list($key, $change) = $this->getCollectionChange($entity, $property)) { 304 | $change[3] = array_merge_recursive((array)$change[3], (array)$deletedElements); 305 | $change[4] = array_merge_recursive((array)$change[4], (array)$insertedElements); 306 | $this->collectionChanges[$key] = $change; 307 | } else { 308 | $this->collectionChanges[] = [$attribute, $entity, $property, $deletedElements, $insertedElements]; 309 | } 310 | } 311 | 312 | public function getCollectionChange(object $entity, string $property): ?array 313 | { 314 | foreach ($this->collectionChanges as $key => $update) { 315 | if ($update[1] === $entity && $update[2] === $property) { 316 | return [$key, $update]; 317 | } 318 | } 319 | 320 | return null; 321 | } 322 | 323 | /** 324 | * Is automatic dispatching of events active. 325 | * This value has no direct effect on this class but can be use elsewhere 326 | * (e.g. in LifecycleEventListener::postFlush()) 327 | * 328 | * @return bool 329 | */ 330 | public function getAutoDispatch(): bool 331 | { 332 | return $this->autoDispatch; 333 | } 334 | 335 | /** 336 | * Set automatic dispatching of events 337 | * 338 | * @param bool $autoDispatch 339 | * 340 | * @return $this 341 | */ 342 | public function setAutoDispatch(bool $autoDispatch): LifecycleEventsDispatcher 343 | { 344 | $this->autoDispatch = $autoDispatch; 345 | 346 | return $this; 347 | } 348 | 349 | /** 350 | * Send out a preAutoDispatch event 351 | */ 352 | public function preAutoDispatch(): void 353 | { 354 | $this->dispatcher->dispatch(new PreAutoDispatchEvent($this), 'w3c.lifecycle.preAutoDispatch'); 355 | } 356 | } 357 | -------------------------------------------------------------------------------- /Tests/Attribute/ChangeTest.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class ChangeTest extends TestCase 17 | { 18 | public function testNoParams() 19 | { 20 | $change = new Change(); 21 | 22 | $reflection = new \ReflectionProperty(User::class, 'name'); 23 | $attribute = $reflection->getAttributes()[0]->newInstance(); 24 | 25 | $this->assertEquals($attribute->class, $change->class); 26 | $this->assertEquals($attribute->event, $change->event); 27 | } 28 | 29 | public function testClass() 30 | { 31 | $reflection = new \ReflectionProperty(UserClass::class, 'name'); 32 | $attribute = $reflection->getAttributes()[0]->newInstance(); 33 | 34 | $this->assertEquals($attribute->class, 'FooBar'); 35 | } 36 | 37 | public function testEvent() 38 | { 39 | $reflection = new \ReflectionProperty(UserEvent::class, 'name'); 40 | $attribute = $reflection->getAttributes()[0]->newInstance(); 41 | 42 | $this->assertEquals($attribute->event, 'foo.bar'); 43 | 44 | 45 | $reflection = new \ReflectionProperty(UserEvent2::class, 'name'); 46 | $attribute = $reflection->getAttributes()[0]->newInstance(); 47 | 48 | $this->assertEquals($attribute->event, 'foo.bar'); 49 | } 50 | 51 | public function testError() 52 | { 53 | $this->expectException(\Error::class); 54 | 55 | $reflection = new \ReflectionClass(UserErrorChange::class); 56 | $attribute = $reflection->getAttributes()[0]->newInstance(); 57 | 58 | var_dump($attribute); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Tests/Attribute/CreateTest.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class CreateTest extends TestCase 17 | { 18 | public function testNoParams() 19 | { 20 | $create = new Create(); 21 | 22 | $reflection = new \ReflectionClass(User::class); 23 | $attribute = $reflection->getAttributes(Create::class)[0]->newInstance(); 24 | 25 | $this->assertEquals($attribute->class, $create->class); 26 | $this->assertEquals($attribute->event, $create->event); 27 | } 28 | 29 | public function testClass() 30 | { 31 | $reflection = new \ReflectionClass(UserClass::class); 32 | $attribute = $reflection->getAttributes(Create::class)[0]->newInstance(); 33 | 34 | $this->assertEquals($attribute->class, 'FooBar'); 35 | } 36 | 37 | public function testEvent() 38 | { 39 | $reflection = new \ReflectionClass(UserEvent::class); 40 | $attribute = $reflection->getAttributes(Create::class)[0]->newInstance(); 41 | 42 | $this->assertEquals($attribute->event, 'foo.bar'); 43 | 44 | 45 | $reflection = new \ReflectionClass(UserEvent2::class); 46 | $attribute = $reflection->getAttributes(Create::class)[0]->newInstance(); 47 | 48 | $this->assertEquals($attribute->event, 'foo.bar'); 49 | } 50 | 51 | public function testError() 52 | { 53 | $this->expectException(\Error::class); 54 | 55 | $reflection = new \ReflectionProperty(UserErrorCreate::class, 'name'); 56 | $reflection->getAttributes(Create::class)[0]->newInstance(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Tests/Attribute/DeleteTest.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class DeleteTest extends TestCase 17 | { 18 | public function testNoParams() 19 | { 20 | $delete = new Delete(); 21 | 22 | $reflection = new \ReflectionClass(User::class); 23 | $attribute = $reflection->getAttributes(Delete::class)[0]->newInstance(); 24 | 25 | $this->assertEquals($attribute->class, $delete->class); 26 | $this->assertEquals($attribute->event, $delete->event); 27 | } 28 | 29 | public function testClass() 30 | { 31 | $reflection = new \ReflectionClass(UserClass::class); 32 | $attribute = $reflection->getAttributes(Delete::class)[0]->newInstance(); 33 | 34 | $this->assertEquals($attribute->class, 'FooBar'); 35 | } 36 | 37 | public function testEvent() 38 | { 39 | $reflection = new \ReflectionClass(UserEvent::class); 40 | $attribute = $reflection->getAttributes(Delete::class)[0]->newInstance(); 41 | 42 | $this->assertEquals($attribute->event, 'foo.bar'); 43 | 44 | 45 | $reflection = new \ReflectionClass(UserEvent2::class); 46 | $attribute = $reflection->getAttributes(Delete::class)[0]->newInstance(); 47 | 48 | $this->assertEquals($attribute->event, 'foo.bar'); 49 | } 50 | 51 | public function testError() 52 | { 53 | $this->expectException(\Error::class); 54 | 55 | $reflection = new \ReflectionProperty(UserErrorDelete::class, 'name'); 56 | $reflection->getAttributes(Delete::class)[0]->newInstance(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Tests/Attribute/Fixtures/Person.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | #[On\Update(monitor_owning: true)] 11 | class Person 12 | { 13 | public $name; 14 | 15 | /** 16 | * @ ORM\ManyToMany(targetEntity="Person", inversedBy="friendOf") 17 | */ 18 | public $friends; 19 | 20 | /** 21 | * @ ORM\ManyToMany(targetEntity="Person", mappedBy="friends") 22 | */ 23 | #[On\Change(monitor_owning: true)] 24 | public $friendOf; 25 | 26 | /** 27 | * @ ORM\ManyToOne(targetEntity="Person", inversedBy="sons") 28 | */ 29 | public $father; 30 | 31 | /** 32 | * @ ORM\OneToMany(targetEntity="Person", mappedBy="father") 33 | */ 34 | #[On\Change(monitor_owning: true)] 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 | #[On\Change(monitor_owning: true)] 46 | public $mentoring; 47 | } 48 | -------------------------------------------------------------------------------- /Tests/Attribute/Fixtures/PersonNoMonitor.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | #[On\Update] 11 | class PersonNoMonitor 12 | { 13 | public $name; 14 | 15 | /** 16 | * @ ORM\ManyToMany(targetEntity="Person", inversedBy="friendOf") 17 | */ 18 | public $friends; 19 | 20 | /** 21 | * @ ORM\ManyToMany(targetEntity="Person", mappedBy="friends") 22 | */ 23 | public $friendOf; 24 | 25 | /** 26 | * @ ORM\ManyToOne(targetEntity="Person", inversedBy="sons") 27 | */ 28 | #[On\Change] 29 | public $father; 30 | 31 | /** 32 | * @ ORM\OneToMany(targetEntity="Person", mappedBy="father") 33 | */ 34 | public $sons; 35 | 36 | /** 37 | * @ ORM\OneToOne(targetEntity="Person", inversedBy="mentoring") 38 | */ 39 | public $mentor; 40 | 41 | /** 42 | * @ ORM\OneToOne(targetEntity="Person", mappedBy="mentor") 43 | */ 44 | public $mentoring; 45 | } 46 | -------------------------------------------------------------------------------- /Tests/Attribute/Fixtures/User.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | #[On\Create] 11 | #[On\Delete] 12 | #[On\Update] 13 | class User 14 | { 15 | #[On\Change] 16 | #[On\IgnoreClassUpdates] 17 | public $name; 18 | 19 | public $friends; 20 | } 21 | -------------------------------------------------------------------------------- /Tests/Attribute/Fixtures/UserClass.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | #[On\Create(class: "FooBar")] 11 | #[On\Delete(class: "FooBar")] 12 | #[On\Update(class: "FooBar")] 13 | class UserClass 14 | { 15 | #[On\Change(class: "FooBar")] 16 | public $name; 17 | } 18 | -------------------------------------------------------------------------------- /Tests/Attribute/Fixtures/UserErrorChange.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | #[On\Change] 11 | class UserErrorChange 12 | { 13 | public $name; 14 | } 15 | -------------------------------------------------------------------------------- /Tests/Attribute/Fixtures/UserErrorCreate.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class UserErrorCreate 11 | { 12 | #[On\Create] 13 | public $name; 14 | } 15 | -------------------------------------------------------------------------------- /Tests/Attribute/Fixtures/UserErrorDelete.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class UserErrorDelete 11 | { 12 | #[On\Delete] 13 | public $name; 14 | } 15 | -------------------------------------------------------------------------------- /Tests/Attribute/Fixtures/UserErrorIgnore.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | #[On\IgnoreClassUpdates] 11 | class UserErrorIgnore 12 | { 13 | public $name; 14 | } 15 | -------------------------------------------------------------------------------- /Tests/Attribute/Fixtures/UserErrorUpdate.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class UserErrorUpdate 11 | { 12 | #[On\Update] 13 | public $name; 14 | } 15 | -------------------------------------------------------------------------------- /Tests/Attribute/Fixtures/UserEvent.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | #[On\Create(event: "foo.bar")] 11 | #[On\Delete(event: "foo.bar")] 12 | #[On\Update(event: "foo.bar")] 13 | class UserEvent 14 | { 15 | #[On\Change(event: "foo.bar")] 16 | public $name; 17 | } 18 | -------------------------------------------------------------------------------- /Tests/Attribute/Fixtures/UserEvent2.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | #[On\Create("foo.bar")] 11 | #[On\Delete("foo.bar")] 12 | #[On\Update("foo.bar")] 13 | class UserEvent2 14 | { 15 | #[On\Change("foo.bar")] 16 | public $name; 17 | } 18 | -------------------------------------------------------------------------------- /Tests/Attribute/IgnoreTest.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class IgnoreTest extends TestCase 14 | { 15 | public function testNoParams() 16 | { 17 | $reflection = new \ReflectionProperty(User::class, 'name'); 18 | $attribute = $reflection->getAttributes(IgnoreClassUpdates::class)[0]->newInstance(); 19 | 20 | $this->assertNotNull($attribute); 21 | } 22 | 23 | public function testError() 24 | { 25 | $this->expectException(\Error::class); 26 | 27 | $reflection = new \ReflectionClass(UserErrorIgnore::class); 28 | $reflection->getAttributes(IgnoreClassUpdates::class)[0]->newInstance(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Tests/Attribute/UpdateTest.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class UpdateTest extends TestCase 17 | { 18 | public function testNoParams() 19 | { 20 | $update = new Update(); 21 | 22 | $reflection = new \ReflectionClass(User::class); 23 | $attribute = $reflection->getAttributes(Update::class)[0]->newInstance(); 24 | 25 | $this->assertEquals($attribute->class, $update->class); 26 | $this->assertEquals($attribute->event, $update->event); 27 | } 28 | 29 | public function testClass() 30 | { 31 | $reflection = new \ReflectionClass(UserClass::class); 32 | $attribute = $reflection->getAttributes(Update::class)[0]->newInstance(); 33 | 34 | $this->assertEquals($attribute->class, 'FooBar'); 35 | } 36 | 37 | public function testEvent() 38 | { 39 | $reflection = new \ReflectionClass(UserEvent::class); 40 | $attribute = $reflection->getAttributes(Update::class)[0]->newInstance(); 41 | 42 | $this->assertEquals($attribute->event, 'foo.bar'); 43 | 44 | 45 | $reflection = new \ReflectionClass(UserEvent2::class); 46 | $attribute = $reflection->getAttributes(Update::class)[0]->newInstance(); 47 | 48 | $this->assertEquals($attribute->event, 'foo.bar'); 49 | } 50 | 51 | public function testError() 52 | { 53 | $this->expectException(\Error::class); 54 | 55 | $reflection = new \ReflectionProperty(UserErrorUpdate::class, 'name'); 56 | $reflection->getAttributes(Update::class)[0]->newInstance(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Tests/Event/LifecycleCollectionChangedEventTest.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class LifecycleCollectionChangedEventTest extends TestCase 13 | { 14 | public function testAccessors() 15 | { 16 | $deleted = [new User()]; 17 | $inserted = [new User()]; 18 | $event = new LifecycleCollectionChangedEvent(new User(), 'friends', $deleted, $inserted); 19 | 20 | $this->assertEquals('friends', $event->getProperty()); 21 | $this->assertEquals($deleted, $event->getDeletedElements()); 22 | $this->assertEquals($inserted, $event->getInsertedElements()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Tests/Event/LifecycleDeletionEventTest.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class LifecycleDeletionEventTest extends TestCase 13 | { 14 | public function testAccessors() 15 | { 16 | $entity = new User(); 17 | $event = new LifecycleDeletionEvent($entity, ['foo' => 'bar', 'baz' => 2]); 18 | 19 | $this->assertSame($entity, $event->getEntity()); 20 | $this->assertSame(['foo' => 'bar', 'baz' => 2], $event->getIdentifier()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Tests/Event/LifecycleEventTest.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class LifecycleEventTest extends TestCase 13 | { 14 | public function testAccessors() 15 | { 16 | $entity = new User(); 17 | $event = new LifecycleEvent($entity); 18 | 19 | $this->assertSame($entity, $event->getEntity()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/Event/LifecyclePropertyChangedEventTest.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class LifecyclePropertyChangedEventTest extends TestCase 13 | { 14 | public function testAccessors() 15 | { 16 | $entity = new User(); 17 | $event = new LifecyclePropertyChangedEvent($entity, 'name', 'foo', 'bar'); 18 | 19 | $this->assertEquals('name', $event->getProperty()); 20 | $this->assertEquals('foo', $event->getOldValue()); 21 | $this->assertEquals('bar', $event->getNewValue()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Tests/Event/LifecycleUpdateEventTest.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class LifecycleUpdateEventTest extends TestCase 13 | { 14 | private $deleted; 15 | private $inserted; 16 | private $propertyChanges; 17 | private $collectionChanges; 18 | /** 19 | * @var LifecycleUpdateEvent 20 | */ 21 | private $event; 22 | 23 | public function setUp() : void 24 | { 25 | $entity = new User(); 26 | $this->propertyChanges = ['name' => ['old' => 'foo', 'new' => 'bar']]; 27 | $this->deleted = [new User()]; 28 | $this->inserted = [new User()]; 29 | $this->collectionChanges = ['friends' => ['deleted' => $this->deleted, 'inserted' => $this->inserted]]; 30 | $this->event = new LifecycleUpdateEvent($entity, $this->propertyChanges, $this->collectionChanges); 31 | } 32 | 33 | public function testAccessors() 34 | { 35 | $this->assertTrue($this->event->havePropertiesChanged()); 36 | $this->assertTrue($this->event->haveCollectionsChanged()); 37 | 38 | $this->assertEquals(['name'], $this->event->getChangedProperties()); 39 | $this->assertEquals('bar', $this->event->getNewValue('name')); 40 | $this->assertEquals('foo', $this->event->getOldValue('name')); 41 | $this->assertTrue($this->event->hasChangedField('name')); 42 | $this->assertFalse($this->event->hasChangedField('family')); 43 | 44 | $this->assertEquals(['friends'], $this->event->getChangedCollections()); 45 | $this->assertSame($this->inserted, $this->event->getInsertedElements('friends')); 46 | $this->assertSame($this->deleted, $this->event->getDeletedElements('friends')); 47 | $this->assertTrue($this->event->hasChangedField('friends')); 48 | $this->assertFalse($this->event->hasChangedField('tags')); 49 | } 50 | 51 | /** 52 | * @dataProvider provideTestInvalidCollection 53 | */ 54 | public function testInvalidCollection($field) 55 | { 56 | $this->expectException(\InvalidArgumentException::class); 57 | 58 | $this->event->getDeletedElements($field); 59 | } 60 | 61 | public function provideTestInvalidCollection() 62 | { 63 | return [['name'], ['tags']]; 64 | } 65 | 66 | /** 67 | * @dataProvider provideTestInvalidProperty 68 | */ 69 | public function testInvalidProperty($field) 70 | { 71 | $this->expectException(\InvalidArgumentException::class); 72 | 73 | $this->event->getOldValue($field); 74 | } 75 | 76 | public function provideTestInvalidProperty() 77 | { 78 | return [['friends'], ['family']]; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Tests/Event/PreAutoDispatchEventTest.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class PreAutoDispatchEventTest extends TestCase 13 | { 14 | public function testAccessors() 15 | { 16 | /** @var LifecycleEventsDispatcher $dispatcher */ 17 | $dispatcher = $this->getMockBuilder(LifecycleEventsDispatcher::class) 18 | ->disableOriginalConstructor() 19 | ->getMock(); 20 | $event = new PreAutoDispatchEvent($dispatcher); 21 | 22 | $this->assertSame($dispatcher, $event->getDispatcher()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Tests/EventListener/Fixtures/OtherEntity.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class OtherEntity 11 | { 12 | #[On\Change] 13 | public $foo; 14 | } 15 | -------------------------------------------------------------------------------- /Tests/EventListener/Fixtures/UserChange.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class UserChange 11 | { 12 | #[On\Change] 13 | public $name; 14 | 15 | public $email; 16 | 17 | #[On\Change] 18 | public $friends; 19 | } 20 | -------------------------------------------------------------------------------- /Tests/EventListener/Fixtures/UserClassUpdateCollection.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class UserNoAnnotation 11 | { 12 | public $name; 13 | 14 | public $friends; 15 | } 16 | -------------------------------------------------------------------------------- /Tests/EventListener/LifecycleEventsListenerInverseNoMonitorTest.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | class LifecycleEventsListenerInverseNoMonitorTest extends TestCase 32 | { 33 | /** 34 | * @var LifecycleEventsListener 35 | */ 36 | private $listener; 37 | 38 | /** 39 | * @var LifecycleEventsDispatcher|MockObject 40 | */ 41 | private $dispatcher; 42 | 43 | /** 44 | * @var EntityManagerInterface|MockObject 45 | */ 46 | private $manager; 47 | 48 | /** 49 | * @var ClassMetadata|MockObject 50 | */ 51 | private $classMetadata; 52 | 53 | /** 54 | * @var array 55 | */ 56 | private $mappings; 57 | 58 | private $person; 59 | private $mentor; 60 | private $father; 61 | private $friend1; 62 | private $friend2; 63 | 64 | public function setUp() : void 65 | { 66 | parent::setUp(); 67 | 68 | $this->person = new PersonNoMonitor(); 69 | $this->mentor = new PersonNoMonitor(); 70 | $this->father = new PersonNoMonitor(); 71 | $this->friend1 = new PersonNoMonitor(); 72 | $this->friend2 = new PersonNoMonitor(); 73 | 74 | $this->dispatcher = $this 75 | ->getMockBuilder(LifecycleEventsDispatcher::class) 76 | ->disableOriginalConstructor() 77 | ->getMock(); 78 | 79 | $this->manager = $this 80 | ->getMockBuilder(EntityManagerInterface::class) 81 | ->disableOriginalConstructor() 82 | ->getMock(); 83 | 84 | $this->classMetadata = $this 85 | ->getMockBuilder(ClassMetadata::class) 86 | ->disableOriginalConstructor() 87 | ->getMock(); 88 | ; 89 | 90 | $father = new ManyToOneAssociationMapping( 91 | 'father', 92 | PersonNoMonitor::class, 93 | PersonNoMonitor::class, 94 | ); 95 | $father->inversedBy = 'sons'; 96 | 97 | $sons = new OneToManyAssociationMapping( 98 | 'sons', 99 | PersonNoMonitor::class, 100 | PersonNoMonitor::class, 101 | ); 102 | $sons->mappedBy = 'father'; 103 | 104 | $friends = new ManyToManyOwningSideMapping( 105 | 'friends', 106 | PersonNoMonitor::class, 107 | PersonNoMonitor::class, 108 | ); 109 | $friends->inversedBy = 'friendOf'; 110 | 111 | $friendOf = new ManyToManyInverseSideMapping( 112 | 'friendOf', 113 | PersonNoMonitor::class, 114 | PersonNoMonitor::class, 115 | ); 116 | $friendOf->mappedBy = 'friends'; 117 | 118 | $mentor = new OneToOneOwningSideMapping( 119 | 'mentor', 120 | PersonNoMonitor::class, 121 | PersonNoMonitor::class, 122 | ); 123 | $mentor->inversedBy = 'mentoring'; 124 | 125 | $mentoring = new OneToOneInverseSideMapping( 126 | 'mentoring', 127 | PersonNoMonitor::class, 128 | PersonNoMonitor::class, 129 | ); 130 | $mentoring->mappedBy = 'mentor'; 131 | 132 | $this->mappings = [ 133 | 'father' => $father, 134 | 'sons' => $sons, 135 | 'friends' => $friends, 136 | 'friendOf' => $friendOf, 137 | 'mentor' => $mentor, 138 | 'mentoring' => $mentoring, 139 | ]; 140 | 141 | $this->classMetadata 142 | ->method('getAssociationMappings') 143 | ->willReturn($this->mappings); 144 | 145 | $this->classMetadata 146 | ->method('hasAssociation') 147 | ->willReturnCallback(function () { 148 | $field = func_get_arg(0); 149 | return in_array($field, array_keys($this->mappings)); 150 | }); 151 | 152 | $this->classMetadata 153 | ->method('getAssociationMapping') 154 | ->willReturnCallback(function () { 155 | $field = func_get_arg(0); 156 | return $this->mappings[$field]; 157 | }); 158 | 159 | $this->classMetadata 160 | ->method('getReflectionProperty') 161 | ->willReturnCallback(function () { 162 | $field = func_get_arg(0); 163 | return new \ReflectionProperty(PersonNoMonitor::class, $field); 164 | }); 165 | 166 | foreach (array_keys($this->mappings) as $field) { 167 | $this->classMetadata->reflFields[$field] = $this 168 | ->getMockBuilder(\ReflectionProperty::class) 169 | ->disableOriginalConstructor() 170 | ->setMethods(['getValue']) 171 | ->getMock(); 172 | } 173 | 174 | $this->classMetadata->expects($this->any()) 175 | ->method('isAssociationInverseSide') 176 | ->willReturnCallback(function () { 177 | $field = func_get_arg(0); 178 | switch ($field) { 179 | case 'mentoring': 180 | case 'friendOf': 181 | case 'sons': 182 | return true; 183 | default: 184 | return false; 185 | } 186 | }); 187 | $this->classMetadata 188 | ->method('isSingleValuedAssociation') 189 | ->willReturnCallback(function () { 190 | $field = func_get_arg(0); 191 | switch ($field) { 192 | case 'mentor': 193 | case 'mentoring': 194 | case 'father': 195 | return true; 196 | default: 197 | return false; 198 | } 199 | }); 200 | $this->classMetadata 201 | ->method('isCollectionValuedAssociation') 202 | ->willReturnCallback(function () { 203 | $field = func_get_arg(0); 204 | switch ($field) { 205 | case 'mentor': 206 | case 'mentoring': 207 | case 'father': 208 | return false; 209 | default: 210 | return true; 211 | } 212 | }); 213 | 214 | $this->listener = new LifecycleEventsListener($this->dispatcher, new AttributeGetter()); 215 | } 216 | 217 | public function testOneToOnePostPersist() 218 | { 219 | $event = new PostPersistEventArgs($this->person, $this->manager); 220 | 221 | $this->manager 222 | ->method('getClassMetadata') 223 | ->with($this->person::class) 224 | ->willReturn($this->classMetadata); 225 | 226 | $this->classMetadata->reflFields['mentor'] 227 | ->method('getValue') 228 | ->willReturn($this->mentor); 229 | 230 | $this->classMetadata 231 | ->method('getName') 232 | ->willReturn($this->person::class); 233 | 234 | $this->dispatcher->expects($this->never()) 235 | ->method('addUpdate'); 236 | 237 | $this->listener->postPersist($event); 238 | } 239 | 240 | public function testOneToOnePreRemove() 241 | { 242 | $event = new PreRemoveEventArgs($this->person, $this->manager); 243 | 244 | $this->manager 245 | ->method('getClassMetadata') 246 | ->with($this->person::class) 247 | ->willReturn($this->classMetadata); 248 | 249 | $this->classMetadata->reflFields['mentor'] 250 | ->method('getValue') 251 | ->willReturn($this->mentor); 252 | 253 | $this->classMetadata 254 | ->method('getName') 255 | ->willReturn($this->person::class); 256 | 257 | $this->dispatcher->expects($this->never()) 258 | ->method('addUpdate'); 259 | 260 | $this->listener->preRemove($event); 261 | } 262 | 263 | public function testOneToOnePreUpdate() 264 | { 265 | $uow = $this 266 | ->getMockBuilder(UnitOfWork::class) 267 | ->disableOriginalConstructor() 268 | ->setMethods(['getScheduledCollectionUpdates', 'getOwner', 'getMapping', 'getDeleteDiff', 'getInsertDiff']) 269 | ->getMock(); 270 | $this->manager->method('getUnitOfWork')->willReturn($uow); 271 | $uow->method('getScheduledCollectionUpdates')->willReturn([]); 272 | 273 | $this->manager 274 | ->method('getClassMetadata') 275 | ->with($this->person::class) 276 | ->willReturn($this->classMetadata); 277 | 278 | $this->classMetadata->reflFields['mentor'] 279 | ->method('getValue') 280 | ->willReturn($this->mentor); 281 | 282 | $this->classMetadata 283 | ->method('getName') 284 | ->willReturn($this->person::class); 285 | 286 | $this->dispatcher->expects($this->exactly(2)) 287 | ->method('addUpdate') 288 | ->withConsecutive( 289 | [ 290 | $this->callback(function ($arg) { 291 | return $arg instanceof Update; 292 | }), 293 | $this->equalTo($this->person), 294 | $this->callback(function ($arg) { 295 | return 296 | array_keys($arg) === ['mentor'] && 297 | $arg['mentor']['old'] === null && 298 | $arg['mentor']['new'] === $this->mentor; 299 | }), 300 | $this->equalTo([]) 301 | ], 302 | [ 303 | $this->callback(function ($arg) { 304 | return $arg instanceof Update; 305 | }), 306 | $this->equalTo($this->person), 307 | $this->callback(function ($arg) { 308 | return 309 | array_keys($arg) === ['mentor'] && 310 | $arg['mentor']['old'] === $this->mentor && 311 | $arg['mentor']['new'] === null; 312 | }), 313 | $this->equalTo([]) 314 | ] 315 | ); 316 | 317 | $changeSet = ['mentor' => [null, $this->mentor]]; 318 | $event = new PreUpdateEventArgs($this->person, $this->manager, $changeSet); 319 | 320 | $this->listener->preUpdate($event); 321 | 322 | $changeSet = ['mentor' => [$this->mentor, null]]; 323 | $event = new PreUpdateEventArgs($this->person, $this->manager, $changeSet); 324 | 325 | $this->listener->preUpdate($event); 326 | } 327 | 328 | 329 | public function testOneToManyPostPersist() 330 | { 331 | $event = new PostPersistEventArgs($this->person, $this->manager); 332 | 333 | $this->manager 334 | ->method('getClassMetadata') 335 | ->with($this->person::class) 336 | ->willReturn($this->classMetadata); 337 | 338 | $this->classMetadata->reflFields['father'] 339 | ->method('getValue') 340 | ->willReturn($this->father); 341 | 342 | $this->classMetadata 343 | ->method('getName') 344 | ->willReturn($this->person::class); 345 | 346 | $this->dispatcher->expects($this->never()) 347 | ->method('addUpdate'); 348 | 349 | $this->listener->postPersist($event); 350 | } 351 | 352 | public function testOneToManyPreRemove() 353 | { 354 | $event = new PreRemoveEventArgs($this->person, $this->manager); 355 | 356 | $this->manager 357 | ->method('getClassMetadata') 358 | ->with($this->person::class) 359 | ->willReturn($this->classMetadata); 360 | 361 | $this->classMetadata->reflFields['father'] 362 | ->method('getValue') 363 | ->willReturn($this->mentor); 364 | 365 | $this->classMetadata 366 | ->method('getName') 367 | ->willReturn($this->person::class); 368 | 369 | $this->dispatcher->expects($this->never()) 370 | ->method('addUpdate'); 371 | 372 | $this->listener->preRemove($event); 373 | } 374 | 375 | public function testOneToManyPreUpdate() 376 | { 377 | $uow = $this 378 | ->getMockBuilder(UnitOfWork::class) 379 | ->disableOriginalConstructor() 380 | ->setMethods(['getScheduledCollectionUpdates', 'getOwner', 'getMapping', 'getDeleteDiff', 'getInsertDiff']) 381 | ->getMock(); 382 | $this->manager->method('getUnitOfWork')->willReturn($uow); 383 | $uow->method('getScheduledCollectionUpdates')->willReturn([]); 384 | 385 | $this->manager 386 | ->method('getClassMetadata') 387 | ->with($this->person::class) 388 | ->willReturn($this->classMetadata); 389 | 390 | $this->classMetadata 391 | ->method('getName') 392 | ->willReturn($this->person::class); 393 | 394 | $this->dispatcher->expects($this->exactly(2)) 395 | ->method('addUpdate') 396 | ->withConsecutive( 397 | [ 398 | $this->callback(function ($arg) { 399 | return $arg instanceof Update; 400 | }), 401 | $this->equalTo($this->person), 402 | $this->callback(function ($arg) { 403 | return 404 | array_keys($arg) === ['father'] && 405 | $arg['father']['old'] === null && 406 | $arg['father']['new'] === $this->father; 407 | }), 408 | $this->equalTo([]) 409 | ], 410 | [ 411 | $this->callback(function ($arg) { 412 | return $arg instanceof Update; 413 | }), 414 | $this->equalTo($this->person), 415 | $this->callback(function ($arg) { 416 | return 417 | array_keys($arg) === ['father'] && 418 | $arg['father']['old'] === $this->father && 419 | $arg['father']['new'] === null; 420 | }), 421 | $this->equalTo([]) 422 | ] 423 | ); 424 | 425 | $changeSet = ['father' => [null, $this->father]]; 426 | $event = new PreUpdateEventArgs($this->person, $this->manager, $changeSet); 427 | 428 | $this->listener->preUpdate($event); 429 | 430 | $changeSet = ['father' => [$this->father, null]]; 431 | $event = new PreUpdateEventArgs($this->person, $this->manager, $changeSet); 432 | 433 | $this->listener->preUpdate($event); 434 | } 435 | 436 | public function testManyToManyPostPersist() 437 | { 438 | $event = new PostPersistEventArgs($this->person, $this->manager); 439 | 440 | $this->manager 441 | ->method('getClassMetadata') 442 | ->with($this->person::class) 443 | ->willReturn($this->classMetadata); 444 | 445 | $this->classMetadata->reflFields['friends'] 446 | ->method('getValue') 447 | ->willReturn(new ArrayCollection([$this->friend1, $this->friend2])); 448 | 449 | $this->classMetadata 450 | ->method('getName') 451 | ->willReturn($this->person::class); 452 | 453 | $this->dispatcher->expects($this->never()) 454 | ->method('addUpdate'); 455 | 456 | $this->listener->postPersist($event); 457 | } 458 | 459 | public function testManyToManyPreRemove() 460 | { 461 | $event = new PreRemoveEventArgs($this->person, $this->manager); 462 | 463 | $this->manager 464 | ->method('getClassMetadata') 465 | ->with($this->person::class) 466 | ->willReturn($this->classMetadata); 467 | 468 | $this->classMetadata->reflFields['friends'] 469 | ->method('getValue') 470 | ->willReturn(new ArrayCollection([$this->friend1, $this->friend2])); 471 | 472 | $this->classMetadata 473 | ->method('getName') 474 | ->willReturn($this->person::class); 475 | 476 | $this->dispatcher->expects($this->never()) 477 | ->method('addUpdate'); 478 | 479 | $this->listener->preRemove($event); 480 | } 481 | 482 | public function testManyToManyPreUpdate() 483 | { 484 | $uow = $this 485 | ->getMockBuilder(UnitOfWork::class) 486 | ->disableOriginalConstructor() 487 | ->setMethods(['getScheduledCollectionUpdates', 'getOwner', 'getMapping', 'getDeleteDiff', 'getInsertDiff']) 488 | ->getMock(); 489 | $this->manager->method('getUnitOfWork')->willReturn($uow); 490 | $uow->method('getScheduledCollectionUpdates')->willReturn([$uow]); 491 | $uow->method('getOwner')->willReturn($this->person); 492 | $uow->method('getMapping')->willReturn(['fieldName' => 'friends']); 493 | $deleted = []; 494 | $uow->method('getDeleteDiff')->willReturn($deleted); 495 | $inserted = [$this->friend1, $this->friend2]; 496 | $uow->method('getInsertDiff')->willReturn($inserted); 497 | 498 | $this->manager 499 | ->method('getClassMetadata') 500 | ->with($this->person::class) 501 | ->willReturn($this->classMetadata); 502 | 503 | $this->classMetadata 504 | ->method('getName') 505 | ->willReturn($this->person::class); 506 | 507 | $this->dispatcher->expects($this->exactly(1)) 508 | ->method('addUpdate') 509 | ->withConsecutive( 510 | [ 511 | $this->callback(function ($arg) { 512 | return $arg instanceof Update; 513 | }), 514 | $this->equalTo($this->person), 515 | $this->equalTo([]), 516 | $this->callback(function ($arg) { 517 | return 518 | array_keys($arg) === ['friends'] && 519 | $arg['friends']['deleted'] === [] && 520 | $arg['friends']['inserted'] === [$this->friend1, $this->friend2]; 521 | }) 522 | ] 523 | ); 524 | 525 | $changeSet = []; 526 | $event = new PreUpdateEventArgs($this->person, $this->manager, $changeSet); 527 | 528 | $this->listener->preUpdate($event); 529 | } 530 | 531 | public function testManyToManyRemovePreUpdate() 532 | { 533 | $uow = $this 534 | ->getMockBuilder(UnitOfWork::class) 535 | ->disableOriginalConstructor() 536 | ->setMethods(['getScheduledCollectionUpdates', 'getOwner', 'getMapping', 'getDeleteDiff', 'getInsertDiff']) 537 | ->getMock(); 538 | $this->manager->method('getUnitOfWork')->willReturn($uow); 539 | $uow->method('getScheduledCollectionUpdates')->willReturn([$uow]); 540 | $uow->method('getOwner')->willReturn($this->person); 541 | $uow->method('getMapping')->willReturn(['fieldName' => 'friends']); 542 | $deleted = [$this->friend1]; 543 | $uow->method('getDeleteDiff')->willReturn($deleted); 544 | $inserted = []; 545 | $uow->method('getInsertDiff')->willReturn($inserted); 546 | 547 | $this->manager 548 | ->method('getClassMetadata') 549 | ->with($this->person::class) 550 | ->willReturn($this->classMetadata); 551 | 552 | $this->classMetadata 553 | ->method('getName') 554 | ->willReturn($this->person::class); 555 | 556 | $this->dispatcher->expects($this->exactly(1)) 557 | ->method('addUpdate') 558 | ->withConsecutive( 559 | [ 560 | $this->callback(function ($arg) { 561 | return $arg instanceof Update; 562 | }), 563 | $this->equalTo($this->person), 564 | $this->equalTo([]), 565 | $this->callback(function ($arg) { 566 | return 567 | array_keys($arg) === ['friends'] && 568 | $arg['friends']['deleted'] === [$this->friend1] && 569 | $arg['friends']['inserted'] === []; 570 | }) 571 | ] 572 | ); 573 | 574 | $changeSet = []; 575 | $event = new PreUpdateEventArgs($this->person, $this->manager, $changeSet); 576 | 577 | $this->listener->preUpdate($event); 578 | } 579 | } 580 | -------------------------------------------------------------------------------- /Tests/EventListener/LifecycleEventsListenerInverseTest.php: -------------------------------------------------------------------------------- 1 | 32 | */ 33 | class LifecycleEventsListenerInverseTest extends TestCase 34 | { 35 | /** 36 | * @var LifecycleEventsListener 37 | */ 38 | private $listener; 39 | 40 | /** 41 | * @var LifecycleEventsDispatcher|MockObject 42 | */ 43 | private $dispatcher; 44 | 45 | /** 46 | * @var EntityManagerInterface|MockObject 47 | */ 48 | private $manager; 49 | 50 | /** 51 | * @var ClassMetadata|MockObject 52 | */ 53 | private $classMetadata; 54 | 55 | /** 56 | * @var array 57 | */ 58 | private $mappings; 59 | 60 | private $person; 61 | private $mentor; 62 | private $father; 63 | private $friend1; 64 | private $friend2; 65 | 66 | public function setUp() : void 67 | { 68 | parent::setUp(); 69 | 70 | $this->person = new Person(); 71 | $this->mentor = new Person(); 72 | $this->father = new Person(); 73 | $this->friend1 = new Person(); 74 | $this->friend2 = new Person(); 75 | 76 | $this->dispatcher = $this 77 | ->getMockBuilder(LifecycleEventsDispatcher::class) 78 | ->disableOriginalConstructor() 79 | ->getMock(); 80 | 81 | $this->manager = $this 82 | ->getMockBuilder(EntityManagerInterface::class) 83 | ->disableOriginalConstructor() 84 | ->getMock(); 85 | 86 | $this->classMetadata = $this 87 | ->getMockBuilder(ClassMetadata::class) 88 | ->disableOriginalConstructor() 89 | ->getMock(); 90 | 91 | $father = new ManyToOneAssociationMapping( 92 | 'father', 93 | Person::class, 94 | Person::class, 95 | ); 96 | $father->inversedBy = 'sons'; 97 | 98 | $sons = new OneToManyAssociationMapping( 99 | 'sons', 100 | Person::class, 101 | Person::class, 102 | ); 103 | $sons->mappedBy = 'father'; 104 | 105 | $friends = new ManyToManyOwningSideMapping( 106 | 'friends', 107 | Person::class, 108 | Person::class, 109 | ); 110 | $friends->inversedBy = 'friendOf'; 111 | 112 | $friendOf = new ManyToManyInverseSideMapping( 113 | 'friendOf', 114 | Person::class, 115 | Person::class, 116 | ); 117 | $friendOf->mappedBy = 'friends'; 118 | 119 | $mentor = new OneToOneOwningSideMapping( 120 | 'mentor', 121 | Person::class, 122 | Person::class, 123 | ); 124 | $mentor->inversedBy = 'mentoring'; 125 | 126 | $mentoring = new OneToOneInverseSideMapping( 127 | 'mentoring', 128 | PersonNoMonitor::class, 129 | PersonNoMonitor::class, 130 | ); 131 | $mentoring->mappedBy = 'mentor'; 132 | 133 | $this->mappings = [ 134 | 'father' => $father, 135 | 'sons' => $sons, 136 | 'friends' => $friends, 137 | 'friendOf' => $friendOf, 138 | 'mentor' => $mentor, 139 | 'mentoring' => $mentoring, 140 | ]; 141 | 142 | $this->classMetadata 143 | ->method('getAssociationMappings') 144 | ->willReturn($this->mappings); 145 | 146 | $this->classMetadata 147 | ->method('hasAssociation') 148 | ->willReturnCallback(function () { 149 | $field = func_get_arg(0); 150 | return in_array($field, array_keys($this->mappings)); 151 | }); 152 | 153 | $this->classMetadata 154 | ->method('getAssociationMapping') 155 | ->willReturnCallback(function () { 156 | $field = func_get_arg(0); 157 | return $this->mappings[$field]; 158 | }); 159 | 160 | $this->classMetadata 161 | ->method('getReflectionProperty') 162 | ->willReturnCallback(function () { 163 | $field = func_get_arg(0); 164 | return new \ReflectionProperty(Person::class, $field); 165 | }); 166 | 167 | foreach (array_keys($this->mappings) as $field) { 168 | $this->classMetadata->reflFields[$field] = $this 169 | ->getMockBuilder(\ReflectionProperty::class) 170 | ->disableOriginalConstructor() 171 | ->setMethods(['getValue']) 172 | ->getMock(); 173 | } 174 | 175 | $this->classMetadata->expects($this->any()) 176 | ->method('isAssociationInverseSide') 177 | ->willReturnCallback(function () { 178 | $field = func_get_arg(0); 179 | switch ($field) { 180 | case 'mentoring': 181 | case 'friendOf': 182 | case 'sons': 183 | return true; 184 | default: 185 | return false; 186 | } 187 | }); 188 | $this->classMetadata 189 | ->method('isSingleValuedAssociation') 190 | ->willReturnCallback(function () { 191 | $field = func_get_arg(0); 192 | switch ($field) { 193 | case 'mentor': 194 | case 'mentoring': 195 | case 'father': 196 | return true; 197 | default: 198 | return false; 199 | } 200 | }); 201 | $this->classMetadata 202 | ->method('isCollectionValuedAssociation') 203 | ->willReturnCallback(function () { 204 | $field = func_get_arg(0); 205 | switch ($field) { 206 | case 'mentor': 207 | case 'mentoring': 208 | case 'father': 209 | return false; 210 | default: 211 | return true; 212 | } 213 | }); 214 | 215 | $this->listener = new LifecycleEventsListener($this->dispatcher, new AttributeGetter()); 216 | } 217 | 218 | public function testOneToOnePostPersist() 219 | { 220 | $event = new PostPersistEventArgs($this->person, $this->manager); 221 | 222 | $this->manager 223 | ->method('getClassMetadata') 224 | ->with($this->person::class) 225 | ->willReturn($this->classMetadata); 226 | 227 | $this->classMetadata 228 | ->method('getName') 229 | ->willReturn($this->person::class); 230 | 231 | $this->classMetadata->reflFields['mentor'] 232 | ->method('getValue') 233 | ->willReturn($this->mentor); 234 | 235 | $this->dispatcher->expects($this->exactly(1)) 236 | ->method('addUpdate') 237 | ->with( 238 | $this->callback(function ($arg) { return $arg instanceof Update; }), 239 | $this->callback(function ($arg) { return $arg === $this->mentor; }), 240 | $this->callback(function ($arg) { 241 | return 242 | array_keys($arg) === ['mentoring'] && 243 | $arg['mentoring']['old'] === null && 244 | $arg['mentoring']['new'] === $this->person; 245 | }), 246 | $this->equalTo([])); 247 | 248 | $this->dispatcher->expects($this->exactly(1)) 249 | ->method('addPropertyChange') 250 | ->with( 251 | $this->callback(function ($arg) { return $arg instanceof Change; }), 252 | $this->callback(function ($arg) { return $arg === $this->mentor; }), 253 | $this->equalTo('mentoring'), 254 | $this->equalTo(null), 255 | $this->callback(function ($arg) { return $arg === $this->person; }) 256 | ); 257 | 258 | $this->listener->postPersist($event); 259 | } 260 | 261 | public function testOneToOnePreRemove() 262 | { 263 | $event = new PreRemoveEventArgs($this->person, $this->manager); 264 | 265 | $this->manager 266 | ->method('getClassMetadata') 267 | ->with($this->person::class) 268 | ->willReturn($this->classMetadata); 269 | 270 | $this->classMetadata 271 | ->method('getName') 272 | ->willReturn($this->person::class); 273 | 274 | $this->classMetadata->reflFields['mentor'] 275 | ->method('getValue') 276 | ->willReturn($this->mentor); 277 | 278 | $this->dispatcher->expects($this->exactly(1)) 279 | ->method('addUpdate') 280 | ->with( 281 | $this->callback(function ($arg) { return $arg instanceof Update; }), 282 | $this->callback(function ($arg) { return $arg === $this->mentor; }), 283 | $this->callback(function ($arg) { 284 | return 285 | array_keys($arg) === ['mentoring'] && 286 | $arg['mentoring']['old'] === $this->person && 287 | $arg['mentoring']['new'] === null; 288 | }), 289 | $this->equalTo([])); 290 | 291 | $this->dispatcher->expects($this->exactly(1)) 292 | ->method('addPropertyChange') 293 | ->with( 294 | $this->callback(function ($arg) { return $arg instanceof Change; }), 295 | $this->callback(function ($arg) { return $arg === $this->mentor; }), 296 | $this->equalTo('mentoring'), 297 | $this->callback(function ($arg) { return $arg === $this->person; }), 298 | $this->equalTo(null) 299 | ); 300 | 301 | $this->listener->preRemove($event); 302 | } 303 | 304 | public function testOneToOnePreUpdate() 305 | { 306 | $uow = $this 307 | ->getMockBuilder(UnitOfWork::class) 308 | ->disableOriginalConstructor() 309 | ->setMethods(['getScheduledCollectionUpdates', 'getOwner', 'getMapping', 'getDeleteDiff', 'getInsertDiff']) 310 | ->getMock(); 311 | $this->manager->method('getUnitOfWork')->willReturn($uow); 312 | $uow->method('getScheduledCollectionUpdates')->willReturn([]); 313 | 314 | $this->manager 315 | ->method('getClassMetadata') 316 | ->with($this->person::class) 317 | ->willReturn($this->classMetadata); 318 | 319 | $this->classMetadata 320 | ->method('getName') 321 | ->willReturn($this->person::class); 322 | 323 | $this->classMetadata->reflFields['mentor'] 324 | ->method('getValue') 325 | ->willReturn($this->mentor); 326 | 327 | $this->dispatcher->expects($this->exactly(4)) 328 | ->method('addUpdate') 329 | ->withConsecutive( 330 | [ 331 | $this->callback(function ($arg) { return $arg instanceof Update; }), 332 | $this->callback(function ($arg) { return $arg === $this->mentor; }), 333 | $this->callback(function ($arg) { 334 | return 335 | array_keys($arg) === ['mentoring'] && 336 | $arg['mentoring']['old'] === null && 337 | $arg['mentoring']['new'] === $this->person; 338 | }), 339 | $this->equalTo([]) 340 | ], 341 | [ 342 | $this->callback(function ($arg) { return $arg instanceof Update; }), 343 | $this->callback(function ($arg) { return $arg === $this->person; }), 344 | $this->callback(function ($arg) { 345 | return 346 | array_keys($arg) === ['mentor'] && 347 | $arg['mentor']['old'] === null && 348 | $arg['mentor']['new'] === $this->mentor; 349 | }), 350 | $this->equalTo([]) 351 | ], 352 | [ 353 | $this->callback(function ($arg) { return $arg instanceof Update; }), 354 | $this->callback(function ($arg) { return $arg === $this->mentor; }), 355 | $this->callback(function ($arg) { 356 | return 357 | array_keys($arg) === ['mentoring'] && 358 | $arg['mentoring']['old'] === $this->person && 359 | $arg['mentoring']['new'] === null; 360 | }), 361 | $this->equalTo([]) 362 | ], 363 | [ 364 | $this->callback(function ($arg) { return $arg instanceof Update; }), 365 | $this->callback(function ($arg) { return $arg === $this->person; }), 366 | 367 | $this->callback(function ($arg) { 368 | return 369 | array_keys($arg) === ['mentor'] && 370 | $arg['mentor']['old'] === $this->mentor && 371 | $arg['mentor']['new'] === null; 372 | }), 373 | $this->equalTo([]) 374 | ] 375 | ); 376 | 377 | $this->dispatcher->expects($this->exactly(2)) 378 | ->method('addPropertyChange') 379 | ->withConsecutive( 380 | [ 381 | $this->callback(function ($arg) { return $arg instanceof Change; }), 382 | $this->callback(function ($arg) { return $arg === $this->mentor; }), 383 | $this->equalTo('mentoring'), 384 | $this->equalTo(null), 385 | $this->callback(function ($arg) { return $arg === $this->person; }) 386 | ], 387 | [ 388 | $this->callback(function ($arg) { return $arg instanceof Change; }), 389 | $this->callback(function ($arg) { return $arg === $this->mentor; }), 390 | $this->equalTo('mentoring'), 391 | $this->callback(function ($arg) { return $arg === $this->person; }), 392 | $this->equalTo(null) 393 | ] 394 | ) 395 | ; 396 | 397 | $changeSet = ['mentor' => [null, $this->mentor]]; 398 | $event = new PreUpdateEventArgs($this->person, $this->manager, $changeSet); 399 | 400 | $this->listener->preUpdate($event); 401 | 402 | $changeSet = ['mentor' => [$this->mentor, null]]; 403 | $event = new PreUpdateEventArgs($this->person, $this->manager, $changeSet); 404 | 405 | $this->listener->preUpdate($event); 406 | } 407 | 408 | 409 | public function testOneToManyPostPersist() 410 | { 411 | $event = new PostPersistEventArgs($this->person, $this->manager); 412 | 413 | $this->manager 414 | ->method('getClassMetadata') 415 | ->with($this->person::class) 416 | ->willReturn($this->classMetadata); 417 | 418 | $this->classMetadata 419 | ->method('getName') 420 | ->willReturn($this->person::class); 421 | 422 | $this->classMetadata->reflFields['father'] 423 | ->method('getValue') 424 | ->willReturn($this->father); 425 | 426 | $this->dispatcher->expects($this->exactly(1)) 427 | ->method('addUpdate') 428 | ->with( 429 | $this->callback(function ($arg) { return $arg instanceof Update; }), 430 | $this->callback(function ($arg) { return $arg === $this->father; }), 431 | $this->equalTo([]), 432 | $this->callback(function ($arg) { 433 | return 434 | array_keys($arg) === ['sons'] && 435 | $arg['sons']['deleted'] === [] && 436 | $arg['sons']['inserted'] === [$this->person]; 437 | })); 438 | 439 | $this->dispatcher->expects($this->exactly(1)) 440 | ->method('addCollectionChange') 441 | ->with( 442 | $this->callback(function ($arg) { return $arg instanceof Change; }), 443 | $this->callback(function ($arg) { return $arg === $this->father; }), 444 | $this->equalTo('sons'), 445 | $this->equalTo([]), 446 | $this->callback(function ($arg) { return $arg === [$this->person]; })); 447 | 448 | $this->listener->postPersist($event); 449 | } 450 | 451 | public function testOneToManyPreRemove() 452 | { 453 | $event = new PreRemoveEventArgs($this->person, $this->manager); 454 | 455 | $this->manager 456 | ->method('getClassMetadata') 457 | ->with($this->person::class) 458 | ->willReturn($this->classMetadata); 459 | 460 | $this->classMetadata 461 | ->method('getName') 462 | ->willReturn($this->person::class); 463 | 464 | $this->classMetadata->reflFields['father'] 465 | ->method('getValue') 466 | ->willReturn($this->father); 467 | 468 | $this->dispatcher->expects($this->exactly(1)) 469 | ->method('addUpdate') 470 | ->with( 471 | $this->callback(function ($arg) { return $arg instanceof Update; }), 472 | $this->callback(function ($arg) { return $arg === $this->father; }), 473 | $this->equalTo([]), 474 | $this->callback(function ($arg) { 475 | return 476 | array_keys($arg) === ['sons'] && 477 | $arg['sons']['deleted'] === [$this->person] && 478 | $arg['sons']['inserted'] === []; 479 | })); 480 | 481 | $this->dispatcher->expects($this->exactly(1)) 482 | ->method('addCollectionChange') 483 | ->with( 484 | $this->callback(function ($arg) { return $arg instanceof Change; }), 485 | $this->callback(function ($arg) { return $arg === $this->father; }), 486 | $this->equalTo('sons'), 487 | $this->callback(function ($arg) { return $arg === [$this->person]; }), 488 | $this->equalTo([])); 489 | 490 | $this->listener->preRemove($event); 491 | } 492 | 493 | public function testOneToManyPreUpdate() 494 | { 495 | $uow = $this 496 | ->getMockBuilder(UnitOfWork::class) 497 | ->disableOriginalConstructor() 498 | ->setMethods(['getScheduledCollectionUpdates', 'getOwner', 'getMapping', 'getDeleteDiff', 'getInsertDiff']) 499 | ->getMock(); 500 | $this->manager->method('getUnitOfWork')->willReturn($uow); 501 | $uow->method('getScheduledCollectionUpdates')->willReturn([]); 502 | 503 | $this->manager 504 | ->method('getClassMetadata') 505 | ->with($this->person::class) 506 | ->willReturn($this->classMetadata); 507 | 508 | $this->classMetadata 509 | ->method('getName') 510 | ->willReturn($this->person::class); 511 | 512 | $this->dispatcher->expects($this->exactly(4)) 513 | ->method('addUpdate') 514 | ->withConsecutive( 515 | [ 516 | $this->callback(function ($arg) { return $arg instanceof Update; }), 517 | $this->callback(function ($arg) { return $arg === $this->father; }), 518 | $this->equalTo([]), 519 | $this->callback(function ($arg) { 520 | return 521 | array_keys($arg) === ['sons'] && 522 | $arg['sons']['deleted'] === [] && 523 | $arg['sons']['inserted'] === [$this->person]; 524 | }) 525 | ], 526 | [ 527 | $this->callback(function ($arg) { return $arg instanceof Update; }), 528 | $this->callback(function ($arg) { return $arg === $this->person; }), 529 | $this->callback(function ($arg) { 530 | return 531 | array_keys($arg) === ['father'] && 532 | $arg['father']['old'] === null && 533 | $arg['father']['new'] === $this->father; 534 | }), 535 | $this->equalTo([]) 536 | ], 537 | [ 538 | $this->callback(function ($arg) { return $arg instanceof Update; }), 539 | $this->callback(function ($arg) { return $arg === $this->father; }), 540 | $this->equalTo([]), 541 | $this->callback(function ($arg) { 542 | return 543 | array_keys($arg) === ['sons'] && 544 | $arg['sons']['deleted'] === [$this->person] && 545 | $arg['sons']['inserted'] === []; 546 | }) 547 | ], 548 | [ 549 | $this->callback(function ($arg) { return $arg instanceof Update; }), 550 | $this->callback(function ($arg) { return $arg === $this->person; }), 551 | $this->callback(function ($arg) { 552 | return 553 | array_keys($arg) === ['father'] && 554 | $arg['father']['old'] === $this->father && 555 | $arg['father']['new'] === null; 556 | }), 557 | $this->equalTo([]) 558 | ] 559 | ); 560 | 561 | $this->dispatcher->expects($this->exactly(2)) 562 | ->method('addCollectionChange') 563 | ->withConsecutive( 564 | [ 565 | $this->callback(function ($arg) { return $arg instanceof Change; }), 566 | $this->callback(function ($arg) { return $arg === $this->father; }), 567 | $this->equalTo('sons'), 568 | $this->equalTo([]), 569 | $this->callback(function ($arg) { return $arg === [$this->person]; }) 570 | ], 571 | [ 572 | $this->callback(function ($arg) { return $arg instanceof Change; }), 573 | $this->callback(function ($arg) { return $arg === $this->father; }), 574 | $this->equalTo('sons'), 575 | $this->callback(function ($arg) { return $arg === [$this->person]; }), 576 | $this->equalTo([]) 577 | ] 578 | ); 579 | 580 | $changeSet = ['father' => [null, $this->father]]; 581 | $event = new PreUpdateEventArgs($this->person, $this->manager, $changeSet); 582 | 583 | $this->listener->preUpdate($event); 584 | 585 | $changeSet = ['father' => [$this->father, null]]; 586 | $event = new PreUpdateEventArgs($this->person, $this->manager, $changeSet); 587 | 588 | $this->listener->preUpdate($event); 589 | } 590 | 591 | public function testManyToManyPostPersist() 592 | { 593 | $event = new PostPersistEventArgs($this->person, $this->manager); 594 | 595 | $this->manager 596 | ->method('getClassMetadata') 597 | ->with($this->person::class) 598 | ->willReturn($this->classMetadata); 599 | 600 | $this->classMetadata 601 | ->method('getName') 602 | ->willReturn($this->person::class); 603 | 604 | $this->classMetadata->reflFields['friends'] 605 | ->method('getValue') 606 | ->willReturn(new ArrayCollection([$this->friend1, $this->friend2])); 607 | 608 | $this->dispatcher->expects($this->exactly(2)) 609 | ->method('addUpdate') 610 | ->withConsecutive( 611 | [ 612 | $this->callback(function ($arg) { return $arg instanceof Update; }), 613 | $this->callback(function ($arg) { return $arg === $this->friend1; }), 614 | $this->equalTo([]), 615 | $this->callback(function ($arg) { 616 | return 617 | array_keys($arg) === ['friendOf'] && 618 | $arg['friendOf']['deleted'] === [] && 619 | $arg['friendOf']['inserted'] === [$this->person]; 620 | }) 621 | ], 622 | [ 623 | $this->callback(function ($arg) { return $arg instanceof Update; }), 624 | $this->callback(function ($arg) { return $arg === $this->friend2; }), 625 | $this->equalTo([]), 626 | $this->callback(function ($arg) { 627 | return 628 | array_keys($arg) === ['friendOf'] && 629 | $arg['friendOf']['deleted'] === [] && 630 | $arg['friendOf']['inserted'] === [$this->person]; 631 | }) 632 | ]); 633 | 634 | $this->dispatcher->expects($this->exactly(2)) 635 | ->method('addCollectionChange') 636 | ->withConsecutive( 637 | [ 638 | $this->callback(function ($arg) { return $arg instanceof Change; }), 639 | $this->callback(function ($arg) { return $arg === $this->friend1; }), 640 | $this->equalTo('friendOf'), 641 | $this->equalTo([]), 642 | $this->callback(function ($arg) { return $arg === [$this->person]; }) 643 | ], 644 | [ 645 | $this->callback(function ($arg) { return $arg instanceof Change; }), 646 | $this->callback(function ($arg) { return $arg === $this->friend2; }), 647 | $this->equalTo('friendOf'), 648 | $this->equalTo([]), 649 | $this->callback(function ($arg) { return $arg === [$this->person]; }) 650 | ] 651 | ); 652 | 653 | $this->listener->postPersist($event); 654 | } 655 | 656 | public function testManyToManyPreRemove() 657 | { 658 | $event = new PreRemoveEventArgs($this->person, $this->manager); 659 | 660 | $this->manager 661 | ->method('getClassMetadata') 662 | ->with($this->person::class) 663 | ->willReturn($this->classMetadata); 664 | 665 | $this->classMetadata 666 | ->method('getName') 667 | ->willReturn($this->person::class); 668 | 669 | $this->classMetadata->reflFields['friends'] 670 | ->method('getValue') 671 | ->willReturn(new ArrayCollection([$this->friend1, $this->friend2])); 672 | 673 | $this->dispatcher->expects($this->exactly(2)) 674 | ->method('addUpdate') 675 | ->withConsecutive( 676 | [ 677 | $this->callback(function ($arg) { return $arg instanceof Update; }), 678 | $this->callback(function ($arg) { return $arg === $this->friend1; }), 679 | $this->equalTo([]), 680 | $this->callback(function ($arg) { 681 | return 682 | array_keys($arg) === ['friendOf'] && 683 | $arg['friendOf']['deleted'] === [$this->person] && 684 | $arg['friendOf']['inserted'] === []; 685 | }) 686 | ], 687 | [ 688 | $this->callback(function ($arg) { return $arg instanceof Update; }), 689 | $this->callback(function ($arg) { return $arg === $this->friend2; }), 690 | $this->equalTo([]), 691 | $this->callback(function ($arg) { 692 | return 693 | array_keys($arg) === ['friendOf'] && 694 | $arg['friendOf']['deleted'] === [$this->person] && 695 | $arg['friendOf']['inserted'] === []; 696 | }) 697 | ]); 698 | 699 | $this->dispatcher->expects($this->exactly(2)) 700 | ->method('addCollectionChange') 701 | ->withConsecutive( 702 | [ 703 | $this->callback(function ($arg) { return $arg instanceof Change; }), 704 | $this->callback(function ($arg) { return $arg === $this->friend1; }), 705 | $this->equalTo('friendOf'), 706 | $this->callback(function ($arg) { return $arg === [$this->person]; }), 707 | $this->equalTo([]) 708 | ], 709 | [ 710 | $this->callback(function ($arg) { return $arg instanceof Change; }), 711 | $this->callback(function ($arg) { return $arg === $this->friend2; }), 712 | $this->equalTo('friendOf'), 713 | $this->callback(function ($arg) { return $arg === [$this->person]; }), 714 | $this->equalTo([]) 715 | ] 716 | ); 717 | 718 | $this->listener->preRemove($event); 719 | } 720 | 721 | public function testManyToManyPreUpdate() 722 | { 723 | $uow = $this 724 | ->getMockBuilder(UnitOfWork::class) 725 | ->disableOriginalConstructor() 726 | ->setMethods(['getScheduledCollectionUpdates', 'getOwner', 'getMapping', 'getDeleteDiff', 'getInsertDiff']) 727 | ->getMock(); 728 | $this->manager->method('getUnitOfWork')->willReturn($uow); 729 | $uow->method('getScheduledCollectionUpdates')->willReturn([$uow]); 730 | $uow->method('getOwner')->willReturn($this->person); 731 | $uow->method('getMapping')->willReturn(['fieldName' => 'friends']); 732 | $deleted = []; 733 | $uow->method('getDeleteDiff')->willReturn($deleted); 734 | $inserted = [$this->friend1, $this->friend2]; 735 | $uow->method('getInsertDiff')->willReturn($inserted); 736 | 737 | $this->manager 738 | ->method('getClassMetadata') 739 | ->with($this->person::class) 740 | ->willReturn($this->classMetadata); 741 | 742 | $this->classMetadata 743 | ->method('getName') 744 | ->willReturn($this->person::class); 745 | 746 | $this->dispatcher->expects($this->exactly(3)) 747 | ->method('addUpdate') 748 | ->withConsecutive( 749 | [ 750 | $this->callback(function ($arg) { return $arg instanceof Update; }), 751 | $this->callback(function ($arg) { return $arg === $this->friend1; }), 752 | $this->equalTo([]), 753 | $this->callback(function ($arg) { 754 | return 755 | array_keys($arg) === ['friendOf'] && 756 | $arg['friendOf']['deleted'] === [] && 757 | $arg['friendOf']['inserted'] === [$this->person]; 758 | }) 759 | ], 760 | [ 761 | $this->callback(function ($arg) { return $arg instanceof Update; }), 762 | $this->callback(function ($arg) { return $arg === $this->friend2; }), 763 | $this->equalTo([]), 764 | $this->callback(function ($arg) { 765 | return 766 | array_keys($arg) === ['friendOf'] && 767 | $arg['friendOf']['deleted'] === [] && 768 | $arg['friendOf']['inserted'] === [$this->person]; 769 | }) 770 | ], 771 | [ 772 | $this->callback(function ($arg) { return $arg instanceof Update; }), 773 | $this->callback(function ($arg) { return $arg === $this->person; }), 774 | $this->equalTo([]), 775 | $this->callback(function ($arg) { 776 | return 777 | array_keys($arg) === ['friends'] && 778 | $arg['friends']['deleted'] === [] && 779 | $arg['friends']['inserted'] === [$this->friend1, $this->friend2]; 780 | }) 781 | ] 782 | ); 783 | 784 | $this->dispatcher->expects($this->exactly(2)) 785 | ->method('addCollectionChange') 786 | ->withConsecutive( 787 | [ 788 | $this->callback(function ($arg) { return $arg instanceof Change; }), 789 | $this->callback(function ($arg) { return $arg === $this->friend1; }), 790 | $this->equalTo('friendOf'), 791 | $this->equalTo([]), 792 | $this->callback(function ($arg) { return $arg === [$this->person]; }) 793 | ], 794 | [ 795 | $this->callback(function ($arg) { return $arg instanceof Change; }), 796 | $this->callback(function ($arg) { return $arg === $this->friend2; }), 797 | $this->equalTo('friendOf'), 798 | $this->equalTo([]), 799 | $this->callback(function ($arg) { return $arg === [$this->person]; }) 800 | ] 801 | ); 802 | 803 | $changeSet = []; 804 | $event = new PreUpdateEventArgs($this->person, $this->manager, $changeSet); 805 | 806 | $this->listener->preUpdate($event); 807 | } 808 | 809 | public function testManyToManyRemovePreUpdate() 810 | { 811 | $uow = $this 812 | ->getMockBuilder(UnitOfWork::class) 813 | ->disableOriginalConstructor() 814 | ->setMethods(['getScheduledCollectionUpdates', 'getOwner', 'getMapping', 'getDeleteDiff', 'getInsertDiff']) 815 | ->getMock(); 816 | $this->manager->method('getUnitOfWork')->willReturn($uow); 817 | $uow->method('getScheduledCollectionUpdates')->willReturn([$uow]); 818 | $uow->method('getOwner')->willReturn($this->person); 819 | $uow->method('getMapping')->willReturn(['fieldName' => 'friends']); 820 | $deleted = [$this->friend1]; 821 | $uow->method('getDeleteDiff')->willReturn($deleted); 822 | $inserted = [$this->friend2]; 823 | $uow->method('getInsertDiff')->willReturn($inserted); 824 | 825 | $this->manager 826 | ->method('getClassMetadata') 827 | ->with($this->person::class) 828 | ->willReturn($this->classMetadata); 829 | 830 | $this->classMetadata 831 | ->method('getName') 832 | ->willReturn($this->person::class); 833 | 834 | $this->classMetadata 835 | ->method('getName') 836 | ->willReturn($this->person::class); 837 | 838 | $this->dispatcher->expects($this->exactly(3)) 839 | ->method('addUpdate') 840 | ->withConsecutive( 841 | [ 842 | $this->callback(function ($arg) { return $arg instanceof Update; }), 843 | $this->callback(function ($arg) { return $arg === $this->friend1; }), 844 | $this->equalTo([]), 845 | $this->callback(function ($arg) { 846 | return 847 | array_keys($arg) === ['friendOf'] && 848 | $arg['friendOf']['deleted'] === [$this->person] && 849 | $arg['friendOf']['inserted'] === []; 850 | }) 851 | ], 852 | [ 853 | $this->callback(function ($arg) { return $arg instanceof Update; }), 854 | $this->callback(function ($arg) { return $arg === $this->friend2; }), 855 | $this->equalTo([]), 856 | $this->callback(function ($arg) { 857 | return 858 | array_keys($arg) === ['friendOf'] && 859 | $arg['friendOf']['deleted'] === [] && 860 | $arg['friendOf']['inserted'] === [$this->person]; 861 | }) 862 | ], 863 | [ 864 | $this->callback(function ($arg) { return $arg instanceof Update; }), 865 | $this->callback(function ($arg) { return $arg === $this->person; }), 866 | $this->equalTo([]), 867 | $this->callback(function ($arg) { 868 | return 869 | array_keys($arg) === ['friends'] && 870 | $arg['friends']['deleted'] === [$this->friend1] && 871 | $arg['friends']['inserted'] === [$this->friend2]; 872 | }) 873 | ] 874 | ); 875 | 876 | $this->dispatcher->expects($this->exactly(2)) 877 | ->method('addCollectionChange') 878 | ->withConsecutive( 879 | [ 880 | $this->callback(function ($arg) { return $arg instanceof Change; }), 881 | $this->callback(function ($arg) { return $arg === $this->friend1; }), 882 | $this->equalTo('friendOf'), 883 | $this->callback(function ($arg) { return $arg === [$this->person]; }), 884 | $this->equalTo([]) 885 | ], 886 | [ 887 | $this->callback(function ($arg) { return $arg instanceof Change; }), 888 | $this->callback(function ($arg) { return $arg === $this->friend2; }), 889 | $this->equalTo('friendOf'), 890 | $this->equalTo([]), 891 | $this->callback(function ($arg) { return $arg === [$this->person]; }) 892 | ] 893 | ); 894 | 895 | $changeSet = []; 896 | $event = new PreUpdateEventArgs($this->person, $this->manager, $changeSet); 897 | 898 | $this->listener->preUpdate($event); 899 | } 900 | } 901 | -------------------------------------------------------------------------------- /Tests/EventListener/LifecycleEventsListenerTest.php: -------------------------------------------------------------------------------- 1 | 28 | */ 29 | class LifecycleEventsListenerTest extends TestCase 30 | { 31 | /** 32 | * @var LifecycleEventsListener 33 | */ 34 | private $listener; 35 | 36 | /** 37 | * @var LifecycleEventsDispatcher|MockObject 38 | */ 39 | private $dispatcher; 40 | 41 | /** 42 | * @var EntityManagerInterface|MockObject 43 | */ 44 | private $manager; 45 | 46 | /** 47 | * @var ClassMetadata|MockObject 48 | */ 49 | private $classMetadata; 50 | 51 | public function setUp() : void 52 | { 53 | parent::setUp(); 54 | 55 | $this->dispatcher = $this 56 | ->getMockBuilder(LifecycleEventsDispatcher::class) 57 | ->disableOriginalConstructor() 58 | ->getMock(); 59 | 60 | $this->manager = $this 61 | ->getMockBuilder(EntityManagerInterface::class) 62 | ->disableOriginalConstructor() 63 | ->getMock(); 64 | 65 | $this->classMetadata = $this 66 | ->getMockBuilder(ClassMetadata::class) 67 | ->disableOriginalConstructor() 68 | ->getMock(); 69 | 70 | $this->listener = new LifecycleEventsListener($this->dispatcher, new AttributeGetter()); 71 | } 72 | 73 | public function testPostPersist() 74 | { 75 | $user = new User(); 76 | $event = new PostPersistEventArgs($user, $this->manager); 77 | 78 | $this->dispatcher->expects($this->once()) 79 | ->method('addCreation'); 80 | 81 | $this->manager 82 | ->method('getClassMetadata') 83 | ->with($user::class) 84 | ->willReturn($this->classMetadata); 85 | 86 | $this->classMetadata 87 | ->method('getAssociationMappings') 88 | ->willReturn(['friends' => []]); 89 | 90 | $this->classMetadata 91 | ->method('getName') 92 | ->willReturn($user::class); 93 | 94 | $this->listener->postPersist($event); 95 | } 96 | 97 | public function testPostPersistNoCreationAnnotation() 98 | { 99 | $user = new UserNoAnnotation(); 100 | $event = new PostPersistEventArgs($user, $this->manager); 101 | 102 | $this->dispatcher->expects($this->never()) 103 | ->method('addCreation'); 104 | 105 | $this->manager 106 | ->method('getClassMetadata') 107 | ->with($user::class) 108 | ->willReturn($this->classMetadata); 109 | 110 | $this->classMetadata 111 | ->method('getAssociationMappings') 112 | ->willReturn(['friends' => []]); 113 | 114 | $this->classMetadata 115 | ->method('getName') 116 | ->willReturn($user::class); 117 | 118 | $this->listener->postPersist($event); 119 | } 120 | 121 | public function testPreRemove() 122 | { 123 | $user = new User(); 124 | $event = new PreRemoveEventArgs($user, $this->manager); 125 | 126 | $this->dispatcher->expects($this->once()) 127 | ->method('addDeletion'); 128 | 129 | $this->manager 130 | ->method('getClassMetadata') 131 | ->with($user::class) 132 | ->willReturn($this->classMetadata); 133 | 134 | $this->classMetadata 135 | ->method('getAssociationMappings') 136 | ->willReturn(['friends' => []]); 137 | 138 | $this->classMetadata 139 | ->method('getName') 140 | ->willReturn($user::class); 141 | 142 | $this->listener->preRemove($event); 143 | } 144 | 145 | public function testPreRemoveNoAnnotation() 146 | { 147 | $user = new UserNoAnnotation(); 148 | $event = new PreRemoveEventArgs($user, $this->manager); 149 | 150 | $this->dispatcher->expects($this->never()) 151 | ->method('addDeletion'); 152 | 153 | $this->manager 154 | ->method('getClassMetadata') 155 | ->with($user::class) 156 | ->willReturn($this->classMetadata); 157 | 158 | $this->classMetadata 159 | ->method('getAssociationMappings') 160 | ->willReturn([]); 161 | 162 | $this->classMetadata 163 | ->method('getName') 164 | ->willReturn($user::class); 165 | 166 | $this->listener->preRemove($event); 167 | } 168 | 169 | public function testPreSoftDelete() 170 | { 171 | $user = new User(); 172 | $event = new PreRemoveEventArgs($user, $this->manager); 173 | 174 | $this->dispatcher->expects($this->once()) 175 | ->method('addDeletion'); 176 | 177 | $this->manager 178 | ->method('getClassMetadata') 179 | ->with($user::class) 180 | ->willReturn($this->classMetadata); 181 | 182 | $this->classMetadata 183 | ->method('getAssociationMappings') 184 | ->willReturn(['friends' => []]); 185 | 186 | $this->classMetadata 187 | ->method('getName') 188 | ->willReturn($user::class); 189 | 190 | $this->listener->preSoftDelete($event); 191 | } 192 | 193 | public function testPreSoftDeleteNoAnnotation() 194 | { 195 | $user = new UserNoAnnotation(); 196 | $event = new PreRemoveEventArgs($user, $this->manager); 197 | 198 | $this->dispatcher->expects($this->never()) 199 | ->method('addDeletion'); 200 | 201 | $this->manager 202 | ->method('getClassMetadata') 203 | ->with($user::class) 204 | ->willReturn($this->classMetadata); 205 | 206 | $this->classMetadata 207 | ->method('getName') 208 | ->willReturn($user::class); 209 | 210 | $this->classMetadata 211 | ->method('getAssociationMappings') 212 | ->willReturn([]); 213 | 214 | $this->listener->preSoftDelete($event); 215 | } 216 | 217 | public function testPreUpdateNoCollection() 218 | { 219 | $user = new UserClassUpdateNoCollection(); 220 | $changeSet = ['name' => [null, 'foo']]; 221 | $event = new PreUpdateEventArgs($user, $this->manager, $changeSet); 222 | 223 | $reflection = new \ReflectionClass($user); 224 | $attribute = $reflection->getAttributes(Update::class)[0]->newInstance(); 225 | 226 | $this->manager 227 | ->method('getClassMetadata') 228 | ->with($user::class) 229 | ->willReturn($this->classMetadata); 230 | 231 | $this->classMetadata 232 | ->method('getName') 233 | ->willReturn($user::class); 234 | 235 | $this->classMetadata 236 | ->method('getReflectionProperty') 237 | ->with('name') 238 | ->willReturn(new \ReflectionProperty($user, 'name')); 239 | 240 | $this->dispatcher->expects($this->once()) 241 | ->method('addUpdate') 242 | ->with($attribute, $user, ['name' => ['old' => null, 'new' => 'foo']], []); 243 | 244 | $this->listener->preUpdate($event); 245 | } 246 | 247 | public function testPreUpdateIgnoreNoCollection() 248 | { 249 | $user = new UserClassUpdateIgnoreNoCollection(); 250 | $changeSet = ['name' => [null, 'foo']]; 251 | $event = new PreUpdateEventArgs($user, $this->manager, $changeSet); 252 | 253 | $reflection = new \ReflectionClass($user); 254 | $attribute = $reflection->getAttributes(Update::class)[0]->newInstance(); 255 | 256 | $this->manager 257 | ->method('getClassMetadata') 258 | ->with($user::class) 259 | ->willReturn($this->classMetadata); 260 | 261 | $this->classMetadata 262 | ->method('getName') 263 | ->willReturn($user::class); 264 | 265 | $this->classMetadata 266 | ->method('getReflectionProperty') 267 | ->with('name') 268 | ->willReturn(new \ReflectionProperty($user, 'name')); 269 | 270 | $this->dispatcher->expects($this->once()) 271 | ->method('addUpdate') 272 | ->with($attribute, $user, [], []); 273 | 274 | $this->listener->preUpdate($event); 275 | } 276 | 277 | public function testPreUpdateIgnoreCollection() 278 | { 279 | $user = new UserClassUpdateIgnoreCollection(); 280 | $changeSet = []; 281 | $event = new PreUpdateEventArgs($user, $this->manager, $changeSet); 282 | 283 | $reflection = new \ReflectionClass($user); 284 | $attribute = $reflection->getAttributes(Update::class)[0]->newInstance(); 285 | 286 | $uow = $this 287 | ->getMockBuilder(UnitOfWork::class) 288 | ->disableOriginalConstructor() 289 | ->setMethods(['getScheduledCollectionUpdates', 'getOwner', 'getMapping', 'getDeleteDiff', 'getInsertDiff']) 290 | ->getMock(); 291 | 292 | $this->manager->method('getUnitOfWork')->willReturn($uow); 293 | $uow->method('getScheduledCollectionUpdates')->willReturn([$uow]); 294 | $uow->method('getOwner')->willReturn($user); 295 | $uow->method('getMapping')->willReturn(['fieldName' => 'friends']); 296 | $deleted = [new User(), new User()]; 297 | $uow->method('getDeleteDiff')->willReturn($deleted); 298 | $inserted = [new User()]; 299 | $uow->method('getInsertDiff')->willReturn($inserted); 300 | 301 | $this->manager 302 | ->method('getClassMetadata') 303 | ->with($user::class) 304 | ->willReturn($this->classMetadata); 305 | 306 | $this->classMetadata 307 | ->method('getName') 308 | ->willReturn($user::class); 309 | 310 | $this->classMetadata 311 | ->method('getReflectionProperty') 312 | ->with('friends') 313 | ->willReturn(new \ReflectionProperty($user, 'friends')); 314 | 315 | $this->dispatcher->expects($this->once()) 316 | ->method('addUpdate') 317 | ->with($attribute, $user, [], []); 318 | 319 | $this->listener->preUpdate($event); 320 | } 321 | 322 | public function testPreUpdateCollection() 323 | { 324 | $user = new UserClassUpdateCollection(); 325 | $changeSet = []; 326 | $event = new PreUpdateEventArgs($user, $this->manager, $changeSet); 327 | 328 | $reflection = new \ReflectionClass($user); 329 | $attribute = $reflection->getAttributes(Update::class)[0]->newInstance(); 330 | 331 | $uow = $this 332 | ->getMockBuilder(UnitOfWork::class) 333 | ->disableOriginalConstructor() 334 | ->setMethods(['getScheduledCollectionUpdates', 'getOwner', 'getMapping', 'getDeleteDiff', 'getInsertDiff']) 335 | ->getMock(); 336 | 337 | $this->manager->method('getUnitOfWork')->willReturn($uow); 338 | $uow->method('getScheduledCollectionUpdates')->willReturn([$uow]); 339 | $uow->method('getOwner')->willReturn($user); 340 | $uow->method('getMapping')->willReturn(['fieldName' => 'friends']); 341 | $deleted = [new User(), new User()]; 342 | $uow->method('getDeleteDiff')->willReturn($deleted); 343 | $inserted = [new User()]; 344 | $uow->method('getInsertDiff')->willReturn($inserted); 345 | 346 | $this->dispatcher->expects($this->once()) 347 | ->method('addUpdate') 348 | ->with($attribute, $user, [], ['friends' => ['deleted' => $deleted, 'inserted' => $inserted]]); 349 | 350 | $this->manager 351 | ->method('getClassMetadata') 352 | ->with($user::class) 353 | ->willReturn($this->classMetadata); 354 | 355 | $this->classMetadata 356 | ->method('getName') 357 | ->willReturn($user::class); 358 | 359 | $this->classMetadata 360 | ->method('getReflectionProperty') 361 | ->with('friends') 362 | ->willReturn(new \ReflectionProperty($user, 'friends')); 363 | 364 | $this->listener->preUpdate($event); 365 | } 366 | 367 | public function testPreUpdateCollectionOtherEntity() 368 | { 369 | $user = new UserClassUpdateCollection(); 370 | $user2 = new UserNoAnnotation(); 371 | $changeSet = []; 372 | $event = new PreUpdateEventArgs($user, $this->manager, $changeSet); 373 | 374 | $reflection = new \ReflectionClass($user); 375 | $attribute = $reflection->getAttributes(Update::class)[0]->newInstance(); 376 | 377 | $uow = $this 378 | ->getMockBuilder(UnitOfWork::class) 379 | ->disableOriginalConstructor() 380 | ->setMethods(['getScheduledCollectionUpdates', 'getOwner', 'getMapping', 'getDeleteDiff', 'getInsertDiff']) 381 | ->getMock(); 382 | 383 | 384 | $this->manager 385 | ->method('getClassMetadata') 386 | ->with($user::class) 387 | ->willReturn($this->classMetadata); 388 | 389 | $this->classMetadata 390 | ->method('getName') 391 | ->willReturn($user::class); 392 | 393 | $this->manager->method('getUnitOfWork')->willReturn($uow); 394 | $uow->method('getScheduledCollectionUpdates')->willReturn([$uow]); 395 | $uow->method('getOwner')->willReturn($user2); 396 | $uow->method('getMapping')->willReturn(['fieldName' => 'friends']); 397 | $deleted = [new User(), new User()]; 398 | $uow->method('getDeleteDiff')->willReturn($deleted); 399 | $inserted = [new User()]; 400 | $uow->method('getInsertDiff')->willReturn($inserted); 401 | 402 | $this->dispatcher->expects($this->once()) 403 | ->method('addUpdate') 404 | ->with($attribute, $user, [], []); 405 | 406 | $this->listener->preUpdate($event); 407 | } 408 | 409 | public function testPreUpdateCollectionDoesNotExist() 410 | { 411 | $this->expectException(\ReflectionException::class); 412 | 413 | $user = new UserClassUpdateCollection(); 414 | $changeSet = []; 415 | $event = new PreUpdateEventArgs($user, $this->manager, $changeSet); 416 | 417 | $reflection = new \ReflectionClass($user); 418 | $attribute = $reflection->getAttributes(Update::class)[0]->newInstance(); 419 | 420 | $uow = $this 421 | ->getMockBuilder(UnitOfWork::class) 422 | ->disableOriginalConstructor() 423 | ->setMethods(['getScheduledCollectionUpdates', 'getOwner', 'getMapping', 'getDeleteDiff', 'getInsertDiff']) 424 | ->getMock(); 425 | 426 | $this->manager->method('getUnitOfWork')->willReturn($uow); 427 | $uow->method('getScheduledCollectionUpdates')->willReturn([$uow]); 428 | $uow->method('getOwner')->willReturn($user); 429 | $uow->method('getMapping')->willReturn(['fieldName' => 'foo']); 430 | $deleted = [new User(), new User()]; 431 | $uow->method('getDeleteDiff')->willReturn($deleted); 432 | $inserted = [new User()]; 433 | $uow->method('getInsertDiff')->willReturn($inserted); 434 | 435 | $this->manager 436 | ->method('getClassMetadata') 437 | ->with($user::class) 438 | ->willReturn($this->classMetadata); 439 | 440 | $this->classMetadata 441 | ->method('getName') 442 | ->willReturn($user::class); 443 | 444 | $this->classMetadata 445 | ->method('getReflectionProperty') 446 | ->with('foo') 447 | ->willReturn(null); 448 | 449 | $this->dispatcher->expects($this->never()) 450 | ->method('addUpdate') 451 | ->with($attribute, $user, [], null); 452 | 453 | $this->listener->preUpdate($event); 454 | } 455 | } 456 | -------------------------------------------------------------------------------- /Tests/EventListener/LifecyclePropertyEventsListenerTest.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | class LifecyclePropertyEventsListenerTest extends TestCase 27 | { 28 | /** 29 | * @var LifecycleEventsListener 30 | */ 31 | private $listener; 32 | 33 | /** 34 | * @var LifecycleEventsDispatcher|MockObject 35 | */ 36 | private $dispatcher; 37 | 38 | /** 39 | * @var EntityManagerInterface|MockObject 40 | */ 41 | private $manager; 42 | 43 | /** 44 | * @var UnitOfWork|MockObject 45 | */ 46 | private $uow; 47 | 48 | /** 49 | * @var ClassMetadata|MockObject 50 | */ 51 | private $classMetadata; 52 | 53 | public function setUp() : void 54 | { 55 | parent::setUp(); 56 | 57 | $this->dispatcher = $this 58 | ->getMockBuilder(LifecycleEventsDispatcher::class) 59 | ->disableOriginalConstructor() 60 | ->getMock(); 61 | 62 | $this->manager = $this 63 | ->getMockBuilder(EntityManagerInterface::class) 64 | ->disableOriginalConstructor() 65 | ->getMock(); 66 | 67 | $this->classMetadata = $this 68 | ->getMockBuilder(ClassMetadata::class) 69 | ->disableOriginalConstructor() 70 | ->getMock(); 71 | 72 | $this->uow = $this 73 | ->getMockBuilder(UnitOfWork::class) 74 | ->disableOriginalConstructor() 75 | ->setMethods(['getScheduledCollectionUpdates', 'getOwner', 'getMapping', 'getDeleteDiff', 'getInsertDiff']) 76 | ->getMock(); 77 | 78 | $this->listener = new LifecyclePropertyEventsListener($this->dispatcher, new AttributeGetter()); 79 | } 80 | 81 | public function testPreUpdateProperty() 82 | { 83 | $user = new UserChange(); 84 | $changeSet = ['name' => ['foo', 'bar']]; 85 | $event = new PreUpdateEventArgs($user, $this->manager, $changeSet); 86 | 87 | $reflection = new \ReflectionProperty(get_class($user), 'name'); 88 | $attribute = $reflection->getAttributes(Change::class)[0]->newInstance(); 89 | 90 | $this->manager->method('getUnitOfWork')->willReturn($this->uow); 91 | $this->uow->method('getScheduledCollectionUpdates')->willReturn([]); 92 | 93 | $this->manager 94 | ->method('getClassMetadata') 95 | ->with($user::class) 96 | ->willReturn($this->classMetadata); 97 | 98 | $this->classMetadata 99 | ->method('getName') 100 | ->willReturn($user::class); 101 | 102 | $this->classMetadata 103 | ->method('getReflectionProperty') 104 | ->with('name') 105 | ->willReturn(new \ReflectionProperty($user, 'name')); 106 | 107 | $this->dispatcher->expects($this->once()) 108 | ->method('addPropertyChange') 109 | ->with($attribute, $user, 'name', 'foo', 'bar'); 110 | 111 | $this->listener->preUpdate($event); 112 | } 113 | 114 | public function testPreUpdateCollection() 115 | { 116 | $user = new UserChange(); 117 | $changeSet = []; 118 | $event = new PreUpdateEventArgs($user, $this->manager, $changeSet); 119 | 120 | $reflection = new \ReflectionProperty(get_class($user), 'name'); 121 | $attribute = $reflection->getAttributes(Change::class)[0]->newInstance(); 122 | 123 | 124 | $this->manager->method('getUnitOfWork')->willReturn($this->uow); 125 | $this->uow->method('getScheduledCollectionUpdates')->willReturn([$this->uow]); 126 | $this->uow->method('getOwner')->willReturn($user); 127 | $this->uow->method('getMapping')->willReturn(['fieldName' => 'friends']); 128 | $deleted = [new User(), new User()]; 129 | $this->uow->method('getDeleteDiff')->willReturn($deleted); 130 | $inserted = [new User()]; 131 | $this->uow->method('getInsertDiff')->willReturn($inserted); 132 | 133 | $this->manager 134 | ->method('getClassMetadata') 135 | ->with($user::class) 136 | ->willReturn($this->classMetadata); 137 | 138 | $this->classMetadata 139 | ->method('getName') 140 | ->willReturn($user::class); 141 | 142 | $this->classMetadata 143 | ->method('getReflectionProperty') 144 | ->with('friends') 145 | ->willReturn(new \ReflectionProperty($user, 'friends')); 146 | 147 | $this->dispatcher->expects($this->once()) 148 | ->method('addCollectionChange') 149 | ->with($attribute, $user, 'friends', $deleted, $inserted); 150 | 151 | $this->listener->preUpdate($event); 152 | } 153 | 154 | public function testPreUpdateCollectionFieldDoesNotExist() 155 | { 156 | $this->expectException(\ReflectionException::class); 157 | 158 | $user = new UserChange(); 159 | $changeSet = []; 160 | $event = new PreUpdateEventArgs($user, $this->manager, $changeSet); 161 | 162 | $this->manager->method('getUnitOfWork')->willReturn($this->uow); 163 | $this->uow->method('getScheduledCollectionUpdates')->willReturn([$this->uow]); 164 | $this->uow->method('getOwner')->willReturn($user); 165 | $this->uow->method('getMapping')->willReturn(['fieldName' => 'foo']); 166 | $deleted = [new User(), new User()]; 167 | $this->uow->method('getDeleteDiff')->willReturn($deleted); 168 | $inserted = [new User()]; 169 | $this->uow->method('getInsertDiff')->willReturn($inserted); 170 | 171 | $this->manager 172 | ->method('getClassMetadata') 173 | ->with($user::class) 174 | ->willReturn($this->classMetadata); 175 | 176 | $this->classMetadata 177 | ->method('getName') 178 | ->willReturn($user::class); 179 | 180 | $this->classMetadata 181 | ->method('getReflectionProperty') 182 | ->with('foo') 183 | ->willReturn(null); 184 | 185 | $this->dispatcher->expects($this->never()) 186 | ->method('addCollectionChange'); 187 | 188 | $this->listener->preUpdate($event); 189 | } 190 | 191 | public function testPreUpdateCollectionFieldNotMonitored() 192 | { 193 | $user = new UserClassUpdateCollection(); 194 | $changeSet = []; 195 | $event = new PreUpdateEventArgs($user, $this->manager, $changeSet); 196 | 197 | $this->manager->method('getUnitOfWork')->willReturn($this->uow); 198 | $this->uow->method('getScheduledCollectionUpdates')->willReturn([$this->uow]); 199 | $this->uow->method('getOwner')->willReturn($user); 200 | $this->uow->method('getMapping')->willReturn(['fieldName' => 'friends']); 201 | $deleted = [new User(), new User()]; 202 | $this->uow->method('getDeleteDiff')->willReturn($deleted); 203 | $inserted = [new User()]; 204 | $this->uow->method('getInsertDiff')->willReturn($inserted); 205 | 206 | $this->manager 207 | ->method('getClassMetadata') 208 | ->with($user::class) 209 | ->willReturn($this->classMetadata); 210 | 211 | $this->classMetadata 212 | ->method('getName') 213 | ->willReturn($user::class); 214 | 215 | $this->classMetadata 216 | ->method('getReflectionProperty') 217 | ->with('friends') 218 | ->willReturn(new \ReflectionProperty($user, 'friends')); 219 | 220 | $this->dispatcher->expects($this->never()) 221 | ->method('addCollectionChange'); 222 | 223 | $this->listener->preUpdate($event); 224 | } 225 | 226 | public function testPreUpdateCollectionOtherEntity() 227 | { 228 | $user = new UserChange(); 229 | $user2 = new UserNoAnnotation(); 230 | $changeSet = []; 231 | $event = new PreUpdateEventArgs($user, $this->manager, $changeSet); 232 | 233 | $this->manager->method('getUnitOfWork')->willReturn($this->uow); 234 | $this->uow->method('getScheduledCollectionUpdates')->willReturn([$this->uow]); 235 | $this->uow->method('getOwner')->willReturn($user2); 236 | $this->uow->method('getMapping')->willReturn(['fieldName' => 'friends']); 237 | $deleted = [new User(), new User()]; 238 | $this->uow->method('getDeleteDiff')->willReturn($deleted); 239 | $inserted = [new User()]; 240 | $this->uow->method('getInsertDiff')->willReturn($inserted); 241 | 242 | $this->dispatcher->expects($this->never()) 243 | ->method('addCollectionChange'); 244 | 245 | $this->listener->preUpdate($event); 246 | } 247 | 248 | public function testPreUpdateCollectionOtherClass() 249 | { 250 | $user = new UserChange(); 251 | $user2 = new OtherEntity(); 252 | $changeSet = []; 253 | $event = new PreUpdateEventArgs($user, $this->manager, $changeSet); 254 | 255 | $this->manager->method('getUnitOfWork')->willReturn($this->uow); 256 | $this->uow->method('getScheduledCollectionUpdates')->willReturn([$this->uow]); 257 | $this->uow->method('getOwner')->willReturn($user2); 258 | $this->uow->method('getMapping')->willReturn(['fieldName' => 'foo']); 259 | $deleted = [new User(), new User()]; 260 | $this->uow->method('getDeleteDiff')->willReturn($deleted); 261 | $inserted = [new User()]; 262 | $this->uow->method('getInsertDiff')->willReturn($inserted); 263 | 264 | $this->dispatcher->expects($this->never()) 265 | ->method('addCollectionChange'); 266 | 267 | $this->listener->preUpdate($event); 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /Tests/EventListener/PostFlushListenerTest.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class PostFlushListenerTest extends TestCase 14 | { 15 | /** 16 | * @var LifecycleEventsDispatcher|\PHPUnit_Framework_MockObject_MockObject 17 | */ 18 | private $dispatcher; 19 | 20 | /** 21 | * @var PostFlushEventArgs|\PHPUnit_Framework_MockObject_MockObject 22 | */ 23 | private $event; 24 | 25 | public function setUp() : void 26 | { 27 | parent::setUp(); 28 | 29 | $this->dispatcher = $this 30 | ->getMockBuilder(LifecycleEventsDispatcher::class) 31 | ->disableOriginalConstructor() 32 | ->getMock(); 33 | 34 | $this->event = $this 35 | ->getMockBuilder(PostFlushEventArgs::class) 36 | ->disableOriginalConstructor() 37 | ->getMock(); 38 | } 39 | 40 | 41 | public function testPostFlushDisabled() 42 | { 43 | $listener = new PostFlushListener($this->dispatcher); 44 | 45 | $this->dispatcher->expects($this->once()) 46 | ->method('getAutoDispatch') 47 | ->willReturn(false); 48 | 49 | $this->dispatcher->expects($this->never()) 50 | ->method('preAutoDispatch'); 51 | 52 | $this->dispatcher->expects($this->never()) 53 | ->method('dispatchEvents'); 54 | 55 | $listener->postFlush($this->event); 56 | } 57 | 58 | public function testPostFlushEnabled() 59 | { 60 | $listener = new PostFlushListener($this->dispatcher); 61 | 62 | $this->dispatcher->expects($this->once()) 63 | ->method('getAutoDispatch') 64 | ->willReturn(true); 65 | 66 | $this->dispatcher->expects($this->once()) 67 | ->method('preAutoDispatch'); 68 | 69 | $this->dispatcher->expects($this->once()) 70 | ->method('dispatchEvents'); 71 | 72 | $listener->postFlush($this->event); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /Tests/Services/AttributeGetterTest.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class AttributeGetterTest extends TestCase 18 | { 19 | /** 20 | * @var AttributeGetter 21 | */ 22 | private $attributeGetter; 23 | 24 | /** 25 | * @var ClassMetadata|MockObject 26 | */ 27 | private $classMetadata; 28 | 29 | public function setUp() : void 30 | { 31 | parent::setUp(); 32 | 33 | $this->classMetadata = $this 34 | ->getMockBuilder(ClassMetadata::class) 35 | ->disableOriginalConstructor() 36 | ->getMock(); 37 | 38 | $this->attributeGetter = new AttributeGetter(); 39 | } 40 | 41 | public function testGetAnnotation() 42 | { 43 | $attribute = $this->attributeGetter->getAttribute( 44 | 'W3C\LifecycleEventsBundle\Tests\Attribute\Fixtures\Person', 45 | Update::class 46 | ); 47 | 48 | $this->assertEquals(Update::class, get_class($attribute)); 49 | 50 | $attribute = $this->attributeGetter->getAttribute( 51 | 'W3C\LifecycleEventsBundle\Tests\Attribute\Fixtures\Person', 52 | Create::class 53 | ); 54 | 55 | $this->assertNull($attribute); 56 | } 57 | 58 | public function testGetPropertyAnnotationOk() 59 | { 60 | $user = new UserChange(); 61 | 62 | $this->classMetadata 63 | ->method('getReflectionProperty') 64 | ->with('name') 65 | ->willReturn(new \ReflectionProperty($user, 'name')); 66 | 67 | $attribute = $this->attributeGetter->getPropertyAttribute($this->classMetadata, 'name', Change::class); 68 | 69 | $this->assertEquals(Change::class, get_class($attribute)); 70 | } 71 | 72 | public function testGetPropertyAnnotationNoAnnotation() 73 | { 74 | $user = new UserChange(); 75 | 76 | $this->classMetadata 77 | ->method('getReflectionProperty') 78 | ->with('email') 79 | ->willReturn(new \ReflectionProperty($user, 'email')); 80 | 81 | $attribute = $this->attributeGetter->getPropertyAttribute($this->classMetadata, 'email', Change::class); 82 | 83 | $this->assertNull($attribute); 84 | } 85 | 86 | public function testGetPropertyAnnotationNoField() 87 | { 88 | $this->expectException(\ReflectionException::class); 89 | 90 | $this->classMetadata 91 | ->method('getReflectionProperty') 92 | ->with('foo') 93 | ->willReturn(null); 94 | $this->classMetadata 95 | ->method('getName') 96 | ->willReturn(UserChange::class); 97 | 98 | $this->attributeGetter->getPropertyAttribute($this->classMetadata, 'foo', Change::class); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Tests/Services/Events/MyCollectionChangedEvent.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class MyCollectionChangedEvent extends Event 11 | { 12 | public function __construct($entity, $property, $deleted, $inserted) 13 | { 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Tests/Services/Events/MyLifecycleEvent.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class MyLifecycleEvent extends Event 11 | { 12 | private $entity; 13 | private $identifier; 14 | 15 | public function __construct($entity, $identifier = null) 16 | { 17 | $this->entity = $entity; 18 | $this->identifier = $identifier; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/Services/Events/MyPropertyChangedEvent.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class MyPropertyChangedEvent extends Event 11 | { 12 | public function __construct($entity, $property, $old, $new) 13 | { 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Tests/Services/Events/MyUpdatedEvent.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class MyUpdatedEvent extends Event 11 | { 12 | public function __construct($entity, $propertiesChanges, $collectionChanges) 13 | { 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Tests/Services/Fixtures/MySubscriber.php: -------------------------------------------------------------------------------- 1 | dispatcher = $dispatcher; 22 | $this->attribute = $attribute; 23 | $this->args = $args; 24 | } 25 | 26 | /** 27 | * @inheritdoc 28 | */ 29 | public static function getSubscribedEvents(): array 30 | { 31 | return [ 32 | LifecycleEvents::CREATED => 'onCalled', 33 | LifecycleEvents::DELETED => 'onCalled', 34 | LifecycleEvents::UPDATED => 'onCalled', 35 | LifecycleEvents::PROPERTY_CHANGED => 'onCalled', 36 | LifecycleEvents::COLLECTION_CHANGED => 'onCalled', 37 | ]; 38 | } 39 | 40 | public function onCalled() 41 | { 42 | if (!$this->ran) { 43 | $this->ran = true; // we don't want to run in an infinite loop 44 | $this->dispatcher->addCreation(new Create(), $this->args); 45 | $this->dispatcher->dispatchEvents(); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Tests/Services/LifecycleEventsDispatcherTest.php: -------------------------------------------------------------------------------- 1 | 33 | */ 34 | class LifecycleEventsDispatcherTest extends TestCase 35 | { 36 | /** 37 | * @var EventDispatcherInterface|\PHPUnit_Framework_MockObject_MockObject 38 | */ 39 | private $sfDispatcher; 40 | 41 | /** 42 | * @var LifecycleEventsDispatcher 43 | */ 44 | private $dispatcher; 45 | 46 | /** 47 | * @var ObjectManager|\PHPUnit_Framework_MockObject_MockObject 48 | */ 49 | private $objectManager; 50 | 51 | /** 52 | * @var ClassMetadata|\PHPUnit_Framework_MockObject_MockObject 53 | */ 54 | private $classMetadata; 55 | 56 | public function setUp() : void 57 | { 58 | parent::setUp(); 59 | 60 | $this->sfDispatcher = $this 61 | ->getMockBuilder(EventDispatcher::class) 62 | ->enableProxyingToOriginalMethods() 63 | ->getMock(); 64 | ; 65 | $this->dispatcher = new LifecycleEventsDispatcher($this->sfDispatcher, true); 66 | $this->objectManager = $this 67 | ->getMockBuilder(ObjectManager::class) 68 | ->disableOriginalConstructor() 69 | ->getMock(); 70 | $this->classMetadata = $this 71 | ->getMockBuilder(ClassMetadata::class) 72 | ->disableOriginalConstructor() 73 | ->getMock(); 74 | } 75 | 76 | public function testGetDispatcher() 77 | { 78 | $this->assertEquals($this->sfDispatcher, $this->dispatcher->getDispatcher()); 79 | } 80 | 81 | public function testDispatchCreationEvents() 82 | { 83 | $user = new User(); 84 | $attribute = new Create(); 85 | $args = new LifecycleEventArgs($user, $this->objectManager); 86 | $this->dispatcher->addCreation($attribute, $args); 87 | 88 | $this->assertCount(1, $this->dispatcher->getCreations()); 89 | 90 | $expectedEvent = new LifecycleEvent($user); 91 | 92 | $this->sfDispatcher->expects($this->once()) 93 | ->method('dispatch') 94 | ->with($expectedEvent, LifecycleEvents::CREATED) 95 | ; 96 | 97 | $this->dispatcher->dispatchEvents(); 98 | } 99 | 100 | /** 101 | * Test that if dispatchEvents is called recursively (could happen if flush happens in a listener), 102 | * events already fired aren't a second time. 103 | */ 104 | public function testDispatchCreationEventsRecursive() 105 | { 106 | $user = new User(); 107 | $attribute = new Create(); 108 | $args = new LifecycleEventArgs($user, $this->objectManager); 109 | 110 | $this->sfDispatcher->addSubscriber(new MySubscriber($this->dispatcher, $attribute, $args)); 111 | 112 | $this->dispatcher->addCreation($attribute, $args); 113 | 114 | $this->assertCount(1, $this->dispatcher->getCreations()); 115 | 116 | // 2 === 1 addCreation above + 1 in MySubscriber::onCalled 117 | $this->sfDispatcher->expects($this->exactly(2)) 118 | ->method('dispatch'); 119 | 120 | $this->dispatcher->dispatchEvents(); 121 | } 122 | 123 | public function testDispatchCreationEventsCustom() 124 | { 125 | $user = new User(); 126 | $attribute = new Create(); 127 | $attribute->event = 'test'; 128 | $attribute->class = MyLifecycleEvent::class; 129 | $args = new LifecycleEventArgs($user, $this->objectManager); 130 | 131 | $this->dispatcher->addCreation($attribute, $args); 132 | 133 | $this->assertCount(1, $this->dispatcher->getCreations()); 134 | 135 | $expectedEvent = new $attribute->class($user); 136 | $this->sfDispatcher->expects($this->once()) 137 | ->method('dispatch') 138 | ->with($expectedEvent, $attribute->event); 139 | 140 | $this->dispatcher->dispatchEvents(); 141 | } 142 | 143 | public function testDispatchDeletionEvents() 144 | { 145 | $user = new User(); 146 | $attribute = new Delete(); 147 | $args = new LifecycleEventArgs($user, $this->objectManager); 148 | 149 | $this->objectManager->method('getClassMetadata')->willReturn($this->classMetadata); 150 | $this->classMetadata->expects($this->once()) 151 | ->method('getIdentifierFieldNames') 152 | ->willReturn(['name']); 153 | $this->classMetadata->expects($this->once()) 154 | ->method('getIdentifierValues') 155 | ->willReturn(['toto']); 156 | 157 | $this->dispatcher->addDeletion($attribute, $args); 158 | 159 | $this->assertCount(1, $this->dispatcher->getDeletions()); 160 | 161 | $expectedEvent = new LifecycleDeletionEvent($user, ['name' => 'toto']); 162 | $this->sfDispatcher->expects($this->once()) 163 | ->method('dispatch') 164 | ->with($expectedEvent, LifecycleEvents::DELETED); 165 | 166 | $this->dispatcher->dispatchEvents(); 167 | } 168 | 169 | /** 170 | * Test that if dispatchEvents is called recursively (could happen if flush happens in a listener), 171 | * events already fired aren't a second time. 172 | */ 173 | public function testDispatchDeletionEventsRecursive() 174 | { 175 | $user = new User(); 176 | $attribute = new Delete(); 177 | $args = new LifecycleEventArgs($user, $this->objectManager); 178 | 179 | $this->sfDispatcher->addSubscriber(new MySubscriber($this->dispatcher, $attribute, $args)); 180 | 181 | $this->objectManager->method('getClassMetadata')->willReturn($this->classMetadata); 182 | $this->classMetadata->expects($this->once()) 183 | ->method('getIdentifierFieldNames') 184 | ->willReturn(['name']); 185 | $this->classMetadata->expects($this->once()) 186 | ->method('getIdentifierValues') 187 | ->willReturn(['toto']); 188 | 189 | $this->dispatcher->addDeletion($attribute, $args); 190 | 191 | $this->assertCount(1, $this->dispatcher->getDeletions()); 192 | 193 | // 2 === 1 addDeletion above + 1 addCreation in MySubscriber::onCalled 194 | $this->sfDispatcher->expects($this->exactly(2)) 195 | ->method('dispatch'); 196 | 197 | $this->dispatcher->dispatchEvents(); 198 | } 199 | 200 | public function testDispatchDeletionEventsCustom() 201 | { 202 | $user = new User(); 203 | $attribute = new Delete(); 204 | $attribute->event = 'test'; 205 | $attribute->class = MyLifecycleEvent::class; 206 | 207 | $args = new LifecycleEventArgs($user, $this->objectManager); 208 | 209 | $this->objectManager->method('getClassMetadata')->willReturn($this->classMetadata); 210 | $this->classMetadata->expects($this->once()) 211 | ->method('getIdentifierFieldNames') 212 | ->willReturn(['name']); 213 | $this->classMetadata->expects($this->once()) 214 | ->method('getIdentifierValues') 215 | ->willReturn(['toto']); 216 | 217 | $this->dispatcher->addDeletion($attribute, $args); 218 | 219 | $this->assertCount(1, $this->dispatcher->getDeletions()); 220 | 221 | $expectedEvent = new $attribute->class($user, ['name' => 'toto']); 222 | $this->sfDispatcher->expects($this->once()) 223 | ->method('dispatch') 224 | ->with($this->equalTo($expectedEvent), $attribute->event); 225 | 226 | $this->dispatcher->dispatchEvents(); 227 | } 228 | 229 | public function testDispatchUpdatesEvents() 230 | { 231 | $user = new User(); 232 | $attribute = new Update(); 233 | $this->dispatcher->addUpdate( 234 | $attribute, 235 | $user, 236 | ['name' => ['foo', 'bar']], 237 | [] 238 | ); 239 | 240 | $this->assertCount(1, $this->dispatcher->getUpdates()); 241 | 242 | $expectedEvent = new LifecycleUpdateEvent($user, ['name' => ['foo', 'bar']], []); 243 | $this->sfDispatcher->expects($this->once()) 244 | ->method('dispatch') 245 | ->with($expectedEvent, LifecycleEvents::UPDATED); 246 | 247 | $this->dispatcher->dispatchEvents(); 248 | } 249 | 250 | /** 251 | * Test that if dispatchEvents is called recursively (could happen if flush happens in a listener), 252 | * events already fired aren't a second time. 253 | */ 254 | public function testDispatchUpdatesEventsRecursive() 255 | { 256 | $user = new User(); 257 | $attribute = new Update(); 258 | $args = new LifecycleEventArgs($user, $this->objectManager); 259 | 260 | $this->sfDispatcher->addSubscriber(new MySubscriber($this->dispatcher, $attribute, $args)); 261 | 262 | $this->dispatcher->addUpdate( 263 | $attribute, 264 | $user, 265 | ['name' => ['foo', 'bar']], 266 | [] 267 | ); 268 | 269 | $this->assertCount(1, $this->dispatcher->getUpdates()); 270 | 271 | // 2 === 1 addUpdate above + 1 addCreation in MySubscriber::onCalled 272 | $this->sfDispatcher->expects($this->exactly(2)) 273 | ->method('dispatch'); 274 | 275 | $this->dispatcher->dispatchEvents(); 276 | } 277 | 278 | public function testDispatchUpdatesEventsCustom() 279 | { 280 | $user = new User(); 281 | $attribute = new Update(); 282 | $attribute->event = 'test'; 283 | $attribute->class = MyUpdatedEvent::class; 284 | 285 | $this->dispatcher->addUpdate( 286 | $attribute, 287 | $user, 288 | ['name' => ['foo', 'bar']], 289 | [] 290 | ); 291 | 292 | $this->assertCount(1, $this->dispatcher->getUpdates()); 293 | 294 | $expectedEvent = new $attribute->class($user, ['name' => ['foo', 'bar']], []); 295 | $this->sfDispatcher->expects($this->once()) 296 | ->method('dispatch') 297 | ->with($expectedEvent, $attribute->event); 298 | 299 | $this->dispatcher->dispatchEvents(); 300 | } 301 | 302 | public function testDispatchPropertyChangeEvents() 303 | { 304 | $user = new User(); 305 | $attribute = new Change(); 306 | $this->dispatcher->addPropertyChange( 307 | $attribute, 308 | $user, 309 | 'name', 310 | 'foo', 311 | 'bar' 312 | ); 313 | 314 | $this->assertCount(1, $this->dispatcher->getPropertyChanges()); 315 | 316 | $expectedEvent = new LifecyclePropertyChangedEvent($user, 'name', 'foo', 'bar'); 317 | $this->sfDispatcher->expects($this->once()) 318 | ->method('dispatch') 319 | ->with($expectedEvent, LifecycleEvents::PROPERTY_CHANGED); 320 | 321 | $this->dispatcher->dispatchEvents(); 322 | } 323 | 324 | /** 325 | * Test that if dispatchEvents is called recursively (could happen if flush happens in a listener), 326 | * events already fired aren't a second time. 327 | */ 328 | public function testDispatchPropertyChangeEventsRecursive() 329 | { 330 | $user = new User(); 331 | $attribute = new Change(); 332 | $args = new LifecycleEventArgs($user, $this->objectManager); 333 | 334 | $this->sfDispatcher->addSubscriber(new MySubscriber($this->dispatcher, $attribute, $args)); 335 | 336 | $this->dispatcher->addPropertyChange( 337 | $attribute, 338 | $user, 339 | 'name', 340 | 'foo', 341 | 'bar' 342 | ); 343 | 344 | $this->assertCount(1, $this->dispatcher->getPropertyChanges()); 345 | 346 | // 2 === 1 addPropertyChange above + 1 addCreation in MySubscriber::onCalled 347 | $this->sfDispatcher->expects($this->exactly(2)) 348 | ->method('dispatch'); 349 | 350 | $this->dispatcher->dispatchEvents(); 351 | } 352 | 353 | public function testDispatchPropertyChangeEventsCustom() 354 | { 355 | $user = new User(); 356 | $attribute = new Change(); 357 | $attribute->event = 'test'; 358 | $attribute->class = MyPropertyChangedEvent::class; 359 | 360 | $this->dispatcher->addPropertyChange( 361 | $attribute, 362 | $user, 363 | 'name', 364 | 'foo', 365 | 'bar' 366 | ); 367 | 368 | $this->assertCount(1, $this->dispatcher->getPropertyChanges()); 369 | 370 | $expectedEvent = new $attribute->class($user, 'name', 'foo', 'bar'); 371 | $this->sfDispatcher->expects($this->once()) 372 | ->method('dispatch') 373 | ->with($expectedEvent, $attribute->event); 374 | 375 | $this->dispatcher->dispatchEvents(); 376 | } 377 | public function testDispatchCollectionChangeEvents() 378 | { 379 | $user = new User(); 380 | $attribute = new Change(); 381 | $deleted = [new User()]; 382 | $inserted = [new User(), new User()]; 383 | $this->dispatcher->addCollectionChange( 384 | $attribute, 385 | $user, 386 | 'friends', 387 | $deleted, 388 | $inserted 389 | ); 390 | 391 | $this->assertCount(1, $this->dispatcher->getCollectionChanges()); 392 | 393 | $expectedEvent = new LifecycleCollectionChangedEvent($user, 'friends', $deleted, $inserted); 394 | $this->sfDispatcher->expects($this->once()) 395 | ->method('dispatch') 396 | ->with($expectedEvent, LifecycleEvents::COLLECTION_CHANGED); 397 | 398 | $this->dispatcher->dispatchEvents(); 399 | } 400 | 401 | /** 402 | * Test that if dispatchEvents is called recursively (could happen if flush happens in a listener), 403 | * events already fired aren't a second time. 404 | */ 405 | public function testDispatchCollectionChangeEventsRecursive() 406 | { 407 | $user = new User(); 408 | $attribute = new Change(); 409 | $args = new LifecycleEventArgs($user, $this->objectManager); 410 | 411 | $this->sfDispatcher->addSubscriber(new MySubscriber($this->dispatcher, $attribute, $args)); 412 | 413 | $deleted = [new User()]; 414 | $inserted = [new User(), new User()]; 415 | $this->dispatcher->addCollectionChange( 416 | $attribute, 417 | $user, 418 | 'friends', 419 | $deleted, 420 | $inserted 421 | ); 422 | 423 | $this->assertCount(1, $this->dispatcher->getCollectionChanges()); 424 | 425 | // 2 === 1 addCollectionChange above + 1 addCreation in MySubscriber::onCalled 426 | $this->sfDispatcher->expects($this->exactly(2)) 427 | ->method('dispatch'); 428 | 429 | $this->dispatcher->dispatchEvents(); 430 | } 431 | 432 | public function testDispatchCollectionChangeEventsCustom() 433 | { 434 | $user = new User(); 435 | $attribute = new Change(); 436 | $attribute->event = 'test'; 437 | $attribute->class = MyCollectionChangedEvent::class; 438 | 439 | $deleted = [new User()]; 440 | $inserted = [new User(), new User()]; 441 | $this->dispatcher->addCollectionChange( 442 | $attribute, 443 | $user, 444 | 'friends', 445 | $deleted, 446 | $inserted 447 | ); 448 | 449 | $this->assertCount(1, $this->dispatcher->getCollectionChanges()); 450 | 451 | $expectedEvent = new $attribute->class($user, 'friends', $deleted, $inserted); 452 | $this->sfDispatcher->expects($this->once()) 453 | ->method('dispatch') 454 | ->with($expectedEvent, $attribute->event); 455 | 456 | $this->dispatcher->dispatchEvents(); 457 | } 458 | 459 | public function testPreAutoDispatch() 460 | { 461 | $this->sfDispatcher->expects($this->once()) 462 | ->method('dispatch') 463 | ->with(new PreAutoDispatchEvent($this->dispatcher),'w3c.lifecycle.preAutoDispatch'); 464 | 465 | $this->dispatcher->preAutoDispatch(); 466 | } 467 | 468 | public function testAutoDispatch() 469 | { 470 | $this->dispatcher->setAutoDispatch(true); 471 | $this->assertTrue($this->dispatcher->getAutoDispatch()); 472 | 473 | $this->dispatcher->setAutoDispatch(false); 474 | $this->assertFalse($this->dispatcher->getAutoDispatch()); 475 | } 476 | 477 | public function testGetUpdate() 478 | { 479 | $user = new User(); 480 | $attribute = new Update(); 481 | 482 | $this->assertNull($this->dispatcher->getUpdate($user)); 483 | 484 | $this->dispatcher->addUpdate( 485 | $attribute, 486 | new User(), 487 | ['name' => ['foo', 'bar']], 488 | [] 489 | ); 490 | 491 | $this->dispatcher->addUpdate( 492 | $attribute, 493 | $user, 494 | ['name' => ['foo', 'bar']], 495 | [] 496 | ); 497 | 498 | $this->dispatcher->addUpdate( 499 | $attribute, 500 | new User(), 501 | ['name' => ['foo', 'bar']], 502 | [] 503 | ); 504 | 505 | $this->assertSame([1, [$attribute, $user, ['name' => ['foo', 'bar']], []]], $this->dispatcher->getUpdate($user)); 506 | } 507 | 508 | public function testAddUpdate() 509 | { 510 | $user = new User(); 511 | $attribute = new Update(); 512 | 513 | $this->assertNull($this->dispatcher->getUpdate($user)); 514 | 515 | $this->dispatcher->addUpdate( 516 | $attribute, 517 | new User(), 518 | ['name' => ['old' => 'foo', 'new' => 'bar']], 519 | [] 520 | ); 521 | 522 | $this->dispatcher->addUpdate( 523 | $attribute, 524 | $user, 525 | ['name' => ['old' => 'foo', 'new' => 'bar']], 526 | [] 527 | ); 528 | 529 | $this->assertCount(2, $this->dispatcher->getUpdates()); 530 | 531 | $this->dispatcher->addUpdate( 532 | $attribute, 533 | $user, 534 | [], 535 | ['friends' => ['deleted' => 'foo', 'inserted' => 'bar']] 536 | ); 537 | 538 | $this->assertCount(2, $this->dispatcher->getUpdates()); 539 | $this->assertSame([ 540 | 1, 541 | [ 542 | $attribute, 543 | $user, 544 | ['name' => ['old' => 'foo', 'new' => 'bar']], 545 | ['friends' => ['deleted' => 'foo', 'inserted' => 'bar']] 546 | ] 547 | ], $this->dispatcher->getUpdate($user)); 548 | 549 | $this->dispatcher->addUpdate( 550 | $attribute, 551 | $user, 552 | ['foo' => ['old' => 'a', 'new' => 'b']], 553 | [] 554 | ); 555 | 556 | $this->assertCount(2, $this->dispatcher->getUpdates()); 557 | $this->assertSame([ 558 | 1, 559 | [ 560 | $attribute, 561 | $user, 562 | ['name' => ['old' => 'foo', 'new' => 'bar'], 'foo' => ['old' => 'a', 'new' => 'b']], 563 | ['friends' => ['deleted' => 'foo', 'inserted' => 'bar']] 564 | ] 565 | ], $this->dispatcher->getUpdate($user)); 566 | } 567 | 568 | public function testGetCollectionChange() 569 | { 570 | $father = new Person(); 571 | $son1 = new Person(); 572 | $son2 = new Person(); 573 | $attribute = new Change(); 574 | 575 | $this->assertNull($this->dispatcher->getCollectionChange($father, 'sons')); 576 | 577 | $this->dispatcher->addCollectionChange( 578 | $attribute, 579 | new Person(), 580 | 'sons', 581 | [new Person(), new Person()], 582 | [new Person()] 583 | ); 584 | 585 | $this->dispatcher->addCollectionChange( 586 | $attribute, 587 | $father, 588 | 'sons', 589 | [$son1], 590 | [$son2] 591 | ); 592 | 593 | $this->dispatcher->addCollectionChange( 594 | $attribute, 595 | new Person(), 596 | 'foo', 597 | [], 598 | ['bar'] 599 | ); 600 | 601 | $this->assertSame([1, [$attribute, $father, 'sons', [$son1], [$son2]]], 602 | $this->dispatcher->getCollectionChange($father, 'sons')); 603 | } 604 | 605 | public function testAddCollectionUpdate() 606 | { 607 | $father = new Person(); 608 | $son1 = new Person(); 609 | $son2 = new Person(); 610 | $son3 = new Person(); 611 | $attribute = new Change(); 612 | 613 | $this->assertNull($this->dispatcher->getCollectionChange($father, 'sons')); 614 | 615 | $this->dispatcher->addCollectionChange( 616 | $attribute, 617 | new Person(), 618 | 'sons', 619 | [new Person(), new Person()], 620 | [new Person()] 621 | ); 622 | 623 | $this->dispatcher->addCollectionChange( 624 | $attribute, 625 | $father, 626 | 'sons', 627 | [], 628 | [$son2] 629 | ); 630 | 631 | $this->assertCount(2, $this->dispatcher->getCollectionChanges()); 632 | 633 | $this->dispatcher->addCollectionChange( 634 | $attribute, 635 | $father, 636 | 'sons', 637 | [$son1], 638 | [$son3] 639 | ); 640 | 641 | $this->assertCount(2, $this->dispatcher->getCollectionChanges()); 642 | $this->assertSame([ 643 | 1, 644 | [ 645 | $attribute, 646 | $father, 647 | 'sons', 648 | [$son1], 649 | [$son2, $son3] 650 | ] 651 | ], $this->dispatcher->getCollectionChange($father, 'sons')); 652 | 653 | $this->dispatcher->addCollectionChange( 654 | $attribute, 655 | $father, 656 | 'foo', 657 | [$son1], 658 | [$son3] 659 | ); 660 | 661 | $this->assertCount(3, $this->dispatcher->getCollectionChanges()); 662 | $this->assertSame([ 663 | 1, 664 | [ 665 | $attribute, 666 | $father, 667 | 'sons', 668 | [$son1], 669 | [$son2, $son3] 670 | ] 671 | ], $this->dispatcher->getCollectionChange($father, 'sons')); 672 | } 673 | } 674 | -------------------------------------------------------------------------------- /W3CLifecycleEventsBundle.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class W3CLifecycleEventsBundle extends Bundle 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "w3c/lifecycle-events-bundle", 3 | "type": "symfony-bundle", 4 | "description": "A Symfony bundle to dispatch usable entity lifecycle events (create, update, delete)", 5 | "keywords": ["lifecycle", "events", "domain"], 6 | "homepage": "https://github.com/w3c/W3CLifecycleEventsBundle", 7 | "license": "W3C", 8 | "authors": [ 9 | { 10 | "name": "Jean-Guilhem Rouel", 11 | "email": "jean-gui@w3.org" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=8.2", 16 | "symfony/http-kernel": "^7.0", 17 | "symfony/config": "^7.0", 18 | "symfony/dependency-injection": "^7.0", 19 | "symfony/yaml": "^7.0", 20 | "symfony/event-dispatcher": "^7.0", 21 | "doctrine/orm": "^3.0", 22 | "doctrine/persistence": "^3.0" 23 | }, 24 | "require-dev": { 25 | "phpunit/phpunit": "^9.5.25", 26 | "symfony/phpunit-bridge": "^6.0", 27 | "php-coveralls/php-coveralls": "^2.0" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "W3C\\LifecycleEventsBundle\\": "" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Annotation 7 | Event 8 | EventListener 9 | Services 10 | 11 | 12 | Tests 13 | vendor 14 | 15 | 16 | 17 | 18 | Tests 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /w3c.json: -------------------------------------------------------------------------------- 1 | { 2 | "group": 109, 3 | "contacts": ["jean-gui"], 4 | "repo-type": "tool" 5 | } 6 | --------------------------------------------------------------------------------