DDC-515 - Enhanced Validate-Schema-Command, integrated it with CLI and besides mapping<->database checks also do consistency checks of the mapping files
This commit is contained in:
parent
f2213c4d00
commit
d00f674a08
@ -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();
|
BIN
lib/Doctrine/DBAL/Driver/.DS_Store
vendored
BIN
lib/Doctrine/DBAL/Driver/.DS_Store
vendored
Binary file not shown.
@ -0,0 +1,80 @@
|
||||
<?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.');
|
||||
}
|
||||
}
|
||||
}
|
@ -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');
|
||||
$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("<error>[Mapping] FAIL - The entity-class '" . $className . "' mapping is invalid:</error>\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('<error>[Database] FAIL - The database schema is not in sync with the current mapping file.</error>' . "\n");
|
||||
$exit += 2;
|
||||
} else {
|
||||
$output->write('<info>[Database] OK - The database schema is in sync with the mapping files.</info>' . "\n");
|
||||
}
|
||||
|
||||
exit($exit);
|
||||
}
|
||||
}
|
196
lib/Doctrine/ORM/Tools/SchemaValidator.php
Normal file
196
lib/Doctrine/ORM/Tools/SchemaValidator.php
Normal file
@ -0,0 +1,196 @@
|
||||
<?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 {
|
||||
|
74
tests/Doctrine/Tests/ORM/Tools/SchemaValidatorTest.php
Normal file
74
tests/Doctrine/Tests/ORM/Tools/SchemaValidatorTest.php
Normal file
@ -0,0 +1,74 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
@ -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();
|
Loading…
Reference in New Issue
Block a user