Blame | Letzte Änderung | Log anzeigen | RSS feed
<?php/** $Id: Schema.php 1838 2007-06-26 00:58:21Z nicobn $** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.** This software consists of voluntary contributions made by many individuals* and is licensed under the LGPL. For more information, see* <http://www.doctrine-project.org>.*//*** Doctrine_Import_Schema** Class for importing Doctrine_Record classes from a yaml schema definition** @package Doctrine* @subpackage Import* @link www.doctrine-project.org* @license http://www.opensource.org/licenses/lgpl-license.php LGPL* @version $Revision: 1838 $* @author Nicolas Bérard-Nault <nicobn@gmail.com>* @author Jonathan H. Wage <jonwage@gmail.com>*/class Doctrine_Import_Schema{/*** Schema definition keys that can be applied at the global level.** @var array*/protected static $_globalDefinitionKeys = array('connection','attributes','templates','actAs','options','package','package_custom_path','inheritance','detect_relations');/*** _relations** Array of all relationships parsed from all schema files** @var array*/protected $_relations = array();/*** _options** Array of options used to configure the model generation from the parsed schema files* This array is forwarded to Doctrine_Import_Builder** @var array*/protected $_options = array('packagesPrefix' => 'Package','packagesPath' => '','packagesFolderName' => 'packages','suffix' => '.php','generateBaseClasses' => true,'generateTableClasses' => false,'generateAccessors' => false,'baseClassPrefix' => 'Base','baseClassesDirectory' => 'generated','baseClassName' => 'Doctrine_Record');/*** _validation** Array used to validate schema element.* See: _validateSchemaElement** @var array*/protected $_validation = array('root' => array('abstract','connection','className','tableName','connection','relations','columns','indexes','attributes','templates','actAs','options','package','package_custom_path','inheritance','detect_relations','listeners','checks','comment'),'column' => array('name','format','fixed','primary','autoincrement','type','length','size','default','scale','values','comment','sequence','protected','zerofill','owner','extra','comment','charset','collation'),'relation' => array('key','class','alias','type','refClass','local','foreign','foreignClass','foreignAlias','foreignType','autoComplete','cascade','onDelete','onUpdate','equal','owningSide','refClassRelationAlias','foreignKeyName','orderBy'),'inheritance'=> array('type','extends','keyField','keyValue'));/*** Returns an array of definition keys that can be applied at the global level.** @return array*/public static function getGlobalDefinitionKeys(){return self::$_globalDefinitionKeys;}/*** getOption** @param string $name* @return void*/public function getOption($name){if (isset($this->_options[$name])) {return $this->_options[$name];}}/*** getOptions** @return void*/public function getOptions(){return $this->_options;}/*** setOption** @param string $name* @param string $value* @return void*/public function setOption($name, $value){if (isset($this->_options[$name])) {$this->_options[$name] = $value;}}/*** setOptions** @param string $options* @return void*/public function setOptions($options){if ( ! empty($options)) {$this->_options = $options;}}/*** buildSchema** Loop throug directories of schema files and parse them all in to one complete array of schema information** @param string $schema Array of schema files or single schema file. Array of directories with schema files or single directory* @param string $format Format of the files we are parsing and building from* @return array $array*/public function buildSchema($schema, $format){$array = array();foreach ((array) $schema AS $s) {if (is_file($s)) {$e = explode('.', $s);if (end($e) === $format) {$array = array_merge($array, $this->parseSchema($s, $format));}} else if (is_dir($s)) {$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($s),RecursiveIteratorIterator::LEAVES_ONLY);foreach ($it as $file) {$e = explode('.', $file->getFileName());if (end($e) === $format) {$array = array_merge($array, $this->parseSchema($file->getPathName(), $format));}}} else {$array = array_merge($array, $this->parseSchema($s, $format));}}$array = $this->_buildRelationships($array);$array = $this->_processInheritance($array);return $array;}/*** importSchema** A method to import a Schema and translate it into a Doctrine_Record object** @param string $schema The file containing the XML schema* @param string $format Format of the schema file* @param string $directory The directory where the Doctrine_Record class will be written* @param array $models Optional array of models to import** @return void*/public function importSchema($schema, $format = 'yml', $directory = null, $models = array()){$schema = (array) $schema;$builder = new Doctrine_Import_Builder();$builder->setTargetPath($directory);$builder->setOptions($this->getOptions());$array = $this->buildSchema($schema, $format);if (count($array) == 0) {throw new Doctrine_Import_Exception(sprintf('No ' . $format . ' schema found in ' . implode(", ", $schema)));}foreach ($array as $name => $definition) {if ( ! empty($models) && !in_array($definition['className'], $models)) {continue;}$builder->buildRecord($definition);}}/*** parseSchema** A method to parse a Schema and translate it into a property array.* The function returns that property array.** @param string $schema Path to the file containing the schema* @param string $type Format type of the schema we are parsing* @return array $build Built array of schema information*/public function parseSchema($schema, $type){$defaults = array('abstract' => false,'className' => null,'tableName' => null,'connection' => null,'relations' => array(),'indexes' => array(),'attributes' => array(),'templates' => array(),'actAs' => array(),'options' => array(),'package' => null,'inheritance' => array(),'detect_relations' => false);$array = Doctrine_Parser::load($schema, $type);// Loop over and build up all the global values and remove them from the array$globals = array();foreach ($array as $key => $value) {if (in_array($key, self::$_globalDefinitionKeys)) {unset($array[$key]);$globals[$key] = $value;}}// Merge the globals that aren't specifically set to each classforeach ($array as $className => $table) {$array[$className] = Doctrine_Lib::arrayDeepMerge($globals, $array[$className]);}$build = array();foreach ($array as $className => $table) {$table = (array) $table;$this->_validateSchemaElement('root', array_keys($table), $className);$columns = array();$className = isset($table['className']) ? (string) $table['className']:(string) $className;if (isset($table['inheritance']['keyField']) || isset($table['inheritance']['keyValue'])) {$table['inheritance']['type'] = 'column_aggregation';}if (isset($table['tableName']) && $table['tableName']) {$tableName = $table['tableName'];} else {if (isset($table['inheritance']['type']) && ($table['inheritance']['type'] == 'column_aggregation')) {$tableName = null;} else {$tableName = Doctrine_Inflector::tableize($className);}}$connection = isset($table['connection']) ? $table['connection']:'current';$columns = isset($table['columns']) ? $table['columns']:array();if ( ! empty($columns)) {foreach ($columns as $columnName => $field) {// Support short syntax: my_column: integer(4)if ( ! is_array($field)) {$original = $field;$field = array();$field['type'] = $original;}$colDesc = array();if (isset($field['name'])) {$colDesc['name'] = $field['name'];} else {$colDesc['name'] = $columnName;}$this->_validateSchemaElement('column', array_keys($field), $className . '->columns->' . $colDesc['name']);// Support short type(length) syntax: my_column: { type: integer(4) }$e = explode('(', $field['type']);if (isset($e[0]) && isset($e[1])) {$colDesc['type'] = $e[0];$value = substr($e[1], 0, strlen($e[1]) - 1);$e = explode(',', $value);$colDesc['length'] = $e[0];if (isset($e[1]) && $e[1]) {$colDesc['scale'] = $e[1];}} else {$colDesc['type'] = isset($field['type']) ? (string) $field['type']:null;$colDesc['length'] = isset($field['length']) ? (int) $field['length']:null;$colDesc['length'] = isset($field['size']) ? (int) $field['size']:$colDesc['length'];}$colDesc['fixed'] = isset($field['fixed']) ? (int) $field['fixed']:null;$colDesc['primary'] = isset($field['primary']) ? (bool) (isset($field['primary']) && $field['primary']):null;$colDesc['default'] = isset($field['default']) ? $field['default']:null;$colDesc['autoincrement'] = isset($field['autoincrement']) ? (bool) (isset($field['autoincrement']) && $field['autoincrement']):null;if (isset($field['sequence'])) {if (true === $field['sequence']) {$colDesc['sequence'] = $tableName;} else {$colDesc['sequence'] = (string) $field['sequence'];}} else {$colDesc['sequence'] = null;}$colDesc['values'] = isset($field['values']) ? (array) $field['values']:null;// Include all the specified and valid validators in the colDesc$validators = Doctrine_Manager::getInstance()->getValidators();foreach ($validators as $validator) {if (isset($field[$validator])) {$colDesc[$validator] = $field[$validator];}}$columns[(string) $columnName] = $colDesc;}}// Apply the default valuesforeach ($defaults as $key => $defaultValue) {if (isset($table[$key]) && ! isset($build[$className][$key])) {$build[$className][$key] = $table[$key];} else {$build[$className][$key] = isset($build[$className][$key]) ? $build[$className][$key]:$defaultValue;}}$build[$className]['className'] = $className;$build[$className]['tableName'] = $tableName;$build[$className]['columns'] = $columns;// Make sure that anything else that is specified in the schema makes it to the final array$build[$className] = Doctrine_Lib::arrayDeepMerge($table, $build[$className]);// We need to keep track of the className for the connection$build[$className]['connectionClassName'] = $build[$className]['className'];}return $build;}/*** _processInheritance** Perform some processing on inheritance.* Sets the default type and sets some default values for certain types** @param string $array* @return void*/protected function _processInheritance($array){// Apply default inheritance configurationforeach ($array as $className => $definition) {if ( ! empty($array[$className]['inheritance'])) {$this->_validateSchemaElement('inheritance', array_keys($definition['inheritance']), $className . '->inheritance');// Default inheritance to concrete inheritanceif ( ! isset($array[$className]['inheritance']['type'])) {$array[$className]['inheritance']['type'] = 'concrete';}// Some magic for setting up the keyField and keyValue column aggregation options// Adds keyField to the parent class automaticallyif ($array[$className]['inheritance']['type'] == 'column_aggregation') {// Set the keyField to 'type' by defaultif ( ! isset($array[$className]['inheritance']['keyField'])) {$array[$className]['inheritance']['keyField'] = 'type';}// Set the keyValue to the name of the child class if it does not existif ( ! isset($array[$className]['inheritance']['keyValue'])) {$array[$className]['inheritance']['keyValue'] = $className;}$parent = $this->_findBaseSuperClass($array, $definition['className']);// Add the keyType column to the parent if a definition does not already existif ( ! isset($array[$parent]['columns'][$array[$className]['inheritance']['keyField']])) {$array[$parent]['columns'][$array[$className]['inheritance']['keyField']] = array('name' => $array[$className]['inheritance']['keyField'], 'type' => 'string', 'length' => 255);}}}}// Array of the array keys to move to the parent, and the value to default the child definition to// after moving it. Will also populate the subclasses array for the inheritance parent$moves = array('columns' => array(),'indexes' => array(),'attributes' => array(),'options' => array(),'checks' => array());foreach ($array as $className => $definition) {// Move any definitions on the schema to the parentif (isset($definition['inheritance']['extends']) && isset($definition['inheritance']['type']) && ($definition['inheritance']['type'] == 'simple' || $definition['inheritance']['type'] == 'column_aggregation')) {$parent = $this->_findBaseSuperClass($array, $definition['className']);foreach ($moves as $move => $resetValue) {if (isset($array[$parent][$move]) && isset($definition[$move])) {$array[$parent][$move] = Doctrine_Lib::arrayDeepMerge($array[$parent][$move], $definition[$move]);$array[$definition['className']][$move] = $resetValue;}}// Populate the parents subclassesif ($definition['inheritance']['type'] == 'column_aggregation') {// Fix for 2015: loop through superclasses' inheritance to the base-superclass to// make sure we collect all keyFields needed (and not only the first)$inheritanceFields = array($definition['inheritance']['keyField'] => $definition['inheritance']['keyValue']);$superClass = $definition['inheritance']['extends'];$multiInheritanceDef = $array[$superClass];while (count($multiInheritanceDef['inheritance']) > 0 && array_key_exists('extends', $multiInheritanceDef['inheritance']) && $multiInheritanceDef['inheritance']['type'] == 'column_aggregation') {$superClass = $multiInheritanceDef['inheritance']['extends'];// keep original keyField with it's keyValueif ( ! isset($inheritanceFields[$multiInheritanceDef['inheritance']['keyField']])) {$inheritanceFields[$multiInheritanceDef['inheritance']['keyField']] = $multiInheritanceDef['inheritance']['keyValue'];}$multiInheritanceDef = $array[$superClass];}$array[$parent]['inheritance']['subclasses'][$definition['className']] = $inheritanceFields;}}}return $array;}/*** Find the base super class for this inheritance child. We need to move all levels of children to the* top most parent.** @param array $array Array of schema information* @return string $class Class to get find the parent for*/protected function _findBaseSuperClass($array, $class){if (isset($array[$class]['inheritance']['extends']) && isset($array[$class]['inheritance']['type']) && ($array[$class]['inheritance']['type'] == 'simple' || $array[$class]['inheritance']['type'] == 'column_aggregation')) {return $this->_findBaseSuperClass($array, $array[$class]['inheritance']['extends']);} else {return $class;}}/*** buildRelationships** Loop through an array of schema information and build all the necessary relationship information* Will attempt to auto complete relationships and simplify the amount of information required* for defining a relationship** @param string $array* @return void*/protected function _buildRelationships($array){// Handle auto detecting relations by the names of columns// User.contact_id will automatically create User hasOne Contact local => contact_id, foreign => idforeach ($array as $className => $properties) {if (isset($properties['columns']) && ! empty($properties['columns']) && isset($properties['detect_relations']) && $properties['detect_relations']) {foreach ($properties['columns'] as $column) {// Check if the column we are inflecting has a _id on the end of it before trying to inflect it and find// the class name for the columnif (strpos($column['name'], '_id')) {$columnClassName = Doctrine_Inflector::classify(str_replace('_id', '', $column['name']));if (isset($array[$columnClassName]) && !isset($array[$className]['relations'][$columnClassName])) {$array[$className]['relations'][$columnClassName] = array();// Set the detected foreign key type and length to the same as the primary key// of the related table$type = isset($array[$columnClassName]['columns']['id']['type']) ? $array[$columnClassName]['columns']['id']['type']:'integer';$length = isset($array[$columnClassName]['columns']['id']['length']) ? $array[$columnClassName]['columns']['id']['length']:8;$array[$className]['columns'][$column['name']]['type'] = $type;$array[$className]['columns'][$column['name']]['length'] = $length;}}}}}foreach ($array as $name => $properties) {if ( ! isset($properties['relations'])) {continue;}$className = $properties['className'];$relations = $properties['relations'];foreach ($relations as $alias => $relation) {$class = isset($relation['class']) ? $relation['class']:$alias;if ( ! isset($array[$class])) {continue;}$relation['class'] = $class;$relation['alias'] = isset($relation['alias']) ? $relation['alias'] : $alias;// Attempt to guess the local and foreignif (isset($relation['refClass'])) {$relation['local'] = isset($relation['local']) ? $relation['local']:Doctrine_Inflector::tableize($name) . '_id';$relation['foreign'] = isset($relation['foreign']) ? $relation['foreign']:Doctrine_Inflector::tableize($class) . '_id';} else {$relation['local'] = isset($relation['local']) ? $relation['local']:Doctrine_Inflector::tableize($relation['class']) . '_id';$relation['foreign'] = isset($relation['foreign']) ? $relation['foreign']:'id';}if (isset($relation['refClass'])) {$relation['type'] = 'many';}if (isset($relation['type']) && $relation['type']) {$relation['type'] = $relation['type'] === 'one' ? Doctrine_Relation::ONE:Doctrine_Relation::MANY;} else {$relation['type'] = Doctrine_Relation::ONE;}if (isset($relation['foreignType']) && $relation['foreignType']) {$relation['foreignType'] = $relation['foreignType'] === 'one' ? Doctrine_Relation::ONE:Doctrine_Relation::MANY;}$relation['key'] = $this->_buildUniqueRelationKey($relation);$this->_validateSchemaElement('relation', array_keys($relation), $className . '->relation->' . $relation['alias']);$this->_relations[$className][$alias] = $relation;}}// Now we auto-complete opposite ends of relationships$this->_autoCompleteOppositeRelations();// Make sure we do not have any duplicate relations$this->_fixDuplicateRelations();// Set the full array of relationships for each class to the final arrayforeach ($this->_relations as $className => $relations) {$array[$className]['relations'] = $relations;}return $array;}/*** fixRelationships** Loop through all relationships building the opposite ends of each relationship* and make sure no duplicate relations exist** @return void*/protected function _autoCompleteOppositeRelations(){foreach($this->_relations as $className => $relations) {foreach ($relations AS $alias => $relation) {if ((isset($relation['equal']) && $relation['equal']) || (isset($relation['autoComplete']) && $relation['autoComplete'] === false)) {continue;}$newRelation = array();$newRelation['foreign'] = $relation['local'];$newRelation['local'] = $relation['foreign'];$newRelation['class'] = isset($relation['foreignClass']) ? $relation['foreignClass']:$className;$newRelation['alias'] = isset($relation['foreignAlias']) ? $relation['foreignAlias']:$className;$newRelation['foreignAlias'] = $alias;// this is so that we know that this relation was autogenerated and// that we do not need to include it if it is explicitly declared in the schema by the users.$newRelation['autogenerated'] = true;if (isset($relation['refClass'])) {$newRelation['refClass'] = $relation['refClass'];$newRelation['type'] = isset($relation['foreignType']) ? $relation['foreignType']:$relation['type'];} else {if (isset($relation['foreignType'])) {$newRelation['type'] = $relation['foreignType'];} else {$newRelation['type'] = $relation['type'] === Doctrine_Relation::ONE ? Doctrine_Relation::MANY:Doctrine_Relation::ONE;}}// Make sure it doesn't already existif ( ! isset($this->_relations[$relation['class']][$newRelation['alias']])) {$newRelation['key'] = $this->_buildUniqueRelationKey($newRelation);$this->_relations[$relation['class']][$newRelation['alias']] = $newRelation;}}}}/*** _fixDuplicateRelations** Ensure the relations for each class are unique and that no duplicated relations exist from the auto generated relations* and the user explicitely defining the opposite end** @return void*/protected function _fixDuplicateRelations(){foreach($this->_relations as $className => $relations) {// This is for checking for duplicates between alias-relations and a auto-generated relations to ensure the result set of unique relations$existingRelations = array();$uniqueRelations = array();foreach ($relations as $relation) {if ( ! in_array($relation['key'], $existingRelations)) {$existingRelations[] = $relation['key'];$uniqueRelations = array_merge($uniqueRelations, array($relation['alias'] => $relation));} else {// check to see if this relationship is not autogenerated, if it's not, then the user must have explicitly declared itif ( ! isset($relation['autogenerated']) || $relation['autogenerated'] != true) {$uniqueRelations = array_merge($uniqueRelations, array($relation['alias'] => $relation));}}}$this->_relations[$className] = $uniqueRelations;}}/*** _buildUniqueRelationKey** Build a unique key to identify a relationship by* Md5 hash of all the relationship parameters** @param string $relation* @return void*/protected function _buildUniqueRelationKey($relation){return md5($relation['local'].$relation['foreign'].$relation['class'].(isset($relation['refClass']) ? $relation['refClass']:null));}/*** _validateSchemaElement** @param string $name* @param string $value* @return void*/protected function _validateSchemaElement($name, $element, $path){$element = (array) $element;$validation = $this->_validation[$name];// Validators are a part of the column validation// This should be fixed, made cleanerif ($name == 'column') {$validators = Doctrine_Manager::getInstance()->getValidators();$validation = array_merge($validation, $validators);}$validation = array_flip($validation);foreach ($element as $key => $value) {if ( ! isset($validation[$value])) {throw new Doctrine_Import_Exception(sprintf('Invalid schema element named "' . $value . '" at path "' . $path . '"'));}}}}