diff --git a/.gitignore b/.gitignore index 3a2a5ba..a6a6a28 100755 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ # Backup entities generated with doctrine:generate:entities command **/Entity/*~ +./.idea diff --git a/Annotations/SyncEntity.php b/Annotations/SyncEntity.php new file mode 100755 index 0000000..1786a19 --- /dev/null +++ b/Annotations/SyncEntity.php @@ -0,0 +1,13 @@ +container = $container; - $this->syncService = $this->container->get('nti.sync'); - } - - public function getSubscribedEvents() - { - return array( - 'onFlush', - 'preRemove', - ); - } - - public function onFlush(OnFlushEventArgs $args) - { - $em = $args->getEntityManager(); - $uow = $em->getUnitOfWork(); - - $somethingChanged = false; - - $identityMap = $uow->getIdentityMap(); - - // ehhehehehee LOL - $deletedEntities = count($uow->getScheduledEntityDeletions()) > 0; - $insertedEntities = count($uow->getScheduledEntityInsertions()) > 0; - - foreach($identityMap as $map) { - foreach($map as $object) { - $changes = $uow->getEntityChangeSet($object); - if(count($changes) > 1 || (count($changes) > 0 && !isset($changes["lastTimestamp"]) && !isset($changes["lastLogin"]))) { - $somethingChanged = true; - break; - } - } - - if($somethingChanged) { - break; - } - } - - if($deletedEntities || $insertedEntities) - $somethingChanged = true; - - foreach ($uow->getScheduledEntityUpdates() as $keyEntity => $entity) { - $changes = $uow->getEntityChangeSet($entity); - - if(count($changes) == 1 && isset($changes["lastTimestamp"]) && !isset($changes["lastLogin"]) && !$somethingChanged) { - $oid = spl_object_hash($entity); - $uow->clearEntityChangeSet($oid); - } else { - $this->handleEntityChange($em, $entity); - } - } - - foreach ($uow->getScheduledEntityInsertions() as $keyEntity => $entity) { - $this->handleEntityChange($em, $entity); - } - - } - - public function preRemove(LifecycleEventArgs $args) { - $entity = $args->getEntity(); - $class = get_class($entity); - $id = null; - - if(method_exists($entity, 'getId')) { - $id = $entity->getId(); - } - - $this->syncService->addToDeleteSyncState($class, $id); - } - - private function handleEntityChange(EntityManagerInterface $em, $entity) { - if(method_exists($entity, 'getLastTimestamp')) { - $timestamp = $entity->getLastTimestamp() ?? time(); - } else { - $timestamp = time(); - } - $class = get_class($entity); - $this->syncService->updateSyncState($em, $class, $timestamp); - } - - -} diff --git a/EventSubscriber/DoctrineEventSubscriber.php b/EventSubscriber/DoctrineEventSubscriber.php new file mode 100755 index 0000000..c06d84d --- /dev/null +++ b/EventSubscriber/DoctrineEventSubscriber.php @@ -0,0 +1,127 @@ +container = $container; + $this->syncService = $this->container->get('nti.sync'); + } + + public function getSubscribedEvents() + { + return array( + 'onFlush', + ); + } + + public function onFlush(OnFlushEventArgs $args) + { + + $em = $args->getEntityManager(); + $uow = $em->getUnitOfWork(); + + foreach ($uow->getScheduledEntityUpdates() as $entity) { + $this->processEntity($em, $entity); + } + + foreach ($uow->getScheduledEntityInsertions() as $entity) { + $this->processEntity($em, $entity); + } + + foreach ($uow->getScheduledEntityDeletions() as $entity) { + $this->processEntity($em, $entity, true); + $this->container->get('nti.sync')->addToDeleteSyncState(ClassUtils::getClass($entity), $entity->getId()); + + } + + /** @var PersistentCollection $collectionUpdate */ + foreach ($uow->getScheduledCollectionUpdates() as $collectionUpdate) { + foreach($collectionUpdate as $entity) { + $this->processEntity($em, $entity); + } + } + + /** @var PersistentCollection $collectionDeletion */ + foreach($uow->getScheduledCollectionDeletions() as $collectionDeletion) { + foreach($collectionDeletion as $entity) { + $this->processEntity($em, $entity, true); + $this->container->get('nti.sync')->addToDeleteSyncState(ClassUtils::getClass($entity), $entity->getId()); + } + } + + } + + private function processEntity(EntityManagerInterface $em, $entity, $deleting = false) + { + + $reflection = new \ReflectionClass(ClassUtils::getClass($entity)); + $annotationReader = new AnnotationReader(); + $syncEntityAnnotation = $annotationReader->getClassAnnotation($reflection, SyncEntity::class); + // Check if the entity should be synchronized + if (!$syncEntityAnnotation) { + return; + } + + $uow = $em->getUnitOfWork(); + $timestamp = time(); + + // Update the mapping's sync state if exists + $mapping = $em->getRepository(SyncMapping::class)->findOneBy(array("class" => ClassUtils::getClass($entity))); + if($mapping) { + $syncState = $em->getRepository(SyncState::class)->findOneBy(array("mapping" => $mapping)); + if(!$syncState) { + $syncState = new SyncState(); + $syncState->setMapping($mapping); + $em->persist($syncState); + } + $syncState->setTimestamp($timestamp); + if($uow->getEntityState($syncState) == UnitOfWork::STATE_MANAGED) { + $uow->recomputeSingleEntityChangeSet($em->getClassMetadata(SyncState::class), $syncState); + } + } + + // Check if this class itself has a lastTimestamp + if(!$deleting && method_exists($entity, 'setLastTimestamp')) { + $entity->setLastTimestamp($timestamp); + $uow->recomputeSingleEntityChangeSet($em->getClassMetadata(ClassUtils::getClass($entity)), $entity); + } + + // Notify relationships + /** @var \ReflectionProperty $property */ + foreach ($reflection->getProperties() as $property) { + + /** @var SyncParent $annotation */ + if (null !== ($annotation = $annotationReader->getPropertyAnnotation($property, SyncParent::class))) { + $getter = $annotation->getter; + $parent = $entity->$getter(); + // Using ClassUtils as $parent is actually a Proxy of the class + $reflrectionParent = new \ReflectionClass(ClassUtils::getClass($parent)); + $syncParentAnnotation = $annotationReader->getClassAnnotation($reflrectionParent, SyncEntity::class); + if(!$syncParentAnnotation) { + continue; + } + $this->processEntity($em, $parent); + } + } + } +} diff --git a/Resources/config/services.yml b/Resources/config/services.yml index 212e162..e065d6d 100755 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -1,6 +1,6 @@ services: nti.sync.doctrine.listener: - class: NTI\SyncBundle\EventListener\DoctrineEventSubscriber + class: NTI\SyncBundle\EventSubscriber\DoctrineEventSubscriber arguments: ["@service_container"] tags: - { name: doctrine.event_subscriber, connection: default } diff --git a/Service/SyncService.php b/Service/SyncService.php index 004ce9c..6d55d43 100755 --- a/Service/SyncService.php +++ b/Service/SyncService.php @@ -5,6 +5,7 @@ use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManagerInterface; use NTI\SyncBundle\Entity\SyncDeleteState; +use NTI\SyncBundle\Entity\SyncFailedItemState; use NTI\SyncBundle\Entity\SyncMapping; use NTI\SyncBundle\Entity\SyncNewItemState; use NTI\SyncBundle\Entity\SyncState; @@ -62,10 +63,7 @@ public function getFromMappings($mappings) { $deletes = $this->em->getRepository(SyncDeleteState::class)->findFromTimestamp($mappingName, $timestamp); $newItems = $this->em->getRepository(SyncNewItemState::class)->findFromTimestampAndMapping($mappingName, $timestamp); - /** - * Failed Items Synchronization - */ - $failedItems = $this->em->getRepository('NTISyncBundle:SyncFailedItemState')->findFromTimestampAndMapping($mappingName, $timestamp); + $failedItems = $this->em->getRepository(SyncFailedItemState::class)->findFromTimestampAndMapping($mappingName, $timestamp); /** @var SyncRepositoryInterface $repository */ $repository = $this->em->getRepository($syncMapping->getClass()); @@ -80,7 +78,7 @@ public function getFromMappings($mappings) { 'changes' => $result["data"], 'deletes' => json_decode($this->container->get('jms_serializer')->serialize($deletes, 'json'), true), 'newItems' => json_decode($this->container->get('jms_serializer')->serialize($newItems, 'json'), true), - 'failedItems' => json_decode($this->container->get('jms_serializer')->serialize($failedItems, 'json'), true), + 'failedItems' => json_decode($this->container->get('jms_serializer')->serialize($failedItems, 'json'), true), SyncState::REAL_LAST_TIMESTAMP => $result[SyncState::REAL_LAST_TIMESTAMP], ); } @@ -88,30 +86,6 @@ public function getFromMappings($mappings) { return $changes; } - public function updateSyncState(EntityManagerInterface $em, $class, $timestamp) { - - $mapping = $em->getRepository(SyncMapping::class)->findOneBy(array("class" => $class)); - if(!$mapping) { - return; - } - - $syncState = $em->getRepository(SyncState::class)->findOneBy(array("mapping" => $mapping)); - - $uow = $em->getUnitOfWork(); - - if(!$syncState) { - $syncState = new SyncState(); - $syncState->setMapping($mapping); - $syncState->setTimestamp($timestamp); - $em->persist($syncState); - $uow->computeChangeSet($em->getClassMetadata(SyncState::class), $syncState); - } else { - $syncState->setTimestamp($timestamp); - $uow->recomputeSingleEntityChangeSet($em->getClassMetadata(SyncState::class), $syncState); - } - - } - /** * Create a new SyncDeleteState for the given class/id * @@ -122,6 +96,7 @@ public function addToDeleteSyncState($class, $id) { $this->em = $this->container->get('doctrine')->getManager(); + /** @var SyncMapping $mapping */ $mapping = $this->em->getRepository(SyncMapping::class)->findOneBy(array("class" => $class)); if(!$mapping) { return; @@ -133,12 +108,7 @@ public function addToDeleteSyncState($class, $id) { $deleteEntry->setTimestamp(time()); $this->em->persist($deleteEntry); - - try { - $this->em->flush(); - } catch (\Exception $ex) { - error_log("Unable to register deletion of object: " . $class . " with ID " . $id); - error_log($ex->getMessage()); - } + $uow = $this->em->getUnitOfWork(); + $uow->computeChangeSet($this->em->getClassMetadata(SyncDeleteState::class), $deleteEntry); } }