Passed
Push — master ( a01295...bde225 )
by Jodie
02:13
created

Smokescreen::setResource()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
1
<?php
2
3
namespace Rexlabs\Smokescreen;
4
5
use Rexlabs\Smokescreen\Exception\InvalidTransformerException;
6
use Rexlabs\Smokescreen\Exception\JsonEncodeException;
7
use Rexlabs\Smokescreen\Exception\MissingResourceException;
8
use Rexlabs\Smokescreen\Exception\UnhandledResourceType;
9
use Rexlabs\Smokescreen\Includes\IncludeParser;
10
use Rexlabs\Smokescreen\Includes\IncludeParserInterface;
11
use Rexlabs\Smokescreen\Includes\Includes;
12
use Rexlabs\Smokescreen\Relations\RelationLoaderInterface;
13
use Rexlabs\Smokescreen\Resource\Collection;
14
use Rexlabs\Smokescreen\Resource\Item;
15
use Rexlabs\Smokescreen\Resource\ResourceInterface;
16
use Rexlabs\Smokescreen\Serializer\DefaultSerializer;
17
use Rexlabs\Smokescreen\Serializer\SerializerInterface;
18
use Rexlabs\Smokescreen\Transformer\TransformerInterface;
19
20
/**
21
 * Smokescreen is a library for transforming and serializing data - typically RESTful API output.
22
 *
23
 * @package Rexlabs\Smokescreen
24
 */
