/library/Doctrine/Doctrine/Import/Schema.php
PHP | 748 lines | 440 code | 88 blank | 220 comment | 97 complexity | e3f7ee85c9a8262611320e543cf29815 MD5 | raw file
- <?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.phpdoctrine.org>.
- */
- /**
- * Doctrine_Import_Schema
- *
- * Class for importing Doctrine_Record classes from a yaml schema definition
- *
- * @package Doctrine
- * @subpackage Import
- * @link www.phpdoctrine.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
- {
- /**
- * _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,
- 'baseClassesPrefix' => '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'),
- 'column' => array('name',
- 'format',
- 'fixed',
- 'primary',
- 'autoincrement',
- 'type',
- 'length',
- 'size',
- 'default',
- 'scale',
- 'values',
- 'comment',
- 'sequence',
- 'protected',
- 'zerofill',
- 'owner',
- 'extra'),
- 'relation' => array('key',
- 'class',
- 'alias',
- 'type',
- 'refClass',
- 'local',
- 'foreign',
- 'foreignClass',
- 'foreignAlias',
- 'foreignType',
- 'autoComplete',
- 'cascade',
- 'onDelete',
- 'onUpdate',
- 'equal',
- 'owningSide',
- 'refClassRelationAlias'),
- 'inheritance'=> array('type',
- 'extends',
- 'keyField',
- 'keyValue'));
- /**
- * _validators
- *
- * Array of available validators
- *
- * @see getValidators()
- * @var array Array of available validators
- */
- protected $_validators = array();
- /**
- * getValidators
- *
- * Retrieve the array of available validators
- *
- * @return array
- */
- public function getValidators()
- {
- if (empty($this->_validators)) {
- $this->_validators = Doctrine_Lib::getValidators();
- }
- return $this->_validators;
- }
- /**
- * 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)) {
- $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())
- {
- $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);
- // Go through the schema and look for global values so we can assign them to each table/class
- $globals = array();
- $globalKeys = array('connection',
- 'attributes',
- 'templates',
- 'actAs',
- 'options',
- 'package',
- 'package_custom_path',
- 'inheritance',
- 'detect_relations');
- // Loop over and build up all the global values and remove them from the array
- foreach ($array as $key => $value) {
- if (in_array($key, $globalKeys)) {
- unset($array[$key]);
- $globals[$key] = $value;
- }
- }
- // Apply the globals to each table if it does not have a custom value set already
- foreach ($array as $className => $table) {
- foreach ($globals as $key => $value) {
- if (!isset($array[$className][$key])) {
- $array[$className][$key] = $value;
- }
- }
- }
- $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;
- $colDesc['sequence'] = isset($field['sequence']) ? (string) $field['sequence']:null;
- $colDesc['values'] = isset($field['values']) ? (array) $field['values']:null;
- // Include all the specified and valid validators in the colDesc
- $validators = $this->getValidators();
- foreach ($validators as $validator) {
- if (isset($field[$validator])) {
- $colDesc[$validator] = $field[$validator];
- }
- }
- $columns[(string) $columnName] = $colDesc;
- }
- }
- // Apply the default values
- foreach ($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 configuration
- foreach ($array as $className => $definition) {
- if ( ! empty($array[$className]['inheritance'])) {
- $this->_validateSchemaElement('inheritance', array_keys($definition['inheritance']), $className . '->inheritance');
- // Default inheritance to concrete inheritance
- if ( ! 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 automatically
- if ($array[$className]['inheritance']['type'] == 'column_aggregation') {
- // Set the keyField to 'type' by default
- if ( ! isset($array[$className]['inheritance']['keyField'])) {
- $array[$className]['inheritance']['keyField'] = 'type';
- }
-
- // Set the keyValue to the name of the child class if it does not exist
- if ( ! 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 exist
- if ( ! 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 parent
- if (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 subclasses
- if ($definition['inheritance']['type'] == 'column_aggregation') {
- $array[$parent]['inheritance']['subclasses'][$definition['className']] = array($definition['inheritance']['keyField'] => $definition['inheritance']['keyValue']);
- }
- }
- }
- 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 => id
- foreach ($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 column
- if (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 foreign
- if (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 array
- foreach ($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;
-
- // 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 exist
- if ( ! 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 it
- if ( ! 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 cleaner
- if ($name == 'column') {
- $validators = $this->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 . '"')
- );
- }
- }
- }
- }