diff --git a/CHANGELOG.md b/CHANGELOG.md index ebc62e6..3c78d54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## master (2015-08-12) + * Persistance now fire's document's events (beforeSave/beforeInsert/beforeUpdate) + ## 1.13.5 (2015-08-07) * Fix getting HAS_ONE and BELONGS relation when related object not found; diff --git a/src/Document.php b/src/Document.php index 29e2bdb..13f6fb3 100644 --- a/src/Document.php +++ b/src/Document.php @@ -1030,6 +1030,10 @@ public function append($selector, $value) return $this; } + public function setUpsert() { + $this->options['upsert'] = 1; + } + /** * Push argument as single element to field value * @@ -1198,7 +1202,7 @@ public function save($validate = true) if($this->triggerEvent('beforeSave')->isCancelled()) { return $this; } - if ($this->isStored()) { + if ($this->isStored() || $this->getOption('upsert')) { if($this->triggerEvent('beforeUpdate')->isCancelled()) { return $this; } @@ -1209,13 +1213,16 @@ public function save($validate = true) $query['__version__'] = $this->get('__version__'); $this->getOperator()->increment('__version__'); } + + $data = ! $this->isStored() ? $this->toArray() : $this->getOperator()->toArray(); // update $status = $this->collection ->getMongoCollection() ->update( $query, - $this->getOperator()->toArray() + $data, + ['upsert'=>$this->getOption('upsert') ] ); // check update status diff --git a/src/Document/RelationManager.php b/src/Document/RelationManager.php index 0927480..b177817 100644 --- a/src/Document/RelationManager.php +++ b/src/Document/RelationManager.php @@ -4,8 +4,8 @@ use Sokil\Mongo\Document; -class RelationManager -{ +class RelationManager { + private $relations; /** @@ -15,255 +15,280 @@ class RelationManager private $document; private $resolvedRelationIds = array(); - - public function __construct(Document $document = null) - { + + public function __construct(Document $document = null) { + $this->document = $document; $this->relations = $document->getRelationDefinition(); } /** * Check if relation with specified name configured + * * @param string $name * @return boolean */ - public function isRelationExists($name) - { + public function isRelationExists($name) { + return isset($this->relations[$name]); } + private $resolvedRelations = []; + /** * Get related documents + * * @param string $relationName name of relation */ - public function getRelated($relationName) - { + public function getRelated($relationName) { // check if relation exists - if (!$this->isRelationExists($relationName)) { + if (! $this->isRelationExists($relationName)) { throw new \Sokil\Mongo\Exception('Relation with name "' . $relationName . '" not found'); } - + // get relation metadata $relation = $this->relations[$relationName]; - + $relationType = $relation[0]; $targetCollectionName = $relation[1]; - - // get target collection - $targetCollection = $this->document - ->getCollection() - ->getDatabase() - ->getCollection($targetCollectionName); - + + if (empty($this->resolvedRelations[$targetCollectionName])) { + + if (false !== stripos($targetCollectionName, '\\')) { + $targetCollection = new $targetCollectionName(); + $targetCollection->enableDocumentPool(); + } + else { + // get target collection + $targetCollection = $this->document->getCollection() + ->getDatabase() + ->getCollection($targetCollectionName); + } + + $this->resolvedRelations[$targetCollectionName] = &$targetCollection; + } + else { + $targetCollection = $this->resolvedRelations[$targetCollectionName]; + } + // check if relation already resolved if (isset($this->resolvedRelationIds[$relationName])) { - if(is_array($this->resolvedRelationIds[$relationName])) { + if (is_array($this->resolvedRelationIds[$relationName])) { // has_many, many_many return $targetCollection->getDocumentsFromDocumentPool($this->resolvedRelationIds[$relationName]); - } else { + } + else { //has_one, belongs return $targetCollection->getDocumentFromDocumentPool($this->resolvedRelationIds[$relationName]); } } - + switch ($relationType) { - + case Document::RELATION_HAS_ONE: - $internalField = '_id'; + if (isset($relation[3])) { + $internalField = $relation[3]; + } + else { + $internalField = '_id'; + } + $externalField = $relation[2]; - - $document = $targetCollection - ->find() + $document = $targetCollection->find() ->where($externalField, $this->document->get($internalField)) ->findOne(); - + if ($document) { $this->resolvedRelationIds[$relationName] = (string) $document->getId(); + // $targetCollection->addDocumentToDocumentPool($document); } - + return $document; - + case Document::RELATION_BELONGS: $internalField = $relation[2]; - + $document = $targetCollection->getDocument($this->document->get($internalField)); - + if ($document) { $this->resolvedRelationIds[$relationName] = (string) $document->getId(); } - + return $document; - + case Document::RELATION_HAS_MANY: $internalField = '_id'; $externalField = $relation[2]; - - $documents = $targetCollection - ->find() + + $documents = $targetCollection->find() ->where($externalField, $this->document->get($internalField)) ->findAll(); - - foreach($documents as $document) { + + foreach ($documents as $document) { $this->resolvedRelationIds[$relationName][] = (string) $document->getId(); } - + return $documents; - + case Document::RELATION_MANY_MANY: $isRelationListStoredInternally = isset($relation[3]) && $relation[3]; if ($isRelationListStoredInternally) { // relation list stored in this document $internalField = $relation[2]; $relatedIdList = $this->document->get($internalField); - if (!$relatedIdList) { + if (! $relatedIdList) { return array(); } - + $externalField = '_id'; - - $documents = $targetCollection - ->find() + + $documents = $targetCollection->find() ->whereIn($externalField, $relatedIdList) ->findAll(); - - } else { + } + else { // relation list stored in external document $internalField = '_id'; $externalField = $relation[2]; - - $documents = $targetCollection - ->find() + + $documents = $targetCollection->find() ->where($externalField, $this->document->get($internalField)) ->findAll(); } - - foreach($documents as $document) { + + foreach ($documents as $document) { $this->resolvedRelationIds[$relationName][] = (string) $document->getId(); } - - return $documents; + return $documents; + default: throw new \Sokil\Mongo\Exception('Unsupported relation type "' . $relationType . '" when resolve relation "' . $relationName . '"'); } } - public function addRelation($relationName, Document $document) - { - if (!$this->isRelationExists($relationName)) { + public function addRelation($relationName, Document $document) { + + if (! $this->isRelationExists($relationName)) { throw new \Exception('Relation "' . $relationName . '" not configured'); } - + $relation = $this->relations[$relationName]; - - list($relationType, $relatedCollectionName, $field) = $relation; - - $relatedCollection = $this->document - ->getCollection() + + list ($relationType,$relatedCollectionName,$field) = $relation; + + $relatedCollection = $this->document->getCollection() ->getDatabase() ->getCollection($relatedCollectionName); - - if (!$relatedCollection->hasDocument($document)) { + + if (! $relatedCollection->hasDocument($document)) { throw new \Sokil\Mongo\Exception('Document must belongs to related collection'); } - + switch ($relationType) { - + case Document::RELATION_BELONGS: - if (!$document->isStored()) { + if (! $document->isStored()) { throw new \Sokil\Mongo\Exception('Document ' . get_class($document) . ' must be saved before adding relation'); } $this->document->set($field, $document->getId()); break; - - case Document::RELATION_HAS_ONE; - if (!$this->document->isStored()) { + + case Document::RELATION_HAS_ONE: + if (! $this->document->isStored()) { throw new \Sokil\Mongo\Exception('Document ' . get_class($this) . ' must be saved before adding relation'); } - $document->set($field, $this->document->getId())->save(); + $document->set($field, $this->document->getId()) + ->save(); break; - + case Document::RELATION_HAS_MANY: - if (!$this->document->isStored()) { + if (! $this->document->isStored()) { throw new \Sokil\Mongo\Exception('Document ' . get_class($this) . ' must be saved before adding relation'); } - $document->set($field, $this->document->getId())->save(); + $document->set($field, $this->document->getId()) + ->save(); break; - + case Document::RELATION_MANY_MANY: $isRelationListStoredInternally = isset($relation[3]) && $relation[3]; if ($isRelationListStoredInternally) { - $this->document->push($field, $document->getId())->save(); - } else { - $document->push($field, $this->document->getId())->save(); + $this->document->push($field, $document->getId()) + ->save(); + } + else { + $document->push($field, $this->document->getId()) + ->save(); } break; - + default: throw new \Sokil\Mongo\Exception('Unsupported relation type "' . $relationType . '" when resolve relation "' . $relationName . '"'); } - + return $this; } - public function removeRelation($relationName, Document $document = null) - { - if (!$this->isRelationExists($relationName)) { + public function removeRelation($relationName, Document $document = null) { + + if (! $this->isRelationExists($relationName)) { throw new \Exception('Relation ' . $relationName . ' not configured'); } - + $relation = $this->relations[$relationName]; - - list($relationType, $relatedCollectionName, $field) = $relation; - - $relatedCollection = $this->document - ->getCollection() + + list ($relationType,$relatedCollectionName,$field) = $relation; + + $relatedCollection = $this->document->getCollection() ->getDatabase() ->getCollection($relatedCollectionName); - - if ($document && !$relatedCollection->hasDocument($document)) { + + if ($document && ! $relatedCollection->hasDocument($document)) { throw new \Sokil\Mongo\Exception('Document must belongs to related collection'); } - + switch ($relationType) { - + case Document::RELATION_BELONGS: $this->document->unsetField($field)->save(); break; - - case Document::RELATION_HAS_ONE; + + case Document::RELATION_HAS_ONE: $document = $this->getRelated($relationName); - if (!$document) { + if (! $document) { // relation not exists return $this; } $document->unsetField($field)->save(); break; - + case Document::RELATION_HAS_MANY: - if (!$document) { + if (! $document) { throw new \Sokil\Mongo\Exception('Related document must be defined'); } $document->unsetField($field)->save(); break; - - + case Document::RELATION_MANY_MANY: - if (!$document) { + if (! $document) { throw new \Sokil\Mongo\Exception('Related document must be defined'); } $isRelationListStoredInternally = isset($relation[3]) && $relation[3]; if ($isRelationListStoredInternally) { - $this->document->pull($field, $document->getId())->save(); - } else { - $document->pull($field, $this->document->getId())->save(); + $this->document->pull($field, $document->getId()) + ->save(); + } + else { + $document->pull($field, $this->document->getId()) + ->save(); } break; - + default: throw new \Sokil\Mongo\Exception('Unsupported relation type "' . $relationType . '" when resolve relation "' . $relationName . '"'); } - + return $this; } + } diff --git a/src/Operator.php b/src/Operator.php index 00ad640..3a168e9 100644 --- a/src/Operator.php +++ b/src/Operator.php @@ -262,6 +262,14 @@ public function pull($expression, $value = null) */ public function unsetField($fieldName) { + + /* Prevents mongo error: could not set and unset field at the same time + if Document have Set and Unset fields at the samae time + */ + if (isset($this->_operators['$set'][$fieldName])) { + unset($this->_operators['$set'][$fieldName]); + } + $this->_operators['$unset'][$fieldName] = ''; return $this; } diff --git a/src/Persistence.php b/src/Persistence.php index a6010b4..b03aed9 100644 --- a/src/Persistence.php +++ b/src/Persistence.php @@ -8,26 +8,28 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ - namespace Sokil\Mongo; /** - * Class use MongoWriteBatch classes from PECL driver above v. 1.5.0. + * Class use MongoWriteBatch classes from PECL driver above v. + * 1.5.0. * Before this version legacy persistencee must be used */ -class Persistence implements \Countable -{ +class Persistence implements \Countable { + const STATE_SAVE = 0; + const STATE_REMOVE = 1; /** + * * @var \SplObjectStorage */ protected $pool; - public function __construct() - { - $this->pool = new \SplObjectStorage; + public function __construct() { + + $this->pool = new \SplObjectStorage(); } /** @@ -36,17 +38,18 @@ public function __construct() * @param Document $document * @return bool */ - public function contains(Document $document) - { + public function contains(Document $document) { + return $this->pool->contains($document); } /** * Get count of documents in pool + * * @return int */ - public function count() - { + public function count() { + return $this->pool->count(); } @@ -56,10 +59,10 @@ public function count() * @param Document $document * @return \Sokil\Mongo\Persistence */ - public function persist(Document $document) - { - $this->pool->attach($document, self::STATE_SAVE); + public function persist(Document $document) { + $this->pool->attach($document, self::STATE_SAVE); + return $this; } @@ -69,10 +72,10 @@ public function persist(Document $document) * @param Document $document * @return \Sokil\Mongo\Persistence */ - public function remove(Document $document) - { - $this->pool->attach($document, self::STATE_REMOVE); + public function remove(Document $document) { + $this->pool->attach($document, self::STATE_REMOVE); + return $this; } @@ -81,52 +84,84 @@ public function remove(Document $document) * * @return \Sokil\Mongo\Persistence */ - public function flush() - { + public function flush() { + $insert = array(); $update = array(); $delete = array(); - + // fill batch objects - foreach($this->pool as $document) { + foreach ($this->pool as $document) { /* @var $document \Sokil\Mongo\Document */ - + // collection $collection = $document->getCollection(); $collectionName = $collection->getName(); - + + if ($document->triggerEvent('beforeSave')->isCancelled()) { + continue; + } + // persisting - switch($this->pool->offsetGet($document)) { + switch ($this->pool->offsetGet($document)) { case self::STATE_SAVE: - if ($document->isStored()) { - if (!isset($update[$collectionName])) { + if ($document->isStored() || $document->getOptions('upsert', 0)) { + + if ($document->getOptions('upsert', 0)) { + if ($document->triggerEvent('beforeInsert')->isCancelled()) { + continue; + } + } + + if ($document->triggerEvent('beforeUpdate')->isCancelled()) { + continue; + } + + if ($document->getOption('upsert', 0)) { + $data = $document->toArray(); + } + else { + $data = $document->getOperator()->toArray(); + } + $data = array( + '$set' => $data + ); + + if (! isset($update[$collectionName])) { $update[$collectionName] = new \MongoUpdateBatch($collection->getMongoCollection()); } $update[$collectionName]->add(array( 'q' => array( - '_id' => $document->getId(), + '_id' => $document->getId() ), - 'u' => $document->getOperator()->toArray(), + 'u' => $data, + 'upsert' => $document->getOptions('upsert', 0) )); - } else { - if (!isset($insert[$collectionName])) { + } + else { + + if ($document->triggerEvent('beforeInsert')->isCancelled()) { + continue; + } + + if (! isset($insert[$collectionName])) { $insert[$collectionName] = new \MongoInsertBatch($collection->getMongoCollection()); } $insert[$collectionName]->add($document->toArray()); } break; - + case self::STATE_REMOVE: // delete document form db if ($document->isStored()) { - if (!isset($delete[$collectionName])) { + if (! isset($delete[$collectionName])) { $delete[$collectionName] = new \MongoDeleteBatch($collection->getMongoCollection()); } $delete[$collectionName]->add(array( 'q' => array( - '_id' => $document->getId(), + '_id' => $document->getId() ), - 'limit' => 1, + 'limit' => 1 )); } // remove link form pool @@ -134,32 +169,50 @@ public function flush() break; } } - + // write operations - $writeOptions = array('w' => 1); + $writeOptions = array( + 'w' => 1 + ); + $aStat = []; + // execute batch insert operations if ($insert) { foreach ($insert as $collectionName => $collectionInsert) { - $collectionInsert->execute($writeOptions); + $aStat['insert'] = $collectionInsert->execute($writeOptions); } } - + // execute batch update operations if ($update) { foreach ($update as $collectionName => $collectionUpdate) { - $collectionUpdate->execute($writeOptions); + $aStat['update'] = $collectionUpdate->execute($writeOptions); } } - + // execute batch delete operations if ($delete) { foreach ($delete as $collectionName => $collectionDelete) { - $collectionDelete->execute($writeOptions); + $aStat['delete'] = $collectionDelete->execute($writeOptions); } } - - return $this; + + $aDetailed = []; + + foreach ($aStat as $aRow) { + foreach ($aRow as $sKey => $mValue) { + if (! isset($aData[$sKey])) $aDetailed[$sKey] = 0; + if (is_numeric($mValue)) { + $aDetailed[$sKey] += $mValue; + } + elseif (is_array($mValue)) { + $aDetailed[$sKey] += count($mValue); + } + } + } + + return $aDetailed; } /** @@ -168,21 +221,23 @@ public function flush() * @param Document $document * @return \Sokil\Mongo\Persistence */ - public function detach(Document $document) - { - $this->pool->detach($document); + public function detach(Document $document) { + $this->pool->detach($document); + return $this; } /** * Detach all documents from pool + * * @return \Sokil\Mongo\Persistence */ - public function clear() - { - $this->pool->removeAll($this->pool); + public function clear() { + $this->pool->removeAll($this->pool); + return $this; } + } diff --git a/tests/CacheTest.php b/tests/CacheTest.php index ee0b44e..972776c 100644 --- a/tests/CacheTest.php +++ b/tests/CacheTest.php @@ -184,6 +184,8 @@ public function testDeleteNotMatchingAnyTag() ->setNeverExpired('c', 'C', array('language', 'compileable')); $this->assertEquals(2, count($this->cache)); + + $this->cache->deleteNotMatchingAnyTag(array('compileable', 'elephant'));