Completed
Push — master ( bae989...d1e271 )
by Alexander
06:14
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 Illuminate\Support\Collection;
15
use InvalidArgumentException;
16
use League\Fractal\Manager;
17
use League\Fractal\Resource\ResourceInterface;
18
use League\Fractal\Serializer\SerializerAbstract;
19
20
/**
21
 * This class is a response builder for building successful JSON API responses and is
22
 * responsible for transforming and serializing the data.
23
 *
24
 * @package flugger/laravel-responder
25
 * @author  Alexander Tømmerås <[email protected]>
26
 * @license The MIT License
27
 */
28
class SuccessResponseBuilder extends ResponseBuilder
29
{
30
    /**
31
     * The manager responsible for transforming and serializing data.
32
     *
33
     * @var \League\Fractal\Manager
34
     */
35
    protected $manager;
36
37
    /**
38
     * The meta data appended to the serialized data.
39
     *
40
     * @var array
41
     */
42
    protected $meta = [];
43
44
    /**
45
     * The Fractal resource instance containing the data and transformer.
46
     *
47
     * @var \League\Fractal\Resource\ResourceInterface
48
     */
49
    protected $resource;
50
51
    /**
52
     * The resource factory used to generate resource instances.
53
     *
54
     * @var \Flugg\Responder\ResourceFactory
55
     */
56
    protected $resourceFactory;
57
58
    /**
59
     * The HTTP status code for the response.
60
     *
61
     * @var int
62
     */
63
    protected $statusCode = 200;
64
65
    /**
66
     * SuccessResponseBuilder constructor.
67
     *
68
     * @param \Illuminate\Contracts\Routing\ResponseFactory $responseFactory
69
     * @param \Flugg\Responder\ResourceFactory              $resourceFactory
70
     * @param \League\Fractal\Manager                       $manager
71
     */
72
    public function __construct(ResponseFactory $responseFactory, ResourceFactory $resourceFactory, Manager $manager)
73
    {
74
        $this->resourceFactory = $resourceFactory;
75
        $this->manager = $manager;
76
        $this->resource = $this->resourceFactory->make();
77
78
        parent::__construct($responseFactory);
79
    }
80
81
    /**
82
     * Add data to the meta data appended to the response data.
83
     *
84
     * @param  array $data
85
     * @return self
86
     */
87
    public function addMeta(array $data):SuccessResponseBuilder
88
    {
89
        $this->meta = array_merge($this->meta, $data);
90
91
        return $this;
92
    }
93
94
    /**
95
     * Set the serializer used to serialize the resource data.
96
     *
97
     * @param  \League\Fractal\Serializer\SerializerAbstract|string $serializer
98
     * @return self
99
     */
100
    public function serializer($serializer):SuccessResponseBuilder
101
    {
102
        $this->manager->setSerializer($this->resolveSerializer($serializer));
103
104
        return $this;
105
    }
106
107
    /**
108
     * Set the HTTP status code for the response.
109
     *
110
     * @param  int $statusCode
111
     * @return self
112
     * @throws \InvalidArgumentException
113
     */
114 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...
115
    {
116
        if ($statusCode < 100 || $statusCode >= 400) {
117
            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...
118
        }
119
120
        return parent::setStatus($statusCode);
121
    }
122
123
    /**
124
     * Set the transformation data. This will set a new resource instance on the response
125
     * builder depending on what type of data is provided.
126
     *
127
     * @param  mixed|null           $data
128
     * @param  callable|string|null $transformer
129
     * @param  string|null          $resourceKey
130
     * @return self
131
     */
132
    public function transform($data = null, $transformer = null, string $resourceKey = null):SuccessResponseBuilder
133
    {
134
        $resource = $this->resourceFactory->make($data);
135
136
        if (! is_null($resource->getData())) {
137
            $model = $this->resolveModel($resource->getData());
138
            $transformer = $this->resolveTransformer($model, $transformer);
139
            $resourceKey = $this->resolveResourceKey($model, $resourceKey);
140
        }
141
142
        $this->resource = $resource->setTransformer($transformer)->setResourceKey($resourceKey);
0 ignored issues
show
Bug introduced by
It seems like $transformer defined by parameter $transformer on line 132 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...
143
144
        return $this;
145
    }
146
147
    /**
148
     * Convert the response to an array.
149
     *
150
     * @return array
151
     */
152
    public function toArray():array
153
    {
154
        return $this->serialize($this->getResource());
155
    }
156
157
    /**
158
     * Get the Fractal resource instance.
159
     *
160
     * @return \League\Fractal\Resource\ResourceInterface
161
     */
162
    public function getResource():ResourceInterface
163
    {
164
        return $this->resource->setMeta($this->meta);
165
    }
166
167
    /**
168
     * Get the Fractal manager responsible for transforming and serializing the data.
169
     *
170
     * @return \League\Fractal\Manager
171
     */
172
    public function getManager():Manager
173
    {
174
        return $this->manager;
175
    }
176
177
    /**
178
     * Resolve a serializer instance from the value.
179
     *
180
     * @param  \League\Fractal\Serializer\SerializerAbstract|string $serializer
181
     * @return \League\Fractal\Serializer\SerializerAbstract
182
     * @throws \Flugg\Responder\Exceptions\InvalidSerializerException
183
     */
184
    protected function resolveSerializer($serializer):SerializerAbstract
185
    {
186
        if (is_string($serializer)) {
187
            $serializer = new $serializer;
188
        }
189
190
        if (! $serializer instanceof SerializerAbstract) {
191
            throw new InvalidSerializerException();
192
        }
193
194
        return $serializer;
195
    }
196
197
    /**
198
     * Resolve a model instance from the data.
199
     *
200
     * @param  \Illuminate\Database\Eloquent\Model|array $data
201
     * @return \Illuminate\Database\Eloquent\Model
202
     * @throws \InvalidArgumentException
203
     */
204
    protected function resolveModel($data):Model
205
    {
206
        if ($data instanceof Model) {
207
            return $data;
208
        }
209
210
        $model = array_values($data)[0];
211
        if (! $model instanceof Model) {
212
            throw new InvalidArgumentException('You can only transform data containing Eloquent models.');
213
        }
214
215
        return $model;
216
    }
217
218
    /**
219
     * Resolve a transformer.
220
     *
221
     * @param  \Illuminate\Database\ELoquent\Model        $model
222
     * @param  \Flugg\Responder\Transformer|callable|null $transformer
223
     * @return \Flugg\Responder\Transformer|callable
224
     */
225
    protected function resolveTransformer(Model $model, $transformer = null)
226
    {
227
        $transformer = $transformer ?: $this->resolveTransformerFromModel($model);
228
229
        if (is_string($transformer)) {
230
            $transformer = new $transformer;
231
        }
232
233
        return $this->parseTransformer($transformer, $model);
234
    }
235
236
    /**
237
     * Resolve a transformer from the model. If the model is not transformable, a closure
238
     * based transformer will be created instead, from the model's fillable attributes.
239
     *
240
     * @param  \Illuminate\Database\ELoquent\Model $model
241
     * @return \Flugg\Responder\Transformer|callable
242
     */
243
    protected function resolveTransformerFromModel(Model $model)
244
    {
245
        if (! $model instanceof Transformable) {
246
            return function () use ($model) {
247
                return $model->toArray();
248
            };
249
        }
250
251
        return $model::transformer();
252
    }
253
254
    /**
255
     * Parse a transformer class and set relations.
256
     *
257
     * @param  \Flugg\Responder\Transformer|callable $transformer
258
     * @param  \Illuminate\Database\ELoquent\Model   $model
259
     * @return \Flugg\Responder\Transformer|callable
260
     * @throws \InvalidTransformerException
261
     */
262
    protected function parseTransformer($transformer, Model $model)
263
    {
264
        if ($transformer instanceof Transformer) {
265
            $transformer = $transformer->setRelations($this->resolveRelations($model));
266
            $this->manager->parseIncludes(array_merge($transformer->getRelations(), $this->resolveNestedRelations($model)));
267
268
        } elseif (! is_callable($transformer)) {
269
            throw new InvalidTransformerException($model);
270
        }
271
272
        return $transformer;
273
    }
274
275
    /**
276
     * Resolve eager loaded relations from the model.
277
     *
278
     * @param  \Illuminate\Database\Eloquent\Model $model
279
     * @return array
280
     */
281
    protected function resolveRelations(Model $model):array
282
    {
283
        return array_keys($model->getRelations());
284
    }
285
286
    /**
287
     * Resolve eager loaded relations from the model including any nested relations.
288
     *
289
     * @param  \Illuminate\Database\Eloquent\Model $model
290
     * @return array
291
     */
292
    protected function resolveNestedRelations(Model $model):array
293
    {
294
        $relations = $model->getRelations();
295
        $keys = array_keys($relations);
296
297
        foreach ($relations as $key => $relation) {
298
            $relation = $relation instanceof Collection ? $relation->first() : $relation;
299
            if (! is_null($relation)) {
300
                $keys = array_merge($keys, array_map(function ($nestedRelation) use ($key) {
301
                    return $key . '.' . $nestedRelation;
302
                }, $this->resolveNestedRelations($relation)));
303
            }
304
        }
305
306
        return $keys;
307
    }
308
309
    /**
310
     * Resolve the resource key from the model.
311
     *
312
     * @param  \Illuminate\Database\Eloquent\Model $model
313
     * @param  string|null                         $resourceKey
314
     * @return string
315
     */
316
    protected function resolveResourceKey(Model $model, string $resourceKey = null):string
317
    {
318
        if (! is_null($resourceKey)) {
319
            return $resourceKey;
320
        }
321
322
        if (method_exists($model, 'getResourceKey')) {
323
            return $model->getResourceKey();
324
        }
325
326
        return $model->getTable();
327
    }
328
329
    /**
330
     * Serialize the transformation data.
331
     *
332
     * @param  ResourceInterface $resource
333
     * @return array
334
     */
335
    protected function serialize(ResourceInterface $resource):array
336
    {
337
        return $this->manager->createData($resource)->toArray();
338
    }
339
}
0 ignored issues
show
Coding Style introduced by
As per coding style, files should not end with a newline character.

This check marks files that end in a newline character, i.e. an empy line.

Loading history...
340