Completed
Branch master (411345)
by Rémi
11:20
created

ResultBuilder::build()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 30
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 13
nc 2
nop 2
dl 0
loc 30
rs 8.8571
c 0
b 0
f 0
1
<?php
2
3
namespace Analogue\ORM\System;
4
5
use Closure;
6
use Analogue\ORM\Relationships\Relationship;
7
8
class ResultBuilder
9
{
10
    /**
11
     * The default mapper used to build entities with.
12
     * @var \Analogue\ORM\System\Mapper
13
     */
14
    protected $defaultMapper;
15
16
    /**
17
     * Relations that will be eager loaded on this query
18
     *
19
     * @var array
20
     */
21
    protected $eagerLoads;
22
23
    /**
24
     * The Entity Map for the entity to build.
25
     *
26
     * @var \Analogue\ORM\EntityMap
27
     */
28
    protected $entityMap;
29
30
    /**
31
     * An array of builders used by this class to build necessary
32
     * entities for each result type.
33
     *
34
     * @var array
35
     */
36
    protected $builders = [];
37
38
    /**
39
     * @param Mapper  $defaultMapper
40
     * @param array   $eagerLoads
0 ignored issues
show
Bug introduced by
There is no parameter named $eagerLoads. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
41
     */
42
    public function __construct(Mapper $defaultMapper)
43
    {
44
        $this->defaultMapper = $defaultMapper;
45
        $this->entityMap = $defaultMapper->getEntityMap();
46
    }
47
48
    /**
49
     * Convert a result set into an array of entities
50
     *
51
     * @param  array $results
52
     * @param  array $eagerLoads  name of the relation to be eager loaded on the Entities
53
     * @return \Illuminate\Support\Collection
54
     */
55
    public function build(array $results, array $eagerLoads)
56
    {
57
        // First, we'll cast every single result to array
58
        $results = array_map(function($item) {
59
            return (array) $item;
60
        }, $results);     
61
62
        // Then, we'll cache every single results as raw attributes, before 
63
        // adding relationships, which will be cached when the relationship's
64
        // query takes place.
65
        $this->defaultMapper->getEntityCache()->add($results);
66
67
        // Launch the queries related to eager loads, and match the 
68
        // current result set to these loaded relationships.
69
        $results = $this->queryEagerLoadedRelationships($results, $eagerLoads);
70
71
        // Note : Maybe we could use a PolymorphicResultBuilder, which would
72
        // be shared by both STI and polymorphic relations, as they share the 
73
        // same process. 
74
75
        switch ($this->entityMap->getInheritanceType()) {
76
            case 'single_table':
77
                return $this->buildUsingSingleTableInheritance($results);
78
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
79
80
            default:
81
                return $this->buildWithDefaultMapper($results);
82
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
83
        }
84
    }
85
86
    /**  
87
     * Launch queries on eager loaded relationships
88
     * 
89
     * @return array
90
     */
91
    protected function queryEagerLoadedRelationships(array $results, array $eagerLoads)
92
    {
93
        $this->eagerLoads = $this->parseRelations($eagerLoads);
94
95
        return $this->eagerLoadRelations($results);
96
    }
97
98
    /**
99
     * Parse a list of relations into individuals.
100
     *
101
     * @param  array $relations
102
     * @return array
103
     */
104
    protected function parseRelations(array $relations)
105
    {
106
        $results = [];
107
108
        foreach ($relations as $name => $constraints) {
109
            // If the "relation" value is actually a numeric key, we can assume that no
110
            // constraints have been specified for the eager load and we'll just put
111
            // an empty Closure with the loader so that we can treat all the same.
112
            if (is_numeric($name)) {
113
                $f = function () {};
114
115
                list($name, $constraints) = [$constraints, $f];
116
            }
117
118
            // We need to separate out any nested includes. Which allows the developers
119
            // to load deep relationships using "dots" without stating each level of
120
            // the relationship with its own key in the array of eager load names.
121
            $results = $this->parseNested($name, $results);
122
123
            $results[$name] = $constraints;
124
        }
125
126
        return $results;
127
    }
128
129
    /**
130
     * Parse the nested relationships in a relation.
131
     *
132
     * @param  string $name
133
     * @param  array  $results
134
     * @return array
135
     */
136
    protected function parseNested($name, $results)
137
    {
138
        $progress = [];
139
140
        // If the relation has already been set on the result array, we will not set it
141
        // again, since that would override any constraints that were already placed
142
        // on the relationships. We will only set the ones that are not specified.
143
        foreach (explode('.', $name) as $segment) {
144
            $progress[] = $segment;
145
146
            if (!isset($results[$last = implode('.', $progress)])) {
147
                $results[$last] = function () {};
148
            }
149
        }
150
151
        return $results;
152
    }
153
154
    /**
155
     * Eager load the relationships on a result set
156
     *
157
     * @param  array $results
158
     * @return array
159
     */
160
    public function eagerLoadRelations(array $results)
161
    {
162
        foreach ($this->eagerLoads as $name => $constraints) {
163
164
            // For nested eager loads we'll skip loading them here and they will be set as an
165
            // eager load on the query to retrieve the relation so that they will be eager
166
            // loaded on that query, because that is where they get hydrated as models.
167
            if (strpos($name, '.') === false) {
168
                $results = $this->loadRelation($results, $name, $constraints);
169
            }
170
        }
171
172
        return $results;
173
    }
174
175
    /**
176
     * Eagerly load the relationship on a set of entities.
177
     *
178
     * @param  array    $results
179
     * @param  string   $name
180
     * @param  \Closure $constraints
181
     * @return array
182
     */
183
    protected function loadRelation(array $results, $name, Closure $constraints) : array
184
    {
185
        // First we will "back up" the existing where conditions on the query so we can
186
        // add our eager constraints. Then we will merge the wheres that were on the
187
        // query back to it in order that any where conditions might be specified.
188
        $relation = $this->getRelation($name);
189
190
        $relation->addEagerConstraints($results);
191
192
        call_user_func($constraints, $relation);
193
194
        // Once we have the results, we just match those back up to their parent models
195
        // using the relationship instance. Then we just return the finished arrays
196
        // of models which have been eagerly hydrated and are readied for return.
197
198
        // Same, this step isn't necessary, as we take the inverse approach than Eloquent :
199
        // filling the attributes before hydration, for more efficiency
200
        //$relationshipResults = $relation->getEager();
0 ignored issues
show
Unused Code Comprehensibility introduced by
60% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
201
202
        return $relation->match($results, $name);
203
    }
204
205
    /**
206
     * Get the relation instance for the given relation name.
207
     *
208
     * @param  string $relation
209
     * @return \Analogue\ORM\Relationships\Relationship
210
     */
211
    public function getRelation($relation)
212
    {
213
        // We want to run a relationship query without any constrains so that we will
214
        // not have to remove these where clauses manually which gets really hacky
215
        // and is error prone while we remove the developer's own where clauses.
216
        $query = Relationship::noConstraints(function () use ($relation) {
217
            return $this->entityMap->$relation($this->defaultMapper->newInstance());
218
        });
219
220
        $nested = $this->nestedRelations($relation);
221
222
        // If there are nested relationships set on the query, we will put those onto
223
        // the query instances so that they can be handled after this relationship
224
        // is loaded. In this way they will all trickle down as they are loaded.
225
        if (count($nested) > 0) {
226
            $query->getQuery()->with($nested);
227
        }
228
229
        return $query;
230
    }
231
232
    /**
233
     * Get the deeply nested relations for a given top-level relation.
234
     *
235
     * @param  string $relation
236
     * @return array
237
     */
238
    protected function nestedRelations($relation)
239
    {
240
        $nested = [];
241
242
        // We are basically looking for any relationships that are nested deeper than
243
        // the given top-level relationship. We will just check for any relations
244
        // that start with the given top relations and adds them to our arrays.
245
        foreach ($this->eagerLoads as $name => $constraints) {
246
            if ($this->isNested($name, $relation)) {
247
                $nested[substr($name, strlen($relation . '.'))] = $constraints;
248
            }
249
        }
250
251
        return $nested;
252
    }
253
254
    /**
255
     * Determine if the relationship is nested.
256
     *
257
     * @param  string $name
258
     * @param  string $relation
259
     * @return bool
260
     */
261
    protected function isNested($name, $relation)
262
    {
263
        $dots = str_contains($name, '.');
264
265
        return $dots && starts_with($name, $relation . '.');
266
    }
267
268
    /**
269
     * Build an entity from results, using the default mapper on this builder.
270
     * This is the default build plan when no table inheritance is being used.
271
     *
272
     * @param  array $results
273
     * @return Collection
274
     */
275
    protected function buildWithDefaultMapper(array $results)
276
    {
277
        $builder = new EntityBuilder($this->defaultMapper, array_keys($this->eagerLoads));
278
279
        return collect($results)->map(function($item, $key) use ($builder) {
0 ignored issues
show
Unused Code introduced by
The parameter $key is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
280
281
            return $builder->build($item);
282
        })->all();
283
    }
284
285
    /**
286
     * Build an entity from results, using single table inheritance.
287
     *
288
     * @param  array $results
289
     * @return Collection
290
     */
291
    protected function buildUsingSingleTableInheritance(array $results)
292
    {
293
        return collect($results)->map(function($item, $key) {
0 ignored issues
show
Unused Code introduced by
The parameter $key is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
294
            $builder = $this->builderForResult($item);
295
            return $builder->build($item);
296
        })->all();
297
    }
298
299
    /**
300
     * Given a result array, return the entity builder needed to correctly
301
     * build the result into an entity. If no getDiscriminatorColumnMap property
302
     * has been defined on the EntityMap, we'll assume that the value stored in
303
     * the $type column is the fully qualified class name of the entity and
304
     * we'll use it instead.
305
     *
306
     * @param  array  $result
307
     * @return EntityBuilder
308
     */
309
    protected function builderForResult(array $result)
310
    {
311
        $type = $result[$this->entityMap->getDiscriminatorColumn()];
312
313
        $columnMap = $this->entityMap->getDiscriminatorColumnMap();
314
315
        $class = isset($columnMap[$type]) ? $columnMap[$type]: $type;
316
317
        if (!isset($this->builders[$type])) {
318
            $this->builders[$type] = new EntityBuilder(
319
                Manager::getInstance()->mapper($class), 
320
                array_keys($this->eagerLoads)
321
            );
322
        }
323
324
        return $this->builders[$type];
325
    }
326
}