ApiResource   B
last analyzed

Complexity

Total Complexity 48

Size/Duplication

Total Lines 402
Duplicated Lines 0 %

Coupling/Cohesion

Components 5
Dependencies 9

Test Coverage

Coverage 42.11%

Importance

Changes 0
Metric Value
wmc 48
lcom 5
cbo 9
dl 0
loc 402
ccs 40
cts 95
cp 0.4211
rs 8.5599
c 0
b 0
f 0

22 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A make() 0 4 1
A toArray() 0 4 1
A with() 0 4 1
A additional() 0 6 1
A wrap() 0 4 1
A withoutWrapping() 0 4 1
A withResponse() 0 4 1
A attributes() 0 6 1
A whenLoaded() 0 6 2
A whenPivotLoaded() 0 10 4
A resolve() 0 18 6
A resolveNestedRelations() 0 13 4
A resolveCollection() 0 6 1
A response() 0 6 2
A toResponse() 0 8 2
A when() 0 6 3
B filter() 0 27 8
A merge() 0 13 2
A mergeWhen() 0 4 2
A transform() 0 6 2
A jsonSerialize() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like ApiResource often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ApiResource, and based on these observations, apply Extract Interface, too.

1
<?php namespace Arcanedev\LaravelApiHelper\Http;
2
3
use ArrayAccess;
4
use Illuminate\Container\Container;
5
use Illuminate\Contracts\Routing\UrlRoutable;
6
use Illuminate\Contracts\Support\Arrayable;
7
use Illuminate\Contracts\Support\Responsable;
8
use Illuminate\Http\Resources\DelegatesToResource;
9
use Illuminate\Http\Resources\Json\PaginatedResourceResponse;
10
use Illuminate\Http\Resources\Json\ResourceResponse;
11
use Illuminate\Http\Resources\MergeValue;
12
use Illuminate\Http\Resources\MissingValue;
13
use Illuminate\Pagination\AbstractPaginator;
14
use Illuminate\Support\Arr;
15
use Illuminate\Support\Collection;
16
use JsonSerializable;
17
18
/**
19
 * Class     ApiResource
20
 *
21
 * @package  Arcanedev\LaravelApiHelper\Http
22
 * @author   ARCANEDEV <[email protected]>
23
 *
24
 * @property  mixed|null  pivot
25
 */
