From d90e71d0028f0a9759cb2bef23ba327d2e1c6ba0 Mon Sep 17 00:00:00 2001 From: romanb Date: Mon, 1 Jun 2009 16:14:11 +0000 Subject: [PATCH] [2.0] First implementation of XmlDriver + tests. First draft of XSD document. --- doctrine-mapping.xsd | 143 ++++++ lib/Doctrine/Common/ClassLoader.php | 2 +- .../ORM/Mapping/ClassMetadataFactory.php | 2 - .../ORM/Mapping/Driver/AnnotationDriver.php | 6 +- lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php | 427 ++++++++++++++++++ lib/Doctrine/ORM/Mapping/OneToOneMapping.php | 2 +- tests/Doctrine/Tests/ORM/Mapping/AllTests.php | 1 + .../Tests/ORM/Mapping/XmlDriverTest.php | 67 +++ tests/Doctrine/Tests/ORM/Mapping/xml/User.php | 13 + .../Mapping/xml/XmlMappingTest.User.dcm.xml | 40 ++ 10 files changed, 696 insertions(+), 7 deletions(-) create mode 100644 doctrine-mapping.xsd create mode 100644 lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php create mode 100644 tests/Doctrine/Tests/ORM/Mapping/XmlDriverTest.php create mode 100644 tests/Doctrine/Tests/ORM/Mapping/xml/User.php create mode 100644 tests/Doctrine/Tests/ORM/Mapping/xml/XmlMappingTest.User.dcm.xml diff --git a/doctrine-mapping.xsd b/doctrine-mapping.xsd new file mode 100644 index 000000000..97d15379b --- /dev/null +++ b/doctrine-mapping.xsd @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lib/Doctrine/Common/ClassLoader.php b/lib/Doctrine/Common/ClassLoader.php index 718f1f21f..92f3cd6a6 100644 --- a/lib/Doctrine/Common/ClassLoader.php +++ b/lib/Doctrine/Common/ClassLoader.php @@ -59,7 +59,7 @@ class ClassLoader } /** - * Set namespace separator + * Sets the namespace separator to use. * * @param string $separator * @return void diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php index 35d54c311..49f02aea5 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php @@ -66,12 +66,10 @@ class ClassMetadataFactory public function setCacheDriver($cacheDriver) { $this->_cacheDriver = $cacheDriver; - /* foreach ($this->_driver->preload() as $className) { $cacheKey = "$className\$CLASSMETADATA"; $this->_cacheDriver->save($cacheKey, $this->getMetadataFor($className), null); } - */ } /** diff --git a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php index 3fa114247..3ab8d4911 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php @@ -61,12 +61,12 @@ class AnnotationDriver )); } - // Evaluate DoctrineInheritanceType annotation + // Evaluate InheritanceType annotation if ($inheritanceTypeAnnot = $annotClass->getAnnotation('InheritanceType')) { $metadata->setInheritanceType($inheritanceTypeAnnot->value); } - // Evaluate DoctrineDiscriminatorColumn annotation + // Evaluate DiscriminatorColumn annotation if ($discrColumnAnnot = $annotClass->getAnnotation('DiscriminatorColumn')) { $metadata->setDiscriminatorColumn(array( 'name' => $discrColumnAnnot->name, @@ -75,7 +75,7 @@ class AnnotationDriver )); } - // Evaluate DoctrineDiscriminatorMap annotation + // Evaluate DiscriminatorValue annotation if ($discrValueAnnot = $annotClass->getAnnotation('DiscriminatorValue')) { $metadata->setDiscriminatorValue($discrValueAnnot->value); } diff --git a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php new file mode 100644 index 000000000..5d9b25505 --- /dev/null +++ b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php @@ -0,0 +1,427 @@ +. + */ + +namespace Doctrine\ORM\Mapping\Driver; + +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Mapping\MappingException; + +/** + * XmlDriver is a metadata driver that enables mapping through XML files. + * + * @author Roman Borschel + * @since 2.0 + */ +class XmlDriver +{ + /** + * The FILE_PER_CLASS mode is an operating mode of the XmlDriver where it loads + * the mapping files of individual classes on demand. This requires the user to + * adhere to the convention of 1 mapping file per class and the file names of + * the mapping files must correspond to the full class name, including namespace, + * with the namespace delimiters '\', replaced by dots '.'. + * + * Example: + * Class: My\Project\Model\User + * Mapping file: My.Project.Model.User.dcm.xml + * + * @var integer + */ + const FILE_PER_CLASS = 1; + + /** + * The PRELOAD mode is an operating mode of the XmlDriver where it loads + * all mapping files in advance. This is the default behavior. It does not + * require a naming convention or the convention of 1 class per mapping file. + * + * @var integer + */ + const PRELOAD = 2; + + /** The paths where to look for mapping files. */ + private $_paths; + /** The operating mode. Either FILE_PER_CLASS or PRELOAD. */ + private $_mode; + /** The file extension of mapping documents. */ + private $_fileExtension = '.dcm.xml'; + /** Any preloaded XML elements. */ + private $_xmlElements = array(); + + /** + * Initializes a new XmlDriver that looks in the given path(s) for mapping + * documents and operates in the specified operating mode. + * + * @param string|array $paths One or multiple paths where mapping documents can be found. + * @param integer $mode The operating mode. Either PRELOAD (default) or FILE_PER_CLASS. + */ + public function __construct($paths, $mode = self::PRELOAD) + { + $this->_paths = $paths; + $this->_mode = $mode; + } + + /** + * Gets any preloaded XML documents. + * + * @return array + */ + public function getPreloadedXmlElements() + { + return $this->_xmlElements; + } + + /** + * Loads the metadata for the specified class into the provided container. + * + * @param string $className + * @param ClassMetadata $metadata + */ + public function loadMetadataForClass($className, ClassMetadata $metadata) + { + if (isset($this->_xmlElements[$className])) { + $xmlRoot = $this->_xmlElements[$className]; + unset($this->_xmlElements[$className]); + } else { + $result = $this->_loadMappingFile($this->_findMappingFile($className)); + $xmlRoot = $result[$className]; + } + + if ($xmlRoot->getName() == 'entity') { + + // Evaluate attributes + if (isset($xmlRoot['table'])) { + $metadata->primaryTable['name'] = (string)$xmlRoot['table']; + } + if (isset($xmlRoot['schema'])) { + $metadata->primaryTable['schema'] = (string)$xmlRoot['schema']; + } + if (isset($xmlRoot['inheritance-type'])) { + $metadata->setInheritanceType((string)$xmlRoot['inheritance-type']); + } + + // Evaluate mappings + if (isset($xmlRoot->field)) { + foreach ($xmlRoot->field as $fieldMapping) { + $mapping = array( + 'fieldName' => (string)$fieldMapping['name'], + 'type' => (string)$fieldMapping['type'] + ); + if (isset($fieldMapping['column'])) { + $mapping['columnName'] = (string)$fieldMapping['column']; + } + if (isset($fieldMapping['length'])) { + $mapping['length'] = (int)$fieldMapping['length']; + } + $metadata->mapField($mapping); + } + } + + + // Evaluate mappings + foreach ($xmlRoot->id as $idElement) { + $mapping = array( + 'id' => true, + 'fieldName' => (string)$idElement['name'], + 'type' => (string)$idElement['type'] + ); + if (isset($idElement['column'])) { + $mapping['columnName'] = (string)$idElement['column']; + } + $metadata->mapField($mapping); + + if (isset($idElement->generator)) { + $metadata->setIdGeneratorType((string)$idElement->generator['strategy']); + } + } + + // Evaluate mappings + if (isset($xmlRoot->{'one-to-one'})) { + foreach ($xmlRoot->{'one-to-one'} as $oneToOneElement) { + $mapping = array( + 'fieldName' => (string)$oneToOneElement['field'], + 'targetEntity' => (string)$oneToOneElement['targetEntity'] + ); + if (isset($oneToOneElement['mappedBy'])) { + $mapping['mappedBy'] = (string)$oneToOneElement['mappedBy']; + } else { + $joinColumns = array(); + if (isset($oneToOneElement->{'join-column'})) { + $joinColumns[] = $this->_getJoinColumnMapping($oneToOneElement->{'join-column'}); + } else if (isset($oneToOneElement->{'join-columns'})) { + foreach ($oneToOneElement->{'join-columns'}->{'join-column'} as $joinColumnElement) { + $joinColumns[] = $this->_getJoinColumnMapping($joinColumnElement); + } + } else { + throw MappingException::invalidMapping($mapping['fieldName']); + } + $mapping['joinColumns'] = $joinColumns; + } + + if (isset($oneToOneElement->cascade)) { + $mapping['cascade'] = $this->_getCascadeMappings($oneToOneElement->cascade); + } + + $metadata->mapOneToOne($mapping); + } + } + + // Evaluate mappings + if (isset($xmlRoot->{'one-to-many'})) { + foreach ($xmlRoot->{'one-to-many'} as $oneToManyElement) { + $mapping = array( + 'fieldName' => (string)$oneToManyElement['field'], + 'targetEntity' => (string)$oneToManyElement['targetEntity'], + 'mappedBy' => (string)$oneToManyElement['mappedBy'] + ); + if (isset($oneToManyElement->cascade)) { + $mapping['cascade'] = $this->_getCascadeMappings($oneToManyElement->cascade); + } + $metadata->mapOneToMany($mapping); + } + } + + // Evaluate mappings + if (isset($xmlRoot->{'many-to-one'})) { + foreach ($xmlRoot->{'many-to-one'} as $manyToOneElement) { + $mapping = array( + 'fieldName' => (string)$manyToOneElement['field'], + 'targetEntity' => (string)$manyToOneElement['targetEntity'] + ); + $joinColumns = array(); + if (isset($manyToOneElement->{'join-column'})) { + $joinColumns[] = $this->_getJoinColumnMapping($manyToOneElement->{'join-column'}); + } else if (isset($manyToOneElement->{'join-columns'})) { + foreach ($manyToOneElement->{'join-columns'}->{'join-column'} as $joinColumnElement) { + $joinColumns[] = $this->_getJoinColumnMapping($joinColumnElement); + } + } else { + throw MappingException::invalidMapping($mapping['fieldName']); + } + $mapping['joinColumns'] = $joinColumns; + if (isset($manyToOneElement->cascade)) { + $mapping['cascade'] = $this->_getCascadeMappings($manyToOneElement->cascade); + } + $metadata->mapManyToOne($mapping); + } + } + + // Evaluate mappings + if (isset($xmlRoot->{'many-to-many'})) { + foreach ($xmlRoot->{'many-to-many'} as $manyToManyElement) { + $mapping = array( + 'fieldName' => (string)$manyToManyElement['field'], + 'targetEntity' => (string)$manyToManyElement['targetEntity'] + ); + + if (isset($manyToManyElement['mappedBy'])) { + $mapping['mappedBy'] = (string)$manyToManyElement['mappedBy']; + } else if (isset($manyToManyElement->{'join-table'})) { + $joinTableElement = $manyToManyElement->{'join-table'}; + $joinTable = array( + 'name' => (string)$joinTableElement['name'] + ); + if (isset($joinTableElement['schema'])) { + $joinTable['schema'] = (string)$joinTableElement['schema']; + } + foreach ($joinTableElement->{'join-columns'}->{'join-column'} as $joinColumnElement) { + $joinTable['joinColumns'][] = $this->_getJoinColumnMapping($joinColumnElement); + } + foreach ($joinTableElement->{'inverse-join-columns'}->{'join-column'} as $joinColumnElement) { + $joinTable['inverseJoinColumns'][] = $this->_getJoinColumnMapping($joinColumnElement); + } + $mapping['joinTable'] = $joinTable; + } else { + throw MappingException::invalidMapping($mapping['fieldName']); + } + + if (isset($manyToManyElement->cascade)) { + $mapping['cascade'] = $this->_getCascadeMappings($manyToManyElement->cascade); + } + + $metadata->mapManyToMany($mapping); + } + } + + } else if ($xmlRoot->getName() == 'mapped-superclass') { + throw MappingException::notImplemented('Mapped superclasses are not yet supported.'); + } + + } + + /** + * Whether the class with the specified name should have its metadata loaded. + * This is only the case if it is either mapped as an Entity or a + * MappedSuperclass. + * + * @param string $className + * @return boolean + */ + public function isTransient($className) + { + $isTransient = true; + if ($this->_mode == self::FILE_PER_CLASS) { + // check whether file exists + foreach ((array)$this->_paths as $path) { + if (file_exists($path . DIRECTORY_SEPARATOR . str_replace('\\', '.', $className) . $this->_fileExtension)) { + $isTransient = false; + break; + } + } + } else { + $isTransient = isset($this->_xmlElements[$className]); + } + + return $isTransient; + } + + /** + * Preloads all mapping information found in any documents within the + * configured paths and returns a list of class names that have been preloaded. + * + * @return array The list of class names that have been preloaded. + */ + public function preload() + { + if ($this->_mode != self::PRELOAD) { + return array(); + } + + foreach ((array)$this->_paths as $path) { + if (is_dir($path)) { + $files = glob($path . '/*' . $this->_fileExtension); + foreach ($files as $file) { + $this->_xmlElements = array_merge($this->_xmlElements, $this->_loadMappingFile($file)); + } + } else if (is_file($path)) { + $this->_xmlElements = array_merge($this->_xmlElements, $this->_loadMappingFile($path)); + } + } + + return array_keys($this->_xmlElements); + } + + /** + * Loads a mapping file with the given name and returns a map + * from class/entity names to their corresponding SimpleXMLElement nodes. + * + * @param string $file The mapping file to load. + * @return array + */ + private function _loadMappingFile($file) + { + $result = array(); + $xmlElement = simplexml_load_file($file); + + if (isset($xmlElement->entity)) { + foreach ($xmlElement->entity as $entityElement) { + $entityName = (string)$entityElement['name']; + $result[$entityName] = $entityElement; + } + } else if (isset($xmlElement->{'mapped-superclass'})) { + foreach ($xmlElement->{'mapped-superclass'} as $mapperSuperClass) { + $className = (string)$mappedSuperClass['name']; + $result[$className] = $mappedSuperClass; + } + } + + return $result; + } + + /** + * Finds the mapping file for the class with the given name by searching + * through the configured paths. + * + * @param $className + * @return string The (absolute) file name. + * @throws MappingException + */ + private function _findMappingFile($className) + { + $fileName = null; + foreach ((array)$this->_paths as $path) { + $fileName = $path . DIRECTORY_SEPARATOR . str_replace('\\', '.', $className) . $this->_fileExtension; + if (file_exists($fileName)) { + break; + } + } + + if ($fileName === null) { + throw MappingException::mappingFileNotFound($className); + } + + return $fileName; + } + + /** + * Constructs a joinColumn mapping array based on the information + * found in the given SimpleXMLElement. + * + * @param $joinColumnElement The XML element. + * @return array The mapping array. + */ + private function _getJoinColumnMapping(\SimpleXMLElement $joinColumnElement) + { + $joinColumn = array( + 'name' => (string)$joinColumnElement['name'], + 'referencedColumnName' => (string)$joinColumnElement['referencedColumnName'] + ); + if (isset($joinColumnElement['unique'])) { + $joinColumn['unique'] = (bool)$joinColumnElement['unique']; + } + if (isset($joinColumnElement['nullable'])) { + $joinColumn['nullable'] = (bool)$joinColumnElement['nullable']; + } + if (isset($joinColumnElement['onDelete'])) { + $joinColumn['onDelete'] = (string)$joinColumnElement['onDelete']; + } + if (isset($joinColumnElement['onUpdate'])) { + $joinColumn['onUpdate'] = (string)$joinColumnElement['onUpdate']; + } + + return $joinColumn; + } + + /** + * Gathers a list of cascade options found in the given cascade element. + * + * @param $cascadeElement The cascade element. + * @return array The list of cascade options. + */ + private function _getCascadeMappings($cascadeElement) + { + $cascades = array(); + if (isset($cascadeElement->{'cascade-save'})) { + $cascades[] = 'save'; + } + if (isset($cascadeElement->{'cascade-delete'})) { + $cascades[] = 'delete'; + } + if (isset($cascadeElement->{'cascade-merge'})) { + $cascades[] = 'merge'; + } + if (isset($cascadeElement->{'cascade-refresh'})) { + $cascades[] = 'refresh'; + } + + return $cascades; + } +} + diff --git a/lib/Doctrine/ORM/Mapping/OneToOneMapping.php b/lib/Doctrine/ORM/Mapping/OneToOneMapping.php index d69135cbc..e902f34a2 100644 --- a/lib/Doctrine/ORM/Mapping/OneToOneMapping.php +++ b/lib/Doctrine/ORM/Mapping/OneToOneMapping.php @@ -89,7 +89,7 @@ class OneToOneMapping extends AssociationMapping if ($this->isOwningSide()) { if ( ! isset($mapping['joinColumns'])) { - throw MappingException::invalidMapping($this->_sourceFieldName); + throw MappingException::invalidMapping($this->sourceFieldName); } $this->joinColumns = $mapping['joinColumns']; foreach ($mapping['joinColumns'] as $joinColumn) { diff --git a/tests/Doctrine/Tests/ORM/Mapping/AllTests.php b/tests/Doctrine/Tests/ORM/Mapping/AllTests.php index 50c6f92a9..968df039f 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/AllTests.php +++ b/tests/Doctrine/Tests/ORM/Mapping/AllTests.php @@ -20,6 +20,7 @@ class AllTests $suite = new \Doctrine\Tests\DoctrineTestSuite('Doctrine Orm Mapping'); $suite->addTestSuite('Doctrine\Tests\ORM\Mapping\ClassMetadataTest'); + $suite->addTestSuite('Doctrine\Tests\ORM\Mapping\XmlDriverTest'); //$suite->addTestSuite('Doctrine\Tests\ORM\Mapping\ClassMetadataFactoryTest'); return $suite; diff --git a/tests/Doctrine/Tests/ORM/Mapping/XmlDriverTest.php b/tests/Doctrine/Tests/ORM/Mapping/XmlDriverTest.php new file mode 100644 index 000000000..1fae46529 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Mapping/XmlDriverTest.php @@ -0,0 +1,67 @@ +assertFalse($xmlDriver->isTransient($className)); + + $xmlDriver->loadMetadataForClass($className, $class); + + $this->assertEquals('cms_users', $class->getTableName()); + $this->assertEquals(ClassMetadata::INHERITANCE_TYPE_NONE, $class->getInheritanceType()); + $this->assertEquals(2, count($class->fieldMappings)); + $this->assertTrue(isset($class->fieldMappings['id'])); + $this->assertTrue(isset($class->fieldMappings['name'])); + $this->assertEquals('string', $class->fieldMappings['name']['type']); + $this->assertEquals(array('id'), $class->identifier); + $this->assertEquals(ClassMetadata::GENERATOR_TYPE_AUTO, $class->getIdGeneratorType()); + + $this->assertEquals(3, count($class->associationMappings)); + $this->assertEquals(1, count($class->inverseMappings)); + + $this->assertTrue($class->associationMappings['address'] instanceof \Doctrine\ORM\Mapping\OneToOneMapping); + $this->assertTrue(isset($class->associationMappings['address'])); + $this->assertTrue($class->associationMappings['address']->isOwningSide); + + $this->assertTrue($class->associationMappings['phonenumbers'] instanceof \Doctrine\ORM\Mapping\OneToManyMapping); + $this->assertTrue(isset($class->associationMappings['phonenumbers'])); + $this->assertFalse($class->associationMappings['phonenumbers']->isOwningSide); + $this->assertTrue($class->associationMappings['phonenumbers']->isInverseSide()); + $this->assertTrue($class->associationMappings['phonenumbers']->isCascadeSave); + + $this->assertTrue($class->associationMappings['groups'] instanceof \Doctrine\ORM\Mapping\ManyToManyMapping); + $this->assertTrue(isset($class->associationMappings['groups'])); + $this->assertTrue($class->associationMappings['groups']->isOwningSide); + + } + + public function testPreloadMode() + { + $className = 'XmlMappingTest\User'; + $xmlDriver = new XmlDriver(__DIR__ . DIRECTORY_SEPARATOR . 'xml'); + $class = new ClassMetadata($className); + + $classNames = $xmlDriver->preload(); + + $this->assertEquals($className, $classNames[0]); + $this->assertEquals(1, count($xmlDriver->getPreloadedXmlElements())); + + $xmlDriver->loadMetadataForClass($className, $class); + + $this->assertEquals(0, count($xmlDriver->getPreloadedXmlElements())); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Mapping/xml/User.php b/tests/Doctrine/Tests/ORM/Mapping/xml/User.php new file mode 100644 index 000000000..9cd7ac40a --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Mapping/xml/User.php @@ -0,0 +1,13 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +