From 932c8e5d1cdacf1a31628f77380bcfc9f8ddd8ab Mon Sep 17 00:00:00 2001 From: "Jonathan H. Wage" Date: Mon, 31 Jan 2011 18:08:47 -0600 Subject: [PATCH] first commit --- .gitmodules | 9 + README.markdown | 114 ++++++ .../ODM/MongoDB/SoftDelete/Configuration.php | 60 +++ .../SoftDelete/Event/LifecycleEventArgs.php | 72 ++++ .../ODM/MongoDB/SoftDelete/Events.php | 59 +++ .../ODM/MongoDB/SoftDelete/Persister.php | 197 ++++++++++ .../MongoDB/SoftDelete/SoftDeleteManager.php | 183 +++++++++ .../ODM/MongoDB/SoftDelete/SoftDeleteable.php | 48 +++ .../ODM/MongoDB/SoftDelete/UnitOfWork.php | 350 ++++++++++++++++++ lib/vendor/doctrine-common | 1 + lib/vendor/doctrine-mongodb | 1 + lib/vendor/doctrine-mongodb-odm | 1 + phpunit.xml.dist | 18 + .../SoftDelete/Tests/ConfigurationTest.php | 22 ++ .../SoftDelete/Tests/FunctionalTest.php | 267 +++++++++++++ .../SoftDelete/Tests/PersisterTest.php | 151 ++++++++ .../SoftDelete/Tests/SoftDeletableTest.php | 34 ++ .../Tests/SoftDeleteManagerTest.php | 179 +++++++++ .../SoftDelete/Tests/UnitOfWorkTest.php | 139 +++++++ tests/bootstrap.php | 20 + 20 files changed, 1925 insertions(+) create mode 100644 .gitmodules create mode 100644 README.markdown create mode 100644 lib/Doctrine/ODM/MongoDB/SoftDelete/Configuration.php create mode 100644 lib/Doctrine/ODM/MongoDB/SoftDelete/Event/LifecycleEventArgs.php create mode 100644 lib/Doctrine/ODM/MongoDB/SoftDelete/Events.php create mode 100644 lib/Doctrine/ODM/MongoDB/SoftDelete/Persister.php create mode 100644 lib/Doctrine/ODM/MongoDB/SoftDelete/SoftDeleteManager.php create mode 100644 lib/Doctrine/ODM/MongoDB/SoftDelete/SoftDeleteable.php create mode 100644 lib/Doctrine/ODM/MongoDB/SoftDelete/UnitOfWork.php create mode 160000 lib/vendor/doctrine-common create mode 160000 lib/vendor/doctrine-mongodb create mode 160000 lib/vendor/doctrine-mongodb-odm create mode 100644 phpunit.xml.dist create mode 100644 tests/Doctrine/ODM/MongoDB/SoftDelete/Tests/ConfigurationTest.php create mode 100644 tests/Doctrine/ODM/MongoDB/SoftDelete/Tests/FunctionalTest.php create mode 100644 tests/Doctrine/ODM/MongoDB/SoftDelete/Tests/PersisterTest.php create mode 100644 tests/Doctrine/ODM/MongoDB/SoftDelete/Tests/SoftDeletableTest.php create mode 100644 tests/Doctrine/ODM/MongoDB/SoftDelete/Tests/SoftDeleteManagerTest.php create mode 100644 tests/Doctrine/ODM/MongoDB/SoftDelete/Tests/UnitOfWorkTest.php create mode 100644 tests/bootstrap.php diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..f838e5d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "lib/vendor/doctrine-mongodb-odm"] + path = lib/vendor/doctrine-mongodb-odm + url = git://github.com/doctrine/mongodb-odm.git +[submodule "lib/vendor/doctrine-common"] + path = lib/vendor/doctrine-common + url = git://github.com/doctrine/common.git +[submodule "lib/vendor/doctrine-mongodb"] + path = lib/vendor/doctrine-mongodb + url = git://github.com/doctrine/mongodb.git diff --git a/README.markdown b/README.markdown new file mode 100644 index 0000000..52aaacd --- /dev/null +++ b/README.markdown @@ -0,0 +1,114 @@ +# Doctrine MongoDB ODM SoftDelete Functionality + +This library gives you some additional classes and API for managing the soft deleted state of Doctrine +MongoDB ODM documents. To get started you just need to configure a few objects and get a SoftDeleteManager +instance: + +## Setup + + use Doctrine\ODM\MongoDB\SoftDelete\Configuration; + use Doctrine\ODM\MongoDB\SoftDelete\UnitOfWork; + use Doctrine\ODM\MongoDB\SoftDelete\SoftDeleteManager; + use Doctrine\Common\EventManager; + + // $dm is a DocumentManager instance we should already have + + $config = new Configuration(); + $uow = new UnitOfWork($dm, $config); + $evm = new EventManager(); + $sdm = new SoftDeleteManager($dm, $config, $uow, $evm); + +## SoftDelete Documents + +In order for your documents to work with the SoftDelete functionality they must implement the +SoftDeleteable interface: + + interface SoftDeleteable + { + function getDeletedAt(); + function isDeleted(); + } + +An implementation might look like this: + + use Doctrine\ODM\MongoDB\SoftDelete\SoftDeleteable; + + /** @mongodb:Document */ + class User implements SoftDeleteable + { + // ... + + /** @mongodb:Date */ + private $deletedAt; + + public function getDeletedAt() + { + return $this->deletedAt; + } + + public function isDeleted() + { + return $this->deletedAt !== null ? true : false; + } + + // ... + } + +## Managing Soft Delete State + +Once you have the $sdm you can start managing the soft delete state of your documents: + + $jwage = $dm->getRepository('User')->findOneByUsername('jwage'); + $fabpot = $dm->getRepository('User')->findOneByUsername('fabpot'); + $sdm->delete($jwage); + $sdm->delete($fabpot); + $sdm->flush(); + +The above would issue a simple query setting the deleted date: + + db.users.update({ _id : { $in : userIds }}, { $set : { deletedAt : new Date() } }) + +Now if we were to restore the documents: + + $sdm->restore($jwage); + $sdm->flush(); + +It would unset the deletedAt date: + + db.users.update({ _id : { $in : userIds }}, { $unset : { deletedAt : true } }) + +## Events + +We trigger some additional event lifecycle events when documents are soft deleted or restored: + +* Events::preSoftDelete +* Events::postSoftDelete +* Events::preSoftDeleteRestore +* Events::postSoftDeleteRestore + +Using the events is easy, just define a class like the following: + + class TestEventSubscriber implements \Doctrine\Common\EventSubscriber + { + public function preSoftDelete(LifecycleEventArgs $args) + { + $document = $args->getDocument(); + $sdm = $args->getSoftDeleteManager(); + } + + public function getSubscribedEvents() + { + return array(Events::preSoftDelete); + } + } + +Now we just need to add the event subscriber to the EventManager: + + $eventSubscriber = new TestEventSubscriber(); + $evm->addEventSubscriber($eventSubscriber); + +When we soft delete something the preSoftDelete() method will be invoked before any queries are sent +to the database: + + $sdm->delete($fabpot); + $sdm->flush(); \ No newline at end of file diff --git a/lib/Doctrine/ODM/MongoDB/SoftDelete/Configuration.php b/lib/Doctrine/ODM/MongoDB/SoftDelete/Configuration.php new file mode 100644 index 0000000..05af493 --- /dev/null +++ b/lib/Doctrine/ODM/MongoDB/SoftDelete/Configuration.php @@ -0,0 +1,60 @@ +. + */ + +namespace Doctrine\ODM\MongoDB\SoftDelete; + +/** + * Configuration class for the SoftDelete functionality. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 1.0 + * @author Jonathan H. Wage + */ +class Configuration +{ + /** + * Array of configurable attributes. + * + * @var array + */ + private $attributes = array( + 'deletedFieldName' => 'deletedAt' + ); + + /** + * Sets the name of the field to use to store the deleted at date. + * + * @param string $deletedFieldName + */ + public function setDeletedFieldName($deletedFieldName) + { + $this->attributes['deletedFieldName'] = $deletedFieldName; + } + + /** + * Gets the name of the field used to store the deleted at date. + * + * @return string $deletedFieldName + */ + public function getDeletedFieldName() + { + return isset($this->attributes['deletedFieldName']) ? $this->attributes['deletedFieldName'] : 'deleted'; + } +} \ No newline at end of file diff --git a/lib/Doctrine/ODM/MongoDB/SoftDelete/Event/LifecycleEventArgs.php b/lib/Doctrine/ODM/MongoDB/SoftDelete/Event/LifecycleEventArgs.php new file mode 100644 index 0000000..1a0d5b8 --- /dev/null +++ b/lib/Doctrine/ODM/MongoDB/SoftDelete/Event/LifecycleEventArgs.php @@ -0,0 +1,72 @@ +. +*/ + +namespace Doctrine\ODM\MongoDB\SoftDelete\Event; + +use Doctrine\ODM\MongoDB\SoftDelete\SoftDeleteable; +use Doctrine\ODM\MongoDB\SoftDelete\SoftDeleteManager; +use Doctrine\Common\EventArgs; + +/** + * Lifecycle Events are triggered by the UnitOfWork during lifecycle transitions + * of documents. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 1.0 + * @author Jonathan H. Wage + */ +class LifecycleEventArgs extends EventArgs +{ + /** + * @var SoftDeleteManager $sdm + */ + private $sdm; + + /** + * @var SoftDeleteable $document + */ + private $document; + + public function __construct(SoftDeleteable $document, SoftDeleteManager $sdm) + { + $this->document = $document; + $this->sdm = $sdm; + } + + /** + * Gets the SoftDeletable document instance. + * + * @return SoftDeleteable $document + */ + public function getDocument() + { + return $this->document; + } + + /** + * Gets the SoftDeleteManager + * + * @return SoftDeleteManager + */ + public function getSoftDeleteManager() + { + return $this->sdm; + } +} \ No newline at end of file diff --git a/lib/Doctrine/ODM/MongoDB/SoftDelete/Events.php b/lib/Doctrine/ODM/MongoDB/SoftDelete/Events.php new file mode 100644 index 0000000..ce9ff47 --- /dev/null +++ b/lib/Doctrine/ODM/MongoDB/SoftDelete/Events.php @@ -0,0 +1,59 @@ +. + */ + +namespace Doctrine\ODM\MongoDB\SoftDelete; + +/** + * Container for all SoftDelete events. + * + * This class cannot be instantiated. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 1.0 + * @author Jonathan H. Wage + */ +final class Events +{ + private function __construct() {} + + /** + * Invoked before a document is soft deleted in the database. + */ + const preSoftDelete = 'preSoftDelete'; + + /** + * Invokes after a document is soft deleted in the database. + */ + const postSoftDelete = 'postSoftDelete'; + + + /** + * Invoked before a documents soft delete is removed and document restored in the database. + */ + const preSoftDeleteRestore = 'preSoftDeleteRestore'; + + /** + * Invoked after a documents soft delete is removed and document restored in the database. + */ + const postSoftDeleteRestore = 'postSoftDeleteRestore'; + +} \ No newline at end of file diff --git a/lib/Doctrine/ODM/MongoDB/SoftDelete/Persister.php b/lib/Doctrine/ODM/MongoDB/SoftDelete/Persister.php new file mode 100644 index 0000000..63b85b2 --- /dev/null +++ b/lib/Doctrine/ODM/MongoDB/SoftDelete/Persister.php @@ -0,0 +1,197 @@ +. + */ + +namespace Doctrine\ODM\MongoDB\SoftDelete; + +use Doctrine\ODM\MongoDB\DocumentManager; +use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; +use Doctrine\MongoDB\Collection; +use MongoDate; + +/** + * The Persister class is responsible for persisting the queued deletions and restorations. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 1.0 + * @author Jonathan H. Wage + */ +class Persister +{ + /** + * ClassMetadata instance. + * + * @var Doctrine\MongoDB\ODM\Mapping\ClassMetadata $class + */ + private $class; + + /** + * Database Collection instance. + * + * @var Doctrine\MongoDB\Collection $collection + */ + private $collection; + + /** + * Array of queued documents to be deleted. + * + * @var array + */ + private $queuedDeletes = array(); + + /** + * Array of queued documents to be restored. + * + * @var array + */ + private $queuedRestores = array(); + + /** + * Constructs a new Persister instance + * + * @param Configuration $configuration + * @param ClassMetadata $class + * @param Collection $collection + */ + public function __construct(Configuration $configuration, ClassMetadata $class, Collection $collection) + { + $this->configuration = $configuration; + $this->class = $class; + $this->collection = $collection; + } + + /** + * Gets the ClassMetadata instance. + * + * @return ClassMetadata $class + */ + public function getClass() + { + return $this->class; + } + + /** + * Gets the database Collection instance. + * + * @return Collection $collection + */ + public function getCollection() + { + return $this->collection; + } + + /** + * Add a SoftDeleteable document to the queued deletes. + * + * @param SoftDeleteable $document + */ + public function addDelete(SoftDeleteable $document) + { + $this->queuedDeletes[spl_object_hash($document)] = $document; + } + + /** + * Gets the array of SoftDeleteable documents queued for deletion. + * + * @return array $queuedDeletes + */ + public function getDeletes() + { + return $this->queuedDeletes; + } + + /** + * Add a SoftDeleteable document to the queued restores. + * + * @param SoftDeleteable $document + */ + public function addRestore(SoftDeleteable $document) + { + $this->queuedRestores[spl_object_hash($document)] = $document; + } + + /** + * Gets the array of SoftDeleteable documents queued for restoration. + * + * @return array $queuedRestores + */ + public function getRestores() + { + return $this->queuedRestores; + } + + /** + * Executes the queued deletes. + * + * @param MongoDate $date Date to the deleted field to. Mainly for testing. + */ + public function executeDeletes(MongoDate $date = null) + { + $ids = array(); + foreach ($this->queuedDeletes as $document) { + $ids[] = $this->class->getIdentifierObject($document); + } + + $query = array( + '_id' => array( + '$in' => $ids + ) + ); + $newObj = array( + '$set' => array( + $this->configuration->getDeletedFieldName() => $date ? $date : new MongoDate() + ) + ); + + $this->collection->update($query, $newObj, array( + 'multiple' => true, + 'safe' => true + )); + + $this->queuedDeletes = array(); + } + + /** + * Executes the queued restores. + */ + public function executeRestores() + { + $ids = array(); + foreach ($this->queuedRestores as $document) { + $ids[] = $this->class->getIdentifierObject($document); + } + + $query = array( + '_id' => array( + '$in' => $ids + ) + ); + $newObj = array( + '$unset' => array( + $this->configuration->getDeletedFieldName() => true + ) + ); + $this->collection->update($query, $newObj, array( + 'multiple' => true, + 'safe' => true + )); + + $this->queuedRestores = array(); + } +} \ No newline at end of file diff --git a/lib/Doctrine/ODM/MongoDB/SoftDelete/SoftDeleteManager.php b/lib/Doctrine/ODM/MongoDB/SoftDelete/SoftDeleteManager.php new file mode 100644 index 0000000..9481b0d --- /dev/null +++ b/lib/Doctrine/ODM/MongoDB/SoftDelete/SoftDeleteManager.php @@ -0,0 +1,183 @@ +. + */ + +namespace Doctrine\ODM\MongoDB\SoftDelete; + +use Doctrine\ODM\MongoDB\DocumentManager; +use Doctrine\Common\EventManager; + +/** + * The SoftDeleteManager class is responsible for managing the deleted state of a SoftDeleteable instance. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 1.0 + * @author Jonathan H. Wage + */ +class SoftDeleteManager +{ + /** + * DocumentManager instance this object wraps. + * + * @var DocumentManager $dm + */ + private $dm; + + /** + * The SoftDelete Configuration instance. + * + * @var Configuration $config + */ + private $config; + + /** + * The SoftDelete UnitOfWork instance. + * + * @var UnitOfWork $unitOfWork + */ + private $unitOfWork; + + /** + * The EventManager instance used for managing events. + * + * @var EventManager $eventManager + */ + private $eventManager; + + /** + * Constructs a new SoftDeleteManager instance. + * + * @param DocumentManager $dm + * @param Configuration $configuration + * @param UnitOfWork $unitOfWork + */ + public function __construct(DocumentManager $dm, Configuration $configuration, UnitOfWork $unitOfWork, EventManager $eventManager = null) + { + $this->dm = $dm; + $this->config = $configuration; + $this->unitOfWork = $unitOfWork; + $this->eventManager = $eventManager ?: new EventManager(); + $this->unitOfWork->setEventManager($this->eventManager); + $this->unitOfWork->setSoftDeleteManager($this); + } + + /** + * Gets the DocumentManager + * + * @return DocumentManager $dm + */ + public function getDocumentManager() + { + return $this->dm; + } + + /** + * Gets the Configuration + * + * @return Configuration $config + */ + public function getConfiguration() + { + return $this->config; + } + + /** + * Gets the UnitOfWork + * + * @return UnitOfWork $unitOfWork + */ + public function getUnitOfWork() + { + return $this->unitOfWork; + } + + /** + * Gets the EventManager + * + * @return EventManager $eventManager + */ + public function getEventManager() + { + return $this->eventManager; + } + + /** + * Creates a new query builder instance that will automatically exclude deleted documents + * by adding a { deletedAt : { $exists : false } } condition. + * + * @param string $documentName The document class name to create the query builder for. + * @return Doctrine\MongoDB\ODM\Query\Builder $qb + */ + public function createQueryBuilder($documentName = null) + { + return $this->dm->createQueryBuilder($documentName) + ->field($this->config->getDeletedFieldName()) + ->exists(false); + } + + /** + * Creates a new query builder instance that will return only deleted documents + * by adding a { deletedAt : { $exists : true } } condition. + * + * @param string $documentName The document class name to create the query builder for. + * @return Doctrine\MongoDB\ODM\Query\Builder $qb + */ + public function createDeletedQueryBuilder($documentName = null) + { + return $this->dm->createQueryBuilder($documentName) + ->field($this->config->getDeletedFieldName()) + ->exists(true); + } + + /** + * Schedules a SoftDeleteable document for soft deletion on next flush(). + * + * @param SoftDeleteable $document + */ + public function delete(SoftDeleteable $document) + { + $this->unitOfWork->delete($document); + } + + /** + * Schedulds a SoftDeleteable document for soft delete restoration on next flush(). + * + * @param SoftDeleteable $document + */ + public function restore(SoftDeleteable $document) + { + $this->unitOfWork->restore($document); + } + + /** + * Flushes all scheduled deletions and restorations to the database. + */ + public function flush() + { + $this->unitOfWork->commit(); + } + + /** + * Clears the UnitOfWork and erases any currently scheduled deletions or restorations. + */ + public function clear() + { + $this->unitOfWork->clear(); + } +} \ No newline at end of file diff --git a/lib/Doctrine/ODM/MongoDB/SoftDelete/SoftDeleteable.php b/lib/Doctrine/ODM/MongoDB/SoftDelete/SoftDeleteable.php new file mode 100644 index 0000000..b34c39f --- /dev/null +++ b/lib/Doctrine/ODM/MongoDB/SoftDelete/SoftDeleteable.php @@ -0,0 +1,48 @@ +. + */ + +namespace Doctrine\ODM\MongoDB\SoftDelete; + +use Doctrine\ODM\MongoDB\DocumentManager; + +/** + * Interface for Doctrine MongoDB ODM documents to implement if they want to use + * the SoftDelete functionality. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 1.0 + * @author Jonathan H. Wage + */ +interface SoftDeleteable +{ + /** + * Gets the date that this object was deleted at. + * + * @return DateTime $deletedAt + */ + function getDeletedAt(); + + /** + * Checks whether this object is deleted or not. + * + * @return boolean $isDeleted + */ + function isDeleted(); +} \ No newline at end of file diff --git a/lib/Doctrine/ODM/MongoDB/SoftDelete/UnitOfWork.php b/lib/Doctrine/ODM/MongoDB/SoftDelete/UnitOfWork.php new file mode 100644 index 0000000..a0b93de --- /dev/null +++ b/lib/Doctrine/ODM/MongoDB/SoftDelete/UnitOfWork.php @@ -0,0 +1,350 @@ +. + */ + +namespace Doctrine\ODM\MongoDB\SoftDelete; + +use Doctrine\ODM\MongoDB\DocumentManager; +use Doctrine\ODM\MongoDB\SoftDelete\Configuration; +use Doctrine\Common\EventManager; +use InvalidArgumentException; +use DateTime; + +/** + * UnitOfWork is responsible for tracking the deleted state of objects and giving you the ability + * to queue deletions and restorations to be committed. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 1.0 + * @author Jonathan H. Wage + */ +class UnitOfWork +{ + /** + * The DocumentManager instance. + * + * @var DocumentManager $dm + */ + private $dm; + + /** + * The SoftDeleteManager instance. + * + * @var string + */ + private $sdm; + + /** + * The SoftDelete configuration instance/ + * + * @var Configuration $configuration + */ + private $configuration; + + /** + * The EventManager instance used for managing events. + * + * @var EventManager $eventManager + */ + private $eventManager; + + /** + * Array of scheduled document deletes. + * + * @var array + */ + private $documentDeletes = array(); + + /** + * Array of scheduled document restores. + * + * @var array + */ + private $documentRestores = array(); + + /** + * Array of lazily instantiated soft delete document persisters. + * + * @var string + */ + private $persisters = array(); + + /** + * Constructs a new UnitOfWork instance. + * + * @param DocumentManager $dm + * @param Configuration $configuration + */ + public function __construct(DocumentManager $dm, Configuration $configuration) + { + $this->dm = $dm; + $this->configuration = $configuration; + } + + /** + * Sets the SoftDeleteManager this UnitOfWork belongs to. + * + * @param SoftDeleteManager $sdm + */ + public function setSoftDeleteManager(SoftDeleteManager $sdm) + { + $this->sdm = $sdm; + } + + /** + * Gets the SoftDeleteManager this UnitOfWork belongs to. + * + * @return SoftDeleteManager $sdm + */ + public function getSoftDeleteManager() + { + return $this->sdm; + } + + /** + * Sets the UnitOfWork event manager. + * + * @param EventManager $eventManager + */ + public function setEventManager(EventManager $eventManager) + { + $this->eventManager = $eventManager; + } + + /** + * Gets the UnitOfWork event manager. + * + * @return EventManager $eventManager + */ + public function getEventManager() + { + return $this->eventManager; + } + + /** + * Gets the DocumentManager instance + * + * @return DocumentManager $dm + */ + public function getDocumentManager() + { + return $this->dm; + } + + /** + * Gets the array of scheduled document deletes. + * + * @return array $documentDeletes + */ + public function getDocumentDeletes() + { + return $this->documentDeletes; + } + + /** + * Gets the array of scheduled document restores. + * + * @return array $documentRestores + */ + public function getDocumentRestores() + { + return $this->documentRestores; + } + + /** + * Checks if a given SoftDeleteable document instance is currently scheduled for delete. + * + * @param SoftDeleteable $document + */ + public function isScheduledForDelete(SoftDeleteable $document) + { + return isset($this->documentDeletes[spl_object_hash($document)]) ? true : false; + } + + /** + * Checks if a given SoftDeleteable document instance is currently scheduled for restore. + * + * @param SoftDeleteable $document + */ + public function isScheduledForRestore(SoftDeleteable $document) + { + return isset($this->documentRestores[spl_object_hash($document)]) ? true : false; + } + + /** + * Gets or creates a Persister instance for the given class name. + * + * @param string $className + * @return Persister $persister + */ + public function getDocumentPersister($className) + { + if (isset($this->persisters[$className])) { + return $this->persisters[$className]; + } + $class = $this->dm->getClassMetadata($className); + $collection = $this->dm->getDocumentCollection($className); + $this->persisters[$className] = new Persister($this->configuration, $class, $collection); + return $this->persisters[$className]; + } + + /** + * Schedules a SoftDeleteable document instance for deletion on next flush. + * + * @param SoftDeleteable $document + * @throws InvalidArgumentException + */ + public function delete(SoftDeleteable $document) + { + if ($document->isDeleted() === true) { + throw new InvalidArgumentException('Document is already deleted.'); + } + + $oid = spl_object_hash($document); + if (isset($this->documentDeletes[$oid])) { + throw new InvalidArgumentException('Document is already scheduled for delete.'); + } + + // If scheduled for restore then remove it + unset($this->documentRestores[$oid]); + + $this->documentDeletes[$oid] = $document; + } + + /** + * Schedules a SoftDeleteable document instance for restoration on next flush. + * + * @param SoftDeleteable $document + * @throws InvalidArgumentException + */ + public function restore(SoftDeleteable $document) + { + if ($document->isDeleted() === false) { + throw new InvalidArgumentException('Document must be already deleted or scheduled for delete in order to be restored.'); + } + + $oid = spl_object_hash($document); + if (isset($this->documentRestores[$oid])) { + throw new InvalidArgumentException('Document is already scheduled for restore.'); + } + + // If scheduled for delete then remove it + unset($this->documentDeletes[$oid]); + + $this->documentRestores[$oid] = $document; + } + + /** + * Commits all the scheduled deletions and restorations to the database. + */ + public function commit() + { + // document deletes + if ($this->documentDeletes) { + $this->executeDeletes(); + } + + // document restores + if ($this->documentRestores) { + $this->executeRestores(); + } + } + + /** + * Clears the UnitOfWork and forgets any currently scheduled deletions or restorations. + */ + public function clear() + { + $this->documentDeletes = array(); + $this->documentRestores = array(); + } + + /** + * Executes the queued deletions. + */ + private function executeDeletes() + { + $deletedFieldName = $this->configuration->getDeletedFieldName(); + + $documentDeletes = array(); + foreach ($this->documentDeletes as $document) { + $className = get_class($document); + $documentDeletes[$className][] = $document; + } + foreach ($documentDeletes as $className => $documents) { + $persister = $this->getDocumentPersister($className); + foreach ($documents as $document) { + + if ($this->eventManager->hasListeners(Events::preSoftDelete)) { + $this->eventManager->dispatchEvent(Events::preSoftDelete, new Event\LifecycleEventArgs($document, $this->sdm)); + } + + $persister->addDelete($document); + } + $persister->executeDeletes(); + + $class = $this->dm->getClassMetadata($className); + + $date = new DateTime(); + foreach ($documents as $document) { + $class->setFieldValue($document, $deletedFieldName, $date); + + if ($this->eventManager->hasListeners(Events::postSoftDelete)) { + $this->eventManager->dispatchEvent(Events::postSoftDelete, new Event\LifecycleEventArgs($document, $this->sdm)); + } + } + } + } + + /** + * Executes the queued restorations. + */ + private function executeRestores() + { + $deletedFieldName = $this->configuration->getDeletedFieldName(); + + $documentRestores = array(); + foreach ($this->documentRestores as $document) { + $className = get_class($document); + $documentRestores[$className][] = $document; + } + foreach ($documentRestores as $className => $documents) { + $persister = $this->getDocumentPersister($className); + foreach ($documents as $document) { + + if ($this->eventManager->hasListeners(Events::preSoftDeleteRestore)) { + $this->eventManager->dispatchEvent(Events::preSoftDeleteRestore, new Event\LifecycleEventArgs($document, $this->sdm)); + } + + $persister->addRestore($document); + } + $persister->executeRestores(); + + $class = $this->dm->getClassMetadata($className); + + foreach ($documents as $document) { + $class->setFieldValue($document, $deletedFieldName, null); + + if ($this->eventManager->hasListeners(Events::postSoftDeleteRestore)) { + $this->eventManager->dispatchEvent(Events::postSoftDeleteRestore, new Event\LifecycleEventArgs($document, $this->sdm)); + } + } + } + } +} \ No newline at end of file diff --git a/lib/vendor/doctrine-common b/lib/vendor/doctrine-common new file mode 160000 index 0000000..602de4d --- /dev/null +++ b/lib/vendor/doctrine-common @@ -0,0 +1 @@ +Subproject commit 602de4d6d8813e567c8e5333d85bda28b4583ffc diff --git a/lib/vendor/doctrine-mongodb b/lib/vendor/doctrine-mongodb new file mode 160000 index 0000000..a467d22 --- /dev/null +++ b/lib/vendor/doctrine-mongodb @@ -0,0 +1 @@ +Subproject commit a467d22d6ccbcae42aa89f4cdf6d35ea957b5ff9 diff --git a/lib/vendor/doctrine-mongodb-odm b/lib/vendor/doctrine-mongodb-odm new file mode 160000 index 0000000..a0667bb --- /dev/null +++ b/lib/vendor/doctrine-mongodb-odm @@ -0,0 +1 @@ +Subproject commit a0667bb05867b101333c6031daeedff80f937b8a diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..470ebbe --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,18 @@ + + + + + + ./tests/Doctrine/ + + + \ No newline at end of file diff --git a/tests/Doctrine/ODM/MongoDB/SoftDelete/Tests/ConfigurationTest.php b/tests/Doctrine/ODM/MongoDB/SoftDelete/Tests/ConfigurationTest.php new file mode 100644 index 0000000..8ca6f7e --- /dev/null +++ b/tests/Doctrine/ODM/MongoDB/SoftDelete/Tests/ConfigurationTest.php @@ -0,0 +1,22 @@ +assertEquals('deletedAt', $configuration->getDeletedFieldName()); + } + + public function testSetDeletedFieldName() + { + $configuration = new Configuration(); + $configuration->setDeletedFieldName('test'); + $this->assertEquals('test', $configuration->getDeletedFieldName()); + } +} \ No newline at end of file diff --git a/tests/Doctrine/ODM/MongoDB/SoftDelete/Tests/FunctionalTest.php b/tests/Doctrine/ODM/MongoDB/SoftDelete/Tests/FunctionalTest.php new file mode 100644 index 0000000..8f52cb3 --- /dev/null +++ b/tests/Doctrine/ODM/MongoDB/SoftDelete/Tests/FunctionalTest.php @@ -0,0 +1,267 @@ +sdm = $this->getTestSoftDeleteManager(); + $this->dm = $this->sdm->getDocumentManager(); + $this->dm->getDocumentCollection(__NAMESPACE__.'\Seller')->drop(); + } + + public function testDelete() + { + $softDeleteable = $this->getTestSoftDeleteable('jwage'); + $this->dm->persist($softDeleteable); + $this->dm->flush(); + + $this->sdm->delete($softDeleteable); + $this->sdm->flush(); + + $this->assertTrue($softDeleteable->isDeleted()); + $this->assertInstanceOf('DateTime', $softDeleteable->getDeletedAt()); + + $check = $this->dm->getDocumentCollection(get_class($softDeleteable))->findOne(); + $this->assertTrue(isset($check['deletedAt'])); + $this->assertInstanceOf('MongoDate', $check['deletedAt']); + } + + public function testDeleteMultiple() + { + $softDeleteable1 = $this->getTestSoftDeleteable('jwage1'); + $softDeleteable2 = $this->getTestSoftDeleteable('jwage2'); + $this->dm->persist($softDeleteable1); + $this->dm->persist($softDeleteable2); + $this->dm->flush(); + + $this->sdm->delete($softDeleteable1); + $this->sdm->delete($softDeleteable2); + $this->sdm->flush(); + + $this->assertTrue($softDeleteable1->isDeleted()); + $this->assertInstanceOf('DateTime', $softDeleteable1->getDeletedAt()); + $this->assertTrue($softDeleteable2->isDeleted()); + $this->assertInstanceOf('DateTime', $softDeleteable2->getDeletedAt()); + + $check1 = $this->dm->getDocumentCollection(get_class($softDeleteable1))->findOne(); + $this->assertTrue(isset($check1['deletedAt'])); + $this->assertInstanceOf('MongoDate', $check1['deletedAt']); + + $check2 = $this->dm->getDocumentCollection(get_class($softDeleteable2))->findOne(); + $this->assertTrue(isset($check2['deletedAt'])); + $this->assertInstanceOf('MongoDate', $check2['deletedAt']); + + $this->sdm->restore($softDeleteable1); + $this->sdm->restore($softDeleteable2); + $this->sdm->flush(); + + $check1 = $this->dm->getDocumentCollection(get_class($softDeleteable1))->findOne(); + $this->assertFalse(isset($check1['deletedAt'])); + + $check2 = $this->dm->getDocumentCollection(get_class($softDeleteable2))->findOne(); + $this->assertFalse(isset($check2['deletedAt'])); + } + + public function testRestore() + { + $softDeleteable = $this->getTestSoftDeleteable('jwage'); + $this->dm->persist($softDeleteable); + $this->dm->flush(); + + $this->sdm->delete($softDeleteable); + $this->sdm->flush(); + + $check = $this->dm->getDocumentCollection(get_class($softDeleteable))->findOne(); + $this->assertTrue(isset($check['deletedAt'])); + $this->assertInstanceOf('MongoDate', $check['deletedAt']); + + $this->sdm->restore($softDeleteable); + $this->sdm->flush(); + + $this->assertFalse($softDeleteable->isDeleted()); + $this->assertNull($softDeleteable->getDeletedAt()); + + $check = $this->dm->getDocumentCollection(get_class($softDeleteable))->findOne(); + $this->assertFalse(isset($check['deletedAt'])); + } + + public function testCreateQueryBuilder() + { + $softDeleteable = $this->getTestSoftDeleteable('jwage'); + $this->dm->persist($softDeleteable); + $this->dm->flush(); + + $check = $this->sdm->createQueryBuilder(get_class($softDeleteable)) + ->getQuery() + ->getSingleResult(); + $this->assertNotNull($check); + + $this->sdm->delete($softDeleteable); + $this->sdm->flush(); + + $check = $this->sdm->createQueryBuilder(get_class($softDeleteable)) + ->getQuery() + ->getSingleResult(); + $this->assertNull($check); + + $check = $this->sdm->createDeletedQueryBuilder(get_class($softDeleteable)) + ->getQuery() + ->getSingleResult(); + $this->assertNotNull($check); + $this->assertTrue($check->isDeleted()); + $this->assertInstanceOf('DateTime', $check->getDeletedAt()); + } + + public function testEvents() + { + $eventManager = $this->sdm->getEventManager(); + $eventSubscriber = new TestEventSubscriber(); + $eventManager->addEventSubscriber($eventSubscriber); + + $softDeleteable = $this->getTestSoftDeleteable('jwage'); + $this->dm->persist($softDeleteable); + $this->dm->flush(); + + $this->sdm->delete($softDeleteable); + $this->sdm->flush(); + + $this->assertEquals(array('preSoftDelete', 'postSoftDelete'), $eventSubscriber->called); + + $eventSubscriber->called = array(); + + $this->sdm->restore($softDeleteable); + $this->sdm->flush(); + + $this->assertEquals(array('preSoftDeleteRestore', 'postSoftDeleteRestore'), $eventSubscriber->called); + } + + private function getTestDocumentManager() + { + $configuration = new ODMConfiguration(); + $configuration->setHydratorDir(__DIR__); + $configuration->setHydratorNamespace('TestHydrator'); + $configuration->setProxyDir(__DIR__); + $configuration->setProxyNamespace('TestProxy'); + + $reader = new AnnotationReader(); + $reader->setDefaultAnnotationNamespace('Doctrine\ODM\MongoDB\Mapping\\'); + $annotationDriver = new AnnotationDriver($reader, __DIR__ . '/Documents'); + $configuration->setMetadataDriverImpl($annotationDriver); + + $conn = new Connection(null, array(), $configuration); + return DocumentManager::create($conn, $configuration); + } + + public function getTestSoftDeleteable($name) + { + return new Seller($name); + } + + public function getTestConfiguration() + { + return new Configuration(); + } + + public function getTestUnitOfWork(DocumentManager $dm, Configuration $configuration) + { + return new UnitOfWork($dm, $configuration); + } + + public function getTestEventManager() + { + return new EventManager(); + } + + private function getTestSoftDeleteManager() + { + $dm = $this->getTestDocumentManager(); + $configuration = $this->getTestConfiguration(); + $unitOfWork = $this->getTestUnitOfWork($dm, $configuration); + $eventManager = $this->getTestEventManager(); + return new SoftDeleteManager($dm, $configuration, $unitOfWork, $eventManager); + } +} + +class TestEventSubscriber implements \Doctrine\Common\EventSubscriber +{ + public $called = array(); + + public function preSoftDelete(LifecycleEventArgs $args) + { + $this->called[] = 'preSoftDelete'; + } + + public function postSoftDelete(LifecycleEventArgs $args) + { + $this->called[] = 'postSoftDelete'; + } + + public function preSoftDeleteRestore(LifecycleEventArgs $args) + { + $this->called[] = 'preSoftDeleteRestore'; + } + + public function postSoftDeleteRestore(LifecycleEventArgs $args) + { + $this->called[] = 'postSoftDeleteRestore'; + } + + public function getSubscribedEvents() + { + return array( + Events::preSoftDelete, + Events::postSoftDelete, + Events::preSoftDeleteRestore, + Events::postSoftDeleteRestore + ); + } +} + +/** @Document */ +class Seller implements SoftDeleteable +{ + /** @Id */ + private $id; + + /** @Date @Index */ + private $deletedAt; + + /** @String */ + private $name; + + public function __construct($name) + { + $this->name = $name; + } + + public function getName() + { + return $this->name; + } + + public function getDeletedAt() + { + return $this->deletedAt; + } + + public function isDeleted() + { + return $this->deletedAt !== null ? true : false; + } +} \ No newline at end of file diff --git a/tests/Doctrine/ODM/MongoDB/SoftDelete/Tests/PersisterTest.php b/tests/Doctrine/ODM/MongoDB/SoftDelete/Tests/PersisterTest.php new file mode 100644 index 0000000..3da9353 --- /dev/null +++ b/tests/Doctrine/ODM/MongoDB/SoftDelete/Tests/PersisterTest.php @@ -0,0 +1,151 @@ +getMockClassMetadata(); + $collection = $this->getMockCollection(); + $configuration = $this->getMockConfiguration(); + $persister = $this->getTestPersister($configuration, $class, $collection); + $this->assertSame($class, $persister->getClass()); + $this->assertSame($collection, $persister->getCollection()); + } + + public function testAddDelete() + { + $class = $this->getMockClassMetadata(); + $collection = $this->getMockCollection(); + $configuration = $this->getMockConfiguration(); + $persister = $this->getTestPersister($configuration, $class, $collection); + $mockSoftDeleteable = $this->getMockSoftDeletable(); + $persister->addDelete($mockSoftDeleteable); + + $expects = array(spl_object_hash($mockSoftDeleteable) => $mockSoftDeleteable); + $this->assertEquals($expects, $persister->getDeletes()); + } + + public function testAddRestore() + { + $class = $this->getMockClassMetadata(); + $collection = $this->getMockCollection(); + $configuration = $this->getMockConfiguration(); + $persister = $this->getTestPersister($configuration, $class, $collection); $mockSoftDeleteable = $this->getMockSoftDeletable(); + $persister->addRestore($mockSoftDeleteable); + + $expects = array(spl_object_hash($mockSoftDeleteable) => $mockSoftDeleteable); + $this->assertEquals($expects, $persister->getRestores()); + } + + public function testExecuteDeletes() + { + $mockSoftDeleteable = $this->getMockSoftDeletable(); + + $class = $this->getMockClassMetadata(); + $class->expects($this->once()) + ->method('getIdentifierObject') + ->with($mockSoftDeleteable) + ->will($this->returnValue(1)); + + $date = new MongoDate(); + + $collection = $this->getMockCollection(); + $collection->expects($this->once()) + ->method('update') + ->with( + array('_id' => array('$in' => array(1))), + array('$set' => array( + 'deletedAt' => $date + )), + array( + 'multiple' => true, + 'safe' => true + ) + ); + + $configuration = $this->getMockConfiguration(); + $configuration->expects($this->once()) + ->method('getDeletedFieldName') + ->will($this->returnValue('deletedAt')); + + $persister = $this->getTestPersister($configuration, $class, $collection); + $persister->addDelete($mockSoftDeleteable); + $persister->executeDeletes($date); + } + + public function testExecuteRestores() + { + $mockSoftDeleteable = $this->getMockSoftDeletable(); + + $class = $this->getMockClassMetadata(); + $class->expects($this->once()) + ->method('getIdentifierObject') + ->with($mockSoftDeleteable) + ->will($this->returnValue(1)); + + $collection = $this->getMockCollection(); + $collection->expects($this->once()) + ->method('update') + ->with( + array('_id' => array('$in' => array(1))), + array('$unset' => array( + 'deletedAt' => true + )), + array( + 'multiple' => true, + 'safe' => true + ) + ); + + $configuration = $this->getMockConfiguration(); + $configuration->expects($this->once()) + ->method('getDeletedFieldName') + ->will($this->returnValue('deletedAt')); + + $persister = $this->getTestPersister($configuration, $class, $collection); + $persister->addRestore($mockSoftDeleteable); + $persister->executeRestores(); + } + + private function getMockSoftDeletable() + { + return $this->getMockBuilder('Doctrine\ODM\MongoDB\SoftDelete\SoftDeleteable') + ->disableOriginalConstructor() + ->getMock(); + } + + private function getMockClassMetadata() + { + return $this->getMockBuilder('Doctrine\ODM\MongoDB\Mapping\ClassMetadata') + ->disableOriginalConstructor() + ->getMock(); + } + + private function getMockCollection() + { + return $this->getMockBuilder('Doctrine\MongoDB\Collection') + ->disableOriginalConstructor() + ->getMock(); + } + + private function getMockConfiguration() + { + return $this->getMockBuilder('Doctrine\ODM\MongoDB\SoftDelete\Configuration') + ->disableOriginalConstructor() + ->getMock(); + } + + private function getTestPersister(Configuration $configuration, ClassMetadata $class, Collection $collection) + { + return new Persister($configuration, $class, $collection); + } +} \ No newline at end of file diff --git a/tests/Doctrine/ODM/MongoDB/SoftDelete/Tests/SoftDeletableTest.php b/tests/Doctrine/ODM/MongoDB/SoftDelete/Tests/SoftDeletableTest.php new file mode 100644 index 0000000..7a3f34b --- /dev/null +++ b/tests/Doctrine/ODM/MongoDB/SoftDelete/Tests/SoftDeletableTest.php @@ -0,0 +1,34 @@ +getMockSoftDeleteable(); + $mockSoftDeleteable->expects($this->once()) + ->method('getDeletedAt') + ->will($this->returnValue($date)); + + $mockSoftDeleteable->expects($this->once()) + ->method('isDeleted') + ->will($this->returnValue(true)); + + $this->assertSame($date, $mockSoftDeleteable->getDeletedAt()); + $this->assertTrue($mockSoftDeleteable->isDeleted()); + } + + private function getMockSoftDeleteable() + { + return $this->getMockBuilder('Doctrine\ODM\MongoDB\SoftDelete\SoftDeleteable') + ->disableOriginalConstructor() + ->getMock(); + } +} \ No newline at end of file diff --git a/tests/Doctrine/ODM/MongoDB/SoftDelete/Tests/SoftDeleteManagerTest.php b/tests/Doctrine/ODM/MongoDB/SoftDelete/Tests/SoftDeleteManagerTest.php new file mode 100644 index 0000000..b715ecd --- /dev/null +++ b/tests/Doctrine/ODM/MongoDB/SoftDelete/Tests/SoftDeleteManagerTest.php @@ -0,0 +1,179 @@ +getMockDocumentManager(); + $configuration = $this->getConfiguration(); + $uow = $this->getMockUnitOfWork(); + $sdm = $this->getTestSoftDeleteManager($dm, $configuration, $uow); + $this->assertSame($dm, $sdm->getDocumentManager()); + } + + public function testCreateQueryBuilder() + { + $mockQb = $this->getMockQb(); + + $dm = $this->getMockDocumentManager(); + $dm->expects($this->once()) + ->method('createQueryBuilder') + ->will($this->returnValue($mockQb)); + + $mockQb->expects($this->once()) + ->method('field') + ->with('deletedAt') + ->will($this->returnValue($mockQb)); + + $mockQb->expects($this->once()) + ->method('exists') + ->with(false) + ->will($this->returnValue($mockQb)); + + $configuration = $this->getConfiguration(); + $uow = $this->getMockUnitOfWork(); + $sdm = $this->getTestSoftDeleteManager($dm, $configuration, $uow); + + $qb = $sdm->createQueryBuilder(); + $this->assertSame($mockQb, $qb); + } + + public function testCreateDeletedQueryBuilder() + { + $mockQb = $this->getMockQb(); + + $dm = $this->getMockDocumentManager(); + $dm->expects($this->once()) + ->method('createQueryBuilder') + ->will($this->returnValue($mockQb)); + + $mockQb->expects($this->once()) + ->method('field') + ->with('deletedAt') + ->will($this->returnValue($mockQb)); + + $mockQb->expects($this->once()) + ->method('exists') + ->with(true) + ->will($this->returnValue($mockQb)); + + $configuration = $this->getConfiguration(); + $uow = $this->getMockUnitOfWork(); + $sdm = $this->getTestSoftDeleteManager($dm, $configuration, $uow); + + $qb = $sdm->createDeletedQueryBuilder(); + $this->assertSame($mockQb, $qb); + } + + public function testDelete() + { + $dm = $this->getMockDocumentManager(); + $configuration = $this->getConfiguration(); + $uow = $this->getMockUnitOfWork(); + + $mockSoftDeleteable = $this->getMockSoftDeletable(); + + $uow->expects($this->once()) + ->method('delete') + ->with($mockSoftDeleteable); + + $sdm = $this->getTestSoftDeleteManager($dm, $configuration, $uow); + + $sdm->delete($mockSoftDeleteable); + } + + public function testRestore() + { + $dm = $this->getMockDocumentManager(); + $configuration = $this->getConfiguration(); + $uow = $this->getMockUnitOfWork(); + + $mockSoftDeleteable = $this->getMockSoftDeletable(); + + $uow->expects($this->once()) + ->method('restore') + ->with($mockSoftDeleteable); + + $sdm = $this->getTestSoftDeleteManager($dm, $configuration, $uow); + + $sdm->restore($mockSoftDeleteable); + } + + public function testFlush() + { + $dm = $this->getMockDocumentManager(); + $configuration = $this->getConfiguration(); + $uow = $this->getMockUnitOfWork(); + + $mockSoftDeleteable = $this->getMockSoftDeletable(); + + $uow->expects($this->once()) + ->method('commit'); + + $sdm = $this->getTestSoftDeleteManager($dm, $configuration, $uow); + + $sdm->flush(); + } + + public function testClear() + { + $dm = $this->getMockDocumentManager(); + $configuration = $this->getConfiguration(); + $uow = $this->getMockUnitOfWork(); + + $mockSoftDeleteable = $this->getMockSoftDeletable(); + + $uow->expects($this->once()) + ->method('clear'); + + $sdm = $this->getTestSoftDeleteManager($dm, $configuration, $uow); + + $sdm->clear(); + } + + private function getTestSoftDeleteManager(DocumentManager $dm, Configuration $configuration, UnitOfWork $uow) + { + return new SoftDeleteManager($dm, $configuration, $uow); + } + + private function getMockSoftDeletable() + { + return $this->getMockBuilder('Doctrine\ODM\MongoDB\SoftDelete\SoftDeleteable') + ->disableOriginalConstructor() + ->getMock(); + } + + private function getMockDocumentManager() + { + return $this->getMockBuilder('Doctrine\ODM\MongoDB\DocumentManager') + ->disableOriginalConstructor() + ->getMock(); + } + + private function getMockQb() + { + return $this->getMockBuilder('Doctrine\ODM\MongoDB\Query\Builder') + ->disableOriginalConstructor() + ->getMock(); + } + + private function getConfiguration() + { + return new Configuration(); + } + + private function getMockUnitOfWork() + { + return $this->getMockBuilder('Doctrine\ODM\MongoDB\SoftDelete\UnitOfWork') + ->disableOriginalConstructor() + ->getMock(); + } +} \ No newline at end of file diff --git a/tests/Doctrine/ODM/MongoDB/SoftDelete/Tests/UnitOfWorkTest.php b/tests/Doctrine/ODM/MongoDB/SoftDelete/Tests/UnitOfWorkTest.php new file mode 100644 index 0000000..ae59beb --- /dev/null +++ b/tests/Doctrine/ODM/MongoDB/SoftDelete/Tests/UnitOfWorkTest.php @@ -0,0 +1,139 @@ +getMockDocumentManager(); + $configuration = $this->getMockConfiguration(); + $uow = $this->getTestUnitOfWork($dm, $configuration); + $this->assertSame($dm, $uow->getDocumentManager()); + } + + public function testDelete() + { + $mockSoftDeleteable = $this->getMockSoftDeletable(); + $mockSoftDeleteable->expects($this->once()) + ->method('isDeleted') + ->will($this->returnValue(false)); + + $dm = $this->getMockDocumentManager(); + $configuration = $this->getMockConfiguration(); + $uow = $this->getTestUnitOfWork($dm, $configuration); + $uow->delete($mockSoftDeleteable); + $this->assertTrue($uow->isScheduledForDelete($mockSoftDeleteable)); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testDeleteThrowsExceptionIfAlreadyDeleted() + { + $mockSoftDeleteable = $this->getMockSoftDeletable(); + $mockSoftDeleteable->expects($this->once()) + ->method('isDeleted') + ->will($this->returnValue(true)); + + $dm = $this->getMockDocumentManager(); + $configuration = $this->getMockConfiguration(); + $uow = $this->getTestUnitOfWork($dm, $configuration); + $uow->delete($mockSoftDeleteable); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testDeleteThrowsExceptionIfAlreadyScheduledForDelete() + { + $mockSoftDeleteable = $this->getMockSoftDeletable(); + $mockSoftDeleteable->expects($this->any()) + ->method('isDeleted') + ->will($this->returnValue(false)); + + $dm = $this->getMockDocumentManager(); + $configuration = $this->getMockConfiguration(); + $uow = $this->getTestUnitOfWork($dm, $configuration); + $uow->delete($mockSoftDeleteable); + $uow->delete($mockSoftDeleteable); + } + + public function testRestore() + { + $mockSoftDeleteable = $this->getMockSoftDeletable(); + $mockSoftDeleteable->expects($this->once()) + ->method('isDeleted') + ->will($this->returnValue(true)); + + $dm = $this->getMockDocumentManager(); + $configuration = $this->getMockConfiguration(); + $uow = $this->getTestUnitOfWork($dm, $configuration); + $uow->restore($mockSoftDeleteable); + $this->assertTrue($uow->isScheduledForRestore($mockSoftDeleteable)); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testRestoreThrowsExceptionIfNotDeleted() + { + $mockSoftDeleteable = $this->getMockSoftDeletable(); + $mockSoftDeleteable->expects($this->once()) + ->method('isDeleted') + ->will($this->returnValue(false)); + + $dm = $this->getMockDocumentManager(); + $configuration = $this->getMockConfiguration(); + $uow = $this->getTestUnitOfWork($dm, $configuration); + $uow->restore($mockSoftDeleteable); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testRestoreThrowsExceptionIfAlreadyScheduledForRestore() + { + $mockSoftDeleteable = $this->getMockSoftDeletable(); + $mockSoftDeleteable->expects($this->any()) + ->method('isDeleted') + ->will($this->returnValue(false)); + + $dm = $this->getMockDocumentManager(); + $configuration = $this->getMockConfiguration(); + $uow = $this->getTestUnitOfWork($dm, $configuration); + $uow->restore($mockSoftDeleteable); + $uow->restore($mockSoftDeleteable); + } + + private function getTestUnitOfWork(DocumentManager $dm, Configuration $configuration) + { + return new UnitOfWork($dm, $configuration); + } + + private function getMockConfiguration() + { + return $this->getMockBuilder('Doctrine\ODM\MongoDB\SoftDelete\Configuration') + ->disableOriginalConstructor() + ->getMock(); + } + + private function getMockDocumentManager() + { + return $this->getMockBuilder('Doctrine\ODM\MongoDB\DocumentManager') + ->disableOriginalConstructor() + ->getMock(); + } + + private function getMockSoftDeletable() + { + return $this->getMockBuilder('Doctrine\ODM\MongoDB\SoftDelete\SoftDeleteable') + ->disableOriginalConstructor() + ->getMock(); + } +} \ No newline at end of file diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..88aff73 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,20 @@ +register(); + +$classLoader = new ClassLoader('Doctrine\ODM\MongoDB\SoftDelete', __DIR__ . '/../lib'); +$classLoader->register(); + +$classLoader = new ClassLoader('Doctrine\ODM\MongoDB', __DIR__ . '/../lib/vendor/doctrine-mongodb-odm/lib'); +$classLoader->register(); + +$classLoader = new ClassLoader('Doctrine\MongoDB', __DIR__ . '/../lib/vendor/doctrine-mongodb/lib'); +$classLoader->register(); + +$classLoader = new ClassLoader('Doctrine\Common', __DIR__ . '/../lib/vendor/doctrine-common/lib'); +$classLoader->register(); \ No newline at end of file