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['localTable'] = $this->_table;
261         $def['class'] = $def['table']->getComponentName();
262         $def['refTable'] = $this->getImpl($def['refClass']);
263
264         $id = $def['refTable']->getIdentifierColumnNames();
265
266         if (count($id) > 1) {
267             if ( ! isset($def['foreign'])) {
268                 // foreign key not set
269                 // try to guess the foreign key
270     
271                 $def['foreign'] = ($def['local'] === $id[0]) ? $id[1] : $id[0];
272             }
273             if ( ! isset($def['local'])) {
274                 // foreign key not set
275                 // try to guess the foreign key
276
277                 $def['local'] = ($def['foreign'] === $id[0]) ? $id[1] : $id[0];
278             }
279         } else {
280
281             if ( ! isset($def['foreign'])) {
282                 // foreign key not set
283                 // try to guess the foreign key
284     
285                 $columns = $this->getIdentifiers($def['table']);
286     
287                 $def['foreign'] = $columns;
288             }
289             if ( ! isset($def['local'])) {
290                 // local key not set
291                 // try to guess the local key
292                 $columns = $this->getIdentifiers($this->_table);
293     
294                 $def['local'] = $columns;
295             }
296         }
297         return $def;
298     }
299
300     /** 
301      * getIdentifiers
302      * gives a list of identifiers from given table
303      *
304      * the identifiers are in format:
305      * [componentName].[identifier]
306      *
307      * @param Doctrine_Table $table     table object to retrieve identifiers from
308      */
309     public function getIdentifiers(Doctrine_Table $table)
310     {
311         $componentNameToLower = strtolower($table->getComponentName());
312         if (is_array($table->getIdentifier())) {
313             $columns = array();      
314             foreach ((array) $table->getIdentifierColumnNames() as $identColName) {
315                 $columns[] = $componentNameToLower . '_' . $identColName;
316             }
317         } else {
318             $columns = $componentNameToLower . '_' . $table->getColumnName(
319                     $table->getIdentifier());
320         }
321
322         return $columns;
323     }
324
325     /**
326      * guessColumns
327      *
328      * @param array $classes                    an array of class names
329      * @param Doctrine_Table $foreignTable      foreign table object
330      * @return array                            an array of column names
331      */
332     public function guessColumns(array $classes, Doctrine_Table $foreignTable)
333     {
334         $conn = $this->_table->getConnection();
335
336         foreach ($classes as $class) {
337             try {
338                 $table   = $conn->getTable($class);
339             } catch (Doctrine_Table_Exception $e) {
340                 continue;
341             }
342             $columns = $this->getIdentifiers($table);
343             $found   = true;
344
345             foreach ((array) $columns as $column) {
346                 if ( ! $foreignTable->hasColumn($column)) {
347                     $found = false;
348                     break;
349                 }
350             }
351             if ($found) {
352                 break;
353             }
354         }
355         
356         if ( ! $found) {
357             throw new Doctrine_Relation_Exception("Couldn't find columns.");
358         }
359
360         return $columns;
361     }
362
363     /**
364      * Completes the given definition
365      *
366      * @param array $def    definition array to be completed
367      * @return array        completed definition array
368      * @todo Description: What does it mean to complete a definition? What is done (not how)?
369      *       Refactor (too long & nesting level)
370      */
371     public function completeDefinition($def)
372     {
373         $conn = $this->_table->getConnection();
374         $def['table'] = $this->getImpl($def['class']);
375         $def['localTable'] = $this->_table;
376         $def['class'] = $def['table']->getComponentName();
377
378         $foreignClasses = array_merge($def['table']->getOption('parents'), array($def['class']));
379         $localClasses   = array_merge($this->_table->getOption('parents'), array($this->_table->getComponentName()));
380
381         $localIdentifierColumnNames = $this->_table->getIdentifierColumnNames();
382         $localIdColumnName = array_pop($localIdentifierColumnNames);
383         $foreignIdentifierColumnNames = $def['table']->getIdentifierColumnNames();
384         $foreignIdColumnName = array_pop($foreignIdentifierColumnNames);
385
386         if (isset($def['local'])) {
387             if ( ! isset($def['foreign'])) {
388                 // local key is set, but foreign key is not
389                 // try to guess the foreign key
390
391                 if ($def['local'] === $localIdColumnName) {
392                     $def['foreign'] = $this->guessColumns($localClasses, $def['table']);
393                 } else {
394                     // the foreign field is likely to be the
395                     // identifier of the foreign class
396                     $def['foreign'] = $foreignIdColumnName;
397                     $def['localKey'] = true;
398                 }
399             } else {
400                 if (count($localIdentifierColumnNames) > 0 || ($def['local'] !== $localIdColumnName && 
401                     $def['type'] == Doctrine_Relation::ONE)) {
402                     $def['localKey'] = true;
403                 }
404             }
405         } else {
406             if (isset($def['foreign'])) {
407                 // local key not set, but foreign key is set
408                 // try to guess the local key
409                 if ($def['foreign'] === $foreignIdColumnName) {
410                     $def['localKey'] = true;
411                     try {
412                         $def['local'] = $this->guessColumns($foreignClasses, $this->_table);
413                     } catch (Doctrine_Relation_Exception $e) {
414                         $def['local'] = $localIdColumnName;
415                     }
416                 } else {
417                     $def['local'] = $localIdColumnName;
418                 }
419             } else {
420                 // neither local or foreign key is being set
421                 // try to guess both keys
422
423                 $conn = $this->_table->getConnection();
424
425                 // the following loops are needed for covering inheritance
426                 foreach ($localClasses as $class) {
427                     $table = $conn->getTable($class);
428                     $identifierColumnNames = $table->getIdentifierColumnNames();
429                     $idColumnName = array_pop($identifierColumnNames);
430                     $column = strtolower($table->getComponentName())
431                             . '_' . $idColumnName;
432
433                     foreach ($foreignClasses as $class2) {
434                         $table2 = $conn->getTable($class2);
435                         if ($table2->hasColumn($column)) {
436                             $def['foreign'] = $column;
437                             $def['local'] = $idColumnName;
438                             return $def;
439                         }
440                     }
441                 }
442
443                 foreach ($foreignClasses as $class) {
444                     $table  = $conn->getTable($class);
445                     $identifierColumnNames = $table->getIdentifierColumnNames();
446                     $idColumnName = array_pop($identifierColumnNames);
447                     $column = strtolower($table->getComponentName())
448                             . '_' . $idColumnName;
449                 
450                     foreach ($localClasses as $class2) {
451                         $table2 = $conn->getTable($class2);
452                         if ($table2->hasColumn($column)) {
453                             $def['foreign']  = $idColumnName;
454                             $def['local']    = $column;
455                             $def['localKey'] = true;
456                             return $def;
457                         }
458                     }
459                 }
460
461                 // auto-add columns and auto-build relation
462                 $columns = array();
463                 foreach ((array) $this->_table->getIdentifierColumnNames() as $id) {
464                     // ?? should this not be $this->_table->getComponentName() ??
465                     $column = strtolower($table->getComponentName())
466                             . '_' . $id;
467
468                     $col = $this->_table->getColumnDefinition($id);
469                     $type = $col['type'];
470                     $length = $col['length'];
471
472                     unset($col['type']);
473                     unset($col['length']);
474                     unset($col['autoincrement']);
475                     unset($col['sequence']);
476                     unset($col['primary']);
477
478                     $def['table']->setColumn($column, $type, $length, $col);
479                     
480                     $columns[] = $column;
481                 }
482                 if (count($columns) > 1) {
483                     $def['foreign'] = $columns;
484                 } else {
485                     $def['foreign'] = $columns[0];
486                 }
487                 $def['local'] = $localIdColumnName;
488             }
489         }
490         return $def;
491     }
492 }