1<?php 2/* 3 * $Id$ 4 * 5 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 6 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 7 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 8 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 9 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 10 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 11 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 12 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 13 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 14 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 15 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 16 * 17 * This software consists of voluntary contributions made by many individuals 18 * and is licensed under the LGPL. For more information, see 19 * <http://www.doctrine-project.org>. 20 */ 21 22/** 23 * Doctrine_Relation_Parser 24 * 25 * @package Doctrine 26 * @subpackage Relation 27 * @author Konsta Vesterinen <kvesteri@cc.hut.fi> 28 * @license http://www.opensource.org/licenses/lgpl-license.php LGPL 29 * @version $Revision$ 30 * @link www.doctrine-project.org 31 * @since 1.0 32 * @todo Composite key support? 33 */ 34class Doctrine_Relation_Parser 35{ 36 /** 37 * @var Doctrine_Table $_table the table object this parser belongs to 38 */ 39 protected $_table; 40 41 /** 42 * @var array $_relations an array containing all the Doctrine_Relation objects for this table 43 */ 44 protected $_relations = array(); 45 46 /** 47 * @var array $_pending relations waiting for parsing 48 */ 49 protected $_pending = array(); 50 51 /** 52 * constructor 53 * 54 * @param Doctrine_Table $table the table object this parser belongs to 55 */ 56 public function __construct(Doctrine_Table $table) 57 { 58 $this->_table = $table; 59 } 60 61 /** 62 * getTable 63 * 64 * @return Doctrine_Table the table object this parser belongs to 65 */ 66 public function getTable() 67 { 68 return $this->_table; 69 } 70 71 /** 72 * getPendingRelation 73 * 74 * @return array an array defining a pending relation 75 */ 76 public function getPendingRelation($name) 77 { 78 if ( ! isset($this->_pending[$name])) { 79 throw new Doctrine_Relation_Exception('Unknown pending relation ' . $name); 80 } 81 82 return $this->_pending[$name]; 83 } 84 85 /** 86 * getPendingRelations 87 * 88 * @return array an array containing all the pending relations 89 */ 90 public function getPendingRelations() 91 { 92 return $this->_pending; 93 } 94 95 /** 96 * unsetPendingRelations 97 * Removes a relation. Warning: this only affects pending relations 98 * 99 * @param string relation to remove 100 */ 101 public function unsetPendingRelations($name) 102 { 103 unset($this->_pending[$name]); 104 } 105 106 /** 107 * Check if a relation alias exists 108 * 109 * @param string $name 110 * @return boolean $bool 111 */ 112 public function hasRelation($name) 113 { 114 if ( ! isset($this->_pending[$name]) && ! isset($this->_relations[$name])) { 115 return false; 116 } 117 118 return true; 119 } 120 121 /** 122 * binds a relation 123 * 124 * @param string $name 125 * @param string $field 126 * @return void 127 */ 128 public function bind($name, $options = array()) 129 { 130 $e = explode(' as ', $name); 131 foreach ($e as &$v) { 132 $v = trim($v); 133 } 134 $name = $e[0]; 135 $alias = isset($e[1]) ? $e[1] : $name; 136 137 if ( ! isset($options['type'])) { 138 throw new Doctrine_Relation_Exception('Relation type not set.'); 139 } 140 141 if ($this->hasRelation($alias)) { 142 unset($this->_relations[$alias]); 143 unset($this->_pending[$alias]); 144 } 145 146 $this->_pending[$alias] = array_merge($options, array('class' => $name, 'alias' => $alias)); 147 148 return $this->_pending[$alias]; 149 } 150 151 /** 152 * getRelation 153 * 154 * @param string $alias relation alias 155 */ 156 public function getRelation($alias, $recursive = true) 157 { 158 if (isset($this->_relations[$alias])) { 159 return $this->_relations[$alias]; 160 } 161 162 if (isset($this->_pending[$alias])) { 163 $def = $this->_pending[$alias]; 164 $identifierColumnNames = $this->_table->getIdentifierColumnNames(); 165 $idColumnName = array_pop($identifierColumnNames); 166 167 // check if reference class name exists 168 // if it does we are dealing with association relation 169 if (isset($def['refClass'])) { 170 $def = $this->completeAssocDefinition($def); 171 $localClasses = array_merge($this->_table->getOption('parents'), array($this->_table->getComponentName())); 172 173 $backRefRelationName = isset($def['refClassRelationAlias']) ? 174 $def['refClassRelationAlias'] : $def['refClass']; 175 if ( ! isset($this->_pending[$backRefRelationName]) && ! isset($this->_relations[$backRefRelationName])) { 176 177 $parser = $def['refTable']->getRelationParser(); 178 179 if ( ! $parser->hasRelation($this->_table->getComponentName())) { 180 $parser->bind($this->_table->getComponentName(), 181 array('type' => Doctrine_Relation::ONE, 182 'local' => $def['local'], 183 'foreign' => $idColumnName, 184 'localKey' => true, 185 )); 186 } 187 188 if ( ! $this->hasRelation($backRefRelationName)) { 189 if (in_array($def['class'], $localClasses)) { 190 $this->bind($def['refClass'] . " as " . $backRefRelationName, array( 191 'type' => Doctrine_Relation::MANY, 192 'foreign' => $def['foreign'], 193 'local' => $idColumnName)); 194 } else { 195 $this->bind($def['refClass'] . " as " . $backRefRelationName, array( 196 'type' => Doctrine_Relation::MANY, 197 'foreign' => $def['local'], 198 'local' => $idColumnName)); 199 } 200 } 201 } 202 if (in_array($def['class'], $localClasses)) { 203 $rel = new Doctrine_Relation_Nest($def); 204 } else { 205 $rel = new Doctrine_Relation_Association($def); 206 } 207 } else { 208 // simple foreign key relation 209 $def = $this->completeDefinition($def); 210 211 if (isset($def['localKey']) && $def['localKey']) { 212 $rel = new Doctrine_Relation_LocalKey($def); 213 214 // Automatically index for foreign keys 215 $foreign = (array) $def['foreign']; 216 217 foreach ($foreign as $fk) { 218 // Check if its already not indexed (primary key) 219 if ( ! $rel['table']->isIdentifier($rel['table']->getFieldName($fk))) { 220 $rel['table']->addIndex($fk, array('fields' => array($fk))); 221 } 222 } 223 } else { 224 $rel = new Doctrine_Relation_ForeignKey($def); 225 } 226 } 227 if (isset($rel)) { 228 // unset pending relation 229 unset($this->_pending[$alias]); 230 $this->_relations[$alias] = $rel; 231 return $rel; 232 } 233 } 234 if ($recursive) { 235 $this->getRelations(); 236 237 return $this->getRelation($alias, false); 238 } else { 239 throw new Doctrine_Table_Exception('Unknown relation alias ' . $alias); 240 } 241 } 242 243 /** 244 * getRelations 245 * returns an array containing all relation objects 246 * 247 * @return array an array of Doctrine_Relation objects 248 */ 249 public function getRelations() 250 { 251 foreach ($this->_pending as $k => $v) { 252 $this->getRelation($k); 253 } 254 255 return $this->_relations; 256 } 257 258 /** 259 * getImpl 260 * returns the table class of the concrete implementation for given template 261 * if the given template is not a template then this method just returns the 262 * table class for the given record 263 * 264 * @param string $template 265 */ 266 public function getImpl($template) 267 { 268 $conn = $this->_table->getConnection(); 269 270 if (class_exists($template) && in_array('Doctrine_Template', class_parents($template))) { 271 $impl = $this->_table->getImpl($template); 272 273 if ($impl === null) { 274 throw new Doctrine_Relation_Parser_Exception("Couldn't find concrete implementation for template " . $template); 275 } 276 } else { 277 $impl = $template; 278 } 279 280 return $conn->getTable($impl); 281 } 282 283 /** 284 * Completes the given association definition 285 * 286 * @param array $def definition array to be completed 287 * @return array completed definition array 288 */ 289 public function completeAssocDefinition($def) 290 { 291 $conn = $this->_table->getConnection(); 292 $def['table'] = $this->getImpl($def['class']); 293 $def['localTable'] = $this->_table; 294 $def['class'] = $def['table']->getComponentName(); 295 $def['refTable'] = $this->getImpl($def['refClass']); 296 297 $id = $def['refTable']->getIdentifierColumnNames(); 298 299 if (count($id) > 1) { 300 if ( ! isset($def['foreign'])) { 301 // foreign key not set 302 // try to guess the foreign key 303 304 $def['foreign'] = ($def['local'] === $id[0]) ? $id[1] : $id[0]; 305 } 306 if ( ! isset($def['local'])) { 307 // foreign key not set 308 // try to guess the foreign key 309 $def['local'] = ($def['foreign'] === $id[0]) ? $id[1] : $id[0]; 310 } 311 } else { 312 313 if ( ! isset($def['foreign'])) { 314 // foreign key not set 315 // try to guess the foreign key 316 317 $columns = $this->getIdentifiers($def['table']); 318 319 $def['foreign'] = $columns; 320 } 321 if ( ! isset($def['local'])) { 322 // local key not set 323 // try to guess the local key 324 $columns = $this->getIdentifiers($this->_table); 325 326 $def['local'] = $columns; 327 } 328 } 329 return $def; 330 } 331 332 /** 333 * getIdentifiers 334 * gives a list of identifiers from given table 335 * 336 * the identifiers are in format: 337 * [componentName].[identifier] 338 * 339 * @param Doctrine_Table $table table object to retrieve identifiers from 340 */ 341 public function getIdentifiers(Doctrine_Table $table) 342 { 343 $componentNameToLower = strtolower($table->getComponentName()); 344 if (is_array($table->getIdentifier())) { 345 $columns = array(); 346 foreach ((array) $table->getIdentifierColumnNames() as $identColName) { 347 $columns[] = $componentNameToLower . '_' . $identColName; 348 } 349 } else { 350 $columns = $componentNameToLower . '_' . $table->getColumnName( 351 $table->getIdentifier()); 352 } 353 354 return $columns; 355 } 356 357 /** 358 * guessColumns 359 * 360 * @param array $classes an array of class names 361 * @param Doctrine_Table $foreignTable foreign table object 362 * @return array an array of column names 363 */ 364 public function guessColumns(array $classes, Doctrine_Table $foreignTable) 365 { 366 $conn = $this->_table->getConnection(); 367 368 foreach ($classes as $class) { 369 try { 370 $table = $conn->getTable($class); 371 } catch (Doctrine_Table_Exception $e) { 372 continue; 373 } 374 $columns = $this->getIdentifiers($table); 375 $found = true; 376 377 foreach ((array) $columns as $column) { 378 if ( ! $foreignTable->hasColumn($column)) { 379 $found = false; 380 break; 381 } 382 } 383 if ($found) { 384 break; 385 } 386 } 387 388 if ( ! $found) { 389 throw new Doctrine_Relation_Exception("Couldn't find columns."); 390 } 391 392 return $columns; 393 } 394 395 /** 396 * Completes the given definition 397 * 398 * @param array $def definition array to be completed 399 * @return array completed definition array 400 * @todo Description: What does it mean to complete a definition? What is done (not how)? 401 * Refactor (too long & nesting level) 402 */ 403 public function completeDefinition($def) 404 { 405 $conn = $this->_table->getConnection(); 406 $def['table'] = $this->getImpl($def['class']); 407 $def['localTable'] = $this->_table; 408 $def['class'] = $def['table']->getComponentName(); 409 410 $foreignClasses = array_merge($def['table']->getOption('parents'), array($def['class'])); 411 $localClasses = array_merge($this->_table->getOption('parents'), array($this->_table->getComponentName())); 412 413 $localIdentifierColumnNames = $this->_table->getIdentifierColumnNames(); 414 $localIdentifierCount = count($localIdentifierColumnNames); 415 $localIdColumnName = array_pop($localIdentifierColumnNames); 416 $foreignIdentifierColumnNames = $def['table']->getIdentifierColumnNames(); 417 $foreignIdColumnName = array_pop($foreignIdentifierColumnNames); 418 419 if (isset($def['local'])) { 420 $def['local'] = $def['localTable']->getColumnName($def['local']); 421 422 if ( ! isset($def['foreign'])) { 423 // local key is set, but foreign key is not 424 // try to guess the foreign key 425 426 if ($def['local'] === $localIdColumnName) { 427 $def['foreign'] = $this->guessColumns($localClasses, $def['table']); 428 } else { 429 // the foreign field is likely to be the 430 // identifier of the foreign class 431 $def['foreign'] = $foreignIdColumnName; 432 $def['localKey'] = true; 433 } 434 } else { 435 $def['foreign'] = $def['table']->getColumnName($def['foreign']); 436 437 if ($localIdentifierCount == 1) { 438 if ($def['local'] == $localIdColumnName && isset($def['owningSide']) 439 && $def['owningSide'] === true) { 440 $def['localKey'] = true; 441 } else if (($def['local'] !== $localIdColumnName && $def['type'] == Doctrine_Relation::ONE)) { 442 $def['localKey'] = true; 443 } 444 } else if ($localIdentifierCount > 1 && ! isset($def['localKey'])) { 445 // It's a composite key and since 'foreign' can not point to a composite 446 // key currently, we know that 'local' must be the foreign key. 447 $def['localKey'] = true; 448 } 449 } 450 } else { 451 if (isset($def['foreign'])) { 452 $def['foreign'] = $def['table']->getColumnName($def['foreign']); 453 454 // local key not set, but foreign key is set 455 // try to guess the local key 456 if ($def['foreign'] === $foreignIdColumnName) { 457 $def['localKey'] = true; 458 try { 459 $def['local'] = $this->guessColumns($foreignClasses, $this->_table); 460 } catch (Doctrine_Relation_Exception $e) { 461 $def['local'] = $localIdColumnName; 462 } 463 } else { 464 $def['local'] = $localIdColumnName; 465 } 466 } else { 467 // neither local or foreign key is being set 468 // try to guess both keys 469 470 $conn = $this->_table->getConnection(); 471 472 // the following loops are needed for covering inheritance 473 foreach ($localClasses as $class) { 474 $table = $conn->getTable($class); 475 $identifierColumnNames = $table->getIdentifierColumnNames(); 476 $idColumnName = array_pop($identifierColumnNames); 477 $column = strtolower($table->getComponentName()) 478 . '_' . $idColumnName; 479 480 foreach ($foreignClasses as $class2) { 481 $table2 = $conn->getTable($class2); 482 if ($table2->hasColumn($column)) { 483 $def['foreign'] = $column; 484 $def['local'] = $idColumnName; 485 return $def; 486 } 487 } 488 } 489 490 foreach ($foreignClasses as $class) { 491 $table = $conn->getTable($class); 492 $identifierColumnNames = $table->getIdentifierColumnNames(); 493 $idColumnName = array_pop($identifierColumnNames); 494 $column = strtolower($table->getComponentName()) 495 . '_' . $idColumnName; 496 497 foreach ($localClasses as $class2) { 498 $table2 = $conn->getTable($class2); 499 if ($table2->hasColumn($column)) { 500 $def['foreign'] = $idColumnName; 501 $def['local'] = $column; 502 $def['localKey'] = true; 503 return $def; 504 } 505 } 506 } 507 508 // auto-add columns and auto-build relation 509 $columns = array(); 510 foreach ((array) $this->_table->getIdentifierColumnNames() as $id) { 511 // ?? should this not be $this->_table->getComponentName() ?? 512 $column = strtolower($table->getComponentName()) 513 . '_' . $id; 514 515 $col = $this->_table->getColumnDefinition($id); 516 $type = $col['type']; 517 $length = $col['length']; 518 519 unset($col['type']); 520 unset($col['length']); 521 unset($col['autoincrement']); 522 unset($col['sequence']); 523 unset($col['primary']); 524 525 $def['table']->setColumn($column, $type, $length, $col); 526 527 $columns[] = $column; 528 } 529 if (count($columns) > 1) { 530 $def['foreign'] = $columns; 531 } else { 532 $def['foreign'] = $columns[0]; 533 } 534 $def['local'] = $localIdColumnName; 535 } 536 } 537 return $def; 538 } 539} 540