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 }