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