26
class ApiResource implements ArrayAccess, JsonSerializable, Responsable, UrlRoutable
27
{
28
    /* -----------------------------------------------------------------
29
     |  Traits
30
     | -----------------------------------------------------------------
31
     */
32
33
    use DelegatesToResource;
34
35
    /* -----------------------------------------------------------------
36
     |  Properties
37
     | -----------------------------------------------------------------
38
     */
39
40
    /**
41
     * The resource instance.
42
     *
43
     * @var mixed
44
     */
45
    public $resource;
46
47
    /**
48
     * The additional data that should be added to the top-level resource array.
49
     *
50
     * @var array
51
     */
52
    public $with = [];
53
54
    /**
55
     * The additional meta data that should be added to the resource response.
56
     *
57
     * Added during response construction by the developer.
58
     *
59
     * @var array
60
     */
61
    public $additional = [];
62
63
    /**
64
     * The "data" wrapper that should be applied.
65
     *
66
     * @var string
67
     */
68
    public static $wrap = 'data';
69
70
    /* -----------------------------------------------------------------
71
     |  Constructor
72
     | -----------------------------------------------------------------
73
     */
74
75
    /**
76
     * PostTransformer constructor.
77
     *
78
     * @param  mixed  $resource
79
     */
80 4
    public function __construct($resource)
81
    {
82 4
        $this->resource = $resource;
83 4
    }
84
85
    /**
86
     * Create a new resource instance.
87
     *
88
     * @param  mixed  $resource
89
     *
90
     * @return static
91
     */
92 4
    public static function make($resource)
93
    {
94 4
        return new static($resource);
95
    }
96
97
    /* -----------------------------------------------------------------
98
     |  Main Methods
99
     | -----------------------------------------------------------------
100
     */
101
102
    /**
103
     * Transform the resource into an array.
104
     *
105
     * @param  \Illuminate\Http\Request  $request
106
     *
107
     * @return array
108
     */
109
    public function toArray($request)
110
    {
111
        return $this->resource->toArray();
112
    }
113
114
    /**
115
     * Get any additional data that should be returned with the resource array.
116
     *
117
     * @param  \Illuminate\Http\Request  $request
118
     *
119
     * @return array
120
     */
121 4
    public function with($request)
122
    {
123 4
        return $this->with;
124
    }
125
126
    /**
127
     * Add additional meta data to the resource response.
128
     *
129
     * @param  array  $data
130
     *
131
     * @return $this
132
     */
133
    public function additional(array $data)
134
    {
135
        $this->additional = $data;
136
137
        return $this;
138
    }
139
140
    /**
141
     * Set the string that should wrap the outer-most resource array.
142
     *
143
     * @param  string  $value
144
     */
145
    public static function wrap($value)
146
    {
147
        static::$wrap = $value;
148
    }
149
150
    /**
151
     * Disable wrapping of the outer-most resource array.
152
     *
153
     * @return void
154
     */
155
    public static function withoutWrapping()
156
    {
157
        static::wrap(null);
158
    }
159
160
161
    /**
162
     * Customize the response for a request.
163
     *
164
     * @param  \Illuminate\Http\Request       $request
165
     * @param  \Illuminate\Http\JsonResponse  $response
166
     *
167
     * @return void
168
     */
169 4
    public function withResponse($request, $response)
170
    {
171
        //
172 4
    }
173
174
    /**
175
     * Merge the given attributes.
176
     *
177
     * @param  array  $attributes
178
     *
179
     * @return \Illuminate\Http\Resources\MergeValue
180
     */
181
    protected function attributes($attributes)
182
    {
183
        return new MergeValue(
184
            Arr::only($this->resource->toArray(), $attributes)
185
        );
186
    }
187
188
    /**
189
     * Retrieve a relationship if it has been loaded.
190
     *
191
     * @param  string  $relationship
192
     *
193
     * @return \Illuminate\Http\Resources\MissingValue|mixed
194
     */
195
    protected function whenLoaded($relationship)
196
    {
197
        return $this->resource->relationLoaded($relationship)
198
            ? $this->resource->{$relationship}
199
            : new MissingValue;
200
    }
201
202
    /**
203
     * Execute a callback if the given pivot table has been loaded.
204
     *
205
     * @param  string  $table
206
     * @param  mixed   $value
207
     * @param  mixed   $default
208
     *
209
     * @return \Illuminate\Http\Resources\MissingValue|mixed
210
     */
211
    protected function whenPivotLoaded($table, $value, $default = null)
212
    {
213
        if (func_num_args() === 2)
214
            $default = new MissingValue;
215
216
        return $this->when(
217
            $this->pivot && ($this->pivot instanceof $table || $this->pivot->getTable() === $table),
218
            ...[$value, $default]
219
        );
220
    }
221
222
    /**
223
     * Resolve the resource to an array.
224
     *
225
     * @param  \Illuminate\Http\Request|null  $request
226
     *
227
     * @return array
228
     */
229 4
    public function resolve($request = null)
230
    {
231 4
        $request = $request ?: Container::getInstance()->make('request');
232
233 4
        if ($this->resource instanceof Collection)
234
            $data = $this->resolveCollection($this->resource, $request);
235 4
        elseif ($this->resource instanceof AbstractPaginator)
236
            $data = $this->resolveCollection($this->resource->getCollection(), $request);
237
        else
238 4
            $data = $this->toArray($request);
239
240 4
        if ($data instanceof Arrayable)
241
            $data = $data->toArray();
242 3
        elseif ($data instanceof JsonSerializable)
0 ignored issues
show
Bug introduced by Unknown
The class JsonSerializable does not exist. Is this class maybe located in a folder that is not analyzed, or in a newer version of your dependencies than listed in your composer.lock/composer.json?
Loading history...
243
            $data = $data->jsonSerialize();
244
245 4
        return $this->resolveNestedRelations((array) $data, $request);
246
    }
247
248
    /**
249
     * Resolve the nested resources to an array.
250
     *
251
     * @param  array                     $data
252
     * @param  \Illuminate\Http\Request  $request
253
     *
254
     * @return array
255
     */
256 4
    protected function resolveNestedRelations($data, $request)
257
    {
258 4
        foreach ($data as $key => $value) {
259 4
            if (is_array($value)) {
260
                $data[$key] = $this->resolveNestedRelations($value, $request);
261
            }
262 3
            elseif ($value instanceof static) {
263 1
                $data[$key] = $value->resolve($request);
264
            }
265
        }
266
267 4
        return $this->filter($data);
268
    }
269
270
    /**
271
     * Resolve the resource to an array.
272
     *
273
     * @param  \Illuminate\Support\Collection  $collection
274
     * @param  \Illuminate\Http\Request|null   $request
275
     *
276
     * @return array
277
     */
278
    public function resolveCollection($collection, $request = null)
279
    {
280
        return $collection->map(function ($item) use ($request) {
281
            return (new static($item))->toArray($request);
282
        })->all();
283
    }
284
285
    /**
286
     * Transform the resource into an HTTP response.
287
     *
288
     * @param  \Illuminate\Http\Request|null  $request
289
     *
290
     * @return \Illuminate\Http\Response
291
     */
292 4
    public function response($request = null)
293
    {
294 4
        return $this->toResponse(
295 4
            $request ?: Container::getInstance()->make('request')
296
        );
297
    }
298
299
    /**
300
     * Create an HTTP response that represents the object.
301
     *
302
     * @param  \Illuminate\Http\Request  $request
303
     *
304
     * @return \Illuminate\Http\Response|mixed
305
     */
306 4
    public function toResponse($request)
307
    {
308
        return (
0 ignored issues
show
Bug Best Practice introduced by Unknown
The return type of return ($this->resource ...->toResponse($request); (Illuminate\Http\JsonResponse) is incompatible with the return type declared by the interface Illuminate\Contracts\Sup...Responsable::toResponse of type Illuminate\Http\Response.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
309 4
            $this->resource instanceof AbstractPaginator
310
                ? new PaginatedResourceResponse($this)
311 4
                : new ResourceResponse($this)
312 4
        )->toResponse($request);
313
    }
314
315
    /**
316
     * Retrieve a value based on a given condition.
317
     *
318
     * @param  bool   $condition
319
     * @param  mixed  $value
320
     * @param  mixed  $default
321
     *
322
     * @return \Illuminate\Http\Resources\MissingValue|mixed
323
     */
324
    protected function when($condition, $value, $default = null)
325
    {
326
        return $condition
327
            ? value($value)
328
            : (func_num_args() === 3 ? value($default) : new MissingValue);
329
    }
330
331
    /**
332
     * Filter the given data, removing any optional values.
333
     *
334
     * @param  array  $data
335
     *
336
     * @return array
337
     */
338 4
    protected function filter($data)
339
    {
340 4
        $index = -1;
341
342 4
        foreach ($data as $key => $value) {
343 4
            $index++;
344
345 4
            if (is_array($value)) {
346
                $data[$key] = $this->filter($value);
347
348
                continue;
349
            }
350
351 4
            if (is_numeric($key) && $value instanceof MergeValue) {
352
                return $this->merge($data, $index, $this->filter($value->data));
353
            }
354
355
            if (
356 4
                $value instanceof MissingValue ||
357 3
                ($value instanceof self && $value->resource instanceof MissingValue)
358
            ) {
359 1
                unset($data[$key]);
360
            }
361
        }
362
363 4
        return $data;
364
    }
365
366
    /**
367
     * Merge the given data in at the given index.
368
     *
369
     * @param  array  $data
370
     * @param  int    $index
371
     * @param  array  $merge
372
     *
373
     * @return array
374
     */
375
    protected function merge($data, $index, $merge)
376
    {
377
        if (array_values($data) === $data) {
378
            return array_merge(
379
                array_merge(array_slice($data, 0, $index, true), $merge),
380
                $this->filter(array_slice($data, $index + 1, null, true))
381
            );
382
        }
383
384
        return array_slice($data, 0, $index, true) +
385
            $merge +
386
            $this->filter(array_slice($data, $index + 1, null, true));
387
    }
388
389
    /**
390
     * Merge a value based on a given condition.
391
     *
392
     * @param  bool   $condition
393
     * @param  mixed  $value
394
     *
395
     * @return \Illuminate\Http\Resources\MissingValue|mixed
396
     */
397
    protected function mergeWhen($condition, $value)
398
    {
399
        return $condition ? new MergeValue(value($value)) : new MissingValue;
400
    }
401
402
    /**
403
     * Transform the given value if it is present.
404
     *
405
     * @param  mixed     $value
406
     * @param  callable  $callback
407
     * @param  mixed     $default
408
     *
409
     * @return mixed
410
     */
411
    protected function transform($value, callable $callback, $default = null)
412
    {
413
        return transform(
414
            $value, $callback, func_num_args() === 3 ? $default : new MissingValue
415
        );
416
    }
417
418
    /**
419
     * Prepare the resource for JSON serialization.
420
     *
421
     * @return array
422
     */
423
    public function jsonSerialize()
424
    {
425
        return $this->resolve(Container::getInstance()->make('request'));
426
    }
427
}
428