25
class Smokescreen implements \JsonSerializable
26
{
27
    /** @var ResourceInterface Item or Collection to be transformed */
28
    protected $resource;
29
30
    /** @var SerializerInterface */
31
    protected $serializer;
32
33
    /** @var IncludeParserInterface */
34
    protected $includeParser;
35
36
    /** @var RelationLoaderInterface */
37
    protected $relationLoader;
38
39
    /** @var Includes */
40
    protected $includes;
41
42
    /**
43
     * Return the current resource.
44
     *
45
     * @return ResourceInterface|mixed|null
46
     */
47
    public function getResource()
48
    {
49
        return $this->resource;
50
    }
51
52
    /**
53
     * Set the resource to be transformed.
54
     *
55
     * @param ResourceInterface|mixed|null $resource
56
     *
57
     * @return $this
58
     */
59
    public function setResource($resource)
60
    {
61
        $this->resource = $resource;
62
63
        return $this;
64
    }
65
66
    /**
67
     * Set the resource item to be transformed.
68
     *
69
     * @param mixed                              $data
70
     * @param TransformerInterface|callable|null $transformer
71
     * @param string|null                        $key
72
     *
73
     * @return $this
74
     */
75
    public function item($data, $transformer = null, $key = null)
76
    {
77
        $this->setResource(new Item($data, $transformer, $key));
78
79
        return $this;
80
    }
81
82
    /**
83
     * Set the resource collection to be transformed
84
     *
85
     * @param mixed                              $data
86
     * @param TransformerInterface|callable|null $transformer
87
     * @param string|null                        $key
88
     * @param callable|null                      $callback
89
     *
90
     * @return $this
91
     */
92
    public function collection($data, TransformerInterface $transformer = null, $key = null, callable $callback = null)
93
    {
94
        $this->setResource(new Collection($data, $transformer, $key));
95
        if ($callback !== null) {
96
            $callback($this->resource);
97
        }
98
99
        return $this;
100
    }
101
102
    /**
103
     * Sets the transformer to be used to transform the resource ... later
104
     *
105
     * @return TransformerInterface|callable|null
106
     * @throws MissingResourceException
107
     */
108
    public function getTransformer()
109
    {
110
        if (!$this->resource) {
111
            throw new MissingResourceException('Resource must be specified before setting a transformer');
112
        }
113
114
        return $this->resource->getTransformer();
115
    }
116
117
    /**
118
     * Sets the transformer to be used to transform the resource ... later
119
     *
120
     * @param TransformerInterface|callable|null $transformer
121
     *
122
     * @return $this
123
     * @throws MissingResourceException
124
     */
125
    public function setTransformer($transformer = null)
126
    {
127
        if (!$this->resource) {
128
            throw new MissingResourceException('Resource must be specified before setting a transformer');
129
        }
130
        $this->resource->setTransformer($transformer);
131
132
        return $this;
133
    }
134
135
    /**
136
     * Returns an object (stdClass) representation of the transformed/serialized data.
137
     *
138
     * @return \stdClass
139
     * @throws \Rexlabs\Smokescreen\Exception\MissingResourceException
140
     * @throws \Rexlabs\Smokescreen\Exception\InvalidTransformerException
141
     * @throws \Rexlabs\Smokescreen\Exception\JsonEncodeException
142
     */
143
    public function toObject(): \stdClass
144
    {
145
        return (object)json_decode($this->toJson(), false);
146
    }
147
148
    /**
149
     * Outputs a JSON string of the resulting transformed and serialized data.
150
     *
151
     * @param int $options
152
     *
153
     * @return string
154
     * @throws \Rexlabs\Smokescreen\Exception\UnhandledResourceType
155
     * @throws \Rexlabs\Smokescreen\Exception\MissingResourceException
156
     * @throws \Rexlabs\Smokescreen\Exception\InvalidTransformerException
157
     * @throws \Rexlabs\Smokescreen\Exception\JsonEncodeException
158
     */
159
    public function toJson($options = 0): string
160
    {
161
        $json = json_encode($this->jsonSerialize(), $options);
162
        if (JSON_ERROR_NONE !== json_last_error()) {
163
            throw new JsonEncodeException(json_last_error_msg());
164
        }
165
166
        return $json;
167
    }
168
169
    /**
170
     * Output the transformed and serialized data as an array.
171
     * Implements PHP's JsonSerializable interface.
172
     *
173
     * @return array
174
     * @see Smokescreen::toArray()
175
     * @throws \Rexlabs\Smokescreen\Exception\UnhandledResourceType
176
     * @throws \Rexlabs\Smokescreen\Exception\InvalidTransformerException
177
     * @throws \Rexlabs\Smokescreen\Exception\MissingResourceException
178
     */
179
    public function jsonSerialize(): array
180
    {
181
        return $this->toArray();
182
    }
183
184
    /**
185
     * Return the transformed data as an array
186
     *
187
     * @return array
188
     * @throws \Rexlabs\Smokescreen\Exception\UnhandledResourceType
189
     * @throws \Rexlabs\Smokescreen\Exception\InvalidTransformerException
190
     * @throws \Rexlabs\Smokescreen\Exception\MissingResourceException
191
     */
192
    public function toArray(): array
193
    {
194
        if (!$this->resource) {
195
            throw new MissingResourceException('No resource has been defined to transform');
196
        }
197
198
        // Kick of serialization of the resource
199
        return $this->serializeResource($this->resource, $this->getIncludes());
200
    }
201
202
    /**
203
     * @return SerializerInterface
204
     */
205
    public function getSerializer(): SerializerInterface
206
    {
207
        return $this->serializer ?? new DefaultSerializer();
208
    }
209
210
    /**
211
     * Set the serializer which will be used to output the transformed resource
212
     *
213
     * @param SerializerInterface|null $serializer
214
     *
215
     * @return $this
216
     */
217
    public function setSerializer(SerializerInterface $serializer = null)
218
    {
219
        $this->serializer = $serializer;
220
221
        return $this;
222
    }
223
224
    /**
225
     * Get the current includes object
226
     *
227
     * @return Includes
228
     */
229
    public function getIncludes(): Includes
230
    {
231
        return $this->includes ?? new Includes();
232
    }
233
234
    /**
235
     * Set the Includes object used for determining included resources
236
     *
237
     * @param Includes $includes
238
     *
239
     * @return $this
240
     */
241
    public function setIncludes(Includes $includes)
242
    {
243
        $this->includes = $includes;
244
245
        return $this;
246
    }
247
248
    /**
249
     * Parse the given string to generate a new Includes object
250
     *
251
     * @param string $str
252
     *
253
     * @return $this
254
     */
255
    public function parseIncludes($str)
256
    {
257
        $this->includes = $this->getIncludeParser()->parse(!empty($str) ? $str : '');
258
259
        return $this;
260
    }
261
262
    /**
263
     * Return the include parser object.
264
     * If not set explicitly via setIncludeParser(), it will return the default IncludeParser object
265
     *
266
     * @return IncludeParserInterface
267
     * @see Smokescreen::setIncludeParser()
268
     */
269
    public function getIncludeParser(): IncludeParserInterface
270
    {
271
        return $this->includeParser ?? new IncludeParser();
272
    }
273
274
    /**
275
     * Set the include parser to handle converting a string to an Includes object
276
     *
277
     * @param IncludeParserInterface $includeParser
278
     *
279
     * @return $this
280
     */
281
    public function setIncludeParser(IncludeParserInterface $includeParser)
282
    {
283
        $this->includeParser = $includeParser;
284
285
        return $this;
286
    }
287
288
    /**
289
     * Get the current relation loader
290
     *
291
     * @return RelationLoaderInterface|null
292
     */
293
    public function getRelationLoader()
294
    {
295
        return $this->relationLoader;
296
    }
297
298
    /**
299
     * Set the relationship loader
300
     *
301
     * @param RelationLoaderInterface $relationLoader
302
     *
303
     * @return $this
304
     */
305
    public function setRelationLoader(RelationLoaderInterface $relationLoader)
306
    {
307
        $this->relationLoader = $relationLoader;
308
309
        return $this;
310
    }
311
312
    /**
313
     * Returns true if a RelationLoaderInterface object has been defined
314
     *
315
     * @return bool
316
     */
317
    public function hasRelationLoader(): bool
318
    {
319
        return $this->relationLoader !== null;
320
    }
321
322
    /**
323
     * @param ResourceInterface|mixed $resource
324
     * @param Includes                $includes
325
     *
326
     * @return array|mixed
327
     * @throws \Rexlabs\Smokescreen\Exception\InvalidSerializerException
328
     * @throws \Rexlabs\Smokescreen\Exception\UnhandledResourceType
329
     * @throws \Rexlabs\Smokescreen\Exception\InvalidTransformerException
330
     */
331
    protected function serializeResource($resource, Includes $includes): array
332
    {
333
        // Relationship loading
334
        if ($resource instanceof ResourceInterface) {
335
            $this->loadRelations($resource);
336
        }
337
338
        // Build the output by recursively transforming each resource
339
        $output = null;
340
        if ($resource instanceof Collection) {
341
            $output = $this->serializeCollection($resource, $includes);
342
        } elseif ($resource instanceof Item) {
343
            $output = $this->serializeItem($resource, $includes);
344
        } elseif (\is_array($resource)) {
345
            $output = $resource;
346
        } elseif (\is_object($resource) && method_exists($resource, 'toArray')) {
347
            $output = $resource->toArray();
348
        } else {
349
            throw new UnhandledResourceType('Unable to serialize resource of type ' . \gettype($resource));
350
        }
351
352
        return $output;
353
    }
354
355
    /**
356
     * Fire the relation loader (if defined) for this resource
357
     *
358
     * @param ResourceInterface $resource
359
     */
360
    protected function loadRelations(ResourceInterface $resource)
361
    {
362
        if ($this->relationLoader !== null) {
363
            $this->relationLoader->load($resource);
364
        }
365
    }
366
367
    /**
368
     * @param Collection $collection
369
     * @param Includes   $includes
370
     *
371
     * @return array
372
     * @throws \Rexlabs\Smokescreen\Exception\InvalidSerializerException
373
     * @throws \Rexlabs\Smokescreen\Exception\UnhandledResourceType
374
     * @throws InvalidTransformerException
375
     */
376
    protected function serializeCollection(Collection $collection, Includes $includes): array
377
    {
378
        // Get the globally set serializer (resource may override)
379
        $defaultSerializer = $this->getSerializer();
380
381
        // Collection resources implement IteratorAggregate ... so that's nice
382
        $items = [];
383
        foreach ($collection as $item) {
384
            // $item might be a Model or an array etc.
385
            $items[] = $this->transformItem($item, $collection->getTransformer(), $includes);
386
        }
387
388
        // The collection can have a custom serializer defined
389
        $serializer = $collection->getSerializer() ?? $defaultSerializer;
390
391
        if ($serializer instanceof SerializerInterface) {
392
            // Serialize via object implementing SerializerInterface
393
            $output = $serializer->collection($collection->getResourceKey(), $items);
394
            if ($collection->hasPaginator()) {
395
                $output = array_merge($output, $serializer->paginator($collection->getPaginator()));
396
            }
397
        } elseif (\is_callable($serializer)) {
398
            // Serialize via a callable/closure
399
            $output = $serializer($collection->getResourceKey(), $items);
400
        } else {
401
            // Serialization disabled for this resource
402
            $output = $items;
403
        }
404
405
        return $output;
406
    }
407
408
    /**
409
     * Apply transformation to the item.
410
     *
411
     * @param mixed                                    $item
412
     * @param mixed|TransformerInterface|callable|null $transformer
413
     * @param Includes                                 $includes
414
     *
415
     * @return array
416
     * @throws \Rexlabs\Smokescreen\Exception\UnhandledResourceType
417
     * @throws \Rexlabs\Smokescreen\Exception\InvalidTransformerException
418
     */
419
    protected function transformItem($item, $transformer, Includes $includes): array
420
    {
421
        if ($transformer === null) {
422
            // No transformation can be applied
423
            return (array)$item;
424
        }
425
        if (\is_callable($transformer)) {
426
            // Callable should simply return an array
427
            return (array)$transformer($item);
428
        }
429
430
        // Only these keys may be mapped
431
        $availableIncludeKeys = $transformer->getAvailableIncludes();
432
433
        // Wanted includes is a either the explicit includes requested, or the defaults for the transformer.
434
        $wantIncludeKeys = $includes->baseKeys() ?: $transformer->getDefaultIncludes();
435
436
        // Find the keys that are declared in the $includes of the transformer
437
        $mappedIncludeKeys = array_filter($wantIncludeKeys, function ($includeKey) use ($availableIncludeKeys) {
438
            return \in_array($includeKey, $availableIncludeKeys, true);
439
        });
440
441
        // We can consider our props anything that has not been mapped
442
        $filterProps = array_filter($wantIncludeKeys, function ($includeKey) use ($mappedIncludeKeys) {
443
            return !\in_array($includeKey, $mappedIncludeKeys, true);
444
        });
445
446
        // Were any filter props explicitly provided?
447
        // If not, see if defaults were provided from the transformer.
448
        if (empty($filterProps)) {
449
            // No explicit props provided
450
            $defaultProps = $transformer->getDefaultProps();
451
            if (!empty($defaultProps)) {
452
                $filterProps = $defaultProps;
453
            }
454
        }
455
456
        // Get the base data from the transformation
457
        $data = (array)$transformer->transform($item);
0 ignored issues
show
Bug introduced by
The method transform() does not exist on Rexlabs\Smokescreen\Tran...er\TransformerInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Rexlabs\Smokescreen\Tran...mer\AbstractTransformer or anonymous//tests/Unit/Tr...ctTransformerTest.php$2 or anonymous//tests/Unit/Tr...ctTransformerTest.php$1 or anonymous//tests/Unit/Tr...ctTransformerTest.php$0 or anonymous//tests/Unit/Tr...ctTransformerTest.php$3 or anonymous//tests/Unit/Re...tractResourceTest.php$1. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

457
        $data = (array)$transformer->/** @scrutinizer ignore-call */ transform($item);
Loading history...
458
459
        // Filter the sparse field-set
460
        if (!empty($filterProps)) {
461
            $filteredData = array_filter($data, function ($key) use ($filterProps) {
462
                return \in_array($key, $filterProps, true);
463
            }, ARRAY_FILTER_USE_KEY);
464
465
            // We must always have some data after filtering
466
            // If our filtered data is empty, we should just ignore it
467
            if (!empty($filteredData)) {
468
                $data = $filteredData;
469
            }
470
        }
471
472
        // Add includes to the payload
473
        $includeMap = $transformer->getIncludeMap();
474
        foreach ($mappedIncludeKeys as $includeKey) {
475
            $resource = $this->executeTransformerInclude($transformer, $includeMap[$includeKey], $item);
476
477
            if ($resource instanceof ResourceInterface) {
478
                // Resource object
479
                $data[$resource->getResourceKey() ?: $includeKey] = !$resource->getData() ? null : $this->serializeResource($resource,
480
                    $includes->splice($includeKey));
481
            } else {
482
                // Plain old array
483
                $data[$includeKey] = $this->serializeResource($resource, $includes->splice($includeKey));
484
            }
485
        }
486
487
        return $data;
488
    }
489
490
    /**
491
     * Execute the transformer
492
     *
493
     * @param mixed $transformer
494
     * @param array $include
495
     * @param mixed $item
496
     *
497
     * @return mixed
498
     */
499
    protected function executeTransformerInclude($transformer, $include, $item)
500
    {
501
        return \call_user_func([$transformer, $include['method']], $item);
502
    }
503
504
    /**
505
     * Applies the serializer to the Item resource.
506
     *
507
     * @param Item     $item
508
     * @param Includes $includes
509
     *
510
     * @return array
511
     * @throws \Rexlabs\Smokescreen\Exception\UnhandledResourceType
512
     * @throws InvalidTransformerException
513
     */
514
    protected function serializeItem(Item $item, Includes $includes): array
515
    {
516
        // Get the globally set serializer (resource may override)
517
        $defaultSerializer = $this->getSerializer();
518
519
        // The collection can have a custom serializer defined
520
        $serializer = $item->getSerializer() ?? $defaultSerializer;
521
        $isSerializerInterface = $serializer instanceof SerializerInterface;
522
523
        // Transform the item data
524
        $itemData = $this->transformItem($item->getData(), $item->getTransformer(), $includes);
525
526
        // Serialize the item data
527
        if ($isSerializerInterface) {
528
            // Serialize via object implementing SerializerInterface
529
            $output = $serializer->item($item->getResourceKey(), $itemData);
530
        } elseif (\is_callable($serializer)) {
531
            // Serialize via a callable/closure
532
            $output = $serializer($item->getResourceKey(), $itemData);
533
        } else {
534
            // No serialization
535
            $output = $itemData;
536
        }
537
538
        return $output;
539
    }
540
}
541
542