From d00f674a08e900d18891d0ed5f49dff096892f45 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Tue, 11 May 2010 23:08:36 +0200 Subject: [PATCH] DDC-515 - Enhanced Validate-Schema-Command, integrated it with CLI and besides mapping<->database checks also do consistency checks of the mapping files --- bin/doctrine.php | 1 + lib/Doctrine/DBAL/Driver/.DS_Store | Bin 6148 -> 0 bytes .../Command/SchemaValidatorCommand.php | 80 +++++++ .../Console/Command/ValidateSchemaCommand.php | 51 +++-- lib/Doctrine/ORM/Tools/SchemaValidator.php | 196 ++++++++++++++++++ tests/Doctrine/Tests/ORM/Tools/AllTests.php | 1 + .../AbstractClassMetadataExporterTest.php | 11 +- .../Tests/ORM/Tools/SchemaValidatorTest.php | 74 +++++++ tools/sandbox/doctrine.php | 1 + 9 files changed, 393 insertions(+), 22 deletions(-) delete mode 100644 lib/Doctrine/DBAL/Driver/.DS_Store create mode 100644 lib/Doctrine/ORM/Tools/Console/Command/SchemaValidatorCommand.php create mode 100644 lib/Doctrine/ORM/Tools/SchemaValidator.php create mode 100644 tests/Doctrine/Tests/ORM/Tools/SchemaValidatorTest.php diff --git a/bin/doctrine.php b/bin/doctrine.php index b1a29efeb..c307cb800 100644 --- a/bin/doctrine.php +++ b/bin/doctrine.php @@ -52,6 +52,7 @@ $cli->addCommands(array( new \Doctrine\ORM\Tools\Console\Command\GenerateProxiesCommand(), new \Doctrine\ORM\Tools\Console\Command\ConvertMappingCommand(), new \Doctrine\ORM\Tools\Console\Command\RunDqlCommand(), + new \Doctrine\ORM\Tools\Console\Command\ValidateSchemaCommand(), )); $cli->run(); \ No newline at end of file diff --git a/lib/Doctrine/DBAL/Driver/.DS_Store b/lib/Doctrine/DBAL/Driver/.DS_Store deleted file mode 100644 index 5008ddfcf53c02e82d7eee2e57c38e5672ef89f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0. +*/ + +namespace Doctrine\ORM\Tools\Console\Command; + +use Symfony\Components\Console\Input\InputArgument, + Symfony\Components\Console\Input\InputOption, + Symfony\Components\Console; + +/** + * Schema Validator Command + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 1.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class SchemaValidatorCommand extends Console\Command\Command +{ + /** + * @see Console\Command\Command + */ + protected function configure() + { + $this + ->setName('orm:validate-schema') + ->setDescription('Validate that the mapping files.') + ->setHelp(<<getHelper('em')->getEntityManager(); + + $validator = new \Doctrine\ORM\Tools\SchemaValidator($em); + $errors = $validator->validateMapping(); + + if ($errors) { + foreach ($errors AS $className => $errorMessages) { + $output->write("The entity-class '" . $className . "' is invalid:\n"); + foreach ($errorMessages AS $errorMessage) { + $output->write('* ' . $errorMessage . "\n"); + } + $output->write("\n"); + } + } + + if (!$validator->schemaInSyncWithMetadata()) { + $output->write('The database schema is not in sync with the current mapping file.'); + } + } +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Tools/Console/Command/ValidateSchemaCommand.php b/lib/Doctrine/ORM/Tools/Console/Command/ValidateSchemaCommand.php index f3053e08a..6b391e2b8 100644 --- a/lib/Doctrine/ORM/Tools/Console/Command/ValidateSchemaCommand.php +++ b/lib/Doctrine/ORM/Tools/Console/Command/ValidateSchemaCommand.php @@ -44,35 +44,44 @@ class ValidateSchemaCommand extends Console\Command\Command */ protected function configure() { - $this->setName('orm:validate-schema') - ->setDescription('Validate that the current metadata schema is valid.'); + $this + ->setName('orm:validate-schema') + ->setDescription('Validate that the mapping files.') + ->setHelp(<<getHelper('em'); + $em = $this->getHelper('em')->getEntityManager(); - /* @var $em \Doctrine\ORM\EntityManager */ - $em = $emHelper->getEntityManager(); + $validator = new \Doctrine\ORM\Tools\SchemaValidator($em); + $errors = $validator->validateMapping(); - $metadatas = $em->getMetadataFactory()->getAllMetadata(); - - if ( ! empty($metadatas)) { - // Create SchemaTool - $tool = new \Doctrine\ORM\Tools\SchemaTool($em); - $updateSql = $tool->getUpdateSchemaSql($metadatas, false); - - if (count($updateSql) == 0) { - $output->write("[Database] OK - Metadata schema exactly matches the database schema."); - } else { - $output->write("[Database] FAIL - There are differences between metadata and database schema."); + $exit = 0; + if ($errors) { + foreach ($errors AS $className => $errorMessages) { + $output->write("[Mapping] FAIL - The entity-class '" . $className . "' mapping is invalid:\n"); + foreach ($errorMessages AS $errorMessage) { + $output->write('* ' . $errorMessage . "\n"); + } + $output->write("\n"); } - } else { - $output->write("No metadata mappings found"); + $exit += 1; } + + if (!$validator->schemaInSyncWithMetadata()) { + $output->write('[Database] FAIL - The database schema is not in sync with the current mapping file.' . "\n"); + $exit += 2; + } else { + $output->write('[Database] OK - The database schema is in sync with the mapping files.' . "\n"); + } + + exit($exit); } } \ No newline at end of file diff --git a/lib/Doctrine/ORM/Tools/SchemaValidator.php b/lib/Doctrine/ORM/Tools/SchemaValidator.php new file mode 100644 index 000000000..12f51f1ce --- /dev/null +++ b/lib/Doctrine/ORM/Tools/SchemaValidator.php @@ -0,0 +1,196 @@ +. +*/ + +namespace Doctrine\ORM\Tools; + +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\Mapping\ManyToManyMapping; +use Doctrine\ORM\Mapping\OneToOneMapping; + +/** + * Performs strict validation of the mapping schema + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 1.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class SchemaValidator +{ + /** + * @var EntityManager + */ + private $em; + + /** + * @param EntityManager $em + */ + public function __construct(EntityManager $em) + { + $this->em = $em; + } + + /** + * Checks the internal consistency of mapping files. + * + * There are several checks that can't be done at runtime or are to expensive, which can be verified + * with this command. For example: + * + * 1. Check if a relation with "mappedBy" is actually connected to that specified field. + * 2. Check if "mappedBy" and "inversedBy" are consistent to each other. + * 3. Check if "referencedColumnName" attributes are really pointing to primary key columns. + * + * @return array + */ + public function validateMapping() + { + $errors = array(); + $cmf = $this->em->getMetadataFactory(); + $classes = $cmf->getAllMetadata(); + + foreach ($classes AS $class) { + /* @var $class ClassMetadata */ + foreach ($class->associationMappings AS $fieldName => $assoc) { + $ce = array(); + if (!$cmf->hasMetadataFor($assoc->targetEntityName)) { + $ce[] = "The target entity '" . $assoc->targetEntityName . "' specified on " . $class->name . '#' . $fieldName . ' is unknown.'; + } + + if ($assoc->mappedBy && $assoc->inversedBy) { + $ce[] = "The association " . $class . "#" . $fieldName . " cannot be defined as both inverse and owning."; + } + + $targetMetadata = $cmf->getMetadataFor($assoc->targetEntityName); + + /* @var $assoc AssociationMapping */ + if ($assoc->mappedBy) { + if ($targetMetadata->hasField($assoc->mappedBy)) { + $ce[] = "The association " . $class->name . "#" . $fieldName . " refers to the owning side ". + "field " . $assoc->targetEntityName . "#" . $assoc->mappedBy . " which is not defined as association."; + } + if (!$targetMetadata->hasAssociation($assoc->mappedBy)) { + $ce[] = "The association " . $class->name . "#" . $fieldName . " refers to the owning side ". + "field " . $assoc->targetEntityName . "#" . $assoc->mappedBy . " which does not exist."; + } + + if ($targetMetadata->associationMappings[$assoc->mappedBy]->inversedBy == null) { + $ce[] = "The field " . $class->name . "#" . $fieldName . " is on the inverse side of a ". + "bi-directional relationship, but the specified mappedBy association on the target-entity ". + $assoc->targetEntityName . "#" . $assoc->mappedBy . " does not contain the required ". + "'inversedBy' attribute."; + } else if ($targetMetadata->associationMappings[$assoc->mappedBy]->inversedBy != $fieldName) { + $ce[] = "The mappings " . $class->name . "#" . $fieldName . " and " . + $assoc->targetEntityName . "#" . $assoc->mappedBy . " are ". + "incosistent with each other."; + } + } + + if ($assoc->inversedBy) { + if ($targetMetadata->hasField($assoc->inversedBy)) { + $ce[] = "The association " . $class->name . "#" . $fieldName . " refers to the inverse side ". + "field " . $assoc->targetEntityName . "#" . $assoc->inversedBy . " which is not defined as association."; + } + if (!$targetMetadata->hasAssociation($assoc->inversedBy)) { + $ce[] = "The association " . $class->name . "#" . $fieldName . " refers to the inverse side ". + "field " . $assoc->targetEntityName . "#" . $assoc->inversedBy . " which does not exist."; + } + + if ($targetMetadata->associationMappings[$assoc->mappedBy]->mappedBy == null) { + $ce[] = "The field " . $class->name . "#" . $fieldName . " is on the inverse side of a ". + "bi-directional relationship, but the specified mappedBy association on the target-entity ". + $assoc->targetEntityName . "#" . $assoc->mappedBy . " does not contain the required ". + "'inversedBy' attribute."; + } else if ($targetMetadata->associationMappings[$assoc->inversedBy]->mappedBy != $fieldName) { + $ce[] = "The mappings " . $class->name . "#" . $fieldName . " and " . + $assoc->targetEntityName . "#" . $assoc->inversedBy . " are ". + "incosistent with each other."; + } + } + + if ($assoc instanceof ManyToManyMapping && $assoc->isOwningSide) { + foreach ($assoc->joinTable['joinColumns'] AS $joinColumn) { + if (!isset($class->fieldNames[$joinColumn['referencedColumnName']])) { + $ce[] = "The referenced column name '" . $joinColumn['referencedColumnName'] . "' does not " . + "have a corresponding field with this column name on the class '" . $class->name . "'."; + break; + } + + $fieldName = $class->fieldNames[$joinColumn['referencedColumnName']]; + if (!in_array($fieldName, $class->identifier)) { + $ce[] = "The referenced column name '" . $joinColumn['referencedColumnName'] . "' " . + "has to be a primary key column."; + } + } + foreach ($assoc->joinTable['inverseJoinColumns'] AS $inverseJoinColumn) { + if (!isset($class->fieldNames[$inverseJoinColumn['referencedColumnName']])) { + $ce[] = "The referenced column name '" . $inverseJoinColumn['referencedColumnName'] . "' does not " . + "have a corresponding field with this column name on the class '" . $class->name . "'."; + break; + } + + $fieldName = $class->fieldNames[$inverseJoinColumn['referencedColumnName']]; + if (!in_array($fieldName, $class->identifier)) { + $ce[] = "The referenced column name '" . $inverseJoinColumn['referencedColumnName'] . "' " . + "has to be a primary key column."; + } + } + } else if ($assoc instanceof OneToOneMapping) { + foreach ($assoc->joinColumns AS $joinColumn) { + if (!isset($class->fieldNames[$joinColumn['referencedColumnName']])) { + $ce[] = "The referenced column name '" . $joinColumn['referencedColumnName'] . "' does not " . + "have a corresponding field with this column name on the class '" . $class->name . "'."; + break; + } + + $fieldName = $class->fieldNames[$joinColumn['referencedColumnName']]; + if (!in_array($fieldName, $class->identifier)) { + $ce[] = "The referenced column name '" . $joinColumn['referencedColumnName'] . "' " . + "has to be a primary key column."; + } + } + } + + if ($ce) { + $errors[$class->name] = $ce; + } + } + } + + return $errors; + } + + /** + * Check if the Database Schema is in sync with the current metadata state. + * + * @return bool + */ + public function schemaInSyncWithMetadata() + { + $schemaTool = new SchemaTool($this->em); + + $allMetadata = $this->em->getMetadataFactory()->getAllMetadata(); + return (count($schemaTool->getUpdateSchemaSql($allMetadata, false)) == 0); + } +} diff --git a/tests/Doctrine/Tests/ORM/Tools/AllTests.php b/tests/Doctrine/Tests/ORM/Tools/AllTests.php index a729044df..faeeb1dd0 100644 --- a/tests/Doctrine/Tests/ORM/Tools/AllTests.php +++ b/tests/Doctrine/Tests/ORM/Tools/AllTests.php @@ -27,6 +27,7 @@ class AllTests $suite->addTestSuite('Doctrine\Tests\ORM\Tools\ConvertDoctrine1SchemaTest'); $suite->addTestSuite('Doctrine\Tests\ORM\Tools\SchemaToolTest'); $suite->addTestSuite('Doctrine\Tests\ORM\Tools\EntityGeneratorTest'); + $suite->addTestSuite('Doctrine\Tests\ORM\Tools\SchemaValidatorTest'); return $suite; } diff --git a/tests/Doctrine/Tests/ORM/Tools/Export/AbstractClassMetadataExporterTest.php b/tests/Doctrine/Tests/ORM/Tools/Export/AbstractClassMetadataExporterTest.php index 3e6d90df5..f1ba8ba92 100644 --- a/tests/Doctrine/Tests/ORM/Tools/Export/AbstractClassMetadataExporterTest.php +++ b/tests/Doctrine/Tests/ORM/Tools/Export/AbstractClassMetadataExporterTest.php @@ -67,7 +67,16 @@ abstract class AbstractClassMetadataExporterTest extends \Doctrine\Tests\OrmTest protected function _createMetadataDriver($type, $path) { - $class = 'Doctrine\ORM\Mapping\Driver\\' . ucfirst($type) . 'Driver'; + $mappingDriver = array( + 'php' => 'PHPDriver', + 'annotation' => 'AnnotationDriver', + 'xml' => 'XmlDriver', + 'yaml' => 'YamlDriver', + ); + $this->assertArrayHasKey($type, $mappingDriver, "There is no metadata driver for the type '" . $type . "'."); + $driverName = $mappingDriver[$type]; + + $class = 'Doctrine\ORM\Mapping\Driver\\' . $driverName; if ($type === 'annotation') { $driver = $class::create($path); } else { diff --git a/tests/Doctrine/Tests/ORM/Tools/SchemaValidatorTest.php b/tests/Doctrine/Tests/ORM/Tools/SchemaValidatorTest.php new file mode 100644 index 000000000..64bb03f36 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Tools/SchemaValidatorTest.php @@ -0,0 +1,74 @@ +em = $this->_getTestEntityManager(); + $this->validator = new SchemaValidator($this->em); + } + + public function testCmsModelSet() + { + $this->em->getConfiguration()->getMetadataDriverImpl()->addPaths(array( + __DIR__ . "/../../Models/CMS" + )); + $this->validator->validateMapping(); + } + + public function testCompanyModelSet() + { + $this->em->getConfiguration()->getMetadataDriverImpl()->addPaths(array( + __DIR__ . "/../../Models/Company" + )); + $this->validator->validateMapping(); + } + + public function testECommerceModelSet() + { + $this->em->getConfiguration()->getMetadataDriverImpl()->addPaths(array( + __DIR__ . "/../../Models/ECommerce" + )); + $this->validator->validateMapping(); + } + + public function testForumModelSet() + { + $this->em->getConfiguration()->getMetadataDriverImpl()->addPaths(array( + __DIR__ . "/../../Models/Forum" + )); + $this->validator->validateMapping(); + } + + public function testNavigationModelSet() + { + $this->em->getConfiguration()->getMetadataDriverImpl()->addPaths(array( + __DIR__ . "/../../Models/Navigation" + )); + $this->validator->validateMapping(); + } + + public function testRoutingModelSet() + { + $this->em->getConfiguration()->getMetadataDriverImpl()->addPaths(array( + __DIR__ . "/../../Models/Routing" + )); + $this->validator->validateMapping(); + } +} \ No newline at end of file diff --git a/tools/sandbox/doctrine.php b/tools/sandbox/doctrine.php index 3b4a856de..f325e143a 100644 --- a/tools/sandbox/doctrine.php +++ b/tools/sandbox/doctrine.php @@ -36,6 +36,7 @@ $cli->addCommands(array( new \Doctrine\ORM\Tools\Console\Command\GenerateProxiesCommand(), new \Doctrine\ORM\Tools\Console\Command\ConvertMappingCommand(), new \Doctrine\ORM\Tools\Console\Command\RunDqlCommand(), + new \Doctrine\ORM\Tools\Console\Command\ValidateSchemaCommand(), )); $cli->run(); \ No newline at end of file