From 9cd0379f537f8447fcf33c05ce1d2e1418786589 Mon Sep 17 00:00:00 2001 From: beberlei Date: Wed, 20 Jan 2010 22:35:18 +0000 Subject: [PATCH] [2.0] DDC-200 Implemented support for @columnDefinition - even with support to pass the definition to join columns if necessary for relations. --- .../DBAL/Platforms/AbstractPlatform.php | 46 +- .../DBAL/Platforms/AbstractPlatform.php.orig | 1814 +++++++++++++++++ lib/Doctrine/DBAL/Schema/Column.php | 22 + .../ORM/Mapping/Driver/AnnotationDriver.php | 4 + .../Mapping/Driver/AnnotationDriver.php.orig | 374 ++++ .../Mapping/Driver/DoctrineAnnotations.php | 1 + lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php | 4 + .../ORM/Mapping/Driver/XmlDriver.php.orig | 452 ++++ .../ORM/Mapping/Driver/YamlDriver.php | 3 + .../ORM/Mapping/Driver/YamlDriver.php.orig | 451 ++++ lib/Doctrine/ORM/Tools/SchemaTool.php | 14 +- lib/Doctrine/ORM/Tools/SchemaTool.php.orig | 605 ++++++ .../Platforms/AbstractPlatformTestCase.php | 9 + .../Doctrine/Tests/DBAL/Schema/ColumnTest.php | 1 + tests/Doctrine/Tests/ORM/Tools/AllTests.php | 3 +- .../Tests/ORM/Tools/SchemaToolTest.php | 24 + 16 files changed, 3811 insertions(+), 16 deletions(-) create mode 100644 lib/Doctrine/DBAL/Platforms/AbstractPlatform.php.orig create mode 100644 lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php.orig create mode 100644 lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php.orig create mode 100644 lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php.orig create mode 100644 lib/Doctrine/ORM/Tools/SchemaTool.php.orig diff --git a/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php b/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php index 58632c4c1..370566660 100644 --- a/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php +++ b/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php @@ -583,7 +583,7 @@ abstract class AbstractPlatform $columnData['precision'] = $column->getPrecision(); $columnData['scale'] = $column->getScale(); $columnData['default'] = $column->getDefault(); - // TODO: Fixed? Unsigned? + $columnData['columnDefinition'] = $column->getColumnDefinition(); if(in_array($column->getName(), $options['primary'])) { $columnData['primary'] = true; @@ -903,30 +903,37 @@ abstract class AbstractPlatform * unique constraint * check * column check constraint + * columnDefinition + * a string that defines the complete column * * @return string DBMS specific SQL code portion that should be used to declare the column. */ public function getColumnDeclarationSql($name, array $field) { - $default = $this->getDefaultValueDeclarationSql($field); + if (isset($field['columnDefinition'])) { + $columnDef = $this->getCustomTypeDeclarationSql($field); + } else { + $default = $this->getDefaultValueDeclarationSql($field); - $charset = (isset($field['charset']) && $field['charset']) ? - ' ' . $this->getColumnCharsetDeclarationSql($field['charset']) : ''; + $charset = (isset($field['charset']) && $field['charset']) ? + ' ' . $this->getColumnCharsetDeclarationSql($field['charset']) : ''; - $collation = (isset($field['collation']) && $field['collation']) ? - ' ' . $this->getColumnCollationDeclarationSql($field['collation']) : ''; + $collation = (isset($field['collation']) && $field['collation']) ? + ' ' . $this->getColumnCollationDeclarationSql($field['collation']) : ''; - $notnull = (isset($field['notnull']) && $field['notnull']) ? ' NOT NULL' : ''; + $notnull = (isset($field['notnull']) && $field['notnull']) ? ' NOT NULL' : ''; - $unique = (isset($field['unique']) && $field['unique']) ? - ' ' . $this->getUniqueFieldDeclarationSql() : ''; + $unique = (isset($field['unique']) && $field['unique']) ? + ' ' . $this->getUniqueFieldDeclarationSql() : ''; - $check = (isset($field['check']) && $field['check']) ? - ' ' . $field['check'] : ''; + $check = (isset($field['check']) && $field['check']) ? + ' ' . $field['check'] : ''; - $typeDecl = $field['type']->getSqlDeclaration($field, $this); + $typeDecl = $field['type']->getSqlDeclaration($field, $this); + $columnDef = $typeDecl . $charset . $default . $notnull . $unique . $check . $collation; + } - return $name . ' ' . $typeDecl . $charset . $default . $notnull . $unique . $check . $collation; + return $name . ' ' . $columnDef; } /** @@ -1081,6 +1088,19 @@ abstract class AbstractPlatform . ')'; } + /** + * getCustomTypeDeclarationSql + * Obtail SQL code portion needed to create a custom column, + * e.g. when a field has the "columnDefinition" keyword. + * Only "AUTOINCREMENT" and "PRIMARY KEY" are added if appropriate. + * + * @return string + */ + public function getCustomTypeDeclarationSql(array $columnDef) + { + return $columnDef['columnDefinition']; + } + /** * getIndexFieldDeclarationList * Obtain DBMS specific SQL code portion needed to set an index diff --git a/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php.orig b/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php.orig new file mode 100644 index 000000000..58632c4c1 --- /dev/null +++ b/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php.orig @@ -0,0 +1,1814 @@ +. + */ + +namespace Doctrine\DBAL\Platforms; + +use Doctrine\DBAL\DBALException, + Doctrine\DBAL\Connection, + Doctrine\DBAL\Types, + Doctrine\DBAL\Schema\Table, + Doctrine\DBAL\Schema\Index, + Doctrine\DBAL\Schema\ForeignKeyConstraint, + Doctrine\DBAL\Schema\TableDiff; + +/** + * Base class for all DatabasePlatforms. The DatabasePlatforms are the central + * point of abstraction of platform-specific behaviors, features and SQL dialects. + * They are a passive source of information. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Lukas Smith (PEAR MDB2 library) + */ +abstract class AbstractPlatform +{ + /** + * @var int + */ + const CREATE_INDEXES = 1; + + /** + * @var int + */ + const CREATE_FOREIGNKEYS = 2; + + /** + * Constructor. + */ + public function __construct() {} + + /** + * Gets the character used for identifier quoting. + * + * @return string + */ + public function getIdentifierQuoteCharacter() + { + return '"'; + } + + /** + * Gets the string portion that starts an SQL comment. + * + * @return string + */ + public function getSqlCommentStartString() + { + return "--"; + } + + /** + * Gets the string portion that ends an SQL comment. + * + * @return string + */ + public function getSqlCommentEndString() + { + return "\n"; + } + + /** + * Gets the maximum length of a varchar field. + * + * @return integer + */ + public function getVarcharMaxLength() + { + return 255; + } + + /** + * Gets all SQL wildcard characters of the platform. + * + * @return array + */ + public function getWildcards() + { + return array('%', '_'); + } + + /** + * Returns the regular expression operator. + * + * @return string + */ + public function getRegexpExpression() + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * Returns the average value of a column + * + * @param string $column the column to use + * @return string generated sql including an AVG aggregate function + */ + public function getAvgExpression($column) + { + return 'AVG(' . $column . ')'; + } + + /** + * Returns the number of rows (without a NULL value) of a column + * + * If a '*' is used instead of a column the number of selected rows + * is returned. + * + * @param string|integer $column the column to use + * @return string generated sql including a COUNT aggregate function + */ + public function getCountExpression($column) + { + return 'COUNT(' . $column . ')'; + } + + /** + * Returns the highest value of a column + * + * @param string $column the column to use + * @return string generated sql including a MAX aggregate function + */ + public function getMaxExpression($column) + { + return 'MAX(' . $column . ')'; + } + + /** + * Returns the lowest value of a column + * + * @param string $column the column to use + * @return string + */ + public function getMinExpression($column) + { + return 'MIN(' . $column . ')'; + } + + /** + * Returns the total sum of a column + * + * @param string $column the column to use + * @return string + */ + public function getSumExpression($column) + { + return 'SUM(' . $column . ')'; + } + + // scalar functions + + /** + * Returns the md5 sum of a field. + * + * Note: Not SQL92, but common functionality + * + * @return string + */ + public function getMd5Expression($column) + { + return 'MD5(' . $column . ')'; + } + + /** + * Returns the length of a text field. + * + * @param string $expression1 + * @param string $expression2 + * @return string + */ + public function getLengthExpression($column) + { + return 'LENGTH(' . $column . ')'; + } + + /** + * Rounds a numeric field to the number of decimals specified. + * + * @param string $expression1 + * @param string $expression2 + * @return string + */ + public function getRoundExpression($column, $decimals = 0) + { + return 'ROUND(' . $column . ', ' . $decimals . ')'; + } + + /** + * Returns the remainder of the division operation + * $expression1 / $expression2. + * + * @param string $expression1 + * @param string $expression2 + * @return string + */ + public function getModExpression($expression1, $expression2) + { + return 'MOD(' . $expression1 . ', ' . $expression2 . ')'; + } + + /** + * trim + * returns the string $str with leading and proceeding space characters removed + * + * @param string $str literal string or column name + * @return string + */ + public function getTrimExpression($str) + { + return 'TRIM(' . $str . ')'; + } + + /** + * rtrim + * returns the string $str with proceeding space characters removed + * + * @param string $str literal string or column name + * @return string + */ + public function getRtrimExpression($str) + { + return 'RTRIM(' . $str . ')'; + } + + /** + * ltrim + * returns the string $str with leading space characters removed + * + * @param string $str literal string or column name + * @return string + */ + public function getLtrimExpression($str) + { + return 'LTRIM(' . $str . ')'; + } + + /** + * upper + * Returns the string $str with all characters changed to + * uppercase according to the current character set mapping. + * + * @param string $str literal string or column name + * @return string + */ + public function getUpperExpression($str) + { + return 'UPPER(' . $str . ')'; + } + + /** + * lower + * Returns the string $str with all characters changed to + * lowercase according to the current character set mapping. + * + * @param string $str literal string or column name + * @return string + */ + public function getLowerExpression($str) + { + return 'LOWER(' . $str . ')'; + } + + /** + * locate + * returns the position of the first occurrence of substring $substr in string $str + * + * @param string $substr literal string to find + * @param string $str literal string + * @return integer + */ + public function getLocateExpression($str, $substr) + { + return 'LOCATE(' . $str . ', ' . $substr . ')'; + } + + /** + * Returns the current system date. + * + * @return string + */ + public function getNowExpression() + { + return 'NOW()'; + } + + /** + * return string to call a function to get a substring inside an SQL statement + * + * Note: Not SQL92, but common functionality. + * + * SQLite only supports the 2 parameter variant of this function + * + * @param string $value an sql string literal or column name/alias + * @param integer $position where to start the substring portion + * @param integer $length the substring portion length + * @return string SQL substring function with given parameters + */ + public function getSubstringExpression($value, $from, $len = null) + { + if ($len === null) + return 'SUBSTRING(' . $value . ' FROM ' . $from . ')'; + else { + return 'SUBSTRING(' . $value . ' FROM ' . $from . ' FOR ' . $len . ')'; + } + } + + /** + * Returns a series of strings concatinated + * + * concat() accepts an arbitrary number of parameters. Each parameter + * must contain an expression + * + * @param string $arg1, $arg2 ... $argN strings that will be concatinated. + * @return string + */ + public function getConcatExpression() + { + return join(' || ' , func_get_args()); + } + + /** + * Returns the SQL for a logical not. + * + * Example: + * + * $q = new Doctrine_Query(); + * $e = $q->expr; + * $q->select('*')->from('table') + * ->where($e->eq('id', $e->not('null')); + * + * + * @return string a logical expression + */ + public function getNotExpression($expression) + { + return 'NOT(' . $expression . ')'; + } + + /** + * Returns the SQL to check if a value is one in a set of + * given values. + * + * in() accepts an arbitrary number of parameters. The first parameter + * must always specify the value that should be matched against. Successive + * must contain a logical expression or an array with logical expressions. + * These expressions will be matched against the first parameter. + * + * @param string $column the value that should be matched against + * @param string|array(string) values that will be matched against $column + * @return string logical expression + */ + public function getInExpression($column, $values) + { + if ( ! is_array($values)) { + $values = array($values); + } + $values = $this->getIdentifiers($values); + + if (count($values) == 0) { + throw \InvalidArgumentException('Values must not be empty.'); + } + return $column . ' IN (' . implode(', ', $values) . ')'; + } + + /** + * Returns SQL that checks if a expression is null. + * + * @param string $expression the expression that should be compared to null + * @return string logical expression + */ + public function getIsNullExpression($expression) + { + return $expression . ' IS NULL'; + } + + /** + * Returns SQL that checks if a expression is not null. + * + * @param string $expression the expression that should be compared to null + * @return string logical expression + */ + public function getIsNotNullExpression($expression) + { + return $expression . ' IS NOT NULL'; + } + + /** + * Returns SQL that checks if an expression evaluates to a value between + * two values. + * + * The parameter $expression is checked if it is between $value1 and $value2. + * + * Note: There is a slight difference in the way BETWEEN works on some databases. + * http://www.w3schools.com/sql/sql_between.asp. If you want complete database + * independence you should avoid using between(). + * + * @param string $expression the value to compare to + * @param string $value1 the lower value to compare with + * @param string $value2 the higher value to compare with + * @return string logical expression + */ + public function getBetweenExpression($expression, $value1, $value2) + { + return $expression . ' BETWEEN ' .$value1 . ' AND ' . $value2; + } + + public function getAcosExpression($value) + { + return 'ACOS(' . $value . ')'; + } + + public function getSinExpression($value) + { + return 'SIN(' . $value . ')'; + } + + public function getPiExpression() + { + return 'PI()'; + } + + public function getCosExpression($value) + { + return 'COS(' . $value . ')'; + } + + public function getForUpdateSql() + { + return 'FOR UPDATE'; + } + + public function getDropDatabaseSql($database) + { + return 'DROP DATABASE ' . $database; + } + + /** + * Drop a Table + * + * @param Table|string $table + * @return string + */ + public function getDropTableSql($table) + { + if ($table instanceof \Doctrine\DBAL\Schema\Table) { + $table = $table->getName(); + } + + return 'DROP TABLE ' . $table; + } + + /** + * Drop index from a table + * + * @param Index|string $name + * @param string|Table $table + * @return string + */ + public function getDropIndexSql($index, $table=null) + { + if($index instanceof \Doctrine\DBAL\Schema\Index) { + $index = $index->getName(); + } else if(!is_string($index)) { + throw new \InvalidArgumentException('AbstractPlatform::getDropIndexSql() expects $index parameter to be string or \Doctrine\DBAL\Schema\Index.'); + } + + return 'DROP INDEX ' . $index; + } + + /** + * Get drop constraint sql + * + * @param \Doctrine\DBAL\Schema\Constraint $constraint + * @param string|Table $table + * @return string + */ + public function getDropConstraintSql($constraint, $table) + { + if ($constraint instanceof \Doctrine\DBAL\Schema\Constraint) { + $constraint = $constraint->getName(); + } + + if ($table instanceof \Doctrine\DBAL\Schema\Table) { + $table = $table->getName(); + } + + return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $constraint; + } + + /** + * @param ForeignKeyConstraint|string $foreignKey + * @param Table|string $table + * @return string + */ + public function getDropForeignKeySql($foreignKey, $table) + { + if ($foreignKey instanceof \Doctrine\DBAL\Schema\ForeignKeyConstraint) { + $foreignKey = $foreignKey->getName(); + } + + if ($table instanceof \Doctrine\DBAL\Schema\Table) { + $table = $table->getName(); + } + + return 'ALTER TABLE ' . $table . ' DROP FOREIGN KEY ' . $foreignKey; + } + + /** + * Gets the SQL statement(s) to create a table with the specified name, columns and constraints + * on this platform. + * + * @param string $table The name of the table. + * @param int $createFlags + * @return array The sequence of SQL statements. + */ + public function getCreateTableSql(Table $table, $createFlags=self::CREATE_INDEXES) + { + if (!is_int($createFlags)) { + throw new \InvalidArgumentException("Second argument of AbstractPlatform::getCreateTableSql() has to be integer."); + } + + $tableName = $table->getName(); + $options = $table->getOptions(); + $options['uniqueConstraints'] = array(); + $options['indexes'] = array(); + $options['primary'] = array(); + + if (($createFlags&self::CREATE_INDEXES) > 0) { + foreach ($table->getIndexes() AS $index) { + /* @var $index Index */ + if ($index->isPrimary()) { + $options['primary'] = $index->getColumns(); + } else { + $options['indexes'][$index->getName()] = $index; + } + } + } + + $columns = array(); + foreach ($table->getColumns() AS $column) { + /* @var \Doctrine\DBAL\Schema\Column $column */ + $columnData = array(); + $columnData['name'] = $column->getName(); + $columnData['type'] = $column->getType(); + $columnData['length'] = $column->getLength(); + $columnData['notnull'] = $column->getNotNull(); + $columnData['unique'] = ($column->hasPlatformOption("unique"))?$column->getPlatformOption('unique'):false; + $columnData['version'] = ($column->hasPlatformOption("version"))?$column->getPlatformOption('version'):false; + if(strtolower($columnData['type']) == "string" && $columnData['length'] === null) { + $columnData['length'] = 255; + } + $columnData['precision'] = $column->getPrecision(); + $columnData['scale'] = $column->getScale(); + $columnData['default'] = $column->getDefault(); + // TODO: Fixed? Unsigned? + + if(in_array($column->getName(), $options['primary'])) { + $columnData['primary'] = true; + + if($table->isIdGeneratorIdentity()) { + $columnData['autoincrement'] = true; + } + } + + $columns[$columnData['name']] = $columnData; + } + + if (($createFlags&self::CREATE_FOREIGNKEYS) > 0) { + $options['foreignKeys'] = array(); + foreach ($table->getForeignKeys() AS $fkConstraint) { + $options['foreignKeys'][] = $fkConstraint; + } + } + + return $this->_getCreateTableSql($tableName, $columns, $options); + } + + /** + * @param string $tableName + * @param array $columns + * @param array $options + * @return array + */ + protected function _getCreateTableSql($tableName, array $columns, array $options = array()) + { + $columnListSql = $this->getColumnDeclarationListSql($columns); + + if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) { + foreach ($options['uniqueConstraints'] as $name => $definition) { + $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSql($name, $definition); + } + } + + if (isset($options['primary']) && ! empty($options['primary'])) { + $columnListSql .= ', PRIMARY KEY(' . implode(', ', array_unique(array_values($options['primary']))) . ')'; + } + + if (isset($options['indexes']) && ! empty($options['indexes'])) { + foreach($options['indexes'] as $index => $definition) { + $columnListSql .= ', ' . $this->getIndexDeclarationSql($index, $definition); + } + } + + $query = 'CREATE TABLE ' . $tableName . ' (' . $columnListSql; + + $check = $this->getCheckDeclarationSql($columns); + if ( ! empty($check)) { + $query .= ', ' . $check; + } + $query .= ')'; + + $sql[] = $query; + + if (isset($options['foreignKeys'])) { + foreach ((array) $options['foreignKeys'] AS $definition) { + $sql[] = $this->getCreateForeignKeySql($definition, $tableName); + } + } + + return $sql; + } + + public function getCreateTemporaryTableSnippetSql() + { + return "CREATE TEMPORARY TABLE"; + } + + /** + * Gets the SQL to create a sequence on this platform. + * + * @param \Doctrine\DBAL\Schema\Sequence $sequence + * @throws DoctrineException + */ + public function getCreateSequenceSql(\Doctrine\DBAL\Schema\Sequence $sequence) + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * Gets the SQL to create a constraint on a table on this platform. + * + * @param Constraint $constraint + * @param string|Table $table + * @return string + */ + public function getCreateConstraintSql(\Doctrine\DBAL\Schema\Constraint $constraint, $table) + { + if ($table instanceof \Doctrine\DBAL\Schema\Table) { + $table = $table->getName(); + } + + $query = 'ALTER TABLE ' . $table . ' ADD CONSTRAINT ' . $constraint->getName(); + + $columns = array(); + foreach ($constraint->getColumns() as $column) { + $columns[] = $column; + } + $columnList = '('. implode(', ', $columns) . ')'; + + $referencesClause = ''; + if ($constraint instanceof \Doctrine\DBAL\Schema\Index) { + if($constraint->isPrimary()) { + $query .= ' PRIMARY KEY'; + } elseif ($constraint->isUnique()) { + $query .= ' UNIQUE'; + } else { + throw new \InvalidArgumentException( + 'Can only create primary or unique constraints, no common indexes with getCreateConstraintSql().' + ); + } + } else if ($constraint instanceof \Doctrine\DBAL\Schema\ForeignKeyConstraint) { + $query .= ' FOREIGN KEY'; + + $foreignColumns = array(); + foreach ($constraint->getForeignColumns() AS $column) { + $foreignColumns[] = $column; + } + + $referencesClause = ' REFERENCES '.$constraint->getForeignTableName(). ' ('.implode(', ', $foreignColumns).')'; + } + $query .= ' '.$columnList.$referencesClause; + + return $query; + } + + /** + * Gets the SQL to create an index on a table on this platform. + * + * @param Index $index + * @param string|Table $table name of the table on which the index is to be created + * @return string + */ + public function getCreateIndexSql(Index $index, $table) + { + if ($table instanceof Table) { + $table = $table->getName(); + } + $name = $index->getName(); + $columns = $index->getColumns(); + + if (count($columns) == 0) { + throw new \InvalidArgumentException("Incomplete definition. 'columns' required."); + } + + $type = ''; + if ($index->isUnique()) { + $type = 'UNIQUE '; + } + + $query = 'CREATE ' . $type . 'INDEX ' . $name . ' ON ' . $table; + + $query .= ' (' . $this->getIndexFieldDeclarationListSql($columns) . ')'; + + return $query; + } + + /** + * Quotes a string so that it can be safely used as a table or column name, + * even if it is a reserved word of the platform. + * + * NOTE: Just because you CAN use quoted identifiers doesn't mean + * you SHOULD use them. In general, they end up causing way more + * problems than they solve. + * + * @param string $str identifier name to be quoted + * @return string quoted identifier string + */ + public function quoteIdentifier($str) + { + $c = $this->getIdentifierQuoteCharacter(); + + return $c . $str . $c; + } + + /** + * Create a new foreign key + * + * @param ForeignKeyConstraint $foreignKey ForeignKey instance + * @param string|Table $table name of the table on which the foreign key is to be created + * @return string + */ + public function getCreateForeignKeySql(ForeignKeyConstraint $foreignKey, $table) + { + if ($table instanceof \Doctrine\DBAL\Schema\Table) { + $table = $table->getName(); + } + + $query = 'ALTER TABLE ' . $table . ' ADD ' . $this->getForeignKeyDeclarationSql($foreignKey); + + return $query; + } + + /** + * Gets the sql statements for altering an existing table. + * + * The method returns an array of sql statements, since some platforms need several statements. + * + * @param TableDiff $diff + * @return array + */ + public function getAlterTableSql(TableDiff $diff) + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * Common code for alter table statement generation that updates the changed Index and Foreign Key definitions. + * + * @param TableDiff $diff + * @return array + */ + protected function _getAlterTableIndexForeignKeySql(TableDiff $diff) + { + if ($diff->newName !== false) { + $tableName = $diff->newName; + } else { + $tableName = $diff->name; + } + + $sql = array(); + if ($this->supportsForeignKeyConstraints()) { + foreach ($diff->addedForeignKeys AS $foreignKey) { + $sql[] = $this->getCreateForeignKeySql($foreignKey, $tableName); + } + foreach ($diff->removedForeignKeys AS $foreignKey) { + $sql[] = $this->getDropForeignKeySql($foreignKey, $tableName); + } + foreach ($diff->changedForeignKeys AS $foreignKey) { + $sql[] = $this->getDropForeignKeySql($foreignKey, $tableName); + $sql[] = $this->getCreateForeignKeySql($foreignKey, $tableName); + } + } + + foreach ($diff->addedIndexes AS $index) { + $sql[] = $this->getCreateIndexSql($index, $tableName); + } + foreach ($diff->removedIndexes AS $index) { + $sql[] = $this->getDropIndexSql($index, $tableName); + } + foreach ($diff->changedIndexes AS $index) { + $sql[] = $this->getDropIndexSql($index, $tableName); + $sql[] = $this->getCreateIndexSql($index, $tableName); + } + + return $sql; + } + + /** + * Get declaration of a number of fields in bulk + * + * @param array $fields a multidimensional associative array. + * The first dimension determines the field name, while the second + * dimension is keyed with the name of the properties + * of the field being declared as array indexes. Currently, the types + * of supported field properties are as follows: + * + * length + * Integer value that determines the maximum length of the text + * field. If this argument is missing the field should be + * declared to have the longest length allowed by the DBMS. + * + * default + * Text value to be used as default for this field. + * + * notnull + * Boolean flag that indicates whether this field is constrained + * to not be set to null. + * charset + * Text value with the default CHARACTER SET for this field. + * collation + * Text value with the default COLLATION for this field. + * unique + * unique constraint + * + * @return string + */ + public function getColumnDeclarationListSql(array $fields) + { + $queryFields = array(); + foreach ($fields as $fieldName => $field) { + $query = $this->getColumnDeclarationSql($fieldName, $field); + $queryFields[] = $query; + } + return implode(', ', $queryFields); + } + + /** + * Obtain DBMS specific SQL code portion needed to declare a generic type + * field to be used in statements like CREATE TABLE. + * + * @param string $name name the field to be declared. + * @param array $field associative array with the name of the properties + * of the field being declared as array indexes. Currently, the types + * of supported field properties are as follows: + * + * length + * Integer value that determines the maximum length of the text + * field. If this argument is missing the field should be + * declared to have the longest length allowed by the DBMS. + * + * default + * Text value to be used as default for this field. + * + * notnull + * Boolean flag that indicates whether this field is constrained + * to not be set to null. + * charset + * Text value with the default CHARACTER SET for this field. + * collation + * Text value with the default COLLATION for this field. + * unique + * unique constraint + * check + * column check constraint + * + * @return string DBMS specific SQL code portion that should be used to declare the column. + */ + public function getColumnDeclarationSql($name, array $field) + { + $default = $this->getDefaultValueDeclarationSql($field); + + $charset = (isset($field['charset']) && $field['charset']) ? + ' ' . $this->getColumnCharsetDeclarationSql($field['charset']) : ''; + + $collation = (isset($field['collation']) && $field['collation']) ? + ' ' . $this->getColumnCollationDeclarationSql($field['collation']) : ''; + + $notnull = (isset($field['notnull']) && $field['notnull']) ? ' NOT NULL' : ''; + + $unique = (isset($field['unique']) && $field['unique']) ? + ' ' . $this->getUniqueFieldDeclarationSql() : ''; + + $check = (isset($field['check']) && $field['check']) ? + ' ' . $field['check'] : ''; + + $typeDecl = $field['type']->getSqlDeclaration($field, $this); + + return $name . ' ' . $typeDecl . $charset . $default . $notnull . $unique . $check . $collation; + } + + /** + * Gets the SQL snippet that declares a floating point column of arbitrary precision. + * + * @param array $columnDef + * @return string + */ + public function getDecimalTypeDeclarationSql(array $columnDef) + { + $columnDef['precision'] = ( ! isset($columnDef['precision']) || empty($columnDef['precision'])) + ? 10 : $columnDef['precision']; + $columnDef['scale'] = ( ! isset($columnDef['scale']) || empty($columnDef['scale'])) + ? 0 : $columnDef['scale']; + + return 'NUMERIC(' . $columnDef['precision'] . ', ' . $columnDef['scale'] . ')'; + } + + /** + * Gets the SQL snippet that declares a boolean column. + * + * @param array $columnDef + * @return string + */ + abstract public function getBooleanTypeDeclarationSql(array $columnDef); + + /** + * Gets the SQL snippet that declares a 4 byte integer column. + * + * @param array $columnDef + * @return string + */ + abstract public function getIntegerTypeDeclarationSql(array $columnDef); + + /** + * Gets the SQL snippet that declares an 8 byte integer column. + * + * @param array $columnDef + * @return string + */ + abstract public function getBigIntTypeDeclarationSql(array $columnDef); + + /** + * Gets the SQL snippet that declares a 2 byte integer column. + * + * @param array $columnDef + * @return string + */ + abstract public function getSmallIntTypeDeclarationSql(array $columnDef); + + /** + * Gets the SQL snippet that declares common properties of an integer column. + * + * @param array $columnDef + * @return string + */ + abstract protected function _getCommonIntegerTypeDeclarationSql(array $columnDef); + + /** + * Obtain DBMS specific SQL code portion needed to set a default value + * declaration to be used in statements like CREATE TABLE. + * + * @param array $field field definition array + * @return string DBMS specific SQL code portion needed to set a default value + */ + public function getDefaultValueDeclarationSql($field) + { + $default = empty($field['notnull']) ? ' DEFAULT NULL' : ''; + + if (isset($field['default'])) { + $default = " DEFAULT '".$field['default']."'"; + if (isset($field['type'])) { + if (in_array((string)$field['type'], array("Integer", "BigInteger", "SmallInteger"))) { + $default = " DEFAULT ".$field['default']; + } else if ((string)$field['type'] == 'DateTime' && $field['default'] == $this->getCurrentTimestampSql()) { + $default = " DEFAULT ".$this->getCurrentTimestampSql(); + } + } + } + return $default; + } + + /** + * Obtain DBMS specific SQL code portion needed to set a CHECK constraint + * declaration to be used in statements like CREATE TABLE. + * + * @param array $definition check definition + * @return string DBMS specific SQL code portion needed to set a CHECK constraint + */ + public function getCheckDeclarationSql(array $definition) + { + $constraints = array(); + foreach ($definition as $field => $def) { + if (is_string($def)) { + $constraints[] = 'CHECK (' . $def . ')'; + } else { + if (isset($def['min'])) { + $constraints[] = 'CHECK (' . $field . ' >= ' . $def['min'] . ')'; + } + + if (isset($def['max'])) { + $constraints[] = 'CHECK (' . $field . ' <= ' . $def['max'] . ')'; + } + } + } + + return implode(', ', $constraints); + } + + /** + * Obtain DBMS specific SQL code portion needed to set a unique + * constraint declaration to be used in statements like CREATE TABLE. + * + * @param string $name name of the unique constraint + * @param Index $index index definition + * @return string DBMS specific SQL code portion needed + * to set a constraint + */ + public function getUniqueConstraintDeclarationSql($name, Index $index) + { + if (count($index->getColumns()) == 0) { + throw \InvalidArgumentException("Incomplete definition. 'columns' required."); + } + + return 'CONSTRAINT' . $name . ' UNIQUE (' + . $this->getIndexFieldDeclarationListSql($index->getColumns()) + . ')'; + } + + /** + * Obtain DBMS specific SQL code portion needed to set an index + * declaration to be used in statements like CREATE TABLE. + * + * @param string $name name of the index + * @param Index $index index definition + * @return string DBMS specific SQL code portion needed to set an index + */ + public function getIndexDeclarationSql($name, Index $index) + { + $type = ''; + + if($index->isUnique()) { + $type = 'UNIQUE '; + } + + if (count($index->getColumns()) == 0) { + throw \InvalidArgumentException("Incomplete definition. 'columns' required."); + } + + return $type . 'INDEX ' . $name . ' (' + . $this->getIndexFieldDeclarationListSql($index->getColumns()) + . ')'; + } + + /** + * getIndexFieldDeclarationList + * Obtain DBMS specific SQL code portion needed to set an index + * declaration to be used in statements like CREATE TABLE. + * + * @return string + */ + public function getIndexFieldDeclarationListSql(array $fields) + { + $ret = array(); + foreach ($fields as $field => $definition) { + if (is_array($definition)) { + $ret[] = $field; + } else { + $ret[] = $definition; + } + } + return implode(', ', $ret); + } + + /** + * A method to return the required SQL string that fits between CREATE ... TABLE + * to create the table as a temporary table. + * + * Should be overridden in driver classes to return the correct string for the + * specific database type. + * + * The default is to return the string "TEMPORARY" - this will result in a + * SQL error for any database that does not support temporary tables, or that + * requires a different SQL command from "CREATE TEMPORARY TABLE". + * + * @return string The string required to be placed between "CREATE" and "TABLE" + * to generate a temporary table, if possible. + */ + public function getTemporaryTableSql() + { + return 'TEMPORARY'; + } + + /** + * Get sql query to show a list of database + * + * @return unknown + */ + public function getShowDatabasesSql() + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * getForeignKeyDeclaration + * Obtain DBMS specific SQL code portion needed to set the FOREIGN KEY constraint + * of a field declaration to be used in statements like CREATE TABLE. + * + * @param array $definition an associative array with the following structure: + * name optional constraint name + * + * local the local field(s) + * + * foreign the foreign reference field(s) + * + * foreignTable the name of the foreign table + * + * onDelete referential delete action + * + * onUpdate referential update action + * + * deferred deferred constraint checking + * + * The onDelete and onUpdate keys accept the following values: + * + * CASCADE: Delete or update the row from the parent table and automatically delete or + * update the matching rows in the child table. Both ON DELETE CASCADE and ON UPDATE CASCADE are supported. + * Between two tables, you should not define several ON UPDATE CASCADE clauses that act on the same column + * in the parent table or in the child table. + * + * SET NULL: Delete or update the row from the parent table and set the foreign key column or columns in the + * child table to NULL. This is valid only if the foreign key columns do not have the NOT NULL qualifier + * specified. Both ON DELETE SET NULL and ON UPDATE SET NULL clauses are supported. + * + * NO ACTION: In standard SQL, NO ACTION means no action in the sense that an attempt to delete or update a primary + * key value is not allowed to proceed if there is a related foreign key value in the referenced table. + * + * RESTRICT: Rejects the delete or update operation for the parent table. NO ACTION and RESTRICT are the same as + * omitting the ON DELETE or ON UPDATE clause. + * + * SET DEFAULT + * + * @return string DBMS specific SQL code portion needed to set the FOREIGN KEY constraint + * of a field declaration. + */ + public function getForeignKeyDeclarationSql(ForeignKeyConstraint $foreignKey) + { + $sql = $this->getForeignKeyBaseDeclarationSql($foreignKey); + $sql .= $this->getAdvancedForeignKeyOptionsSql($foreignKey); + + return $sql; + } + + /** + * Return the FOREIGN KEY query section dealing with non-standard options + * as MATCH, INITIALLY DEFERRED, ON UPDATE, ... + * + * @param ForeignKeyConstraint $foreignKey foreign key definition + * @return string + */ + public function getAdvancedForeignKeyOptionsSql(ForeignKeyConstraint $foreignKey) + { + $query = ''; + if ($this->supportsForeignKeyOnUpdate() && $foreignKey->hasOption('onUpdate')) { + $query .= ' ON UPDATE ' . $this->getForeignKeyReferentialActionSql($foreignKey->getOption('onUpdate')); + } + if ($foreignKey->hasOption('onDelete')) { + $query .= ' ON DELETE ' . $this->getForeignKeyReferentialActionSql($foreignKey->getOption('onDelete')); + } + return $query; + } + + /** + * returns given referential action in uppercase if valid, otherwise throws + * an exception + * + * @throws Doctrine_Exception_Exception if unknown referential action given + * @param string $action foreign key referential action + * @param string foreign key referential action in uppercase + */ + public function getForeignKeyReferentialActionSql($action) + { + $upper = strtoupper($action); + switch ($upper) { + case 'CASCADE': + case 'SET NULL': + case 'NO ACTION': + case 'RESTRICT': + case 'SET DEFAULT': + return $upper; + break; + default: + throw \InvalidArgumentException('Invalid foreign key action: ' . $upper); + } + } + + /** + * Obtain DBMS specific SQL code portion needed to set the FOREIGN KEY constraint + * of a field declaration to be used in statements like CREATE TABLE. + * + * @param ForeignKeyConstraint $foreignKey + * @return string + */ + public function getForeignKeyBaseDeclarationSql(ForeignKeyConstraint $foreignKey) + { + $sql = ''; + if (strlen($foreignKey->getName())) { + $sql .= 'CONSTRAINT ' . $foreignKey->getName() . ' '; + } + $sql .= 'FOREIGN KEY ('; + + if (count($foreignKey->getLocalColumns()) == 0) { + throw new \InvalidArgumentException("Incomplete definition. 'local' required."); + } + if (count($foreignKey->getForeignColumns()) == 0) { + throw new \InvalidArgumentException("Incomplete definition. 'foreign' required."); + } + if (strlen($foreignKey->getForeignTableName()) == 0) { + throw new \InvalidArgumentException("Incomplete definition. 'foreignTable' required."); + } + + $sql .= implode(', ', $foreignKey->getLocalColumns()) + . ') REFERENCES ' + . $foreignKey->getForeignTableName() . '(' + . implode(', ', $foreignKey->getForeignColumns()) . ')'; + + return $sql; + } + + /** + * Obtain DBMS specific SQL code portion needed to set the UNIQUE constraint + * of a field declaration to be used in statements like CREATE TABLE. + * + * @return string DBMS specific SQL code portion needed to set the UNIQUE constraint + * of a field declaration. + */ + public function getUniqueFieldDeclarationSql() + { + return 'UNIQUE'; + } + + /** + * Obtain DBMS specific SQL code portion needed to set the CHARACTER SET + * of a field declaration to be used in statements like CREATE TABLE. + * + * @param string $charset name of the charset + * @return string DBMS specific SQL code portion needed to set the CHARACTER SET + * of a field declaration. + */ + public function getColumnCharsetDeclarationSql($charset) + { + return ''; + } + + /** + * Obtain DBMS specific SQL code portion needed to set the COLLATION + * of a field declaration to be used in statements like CREATE TABLE. + * + * @param string $collation name of the collation + * @return string DBMS specific SQL code portion needed to set the COLLATION + * of a field declaration. + */ + public function getColumnCollationDeclarationSql($collation) + { + return ''; + } + + /** + * build a pattern matching string + * + * EXPERIMENTAL + * + * WARNING: this function is experimental and may change signature at + * any time until labelled as non-experimental + * + * @access public + * + * @param array $pattern even keys are strings, odd are patterns (% and _) + * @param string $operator optional pattern operator (LIKE, ILIKE and maybe others in the future) + * @param string $field optional field name that is being matched against + * (might be required when emulating ILIKE) + * + * @return string SQL pattern + */ + public function getMatchPatternExpression($pattern, $operator = null, $field = null) + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * Whether the platform prefers sequences for ID generation. + * Subclasses should override this method to return TRUE if they prefer sequences. + * + * @return boolean + */ + public function prefersSequences() + { + return false; + } + + /** + * Whether the platform prefers identity columns (eg. autoincrement) for ID generation. + * Subclasses should override this method to return TRUE if they prefer identity columns. + * + * @return boolean + */ + public function prefersIdentityColumns() + { + return false; + } + + /** + * Some platforms need the boolean values to be converted. + * + * The default conversion in this implementation converts to integers (false => 0, true => 1). + * + * @param mixed $item + */ + public function convertBooleans($item) + { + if (is_array($item)) { + foreach ($item as $k => $value) { + if (is_bool($value)) { + $item[$k] = (int) $value; + } + } + } else if (is_bool($item)) { + $item = (int) $item; + } + return $item; + } + + /** + * Gets the SQL statement specific for the platform to set the charset. + * + * This function is MySQL specific and required by + * {@see \Doctrine\DBAL\Connection::setCharset($charset)} + * + * @param string $charset + * @return string + */ + public function getSetCharsetSql($charset) + { + return "SET NAMES '".$charset."'"; + } + + /** + * Gets the SQL specific for the platform to get the current date. + * + * @return string + */ + public function getCurrentDateSql() + { + return 'CURRENT_DATE'; + } + + /** + * Gets the SQL specific for the platform to get the current time. + * + * @return string + */ + public function getCurrentTimeSql() + { + return 'CURRENT_TIME'; + } + + /** + * Gets the SQL specific for the platform to get the current timestamp + * + * @return string + */ + public function getCurrentTimestampSql() + { + return 'CURRENT_TIMESTAMP'; + } + + /** + * Get sql for transaction isolation level Connection constant + * + * @param integer $level + */ + protected function _getTransactionIsolationLevelSql($level) + { + switch ($level) { + case Connection::TRANSACTION_READ_UNCOMMITTED: + return 'READ UNCOMMITTED'; + case Connection::TRANSACTION_READ_COMMITTED: + return 'READ COMMITTED'; + case Connection::TRANSACTION_REPEATABLE_READ: + return 'REPEATABLE READ'; + case Connection::TRANSACTION_SERIALIZABLE: + return 'SERIALIZABLE'; + default: + throw new \InvalidArgumentException('Invalid isolation level:' . $level); + } + } + + public function getListDatabasesSql() + { + throw DBALException::notSupported(__METHOD__); + } + + public function getListFunctionsSql() + { + throw DBALException::notSupported(__METHOD__); + } + + public function getListTriggersSql($table = null) + { + throw DBALException::notSupported(__METHOD__); + } + + public function getListSequencesSql($database) + { + throw DBALException::notSupported(__METHOD__); + } + + public function getListTableConstraintsSql($table) + { + throw DBALException::notSupported(__METHOD__); + } + + public function getListTableColumnsSql($table) + { + throw DBALException::notSupported(__METHOD__); + } + + public function getListTablesSql() + { + throw DBALException::notSupported(__METHOD__); + } + + public function getListUsersSql() + { + throw DBALException::notSupported(__METHOD__); + } + + public function getListViewsSql() + { + throw DBALException::notSupported(__METHOD__); + } + + public function getListTableIndexesSql($table) + { + throw DBALException::notSupported(__METHOD__); + } + + public function getListTableForeignKeysSql($table) + { + throw DBALException::notSupported(__METHOD__); + } + + public function getCreateViewSql($name, $sql) + { + throw DBALException::notSupported(__METHOD__); + } + + public function getDropViewSql($name) + { + throw DBALException::notSupported(__METHOD__); + } + + public function getDropSequenceSql($sequence) + { + throw DBALException::notSupported(__METHOD__); + } + + public function getSequenceNextValSql($sequenceName) + { + throw DBALException::notSupported(__METHOD__); + } + + public function getCreateDatabaseSql($database) + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * Get sql to set the transaction isolation level + * + * @param integer $level + */ + public function getSetTransactionIsolationSql($level) + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * Obtain DBMS specific SQL code portion needed to set the CHARACTER SET + * of a field declaration to be used in statements like CREATE TABLE. + * + * @param string $charset name of the charset + * @return string DBMS specific SQL code portion needed to set the CHARACTER SET + * of a field declaration. + */ + public function getCharsetFieldDeclaration($charset) + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * Obtain DBMS specific SQL to be used to create datetime fields in + * statements like CREATE TABLE + * + * @param array $fieldDeclaration + * @return string + */ + public function getDateTimeTypeDeclarationSql(array $fieldDeclaration) + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * Obtain DBMS specific SQL to be used to create date fields in statements + * like CREATE TABLE. + * + * @param array $fieldDeclaration + * @return string + */ + public function getDateTypeDeclarationSql(array $fieldDeclaration) + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * Obtain DBMS specific SQL to be used to create time fields in statements + * like CREATE TABLE. + * + * @param array $fieldDeclaration + * @return string + */ + public function getTimeTypeDeclarationSql(array $fieldDeclaration) + { + throw DoctrineException::getTimeTypeDeclarationNotSupported($this); + } + + /** + * Gets the default transaction isolation level of the platform. + * + * @return integer The default isolation level. + * @see Doctrine\DBAL\Connection\TRANSACTION_* constants. + */ + public function getDefaultTransactionIsolationLevel() + { + return Connection::TRANSACTION_READ_COMMITTED; + } + + /* supports*() metods */ + + /** + * Whether the platform supports sequences. + * + * @return boolean + */ + public function supportsSequences() + { + return false; + } + + /** + * Whether the platform supports identity columns. + * Identity columns are columns that recieve an auto-generated value from the + * database on insert of a row. + * + * @return boolean + */ + public function supportsIdentityColumns() + { + return false; + } + + /** + * Whether the platform supports indexes. + * + * @return boolean + */ + public function supportsIndexes() + { + return true; + } + + public function supportsAlterTable() + { + return true; + } + + /** + * Whether the platform supports transactions. + * + * @return boolean + */ + public function supportsTransactions() + { + return true; + } + + /** + * Whether the platform supports savepoints. + * + * @return boolean + */ + public function supportsSavepoints() + { + return true; + } + + /** + * Whether the platform supports primary key constraints. + * + * @return boolean + */ + public function supportsPrimaryConstraints() + { + return true; + } + + /** + * Does the platform supports foreign key constraints? + * + * @return boolean + */ + public function supportsForeignKeyConstraints() + { + return true; + } + + /** + * Does this platform supports onUpdate in foreign key constraints? + * + * @return bool + */ + public function supportsForeignKeyOnUpdate() + { + return ($this->supportsForeignKeyConstraints() && true); + } + + /** + * Whether the platform supports database schemas. + * + * @return boolean + */ + public function supportsSchemas() + { + return false; + } + + /** + * @return bool + */ + public function createsExplicitIndexForForeignKeys() + { + return false; + } + + /** + * Whether the platform supports getting the affected rows of a recent + * update/delete type query. + * + * @return boolean + */ + public function supportsGettingAffectedRows() + { + return true; + } + + public function getIdentityColumnNullInsertSql() + { + return ""; + } + + /** + * Gets the format string, as accepted by the date() function, that describes + * the format of a stored datetime value of this platform. + * + * @return string The format string. + * + * @todo We need to get the specific format for each dbms and override this + * function for each platform + */ + public function getDateTimeFormatString() + { + return 'Y-m-d H:i:s'; + } + + /** + * Gets the format string, as accepted by the date() function, that describes + * the format of a stored date value of this platform. + * + * @return string The format string. + */ + public function getDateFormatString() + { + return 'Y-m-d'; + } + + /** + * Gets the format string, as accepted by the date() function, that describes + * the format of a stored time value of this platform. + * + * @return string The format string. + */ + public function getTimeFormatString() + { + return 'H:i:s'; + } + + public function modifyLimitQuery($query, $limit, $offset = null) + { + if ( ! is_null($limit)) { + $query .= ' LIMIT ' . $limit; + } + + if ( ! is_null($offset)) { + $query .= ' OFFSET ' . $offset; + } + + return $query; + } + + /** + * Gets the SQL snippet used to declare a VARCHAR column type. + * + * @param array $field + */ + abstract public function getVarcharTypeDeclarationSql(array $field); + + /** + * Gets the SQL snippet used to declare a CLOB column type. + * + * @param array $field + */ + abstract public function getClobTypeDeclarationSql(array $field); + + /** + * Gets the name of the platform. + * + * @return string + */ + abstract public function getName(); + + /** + * Gets the character casing of a column in an SQL result set of this platform. + * + * @param string $column The column name for which to get the correct character casing. + * @return string The column name in the character casing used in SQL result sets. + */ + public function getSqlResultCasing($column) + { + return $column; + } + + /** + * Makes any fixes to a name of a schema element (table, sequence, ...) that are required + * by restrictions of the platform, like a maximum length. + * + * @param string $schemaName + * @return string + */ + public function fixSchemaElementName($schemaElementName) + { + return $schemaElementName; + } + + /** + * Maximum length of any given databse identifier, like tables or column names. + * + * @return int + */ + public function getMaxIdentifierLength() + { + return 63; + } + + /** + * Get the insert sql for an empty insert statement + * + * @param string $tableName + * @param string $identifierColumnName + * @return string $sql + */ + public function getEmptyIdentityInsertSql($tableName, $identifierColumnName) + { + return 'INSERT INTO ' . $tableName . ' (' . $identifierColumnName . ') VALUES (null)'; + } +} diff --git a/lib/Doctrine/DBAL/Schema/Column.php b/lib/Doctrine/DBAL/Schema/Column.php index 23d44e9ba..3b446880e 100644 --- a/lib/Doctrine/DBAL/Schema/Column.php +++ b/lib/Doctrine/DBAL/Schema/Column.php @@ -80,6 +80,11 @@ class Column extends AbstractAsset */ protected $_platformOptions = array(); + /** + * @var string + */ + protected $_columnDefinition = null; + /** * Create a new Column * @@ -226,6 +231,17 @@ class Column extends AbstractAsset return $this; } + /** + * + * @param string + * @return Column + */ + public function setColumnDefinition($value) + { + $this->_columnDefinition = $value; + return $this; + } + public function getType() { return $this->_type; @@ -281,6 +297,11 @@ class Column extends AbstractAsset return $this->_platformOptions[$name]; } + public function getColumnDefinition() + { + return $this->_columnDefinition; + } + /** * @param Visitor $visitor */ @@ -304,6 +325,7 @@ class Column extends AbstractAsset 'scale' => $this->_scale, 'fixed' => $this->_fixed, 'unsigned' => $this->_unsigned, + 'columnDefinition' => $this->_columnDefinition, ), $this->_platformOptions); } } \ No newline at end of file diff --git a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php index 249ffa2f0..410c23344 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php @@ -200,6 +200,10 @@ class AnnotationDriver implements Driver $mapping['columnName'] = $columnAnnot->name; } + if (isset($columnAnnot->columnDefinition)) { + $mapping['columnDefinition'] = $columnAnnot->columnDefinition; + } + if ($idAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\Id')) { $mapping['id'] = true; } diff --git a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php.orig b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php.orig new file mode 100644 index 000000000..249ffa2f0 --- /dev/null +++ b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php.orig @@ -0,0 +1,374 @@ +. + */ + +namespace Doctrine\ORM\Mapping\Driver; + +use Doctrine\Common\DoctrineException, + Doctrine\Common\Cache\ArrayCache, + Doctrine\Common\Annotations\AnnotationReader, + Doctrine\ORM\Mapping\ClassMetadataInfo, + Doctrine\ORM\Mapping\MappingException; + +require __DIR__ . '/DoctrineAnnotations.php'; + +/** + * The AnnotationDriver reads the mapping metadata from docblock annotations. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class AnnotationDriver implements Driver +{ + /** The AnnotationReader. */ + private $_reader; + private $_classDirectory; + + /** + * Initializes a new AnnotationDriver that uses the given AnnotationReader for reading + * docblock annotations. + * + * @param AnnotationReader $reader The AnnotationReader to use. + */ + public function __construct(AnnotationReader $reader, $classDirectory = null) + { + $this->_reader = $reader; + $this->_classDirectory = $classDirectory; + } + + public function setClassDirectory($classDirectory) + { + $this->_classDirectory = $classDirectory; + } + + /** + * {@inheritdoc} + */ + public function loadMetadataForClass($className, ClassMetadataInfo $metadata) + { + $class = $metadata->getReflectionClass(); + + $classAnnotations = $this->_reader->getClassAnnotations($class); + + // Evaluate Entity annotation + if (isset($classAnnotations['Doctrine\ORM\Mapping\Entity'])) { + $entityAnnot = $classAnnotations['Doctrine\ORM\Mapping\Entity']; + $metadata->setCustomRepositoryClass($entityAnnot->repositoryClass); + } else if (isset($classAnnotations['Doctrine\ORM\Mapping\MappedSuperclass'])) { + $metadata->isMappedSuperclass = true; + } else { + throw DoctrineException::classIsNotAValidEntityOrMappedSuperClass($className); + } + + // Evaluate DoctrineTable annotation + if (isset($classAnnotations['Doctrine\ORM\Mapping\Table'])) { + $tableAnnot = $classAnnotations['Doctrine\ORM\Mapping\Table']; + $primaryTable = array( + 'name' => $tableAnnot->name, + 'schema' => $tableAnnot->schema + ); + + if ($tableAnnot->indexes !== null) { + foreach ($tableAnnot->indexes as $indexAnnot) { + $primaryTable['indexes'][$indexAnnot->name] = array( + 'columns' => $indexAnnot->columns + ); + } + } + + if ($tableAnnot->uniqueConstraints !== null) { + foreach ($tableAnnot->uniqueConstraints as $uniqueConstraint) { + $primaryTable['uniqueConstraints'][$uniqueConstraint->name] = array( + 'columns' => $uniqueConstraint->columns + ); + } + } + + $metadata->setPrimaryTable($primaryTable); + } + + // Evaluate InheritanceType annotation + if (isset($classAnnotations['Doctrine\ORM\Mapping\InheritanceType'])) { + $inheritanceTypeAnnot = $classAnnotations['Doctrine\ORM\Mapping\InheritanceType']; + $metadata->setInheritanceType(constant('\Doctrine\ORM\Mapping\ClassMetadata::INHERITANCE_TYPE_' . $inheritanceTypeAnnot->value)); + } + + // Evaluate DiscriminatorColumn annotation + if (isset($classAnnotations['Doctrine\ORM\Mapping\DiscriminatorColumn'])) { + $discrColumnAnnot = $classAnnotations['Doctrine\ORM\Mapping\DiscriminatorColumn']; + $metadata->setDiscriminatorColumn(array( + 'name' => $discrColumnAnnot->name, + 'type' => $discrColumnAnnot->type, + 'length' => $discrColumnAnnot->length + )); + } + + // Evaluate DiscriminatorMap annotation + if (isset($classAnnotations['Doctrine\ORM\Mapping\DiscriminatorMap'])) { + $discrMapAnnot = $classAnnotations['Doctrine\ORM\Mapping\DiscriminatorMap']; + $metadata->setDiscriminatorMap($discrMapAnnot->value); + } + + // Evaluate DoctrineChangeTrackingPolicy annotation + if (isset($classAnnotations['Doctrine\ORM\Mapping\ChangeTrackingPolicy'])) { + $changeTrackingAnnot = $classAnnotations['Doctrine\ORM\Mapping\ChangeTrackingPolicy']; + $metadata->setChangeTrackingPolicy($changeTrackingAnnot->value); + } + + // Evaluate annotations on properties/fields + foreach ($class->getProperties() as $property) { + if ($metadata->isMappedSuperclass && ! $property->isPrivate() + || + $metadata->isInheritedField($property->name) + || + $metadata->isInheritedAssociation($property->name)) { + continue; + } + + $mapping = array(); + $mapping['fieldName'] = $property->getName(); + + // Check for JoinColummn/JoinColumns annotations + $joinColumns = array(); + + if ($joinColumnAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\JoinColumn')) { + $joinColumns[] = array( + 'name' => $joinColumnAnnot->name, + 'referencedColumnName' => $joinColumnAnnot->referencedColumnName, + 'unique' => $joinColumnAnnot->unique, + 'nullable' => $joinColumnAnnot->nullable, + 'onDelete' => $joinColumnAnnot->onDelete, + 'onUpdate' => $joinColumnAnnot->onUpdate + ); + } else if ($joinColumnsAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\JoinColumns')) { + foreach ($joinColumnsAnnot->value as $joinColumn) { + $joinColumns[] = array( + 'name' => $joinColumn->name, + 'referencedColumnName' => $joinColumn->referencedColumnName, + 'unique' => $joinColumn->unique, + 'nullable' => $joinColumn->nullable, + 'onDelete' => $joinColumn->onDelete, + 'onUpdate' => $joinColumn->onUpdate + ); + } + } + + // Field can only be annotated with one of: + // @Column, @OneToOne, @OneToMany, @ManyToOne, @ManyToMany + if ($columnAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\Column')) { + if ($columnAnnot->type == null) { + throw DoctrineException::propertyTypeIsRequired($property->getName()); + } + + $mapping['type'] = $columnAnnot->type; + $mapping['length'] = $columnAnnot->length; + $mapping['precision'] = $columnAnnot->precision; + $mapping['scale'] = $columnAnnot->scale; + $mapping['nullable'] = $columnAnnot->nullable; + $mapping['unique'] = $columnAnnot->unique; + if ($columnAnnot->options) { + $mapping['options'] = $columnAnnot->options; + } + + if (isset($columnAnnot->default)) { + $mapping['default'] = $columnAnnot->default; + } + + if (isset($columnAnnot->name)) { + $mapping['columnName'] = $columnAnnot->name; + } + + if ($idAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\Id')) { + $mapping['id'] = true; + } + + if ($generatedValueAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\GeneratedValue')) { + $metadata->setIdGeneratorType(constant('Doctrine\ORM\Mapping\ClassMetadata::GENERATOR_TYPE_' . $generatedValueAnnot->strategy)); + } + + if ($versionAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\Version')) { + $metadata->setVersionMapping($mapping); + } + + $metadata->mapField($mapping); + + // Check for SequenceGenerator/TableGenerator definition + if ($seqGeneratorAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\SequenceGenerator')) { + $metadata->setSequenceGeneratorDefinition(array( + 'sequenceName' => $seqGeneratorAnnot->sequenceName, + 'allocationSize' => $seqGeneratorAnnot->allocationSize, + 'initialValue' => $seqGeneratorAnnot->initialValue + )); + } else if ($tblGeneratorAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\TableGenerator')) { + throw DoctrineException::tableIdGeneratorNotImplemented(); + } + } else if ($oneToOneAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\OneToOne')) { + $mapping['targetEntity'] = $oneToOneAnnot->targetEntity; + $mapping['joinColumns'] = $joinColumns; + $mapping['mappedBy'] = $oneToOneAnnot->mappedBy; + $mapping['cascade'] = $oneToOneAnnot->cascade; + $mapping['orphanRemoval'] = $oneToOneAnnot->orphanRemoval; + $mapping['fetch'] = constant('Doctrine\ORM\Mapping\AssociationMapping::FETCH_' . $oneToOneAnnot->fetch); + $metadata->mapOneToOne($mapping); + } else if ($oneToManyAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\OneToMany')) { + $mapping['mappedBy'] = $oneToManyAnnot->mappedBy; + $mapping['targetEntity'] = $oneToManyAnnot->targetEntity; + $mapping['cascade'] = $oneToManyAnnot->cascade; + $mapping['orphanRemoval'] = $oneToManyAnnot->orphanRemoval; + $mapping['fetch'] = constant('Doctrine\ORM\Mapping\AssociationMapping::FETCH_' . $oneToManyAnnot->fetch); + $metadata->mapOneToMany($mapping); + } else if ($manyToOneAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\ManyToOne')) { + $mapping['joinColumns'] = $joinColumns; + $mapping['cascade'] = $manyToOneAnnot->cascade; + $mapping['targetEntity'] = $manyToOneAnnot->targetEntity; + $mapping['fetch'] = constant('Doctrine\ORM\Mapping\AssociationMapping::FETCH_' . $manyToOneAnnot->fetch); + $metadata->mapManyToOne($mapping); + } else if ($manyToManyAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\ManyToMany')) { + $joinTable = array(); + + if ($joinTableAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\JoinTable')) { + $joinTable = array( + 'name' => $joinTableAnnot->name, + 'schema' => $joinTableAnnot->schema + ); + + foreach ($joinTableAnnot->joinColumns as $joinColumn) { + $joinTable['joinColumns'][] = array( + 'name' => $joinColumn->name, + 'referencedColumnName' => $joinColumn->referencedColumnName, + 'unique' => $joinColumn->unique, + 'nullable' => $joinColumn->nullable, + 'onDelete' => $joinColumn->onDelete, + 'onUpdate' => $joinColumn->onUpdate + ); + } + + foreach ($joinTableAnnot->inverseJoinColumns as $joinColumn) { + $joinTable['inverseJoinColumns'][] = array( + 'name' => $joinColumn->name, + 'referencedColumnName' => $joinColumn->referencedColumnName, + 'unique' => $joinColumn->unique, + 'nullable' => $joinColumn->nullable, + 'onDelete' => $joinColumn->onDelete, + 'onUpdate' => $joinColumn->onUpdate + ); + } + } + + $mapping['joinTable'] = $joinTable; + $mapping['targetEntity'] = $manyToManyAnnot->targetEntity; + $mapping['mappedBy'] = $manyToManyAnnot->mappedBy; + $mapping['cascade'] = $manyToManyAnnot->cascade; + $mapping['fetch'] = constant('Doctrine\ORM\Mapping\AssociationMapping::FETCH_' . $manyToManyAnnot->fetch); + $metadata->mapManyToMany($mapping); + } + } + + // Evaluate HasLifecycleCallbacks annotation + if (isset($classAnnotations['Doctrine\ORM\Mapping\HasLifecycleCallbacks'])) { + foreach ($class->getMethods() as $method) { + if ($method->isPublic()) { + $annotations = $this->_reader->getMethodAnnotations($method); + + if (isset($annotations['Doctrine\ORM\Mapping\PrePersist'])) { + $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::prePersist); + } + + if (isset($annotations['Doctrine\ORM\Mapping\PostPersist'])) { + $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::postPersist); + } + + if (isset($annotations['Doctrine\ORM\Mapping\PreUpdate'])) { + $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::preUpdate); + } + + if (isset($annotations['Doctrine\ORM\Mapping\PostUpdate'])) { + $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::postUpdate); + } + + if (isset($annotations['Doctrine\ORM\Mapping\PreRemove'])) { + $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::preRemove); + } + + if (isset($annotations['Doctrine\ORM\Mapping\PostRemove'])) { + $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::postRemove); + } + + if (isset($annotations['Doctrine\ORM\Mapping\PostLoad'])) { + $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::postLoad); + } + } + } + } + } + + /** + * Whether the class with the specified name should have its metadata loaded. + * This is only the case if it is annotated with either @Entity or + * @MappedSuperclass in the class doc block. + * + * @param string $className + * @return boolean + */ + public function isTransient($className) + { + $classAnnotations = $this->_reader->getClassAnnotations(new \ReflectionClass($className)); + + return ! isset($classAnnotations['Doctrine\ORM\Mapping\Entity']) && + ! isset($classAnnotations['Doctrine\ORM\Mapping\MappedSuperclass']); + } + + /** + * {@inheritDoc} + */ + public function getAllClassNames() + { + if ($this->_classDirectory) { + $iter = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->_classDirectory), + \RecursiveIteratorIterator::LEAVES_ONLY); + + $declared = get_declared_classes(); + foreach ($iter as $item) { + $info = pathinfo($item->getPathName()); + if ( ! isset($info['extension']) || $info['extension'] != 'php') { + continue; + } + require_once $item->getPathName(); + } + $declared = array_diff(get_declared_classes(), $declared); + + $classes = array(); + foreach ($declared as $className) { + if ( ! $this->isTransient($className)) { + $classes[] = $className; + } + } + return $classes; + } else { + return array(); + } + } + +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php b/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php index e6a2f3226..3d0ae896d 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php +++ b/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php @@ -63,6 +63,7 @@ final class Column extends Annotation { public $default; //TODO: remove? public $name; public $options = array(); + public $columnDefinition; } final class OneToOne extends Annotation { public $targetEntity; diff --git a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php index b440745ec..a45fa4773 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php @@ -159,6 +159,10 @@ class XmlDriver extends AbstractFileDriver $metadata->setVersionMapping($mapping); } + if (isset($fieldMapping['columnDefinition'])) { + $mapping['columnDefinition'] = (string)$fieldMapping['columnDefinition']; + } + $metadata->mapField($mapping); } } diff --git a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php.orig b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php.orig new file mode 100644 index 000000000..b440745ec --- /dev/null +++ b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php.orig @@ -0,0 +1,452 @@ +. + */ + +namespace Doctrine\ORM\Mapping\Driver; + +use Doctrine\ORM\Mapping\ClassMetadataInfo, + Doctrine\ORM\Mapping\MappingException; + +/** + * XmlDriver is a metadata driver that enables mapping through XML files. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class XmlDriver extends AbstractFileDriver +{ + protected $_fileExtension = '.dcm.xml'; + + /** + * Loads the metadata for the specified class into the provided container. + * + * @param string $className + * @param ClassMetadata $metadata + */ + public function loadMetadataForClass($className, ClassMetadataInfo $metadata) + { + $xmlRoot = $this->getElement($className); + + if ($xmlRoot->getName() == 'entity') { + $metadata->setCustomRepositoryClass( + isset($xmlRoot['repository-class']) ? (string)$xmlRoot['repository-class'] : null + ); + } else if ($xmlRoot->getName() == 'mapped-superclass') { + $metadata->isMappedSuperclass = true; + } else { + throw DoctrineException::classIsNotAValidEntityOrMapperSuperClass($className); + } + + // 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 + if (isset($xmlRoot->{'discriminator-column'})) { + $discrColumn = $xmlRoot->{'discriminator-column'}; + $metadata->setDiscriminatorColumn(array( + 'name' => (string)$discrColumn['name'], + 'type' => (string)$discrColumn['type'], + 'length' => (string)$discrColumn['length'] + )); + } + + // Evaluate + if (isset($xmlRoot->{'discriminator-map'})) { + $metadata->setDiscriminatorMap((array)$xmlRoot->{'discriminator-map'}); + } + + // Evaluate + if (isset($xmlRoot->{'change-tracking-policy'})) { + $metadata->setChangeTrackingPolicy(constant('Doctrine\ORM\Mapping\ClassMetadata::CHANGETRACKING_' + . strtoupper((string)$xmlRoot->{'change-tracking-policy'}))); + } + + // Evaluate + if (isset($xmlRoot->indexes)) { + foreach ($xmlRoot->indexes->index as $index) { + if (is_string($index['columns'])) { + $columns = explode(',', $index['columns']); + } else { + $columns = $index['columns']; + } + + $metadata->primaryTable['indexes'][$index['name']] = array( + 'columns' => $columns + ); + } + } + + // Evaluate + if (isset($xmlRoot->{'unique-constraints'})) { + foreach ($xmlRoot->{'unique-constraints'}->{'unique-constraint'} as $unique) { + if (is_string($unique['columns'])) { + $columns = explode(',', $unique['columns']); + } else { + $columns = $unique['columns']; + } + + $metadata->primaryTable['uniqueConstraints'][$unique['name']] = array( + 'columns' => $columns + ); + } + } + + // 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']; + } + + if (isset($fieldMapping['precision'])) { + $mapping['precision'] = (int)$fieldMapping['precision']; + } + + if (isset($fieldMapping['scale'])) { + $mapping['scale'] = (int)$fieldMapping['scale']; + } + + if (isset($fieldMapping['unique'])) { + $mapping['unique'] = (bool)$fieldMapping['unique']; + } + + if (isset($fieldMapping['options'])) { + $mapping['options'] = (array)$fieldMapping['options']; + } + + if (isset($fieldMapping['version']) && $fieldMapping['version']) { + $metadata->setVersionMapping($mapping); + } + + $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(constant('Doctrine\ORM\Mapping\ClassMetadata::GENERATOR_TYPE_' + . strtoupper((string)$idElement->generator['strategy']))); + } + + // Check for SequenceGenerator/TableGenerator definition + if (isset($idElement->{'sequence-generator'})) { + $seqGenerator = $idElement->{'sequence-generator'}; + $metadata->setSequenceGeneratorDefinition(array( + 'sequenceName' => $seqGenerator->{'sequence-name'}, + 'allocationSize' => $seqGenerator->{'allocation-size'}, + 'initialValue' => $seqGeneratorAnnot->{'initial-value'} + )); + } else if (isset($idElement->{'table-generator'})) { + throw DoctrineException::tableIdGeneratorNotImplemented(); + } + } + + // Evaluate mappings + if (isset($xmlRoot->{'one-to-one'})) { + foreach ($xmlRoot->{'one-to-one'} as $oneToOneElement) { + $mapping = array( + 'fieldName' => (string)$oneToOneElement['field'], + 'targetEntity' => (string)$oneToOneElement['target-entity'] + ); + + if (isset($oneToOneElement['fetch'])) { + $mapping['fetch'] = constant('Doctrine\ORM\Mapping\AssociationMapping::FETCH_' . (string)$oneToOneElement['fetch']); + } + + if (isset($oneToOneElement['mapped-by'])) { + $mapping['mappedBy'] = (string)$oneToOneElement['mapped-by']; + } 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); + } + + if (isset($oneToOneElement->{'orphan-removal'})) { + $mapping['orphanRemoval'] = (bool)$oneToOneElement->{'orphan-removal'}; + } + + $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['target-entity'], + 'mappedBy' => (string)$oneToManyElement['mapped-by'] + ); + + if (isset($oneToManyElement['fetch'])) { + $mapping['fetch'] = constant('Doctrine\ORM\Mapping\AssociationMapping::FETCH_' . (string)$oneToManyElement['fetch']); + } + + if (isset($oneToManyElement->cascade)) { + $mapping['cascade'] = $this->_getCascadeMappings($oneToManyElement->cascade); + } + + if (isset($oneToManyElement->{'orphan-removal'})) { + $mapping['orphanRemoval'] = (bool)$oneToManyElement->{'orphan-removal'}; + } + + $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['target-entity'] + ); + + if (isset($manyToOneElement['fetch'])) { + $mapping['fetch'] = constant('Doctrine\ORM\Mapping\AssociationMapping::FETCH_' . (string)$manyToOneElement['fetch']); + } + + $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) { + if (!isset($joinColumnElement['name'])) { + $joinColumnElement['name'] = $name; + } + + $joinColumns[] = $this->_getJoinColumnMapping($joinColumnElement); + } + } else { + throw MappingException::invalidMapping($mapping['fieldName']); + } + + $mapping['joinColumns'] = $joinColumns; + + if (isset($manyToOneElement->cascade)) { + $mapping['cascade'] = $this->_getCascadeMappings($manyToOneElement->cascade); + } + + if (isset($manyToOneElement->{'orphan-removal'})) { + $mapping['orphanRemoval'] = (bool)$manyToOneElement->{'orphan-removal'}; + } + + $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['target-entity'] + ); + + if (isset($manyToManyElement['fetch'])) { + $mapping['fetch'] = constant('Doctrine\ORM\Mapping\AssociationMapping::FETCH_' . (string)$manyToManyElement['fetch']); + } + + if (isset($manyToManyElement['mapped-by'])) { + $mapping['mappedBy'] = (string)$manyToManyElement['mapped-by']; + } 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); + } + + if (isset($manyToManyElement->{'orphan-removal'})) { + $mapping['orphanRemoval'] = (bool)$manyToManyElement->{'orphan-removal'}; + } + + $metadata->mapManyToMany($mapping); + } + } + + // Evaluate + if (isset($xmlRoot->{'lifecycle-callbacks'})) { + foreach ($xmlRoot->{'lifecycle-callbacks'}->{'lifecycle-callback'} as $lifecycleCallback) { + $metadata->addLifecycleCallback((string)$lifecycleCallback['method'], constant('\Doctrine\ORM\Events::' . (string)$lifecycleCallback['type'])); + } + } + } + + /** + * 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 + */ + protected 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; + } + + /** + * 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['referenced-column-name'] + ); + + 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['on-delete']; + } + + if (isset($joinColumnElement['onUpdate'])) { + $joinColumn['onUpdate'] = (string)$joinColumnElement['on-update']; + } + + 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-persist'})) { + $cascades[] = 'persist'; + } + + if (isset($cascadeElement->{'cascade-remove'})) { + $cascades[] = 'remove'; + } + + if (isset($cascadeElement->{'cascade-merge'})) { + $cascades[] = 'merge'; + } + + if (isset($cascadeElement->{'cascade-refresh'})) { + $cascades[] = 'refresh'; + } + + return $cascades; + } +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php index 6ae95dd12..05a9e7010 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php @@ -204,6 +204,9 @@ class YamlDriver extends AbstractFileDriver if (isset($fieldMapping['version']) && $fieldMapping['version']) { $metadata->setVersionMapping($mapping); } + if (isset($fieldMapping['columnDefinition'])) { + $mapping['columnDefinition'] = $fieldMapping['columnDefinition']; + } $metadata->mapField($mapping); } diff --git a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php.orig b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php.orig new file mode 100644 index 000000000..6ae95dd12 --- /dev/null +++ b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php.orig @@ -0,0 +1,451 @@ +. + */ + +namespace Doctrine\ORM\Mapping\Driver; + +use Doctrine\ORM\Mapping\ClassMetadataInfo, + Doctrine\Common\DoctrineException, + Doctrine\ORM\Mapping\MappingException; + +if ( ! class_exists('sfYaml', false)) { + require_once __DIR__ . '/../../../../vendor/sfYaml/sfYaml.class.php'; + require_once __DIR__ . '/../../../../vendor/sfYaml/sfYamlDumper.class.php'; + require_once __DIR__ . '/../../../../vendor/sfYaml/sfYamlInline.class.php'; + require_once __DIR__ . '/../../../../vendor/sfYaml/sfYamlParser.class.php'; +} + +/** + * The YamlDriver reads the mapping metadata from yaml schema files. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class YamlDriver extends AbstractFileDriver +{ + protected $_fileExtension = '.dcm.yml'; + + public function loadMetadataForClass($className, ClassMetadataInfo $metadata) + { + $element = $this->getElement($className); + + if ($element['type'] == 'entity') { + $metadata->setCustomRepositoryClass( + isset($element['repositoryClass']) ? $element['repositoryClass'] : null + ); + } else if ($element['type'] == 'mappedSuperclass') { + $metadata->isMappedSuperclass = true; + } else { + throw DoctrineException::classIsNotAValidEntityOrMapperSuperClass($className); + } + + // Evaluate root level properties + if (isset($element['table'])) { + $metadata->primaryTable['name'] = $element['table']; + } + + if (isset($element['schema'])) { + $metadata->primaryTable['schema'] = $element['schema']; + } + + if (isset($element['inheritanceType'])) { + $metadata->setInheritanceType(constant('Doctrine\ORM\Mapping\ClassMetadata::INHERITANCE_TYPE_' . strtoupper($element['inheritanceType']))); + } + + // Evaluate discriminatorColumn + if (isset($element['discriminatorColumn'])) { + $discrColumn = $element['discriminatorColumn']; + $metadata->setDiscriminatorColumn(array( + 'name' => $discrColumn['name'], + 'type' => $discrColumn['type'], + 'length' => $discrColumn['length'] + )); + } + + // Evaluate discriminatorMap + if (isset($element['discriminatorMap'])) { + $metadata->setDiscriminatorMap($element['discriminatorMap']); + } + + // Evaluate changeTrackingPolicy + if (isset($element['changeTrackingPolicy'])) { + $metadata->setChangeTrackingPolicy(constant('Doctrine\ORM\Mapping\ClassMetadata::CHANGETRACKING_' + . strtoupper($element['changeTrackingPolicy']))); + } + + // Evaluate indexes + if (isset($element['indexes'])) { + foreach ($element['indexes'] as $name => $index) { + if ( ! isset($index['name'])) { + $index['name'] = $name; + } + + if (is_string($index['columns'])) { + $columns = explode(',', $index['columns']); + } else { + $columns = $index['columns']; + } + + $metadata->primaryTable['indexes'][$index['name']] = array( + 'columns' => $columns + ); + } + } + + // Evaluate uniqueConstraints + if (isset($element['uniqueConstraints'])) { + foreach ($element['uniqueConstraints'] as $name => $unique) { + if ( ! isset($unique['name'])) { + $unique['name'] = $name; + } + + if (is_string($unique['columns'])) { + $columns = explode(',', $unique['columns']); + } else { + $columns = $unique['columns']; + } + + $metadata->primaryTable['uniqueConstraints'][$unique['name']] = array( + 'columns' => $columns + ); + } + } + + if (isset($element['id'])) { + // Evaluate identifier settings + foreach ($element['id'] as $name => $idElement) { + $mapping = array( + 'id' => true, + 'fieldName' => $name, + 'type' => $idElement['type'] + ); + + if (isset($idElement['column'])) { + $mapping['columnName'] = $idElement['column']; + } + + $metadata->mapField($mapping); + + if (isset($idElement['generator'])) { + $metadata->setIdGeneratorType(constant('Doctrine\ORM\Mapping\ClassMetadata::GENERATOR_TYPE_' + . strtoupper($idElement['generator']['strategy']))); + } + } + } + + // Evaluate fields + if (isset($element['fields'])) { + foreach ($element['fields'] as $name => $fieldMapping) { + $e = explode('(', $fieldMapping['type']); + $fieldMapping['type'] = $e[0]; + if (isset($e[1])) { + $fieldMapping['length'] = substr($e[1], 0, strlen($e[1]) - 1); + } + $mapping = array( + 'fieldName' => $name, + 'type' => $fieldMapping['type'] + ); + if (isset($fieldMapping['id'])) { + $mapping['id'] = true; + if (isset($fieldMapping['generator']['strategy'])) { + $metadata->setIdGeneratorType(constant('Doctrine\ORM\Mapping\ClassMetadata::GENERATOR_TYPE_' + . strtoupper($fieldMapping['generator']['strategy']))); + } + } + // Check for SequenceGenerator/TableGenerator definition + if (isset($fieldMapping['sequenceGenerator'])) { + $metadata->setSequenceGeneratorDefinition($fieldMapping['sequenceGenerator']); + } else if (isset($fieldMapping['tableGenerator'])) { + throw DoctrineException::tableIdGeneratorNotImplemented(); + } + if (isset($fieldMapping['column'])) { + $mapping['columnName'] = $fieldMapping['column']; + } + if (isset($fieldMapping['length'])) { + $mapping['length'] = $fieldMapping['length']; + } + if (isset($fieldMapping['precision'])) { + $mapping['precision'] = $fieldMapping['precision']; + } + if (isset($fieldMapping['scale'])) { + $mapping['scale'] = $fieldMapping['scale']; + } + if (isset($fieldMapping['unique'])) { + $mapping['unique'] = (bool)$fieldMapping['unique']; + } + if (isset($fieldMapping['options'])) { + $mapping['options'] = $fieldMapping['options']; + } + if (isset($fieldMapping['nullable'])) { + $mapping['nullable'] = $fieldMapping['nullable']; + } + if (isset($fieldMapping['version']) && $fieldMapping['version']) { + $metadata->setVersionMapping($mapping); + } + + $metadata->mapField($mapping); + } + } + + // Evaluate oneToOne relationships + if (isset($element['oneToOne'])) { + foreach ($element['oneToOne'] as $name => $oneToOneElement) { + $mapping = array( + 'fieldName' => $name, + 'targetEntity' => $oneToOneElement['targetEntity'] + ); + + if (isset($oneToOneElement['fetch'])) { + $mapping['fetch'] = constant('Doctrine\ORM\Mapping\AssociationMapping::FETCH_' . $oneToOneElement['fetch']); + } + + if (isset($oneToOneElement['mappedBy'])) { + $mapping['mappedBy'] = $oneToOneElement['mappedBy']; + } else { + $joinColumns = array(); + + if (isset($oneToOneElement['joinColumn'])) { + $joinColumns[] = $this->_getJoinColumnMapping($oneToOneElement['joinColumn']); + } else if (isset($oneToOneElement['joinColumns'])) { + foreach ($oneToOneElement['joinColumns'] as $name => $joinColumnElement) { + if (!isset($joinColumnElement['name'])) { + $joinColumnElement['name'] = $name; + } + + $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 oneToMany relationships + if (isset($element['oneToMany'])) { + foreach ($element['oneToMany'] as $name => $oneToManyElement) { + $mapping = array( + 'fieldName' => $name, + 'targetEntity' => $oneToManyElement['targetEntity'], + 'mappedBy' => $oneToManyElement['mappedBy'] + ); + + if (isset($oneToManyElement['fetch'])) { + $mapping['fetch'] = constant('Doctrine\ORM\Mapping\AssociationMapping::FETCH_' . $oneToManyElement['fetch']); + } + + if (isset($oneToManyElement['cascade'])) { + $mapping['cascade'] = $this->_getCascadeMappings($oneToManyElement['cascade']); + } + + $metadata->mapOneToMany($mapping); + } + } + + // Evaluate manyToOne relationships + if (isset($element['manyToOne'])) { + foreach ($element['manyToOne'] as $name => $manyToOneElement) { + $mapping = array( + 'fieldName' => $name, + 'targetEntity' => $manyToOneElement['targetEntity'] + ); + + if (isset($manyToOneElement['fetch'])) { + $mapping['fetch'] = constant('Doctrine\ORM\Mapping\AssociationMapping::FETCH_' . $manyToOneElement['fetch']); + } + + $joinColumns = array(); + + if (isset($manyToOneElement['joinColumn'])) { + $joinColumns[] = $this->_getJoinColumnMapping($manyToOneElement['joinColumn']); + } else if (isset($manyToOneElement['joinColumns'])) { + foreach ($manyToOneElement['joinColumns'] as $name => $joinColumnElement) { + if (!isset($joinColumnElement['name'])) { + $joinColumnElement['name'] = $name; + } + + $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 manyToMany relationships + if (isset($element['manyToMany'])) { + foreach ($element['manyToMany'] as $name => $manyToManyElement) { + $mapping = array( + 'fieldName' => $name, + 'targetEntity' => $manyToManyElement['targetEntity'] + ); + + if (isset($manyToManyElement['fetch'])) { + $mapping['fetch'] = constant('Doctrine\ORM\Mapping\AssociationMapping::FETCH_' . $manyToManyElement['fetch']); + } + + if (isset($manyToManyElement['mappedBy'])) { + $mapping['mappedBy'] = $manyToManyElement['mappedBy']; + } else if (isset($manyToManyElement['joinTable'])) { + $joinTableElement = $manyToManyElement['joinTable']; + $joinTable = array( + 'name' => $joinTableElement['name'] + ); + + if (isset($joinTableElement['schema'])) { + $joinTable['schema'] = $joinTableElement['schema']; + } + + foreach ($joinTableElement['joinColumns'] as $name => $joinColumnElement) { + if (!isset($joinColumnElement['name'])) { + $joinColumnElement['name'] = $name; + } + + $joinTable['joinColumns'][] = $this->_getJoinColumnMapping($joinColumnElement); + } + + foreach ($joinTableElement['inverseJoinColumns'] as $name => $joinColumnElement) { + if (!isset($joinColumnElement['name'])) { + $joinColumnElement['name'] = $name; + } + + $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); + } + } + + // Evaluate lifeCycleCallbacks + if (isset($element['lifecycleCallbacks'])) { + foreach ($element['lifecycleCallbacks'] as $method => $type) { + $metadata->addLifecycleCallback($method, constant('\Doctrine\ORM\Events::' . $type)); + } + } + } + + /** + * Constructs a joinColumn mapping array based on the information + * found in the given join column element. + * + * @param $joinColumnElement The array join column element + * @return array The mapping array. + */ + private function _getJoinColumnMapping($joinColumnElement) + { + $joinColumn = array( + 'name' => $joinColumnElement['name'], + 'referencedColumnName' => $joinColumnElement['referencedColumnName'] + ); + + if (isset($joinColumnElement['fieldName'])) { + $joinColumn['fieldName'] = (string) $joinColumnElement['fieldName']; + } + + if (isset($joinColumnElement['unique'])) { + $joinColumn['unique'] = (bool) $joinColumnElement['unique']; + } + + if (isset($joinColumnElement['nullable'])) { + $joinColumn['nullable'] = (bool) $joinColumnElement['nullable']; + } + + if (isset($joinColumnElement['onDelete'])) { + $joinColumn['onDelete'] = $joinColumnElement['onDelete']; + } + + if (isset($joinColumnElement['onUpdate'])) { + $joinColumn['onUpdate'] = $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['cascadePersist'])) { + $cascades[] = 'persist'; + } + + if (isset($cascadeElement['cascadeRemove'])) { + $cascades[] = 'remove'; + } + + if (isset($cascadeElement['cascadeMerge'])) { + $cascades[] = 'merge'; + } + + if (isset($cascadeElement['cascadeRefresh'])) { + $cascades[] = 'refresh'; + } + + return $cascades; + } + + /** + * Loads a mapping file with the given name and returns a map + * from class/entity names to their corresponding elements. + * + * @param string $file The mapping file to load. + * @return array + */ + protected function _loadMappingFile($file) + { + return \sfYaml::load($file); + } +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Tools/SchemaTool.php b/lib/Doctrine/ORM/Tools/SchemaTool.php index f3f1aeb30..571671be3 100644 --- a/lib/Doctrine/ORM/Tools/SchemaTool.php +++ b/lib/Doctrine/ORM/Tools/SchemaTool.php @@ -306,6 +306,10 @@ class SchemaTool if (isset($mapping['default'])) { $options['default'] = $mapping['default']; } + + if (isset($mapping['columnDefinition'])) { + $options['columnDefinition'] = $mapping['columnDefinition']; + } if ($table->hasColumn($columnName)) { // required in some inheritence scenarios @@ -392,8 +396,9 @@ class SchemaTool foreach ($joinColumns as $joinColumn) { // Note that this thing might be quoted, i.e. `foo`, [foo], ... $columnName = $mapping->getQuotedJoinColumnName($joinColumn['name'], $this->_platform); + $referencedFieldName = $class->getFieldName($joinColumn['referencedColumnName']); - if (!$class->hasField($class->getFieldName($joinColumn['referencedColumnName']))) { + if (!$class->hasField($referencedFieldName)) { throw new \Doctrine\Common\DoctrineException( "Column name `".$joinColumn['referencedColumnName']."` referenced for relation from ". "$mapping->sourceEntityName towards $mapping->targetEntityName does not exist." @@ -408,8 +413,13 @@ class SchemaTool // Only add the column to the table if it does not exist already. // It might exist already if the foreign key is mapped into a regular // property as well. + + $fieldMapping = $class->getFieldMapping($referencedFieldName); + $columnDef = isset($fieldMapping['columnDefinition']) ? $fieldMapping['columnDefinition'] : null; + $columnOptions = array('notnull' => false, 'columnDefinition' => $columnDef); + $theJoinTable->createColumn( - $columnName, $class->getTypeOfColumn($joinColumn['referencedColumnName']), array('notnull' => false) + $columnName, $class->getTypeOfColumn($joinColumn['referencedColumnName']), $columnOptions ); } diff --git a/lib/Doctrine/ORM/Tools/SchemaTool.php.orig b/lib/Doctrine/ORM/Tools/SchemaTool.php.orig new file mode 100644 index 000000000..f3f1aeb30 --- /dev/null +++ b/lib/Doctrine/ORM/Tools/SchemaTool.php.orig @@ -0,0 +1,605 @@ +. + */ + +namespace Doctrine\ORM\Tools; + +use Doctrine\DBAL\Types\Type, + Doctrine\ORM\EntityManager, + Doctrine\ORM\Internal\CommitOrderCalculator; + +/** + * The SchemaTool is a tool to create/drop/update database schemas based on + * ClassMetadata class descriptors. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Benjamin Eberlei + */ +class SchemaTool +{ + /** + * @var string + */ + const DROP_METADATA = "metadata"; + /** + * @var string + */ + const DROP_DATABASE = "database"; + + /** + * @var \Doctrine\ORM\EntityManager + */ + private $_em; + + /** + * @var \Doctrine\DBAL\Platforms\AbstractPlatform + */ + private $_platform; + + /** + * Initializes a new SchemaTool instance that uses the connection of the + * provided EntityManager. + * + * @param Doctrine\ORM\EntityManager $em + */ + public function __construct(EntityManager $em) + { + $this->_em = $em; + $this->_platform = $em->getConnection()->getDatabasePlatform(); + } + + /** + * Creates the database schema for the given array of ClassMetadata instances. + * + * @param array $classes + */ + public function createSchema(array $classes) + { + $createSchemaSql = $this->getCreateSchemaSql($classes); + $conn = $this->_em->getConnection(); + + foreach ($createSchemaSql as $sql) { + $conn->execute($sql); + } + } + + /** + * Gets the list of DDL statements that are required to create the database schema for + * the given list of ClassMetadata instances. + * + * @param array $classes + * @return array $sql The SQL statements needed to create the schema for the classes. + */ + public function getCreateSchemaSql(array $classes) + { + $schema = $this->getSchemaFromMetadata($classes); + return $schema->toSql($this->_platform); + } + + /** + * From a given set of metadata classes this method creates a Schema instance. + * + * @param array $classes + * @return Schema + */ + public function getSchemaFromMetadata(array $classes) + { + $processedClasses = array(); // Reminder for processed classes, used for hierarchies + + $sm = $this->_em->getConnection()->getSchemaManager(); + $schema = new \Doctrine\DBAL\Schema\Schema(array(), array(), $sm->createSchemaConfig()); + + foreach ($classes as $class) { + if (isset($processedClasses[$class->name]) || $class->isMappedSuperclass) { + continue; + } + + $table = $schema->createTable($class->getQuotedTableName($this->_platform)); + + if ($class->isIdGeneratorIdentity()) { + $table->setIdGeneratorType(\Doctrine\DBAL\Schema\Table::ID_IDENTITY); + } else if ($class->isIdGeneratorSequence()) { + $table->setIdGeneratorType(\Doctrine\DBAL\Schema\Table::ID_SEQUENCE); + } + + $columns = array(); // table columns + + if ($class->isInheritanceTypeSingleTable()) { + $columns = $this->_gatherColumns($class, $table); + $this->_gatherRelationsSql($class, $table, $schema); + + // Add the discriminator column + $discrColumnDef = $this->_getDiscriminatorColumnDefinition($class, $table); + + // Aggregate all the information from all classes in the hierarchy + foreach ($class->parentClasses as $parentClassName) { + // Parent class information is already contained in this class + $processedClasses[$parentClassName] = true; + } + + foreach ($class->subClasses as $subClassName) { + $subClass = $this->_em->getClassMetadata($subClassName); + $this->_gatherColumns($subClass, $table); + $this->_gatherRelationsSql($subClass, $table, $schema); + $processedClasses[$subClassName] = true; + } + } else if ($class->isInheritanceTypeJoined()) { + // Add all non-inherited fields as columns + $pkColumns = array(); + foreach ($class->fieldMappings as $fieldName => $mapping) { + if ( ! isset($mapping['inherited'])) { + $columnName = $class->getQuotedColumnName($mapping['fieldName'], $this->_platform); + $this->_gatherColumn($class, $mapping, $table); + + if ($class->isIdentifier($fieldName)) { + $pkColumns[] = $columnName; + } + } + } + + $this->_gatherRelationsSql($class, $table, $schema); + + // Add the discriminator column only to the root table + if ($class->name == $class->rootEntityName) { + $discrColumnDef = $this->_getDiscriminatorColumnDefinition($class, $table); + } else { + // Add an ID FK column to child tables + /* @var Doctrine\ORM\Mapping\ClassMetadata $class */ + $idMapping = $class->fieldMappings[$class->identifier[0]]; + $this->_gatherColumn($class, $idMapping, $table); + $columnName = $class->getQuotedColumnName($class->identifier[0], $this->_platform); + + $pkColumns[] = $columnName; + if ($table->isIdGeneratorIdentity()) { + $table->setIdGeneratorType(\Doctrine\DBAL\Schema\Table::ID_NONE); + } + + // Add a FK constraint on the ID column + $table->addUnnamedForeignKeyConstraint( + $this->_em->getClassMetadata($class->rootEntityName)->getQuotedTableName($this->_platform), + array($columnName), array($columnName), array('onDelete' => 'CASCADE') + ); + } + + $table->setPrimaryKey($pkColumns); + + } else if ($class->isInheritanceTypeTablePerClass()) { + throw DoctrineException::notSupported(); + } else { + $this->_gatherColumns($class, $table); + $this->_gatherRelationsSql($class, $table, $schema); + } + + if (isset($class->primaryTable['indexes'])) { + foreach ($class->primaryTable['indexes'] AS $indexName => $indexData) { + $table->addIndex($indexData['columns'], $indexName); + } + } + + if (isset($class->primaryTable['uniqueConstraints'])) { + foreach ($class->primaryTable['uniqueConstraints'] AS $indexName => $indexData) { + $table->addUniqueIndex($indexData['columns'], $indexName); + } + } + + $processedClasses[$class->name] = true; + + if ($class->isIdGeneratorSequence() && $class->name == $class->rootEntityName) { + $seqDef = $class->getSequenceGeneratorDefinition(); + + if (!$schema->hasSequence($seqDef['sequenceName'])) { + $schema->createSequence( + $seqDef['sequenceName'], + $seqDef['allocationSize'], + $seqDef['initialValue'] + ); + } + } + } + + return $schema; + } + + /** + * Gets a portable column definition as required by the DBAL for the discriminator + * column of a class. + * + * @param ClassMetadata $class + * @return array The portable column definition of the discriminator column as required by + * the DBAL. + */ + private function _getDiscriminatorColumnDefinition($class, $table) + { + $discrColumn = $class->discriminatorColumn; + + $table->createColumn( + $class->getQuotedDiscriminatorColumnName($this->_platform), + $discrColumn['type'], + array('length' => $discrColumn['length'], 'notnull' => true) + ); + } + + /** + * Gathers the column definitions as required by the DBAL of all field mappings + * found in the given class. + * + * @param ClassMetadata $class + * @param Table $table + * @return array The list of portable column definitions as required by the DBAL. + */ + private function _gatherColumns($class, $table) + { + $columns = array(); + $pkColumns = array(); + + foreach ($class->fieldMappings as $fieldName => $mapping) { + $column = $this->_gatherColumn($class, $mapping, $table); + + if ($class->isIdentifier($mapping['fieldName'])) { + $pkColumns[] = $class->getQuotedColumnName($mapping['fieldName'], $this->_platform); + } + } + // For now, this is a hack required for single table inheritence, since this method is called + // twice by single table inheritence relations + if(!$table->hasIndex('primary')) { + $table->setPrimaryKey($pkColumns); + } + + return $columns; + } + + /** + * Creates a column definition as required by the DBAL from an ORM field mapping definition. + * + * @param ClassMetadata $class The class that owns the field mapping. + * @param array $mapping The field mapping. + * @param Table $table + * @return array The portable column definition as required by the DBAL. + */ + private function _gatherColumn($class, array $mapping, $table) + { + $columnName = $class->getQuotedColumnName($mapping['fieldName'], $this->_platform); + $columnType = $mapping['type']; + + $options = array(); + $options['length'] = isset($mapping['length']) ? $mapping['length'] : null; + $options['notnull'] = isset($mapping['nullable']) ? ! $mapping['nullable'] : true; + + $options['platformOptions'] = array(); + $options['platformOptions']['version'] = $class->isVersioned && $class->versionField == $mapping['fieldName'] ? true : false; + + if(strtolower($columnType) == 'string' && $options['length'] === null) { + $options['length'] = 255; + } + + if (isset($mapping['precision'])) { + $options['precision'] = $mapping['precision']; + } + + if (isset($mapping['scale'])) { + $options['scale'] = $mapping['scale']; + } + + if (isset($mapping['default'])) { + $options['default'] = $mapping['default']; + } + + if ($table->hasColumn($columnName)) { + // required in some inheritence scenarios + $table->changeColumn($columnName, $options); + } else { + $table->createColumn($columnName, $columnType, $options); + } + + $isUnique = isset($mapping['unique']) ? $mapping['unique'] : false; + if ($isUnique) { + $table->addUniqueIndex(array($columnName)); + } + } + + /** + * Gathers the SQL for properly setting up the relations of the given class. + * This includes the SQL for foreign key constraints and join tables. + * + * @param ClassMetadata $class + * @param array $sql The sequence of SQL statements where any new statements should be appended. + * @param array $columns The list of columns in the class's primary table where any additional + * columns required by relations should be appended. + * @param array $constraints The constraints of the table where any additional constraints + * required by relations should be appended. + * @return void + */ + private function _gatherRelationsSql($class, $table, $schema) + { + foreach ($class->associationMappings as $fieldName => $mapping) { + if (isset($class->inheritedAssociationFields[$fieldName])) { + continue; + } + + $foreignClass = $this->_em->getClassMetadata($mapping->targetEntityName); + + if ($mapping->isOneToOne() && $mapping->isOwningSide) { + $primaryKeyColumns = $uniqueConstraints = array(); // unnecessary for this relation-type + + $this->_gatherRelationJoinColumns($mapping->getJoinColumns(), $table, $foreignClass, $mapping, $primaryKeyColumns, $uniqueConstraints); + } else if ($mapping->isOneToMany() && $mapping->isOwningSide) { + //... create join table, one-many through join table supported later + throw DoctrineException::notSupported(); + } else if ($mapping->isManyToMany() && $mapping->isOwningSide) { + // create join table + $joinTable = $mapping->getJoinTable(); + + $theJoinTable = $schema->createTable($mapping->getQuotedJoinTableName($this->_platform)); + + $primaryKeyColumns = $uniqueConstraints = array(); + + // Build first FK constraint (relation table => source table) + $this->_gatherRelationJoinColumns($joinTable['joinColumns'], $theJoinTable, $class, $mapping, $primaryKeyColumns, $uniqueConstraints); + + // Build second FK constraint (relation table => target table) + $this->_gatherRelationJoinColumns($joinTable['inverseJoinColumns'], $theJoinTable, $foreignClass, $mapping, $primaryKeyColumns, $uniqueConstraints); + + foreach($uniqueConstraints AS $indexName => $unique) { + $theJoinTable->addUniqueIndex( + $unique['columns'], is_numeric($indexName) ? null : $indexName + ); + } + + $theJoinTable->setPrimaryKey($primaryKeyColumns); + } + } + } + + /** + * Gather columns and fk constraints that are required for one part of relationship. + * + * @param array $joinColumns + * @param \Doctrine\DBAL\Schema\Table $theJoinTable + * @param ClassMetadata $class + * @param \Doctrine\ORM\Mapping\AssociationMapping $mapping + * @param array $primaryKeyColumns + * @param array $uniqueConstraints + */ + private function _gatherRelationJoinColumns($joinColumns, $theJoinTable, $class, $mapping, &$primaryKeyColumns, &$uniqueConstraints) + { + $localColumns = array(); + $foreignColumns = array(); + $fkOptions = array(); + + foreach ($joinColumns as $joinColumn) { + // Note that this thing might be quoted, i.e. `foo`, [foo], ... + $columnName = $mapping->getQuotedJoinColumnName($joinColumn['name'], $this->_platform); + + if (!$class->hasField($class->getFieldName($joinColumn['referencedColumnName']))) { + throw new \Doctrine\Common\DoctrineException( + "Column name `".$joinColumn['referencedColumnName']."` referenced for relation from ". + "$mapping->sourceEntityName towards $mapping->targetEntityName does not exist." + ); + } + + $primaryKeyColumns[] = $columnName; + $localColumns[] = $columnName; + $foreignColumns[] = $joinColumn['referencedColumnName']; + + if ( ! $theJoinTable->hasColumn($joinColumn['name'])) { + // Only add the column to the table if it does not exist already. + // It might exist already if the foreign key is mapped into a regular + // property as well. + $theJoinTable->createColumn( + $columnName, $class->getTypeOfColumn($joinColumn['referencedColumnName']), array('notnull' => false) + ); + } + + if (isset($joinColumn['unique']) && $joinColumn['unique'] == true) { + $uniqueConstraints[] = array('columns' => $columnName); + } + + if (isset($joinColumn['onUpdate'])) { + $fkOptions['onUpdate'] = $joinColumn['onUpdate']; + } + + if (isset($joinColumn['onDelete'])) { + $fkOptions['onDelete'] = $joinColumn['onDelete']; + } + } + + $theJoinTable->addUnnamedForeignKeyConstraint( + $class->getQuotedTableName($this->_platform), $localColumns, $foreignColumns, $fkOptions + ); + } + + /** + * Drops the database schema for the given classes. + * + * In any way when an exception is thrown it is supressed since drop was + * issued for all classes of the schema and some probably just don't exist. + * + * @param array $classes + * @param string $mode + * @return void + */ + public function dropSchema(array $classes, $mode=self::DROP_METADATA) + { + $dropSchemaSql = $this->getDropSchemaSql($classes, $mode); + $conn = $this->_em->getConnection(); + + foreach ($dropSchemaSql as $sql) { + $conn->execute($sql); + } + } + + /** + * Gets the SQL needed to drop the database schema for the given classes. + * + * @param array $classes + * @param string $mode + * @return array + */ + public function getDropSchemaSql(array $classes) + { + $sm = $this->_em->getConnection()->getSchemaManager(); + $schema = $sm->createSchema(); + + $visitor = new \Doctrine\DBAL\Schema\Visitor\DropSchemaSqlCollector($this->_platform); + /* @var $schema \Doctrine\DBAL\Schema\Schema */ + $schema->visit($visitor); + return $visitor->getQueries(); + } + + /** + * Drop all tables of the database connection. + * + * @return array + */ + private function _getDropSchemaTablesDatabaseMode($classes) + { + $conn = $this->_em->getConnection(); + + $sm = $conn->getSchemaManager(); + /* @var $sm \Doctrine\DBAL\Schema\AbstractSchemaManager */ + + $allTables = $sm->listTables(); + + $orderedTables = $this->_getDropSchemaTablesMetadataMode($classes); + foreach($allTables AS $tableName) { + if(!in_array($tableName, $orderedTables)) { + $orderedTables[] = $tableName; + } + } + + return $orderedTables; + } + + private function _getDropSchemaTablesMetadataMode(array $classes) + { + $orderedTables = array(); + + $commitOrder = $this->_getCommitOrder($classes); + $associationTables = $this->_getAssociationTables($commitOrder); + + // Drop association tables first + foreach ($associationTables as $associationTable) { + $orderedTables[] = $associationTable; + } + + // Drop tables in reverse commit order + for ($i = count($commitOrder) - 1; $i >= 0; --$i) { + $class = $commitOrder[$i]; + + if (($class->isInheritanceTypeSingleTable() && $class->name != $class->rootEntityName) + || $class->isMappedSuperclass) { + continue; + } + + $orderedTables[] = $class->getTableName(); + } + + //TODO: Drop other schema elements, like sequences etc. + + return $orderedTables; + } + + /** + * Updates the database schema of the given classes by comparing the ClassMetadata + * instances to the current database schema that is inspected. + * + * @param array $classes + * @return void + */ + public function updateSchema(array $classes, $saveMode=false) + { + $updateSchemaSql = $this->getUpdateSchemaSql($classes, $saveMode); + $conn = $this->_em->getConnection(); + + foreach ($updateSchemaSql as $sql) { + $conn->execute($sql); + } + } + + /** + * Gets the sequence of SQL statements that need to be performed in order + * to bring the given class mappings in-synch with the relational schema. + * + * @param array $classes The classes to consider. + * @return array The sequence of SQL statements. + */ + public function getUpdateSchemaSql(array $classes, $saveMode=false) + { + $sm = $this->_em->getConnection()->getSchemaManager(); + + $fromSchema = $sm->createSchema(); + $toSchema = $this->getSchemaFromMetadata($classes); + + $comparator = new \Doctrine\DBAL\Schema\Comparator(); + $schemaDiff = $comparator->compare($fromSchema, $toSchema); + + if ($saveMode) { + return $schemaDiff->toSaveSql($this->_platform); + } else { + return $schemaDiff->toSql($this->_platform); + } + } + + private function _getCommitOrder(array $classes) + { + $calc = new CommitOrderCalculator; + + // Calculate dependencies + foreach ($classes as $class) { + $calc->addClass($class); + + foreach ($class->associationMappings as $assoc) { + if ($assoc->isOwningSide) { + $targetClass = $this->_em->getClassMetadata($assoc->targetEntityName); + + if ( ! $calc->hasClass($targetClass->name)) { + $calc->addClass($targetClass); + } + + // add dependency ($targetClass before $class) + $calc->addDependency($targetClass, $class); + } + } + } + + return $calc->getCommitOrder(); + } + + private function _getAssociationTables(array $classes) + { + $associationTables = array(); + + foreach ($classes as $class) { + foreach ($class->associationMappings as $assoc) { + if ($assoc->isOwningSide && $assoc->isManyToMany()) { + $associationTables[] = $assoc->joinTable['name']; + } + } + } + + return $associationTables; + } +} diff --git a/tests/Doctrine/Tests/DBAL/Platforms/AbstractPlatformTestCase.php b/tests/Doctrine/Tests/DBAL/Platforms/AbstractPlatformTestCase.php index 9d5ae6079..a100b37f0 100644 --- a/tests/Doctrine/Tests/DBAL/Platforms/AbstractPlatformTestCase.php +++ b/tests/Doctrine/Tests/DBAL/Platforms/AbstractPlatformTestCase.php @@ -4,6 +4,9 @@ namespace Doctrine\Tests\DBAL\Platforms; abstract class AbstractPlatformTestCase extends \Doctrine\Tests\DbalTestCase { + /** + * @var Doctrine\DBAL\Platforms\AbstractPlatform + */ protected $_platform; abstract public function createPlatform(); @@ -125,4 +128,10 @@ abstract class AbstractPlatformTestCase extends \Doctrine\Tests\DbalTestCase $this->assertEquals($expectedSql, $sql); } + + public function testGetCustomColumnDeclarationSql() + { + $field = array('columnDefinition' => 'MEDIUMINT(6) UNSIGNED'); + $this->assertEquals('foo MEDIUMINT(6) UNSIGNED', $this->_platform->getColumnDeclarationSql('foo', $field)); + } } diff --git a/tests/Doctrine/Tests/DBAL/Schema/ColumnTest.php b/tests/Doctrine/Tests/DBAL/Schema/ColumnTest.php index 931dace4d..4b6cd85a4 100644 --- a/tests/Doctrine/Tests/DBAL/Schema/ColumnTest.php +++ b/tests/Doctrine/Tests/DBAL/Schema/ColumnTest.php @@ -44,6 +44,7 @@ class ColumnTest extends \PHPUnit_Framework_TestCase 'scale' => 2, 'fixed' => true, 'unsigned' => true, + 'columnDefinition' => null, 'foo' => 'bar', ); diff --git a/tests/Doctrine/Tests/ORM/Tools/AllTests.php b/tests/Doctrine/Tests/ORM/Tools/AllTests.php index 9f837d05e..acff3e8b3 100644 --- a/tests/Doctrine/Tests/ORM/Tools/AllTests.php +++ b/tests/Doctrine/Tests/ORM/Tools/AllTests.php @@ -18,10 +18,11 @@ class AllTests public static function suite() { - $suite = new \Doctrine\Tests\DoctrineTestSuite('Doctrine Orm Hydration'); + $suite = new \Doctrine\Tests\DoctrineTestSuite('Doctrine Orm Tools'); $suite->addTestSuite('Doctrine\Tests\ORM\Tools\Export\ClassMetadataExporterTest'); $suite->addTestSuite('Doctrine\Tests\ORM\Tools\ConvertDoctrine1SchemaTest'); + $suite->addTestSuite('Doctrine\Tests\ORM\Tools\SchemaToolTest'); return $suite; } diff --git a/tests/Doctrine/Tests/ORM/Tools/SchemaToolTest.php b/tests/Doctrine/Tests/ORM/Tools/SchemaToolTest.php index af7225619..cfc52203a 100644 --- a/tests/Doctrine/Tests/ORM/Tools/SchemaToolTest.php +++ b/tests/Doctrine/Tests/ORM/Tools/SchemaToolTest.php @@ -32,4 +32,28 @@ class SchemaToolTest extends \Doctrine\Tests\OrmTestCase array_map('strtolower', $schema->getTable('cms_users')->getIndex('cms_users_username_uniq')->getColumns()) ); } + + /** + * @group DDC-200 + */ + public function testPassColumnDefinitionToJoinColumn() + { + $customColumnDef = "MEDIUMINT(6) UNSIGNED NOT NULL"; + + $em = $this->_getTestEntityManager(); + $schemaTool = new SchemaTool($em); + + $avatar = $em->getClassMetadata('Doctrine\Tests\Models\Forum\ForumAvatar'); + $avatar->fieldMappings['id']['columnDefinition'] = $customColumnDef; + $user = $em->getClassMetadata('Doctrine\Tests\Models\Forum\ForumUser'); + + $classes = array($avatar, $user); + + $schema = $schemaTool->getSchemaFromMetadata($classes); + + $this->assertTrue($schema->hasTable('forum_users')); + $table = $schema->getTable("forum_users"); + $this->assertTrue($table->hasColumn('avatar_id')); + $this->assertEquals($customColumnDef, $table->getColumn('avatar_id')->getColumnDefinition()); + } } \ No newline at end of file