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.com>.
20  */
21
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.com
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         //$s = microtime(true);
64         
65         $this->_tableAliases = $tableAliases;
66         
67         if ($hydrationMode == Doctrine::HYDRATE_NONE) {
68             return $stmt->fetchAll(PDO::FETCH_NUM);
69         }
70         
71         if ($hydrationMode === null) {
72             $hydrationMode = $this->_hydrationMode;
73         }
74
75         if ($hydrationMode === Doctrine::HYDRATE_ARRAY) {
76             $driver = new Doctrine_Hydrator_ArrayDriver();
77         } else {
78             $driver = new Doctrine_Hydrator_RecordDriver();
79         }
80
81         $event = new Doctrine_Event(null, Doctrine_Event::HYDRATE, null);
82
83
84         // Used variables during hydration
85         reset($this->_queryComponents);
86         $rootAlias = key($this->_queryComponents);
87         $rootComponentName = $this->_queryComponents[$rootAlias]['table']->getComponentName();
88         // if only one component is involved we can make our lives easier
89         $isSimpleQuery = count($this->_queryComponents) <= 1;
90         // Holds the resulting hydrated data structure
91         $result = array();
92         // Holds hydration listeners that get called during hydration
93         $listeners = array();
94         // Lookup map to quickly discover/lookup existing records in the result 
95         $identifierMap = array();
96         // Holds for each component the last previously seen element in the result set
97         $prev = array();
98         // holds the values of the identifier/primary key fields of components,
99         // separated by a pipe '|' and grouped by component alias (r, u, i, ... whatever)
100         $id = array(); 
101         
102         $result = $driver->getElementCollection($rootComponentName);
103
104         if ($stmt === false || $stmt === 0) {
105             return $result;
106         }
107
108         // Initialize
109         foreach ($this->_queryComponents as $dqlAlias => $data) {
110             $componentName = $data['table']->getComponentName();
111             $listeners[$componentName] = $data['table']->getRecordListener();
112             $identifierMap[$dqlAlias] = array();
113             $prev[$dqlAlias] = array();
114             $id[$dqlAlias] = '';
115         }
116         
117         // Process result set
118         $cache = array();
119         while ($data = $stmt->fetch(Doctrine::FETCH_ASSOC)) {
120             $nonemptyComponents = array();
121             $rowData = $this->_gatherRowData($data, $cache, $id, $nonemptyComponents);
122
123             //
124             // hydrate the data of the root component from the current row
125             //
126             $table = $this->_queryComponents[$rootAlias]['table'];
127             $componentName = $table->getComponentName();
128             $event->set('data', $rowData[$rootAlias]);
129             $listeners[$componentName]->preHydrate($event);
130             $element = $driver->getElement($rowData[$rootAlias], $componentName);
131             $index = false;
132             
133             // Check for an existing element
134             if ($isSimpleQuery || ! isset($identifierMap[$rootAlias][$id[$rootAlias]])) {
135                 $event->set('data', $element);
136                 $listeners[$componentName]->postHydrate($event);
137
138                 // do we need to index by a custom field?
139                 if ($field = $this->_getCustomIndexField($rootAlias)) {
140                     if (isset($result[$field])) {
141                         throw new Doctrine_Hydrator_Exception("Couldn't hydrate. Found non-unique key mapping.");
142                     } else if ( ! isset($element[$field])) {
143                         throw new Doctrine_Hydrator_Exception("Couldn't hydrate. Found a non-existent key.");
144                     }
145                     $result[$element[$field]] = $element;
146                 } else {
147                     $result[] = $element;
148                 }
149
150                 $identifierMap[$rootAlias][$id[$rootAlias]] = $driver->getLastKey($result);
151             } else {
152                 $index = $identifierMap[$rootAlias][$id[$rootAlias]];
153             }
154
155             $this->_setLastElement($prev, $result, $index, $rootAlias, false);
156             unset($rowData[$rootAlias]);
157             
158             // end hydrate data of the root component for the current row
159             
160             
161             // $prev[$rootAlias] now points to the last element in $result.
162             // now hydrate the rest of the data found in the current row, that belongs to other
163             // (related) components.
164             $oneToOne = false;
165             foreach ($rowData as $dqlAlias => $data) {
166                 $index = false;
167                 $map   = $this->_queryComponents[$dqlAlias];
168                 $table = $map['table'];
169                 $componentName = $table->getComponentName();
170                 $event->set('data', $data);
171                 $listeners[$componentName]->preHydrate($event);
172
173                 $element = $driver->getElement($data, $componentName);
174
175                 $parent   = $map['parent'];
176                 $relation = $map['relation'];
177                 $relationAlias = $map['relation']->getAlias();
178
179                 $path = $parent . '.' . $dqlAlias;
180
181                 if ( ! isset($prev[$parent])) {
182                     break;
183                 }
184                 
185                 // check the type of the relation
186                 if ( ! $relation->isOneToOne() && $driver->initRelated($prev[$parent], $relationAlias)) {
187                     // append element
188                     if (isset($nonemptyComponents[$dqlAlias])) {
189                         if ($isSimpleQuery || ! isset($identifierMap[$path][$id[$parent]][$id[$dqlAlias]])) {
190                             $event->set('data', $element);
191                             $listeners[$componentName]->postHydrate($event);
192
193                             if ($field = $this->_getCustomIndexField($dqlAlias)) {
194                                 if (isset($prev[$parent][$relationAlias][$field])) {
195                                     throw new Doctrine_Hydrator_Exception("Couldn't hydrate. Found non-unique key mapping.");
196                                 } else if ( ! isset($element[$field])) {
197                                     throw new Doctrine_Hydrator_Exception("Couldn't hydrate. Found a non-existent key.");
198                                 }
199                                 $prev[$parent][$relationAlias][$element[$field]] = $element;
200                             } else {
201                                 $prev[$parent][$relationAlias][] = $element;
202                             }
203
204                             $identifierMap[$path][$id[$parent]][$id[$dqlAlias]] = $driver->getLastKey($prev[$parent][$relationAlias]);
205                         } else {
206                             $index = $identifierMap[$path][$id[$parent]][$id[$dqlAlias]];
207                         }
208                     }
209                     // register collection for later snapshots
210                     $driver->registerCollection($prev[$parent][$relationAlias]);
211                 } else {
212                     // 1-1 relation
213                     $oneToOne = true; 
214                     if ( ! isset($nonemptyComponents[$dqlAlias])) {
215                         $prev[$parent][$relationAlias] = $driver->getNullPointer();
216                     } else {
217                         $prev[$parent][$relationAlias] = $element;
218                     }
219                 }
220                 $coll =& $prev[$parent][$relationAlias];
221                 $this->_setLastElement($prev, $coll, $index, $dqlAlias, $oneToOne);
222                 $id[$dqlAlias] = '';
223             }
224             $id[$rootAlias] = '';
225         }
226         
227         $stmt->closeCursor();
228         
229         $driver->flush();
230         
231         //$e = microtime(true);
232         //echo 'Hydration took: ' . ($e - $s) . ' for '.count($result).' records<br />';
233
234         return $result;
235     }
236
237     /**
238      * _setLastElement
239      *
240      * sets the last element of given data array / collection
241      * as previous element
242      *
243      * @param boolean|integer $index
244      * @return void
245      * @todo Detailed documentation
246      */
247     protected function _setLastElement(&$prev, &$coll, $index, $dqlAlias, $oneToOne)
248     {
249         if ($coll === self::$_null) {
250             return false;
251         }
252         
253         if ($index !== false) {
254             // Link element at $index to previous element for the component 
255             // identified by the DQL alias $alias
256             $prev[$dqlAlias] =& $coll[$index];
257             return;
258         }
259         
260         if (is_array($coll) && $coll) {
261             if ($oneToOne) {
262                 $prev[$dqlAlias] =& $coll;
263             } else {
264                 end($coll);
265                 $prev[$dqlAlias] =& $coll[key($coll)];
266             }
267         } else if (count($coll) > 0) {
268             $prev[$dqlAlias] = $coll->getLast();
269         } else if (isset($prev[$dqlAlias])) {
270             unset($prev[$dqlAlias]);
271         }
272     }
273     
274     /**
275      * Puts the fields of a data row into a new array, grouped by the component
276      * they belong to. The column names in the result set are mapped to their 
277      * field names during this procedure.
278      * 
279      * @return array  An array with all the fields (name => value) of the data row, 
280      *                grouped by their component (alias).
281      */
282     protected function _gatherRowData(&$data, &$cache, &$id, &$nonemptyComponents)
283     {
284         $rowData = array();
285
286         foreach ($data as $key => $value) {
287             // Parse each column name only once. Cache the results.
288             if ( ! isset($cache[$key])) {
289                 $e = explode('__', $key);
290                 $last = strtolower(array_pop($e));
291                 $cache[$key]['dqlAlias'] = $this->_tableAliases[strtolower(implode('__', $e))];
292                 $fieldName = $this->_queryComponents[$cache[$key]['dqlAlias']]['table']->getFieldName($last);
293                 $cache[$key]['fieldName'] = $fieldName;
294             }
295
296             $map   = $this->_queryComponents[$cache[$key]['dqlAlias']];
297             $table = $map['table'];
298             $dqlAlias = $cache[$key]['dqlAlias'];
299             $fieldName = $cache[$key]['fieldName'];
300
301             if (isset($this->_queryComponents[$dqlAlias]['agg'][$fieldName])) {
302                 $fieldName = $this->_queryComponents[$dqlAlias]['agg'][$fieldName];
303             }
304
305             if ($table->isIdentifier($fieldName)) {
306                 $id[$dqlAlias] .= '|' . $value;
307             }
308
309             $rowData[$dqlAlias][$fieldName] = $table->prepareValue($fieldName, $value);
310
311             if ( ! isset($nonemptyComponents[$dqlAlias]) && $value !== null) {
312                 $nonemptyComponents[$dqlAlias] = true;
313             }
314         }
315         
316         return $rowData;
317     }
318     
319     /** 
320      * Gets the custom field used for indexing for the specified component alias.
321      * 
322      * @return string  The field name of the field used for indexing or NULL
323      *                 if the component does not use any custom field indices.
324      */
325     protected function _getCustomIndexField($alias)
326     {
327         return isset($this->_queryComponents[$alias]['map']) ? $this->_queryComponents[$alias]['map'] : null;
328     }
329     
330 }