Completed
Push — master ( 7420d4...f4d0ab )
by Alexander
05:26
created

SuccessResponseBuilder::addMeta()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 6
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($relations = $this->resolveNestedRelations($resource->getData()));
163
164
            if ($transformer->allRelationsAllowed()) {
165
                $transformer->setRelations($relations);
166
            }
167
        }
168
169
        $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...
170
171
        return $this;
172
    }
173
174
    /**
175
     * Convert the response to an array.
176
     *
177
     * @return array
178
     */
179
    public function toArray():array
180
    {
181
        return $this->serialize($this->getResource());
182
    }
183
184
    /**
185
     * Get the Fractal resource instance.
186
     *
187
     * @return \League\Fractal\Resource\ResourceInterface
188
     */
189
    public function getResource():ResourceInterface
190
    {
191
        $this->manager->parseIncludes($this->relations);
192
        $transformer = $this->resource->getTransformer();
193
194
        if ($transformer instanceof Transformer) {
195
            $this->resource->setTransformer($transformer->setRelations($this->manager->getRequestedIncludes()));
0 ignored issues
show
Documentation introduced by
$transformer->setRelatio...getRequestedIncludes()) is of type object<Flugg\Responder\Transformer>, but the function expects a callable.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

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