/** * {@inheritdoc} */ public function add($value) { /* Initialize the collection before calling add() so this append operation * uses the appropriate key. Otherwise, we risk overwriting original data * when $newObjects are re-added in a later call to initialize(). */ if (isset($this->mapping['strategy']) && CollectionHelper::isHash($this->mapping['strategy'])) { $this->initialize(); } $this->coll->add($value); $this->changed(); if ($this->uow !== null && $this->isOrphanRemovalEnabled() && $value !== null) { $this->uow->unscheduleOrphanRemoval($value); } return true; }
/** * Marks the PersistentCollection's top-level owner as having a relation to * a collection scheduled for update or deletion. * * If the owner is not scheduled for any lifecycle action, it will be * scheduled for update to ensure that versioning takes place if necessary. * * If the collection is nested within atomic collection, it is immediately * unscheduled and atomic one is scheduled for update instead. This makes * calculating update data way easier. * * @param PersistentCollection $coll */ private function scheduleCollectionOwner(PersistentCollection $coll) { $document = $this->getOwningDocument($coll->getOwner()); $this->hasScheduledCollections[spl_object_hash($document)][spl_object_hash($coll)] = $coll; if ($document !== $coll->getOwner()) { $parent = $coll->getOwner(); while (null !== ($parentAssoc = $this->getParentAssociation($parent))) { list($mapping, $parent, ) = $parentAssoc; } if (isset($mapping['strategy']) && CollectionHelper::isAtomic($mapping['strategy'])) { $class = $this->dm->getClassMetadata(get_class($document)); $atomicCollection = $class->getFieldValue($document, $mapping['fieldName']); $this->scheduleCollectionUpdate($atomicCollection); $this->unscheduleCollectionDeletion($coll); $this->unscheduleCollectionUpdate($coll); } } if (!$this->isDocumentScheduled($document)) { $this->scheduleForUpdate($document); } }
/** * Sets a PersistentCollection instance. * * This method is intended to be used with the "set" or "setArray" * strategies. The "setArray" strategy will ensure that the collection is * set as a BSON array, which means the collection elements will be * reindexed numerically before storage. * * @param PersistentCollection $coll * @param array $options */ private function setCollection(PersistentCollection $coll, array $options) { list($propertyPath, $parent) = $this->getPathAndParent($coll); $coll->initialize(); $mapping = $coll->getMapping(); $setData = $this->pb->prepareAssociatedCollectionValue($coll, CollectionHelper::usesSet($mapping['strategy'])); $query = array('$set' => array($propertyPath => $setData)); $this->executeQuery($parent, $query, $options); }
/** * Prepares a query value and converts the PHP value to the database value * if it is an identifier. * * It also handles converting $fieldName to the database name if they are different. * * @param string $fieldName * @param mixed $value * @param ClassMetadata $class Defaults to $this->class * @param boolean $prepareValue Whether or not to prepare the value * @return array Prepared field name and value */ private function prepareQueryElement($fieldName, $value = null, $class = null, $prepareValue = true) { $class = isset($class) ? $class : $this->class; // @todo Consider inlining calls to ClassMetadataInfo methods // Process all non-identifier fields by translating field names if ($class->hasField($fieldName) && !$class->isIdentifier($fieldName)) { $mapping = $class->fieldMappings[$fieldName]; $fieldName = $mapping['name']; if (!$prepareValue) { return array($fieldName, $value); } // Prepare mapped, embedded objects if (!empty($mapping['embedded']) && is_object($value) && !$this->dm->getMetadataFactory()->isTransient(get_class($value))) { return array($fieldName, $this->pb->prepareEmbeddedDocumentValue($mapping, $value)); } // No further preparation unless we're dealing with a simple reference // We can't have expressions in empty() with PHP < 5.5, so store it in a variable $arrayValue = (array) $value; if (empty($mapping['reference']) || empty($mapping['simple']) || empty($arrayValue)) { return array($fieldName, $value); } // Additional preparation for one or more simple reference values $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']); if (!is_array($value)) { return array($fieldName, $targetClass->getDatabaseIdentifierValue($value)); } // Objects without operators or with DBRef fields can be converted immediately if (!$this->hasQueryOperators($value) || $this->hasDBRefFields($value)) { return array($fieldName, $targetClass->getDatabaseIdentifierValue($value)); } return array($fieldName, $this->prepareQueryExpression($value, $targetClass)); } // Process identifier fields if ($class->hasField($fieldName) && $class->isIdentifier($fieldName) || $fieldName === '_id') { $fieldName = '_id'; if (!$prepareValue) { return array($fieldName, $value); } if (!is_array($value)) { return array($fieldName, $class->getDatabaseIdentifierValue($value)); } // Objects without operators or with DBRef fields can be converted immediately if (!$this->hasQueryOperators($value) || $this->hasDBRefFields($value)) { return array($fieldName, $class->getDatabaseIdentifierValue($value)); } return array($fieldName, $this->prepareQueryExpression($value, $class)); } // No processing for unmapped, non-identifier, non-dotted field names if (strpos($fieldName, '.') === false) { return array($fieldName, $value); } /* Process "fieldName.objectProperty" queries (on arrays or objects). * * We can limit parsing here, since at most three segments are * significant: "fieldName.objectProperty" with an optional index or key * for collections stored as either BSON arrays or objects. */ $e = explode('.', $fieldName, 4); // No further processing for unmapped fields if (!isset($class->fieldMappings[$e[0]])) { return array($fieldName, $value); } $mapping = $class->fieldMappings[$e[0]]; $e[0] = $mapping['name']; // Hash and raw fields will not be prepared beyond the field name if ($mapping['type'] === Type::HASH || $mapping['type'] === Type::RAW) { $fieldName = implode('.', $e); return array($fieldName, $value); } if (isset($mapping['strategy']) && CollectionHelper::isHash($mapping['strategy']) && isset($e[2])) { $objectProperty = $e[2]; $objectPropertyPrefix = $e[1] . '.'; $nextObjectProperty = implode('.', array_slice($e, 3)); } elseif ($e[1] != '$') { $fieldName = $e[0] . '.' . $e[1]; $objectProperty = $e[1]; $objectPropertyPrefix = ''; $nextObjectProperty = implode('.', array_slice($e, 2)); } elseif (isset($e[2])) { $fieldName = $e[0] . '.' . $e[1] . '.' . $e[2]; $objectProperty = $e[2]; $objectPropertyPrefix = $e[1] . '.'; $nextObjectProperty = implode('.', array_slice($e, 3)); } else { $fieldName = $e[0] . '.' . $e[1]; return array($fieldName, $value); } // No further processing for fields without a targetDocument mapping if (!isset($mapping['targetDocument'])) { if ($nextObjectProperty) { $fieldName .= '.' . $nextObjectProperty; } return array($fieldName, $value); } $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']); // No further processing for unmapped targetDocument fields if (!$targetClass->hasField($objectProperty)) { if ($nextObjectProperty) { $fieldName .= '.' . $nextObjectProperty; } return array($fieldName, $value); } $targetMapping = $targetClass->getFieldMapping($objectProperty); $objectPropertyIsId = $targetClass->isIdentifier($objectProperty); // Prepare DBRef identifiers or the mapped field's property path $fieldName = $objectPropertyIsId && !empty($mapping['reference']) && empty($mapping['simple']) ? $e[0] . '.$id' : $e[0] . '.' . $objectPropertyPrefix . $targetMapping['name']; // Process targetDocument identifier fields if ($objectPropertyIsId) { if (!$prepareValue) { return array($fieldName, $value); } if (!is_array($value)) { return array($fieldName, $targetClass->getDatabaseIdentifierValue($value)); } // Objects without operators or with DBRef fields can be converted immediately if (!$this->hasQueryOperators($value) || $this->hasDBRefFields($value)) { return array($fieldName, $targetClass->getDatabaseIdentifierValue($value)); } return array($fieldName, $this->prepareQueryExpression($value, $targetClass)); } /* The property path may include a third field segment, excluding the * collection item pointer. If present, this next object property must * be processed recursively. */ if ($nextObjectProperty) { // Respect the targetDocument's class metadata when recursing $nextTargetClass = isset($targetMapping['targetDocument']) ? $this->dm->getClassMetadata($targetMapping['targetDocument']) : null; list($key, $value) = $this->prepareQueryElement($nextObjectProperty, $value, $nextTargetClass, $prepareValue); $fieldName .= '.' . $key; } return array($fieldName, $value); }
/** * Returns the collection representation to be stored and unschedules it afterwards. * * @param PersistentCollection $coll * @param bool $includeNestedCollections * @return array */ public function prepareAssociatedCollectionValue(PersistentCollection $coll, $includeNestedCollections = false) { $mapping = $coll->getMapping(); $pb = $this; $callback = isset($mapping['embedded']) ? function ($v) use($pb, $mapping, $includeNestedCollections) { return $pb->prepareEmbeddedDocumentValue($mapping, $v, $includeNestedCollections); } : function ($v) use($pb, $mapping) { return $pb->prepareReferencedDocumentValue($mapping, $v); }; $setData = $coll->map($callback)->toArray(); if (CollectionHelper::isList($mapping['strategy'])) { $setData = array_values($setData); } $this->uow->unscheduleCollectionDeletion($coll); $this->uow->unscheduleCollectionUpdate($coll); return $setData; }
/** * Validates the storage strategy of a mapping for consistency * @param array $mapping * @throws \Doctrine\ODM\MongoDB\Mapping\MappingException */ private function applyStorageStrategy(array &$mapping) { if (!isset($mapping['type']) || isset($mapping['id'])) { return; } switch (true) { case $mapping['type'] == 'int': case $mapping['type'] == 'float': case $mapping['type'] == 'increment': $defaultStrategy = self::STORAGE_STRATEGY_SET; $allowedStrategies = [self::STORAGE_STRATEGY_SET, self::STORAGE_STRATEGY_INCREMENT]; break; case $mapping['type'] == 'many': $defaultStrategy = CollectionHelper::DEFAULT_STRATEGY; $allowedStrategies = [self::STORAGE_STRATEGY_PUSH_ALL, self::STORAGE_STRATEGY_ADD_TO_SET, self::STORAGE_STRATEGY_SET, self::STORAGE_STRATEGY_SET_ARRAY, self::STORAGE_STRATEGY_ATOMIC_SET, self::STORAGE_STRATEGY_ATOMIC_SET_ARRAY]; break; default: $defaultStrategy = self::STORAGE_STRATEGY_SET; $allowedStrategies = [self::STORAGE_STRATEGY_SET]; } if (!isset($mapping['strategy'])) { $mapping['strategy'] = $defaultStrategy; } if (!in_array($mapping['strategy'], $allowedStrategies)) { throw MappingException::invalidStorageStrategy($this->name, $mapping['fieldName'], $mapping['type'], $mapping['strategy']); } if (isset($mapping['reference']) && $mapping['type'] === 'many' && $mapping['isOwningSide'] && !empty($mapping['sort']) && !CollectionHelper::usesSet($mapping['strategy'])) { throw MappingException::referenceManySortMustNotBeUsedWithNonSetCollectionStrategy($this->name, $mapping['fieldName'], $mapping['strategy']); } }
/** * Map a field. * * @param array $mapping The mapping information. * * @return array * * @throws MappingException */ public function mapField(array $mapping) { if (!isset($mapping['fieldName']) && isset($mapping['name'])) { $mapping['fieldName'] = $mapping['name']; } if (!isset($mapping['fieldName'])) { throw MappingException::missingFieldName($this->name); } if (!isset($mapping['name'])) { $mapping['name'] = $mapping['fieldName']; } if ($this->identifier === $mapping['name'] && empty($mapping['id'])) { throw MappingException::mustNotChangeIdentifierFieldsType($this->name, $mapping['name']); } if (isset($this->fieldMappings[$mapping['fieldName']])) { //throw MappingException::duplicateFieldMapping($this->name, $mapping['fieldName']); } if ($this->discriminatorField !== null && $this->discriminatorField == $mapping['name']) { throw MappingException::discriminatorFieldConflict($this->name, $this->discriminatorField); } if (isset($mapping['targetDocument']) && strpos($mapping['targetDocument'], '\\') === false && strlen($this->namespace)) { $mapping['targetDocument'] = $this->namespace . '\\' . $mapping['targetDocument']; } if (isset($mapping['discriminatorMap'])) { foreach ($mapping['discriminatorMap'] as $key => $class) { if (strpos($class, '\\') === false && strlen($this->namespace)) { $mapping['discriminatorMap'][$key] = $this->namespace . '\\' . $class; } } } if (isset($mapping['cascade']) && isset($mapping['embedded'])) { throw MappingException::cascadeOnEmbeddedNotAllowed($this->name, $mapping['fieldName']); } $cascades = isset($mapping['cascade']) ? array_map('strtolower', (array) $mapping['cascade']) : array(); if (in_array('all', $cascades) || isset($mapping['embedded'])) { $cascades = array('remove', 'persist', 'refresh', 'merge', 'detach'); } if (isset($mapping['embedded'])) { unset($mapping['cascade']); } elseif (isset($mapping['cascade'])) { $mapping['cascade'] = $cascades; } $mapping['isCascadeRemove'] = in_array('remove', $cascades); $mapping['isCascadePersist'] = in_array('persist', $cascades); $mapping['isCascadeRefresh'] = in_array('refresh', $cascades); $mapping['isCascadeMerge'] = in_array('merge', $cascades); $mapping['isCascadeDetach'] = in_array('detach', $cascades); if (isset($mapping['type']) && $mapping['type'] === 'file') { $mapping['file'] = true; } if (isset($mapping['file']) && $mapping['file'] === true) { $this->file = $mapping['fieldName']; $mapping['name'] = 'file'; } if (isset($mapping['distance']) && $mapping['distance'] === true) { $this->distance = $mapping['fieldName']; } if (isset($mapping['id']) && $mapping['id'] === true) { $mapping['name'] = '_id'; $this->identifier = $mapping['fieldName']; if (isset($mapping['strategy'])) { $this->generatorType = constant('Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata::GENERATOR_TYPE_' . strtoupper($mapping['strategy'])); } $this->generatorOptions = isset($mapping['options']) ? $mapping['options'] : array(); switch ($this->generatorType) { case self::GENERATOR_TYPE_AUTO: $mapping['type'] = 'id'; break; default: if (!empty($this->generatorOptions['type'])) { $mapping['type'] = $this->generatorOptions['type']; } elseif (empty($mapping['type'])) { $mapping['type'] = $this->generatorType === self::GENERATOR_TYPE_INCREMENT ? 'int_id' : 'custom_id'; } } unset($this->generatorOptions['type']); } if (!isset($mapping['nullable'])) { $mapping['nullable'] = false; } if (isset($mapping['reference']) && !empty($mapping['simple']) && !isset($mapping['targetDocument'])) { throw MappingException::simpleReferenceRequiresTargetDocument($this->name, $mapping['fieldName']); } if (isset($mapping['reference']) && empty($mapping['targetDocument']) && empty($mapping['discriminatorMap']) && (isset($mapping['mappedBy']) || isset($mapping['inversedBy']))) { throw MappingException::owningAndInverseReferencesRequireTargetDocument($this->name, $mapping['fieldName']); } if ($this->isEmbeddedDocument && $mapping['type'] === 'many' && CollectionHelper::isAtomic($mapping['strategy'])) { throw MappingException::atomicCollectionStrategyNotAllowed($mapping['strategy'], $this->name, $mapping['fieldName']); } if (isset($mapping['reference']) && $mapping['type'] === 'one') { $mapping['association'] = self::REFERENCE_ONE; } if (isset($mapping['reference']) && $mapping['type'] === 'many') { $mapping['association'] = self::REFERENCE_MANY; } if (isset($mapping['embedded']) && $mapping['type'] === 'one') { $mapping['association'] = self::EMBED_ONE; } if (isset($mapping['embedded']) && $mapping['type'] === 'many') { $mapping['association'] = self::EMBED_MANY; } if (isset($mapping['association']) && !isset($mapping['targetDocument']) && !isset($mapping['discriminatorField'])) { $mapping['discriminatorField'] = self::DEFAULT_DISCRIMINATOR_FIELD; } /* if (isset($mapping['type']) && ($mapping['type'] === 'one' || $mapping['type'] === 'many')) { $mapping['type'] = $mapping['type'] === 'one' ? self::ONE : self::MANY; } */ if (isset($mapping['version'])) { $mapping['notSaved'] = true; $this->setVersionMapping($mapping); } if (isset($mapping['lock'])) { $mapping['notSaved'] = true; $this->setLockMapping($mapping); } $mapping['isOwningSide'] = true; $mapping['isInverseSide'] = false; if (isset($mapping['reference'])) { if (isset($mapping['inversedBy']) && $mapping['inversedBy']) { $mapping['isOwningSide'] = true; $mapping['isInverseSide'] = false; } if (isset($mapping['mappedBy']) && $mapping['mappedBy']) { $mapping['isInverseSide'] = true; $mapping['isOwningSide'] = false; } if (isset($mapping['repositoryMethod'])) { $mapping['isInverseSide'] = true; $mapping['isOwningSide'] = false; } if (!isset($mapping['orphanRemoval'])) { $mapping['orphanRemoval'] = false; } } if (isset($mapping['reference']) && $mapping['type'] === 'many' && $mapping['isOwningSide'] && !empty($mapping['sort']) && !CollectionHelper::usesSet($mapping['strategy'])) { throw MappingException::referenceManySortMustNotBeUsedWithNonSetCollectionStrategy($this->name, $mapping['fieldName'], $mapping['strategy']); } $this->fieldMappings[$mapping['fieldName']] = $mapping; if (isset($mapping['association'])) { $this->associationMappings[$mapping['fieldName']] = $mapping; } return $mapping; }
private function loadReferenceManyCollectionOwningSide(PersistentCollection $collection) { $hints = $collection->getHints(); $mapping = $collection->getMapping(); $groupedIds = array(); $sorted = isset($mapping['sort']) && $mapping['sort']; foreach ($collection->getMongoData() as $key => $reference) { if (isset($mapping['simple']) && $mapping['simple']) { $className = $mapping['targetDocument']; $mongoId = $reference; } else { $className = $this->uow->getClassNameForAssociation($mapping, $reference); $mongoId = $reference['$id']; } $id = $this->dm->getClassMetadata($className)->getPHPIdentifierValue($mongoId); // create a reference to the class and id $reference = $this->dm->getReference($className, $id); // no custom sort so add the references right now in the order they are embedded if (!$sorted) { if (CollectionHelper::isHash($mapping['strategy'])) { $collection->set($key, $reference); } else { $collection->add($reference); } } // only query for the referenced object if it is not already initialized or the collection is sorted if ($reference instanceof Proxy && !$reference->__isInitialized__ || $sorted) { $groupedIds[$className][] = $mongoId; } } foreach ($groupedIds as $className => $ids) { $class = $this->dm->getClassMetadata($className); $mongoCollection = $this->dm->getDocumentCollection($className); $criteria = $this->cm->merge(array('_id' => array('$in' => array_values($ids))), $this->dm->getFilterCollection()->getFilterCriteria($class), isset($mapping['criteria']) ? $mapping['criteria'] : array()); $criteria = $this->uow->getDocumentPersister($className)->prepareQueryOrNewObj($criteria); $cursor = $mongoCollection->find($criteria); if (isset($mapping['sort'])) { $cursor->sort($mapping['sort']); } if (isset($mapping['limit'])) { $cursor->limit($mapping['limit']); } if (isset($mapping['skip'])) { $cursor->skip($mapping['skip']); } if (!empty($hints[Query::HINT_SLAVE_OKAY])) { $cursor->slaveOkay(true); } if (!empty($hints[Query::HINT_READ_PREFERENCE])) { $cursor->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]); } $documents = $cursor->toArray(false); foreach ($documents as $documentData) { $document = $this->uow->getById($documentData['_id'], $class); $data = $this->hydratorFactory->hydrate($document, $documentData); $this->uow->setOriginalDocumentData($document, $data); $document->__isInitialized__ = true; if ($sorted) { $collection->add($document); } } } }