diff --git a/lib/Doctrine/Import/Builder.php b/lib/Doctrine/Import/Builder.php index 7dc64d4ab..913c50841 100644 --- a/lib/Doctrine/Import/Builder.php +++ b/lib/Doctrine/Import/Builder.php @@ -179,6 +179,14 @@ class Doctrine_Import_Builder extends Doctrine_Builder */ protected $_phpDocEmail = '##EMAIL##'; + + /** + * Contains the actAs columns after running buildSetUp + * + * @var array + */ + private $_actAsColumns = array(); + /** * _tpl * @@ -396,9 +404,7 @@ public function buildTableDefinition(array $definition) /** * buildSetUp * - * @param array $options - * @param array $columns - * @param array $relations + * @param array $definition * @return string */ public function buildSetUp(array $definition) @@ -857,21 +863,33 @@ public function buildPhpDocs(array $definition) return $ret; } + /** + * find class matching $name + * + * @param $name + * @return class-string + */ + private function findTemplateClassMatchingName($name) + { + $classname = $name; + if (class_exists("Doctrine_Template_$name", true)) { + $classname = "Doctrine_Template_$name"; + } + + return $classname; + } + /** * emit a behavior assign * * @param int $level * @param string $name * @param string $option + * @param class-string $classname * @return string assignation code */ - private function emitAssign($level, $name, $option) + private function emitAssign($level, $name, $option, $classname) { - // find class matching $name - $classname = $name; - if (class_exists("Doctrine_Template_$name", true)) { - $classname = "Doctrine_Template_$name"; - } return " \$" . strtolower($name) . "$level = new $classname($option);". PHP_EOL; } @@ -943,6 +961,7 @@ private function innerBuildActAs($actAs, $level = 0, $parent = null, array &$emi $currentParent = $parent; if (is_array($actAs)) { foreach($actAs as $template => $options) { + $className = $this->findTemplateClassMatchingName($template); if ($template == 'actAs') { // found another actAs $build .= $this->innerBuildActAs($options, $level + 1, $parent, $emittedActAs); @@ -959,7 +978,8 @@ private function innerBuildActAs($actAs, $level = 0, $parent = null, array &$emi } $optionPHP = $this->varExport($realOptions); - $build .= $this->emitAssign($level, $template, $optionPHP); + $build .= $this->emitAssign($level, $template, $optionPHP, $className); + $this->determineActAsColumns($className, $realOptions); if ($level == 0) { $emittedActAs[] = $this->emitActAs($level, $template); } else { @@ -969,7 +989,8 @@ private function innerBuildActAs($actAs, $level = 0, $parent = null, array &$emi $parent = $template; $build .= $this->innerBuildActAs($leftActAs, $level, $template, $emittedActAs); } else { - $build .= $this->emitAssign($level, $template, null); + $build .= $this->emitAssign($level, $template, null, $className); + $this->determineActAsColumns($className, array($options)); if ($level == 0) { $emittedActAs[] = $this->emitActAs($level, $template); } else { @@ -979,7 +1000,9 @@ private function innerBuildActAs($actAs, $level = 0, $parent = null, array &$emi } } } else { - $build .= $this->emitAssign($level, $actAs, null); + $className = $this->findTemplateClassMatchingName($actAs); + $build .= $this->emitAssign($level, $actAs, null, $className); + $this->determineActAsColumns($className, array()); if ($level == 0) { $emittedActAs[] = $this->emitActAs($level, $actAs); } else { @@ -990,6 +1013,70 @@ private function innerBuildActAs($actAs, $level = 0, $parent = null, array &$emi return $build; } + /** + * Adds the columns of the used actAs behaviors to the comment block. + * + * @param class-string $className + * @param array $instanceOptions + * + * @throws Doctrine_Import_Builder_Exception + */ + private function determineActAsColumns($className, $instanceOptions) + { + // No class specified or class does not exist. + if (!$className || !class_exists($className)) { + return; + } + + $actAsInstance = new $className($instanceOptions); + $options = $actAsInstance->getOptions(); + + // Some behaviors do not contain an array of columns, e.g. SoftDelete. + if (!is_array(reset($options))) { + $options = array($options); + } + + foreach ($options as $name => $column) { + if (!is_array($column) || !array_key_exists('name', $column) || !array_key_exists('type', $column)) { + // 'name' or 'type' not found. Unfortunately there is no logger. What is the best way to abort here? + continue; + } + + if (array_key_exists('disabled', $column) && $column['disabled']) { + // Column has been disabled. + continue; + } + + // Add field, if it does not exist already. + if (array_key_exists($name, $this->_actAsColumns)) { + continue; + } + + $this->_actAsColumns[$name] = $column; + } + } + + private function mergeDefinitionAndActAsColumns(array $definitionColumns, array $actAsColumns) + { + $result = $definitionColumns; + + foreach ($actAsColumns as $actAsOptionName => $actAsColumn) { + $actAsColumnName = isset($actAsColumn['name']) ? $actAsColumn['name'] : $actAsOptionName; + + foreach ($result as $optionName => $column) { + $name = isset($column['name']) ? $column['name'] : $optionName; + if ($name === $actAsColumnName) { + continue 2; + } + } + + $result[$actAsOptionName] = $actAsColumn; + } + + return $result; + } + + /** * Build php code for adding record listeners * @@ -1122,6 +1209,8 @@ public function buildDefinition(array $definition) $className = $definition['className']; $extends = isset($definition['inheritance']['extends']) ? $definition['inheritance']['extends']:$this->_baseClassName; + // Clear actAsColumns + $this->_actAsColumns = array(); if ( ! (isset($definition['no_definition']) && $definition['no_definition'] === true)) { $tableDefinitionCode = $this->buildTableDefinition($definition); $setUpCode = $this->buildSetUp($definition); @@ -1136,6 +1225,7 @@ public function buildDefinition(array $definition) $setUpCode.= $this->buildToString($definition); + $definition['columns'] = $this->mergeDefinitionAndActAsColumns($definition['columns'], $this->_actAsColumns); $docs = PHP_EOL . $this->buildPhpDocs($definition); $content = sprintf(self::$_tpl, $docs, $abstract, diff --git a/tests/Ticket/gh110/Ticket_gh110_TestRecord.snapshot b/tests/Ticket/gh110/Ticket_gh110_TestRecord.snapshot new file mode 100644 index 000000000..309a3186b --- /dev/null +++ b/tests/Ticket/gh110/Ticket_gh110_TestRecord.snapshot @@ -0,0 +1,60 @@ +/** + * Ticket_gh110_TestRecord + * + * This class has been auto-generated by the Doctrine ORM Framework + * + * @property int $id Type: integer(4) + * @property my_custom_type $created_at Type: my_custom_type + * @property string $deleted_at Type: timestamp, Timestamp in ISO-8601 format (YYYY-MM-DD HH:MI:SS) + * + * @method int getId() Type: integer(4) + * @method my_custom_type getCreatedAt() Type: my_custom_type + * @method string getDeletedAt() Type: timestamp, Timestamp in ISO-8601 format (YYYY-MM-DD HH:MI:SS) + * + * @method Ticket_gh110_TestRecord setId(int $val) Type: integer(4) + * @method Ticket_gh110_TestRecord setCreatedAt(my_custom_type $val) Type: my_custom_type + * @method Ticket_gh110_TestRecord setDeletedAt(string $val) Type: timestamp, Timestamp in ISO-8601 format (YYYY-MM-DD HH:MI:SS) + * + * @package ##PACKAGE## + * @subpackage ##SUBPACKAGE## + * @author ##NAME## <##EMAIL##> + * @version {{REPLACED}} + */ +class Ticket_gh110_TestRecord extends Doctrine_Record +{ + public function setTableDefinition() + { + $this->hasColumn('id', 'integer', 4, array( + 'type' => 'integer', + 'length' => 4, + )); + $this->hasColumn('created_at', 'my_custom_type', null, array( + 'type' => 'my_custom_type', + 'length' => '', + )); + } + + public function setUp() + { + parent::setUp(); + $softdelete0 = new Doctrine_Template_SoftDelete(array( + )); + $timestampable0 = new Doctrine_Template_Timestampable(array( + 'updated' => + array( + 'disabled' => true, + ), + 'unknown_column' => + array( + ), + )); + $unknownactas0 = new UnknownActAs(array( + )); + $gh110_template0 = new Doctrine_Template_gh110_Template(array( + )); + $this->actAs($softdelete0); + $this->actAs($timestampable0); + $this->actAs($unknownactas0); + $this->actAs($gh110_template0); + } +} \ No newline at end of file diff --git a/tests/Ticket/gh110TestCase.php b/tests/Ticket/gh110TestCase.php new file mode 100644 index 000000000..65c15b0eb --- /dev/null +++ b/tests/Ticket/gh110TestCase.php @@ -0,0 +1,76 @@ +buildDefinition( + array( + 'className' => 'Ticket_gh110_TestRecord', + 'topLevelClassName' => 'Ticket_gh110_TestRecord', + 'is_base_class' => true, + 'columns' => array( + 'id' => array( + 'type' => 'integer', + 'length' => 4, + ), + 'my_custom_created_at' => array( + 'name' => 'created_at', + 'type' => 'my_custom_type', + 'length' => '', + ) + ), + 'actAs' => array( + 'SoftDelete' => array(), + 'Timestampable' => array( + 'updated' => array( + 'disabled' => true, + ), + 'unknown_column' => array() + ), + 'UnknownActAs' => array(), + // This template brings an already defined column + 'gh110_Template' => array(), + ) + ) + ); + + // We must replace the version as there is a timestamp in it. + $class = preg_replace('/^.*@version.*$/m', ' * @version {{REPLACED}}', $class); + // Can be used to update the snapshot. + //file_put_contents(dirname(__FILE__) . '/gh110/Ticket_gh110_TestRecord.snapshot', $class); + $this->assertEqual($class, file_get_contents(dirname(__FILE__) . '/gh110/Ticket_gh110_TestRecord.snapshot')); + } +} + +class Doctrine_Template_gh110_Template extends Doctrine_Template +{ + protected $_options = array( + 'created' => array( + 'name' => 'created_at', + 'alias' => null, + 'type' => 'timestamp', + 'format' => 'Y-m-d H:i:s', + 'disabled' => false, + 'expression' => false, + 'options' => array('notnull' => true) + ) + ); + + /** + * Set table definition for Timestampable behavior + * + * @return void + */ + public function setTableDefinition() + { + if ( ! $this->_options['created']['disabled']) { + $name = $this->_options['created']['name']; + if ($this->_options['created']['alias']) { + $name .= ' as ' . $this->_options['created']['alias']; + } + $this->hasColumn($name, $this->_options['created']['type'], null, $this->_options['created']['options']); + } + } +}