Completed
Push — master ( 9fa93a...252e01 )
by Alexander
03:36
created

SuccessResponseBuilder::resolveSerializer()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 6
c 0
b 0
f 0
nc 4
nop 1
dl 0
loc 12
rs 9.4285
1
<?php
2
3
namespace Flugg\Responder\Http;
4
5
use Flugg\Responder\Contracts\Transformable;
6
use Flugg\Responder\Exceptions\InvalidSerializerException;
7
use Flugg\Responder\Exceptions\InvalidTransformerException;
8
use Flugg\Responder\ResourceFactory;
9
use Flugg\Responder\ResourceResolver;
10
use Flugg\Responder\Transformation;
11
use Flugg\Responder\Transformer;
12
use Illuminate\Contracts\Routing\ResponseFactory;
13
use Illuminate\Database\Eloquent\Model;
14
use InvalidArgumentException;
15
use League\Fractal\Manager;
16
use League\Fractal\Resource\ResourceInterface;
17
use League\Fractal\Serializer\SerializerAbstract;
18
19
/**
20
 * This class is a response builder for building successful JSON API responses and is
21
 * responsible for transforming and serializing the data.
22
 *
23
 * @package flugger/laravel-responder
24
 * @author  Alexander Tømmerås <[email protected]>
25
 * @license The MIT License
26
 */
