Commit d00f674a authored by Benjamin Eberlei's avatar Benjamin Eberlei

DDC-515 - Enhanced Validate-Schema-Command, integrated it with CLI and besides...

DDC-515 - Enhanced Validate-Schema-Command, integrated it with CLI and besides mapping<->database checks also do consistency checks of the mapping files
parent f2213c4d
......@@ -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
<?php
/*
* $Id$
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the LGPL. For more information, see
* <http://www.doctrine-project.org>.
*/
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 <kontakt@beberlei.de>
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
* @author Jonathan Wage <jonwage@gmail.com>
* @author Roman Borschel <roman@code-factory.org>
*/
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(<<<EOT
'Validate that the mapping files are correct and in sync with the database.'
EOT
);
}
/**
* @see Console\Command\Command
*/
protected function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output)
{
$em = $this->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
......@@ -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(<<<EOT
'Validate that the mapping files are correct and in sync with the database.'
EOT
);
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @see Console\Command\Command
*/
protected function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output)
{
$emHelper = $this->getHelper('em');
/* @var $em \Doctrine\ORM\EntityManager */
$em = $emHelper->getEntityManager();
$metadatas = $em->getMetadataFactory()->getAllMetadata();
$em = $this->getHelper('em')->getEntityManager();
if ( ! empty($metadatas)) {
// Create SchemaTool
$tool = new \Doctrine\ORM\Tools\SchemaTool($em);
$updateSql = $tool->getUpdateSchemaSql($metadatas, false);
$validator = new \Doctrine\ORM\Tools\SchemaValidator($em);
$errors = $validator->validateMapping();
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("<error>[Mapping] FAIL - The entity-class '" . $className . "' mapping is invalid:</error>\n");
foreach ($errorMessages AS $errorMessage) {
$output->write('* ' . $errorMessage . "\n");
}
$output->write("\n");
}
$exit += 1;
}
if (!$validator->schemaInSyncWithMetadata()) {
$output->write('<error>[Database] FAIL - The database schema is not in sync with the current mapping file.</error>' . "\n");
$exit += 2;
} else {
$output->write("No metadata mappings found");
$output->write('<info>[Database] OK - The database schema is in sync with the mapping files.</info>' . "\n");
}
exit($exit);
}
}
\ No newline at end of file
<?php
/*
* $Id$
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the LGPL. For more information, see
* <http://www.doctrine-project.org>.
*/
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 <kontakt@beberlei.de>
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
* @author Jonathan Wage <jonwage@gmail.com>
* @author Roman Borschel <roman@code-factory.org>
*/
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);
}
}
......@@ -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;
}
......
......@@ -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 {
......
<?php
namespace Doctrine\Tests\ORM\Tools;
use Doctrine\ORM\Tools\SchemaValidator;
require_once __DIR__ . '/../../TestInit.php';
class SchemaValidatorTest extends \Doctrine\Tests\OrmTestCase
{
/**
* @var EntityManager
*/
private $em = null;
/**
* @var SchemaValidator
*/
private $validator = null;
public function setUp()
{
$this->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
......@@ -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
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment