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