27
class SuccessResponseBuilder extends ResponseBuilder
28
{
29
    /**
30
     * The manager responsible for transforming and serializing data.
31
     *
32
     * @var \League\Fractal\Manager
33
     */
34
    protected $manager;
35
36
    /**
37
     * The meta data appended to the serialized data.
38
     *
39
     * @var array
40
     */
41
    protected $meta = [];
42
43
    /**
44
     * The included relations.
45
     *
46
     * @var array
47
     */
48
    protected $relations = [];
49
50
    /**
51
     * The Fractal resource instance containing the data and transformer.
52
     *
53
     * @var \League\Fractal\Resource\ResourceInterface
54
     */
55
    protected $resource;
56
57
    /**
58
     * The resource factory used to generate resource instances.
59
     *
60
     * @var \Flugg\Responder\ResourceFactory
61
     */
62
    protected $resourceFactory;
63
64
    /**
65
     * The HTTP status code for the response.
66
     *
67
     * @var int
68
     */
69
    protected $statusCode = 200;
70
71
    /**
72
     * SuccessResponseBuilder constructor.
73
     *
74
     * @param \Illuminate\Contracts\Routing\ResponseFactory $responseFactory
75
     * @param \Flugg\Responder\ResourceFactory              $resourceFactory
76
     * @param \League\Fractal\Manager                       $manager
77
     */
78
    public function __construct(ResponseFactory $responseFactory, ResourceFactory $resourceFactory, Manager $manager)
79
    {
80
        $this->resourceFactory = $resourceFactory;
81
        $this->manager = $manager;
82
        $this->resource = $this->resourceFactory->make();
83
84
        parent::__construct($responseFactory);
85
    }
86
87
    /**
88
     * Add data to the meta data appended to the response data.
89
     *
90
     * @param  array $data
91
     * @return self
92
     */
93
    public function addMeta(array $data):SuccessResponseBuilder
94
    {
95
        $this->meta = array_merge($this->meta, $data);
96
97
        return $this;
98
    }
99
100
    /**
101
     * Set the serializer used to serialize the resource data.
102
     *
103
     * @param  array|string $relations
104
     * @return self
105
     */
106
    public function include($relations):SuccessResponseBuilder
107
    {
108
        $this->relations = array_merge($this->relations, $relations);
109
110
        return $this;
111
    }
112
113
    /**
114
     * Set the serializer used to serialize the resource data.
115
     *
116
     * @param  \League\Fractal\Serializer\SerializerAbstract|string $serializer
117
     * @return self
118
     */
119
    public function serializer($serializer):SuccessResponseBuilder
120
    {
121
        $this->manager->setSerializer($this->resolveSerializer($serializer));
122
123
        return $this;
124
    }
125
126
    /**
127
     * Set the HTTP status code for the response.
128
     *
129
     * @param  int $statusCode
130
     * @return self
131
     * @throws \InvalidArgumentException
132
     */
133 View Code Duplication
    public function setStatus(int $statusCode):ResponseBuilder
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
134
    {
135
        if ($statusCode < 100 || $statusCode >= 400) {
136
            throw new InvalidArgumentException("{$statusCode} is not a valid success HTTP status code.");
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $statusCode instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
137
        }
138
139
        return parent::setStatus($statusCode);
140
    }
141
142
    /**
143
     * Set the transformation data. This will set a new resource instance on the response
144
     * builder depending on what type of data is provided.
145
     *
146
     * @param  mixed|null           $data
147
     * @param  callable|string|null $transformer
148
     * @param  string|null          $resourceKey
149
     * @return self
150
     */
151
    public function transform($data = null, $transformer = null, string $resourceKey = null):SuccessResponseBuilder
152
    {
153
        $resource = $this->resourceFactory->make($data);
154
155
        if (! is_null($resource->getData())) {
156
            $model = $this->resolveModel($resource->getData());
157
            $transformer = $this->resolveTransformer($model, $transformer);
158
            $resourceKey = $this->resolveResourceKey($model, $resourceKey);
159
        }
160
161
        if ($transformer instanceof Transformer) {
162
            $this->include($this->resolveNestedRelations($resource->getData()));
163
            $this->manager->parseIncludes(array_merge($transformer->getRelations(), $this->relations));
164
            $transformer->setRelations($this->manager->getRequestedIncludes());
165
        }
166
167
        $this->resource = $resource->setTransformer($transformer)->setResourceKey($resourceKey);
0 ignored issues
show
Bug introduced by
It seems like $transformer defined by parameter $transformer on line 151 can also be of type null; however, League\Fractal\Resource\...rface::setTransformer() does only seem to accept callable, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
168
169
        return $this;
170
    }
171
172
    /**
173
     * Convert the response to an array.
174
     *
175
     * @return array
176
     */
177
    public function toArray():array
178
    {
179
        return $this->serialize($this->getResource());
180
    }
181
182
    /**
183
     * Get the Fractal resource instance.
184
     *
185
     * @return \League\Fractal\Resource\ResourceInterface
186
     */
187
    public function getResource():ResourceInterface
188
    {
189
        return $this->resource->setMeta($this->meta);
190
    }
191
192
    /**
193
     * Get the Fractal manager responsible for transforming and serializing the data.
194
     *
195
     * @return \League\Fractal\Manager
196
     */
197
    public function getManager():Manager
198
    {
199
        return $this->manager;
200
    }
201
202
    /**
203
     * Resolve a serializer instance from the value.
204
     *
205
     * @param  \League\Fractal\Serializer\SerializerAbstract|string $serializer
206
     * @return \League\Fractal\Serializer\SerializerAbstract
207
     * @throws \Flugg\Responder\Exceptions\InvalidSerializerException
208
     */
209
    protected function resolveSerializer($serializer):SerializerAbstract
210
    {
211
        if (is_string($serializer)) {
212
            $serializer = new $serializer;
213
        }
214
215
        if (! $serializer instanceof SerializerAbstract) {
216
            throw new InvalidSerializerException();
217
        }
218
219
        return $serializer;
220
    }
221
222
    /**
223
     * Resolve a model instance from the data.
224
     *
225
     * @param  \Illuminate\Database\Eloquent\Model|array $data
226
     * @return \Illuminate\Database\Eloquent\Model
227
     * @throws \InvalidArgumentException
228
     */
229
    protected function resolveModel($data):Model
230
    {
231
        if ($data instanceof Model) {
232
            return $data;
233
        }
234
235
        $model = array_values($data)[0];
236
        if (! $model instanceof Model) {
237
            throw new InvalidArgumentException('You can only transform data containing Eloquent models.');
238
        }
239
240
        return $model;
241
    }
242
243
    /**
244
     * Resolve a transformer.
245
     *
246
     * @param  \Illuminate\Database\ELoquent\Model        $model
247
     * @param  \Flugg\Responder\Transformer|callable|null $transformer
248
     * @return \Flugg\Responder\Transformer|callable
249
     */
250
    protected function resolveTransformer(Model $model, $transformer = null)
251
    {
252
        $transformer = $transformer ?: $this->resolveTransformerFromModel($model);
253
254
        if (is_string($transformer)) {
255
            $transformer = new $transformer;
256
        }
257
258
        return $this->parseTransformer($transformer, $model);
259
    }
260
261
    /**
262
     * Resolve a transformer from the model. If the model is not transformable, a closure
263
     * based transformer will be created instead, from the model's fillable attributes.
264
     *
265
     * @param  \Illuminate\Database\ELoquent\Model $model
266
     * @return \Flugg\Responder\Transformer|callable
267
     */
268
    protected function resolveTransformerFromModel(Model $model)
269
    {
270
        if (! $model instanceof Transformable) {
271
            return function ($model) {
272
                return $model->toArray();
273
            };
274
        }
275
276
        return $model::transformer();
277
    }
278
279
    /**
280
     * Parse a transformer class and set relations.
281
     *
282
     * @param  \Flugg\Responder\Transformer|callable $transformer
283
     * @param  \Illuminate\Database\ELoquent\Model   $model
284
     * @return \Flugg\Responder\Transformer|callable
285
     * @throws \InvalidTransformerException
286
     */
287
    protected function parseTransformer($transformer, Model $model)
288
    {
289
        if ($transformer instanceof Transformer) {
290
            $relations = $transformer->allRelationsAllowed() ? $this->resolveRelations($model) : $transformer->getRelations();
291
            $transformer = $transformer->setRelations($relations);
292
293
        } elseif (! is_callable($transformer)) {
294
            throw new InvalidTransformerException($model);
295
        }
296
297
        return $transformer;
298
    }
299
300
    /**
301
     * Resolve eager loaded relations from the model.
302
     *
303
     * @param  \Illuminate\Database\Eloquent\Model $model
304
     * @return array
305
     */
306
    protected function resolveRelations(Model $model):array
307
    {
308
        return array_keys($model->getRelations());
309
    }
310
311
    /**
312
     * Resolve eager loaded relations from the model including any nested relations.
313
     *
314
     * @param  \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model $data
315
     * @return array
316
     */
317
    protected function resolveNestedRelations($data):array
318
    {
319
        if (is_null($data)) {
320
            return [];
321
        }
322
323
        $data = $data instanceof Model ? [$data] : $data;
324
325
        return collect($data)->flatMap(function ($model) {
326
            $relations = collect($model->getRelations());
327
328
            return $relations->keys()->merge($relations->flatMap(function ($relation, $key) {
329
                return collect($this->resolveNestedRelations($relation))->map(function ($nestedRelation) use ($key) {
330
                    return $key . '.' . $nestedRelation;
331
                });
332
            }));
333
        })->toArray();
334
    }
335
336
    /**
337
     * Resolve the resource key from the model.
338
     *
339
     * @param  \Illuminate\Database\Eloquent\Model $model
340
     * @param  string|null                         $resourceKey
341
     * @return string
342
     */
343
    protected function resolveResourceKey(Model $model, string $resourceKey = null):string
344
    {
345
        if (! is_null($resourceKey)) {
346
            return $resourceKey;
347
        }
348
349
        if (method_exists($model, 'getResourceKey')) {
350
            return $model->getResourceKey();
351
        }
352
353
        return $model->getTable();
354
    }
355
356
    /**
357
     * Serialize the transformation data.
358
     *
359
     * @param  ResourceInterface $resource
360
     * @return array
361
     */
362
    protected function serialize(ResourceInterface $resource):array
363
    {
364
        return $this->manager->createData($resource)->toArray();
365
    }
366
}