Hydrator::getRelationObject()   D
last analyzed

Complexity

Conditions 9
Paths 20

Size

Total Lines 43
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 2 Features 1
Metric Value
c 5
b 2
f 1
dl 0
loc 43
rs 4.909
cc 9
eloc 27
nc 20
nop 4
1
<?php
2
/**
3
 * Fwk
4
 *
5
 * Copyright (c) 2011-2012, Julien Ballestracci <[email protected]>.
6
 * All rights reserved.
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 *
11
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
12
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
13
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
14
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
15
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
16
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
17
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
19
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
20
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
21
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
22
 * POSSIBILITY OF SUCH DAMAGE.
23
 *
24
 * PHP Version 5.3
25
 *
26
 * @category  Database
27
 * @package   Fwk\Db
28
 * @author    Julien Ballestracci <[email protected]>
29
 * @copyright 2011-2012 Julien Ballestracci <[email protected]>
30
 * @license   http://www.opensource.org/licenses/bsd-license.php  BSD License
31
 * @link      http://www.phpfwk.com
32
 */
33
namespace Fwk\Db;
34
35
use Fwk\Db\Registry\RegistryState;
36
use Fwk\Db\Relations\AbstractManyRelation;
37
use Fwk\Db\Relations\One2Many;
38
use Fwk\Db\Relations\One2One;
39
40
/**
41
 * This class transforms a resultset from a query into a set of corresponding
42
 * entities.
43
 *
44
 * @category Utilities
45
 * @package  Fwk\Db
46
 * @author   Julien Ballestracci <[email protected]>
47
 * @license  http://www.opensource.org/licenses/bsd-license.php  BSD License
48
 * @link     http://www.phpfwk.com
49
 */
