Coverage for Doctrine_Hydrator

Back to coverage report

1 <?php
2 /*
3  *  $Id: Hydrate.php 3192 2007-11-19 17:55:23Z romanb $
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 Doctrine::autoload('Doctrine_Hydrator_Abstract');
22 /**
23  * Doctrine_Hydrate is a base class for Doctrine_RawSql and Doctrine_Query.
24  * Its purpose is to populate object graphs.
25  *
26  *
27  * @package     Doctrine
28  * @subpackage  Hydrate
29  * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
30  * @link        www.phpdoctrine.org
31  * @since       1.0
32  * @version     $Revision: 3192 $
33  * @author      Konsta Vesterinen <kvesteri@cc.hut.fi>
34  */
35 class Doctrine_Hydrator extends Doctrine_Hydrator_Abstract
36 {    
37     /**
38      * hydrateResultSet
39      * parses the data returned by statement object
40      *
41      * This is method defines the core of Doctrine's object population algorithm
42      * hence this method strives to be as fast as possible
43      *
44      * The key idea is the loop over the rowset only once doing all the needed operations
45      * within this massive loop.
46      *
47      * @todo: Detailed documentation. Refactor (too long & nesting level).
48      *
49      * @param mixed $stmt
50      * @param array $tableAliases  Array that maps table aliases (SQL alias => DQL alias)
51      * @param array $aliasMap  Array that maps DQL aliases to their components
52      *                         (DQL alias => array(
53      *                              'table' => Table object,
54      *                              'parent' => Parent DQL alias (if any),
55      *                              'relation' => Relation object (if any),
56      *                              'map' => Custom index to use as the key in the result (if any)
57      *                              )
58      *                         )
59      * @return array
60      */
61     public function hydrateResultSet($stmt, $tableAliases, $hydrationMode = null)
62     {
63         if ($hydrationMode === null) {
64             $hydrationMode = $this->_hydrationMode;
65         }
66         
67         $this->_tableAliases = $tableAliases;
68         
69         if ($hydrationMode == Doctrine::HYDRATE_NONE) {
70             return $stmt->fetchAll(PDO::FETCH_NUM);
71         }
72
73         if ($hydrationMode == Doctrine::HYDRATE_ARRAY) {
74             $driver = new Doctrine_Hydrator_ArrayDriver();
75         } else {
76             $driver = new Doctrine_Hydrator_RecordDriver();
77         }
78
79         $event = new Doctrine_Event(null, Doctrine_Event::HYDRATE, null);
80
81
82         // Used variables during hydration
83         reset($this->_queryComponents);
84         $rootAlias = key($this->_queryComponents);
85         $rootComponentName = $this->_queryComponents[$rootAlias]['table']->getComponentName();
86         // if only one component is involved we can make our lives easier
87         $isSimpleQuery = count($this->_queryComponents) <= 1;
88         // Holds the resulting hydrated data structure
89         $result = array();
90         // Holds hydration listeners that get called during hydration
91         $listeners = array();
92         // Lookup map to quickly discover/lookup existing records in the result 
93         $identifierMap = array();
94         // Holds for each component the last previously seen element in the result set
95         $prev = array();
96         // holds the values of the identifier/primary key fields of components,
97         // separated by a pipe '|' and grouped by component alias (r, u, i, ... whatever)
98         $id = array(); 
99         
100         $result = $driver->getElementCollection($rootComponentName);
101
102         if ($stmt === false || $stmt === 0) {
103             return $result;
104         }
105
106         // Initialize
107         foreach ($this->_queryComponents as $dqlAlias => $data) {
108             $componentName = $data['table']->getComponentName();
109             $listeners[$componentName] = $data['table']->getRecordListener();
110             $identifierMap[$dqlAlias] = array();
111             $prev[$dqlAlias] = array();
112             $id[$dqlAlias] = '';
113         }
114         
115         // Process result set
116         $cache = array();
117         while ($data = $stmt->fetch(Doctrine::FETCH_ASSOC)) {
118             $nonemptyComponents = array();
119             $rowData = $this->_gatherRowData($data, $cache, $id, $nonemptyComponents);
120
121             //
122             // hydrate the data of the root component from the current row
123             //
124             $table = $this->_queryComponents[$rootAlias]['table'];
125             $componentName = $table->getComponentName();
126             $event->set('data', $rowData[$rootAlias]);
127             $listeners[$componentName]->preHydrate($event);
128             $element = $driver->getElement($rowData[$rootAlias], $componentName);
129             $index = false;
130             
131             // Check for an existing element
132             if ($isSimpleQuery || ! isset($identifierMap[$rootAlias][$id[$rootAlias]])) {
133                 $event->set('data', $element);
134                 $listeners[$componentName]->postHydrate($event);
135
136                 // do we need to index by a custom field?
137                 if ($field = $this->_getCustomIndexField($rootAlias)) {
138                     if (isset($result[$field])) {
139                         throw new Doctrine_Hydrator_Exception("Couldn't hydrate. Found non-unique key mapping.");
140                     } else if ( ! isset($element[$field])) {
141                         throw new Doctrine_Hydrator_Exception("Couldn't hydrate. Found a non-existent key.");
142                     }
143                     $result[$element[$field]] = $element;
144                 } else {
145                     $result[] = $element;
146                 }
147
148                 $identifierMap[$rootAlias][$id[$rootAlias]] = $driver->getLastKey($result);
149             } else {
150                 $index = $identifierMap[$rootAlias][$id[$rootAlias]];
151             }
152
153             $this->_setLastElement($prev, $result, $index, $rootAlias, false);
154             unset($rowData[$rootAlias]);
155             
156             // end hydrate data of the root component for the current row
157             
158             
159             // $prev[$rootAlias] now points to the last element in $result.
160             // now hydrate the rest of the data found in the current row, that belongs to other
161             // (related) components.
162             $oneToOne = false;
163             foreach ($rowData as $dqlAlias => $data) {
164                 $index = false;
165                 $map   = $this->_queryComponents[$dqlAlias];
166                 $table = $map['table'];
167                 $componentName = $table->getComponentName();
168                 $event->set('data', $data);
169                 $listeners[$componentName]->preHydrate($event);
170
171                 $element = $driver->getElement($data, $componentName);
172
173                 $parent   = $map['parent'];
174                 $relation = $map['relation'];
175                 $relationAlias = $map['relation']->getAlias();
176
177                 $path = $parent . '.' . $dqlAlias;
178
179                 if ( ! isset($prev[$parent])) {
180                     continue;
181                 }
182                 
183                 // check the type of the relation
184                 if ( ! $relation->isOneToOne() && $driver->initRelated($prev[$parent], $relationAlias)) {
185                     // append element
186                     if (isset($nonemptyComponents[$dqlAlias])) {
187                         if ($isSimpleQuery || ! isset($identifierMap[$path][$id[$parent]][$id[$dqlAlias]])) {
188                             $event->set('data', $element);
189                             $listeners[$componentName]->postHydrate($event);
190
191                             if ($field = $this->_getCustomIndexField($dqlAlias)) {
192                                 if (isset($prev[$parent][$relationAlias][$field])) {
193                                     throw new Doctrine_Hydrator_Exception("Couldn't hydrate. Found non-unique key mapping.");
194                                 } else if ( ! isset($element[$field])) {
195                                     throw new Doctrine_Hydrator_Exception("Couldn't hydrate. Found a non-existent key.");
196                                 }
197                                 $prev[$parent][$relationAlias][$element[$field]] = $element;
198                             } else {
199                                 $prev[$parent][$relationAlias][] = $element;
200                             }
201
202                             $identifierMap[$path][$id[$parent]][$id[$dqlAlias]] = $driver->getLastKey($prev[$parent][$relationAlias]);
203                         } else {
204                             $index = $identifierMap[$path][$id[$parent]][$id[$dqlAlias]];
205                         }
206                     }
207                     // register collection for later snapshots
208                     $driver->registerCollection($prev[$parent][$relationAlias]);
209                 } else {
210                     // 1-1 relation
211                     $oneToOne = true; 
212                     if ( ! isset($nonemptyComponents[$dqlAlias])) {
213                         $prev[$parent][$relationAlias] = $driver->getNullPointer();
214                     } else {
215                         $prev[$parent][$relationAlias] = $element;
216                     }
217                 }
218                 $coll =& $prev[$parent][$relationAlias];
219                 $this->_setLastElement($prev, $coll, $index, $dqlAlias, $oneToOne);
220                 $id[$dqlAlias] = '';
221             }
222             $id[$rootAlias] = '';
223         }
224         
225         $stmt->closeCursor();
226         
227         $driver->flush();
228         
229         //$e = microtime(true);
230         //echo 'Hydration took: ' . ($e - $s) . ' for '.count($result).' records<br />';
231
232         return $result;
233     }
234
235     /**
236      * _setLastElement
237      *
238      * sets the last element of given data array / collection
239      * as previous element
240      *
241      * @param boolean|integer $index
242      * @return void
243      * @todo Detailed documentation
244      */
245     protected function _setLastElement(&$prev, &$coll, $index, $dqlAlias, $oneToOne)
246     {
247         if ($coll === self::$_null) {
248             return false;
249         }
250         
251         if ($index !== false) {
252             // Link element at $index to previous element for the component 
253             // identified by the DQL alias $alias
254             $prev[$dqlAlias] =& $coll[$index];
255             return;
256         }
257         
258         if (is_array($coll) && $coll) {
259             if ($oneToOne) {
260                 $prev[$dqlAlias] =& $coll;
261             } else {
262                 end($coll);
263                 $prev[$dqlAlias] =& $coll[key($coll)];
264             }
265         } else if (count($coll) > 0) {
266             $prev[$dqlAlias] = $coll->getLast();
267         } else if (isset($prev[$dqlAlias])) {
268             unset($prev[$dqlAlias]);
269         }
270     }
271     
272     /**
273      * Puts the fields of a data row into a new array, grouped by the component
274      * they belong to. The column names in the result set are mapped to their 
275      * field names during this procedure.
276      * 
277      * @return array  An array with all the fields (name => value) of the data row, 
278      *                grouped by their component (alias).
279      */
280     protected function _gatherRowData(&$data, &$cache, &$id, &$nonemptyComponents)
281     {
282         $rowData = array();
283
284         foreach ($data as $key => $value) {
285             // Parse each column name only once. Cache the results.
286             if ( ! isset($cache[$key])) {
287                 $e = explode('__', $key);
288                 $last = strtolower(array_pop($e));
289                 $cache[$key]['dqlAlias'] = $this->_tableAliases[strtolower(implode('__', $e))];
290                 $table = $this->_queryComponents[$cache[$key]['dqlAlias']]['table'];
291                 $fieldName = $table->getFieldName($last);
292                 $cache[$key]['fieldName'] = $fieldName;
293                 if ($table->isIdentifier($fieldName)) {
294                     $cache[$key]['isIdentifier'] = true;
295                 } else {
296                     $cache[$key]['isIdentifier'] = false;
297                 }
298                 $type = $table->getTypeOfColumn($last);
299                 if ($type == 'integer' || $type == 'string') {
300                     $cache[$key]['isSimpleType'] = true;
301                 } else {
302                     $cache[$key]['type'] = $type;
303                     $cache[$key]['isSimpleType'] = false;
304                 }
305             }
306
307             $map   = $this->_queryComponents[$cache[$key]['dqlAlias']];
308             $table = $map['table'];
309             $dqlAlias = $cache[$key]['dqlAlias'];
310             $fieldName = $cache[$key]['fieldName'];
311
312             if (isset($this->_queryComponents[$dqlAlias]['agg'][$fieldName])) {
313                 $fieldName = $this->_queryComponents[$dqlAlias]['agg'][$fieldName];
314             }
315
316             if ($cache[$key]['isIdentifier']) {
317                 $id[$dqlAlias] .= '|' . $value;
318             }
319
320             if ($cache[$key]['isSimpleType']) {
321                 $rowData[$dqlAlias][$fieldName] = $value;
322             } else {
323                 $rowData[$dqlAlias][$fieldName] = $table->prepareValue(
324                         $fieldName, $value, $cache[$key]['type']);
325             }
326
327             if ( ! isset($nonemptyComponents[$dqlAlias]) && $value !== null) {
328                 $nonemptyComponents[$dqlAlias] = true;
329             }
330         }
331         
332         return $rowData;
333     }
334     
335     /** 
336      * Gets the custom field used for indexing for the specified component alias.
337      * 
338      * @return string  The field name of the field used for indexing or NULL
339      *                 if the component does not use any custom field indices.
340      */
341     protected function _getCustomIndexField($alias)
342     {
343         return isset($this->_queryComponents[$alias]['map']) ? $this->_queryComponents[$alias]['map'] : null;
344     }
345     
346 }