Completed
Push — master ( dd842b...72a857 )
by Alexander
03:27
created

TransformBuilder::eagerLoadRelations()   A

Complexity

Conditions 3
Paths 1

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 7
nc 1
nop 3
dl 0
loc 14
ccs 7
cts 7
cp 1
crap 3
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace Flugg\Responder;
4
5
use Flugg\Responder\Contracts\Pagination\PaginatorFactory;
6
use Flugg\Responder\Contracts\Resources\ResourceFactory;
7
use Flugg\Responder\Contracts\TransformFactory;
8
use Flugg\Responder\Exceptions\InvalidSuccessSerializerException;
9
use Flugg\Responder\Pagination\CursorPaginator;
10
use Flugg\Responder\Transformers\Transformer as BaseTransformer;
11
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
12
use Illuminate\Database\Eloquent\Collection;
13
use Illuminate\Database\Eloquent\Model;
14
use League\Fractal\Pagination\Cursor;
15
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
16
use League\Fractal\Resource\Collection as CollectionResource;
17
use League\Fractal\Resource\NullResource;
18
use League\Fractal\Serializer\SerializerAbstract;
19
20
/**
21
 * A builder class responsible for building transformed arrays.
22
 *
23
 * @package flugger/laravel-responder
24
 * @author  Alexander Tømmerås <[email protected]>
25
 * @license The MIT License
26
 */