50
class Hydrator
51
{
52
    /**
53
     * The query
54
     *
55
     * @var Query
56
     */
57
    protected $query;
58
59
    /**
60
     * The Connection
61
     *
62
     * @var Connection
63
     */
64
    protected $connection;
65
66
    /**
67
     * Columns description
68
     *
69
     * @var array
70
     */
71
    protected $columns;
72
73
    /**
74
     * Parsing stack informations
75
     *
76
     * @var array
77
     */
78
    protected $stack;
79
80
    /**
81
     * Entities that need to be marked as fresh in registry
82
     *
83
     * @var array
84
     */
85
    protected $markAsFresh;
86
87
    /**
88
     * Constructor
89
     *
90
     * @param Query      $query      Executed query
91
     * @param Connection $connection Database Connection
92
     * @param array      $columns    Columns description
93
     *
94
     * @return void
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
95
     */
96
    public function __construct(Query $query, Connection $connection, array $columns)
97
    {
98
        $this->query        = $query;
99
        $this->columns      = $columns;
100
        $this->connection   = $connection;
101
102
        $joins      = (array) $query['joins'];
103
        $tables     = array();
104
        $itx        = 0;
105
106
        foreach ($columns as $column => $infos) {
107
            $itx++;
108
            $table          = $infos['table'];
109
110
            if (isset($tables[$table])) {
111
                $tables[$table]['columns'][$column] =  $infos['column'];
112
                continue;
113
            }
114
115
            $skipped    = false;
116
            $jointure   = false;
117
            $joinOpt    = false;
118
119
            foreach ($joins as $join) {
120
                $jointure = false;
121
                $skipped  = false;
122
123
                if (\strpos($join['table'], ' ') !== false) {
124
                    list($joinTable, $alias) = explode(' ', $join['table']);
0 ignored issues
show
Unused Code introduced by
The assignment to $alias is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
125
                } else {
126
                    $joinTable  = $join['table'];
127
                    $alias      = null;
0 ignored issues
show
Unused Code introduced by
$alias is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
128
                }
129
130
                if ($joinTable == $table) {
131
                    if ($join['options']['skipped'] == true) {
132
                        $skipped = true;
133
                    }
134
135
                    $jointure  = true;
136
                    $joinOpt   = $join;
137
                    break;
138
                }
139
            }
140
141
            if ($skipped && $jointure) {
142
                continue;
143
            }
144
145
            $tables[$table]['columns']  = array();
146
            if ($jointure) {
147
                $tables[$table]['join'] = $joinOpt;
148
                $tables[$table]['entity'] = $joinOpt['options']['entity'];
149
                $tables[$table]['entityListeners']
150
                    = $joinOpt['options']['entityListeners'];
151
            } else {
152
                $tables[$table]['entity']
153
                    = ($itx == 1 ? $query['entity'] : $this->connection->table($table)->getDefaultEntity());
154
                $tables[$table]['entityListeners']
155
                    = (count($query['entityListeners'])  ?
156
                    $query['entityListeners'] :
157
                    $this->connection->table($table)->getDefaultEntityListeners()
158
                );
159
            }
160
            $tables[$table]['columns'][$column] =  $infos['column'];
161
        }
162
163
        $this->stack = $tables;
164
    }
165
166
    /**
167
     * Transform raw results from database into ORM-style entities
168
     *
169
     * @param array $results Raw PDO results
170
     *
171
     * @return array
172
     */
173
    public function hydrate(array $results)
174
    {
175
        $final      = array();
176
        $joinData   = array();
177
178
        foreach ($results as $result) {
179
            $mainObj        = null;
180
            $mainObjRefs    = null;
181
            $mainObjTable   = null;
182
183
            foreach ($this->stack as $tableName => $infos) {
184
                $columns    = $infos['columns'];
185
                $isJoin     = (isset($infos['join']) ? true : false);
186
                $entityClass= $infos['entity'];
187
                $ids        = $this->getIdentifiers($tableName, $result);
188
                $obj        = $this->loadEntityClass(
189
                    $tableName,
190
                    $ids,
191
                    $entityClass,
192
                    $infos['entityListeners']
193
                );
194
                $access     = new Accessor($obj);
195
                $values     = $this->getValuesFromSet($columns, $result);
196
                $access->setValues($values);
197
                $mainObjRefs = $ids;
198
199
                foreach ($access->getRelations() as $relation) {
200
                    $relation->setConnection($this->connection);
201
                    $relation->setParentRefs($access->get($relation->getLocal()));
0 ignored issues
show
Bug introduced by
The method setParentRefs() does not exist on Fwk\Db\RelationInterface. Did you maybe mean setParent()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
202
                }
203
204
                if (!$isJoin) {
205
                    $mainObj        = $obj;
206
                    $mainObjTable   = $tableName;
207
                    unset($access, $values, $isJoin, $columns);
208
                    if (!in_array($mainObj, $final, true)) {
209
                        array_push($final, $mainObj);
210
                    }
211
                    continue;
212
                }
213
214
                $idsHash    = $tableName . implode(':', $ids);
215
                $access     = new Accessor($mainObj);
216
                $columnName = $infos['join']['options']['column'];
217
218
                $current = (isset($joinData[$idsHash . $columnName]) ?
219
                    $joinData[$idsHash . $columnName] :
220
                    $this->getRelationObject(
221
                        $mainObj,
222
                        $columnName,
223
                        $infos['join'],
224
                        $entityClass
225
                    )
226
                );
227
228
                $joinData[$idsHash . $columnName] = $current;
229
                $current->add($obj, $ids);
230
                $current->setFetched(true);
231
                $current->setParentRefs($mainObjRefs);
232
233
                $tableObj   = $this->connection->table($mainObjTable);
234
                $current->setParent(
235
                    $mainObj,
236
                    $tableObj->getRegistry()->getEventDispatcher($mainObj)
237
                );
238
239
                $access->set($columnName, $current);
240
241
                unset($access, $values, $isJoin, $columns, $current);
242
            }
243
244
            $access = new Accessor($mainObj);
245
            $relations  = $access->getRelations();
246
            foreach ($relations as $relation) {
247
                $tableObj   = $this->connection->table($mainObjTable);
248
                $relation->setParent(
249
                    $mainObj,
250
                    $tableObj->getRegistry()->getEventDispatcher($mainObj)
251
                );
252
            }
253
254
            unset($mainObj, $mainObjRefs);
255
        }
256
257
        $this->markAsFresh();
258
259
        return $final;
260
    }
261
262
    /**
263
     * Return RelationInterface object of an entity
264
     *
265
     * @param mixed  $object      The entity
266
     * @param string $columnName  RelationInterface's column name
267
     * @param array  $join        Join descriptor (array)
268
     * @param string $entityClass RelationInterface's entity class name
0 ignored issues
show
Documentation introduced by
Should the type for parameter $entityClass not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
269
     *
270
     * @return RelationInterface
271
     */
272
    public function getRelationObject($object, $columnName, array $join,
273
                                      $entityClass = null
274
    ) {
275
        $access = new Accessor($object);
276
        $test   = $access->get($columnName);
277
278
        if (strpos($join['table'], ' ') !== false) {
279
            list($table, ) = explode(' ', $join['table']);
280
        } else {
281
            $table = $join['table'];
282
        }
283
284
        if ($test instanceof \Fwk\Db\RelationInterface && $entityClass == $test->getEntity()) {
285
            if ($test instanceof One2One) {
286
                return $test;
287
            } elseif ($test instanceof AbstractManyRelation && $join['options']['reference'] == $test->getReference()) {
288
                return $test;
289
            }
290
        }
291
292
        if (strpos($join['table'], ' ') !== false) {
293
            list($table, ) = explode(' ', $join['table']);
294
        } else {
295
            $table = $join['table'];
296
        }
297
298
        $ref    = new One2Many(
299
            $join['local'],
300
            $join['foreign'],
301
            $table,
302
            $entityClass,
303
            $join['options']['entityListeners']
304
        );
305
306
        if (!empty($join['options']['reference'])) {
307
            $ref->setReference($join['options']['reference']);
308
        }
309
310
        // $ref->setRegistry($this->connection->table($table)->getRegistry());
311
        $ref->setConnection($this->connection);
312
313
        return $ref;
314
    }
315
316
    /**
317
     * Transforms aliased results into results with real columns names
318
     *
319
     * @param array $columns   Columns description
320
     * @param array $resultSet Result set
321
     *
322
     * @return array
323
     */
324
    public function getValuesFromSet(array $columns, array $resultSet)
325
    {
326
        $final = array();
327
        foreach ($columns as $alias => $realName) {
328
            $final[$realName] = $resultSet[$alias];
329
        }
330
331
        return $final;
332
    }
333
334
    /**
335
     * Loads an entity
336
     *
337
     * @param string $tableName   Table's name
338
     * @param array  $identifiers Entity identifiers
339
     * @param string $entityClass Entity class name
0 ignored issues
show
Documentation introduced by
Should the type for parameter $entityClass not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
340
     * @param array  $listeners   Entity's listeners
341
     *
342
     * @return mixed
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use object.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
343
     */
344
    protected function loadEntityClass($tableName, array $identifiers,
345
                                       $entityClass = null, array $listeners = array()
346
    ) {
347
        $tableObj = $this->connection->table($tableName);
348
        $registry = $tableObj->getRegistry();
349
350
        if ($entityClass === null) {
351
            $entityClass = $tableObj->getDefaultEntity();
352
        }
353
354
        $obj = $registry->get($identifiers, $entityClass);
355
356
        if (null === $obj) {
357
            $obj = new $entityClass;
358
            $registry->store(
359
                $obj, $identifiers, RegistryState::FRESH, array(
360
                    'listeners' => $listeners
361
                )
362
            );
363
            $this->markAsFresh[] = array(
364
                'registry' => $registry,
365
                'entity' => $obj,
366
                'table' => $tableObj
367
            );
368
        }
369
370
        return $obj;
371
    }
372
373
    /**
374
     * Mark freshly built entities as fresh (aka "just fetched")
375
     *
376
     * @return void
377
     */
378
    protected function markAsFresh()
379
    {
380
        if (!is_array($this->markAsFresh) || !count($this->markAsFresh)) {
381
            return;
382
        }
383
384
        foreach ($this->markAsFresh as $infos) {
385
            $infos['registry']->defineInitialValues(
386
                $infos['entity'],
387
                $this->connection,
388
                $infos['table']
389
            );
390
        }
391
392
        unset($this->markAsFresh);
393
    }
394
395
    /**
396
     * Returns an array of identifiers for the given table
397
     *
398
     * @param string $tableName Table's name
399
     * @param array  $results   Raw PDO results
400
     *
401
     * @return array
402
     */
403
    protected function getIdentifiers($tableName, array $results)
404
    {
405
        $tableObj   = $this->connection->table($tableName);
406
        $tableIds   = $tableObj->getIdentifiersKeys();
407
408
        if (!count($tableIds)) {
409
            return array();
410
        }
411
412
        $final = array();
413
        foreach ((array) $this->columns as $colName  => $infos) {
414
            if ($infos['table'] == $tableName
415
                && in_array($infos['column'], $tableIds)
416
            ) {
417
                $final[$infos['column']] = $results[$colName];
418
            }
419
        }
420
421
        return $final;
422
    }
423
}