Subversion-Projekte lars-tiefland.php_share

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 1
<?php
2
/*
3
 *  $Id$
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.doctrine-project.org>.
20
 */
21
 
22
/**
23
 * Abstract base class for child drivers to hydrate the object graph in to
24
 * various data types. For example Doctrine_Record instances or PHP arrays
25
 *
26
 * @package     Doctrine
27
 * @subpackage  Hydrate
28
 * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
29
 * @link        www.doctrine-project.org
30
 * @since       1.0
31
 * @version     $Revision$
32
 * @author      Konsta Vesterinen <kvesteri@cc.hut.fi>
33
 * @author      Roman Borschel <roman@code-factory.org>
34
 * @author      Jonathan H. Wage <jonwage@gmail.com>
35
 */
36
abstract class Doctrine_Hydrator_Graph extends Doctrine_Hydrator_Abstract
37
{
38
    protected $_tables = array();
39
 
40
    /**
41
     * Gets the custom field used for indexing for the specified component alias.
42
     *
43
     * @return string  The field name of the field used for indexing or NULL
44
     *                 if the component does not use any custom field indices.
45
     */
46
    protected function _getCustomIndexField($alias)
47
    {
48
        return isset($this->_queryComponents[$alias]['map']) ? $this->_queryComponents[$alias]['map'] : null;
49
    }
50
 
51
    public function hydrateResultSet($stmt)
52
    {
53
        // Used variables during hydration
54
        reset($this->_queryComponents);
55
        $rootAlias = key($this->_queryComponents);
56
        $this->_rootAlias = $rootAlias;
57
        $rootComponentName = $this->_queryComponents[$rootAlias]['table']->getComponentName();
58
        // if only one component is involved we can make our lives easier
59
        $isSimpleQuery = count($this->_queryComponents) <= 1;
60
        // Holds the resulting hydrated data structure
61
        $result = array();
62
        // Holds array of record instances so we can call hooks on it
63
        $instances = array();
64
        // Holds hydration listeners that get called during hydration
65
        $listeners = array();
66
        // Lookup map to quickly discover/lookup existing records in the result
67
        $identifierMap = array();
68
        // Holds for each component the last previously seen element in the result set
69
        $prev = array();
70
        // holds the values of the identifier/primary key fields of components,
71
        // separated by a pipe '|' and grouped by component alias (r, u, i, ... whatever)
72
        // the $idTemplate is a prepared template. $id is set to a fresh template when
73
        // starting to process a row.
74
        $id = array();
75
        $idTemplate = array();
76
 
77
        // Initialize
78
        foreach ($this->_queryComponents as $dqlAlias => $data) {
79
            $componentName = $data['table']->getComponentName();
80
            $instances[$componentName] = $data['table']->getRecordInstance();
81
            $listeners[$componentName] = $data['table']->getRecordListener();
82
            $identifierMap[$dqlAlias] = array();
83
            $prev[$dqlAlias] = null;
84
            $idTemplate[$dqlAlias] = '';
85
        }
86
        $cache = array();
87
 
88
        $result = $this->getElementCollection($rootComponentName);
89
        if ($result instanceof Doctrine_Collection && $indexField = $this->_getCustomIndexField($rootAlias)) {
90
            $result->setKeyColumn($indexField);
91
        }
92
        if ($stmt === false || $stmt === 0) {
93
            return $result;
94
        }
95
 
96
        // Process result set
97
        $cache = array();
98
 
99
        $event = new Doctrine_Event(null, Doctrine_Event::HYDRATE, null);
100
 
101
        if ($this->_hydrationMode == Doctrine_Core::HYDRATE_ON_DEMAND) {
102
            if ( ! is_null($this->_priorRow)) {
103
                $data = $this->_priorRow;
104
                $this->_priorRow = null;
105
            } else {
106
                $data = $stmt->fetch(Doctrine_Core::FETCH_ASSOC);
107
                if ( ! $data) {
108
                    return $result;
109
                }
110
            }
111
            $activeRootIdentifier = null;
112
        } else {
113
            $data = $stmt->fetch(Doctrine_Core::FETCH_ASSOC);
114
            if ( ! $data) {
115
                return $result;
116
            }
117
        }
118
 
119
        do {
120
            $table = $this->_queryComponents[$rootAlias]['table'];
121
 
122
            if ($table->getConnection()->getAttribute(Doctrine_Core::ATTR_PORTABILITY) & Doctrine_Core::PORTABILITY_RTRIM) {
123
                array_map('rtrim', $data);
124
            }
125
 
126
            $id = $idTemplate; // initialize the id-memory
127
            $nonemptyComponents = array();
128
            $rowData = $this->_gatherRowData($data, $cache, $id, $nonemptyComponents);
129
 
130
            if ($this->_hydrationMode == Doctrine_Core::HYDRATE_ON_DEMAND)  {
131
                if (is_null($activeRootIdentifier)) {
132
                    // first row for this record
133
                    $activeRootIdentifier = $id[$rootAlias];
134
                } else if ($activeRootIdentifier != $id[$rootAlias]) {
135
                    // first row for the next record
136
                    $this->_priorRow = $data;
137
                    return $result;
138
                }
139
            }
140
 
141
            //
142
            // hydrate the data of the root component from the current row
143
            //
144
            $componentName = $table->getComponentName();
145
            // Ticket #1115 (getInvoker() should return the component that has addEventListener)
146
            $event->setInvoker($table);
147
            $event->set('data', $rowData[$rootAlias]);
148
            $listeners[$componentName]->preHydrate($event);
149
            $instances[$componentName]->preHydrate($event);
150
 
151
            $index = false;
152
 
153
            // Check for an existing element
154
            if ($isSimpleQuery || ! isset($identifierMap[$rootAlias][$id[$rootAlias]])) {
155
                $element = $this->getElement($rowData[$rootAlias], $componentName);
156
                $event->set('data', $element);
157
                $listeners[$componentName]->postHydrate($event);
158
                $instances[$componentName]->postHydrate($event);
159
 
160
                // do we need to index by a custom field?
161
                if ($field = $this->_getCustomIndexField($rootAlias)) {
162
                    if ( ! isset($element[$field])) {
163
                        throw new Doctrine_Hydrator_Exception("Couldn't hydrate. Found a non-existent key named '$field'.");
164
                    } else if (isset($result[$element[$field]])) {
165
                        throw new Doctrine_Hydrator_Exception("Couldn't hydrate. Found non-unique key mapping named '{$element[$field]}' for the field named '$field'.");
166
                    }
167
                    $result[$element[$field]] = $element;
168
                } else {
169
                    $result[] = $element;
170
                }
171
 
172
                $identifierMap[$rootAlias][$id[$rootAlias]] = $this->getLastKey($result);
173
            } else {
174
                $index = $identifierMap[$rootAlias][$id[$rootAlias]];
175
            }
176
 
177
            $this->setLastElement($prev, $result, $index, $rootAlias, false);
178
            unset($rowData[$rootAlias]);
179
 
180
            // end hydrate data of the root component for the current row
181
 
182
            // $prev[$rootAlias] now points to the last element in $result.
183
            // now hydrate the rest of the data found in the current row, that belongs to other
184
            // (related) components.
185
            foreach ($rowData as $dqlAlias => $data) {
186
                $index = false;
187
                $map = $this->_queryComponents[$dqlAlias];
188
                $table = $map['table'];
189
                $componentName = $table->getComponentName();
190
                $event->set('data', $data);
191
                $event->setInvoker($table);
192
                $listeners[$componentName]->preHydrate($event);
193
                $instances[$componentName]->preHydrate($event);
194
 
195
                // It would be nice if this could be moved to the query parser but I could not find a good place to implement it
196
                if ( ! isset($map['parent'])) {
197
                    throw new Doctrine_Hydrator_Exception(
198
                        '"' . $componentName . '" with an alias of "' . $dqlAlias . '"' .
199
                        ' in your query does not reference the parent component it is related to.'
200
                    );
201
                }
202
 
203
                $parent = $map['parent'];
204
                $relation = $map['relation'];
205
                $relationAlias = $map['relation']->getAlias();
206
 
207
                $path = $parent . '.' . $dqlAlias;
208
 
209
                if ( ! isset($prev[$parent])) {
210
                    unset($prev[$dqlAlias]); // Ticket #1228
211
                    continue;
212
                }
213
 
214
                $indexField = $this->_getCustomIndexField($dqlAlias);
215
 
216
                // check the type of the relation
217
                if ( ! $relation->isOneToOne() && $this->initRelated($prev[$parent], $relationAlias, $indexField)) {
218
                    $oneToOne = false;
219
                    // append element
220
                    if (isset($nonemptyComponents[$dqlAlias])) {
221
                        $indexExists = isset($identifierMap[$path][$id[$parent]][$id[$dqlAlias]]);
222
                        $index = $indexExists ? $identifierMap[$path][$id[$parent]][$id[$dqlAlias]] : false;
223
                        $indexIsValid = $index !== false ? isset($prev[$parent][$relationAlias][$index]) : false;
224
                        if ( ! $indexExists || ! $indexIsValid) {
225
                            $element = $this->getElement($data, $componentName);
226
                            $event->set('data', $element);
227
                            $listeners[$componentName]->postHydrate($event);
228
                            $instances[$componentName]->postHydrate($event);
229
 
230
                            if ($field = $this->_getCustomIndexField($dqlAlias)) {
231
                                if ( ! isset($element[$field])) {
232
                                    throw new Doctrine_Hydrator_Exception("Couldn't hydrate. Found a non-existent key named '$field'.");
233
                                } else if (isset($prev[$parent][$relationAlias][$element[$field]])) {
234
                                    throw new Doctrine_Hydrator_Exception("Couldn't hydrate. Found non-unique key mapping named '$field'.");
235
                                }
236
                                $prev[$parent][$relationAlias][$element[$field]] = $element;
237
                            } else {
238
                                $prev[$parent][$relationAlias][] = $element;
239
                            }
240
                            $identifierMap[$path][$id[$parent]][$id[$dqlAlias]] = $this->getLastKey($prev[$parent][$relationAlias]);
241
                        }
242
                        $collection = $prev[$parent][$relationAlias];
243
                        if ($collection instanceof Doctrine_Collection && $indexField) {
244
                            $collection->setKeyColumn($indexField);
245
                        }
246
 
247
                        // register collection for later snapshots
248
                        $this->registerCollection($collection);
249
                    }
250
                } else {
251
                    // 1-1 relation
252
                    $oneToOne = true;
253
                    if ( ! isset($nonemptyComponents[$dqlAlias]) && ! isset($prev[$parent][$relationAlias])) {
254
                        $prev[$parent][$relationAlias] = $this->getNullPointer();
255
                    } else if ( ! isset($prev[$parent][$relationAlias])) {
256
                        $element = $this->getElement($data, $componentName);
257
 
258
						// [FIX] Tickets #1205 and #1237
259
                        $event->set('data', $element);
260
                        $listeners[$componentName]->postHydrate($event);
261
                        $instances[$componentName]->postHydrate($event);
262
 
263
                        $prev[$parent][$relationAlias] = $element;
264
                    }
265
                }
266
                if ($prev[$parent][$relationAlias] !== null) {
267
                    $coll =& $prev[$parent][$relationAlias];
268
                    $this->setLastElement($prev, $coll, $index, $dqlAlias, $oneToOne);
269
                }
270
            }
271
        } while ($data = $stmt->fetch(Doctrine_Core::FETCH_ASSOC));
272
 
273
        $stmt->closeCursor();
274
        $this->flush();
275
 
276
        return $result;
277
    }
278
 
279
    /**
280
     * Puts the fields of a data row into a new array, grouped by the component
281
     * they belong to. The column names in the result set are mapped to their
282
     * field names during this procedure.
283
     *
284
     * @return array  An array with all the fields (name => value) of the data row,
285
     *                grouped by their component (alias).
286
     */
287
    protected function _gatherRowData(&$data, &$cache, &$id, &$nonemptyComponents)
288
    {
289
        $rowData = array();
290
 
291
        foreach ($data as $key => $value) {
292
            // Parse each column name only once. Cache the results.
293
            if ( ! isset($cache[$key])) {
294
                // check ignored names. fastest solution for now. if we get more we'll start
295
                // to introduce a list.
296
                if ($this->_isIgnoredName($key)) {
297
                    continue;
298
                }
299
 
300
                $e = explode('__', $key);
301
                $last = strtolower(array_pop($e));
302
                $cache[$key]['dqlAlias'] = $this->_tableAliases[strtolower(implode('__', $e))];
303
                $table = $this->_queryComponents[$cache[$key]['dqlAlias']]['table'];
304
                $fieldName = $table->getFieldName($last);
305
                $cache[$key]['fieldName'] = $fieldName;
306
                if ($table->isIdentifier($fieldName)) {
307
                    $cache[$key]['isIdentifier'] = true;
308
                } else {
309
                  $cache[$key]['isIdentifier'] = false;
310
                }
311
                $type = $table->getTypeOfColumn($last);
312
                if ($type == 'integer' || $type == 'string') {
313
                    $cache[$key]['isSimpleType'] = true;
314
                } else {
315
                    $cache[$key]['type'] = $type;
316
                    $cache[$key]['isSimpleType'] = false;
317
                }
318
            }
319
 
320
            $map = $this->_queryComponents[$cache[$key]['dqlAlias']];
321
            $table = $map['table'];
322
            $dqlAlias = $cache[$key]['dqlAlias'];
323
            $fieldName = $cache[$key]['fieldName'];
324
            $agg = false;
325
            if (isset($this->_queryComponents[$dqlAlias]['agg'][$fieldName])) {
326
                $fieldName = $this->_queryComponents[$dqlAlias]['agg'][$fieldName];
327
                $agg = true;
328
            }
329
 
330
            if ($cache[$key]['isIdentifier']) {
331
                $id[$dqlAlias] .= '|' . $value;
332
            }
333
 
334
            if ($cache[$key]['isSimpleType']) {
335
                $preparedValue = $value;
336
            } else {
337
                $preparedValue = $table->prepareValue($fieldName, $value, $cache[$key]['type']);
338
            }
339
 
340
            // Ticket #1380
341
            // Hydrate aggregates in to the root component as well.
342
            // So we know that all aggregate values will always be available in the root component
343
            if ($agg) {
344
                $rowData[$this->_rootAlias][$fieldName] = $preparedValue;
345
                if (isset($rowData[$dqlAlias])) {
346
                    $rowData[$dqlAlias][$fieldName] = $preparedValue;
347
                }
348
            } else {
349
                $rowData[$dqlAlias][$fieldName] = $preparedValue;
350
            }
351
 
352
            if ( ! isset($nonemptyComponents[$dqlAlias]) && $value !== null) {
353
                $nonemptyComponents[$dqlAlias] = true;
354
            }
355
        }
356
 
357
        return $rowData;
358
    }
359
 
360
    abstract public function getElementCollection($component);
361
 
362
    abstract public function registerCollection($coll);
363
 
364
    abstract public function initRelated(&$record, $name, $keyColumn = null);
365
 
366
    abstract public function getNullPointer();
367
 
368
    abstract public function getElement(array $data, $component);
369
 
370
    abstract public function getLastKey(&$coll);
371
 
372
    abstract public function setLastElement(&$prev, &$coll, $index, $dqlAlias, $oneToOne);
373
 
374
    public function flush()
375
    {
376
 
377
    }
378
 
379
    /**
380
     * Get the classname to return. Most often this is just the options['name']
381
     *
382
     * Check the subclasses option and the inheritanceMap for each subclass to see
383
     * if all the maps in a subclass is met. If this is the case return that
384
     * subclass name. If no subclasses match or if there are no subclasses defined
385
     * return the name of the class for this tables record.
386
     *
387
     * @todo this function could use reflection to check the first time it runs
388
     * if the subclassing option is not set.
389
     *
390
     * @return string The name of the class to create
391
     *
392
     */
393
    protected function _getClassnameToReturn(array &$data, $component)
394
    {
395
        if ( ! isset($this->_tables[$component])) {
396
            $this->_tables[$component] = Doctrine_Core::getTable($component);
397
        }
398
 
399
        if ( ! ($subclasses = $this->_tables[$component]->getOption('subclasses'))) {
400
            return $component;
401
        }
402
 
403
        $matchedComponents = array($component);
404
        foreach ($subclasses as $subclass) {
405
            $table = Doctrine_Core::getTable($subclass);
406
            $inheritanceMap = $table->getOption('inheritanceMap');
407
            if (count($inheritanceMap) > 1) {
408
                $needMatches = count($inheritanceMap);
409
                foreach ($inheritanceMap as $key => $value) {
410
                    $key = $this->_tables[$component]->getFieldName($key);
411
                    if ( isset($data[$key]) && $data[$key] == $value) {
412
                        --$needMatches;
413
                    }
414
                }
415
                if ($needMatches == 0) {
416
                    $matchedComponents[] = $table->getComponentName();
417
                }
418
            } else {
419
                list($key, $value) = each($inheritanceMap);
420
                $key = $this->_tables[$component]->getFieldName($key);
421
                if ( ! isset($data[$key]) || $data[$key] != $value) {
422
                    continue;
423
                } else {
424
                    $matchedComponents[] = $table->getComponentName();
425
                }
426
            }
427
        }
428
 
429
        $matchedComponent = $matchedComponents[count($matchedComponents)-1];
430
 
431
        if ( ! isset($this->_tables[$matchedComponent])) {
432
            $this->_tables[$matchedComponent] = Doctrine_Core::getTable($matchedComponent);
433
        }
434
 
435
        return $matchedComponent;
436
    }
437
}