/** * @param ClassMetadata $class * @param string $hydratorClassName * @param string $fileName */ private function generateHydratorClass(ClassMetadata $class, $hydratorClassName, $fileName) { $code = ''; foreach ($class->fieldMappings as $fieldName => $mapping) { if (isset($mapping['alsoLoadFields'])) { foreach ($mapping['alsoLoadFields'] as $name) { $code .= sprintf(<<<EOF /** @AlsoLoad("{$name}") */ if (!array_key_exists('%1\$s', \$data) && array_key_exists('{$name}', \$data)) { \$data['%1\$s'] = \$data['{$name}']; } EOF , $mapping['name']); } } if ($mapping['type'] === 'date') { $code .= sprintf(<<<EOF /** @Field(type="date") */ if (isset(\$data['%1\$s'])) { \$value = \$data['%1\$s']; %3\$s \$this->class->reflFields['%2\$s']->setValue(\$document, clone \$return); \$hydratedData['%2\$s'] = \$return; } EOF , $mapping['name'], $mapping['fieldName'], Type::getType($mapping['type'])->closureToPHP()); } elseif (!isset($mapping['association'])) { $code .= sprintf(<<<EOF /** @Field(type="{$mapping['type']}") */ if (isset(\$data['%1\$s'])) { \$value = \$data['%1\$s']; %3\$s \$this->class->reflFields['%2\$s']->setValue(\$document, \$return); \$hydratedData['%2\$s'] = \$return; } EOF , $mapping['name'], $mapping['fieldName'], Type::getType($mapping['type'])->closureToPHP()); } elseif ($mapping['association'] === ClassMetadata::REFERENCE_ONE && $mapping['isOwningSide']) { $code .= sprintf(<<<EOF /** @ReferenceOne */ if (isset(\$data['%1\$s'])) { \$reference = \$data['%1\$s']; if (isset(\$this->class->fieldMappings['%2\$s']['simple']) && \$this->class->fieldMappings['%2\$s']['simple']) { \$className = \$this->class->fieldMappings['%2\$s']['targetDocument']; \$mongoId = \$reference; } else { \$className = \$this->unitOfWork->getClassNameForAssociation(\$this->class->fieldMappings['%2\$s'], \$reference); \$mongoId = \$reference['\$id']; } \$targetMetadata = \$this->dm->getClassMetadata(\$className); \$id = \$targetMetadata->getPHPIdentifierValue(\$mongoId); \$return = \$this->dm->getReference(\$className, \$id); \$this->class->reflFields['%2\$s']->setValue(\$document, \$return); \$hydratedData['%2\$s'] = \$return; } EOF , $mapping['name'], $mapping['fieldName']); } elseif ($mapping['association'] === ClassMetadata::REFERENCE_ONE && $mapping['isInverseSide']) { if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) { $code .= sprintf(<<<EOF \$className = \$this->class->fieldMappings['%2\$s']['targetDocument']; \$return = \$this->dm->getRepository(\$className)->%3\$s(\$document); \$this->class->reflFields['%2\$s']->setValue(\$document, \$return); \$hydratedData['%2\$s'] = \$return; EOF , $mapping['name'], $mapping['fieldName'], $mapping['repositoryMethod']); } else { $code .= sprintf(<<<EOF \$mapping = \$this->class->fieldMappings['%2\$s']; \$className = \$mapping['targetDocument']; \$targetClass = \$this->dm->getClassMetadata(\$mapping['targetDocument']); \$mappedByMapping = \$targetClass->fieldMappings[\$mapping['mappedBy']]; \$mappedByFieldName = isset(\$mappedByMapping['simple']) && \$mappedByMapping['simple'] ? \$mapping['mappedBy'] : \$mapping['mappedBy'].'.\$id'; \$criteria = array_merge( array(\$mappedByFieldName => \$data['_id']), isset(\$this->class->fieldMappings['%2\$s']['criteria']) ? \$this->class->fieldMappings['%2\$s']['criteria'] : array() ); \$sort = isset(\$this->class->fieldMappings['%2\$s']['sort']) ? \$this->class->fieldMappings['%2\$s']['sort'] : array(); \$return = \$this->unitOfWork->getDocumentPersister(\$className)->load(\$criteria, null, array(), 0, \$sort); \$this->class->reflFields['%2\$s']->setValue(\$document, \$return); \$hydratedData['%2\$s'] = \$return; EOF , $mapping['name'], $mapping['fieldName']); } } elseif ($mapping['association'] === ClassMetadata::REFERENCE_MANY || $mapping['association'] === ClassMetadata::EMBED_MANY) { $code .= sprintf(<<<EOF /** @Many */ \$mongoData = isset(\$data['%1\$s']) ? \$data['%1\$s'] : null; \$return = new \\CosmoW\\ODM\\Riak\\PersistentCollection(new \\Doctrine\\Common\\Collections\\ArrayCollection(), \$this->dm, \$this->unitOfWork); \$return->setHints(\$hints); \$return->setOwner(\$document, \$this->class->fieldMappings['%2\$s']); \$return->setInitialized(false); if (\$mongoData) { \$return->setMongoData(\$mongoData); } \$this->class->reflFields['%2\$s']->setValue(\$document, \$return); \$hydratedData['%2\$s'] = \$return; EOF , $mapping['name'], $mapping['fieldName']); } elseif ($mapping['association'] === ClassMetadata::EMBED_ONE) { $code .= sprintf(<<<EOF /** @EmbedOne */ if (isset(\$data['%1\$s'])) { \$embeddedDocument = \$data['%1\$s']; \$className = \$this->unitOfWork->getClassNameForAssociation(\$this->class->fieldMappings['%2\$s'], \$embeddedDocument); \$embeddedMetadata = \$this->dm->getClassMetadata(\$className); \$return = \$embeddedMetadata->newInstance(); \$this->unitOfWork->setParentAssociation(\$return, \$this->class->fieldMappings['%2\$s'], \$document, '%1\$s'); \$embeddedData = \$this->dm->getHydratorFactory()->hydrate(\$return, \$embeddedDocument, \$hints); \$embeddedId = \$embeddedMetadata->identifier && isset(\$embeddedData[\$embeddedMetadata->identifier]) ? \$embeddedData[\$embeddedMetadata->identifier] : null; \$this->unitOfWork->registerManaged(\$return, \$embeddedId, \$embeddedData); \$this->class->reflFields['%2\$s']->setValue(\$document, \$return); \$hydratedData['%2\$s'] = \$return; } EOF , $mapping['name'], $mapping['fieldName']); } } $className = $class->name; $namespace = $this->hydratorNamespace; $code = sprintf(<<<EOF <?php namespace {$namespace}; use CosmoW\\ODM\\Riak\\DocumentManager; use CosmoW\\ODM\\Riak\\Mapping\\ClassMetadata; use CosmoW\\ODM\\Riak\\Hydrator\\HydratorInterface; use CosmoW\\ODM\\Riak\\UnitOfWork; /** * THIS CLASS WAS GENERATED BY THE DOCTRINE ODM. DO NOT EDIT THIS FILE. */ class {$hydratorClassName} implements HydratorInterface { private \$dm; private \$unitOfWork; private \$class; public function __construct(DocumentManager \$dm, UnitOfWork \$uow, ClassMetadata \$class) { \$this->dm = \$dm; \$this->unitOfWork = \$uow; \$this->class = \$class; } public function hydrate(\$document, \$data, array \$hints = array()) { \$hydratedData = array(); %s return \$hydratedData; } } EOF , $code); if ($fileName === false) { if (!class_exists($namespace . '\\' . $hydratorClassName)) { eval(substr($code, 5)); } } else { $parentDirectory = dirname($fileName); if (!is_dir($parentDirectory) && false === @mkdir($parentDirectory, 0775, true)) { throw HydratorException::hydratorDirectoryNotWritable(); } if (!is_writable($parentDirectory)) { throw HydratorException::hydratorDirectoryNotWritable(); } $tmpFileName = $fileName . '.' . uniqid('', true); file_put_contents($tmpFileName, $code); rename($tmpFileName, $fileName); chmod($fileName, 0664); } }
private function generateDocumentStubMethod(ClassMetadataInfo $metadata, $type, $fieldName, $typeHint = null, $defaultValue = null) { // Add/remove methods should use the singular form of the field name $formattedFieldName = in_array($type, array('add', 'remove')) ? Inflector::singularize($fieldName) : $fieldName; $methodName = $type . Inflector::classify($formattedFieldName); $variableName = Inflector::camelize($formattedFieldName); if ($this->hasMethod($methodName, $metadata)) { return; } $description = ucfirst($type) . ' ' . $variableName; $types = Type::getTypesMap(); $methodTypeHint = $typeHint && !isset($types[$typeHint]) ? '\\' . $typeHint . ' ' : null; $variableType = $typeHint ? $typeHint . ' ' : null; $replacements = array('<description>' => $description, '<methodTypeHint>' => $methodTypeHint, '<variableType>' => $variableType, '<variableName>' => $variableName, '<methodName>' => $methodName, '<fieldName>' => $fieldName, '<variableDefault>' => $defaultValue !== null ? ' = ' . $defaultValue : ''); $templateVar = sprintf('%sMethodTemplate', $type); $method = str_replace(array_keys($replacements), array_values($replacements), self::${$templateVar}); return $this->prefixCodeWithSpaces($method); }
/** * Casts the identifier to its database type. * * @param mixed $id * @return mixed $id */ public function getDatabaseIdentifierValue($id) { $idType = $this->fieldMappings[$this->identifier]['type']; return Type::getType($idType)->convertToDatabaseValue($id); }
/** * Prepares the query criteria or new document object. * * PHP field names and types will be converted to those used by Riak. * * @param array $query * @return array */ public function prepareQueryOrNewObj(array $query) { $preparedQuery = array(); foreach ($query as $key => $value) { // Recursively prepare logical query clauses if (in_array($key, array('$and', '$or', '$nor')) && is_array($value)) { foreach ($value as $k2 => $v2) { $preparedQuery[$key][$k2] = $this->prepareQueryOrNewObj($v2); } continue; } if (isset($key[0]) && $key[0] === '$' && is_array($value)) { $preparedQuery[$key] = $this->prepareQueryOrNewObj($value); continue; } list($key, $value) = $this->prepareQueryElement($key, $value, null, true); $preparedQuery[$key] = is_array($value) ? array_map('CosmoW\\ODM\\Riak\\Types\\Type::convertPHPToDatabaseValue', $value) : Type::convertPHPToDatabaseValue($value); } return $preparedQuery; }
/** * Used to do the common work of computeChangeSet and recomputeSingleDocumentChangeSet * * @param \CosmoW\ODM\Riak\Mapping\ClassMetadata $class * @param object $document * @param boolean $recompute */ private function computeOrRecomputeChangeSet(ClassMetadata $class, $document, $recompute = false) { $oid = spl_object_hash($document); $actualData = $this->getDocumentActualData($document); $isNewDocument = !isset($this->originalDocumentData[$oid]); if ($isNewDocument) { // Document is either NEW or MANAGED but not yet fully persisted (only has an id). // These result in an INSERT. $this->originalDocumentData[$oid] = $actualData; $changeSet = array(); foreach ($actualData as $propName => $actualValue) { /* At this PersistentCollection shouldn't be here, probably it * was cloned and its ownership must be fixed */ if ($actualValue instanceof PersistentCollection && $actualValue->getOwner() !== $document) { $actualData[$propName] = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName); $actualValue = $actualData[$propName]; } $changeSet[$propName] = array(null, $actualValue); } $this->documentChangeSets[$oid] = $changeSet; } else { // Document is "fully" MANAGED: it was already fully persisted before // and we have a copy of the original data $originalData = $this->originalDocumentData[$oid]; $isChangeTrackingNotify = $class->isChangeTrackingNotify(); if ($isChangeTrackingNotify && !$recompute && isset($this->documentChangeSets[$oid])) { $changeSet = $this->documentChangeSets[$oid]; } else { $changeSet = array(); } foreach ($actualData as $propName => $actualValue) { // skip not saved fields if (isset($class->fieldMappings[$propName]['notSaved']) && $class->fieldMappings[$propName]['notSaved'] === true) { continue; } $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null; // skip if value has not changed if ($orgValue === $actualValue) { // but consider dirty GridFSFile instances as changed if (!(isset($class->fieldMappings[$propName]['file']) && $actualValue->isDirty())) { continue; } } // if relationship is a embed-one, schedule orphan removal to trigger cascade remove operations if (isset($class->fieldMappings[$propName]['embedded']) && $class->fieldMappings[$propName]['type'] === 'one') { if ($orgValue !== null) { $this->scheduleOrphanRemoval($orgValue); } $changeSet[$propName] = array($orgValue, $actualValue); continue; } // if owning side of reference-one relationship if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === 'one' && $class->fieldMappings[$propName]['isOwningSide']) { if ($orgValue !== null && $class->fieldMappings[$propName]['orphanRemoval']) { $this->scheduleOrphanRemoval($orgValue); } $changeSet[$propName] = array($orgValue, $actualValue); continue; } if ($isChangeTrackingNotify) { continue; } // ignore inverse side of reference-many relationship if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === 'many' && $class->fieldMappings[$propName]['isInverseSide']) { continue; } // Persistent collection was exchanged with the "originally" // created one. This can only mean it was cloned and replaced // on another document. if ($actualValue instanceof PersistentCollection && $actualValue->getOwner() !== $document) { $actualValue = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName); } // if embed-many or reference-many relationship if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'many') { $changeSet[$propName] = array($orgValue, $actualValue); /* If original collection was exchanged with a non-empty value * and $set will be issued, there is no need to $unset it first */ if ($actualValue && $actualValue->isDirty() && CollectionHelper::usesSet($class->fieldMappings[$propName]['strategy'])) { continue; } if ($orgValue instanceof PersistentCollection) { $this->scheduleCollectionDeletion($orgValue); } continue; } // skip equivalent date values if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'date') { $dateType = Type::getType('date'); $dbOrgValue = $dateType->convertToDatabaseValue($orgValue); $dbActualValue = $dateType->convertToDatabaseValue($actualValue); if ($dbOrgValue instanceof \MongoDate && $dbActualValue instanceof \MongoDate && $dbOrgValue == $dbActualValue) { continue; } } // regular field $changeSet[$propName] = array($orgValue, $actualValue); } if ($changeSet) { $this->documentChangeSets[$oid] = $recompute && isset($this->documentChangeSets[$oid]) ? $changeSet + $this->documentChangeSets[$oid] : $changeSet; $this->originalDocumentData[$oid] = $actualData; $this->scheduleForUpdate($document); } } // Look for changes in associations of the document $associationMappings = array_filter($class->associationMappings, function ($assoc) { return empty($assoc['notSaved']); }); foreach ($associationMappings as $mapping) { $value = $class->reflFields[$mapping['fieldName']]->getValue($document); if ($value === null) { continue; } $this->computeAssociationChanges($document, $mapping, $value); if (isset($mapping['reference'])) { continue; } $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value->unwrap(); foreach ($values as $obj) { $oid2 = spl_object_hash($obj); if (isset($this->documentChangeSets[$oid2])) { $this->documentChangeSets[$oid][$mapping['fieldName']] = array($value, $value); if (!$isNewDocument) { $this->scheduleForUpdate($document); } break; } } } }
/** * Returns the embedded document to be stored in Riak. * * The return value will usually be an associative array with string keys * corresponding to field names on the embedded document. An object may be * returned if the document is empty, to ensure that a BSON object will be * stored in lieu of an array. * * If $includeNestedCollections is true, nested collections will be included * in this prepared value and the option will cascade to all embedded * associations. If any nested PersistentCollections (embed or reference) * within this value were previously scheduled for deletion or update, they * will also be unscheduled. * * @param array $embeddedMapping * @param object $embeddedDocument * @param boolean $includeNestedCollections * @return array|object * @throws \UnexpectedValueException if an unsupported associating mapping is found */ public function prepareEmbeddedDocumentValue(array $embeddedMapping, $embeddedDocument, $includeNestedCollections = false) { $embeddedDocumentValue = array(); $class = $this->dm->getClassMetadata(get_class($embeddedDocument)); foreach ($class->fieldMappings as $mapping) { // Skip notSaved fields if (!empty($mapping['notSaved'])) { continue; } // Inline ClassMetadataInfo::getFieldValue() $rawValue = $class->reflFields[$mapping['fieldName']]->getValue($embeddedDocument); $value = null; if ($rawValue !== null) { switch (isset($mapping['association']) ? $mapping['association'] : null) { // @Field, @String, @Date, etc. case null: $value = Type::getType($mapping['type'])->convertToDatabaseValue($rawValue); break; case ClassMetadata::EMBED_ONE: case ClassMetadata::REFERENCE_ONE: // Nested collections should only be included for embedded relationships $value = $this->prepareAssociatedDocumentValue($mapping, $rawValue, $includeNestedCollections && isset($mapping['embedded'])); break; case ClassMetadata::EMBED_MANY: case ClassMetadata::REFERENCE_MANY: // Skip PersistentCollections already scheduled for deletion if (!$includeNestedCollections && $rawValue instanceof PersistentCollection && $this->uow->isCollectionScheduledForDeletion($rawValue)) { break; } $value = $this->prepareAssociatedCollectionValue($rawValue, $includeNestedCollections); break; default: throw new \UnexpectedValueException('Unsupported mapping association: ' . $mapping['association']); } } // Omit non-nullable fields that would have a null value if ($value === null && $mapping['nullable'] === false) { continue; } $embeddedDocumentValue[$mapping['name']] = $value; } /* Add a discriminator value if the embedded document is not mapped * explicitly to a targetDocument class. */ if (!isset($embeddedMapping['targetDocument'])) { $discriminatorField = $embeddedMapping['discriminatorField']; $discriminatorValue = isset($embeddedMapping['discriminatorMap']) ? array_search($class->name, $embeddedMapping['discriminatorMap']) : $class->name; /* If the discriminator value was not found in the map, use the full * class name. In the future, it may be preferable to throw an * exception here (perhaps based on some strictness option). * * @see DocumentManager::createDBRef() */ if ($discriminatorValue === false) { $discriminatorValue = $class->name; } $embeddedDocumentValue[$discriminatorField] = $discriminatorValue; } /* If the class has a discriminator (field and value), use it. A child * class that is not defined in the discriminator map may only have a * discriminator field and no value, so default to the full class name. */ if (isset($class->discriminatorField)) { $embeddedDocumentValue[$class->discriminatorField] = isset($class->discriminatorValue) ? $class->discriminatorValue : $class->name; } // Ensure empty embedded documents are stored as BSON objects if (empty($embeddedDocumentValue)) { return (object) $embeddedDocumentValue; } /* @todo Consider always casting the return value to an object, or * building $embeddedDocumentValue as an object instead of an array, to * handle the edge case where all database field names are sequential, * numeric keys. */ return $embeddedDocumentValue; }