Completed
Pull Request — master (#12)
by Jodie
02:53
created

Smokescreen::include()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 5
c 0
b 0
f 0
rs 9.4285
cc 2
eloc 3
nc 2
nop 1
1
<?php
2
3
namespace Rexlabs\Laravel\Smokescreen;
4
5
use Illuminate\Contracts\Support\Arrayable;
6
use Illuminate\Contracts\Support\Jsonable;
7
use Illuminate\Contracts\Support\Responsable;
8
use Illuminate\Database\Eloquent\Builder;
9
use Illuminate\Database\Eloquent\Collection;
10
use Illuminate\Database\Eloquent\Model;
11
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
12
use Illuminate\Database\Eloquent\Relations\HasMany;
13
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
14
use Illuminate\Database\Eloquent\Relations\Relation;
15
use Illuminate\Http\JsonResponse;
16
use Illuminate\Http\Request;
17
use Illuminate\Http\Response;
18
use Illuminate\Pagination\LengthAwarePaginator;
19
use Illuminate\Support\Arr;
20
use Rexlabs\Laravel\Smokescreen\Pagination\Paginator as PaginatorBridge;
21
use Rexlabs\Laravel\Smokescreen\Relations\RelationLoader;
22
use Rexlabs\Laravel\Smokescreen\Resources\CollectionResource;
23
use Rexlabs\Laravel\Smokescreen\Resources\ItemResource;
24
use Rexlabs\Laravel\Smokescreen\Transformers\TransformerResolver;
25
use Rexlabs\Smokescreen\Exception\MissingResourceException;
26
use Rexlabs\Smokescreen\Helpers\JsonHelper;
27
use Rexlabs\Smokescreen\Relations\RelationLoaderInterface;
28
use Rexlabs\Smokescreen\Resource\Item;
29
use Rexlabs\Smokescreen\Resource\ResourceInterface;
30
use Rexlabs\Smokescreen\Serializer\SerializerInterface;
31
use Rexlabs\Smokescreen\Transformer\TransformerInterface;
32
use Rexlabs\Smokescreen\Transformer\TransformerResolverInterface;
33
34
/**
35
 * Smokescreen for Laravel.
36
 * Tightly integrates the rexlabs/smokescreen resource transformation library with the Laravel framework.
37
 *
38
 * @author    Jodie Dunlop <[email protected]>
39
 * @copyright Rex Software 2018
40
 */
41
class Smokescreen implements \JsonSerializable, Jsonable, Arrayable, Responsable
42
{
43
    const TYPE_ITEM_RESOURCE = 'item';
44
    const TYPE_COLLECTION_RESOURCE = 'collection';
45
    const TYPE_AMBIGUOUS_RESOURCE = 'ambiguous';
46
47
    /** @var \Rexlabs\Smokescreen\Smokescreen */
48
    protected $smokescreen;
49
50
    /** @var string|null */
51
    protected $includes;
52
53
    /** @var string|bool Whether includes should be parsed from a request key */
54
    protected $autoParseIncludes = true;
55
56
    /** @var SerializerInterface|null */
57
    protected $serializer;
58
59
    /** @var Request|null */
60
    protected $request;
61
62
    /** @var Response|null */
63
    protected $response;
64
65
    /** @var ResourceInterface|null */
66
    protected $resource;
67
68
    /** @var array */
69
    protected $config;
70
71
    /** @var array */
72
    protected $injections;
73
74
    /**
75
     * Smokescreen constructor.
76
     *
77
     * @param \Rexlabs\Smokescreen\Smokescreen $smokescreen
78
     * @param array                            $config
79
     */
80
    public function __construct(\Rexlabs\Smokescreen\Smokescreen $smokescreen, array $config = [])
81
    {
82
        $this->smokescreen = $smokescreen;
83
        $this->setConfig($config);
84
    }
85
86
    /**
87
     * Creates a new Smokescreen object.
88
     *
89
     * @param \Rexlabs\Smokescreen\Smokescreen|null $smokescreen
90
     * @param array                                 $config
91
     *
92
     * @return static
93
     */
94
    public static function make(\Rexlabs\Smokescreen\Smokescreen $smokescreen = null, array $config = [])
95
    {
96
        return new static($smokescreen ?? new \Rexlabs\Smokescreen\Smokescreen(), $config);
97
    }
98
99
    /**
100
     * Set the resource (item or collection) data to be transformed.
101
     * You should pass in an instance of a Model.
102
     *
103
     * @param mixed|Model|array                  $data
104
     * @param callable|TransformerInterface|null $transformer
105
     * @param string|null                        $resourceKey
106
     *
107
     * @throws \Rexlabs\Smokescreen\Exception\InvalidTransformerException
108
     *
109
     * @return $this|\Illuminate\Contracts\Support\Responsable
110
     */
111
    public function transform($data, $transformer = null, $resourceKey = null)
112
    {
113
        switch ($this->determineResourceType($data)) {
114
            case self::TYPE_ITEM_RESOURCE:
115
                $this->item($data, $transformer, $resourceKey);
116
                break;
117
            case self::TYPE_COLLECTION_RESOURCE:
118
                $this->collection($data, $transformer, $resourceKey);
119
                break;
120
            default:
121
                $this->item($data, $transformer, $resourceKey);
122
                break;
123
        }
124
125
        return $this;
126
    }
127
128
    /**
129
     * @param mixed $data
130
     *
131
     * @return string
132
     */
133
    public function determineResourceType($data): string
134
    {
135
        if ($data instanceof ItemResource) {
136
            // Explicitly declared itself as an Item
137
            return self::TYPE_ITEM_RESOURCE;
138
        }
139
140
        if ($data instanceof CollectionResource) {
141
            // Explicitly declared itself as a Collection
142
            return self::TYPE_COLLECTION_RESOURCE;
143
        }
144
145
        if ($data instanceof Model) {
146
            // Eloquent model treated as an item by default
147
            return self::TYPE_ITEM_RESOURCE;
148
        }
149
150
        if ($data instanceof Collection) {
151
            // Is an instance or extended class of Laravel Support\Collection
152
            return self::TYPE_COLLECTION_RESOURCE;
153
        }
154
155
        if ($data instanceof \Illuminate\Database\Eloquent\Builder || $data instanceof \Illuminate\Database\Query\Builder) {
156
            // Treat query builders as a collection
157
            return self::TYPE_COLLECTION_RESOURCE;
158
        }
159
160
        if ($data instanceof LengthAwarePaginator) {
161
            // Is an instance of Pagination
162
            return self::TYPE_COLLECTION_RESOURCE;
163
        }
164
165
        if ($data instanceof HasMany || $data instanceof HasManyThrough || $data instanceof BelongsToMany) {
166
            // Many relationships are treated as a collection
167
            return self::TYPE_COLLECTION_RESOURCE;
168
        }
169
170
        if ($data instanceof Arrayable) {
171
            // Get array data for Arrayable so that we can determine resource type
172
            $data = $data->toArray();
173
        }
174
175
        if (\is_array($data)) {
176
            // Handle plain arrays
177
            if (Arr::isAssoc($data)) {
178
                // Associative arrays are treated as items
179
                return self::TYPE_ITEM_RESOURCE;
180
            }
181
182
            // All other arrays are considered collections
183
            return self::TYPE_COLLECTION_RESOURCE;
184
        }
185
186
        // Everything else is ambiguous resource type
187
        return self::TYPE_AMBIGUOUS_RESOURCE;
188
    }
189
190
    /**
191
     * Set an item resource to be transformed.
192
     *
193
     * @param mixed                              $data
194
     * @param callable|TransformerInterface|null $transformer
195
     * @param string|null                        $resourceKey
196
     *
197
     * @throws \Rexlabs\Smokescreen\Exception\InvalidTransformerException
198
     *
199
     * @return $this|\Illuminate\Contracts\Support\Responsable
200
     */
201
    public function item($data, $transformer = null, $resourceKey = null)
202
    {
203
        $this->setResource(new Item($data, $transformer, $resourceKey));
204
205
        return $this;
206
    }
207
208
    /**
209
     * Set a collection resource to be transformed.
210
     *
211
     * @param mixed                              $data
212
     * @param callable|TransformerInterface|null $transformer
213
     * @param string|null                        $resourceKey
214
     *
215
     * @throws \Rexlabs\Smokescreen\Exception\InvalidTransformerException
216
     *
217
     * @return $this|\Illuminate\Contracts\Support\Responsable
218
     */
219
    public function collection($data, $transformer = null, $resourceKey = null)
220
    {
221
        $paginator = null;
222
223
        if ($data instanceof LengthAwarePaginator) {
224
            $paginator = $data;
225
            $data = $data->getCollection();
226
        } elseif ($data instanceof Relation) {
227
            $data = $data->get();
228
        } elseif ($data instanceof Builder) {
229
            $data = $data->get();
230
        } elseif ($data instanceof Model) {
231
            $data = new Collection([$data]);
232
        }
233
234
        // Create a new collection resource
235
        $resource = new \Rexlabs\Smokescreen\Resource\Collection($data, $transformer, $resourceKey);
236
        if ($paginator !== null) {
237
            // Assign any paginator to the resource
238
            $resource->setPaginator(new PaginatorBridge($paginator));
239
        }
240
        $this->setResource($resource);
241
242
        return $this;
243
    }
244
245
    /**
246
     * Set the transformer used to transform the resource(s).
247
     *
248
     * @param TransformerInterface|callable|null $transformer
249
     *
250
     * @throws \Rexlabs\Smokescreen\Exception\MissingResourceException
251
     *
252
     * @return $this|\Illuminate\Contracts\Support\Responsable
253
     */
254
    public function transformWith($transformer)
255
    {
256
        if ($this->resource === null) {
257
            throw new MissingResourceException('Cannot set transformer before setting resource');
258
        }
259
        $this->resource->setTransformer($transformer);
260
261
        return $this;
262
    }
263
264
    /**
265
     * Set the default serializer to be used for resources which do not have an explictly set serializer.
266
     *
267
     * @param SerializerInterface|null $serializer
268
     *
269
     * @return $this|\Illuminate\Contracts\Support\Responsable
270
     */
271
    public function serializeWith($serializer)
272
    {
273
        $this->serializer = $serializer;
274
275
        return $this;
276
    }
277
278
    /**
279
     * Set the relationship loader.
280
     * The relationship loader takes the relationships defined on a transformer, and eager-loads them.
281
     *
282
     * @param RelationLoaderInterface $relationLoader
283
     *
284
     * @return $this|\Illuminate\Contracts\Support\Responsable
285
     */
286
    public function loadRelationsVia(RelationLoaderInterface $relationLoader)
287
    {
288
        $this->smokescreen->setRelationLoader($relationLoader);
289
290
        return $this;
291
    }
292
293
    /**
294
     * Sets the resolver to be used for locating transformers for resources.
295
     *
296
     * @param TransformerResolverInterface $transformerResolver
297
     *
298
     * @return $this
299
     */
300
    public function resolveTransformerVia(TransformerResolverInterface $transformerResolver)
301
    {
302
        $this->smokescreen->setTransformerResolver($transformerResolver);
303
304
        return $this;
305
    }
306
307
    /**
308
     * Returns an object representation of the transformed/serialized data.
309
     *
310
     * @throws \Rexlabs\Smokescreen\Exception\JsonEncodeException
311
     * @throws \Rexlabs\Smokescreen\Exception\InvalidTransformerException
312
     * @throws \Rexlabs\Laravel\Smokescreen\Exceptions\UnresolvedTransformerException
313
     * @throws \Rexlabs\Smokescreen\Exception\MissingResourceException
314
     *
315
     * @return \stdClass
316
     */
317
    public function toObject(): \stdClass
318
    {
319
        return json_decode($this->toJson(), false);
320
    }
321
322
    /**
323
     * Outputs a JSON string of the resulting transformed and serialized data.
324
     * Implements Laravel's Jsonable interface.
325
     *
326
     * @param int $options
327
     *
328
     * @throws \Rexlabs\Smokescreen\Exception\InvalidTransformerException
329
     * @throws \Rexlabs\Laravel\Smokescreen\Exceptions\UnresolvedTransformerException
330
     * @throws \Rexlabs\Smokescreen\Exception\JsonEncodeException
331
     * @throws \Rexlabs\Smokescreen\Exception\MissingResourceException
332
     *
333
     * @return string
334
     */
335
    public function toJson($options = 0): string
336
    {
337
        return JsonHelper::encode($this->jsonSerialize(), $options);
338
    }
339
340
    /**
341
     * Output the transformed and serialized data as an array.
342
     * Implements PHP's JsonSerializable interface.
343
     *
344
     * @throws \Rexlabs\Smokescreen\Exception\InvalidTransformerException
345
     * @throws \Rexlabs\Laravel\Smokescreen\Exceptions\UnresolvedTransformerException
346
     * @throws \Rexlabs\Smokescreen\Exception\MissingResourceException
347
     *
348
     * @return array
349
     *
350
     * @see Smokescreen::toArray()
351
     * @throws \Rexlabs\Smokescreen\Exception\IncludeException
352
     */
353
    public function jsonSerialize(): array
354
    {
355
        return $this->toArray();
356
    }
357
358
    /**
359
     * Inject some data into the payload under given key (supports dot-notation).
360
     * This method can be called multiple times.
361
     * @param string $key
362
     * @param mixed $data
363
     *
364
     * @return $this
365
     */
366
    public function inject($key, $data)
367
    {
368
        $this->injections[$key] = $data;
369
370
        return $this;
371
    }
372
373
    /**
374
     * Output the transformed and serialized data as an array.
375
     * This kicks off the transformation via the base Smokescreen object.
376
     *
377
     * @throws \Rexlabs\Smokescreen\Exception\UnhandledResourceType
378
     * @throws \Rexlabs\Laravel\Smokescreen\Exceptions\UnresolvedTransformerException
379
     * @throws \Rexlabs\Smokescreen\Exception\InvalidTransformerException
380
     * @throws \Rexlabs\Smokescreen\Exception\MissingResourceException
381
     *
382
     * @return array
383
     * @throws \Rexlabs\Smokescreen\Exception\IncludeException
384
     */
385
    public function toArray(): array
386
    {
387
        // We must have a resource provided to transform.
388
        if ($this->resource === null) {
389
            throw new MissingResourceException('Resource is not defined');
390
        }
391
392
        // Assign the resource in the base instance.
393
        $this->smokescreen->setResource($this->resource);
394
395
        // Serializer may be overridden via config.
396
        // We may be setting the serializer to null, in which case a default
397
        // will be provided.
398
        $serializer = $this->serializer ?? null;
399
        $this->smokescreen->setSerializer($serializer);
400
401
        // Assign any includes.
402
        if ($this->includes) {
403
            // Includes have been set explicitly.
404
            $this->smokescreen->parseIncludes($this->includes);
405
        } elseif ($this->autoParseIncludes) {
406
            // If autoParseIncludes is not false, then try to parse from the
407
            // request object.
408
            $this->smokescreen->parseIncludes((string) $this->request()->input($this->getIncludeKey()));
409
        } else {
410
            // Empty includes
411
            $this->smokescreen->parseIncludes('');
412
        }
413
414
        // Provide a custom transformer resolver which can interrogate the
415
        // underlying model and attempt to resolve a transformer class.
416
        if ($this->smokescreen->getTransformerResolver() === null) {
417
            $this->smokescreen->setTransformerResolver(
418
                new TransformerResolver($this->config['transformer_namespace'] ?? 'App\\Transformers')
419
            );
420
        }
421
422
        // We will provide the Laravel relationship loader if none has already
423
        // been explicitly defined.
424
        if (!$this->smokescreen->hasRelationLoader()) {
425
            $this->smokescreen->setRelationLoader(new RelationLoader());
426
        }
427
428
        // Kick off the transformation via the Smokescreen base library.
429
        $data = $this->smokescreen->toArray();
430
        if (!empty($this->injections)) {
431
            foreach ($this->injections as $key => $inject) {
432
                Arr::set($data, $key, $inject);
433
            }
434
        }
435
436
        return $data;
437
    }
438
439
    /**
440
     * Get a Laravel request object.  If not set explicitly via setRequest(...) then
441
     * it will be automatically resolved out of the container. You're welcome.
442
     *
443
     * @return \Illuminate\Http\Request
444
     */
445
    public function request(): Request
446
    {
447
        if ($this->request === null) {
448
            // Resolve request out of the container.
449
            $this->request = app('request');
450
        }
451
452
        return $this->request;
453
    }
454
455
    /**
456
     * Determine which key is used for the includes when passing from the Request
457
     * If the autoParseIncludes property is set to a string value this will be used
458
     * otherwise, the 'include_key' from the configuration.
459
     * Defaults to 'include'.
460
     *
461
     * @return string
462
     */
463
    public function getIncludeKey(): string
464
    {
465
        if (\is_string($this->autoParseIncludes)) {
466
            // When set to a string value, indicates the include key
467
            return $this->autoParseIncludes;
468
        }
469
470
        return $this->config['include_key'] ?? 'include';
471
    }
472
473
    /**
474
     * Generates a Response object.
475
     * Implements Laravel's Responsable contract, so that you can return smokescreen object from a controller.
476
     *
477
     * @param \Illuminate\Http\Request $request
478
     *
479
     * @throws \Rexlabs\Smokescreen\Exception\UnhandledResourceType
480
     * @throws \Rexlabs\Smokescreen\Exception\InvalidTransformerException
481
     * @throws \Rexlabs\Laravel\Smokescreen\Exceptions\UnresolvedTransformerException
482
     * @throws \Rexlabs\Smokescreen\Exception\MissingResourceException
483
     *
484
     * @return \Illuminate\Http\JsonResponse|\Illuminate\Http\Response
485
     */
486
    public function toResponse($request)
487
    {
488
        return $this->response();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->response() returns the type Illuminate\Http\JsonResponse which is incompatible with the return type mandated by Illuminate\Contracts\Sup...sponsable::toResponse() of Illuminate\Http\Response.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
489
    }
490
491
    /**
492
     * Return a JsonResponse object containing the resolved/compiled JSON data.
493
     * Note, since the generated Response is cached, consecutive calls to response() will not change the
494
     * response based on the given parameters. You can use withResponse($callback) to easily modify the response,
495
     * or via $this->response()->setStatusCode() etc.
496
     *
497
     * @param int   $statusCode
498
     * @param array $headers
499
     * @param int   $options
500
     *
501
     * @throws \Rexlabs\Smokescreen\Exception\UnhandledResourceType
502
     * @throws \Rexlabs\Smokescreen\Exception\InvalidTransformerException
503
     * @throws \Rexlabs\Laravel\Smokescreen\Exceptions\UnresolvedTransformerException
504
     * @throws \Rexlabs\Smokescreen\Exception\MissingResourceException
505
     *
506
     * @return \Illuminate\Http\JsonResponse
507
     *
508
     * @see Smokescreen::toArray()
509
     */
510
    public function response(int $statusCode = 200, array $headers = [], int $options = 0): JsonResponse
511
    {
512
        // Response will only be generated once. use clearResponse() to clear.
513
        if ($this->response === null) {
514
            $this->response = new JsonResponse($this->toArray(), $statusCode, $headers, $options);
0 ignored issues
show
Documentation Bug introduced by
It seems like new Illuminate\Http\Json...de, $headers, $options) of type Illuminate\Http\JsonResponse is incompatible with the declared type null|Illuminate\Http\Response of property $response.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
515
        }
516
517
        return $this->response;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->response could return the type Illuminate\Http\Response which is incompatible with the type-hinted return Illuminate\Http\JsonResponse. Consider adding an additional type-check to rule them out.
Loading history...
518
    }
519
520
    /**
521
     * Returns a fresh (uncached) response.
522
     * See the response() method.
523
     *
524
     * @param int   $statusCode
525
     * @param array $headers
526
     * @param int   $options
527
     *
528
     * @throws \Rexlabs\Smokescreen\Exception\UnhandledResourceType
529
     * @throws \Rexlabs\Smokescreen\Exception\InvalidTransformerException
530
     * @throws \Rexlabs\Laravel\Smokescreen\Exceptions\UnresolvedTransformerException
531
     * @throws \Rexlabs\Smokescreen\Exception\MissingResourceException
532
     *
533
     * @return \Illuminate\Http\JsonResponse
534
     *
535
     * @see Smokescreen::toArray()
536
     * @see Smokescreen::response()
537
     */
538
    public function freshResponse(int $statusCode = 200, array $headers = [], int $options = 0): JsonResponse
539
    {
540
        $this->clearResponse();
541
542
        return $this->response($statusCode, $headers, $options);
543
    }
544
545
    /**
546
     * Clear the cached response object.
547
     *
548
     * @return $this|\Illuminate\Contracts\Support\Responsable
549
     */
550
    public function clearResponse()
551
    {
552
        $this->response = null;
553
554
        return $this;
555
    }
556
557
    /**
558
     * Apply a callback to the response.  The response will be generated if it has not already been.
559
     *
560
     * @param callable $apply
561
     *
562
     * @throws \Rexlabs\Smokescreen\Exception\UnhandledResourceType
563
     * @throws \Rexlabs\Smokescreen\Exception\InvalidTransformerException
564
     * @throws \Rexlabs\Laravel\Smokescreen\Exceptions\UnresolvedTransformerException
565
     * @throws \Rexlabs\Smokescreen\Exception\MissingResourceException
566
     *
567
     * @return $this|\Illuminate\Contracts\Support\Responsable
568
     */
569
    public function withResponse(callable $apply)
570
    {
571
        $apply($this->response());
572
573
        return $this;
574
    }
575
576
    /**
577
     * Set the include string.
578
     *
579
     * @param string|null $includes
580
     *
581
     * @return $this|\Illuminate\Contracts\Support\Responsable
582
     */
583
    public function include($includes)
584
    {
585
        $this->includes = $includes === null ? $includes : (string) $includes;
586
587
        return $this;
588
    }
589
590
    /**
591
     * Disable all includes.
592
     *
593
     * @return $this|\Illuminate\Contracts\Support\Responsable
594
     */
595
    public function noIncludes()
596
    {
597
        $this->includes = null;
598
        $this->autoParseIncludes = false;
599
600
        return $this;
601
    }
602
603
    /**
604
     * Set the Laravel request object which will be used to resolve parameters.
605
     *
606
     * @param \Illuminate\Http\Request $request
607
     *
608
     * @return $this|\Illuminate\Contracts\Support\Responsable
609
     */
610
    public function setRequest(Request $request)
611
    {
612
        $this->request = $request;
613
614
        return $this;
615
    }
616
617
    /**
618
     * @return null|ResourceInterface
619
     */
620
    public function getResource()
621
    {
622
        return $this->resource;
623
    }
624
625
    /**
626
     * @param $resource null|ResourceInterface
627
     *
628
     * @return Smokescreen
629
     */
630
    public function setResource($resource)
631
    {
632
        $this->resource = $resource;
633
634
        // Clear any cached response when the resource changes
635
        $this->clearResponse();
636
637
        return $this;
638
    }
639
640
    /**
641
     * Get the underlying Smokescreen instance that we are wrapping in our laravel-friendly layer.
642
     *
643
     * @return \Rexlabs\Smokescreen\Smokescreen
644
     */
645
    public function getBaseSmokescreen(): \Rexlabs\Smokescreen\Smokescreen
646
    {
647
        return $this->smokescreen;
648
    }
649
650
    /**
651
     * @param array $config
652
     */
653
    protected function setConfig(array $config)
654
    {
655
        if (!empty($config['default_serializer'])) {
656
            $serializer = $config['default_serializer'];
657
            if (\is_string($serializer)) {
658
                // Given serializer is expected to be a class path
659
                // Instantiate via the container
660
                $serializer = app()->make($serializer);
661
            }
662
            $this->serializeWith($serializer);
663
            unset($config['default_serializer']);
664
        }
665
666
        $this->config = $config;
667
    }
668
}
669