| 1 | <?php |
| 2 | /* |
| 3 | * $Id: Table.php 1397 2007-05-19 19:54:15Z zYne $ |
| 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.phpdoctrine.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: 1397 $ |
| 30 | * @link www.phpdoctrine.org |
| 31 | * @since 1.0 |
| 32 | * @todo Composite key support? |
| 33 | */ |
| 34 | class 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 | public function hasRelation($name) |
| 86 | { |
| 87 | if ( ! isset($this->_pending[$name]) && ! isset($this->_relations[$name])) { |
| 88 | return false; |
| 89 | } |
| 90 | |
| 91 | return true; |
| 92 | } |
| 93 | |
| 94 | /** |
| 95 | * binds a relation |
| 96 | * |
| 97 | * @param string $name |
| 98 | * @param string $field |
| 99 | * @return void |
| 100 | */ |
| 101 | public function bind($name, $options = array()) |
| 102 | { |
| 103 | if (isset($this->relations[$name])) { |
| 104 | unset($this->relations[$name]); |
| 105 | } |
| 106 | |
| 107 | /* looks like old code? |
| 108 | $lower = strtolower($name); |
| 109 | if ($this->_table->hasColumn($lower)) { |
| 110 | throw new Doctrine_Relation_Exception("Couldn't bind relation. Column with name " . $lower . ' already exists!'); |
| 111 | } |
| 112 | */ |
| 113 | |
| 114 | $e = explode(' as ', $name); |
| 115 | $name = $e[0]; |
| 116 | $alias = isset($e[1]) ? $e[1] : $name; |
| 117 | |
| 118 | if ( ! isset($options['type'])) { |
| 119 | throw new Doctrine_Relation_Exception('Relation type not set.'); |
| 120 | } |
| 121 | |
| 122 | $this->_pending[$alias] = array_merge($options, array('class' => $name, 'alias' => $alias)); |
| 123 | /** |
| 124 | $m = Doctrine_Manager::getInstance(); |
| 125 | |
| 126 | if (isset($options['onDelete'])) { |
| 127 | $m->addDeleteAction($name, $this->_table->getComponentName(), $options['onDelete']); |
| 128 | } |
| 129 | if (isset($options['onUpdate'])) { |
| 130 | $m->addUpdateAction($name, $this->_table->getComponentName(), $options['onUpdate']); |
| 131 | } |
| 132 | */ |
| 133 | |
| 134 | return $this->_pending[$alias]; |
| 135 | } |
| 136 | |
| 137 | /** |
| 138 | * getRelation |
| 139 | * |
| 140 | * @param string $alias relation alias |
| 141 | */ |
| 142 | public function getRelation($alias, $recursive = true) |
| 143 | { |
| 144 | if (isset($this->_relations[$alias])) { |
| 145 | return $this->_relations[$alias]; |
| 146 | } |
| 147 | |
| 148 | if (isset($this->_pending[$alias])) { |
| 149 | $def = $this->_pending[$alias]; |
| 150 | $identifierColumnNames = $this->_table->getIdentifierColumnNames(); |
| 151 | $idColumnName = array_pop($identifierColumnNames); |
| 152 | |
| 153 | // check if reference class name exists |
| 154 | // if it does we are dealing with association relation |
| 155 | if (isset($def['refClass'])) { |
| 156 | $def = $this->completeAssocDefinition($def); |
| 157 | $localClasses = array_merge($this->_table->getOption('parents'), array($this->_table->getComponentName())); |
| 158 | |
| 159 | if ( ! isset($this->_pending[$def['refClass']]) && |
| 160 | ! isset($this->_relations[$def['refClass']])) { |
| 161 | |
| 162 | $parser = $def['refTable']->getRelationParser(); |
| 163 | if ( ! $parser->hasRelation($this->_table->getComponentName())) { |
| 164 | $parser->bind($this->_table->getComponentName(), |
| 165 | array('type' => Doctrine_Relation::ONE, |
| 166 | 'local' => $def['local'], |
| 167 | 'foreign' => $idColumnName, |
| 168 | 'localKey' => true, |
| 169 | )); |
| 170 | } |
| 171 | |
| 172 | if ( ! $this->hasRelation($def['refClass'])) { |
| 173 | $this->bind($def['refClass'], array('type' => Doctrine_Relation::MANY, |
| 174 | 'foreign' => $def['local'], |
| 175 | 'local' => $idColumnName)); |
| 176 | } |
| 177 | } |
| 178 | if (in_array($def['class'], $localClasses)) { |
| 179 | $rel = new Doctrine_Relation_Nest($def); |
| 180 | } else { |
| 181 | $rel = new Doctrine_Relation_Association($def); |
| 182 | } |
| 183 | } else { |
| 184 | // simple foreign key relation |
| 185 | $def = $this->completeDefinition($def); |
| 186 | |
| 187 | if (isset($def['localKey'])) { |
| 188 | $rel = new Doctrine_Relation_LocalKey($def); |
| 189 | } else { |
| 190 | $rel = new Doctrine_Relation_ForeignKey($def); |
| 191 | } |
| 192 | } |
| 193 | if (isset($rel)) { |
| 194 | // unset pending relation |
| 195 | unset($this->_pending[$alias]); |
| 196 | |
| 197 | $this->_relations[$alias] = $rel; |
| 198 | return $rel; |
| 199 | } |
| 200 | } |
| 201 | if ($recursive) { |
| 202 | $this->getRelations(); |
| 203 | |
| 204 | return $this->getRelation($alias, false); |
| 205 | } else { |
| 206 | throw new Doctrine_Table_Exception('Unknown relation alias ' . $alias); |
| 207 | } |
| 208 | } |
| 209 | |
| 210 | /** |
| 211 | * getRelations |
| 212 | * returns an array containing all relation objects |
| 213 | * |
| 214 | * @return array an array of Doctrine_Relation objects |
| 215 | */ |
| 216 | public function getRelations() |
| 217 | { |
| 218 | foreach ($this->_pending as $k => $v) { |
| 219 | $this->getRelation($k); |
| 220 | } |
| 221 | |
| 222 | return $this->_relations; |
| 223 | } |
| 224 | |
| 225 | /** |
| 226 | * getImpl |
| 227 | * returns the table class of the concrete implementation for given template |
| 228 | * if the given template is not a template then this method just returns the |
| 229 | * table class for the given record |
| 230 | * |
| 231 | * @param string $template |
| 232 | */ |
| 233 | public function getImpl($template) |
| 234 | { |
| 235 | $conn = $this->_table->getConnection(); |
| 236 | |
| 237 | if (in_array('Doctrine_Template', class_parents($template))) { |
| 238 | $impl = $this->_table->getImpl($template); |
| 239 | |
| 240 | if ($impl === null) { |
| 241 | throw new Doctrine_Relation_Parser_Exception("Couldn't find concrete implementation for template " . $template); |
| 242 | } |
| 243 | } else { |
| 244 | $impl = $template; |
| 245 | } |
| 246 | |
| 247 | return $conn->getTable($impl); |
| 248 | } |
| 249 | |
| 250 | /** |
| 251 | * Completes the given association definition |
| 252 | * |
| 253 | * @param array $def definition array to be completed |
| 254 | * @return array completed definition array |
| 255 | */ |
| 256 | public function completeAssocDefinition($def) |
| 257 | { |
| 258 | $conn = $this->_table->getConnection(); |
| 259 | $def['table'] = $this->getImpl($def['class']); |
| 260 | $def['class'] = $def['table']->getComponentName(); |
| 261 | $def['refTable'] = $this->getImpl($def['refClass']); |
| 262 | |
| 263 | $id = $def['refTable']->getIdentifierColumnNames(); |
| 264 | |
| 265 | if (count($id) > 1) { |
| 266 | if ( ! isset($def['foreign'])) { |
| 267 | // foreign key not set |
| 268 | // try to guess the foreign key |
| 269 | |
| 270 | $def['foreign'] = ($def['local'] === $id[0]) ? $id[1] : $id[0]; |
| 271 | } |
| 272 | if ( ! isset($def['local'])) { |
| 273 | // foreign key not set |
| 274 | // try to guess the foreign key |
| 275 | |
| 276 | $def['local'] = ($def['foreign'] === $id[0]) ? $id[1] : $id[0]; |
| 277 | } |
| 278 | } else { |
| 279 | |
| 280 | if ( ! isset($def['foreign'])) { |
| 281 | // foreign key not set |
| 282 | // try to guess the foreign key |
| 283 | |
| 284 | $columns = $this->getIdentifiers($def['table']); |
| 285 | |
| 286 | $def['foreign'] = $columns; |
| 287 | } |
| 288 | if ( ! isset($def['local'])) { |
| 289 | // local key not set |
| 290 | // try to guess the local key |
| 291 | $columns = $this->getIdentifiers($this->_table); |
| 292 | |
| 293 | $def['local'] = $columns; |
| 294 | } |
| 295 | } |
| 296 | return $def; |
| 297 | } |
| 298 | |
| 299 | /** |
| 300 | * getIdentifiers |
| 301 | * gives a list of identifiers from given table |
| 302 | * |
| 303 | * the identifiers are in format: |
| 304 | * [componentName].[identifier] |
| 305 | * |
| 306 | * @param Doctrine_Table $table table object to retrieve identifiers from |
| 307 | */ |
| 308 | public function getIdentifiers(Doctrine_Table $table) |
| 309 | { |
| 310 | $componentNameToLower = strtolower($table->getComponentName()); |
| 311 | if (is_array($table->getIdentifier())) { |
| 312 | $columns = array(); |
| 313 | foreach ((array) $table->getIdentifierColumnNames() as $identColName) { |
| 314 | $columns[] = $componentNameToLower . '_' . $identColName; |
| 315 | } |
| 316 | } else { |
| 317 | $columns = $componentNameToLower . '_' . $table->getColumnName( |
| 318 | $table->getIdentifier()); |
| 319 | } |
| 320 | |
| 321 | return $columns; |
| 322 | } |
| 323 | |
| 324 | /** |
| 325 | * guessColumns |
| 326 | * |
| 327 | * @param array $classes an array of class names |
| 328 | * @param Doctrine_Table $foreignTable foreign table object |
| 329 | * @return array an array of column names |
| 330 | */ |
| 331 | public function guessColumns(array $classes, Doctrine_Table $foreignTable) |
| 332 | { |
| 333 | $conn = $this->_table->getConnection(); |
| 334 | |
| 335 | foreach ($classes as $class) { |
| 336 | try { |
| 337 | $table = $conn->getTable($class); |
| 338 | } catch (Doctrine_Table_Exception $e) { |
| 339 | continue; |
| 340 | } |
| 341 | $columns = $this->getIdentifiers($table); |
| 342 | $found = true; |
| 343 | |
| 344 | foreach ((array) $columns as $column) { |
| 345 | if ( ! $foreignTable->hasColumn($column)) { |
| 346 | $found = false; |
| 347 | break; |
| 348 | } |
| 349 | } |
| 350 | if ($found) { |
| 351 | break; |
| 352 | } |
| 353 | } |
| 354 | |
| 355 | if ( ! $found) { |
| 356 | throw new Doctrine_Relation_Exception("Couldn't find columns."); |
| 357 | } |
| 358 | |
| 359 | return $columns; |
| 360 | } |
| 361 | |
| 362 | /** |
| 363 | * Completes the given definition |
| 364 | * |
| 365 | * @param array $def definition array to be completed |
| 366 | * @return array completed definition array |
| 367 | * @todo Description: What does it mean to complete a definition? What is done (not how)? |
| 368 | * Refactor (too long & nesting level) |
| 369 | */ |
| 370 | public function completeDefinition($def) |
| 371 | { |
| 372 | $conn = $this->_table->getConnection(); |
| 373 | $def['table'] = $this->getImpl($def['class']); |
| 374 | $def['class'] = $def['table']->getComponentName(); |
| 375 | |
| 376 | $foreignClasses = array_merge($def['table']->getOption('parents'), array($def['class'])); |
| 377 | $localClasses = array_merge($this->_table->getOption('parents'), array($this->_table->getComponentName())); |
| 378 | |
| 379 | $localIdentifierColumnNames = $this->_table->getIdentifierColumnNames(); |
| 380 | $localIdColumnName = array_pop($localIdentifierColumnNames); |
| 381 | $foreignIdentifierColumnNames = $def['table']->getIdentifierColumnNames(); |
| 382 | $foreignIdColumnName = array_pop($foreignIdentifierColumnNames); |
| 383 | |
| 384 | if (isset($def['local'])) { |
| 385 | if ( ! isset($def['foreign'])) { |
| 386 | // local key is set, but foreign key is not |
| 387 | // try to guess the foreign key |
| 388 | |
| 389 | if ($def['local'] === $localIdColumnName) { |
| 390 | $def['foreign'] = $this->guessColumns($localClasses, $def['table']); |
| 391 | } else { |
| 392 | // the foreign field is likely to be the |
| 393 | // identifier of the foreign class |
| 394 | $def['foreign'] = $foreignIdColumnName; |
| 395 | $def['localKey'] = true; |
| 396 | } |
| 397 | } else { |
| 398 | if ($def['local'] !== $localIdColumnName && |
| 399 | $def['type'] == Doctrine_Relation::ONE) { |
| 400 | $def['localKey'] = true; |
| 401 | } |
| 402 | } |
| 403 | } else { |
| 404 | if (isset($def['foreign'])) { |
| 405 | // local key not set, but foreign key is set |
| 406 | // try to guess the local key |
| 407 | if ($def['foreign'] === $foreignIdColumnName) { |
| 408 | $def['localKey'] = true; |
| 409 | try { |
| 410 | $def['local'] = $this->guessColumns($foreignClasses, $this->_table); |
| 411 | } catch (Doctrine_Relation_Exception $e) { |
| 412 | $def['local'] = $localIdColumnName; |
| 413 | } |
| 414 | } else { |
| 415 | $def['local'] = $localIdColumnName; |
| 416 | } |
| 417 | } else { |
| 418 | // neither local or foreign key is being set |
| 419 | // try to guess both keys |
| 420 | |
| 421 | $conn = $this->_table->getConnection(); |
| 422 | |
| 423 | // the following loops are needed for covering inheritance |
| 424 | foreach ($localClasses as $class) { |
| 425 | $table = $conn->getTable($class); |
| 426 | $identifierColumnNames = $table->getIdentifierColumnNames(); |
| 427 | $idColumnName = array_pop($identifierColumnNames); |
| 428 | $column = strtolower($table->getComponentName()) |
| 429 | . '_' . $idColumnName; |
| 430 | |
| 431 | foreach ($foreignClasses as $class2) { |
| 432 | $table2 = $conn->getTable($class2); |
| 433 | if ($table2->hasColumn($column)) { |
| 434 | $def['foreign'] = $column; |
| 435 | $def['local'] = $idColumnName; |
| 436 | return $def; |
| 437 | } |
| 438 | } |
| 439 | } |
| 440 | |
| 441 | foreach ($foreignClasses as $class) { |
| 442 | $table = $conn->getTable($class); |
| 443 | $identifierColumnNames = $table->getIdentifierColumnNames(); |
| 444 | $idColumnName = array_pop($identifierColumnNames); |
| 445 | $column = strtolower($table->getComponentName()) |
| 446 | . '_' . $idColumnName; |
| 447 | |
| 448 | foreach ($localClasses as $class2) { |
| 449 | $table2 = $conn->getTable($class2); |
| 450 | if ($table2->hasColumn($column)) { |
| 451 | $def['foreign'] = $idColumnName; |
| 452 | $def['local'] = $column; |
| 453 | $def['localKey'] = true; |
| 454 | return $def; |
| 455 | } |
| 456 | } |
| 457 | } |
| 458 | |
| 459 | // auto-add columns and auto-build relation |
| 460 | $columns = array(); |
| 461 | foreach ((array) $this->_table->getIdentifierColumnNames() as $id) { |
| 462 | // ?? should this not be $this->_table->getComponentName() ?? |
| 463 | $column = strtolower($table->getComponentName()) |
| 464 | . '_' . $id; |
| 465 | |
| 466 | $col = $this->_table->getColumnDefinition($id); |
| 467 | $type = $col['type']; |
| 468 | $length = $col['length']; |
| 469 | |
| 470 | unset($col['type']); |
| 471 | unset($col['length']); |
| 472 | unset($col['autoincrement']); |
| 473 | unset($col['sequence']); |
| 474 | unset($col['primary']); |
| 475 | |
| 476 | $def['table']->setColumn($column, $type, $length, $col); |
| 477 | |
| 478 | $columns[] = $column; |
| 479 | } |
| 480 | if (count($columns) > 1) { |
| 481 | $def['foreign'] = $columns; |
| 482 | } else { |
| 483 | $def['foreign'] = $columns[0]; |
| 484 | } |
| 485 | $def['local'] = $localIdColumnName; |
| 486 | } |
| 487 | } |
| 488 | return $def; |
| 489 | } |
| 490 | } |