# Coverage for Doctrine_Relation_Parser

Back to coverage report

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 | }