27
class TransformBuilder
28
{
29
    /**
30
     * A factory class for making Fractal resources.
31
     *
32
     * @var \Flugg\Responder\Contracts\Resources\ResourceFactory
33
     */
34
    protected $resourceFactory;
35
36
    /**
37
     * A factory for making transformed arrays.
38
     *
39
     * @var \Flugg\Responder\Contracts\TransformFactory
40
     */
41
    private $transformFactory;
42
43
    /**
44
     * A factory used to build Fractal paginator adapters.
45
     *
46
     * @var \Flugg\Responder\Contracts\Pagination\PaginatorFactory
47
     */
48
    protected $paginatorFactory;
49
50
    /**
51
     * The resource that's being built.
52
     *
53
     * @var \League\Fractal\Resource\ResourceInterface
54
     */
55
    protected $resource;
56
57
    /**
58
     * A serializer for formatting data after transforming.
59
     *
60
     * @var \League\Fractal\Serializer\SerializerAbstract
61
     */
62
    protected $serializer;
63
64
    /**
65
     * A list of included relations.
66
     *
67
     * @var array
68
     */
69
    protected $with = [];
70
71
    /**
72
     * A list of excluded relations.
73
     *
74
     * @var array
75
     */
76
    protected $without = [];
77
78
    /**
79
     * A list of sparse fieldsets.
80
     *
81
     * @var array
82
     */
83
    protected $only = [];
84
85
    /**
86
     * Construct the builder class.
87
     *
88
     * @param \Flugg\Responder\Contracts\Resources\ResourceFactory   $resourceFactory
89
     * @param \Flugg\Responder\Contracts\TransformFactory            $transformFactory
90
     * @param \Flugg\Responder\Contracts\Pagination\PaginatorFactory $paginatorFactory
91
     */
92 66
    public function __construct(ResourceFactory $resourceFactory, TransformFactory $transformFactory, PaginatorFactory $paginatorFactory)
93
    {
94 66
        $this->resourceFactory = $resourceFactory;
95 66
        $this->transformFactory = $transformFactory;
96 66
        $this->paginatorFactory = $paginatorFactory;
97 66
    }
98
99
    /**
100
     * Make a resource from the given data and transformer and set the resource key.
101
     *
102
     * @param  mixed                                                          $data
103
     * @param  \Flugg\Responder\Transformers\Transformer|callable|string|null $transformer
104
     * @param  string|null                                                    $resourceKey
105
     * @return $this
106
     */
107 65
    public function resource($data = null, $transformer = null, string $resourceKey = null)
108
    {
109 65
        $this->resource = $this->resourceFactory->make($data, $transformer, $resourceKey);
110
111 65
        if ($data instanceof CursorPaginator) {
112 1
            $this->cursor($this->paginatorFactory->makeCursor($data));
113
        } elseif ($data instanceof LengthAwarePaginator) {
114 2
            $this->paginator($this->paginatorFactory->make($data));
0 ignored issues
show
Compatibility introduced by
$this->paginatorFactory->make($data) of type object<League\Fractal\Pa...ion\PaginatorInterface> is not a sub-type of object<League\Fractal\Pa...minatePaginatorAdapter>. It seems like you assume a concrete implementation of the interface League\Fractal\Pagination\PaginatorInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
115
        }
116
117 65
        return $this;
118
    }
119
120
    /**
121
     * Manually set the cursor on the resource.
122
     *
123
     * @param  \League\Fractal\Pagination\Cursor $cursor
124
     * @return $this
125
     */
126 3
    public function cursor(Cursor $cursor)
127
    {
128 3
        if ($this->resource instanceof CollectionResource) {
129 3
            $this->resource->setCursor($cursor);
130
        }
131
132 3
        return $this;
133
    }
134
135
    /**
136
     * Manually set the paginator on the resource.
137
     *
138
     * @param  \League\Fractal\Pagination\IlluminatePaginatorAdapter $paginator
139
     * @return $this
140
     */
141 4
    public function paginator(IlluminatePaginatorAdapter $paginator)
142
    {
143 4
        if ($this->resource instanceof CollectionResource) {
144 4
            $this->resource->setPaginator($paginator);
145
        }
146
147 4
        return $this;
148
    }
149
150
    /**
151
     * Add meta data appended to the response data.
152
     *
153
     * @param  array $data
154
     * @return $this
155
     */
156 2
    public function meta(array $data)
157
    {
158 2
        $this->resource->setMeta($data);
159
160 2
        return $this;
161
    }
162
163
    /**
164
     * Include relations to the transform.
165
     *
166
     * @param  string[]|string $relations
167
     * @return $this
168
     */
169 52
    public function with($relations)
170
    {
171 52
        $relations = is_array($relations) ? $relations : func_get_args();
172
173 52
        foreach ($relations as $relation => $constraint) {
174 16
            if (is_numeric($relation)) {
175 15
                $relation = $constraint;
176 15
                $constraint = null;
177
            }
178
179 16
            $this->with = array_merge($this->with, [$relation => $constraint]);
180
        }
181
182 52
        return $this;
183
    }
184
185
    /**
186
     * Exclude relations from the transform.
187
     *
188
     * @param  string[]|string $relations
189
     * @return $this
190
     */
191 3
    public function without($relations)
192
    {
193 3
        $this->without = array_merge($this->without, is_array($relations) ? $relations : func_get_args());
194
195 3
        return $this;
196
    }
197
198
    /**
199
     * Filter fields to output using sparse fieldsets.
200
     *
201
     * @param  string[]|string $fields
202
     * @return $this
203
     */
204 48
    public function only($fields)
205
    {
206 48
        $this->only = array_merge($this->only, is_array($fields) ? $fields : func_get_args());
207
208 48
        return $this;
209
    }
210
211
    /**
212
     * Set the serializer.
213
     *
214
     * @param  \League\Fractal\Serializer\SerializerAbstract|string $serializer
215
     * @return $this
216
     * @throws \Flugg\Responder\Exceptions\InvalidSuccessSerializerException
217
     */
218 66
    public function serializer($serializer)
219
    {
220 66
        if (is_string($serializer)) {
221 5
            $serializer = new $serializer;
222
        }
223
224 66
        if (! $serializer instanceof SerializerAbstract) {
225 1
            throw new InvalidSuccessSerializerException;
226
        }
227
228 66
        $this->serializer = $serializer;
229
230 66
        return $this;
231
    }
232
233
    /**
234
     * Transform and serialize the data and return the transformed array.
235
     *
236
     * @return array|null
237
     */
238 59
    public function transform()
239
    {
240 59
        $this->prepareRelations($this->resource->getData(), $this->resource->getTransformer());
241
242 59
        return $this->transformFactory->make($this->resource ?: new NullResource, $this->serializer, [
243 59
            'includes' => $this->with,
244 59
            'excludes' => $this->without,
245 59
            'fieldsets' => $this->only,
246
        ]);
247
    }
248
249
    /**
250
     * Prepare requested relations for the transformation.
251
     *
252
     * @param  mixed                                                          $data
253
     * @param  \Flugg\Responder\Transformers\Transformer|callable|string|null $transformer
254
     * @return void
255
     */
256 59
    protected function prepareRelations($data, $transformer)
257
    {
258 59
        if ($transformer instanceof BaseTransformer) {
259 17
            $whitelisted = $transformer->whitelistedRelations();
260 17
            $default = $transformer->defaultRelations();
261 17
            $relations = $this->addMissingQueryConstraints($this->with, $whitelisted);
262 17
            $this->with = $this->removeForbiddenRelations(array_merge($relations, $default), array_merge($whitelisted, $default));
263
        }
264
265 59
        if ($data instanceof Model || $data instanceof Collection) {
266 32
            $this->eagerLoadRelations($data, $this->with, $transformer);
267
        }
268
269 59
        $this->with = array_keys($this->with);
270 59
    }
271
272
    /**
273
     * Add query constraints defined as "load" methods in the transformers to the list of
274
     * requested relations.
275
     *
276
     * @param  array $requested
277
     * @param  array $whitelisted
278
     * @return array
279
     */
280
    protected function addMissingQueryConstraints(array $requested, array $whitelisted): array
281
    {
282 17
        return collect(array_keys($requested))->reduce(function ($relations, $relation) use ($requested, $whitelisted) {
283 10
            $constraint = $requested[$relation];
284
285 10
            return array_merge($relations, [$relation => $constraint ? $constraint : $whitelisted[$relation] ?? null]);
286 17
        }, []);
287
    }
288
289
    /**
290
     * Filter out relations that are not whitelisted in the transformer or relations that
291
     * have been explicitly excluded using the [without] method.
292
     *
293
     * @param  array $relations
294
     * @param  array $whitelisted
295
     * @return array
296
     */
297
    protected function removeForbiddenRelations(array $relations, array $whitelisted): array
298
    {
299 17
        return array_filter($relations, function ($relation) use ($whitelisted) {
300 12
            $relation = $this->stripParametersFromRelation($relation);
301
302 12
            return key_exists($relation, $whitelisted) && ! in_array($relation, $this->without);
303 17
        }, ARRAY_FILTER_USE_KEY);
304
    }
305
306
    /**
307
     * Strip parameter suffix from the relation string by only taking what is in front of
308
     * the colon.
309
     *
310
     * @param  string $relation
311
     * @return string
312
     */
313 16
    protected function stripParametersFromRelation(string $relation): string
314
    {
315 16
        return explode(':', $relation)[0];
316
    }
317
318
    /**
319
     * Eager load all requested relations except the ones defined as an "include" method
320
     * in the transformers. We also strip away any parameters from the relation name
321
     * and normalize relations by swapping "null" constraints to empty closures.
322
     *
323
     * @param  mixed                                                          $data
324
     * @param  array                                                          $requested
325
     * @param  \Flugg\Responder\Transformers\Transformer|callable|string|null $transformer
326
     * @return void
327
     */
328
    protected function eagerLoadRelations($data, array $requested, $transformer)
329
    {
330 32
        $relations = collect(array_keys($requested))->reduce(function ($eagerLoads, $relation) use ($requested, $transformer) {
331 15
            $identifier = $this->stripParametersFromRelation($relation);
332
333 15
            if (method_exists($transformer, 'include' . ucfirst($identifier))) {
334 2
                return $eagerLoads;
335
            }
336
337
            return array_merge($eagerLoads, [$identifier => $requested[$relation] ?: function () { }]);
338 32
        }, []);
339
340 32
        $data->load($relations);
341
    }
342
}