Completed
Pull Request — master (#11)
by Jodie
02:41
created

Smokescreen::serializeCollection()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 30
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 5

Importance

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

483
        $data = (array) $transformer->/** @scrutinizer ignore-call */ transform($item);
Loading history...
484
485
        // Filter the sparse field-set
486 5
        if (!empty($filterProps)) {
487 1
            $filteredData = array_filter($data, function ($key) use ($filterProps) {
488 1
                return \in_array($key, $filterProps, true);
489 1
            }, ARRAY_FILTER_USE_KEY);
490
491
            // We must always have some data after filtering
492
            // If our filtered data is empty, we should just ignore it
493 1
            if (!empty($filteredData)) {
494 1
                $data = $filteredData;
495
            }
496
        }
497
498
        // Add includes to the payload
499 5
        $includeMap = $transformer->getIncludeMap();
500 5
        foreach ($mappedIncludeKeys as $includeKey) {
501 3
            $resource = $this->executeTransformerInclude($transformer, $includeKey, $includeMap[$includeKey], $item);
502
503 3
            if ($resource instanceof ResourceInterface) {
504
                // Resource object
505 2
                $data[$resource->getResourceKey() ?: $includeKey] = !$resource->getData() ? null : $this->serializeResource($resource,
506 2
                    $includes->splice($includeKey));
507
            } else {
508
                // Plain old array
509 3
                $data[$includeKey] = $this->serializeResource($resource, $includes->splice($includeKey));
510
            }
511
        }
512
513 5
        return $data;
514
    }
515
516
    /**
517
     * Execute the transformer.
518
     *
519
     * @param mixed  $transformer
520
     * @param string $includeKey
521
     * @param array  $includeDefinition
522
     * @param mixed  $item
523
     *
524
     * @return ResourceInterface
525
     * @throws \Rexlabs\Smokescreen\Exception\UnhandledResourceType
526
     * @throws \Rexlabs\Smokescreen\Exception\InvalidTransformerException
527
     * @throws IncludeException
528
     */
529 3
    protected function executeTransformerInclude($transformer, $includeKey, $includeDefinition, $item)
530
    {
531
        // Transformer explicitly provided an include method
532 3
        $method = $includeDefinition['method'];
533 3
        if (method_exists($transformer, $method)) {
534 3
            return $transformer->$method($item);
535
        }
536
537
        // Otherwise try handle the include automatically
538
        return $this->autoWireInclude($includeKey, $includeDefinition, $item);
539
    }
540
541
    /**
542
     * @param string $includeKey
543
     * @param array  $includeDefinition
544
     * @param        $item
545
     *
546
     * @return Collection|Item
547
     * @throws \Rexlabs\Smokescreen\Exception\InvalidTransformerException
548
     * @throws IncludeException
549
     */
550
    protected function autoWireInclude($includeKey, $includeDefinition, $item)
551
    {
552
        // Get the included data
553
        $data = null;
554
        if (\is_array($item) || $item instanceof \ArrayAccess) {
555
            $data = $item[$includeKey];
556
        } elseif (\is_object($item) && property_exists($item, $includeKey)) {
557
            $data = $item->$includeKey;
558
        } else {
559
            throw new IncludeException("Cannot auto-wire include for $includeKey: Cannot get include data");
560
        }
561
562
        // Wrap the included data in a resource
563
        $resourceType = $includeDefinition['resource_type'] ?? 'item';
564
        switch ($resourceType) {
565
            case 'collection':
566
                return new Collection($data);
567
            case 'item':
568
                return new Item($data);
569
            default:
570
                throw new IncludeException("Cannot auto-wire include for $includeKey: Invalid resource type $resourceType");
571
        }
572
    }
573
574
    /**
575
     * Applies the serializer to the Item resource.
576
     *
577
     * @param Item     $item
578
     * @param Includes $includes
579
     *
580
     * @throws \Rexlabs\Smokescreen\Exception\UnhandledResourceType
581
     * @throws InvalidTransformerException
582
     *
583
     * @return array
584
     * @throws IncludeException
585
     */
586 11
    protected function serializeItem(Item $item, Includes $includes): array
587
    {
588
        // Get the globally set serializer (resource may override)
589 11
        $defaultSerializer = $this->getSerializer();
590
591
        // The collection can have a custom serializer defined
592 11
        $serializer = $item->getSerializer() ?? $defaultSerializer;
593 11
        $isSerializerInterface = $serializer instanceof SerializerInterface;
594
595
        // Transform the item data
596 11
        $itemData = $this->transformItem($item->getData(), $item->getTransformer(), $includes);
597
598
        // Serialize the item data
599 11
        if ($isSerializerInterface) {
600
            // Serialize via object implementing SerializerInterface
601 9
            $output = $serializer->item($item->getResourceKey(), $itemData);
602 2
        } elseif (\is_callable($serializer)) {
603
            // Serialize via a callable/closure
604 1
            $output = $serializer($item->getResourceKey(), $itemData);
605
        } else {
606
            // No serialization
607 1
            $output = $itemData;
608
        }
609
610 11
        return $output;
611
    }
612
613
    /**
614
     * Resolve the transformer to be used for a resource.
615
     * Returns an interface, callable or null when a transformer cannot be resolved.
616
     *
617
     * @param $resource
618
     *
619
     * @return TransformerInterface|mixed|null
620
     */
621 7
    protected function resolveTransformerForResource($resource)
622
    {
623 7
        $transformer = null;
624
625 7
        if ($this->transformerResolver !== null) {
626
            $transformer = $this->transformerResolver->resolve($resource);
627
        }
628
629 7
        return $transformer;
630
    }
631
632
    /**
633
     * @return TransformerResolverInterface|null
634
     */
635
    public function getTransformerResolver()
636
    {
637
        return $this->transformerResolver;
638
    }
639
640
    /**
641
     * Set the transformer resolve to user
642
     *
643
     * @param TransformerResolverInterface|null $transformerResolver
644
     *
645
     * @return $this
646
     */
647
    public function setTransformerResolver(TransformerResolverInterface $transformerResolver = null)
648
    {
649
        $this->transformerResolver = $transformerResolver;
650
651
        return $this;
652
    }
653
}
654