# 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 |
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 | //$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 | }