Completed
Pull Request — master (#9)
by Jodie
03:44
created

Smokescreen::getIncludeParser()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 0
crap 1
1
<?php
2
3
namespace Rexlabs\Smokescreen;
4
5
use Rexlabs\Smokescreen\Exception\IncludeException;
6
use Rexlabs\Smokescreen\Exception\MissingResourceException;
7
use Rexlabs\Smokescreen\Exception\UnhandledResourceType;
8
use Rexlabs\Smokescreen\Helpers\ArrayHelper;
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 21
    public function setResource($resource)
63
    {
64 21
        $this->resource = $resource;
65
66 21
        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
     * @throws \Rexlabs\Smokescreen\Exception\InvalidTransformerException
77
     *
78
     * @return $this
79
     */
80 15
    public function item($data, $transformer = null, $key = null)
81
    {
82 15
        $this->setResource(new Item($data, $transformer, $key));
83
84 14
        return $this;
85
    }
86
87
    /**
88
     * Set the resource collection to be transformed.
89
     *
90
     * @param mixed                           $data
91
     * @param TransformerInterface|mixed|null $transformer
92
     * @param string|null                     $key
93
     * @param callable|null                   $callback
94
     *
95
     * @return $this
96
     * @throws \Rexlabs\Smokescreen\Exception\InvalidTransformerException
97
     */
98 7
    public function collection($data, TransformerInterface $transformer = null, $key = null, callable $callback = null)
99
    {
100 7
        $this->setResource(new Collection($data, $transformer, $key));
101 7
        if ($callback !== null) {
102 1
            $callback($this->resource);
103
        }
104
105 7
        return $this;
106
    }
107
108
    /**
109
     * Sets the transformer to be used to transform the resource ... later.
110
     *
111
     * @throws MissingResourceException
112
     *
113
     * @return TransformerInterface|mixed|null
114
     */
115 5
    public function getTransformer()
116
    {
117 5
        if (!$this->resource) {
118 1
            throw new MissingResourceException('Resource must be specified before setting a transformer');
119
        }
120
121 4
        return $this->resource->getTransformer();
122
    }
123
124
    /**
125
     * Sets the transformer to be used to transform the resource ... later.
126
     *
127
     * @param TransformerInterface|mixed|null $transformer
128
     *
129
     * @throws MissingResourceException
130
     *
131
     * @return $this
132
     */
133 2
    public function setTransformer($transformer = null)
134
    {
135 2
        if (!$this->resource) {
136 1
            throw new MissingResourceException('Resource must be specified before setting a transformer');
137
        }
138 1
        $this->resource->setTransformer($transformer);
139
140 1
        return $this;
141
    }
142
143
    /**
144
     * Returns an object (stdClass) representation of the transformed/serialized data.
145
     *
146
     * @throws \Rexlabs\Smokescreen\Exception\InvalidSerializerException
147
     * @throws \Rexlabs\Smokescreen\Exception\UnhandledResourceType
148
     * @throws \Rexlabs\Smokescreen\Exception\MissingResourceException
149
     * @throws \Rexlabs\Smokescreen\Exception\InvalidTransformerException
150
     * @throws \Rexlabs\Smokescreen\Exception\JsonEncodeException
151
     * @throws \Rexlabs\Smokescreen\Exception\IncludeException
152
     *
153
     * @return \stdClass
154
     */
155 1
    public function toObject(): \stdClass
156
    {
157 1
        return (object) json_decode($this->toJson());
158
    }
159
160
    /**
161
     * Outputs a JSON string of the resulting transformed and serialized data.
162
     *
163
     * @param int $options
164
     *
165
     * @throws \Rexlabs\Smokescreen\Exception\InvalidSerializerException
166
     * @throws \Rexlabs\Smokescreen\Exception\UnhandledResourceType
167
     * @throws \Rexlabs\Smokescreen\Exception\MissingResourceException
168
     * @throws \Rexlabs\Smokescreen\Exception\InvalidTransformerException
169
     * @throws \Rexlabs\Smokescreen\Exception\JsonEncodeException
170
     * @throws \Rexlabs\Smokescreen\Exception\IncludeException
171
     *
172
     * @return string
173
     */
174 2
    public function toJson($options = 0): string
175
    {
176 2
        return JsonHelper::encode($this->jsonSerialize(), $options);
177
    }
178
179
    /**
180
     * Output the transformed and serialized data as an array.
181
     * Implements PHP's JsonSerializable interface.
182
     *
183
     * @throws \Rexlabs\Smokescreen\Exception\InvalidSerializerException
184
     * @throws \Rexlabs\Smokescreen\Exception\UnhandledResourceType
185
     * @throws \Rexlabs\Smokescreen\Exception\InvalidTransformerException
186
     * @throws \Rexlabs\Smokescreen\Exception\MissingResourceException
187
     * @throws \Rexlabs\Smokescreen\Exception\IncludeException
188
     *
189
     * @return array
190
     *
191
     * @see Smokescreen::toArray()
192
     */
193 2
    public function jsonSerialize(): array
194
    {
195 2
        return $this->toArray();
196
    }
197
198
    /**
199
     * Return the transformed data as an array.
200
     *
201
     * @throws \Rexlabs\Smokescreen\Exception\InvalidSerializerException
202
     * @throws \Rexlabs\Smokescreen\Exception\UnhandledResourceType
203
     * @throws \Rexlabs\Smokescreen\Exception\InvalidTransformerException
204
     * @throws \Rexlabs\Smokescreen\Exception\MissingResourceException
205
     * @throws \Rexlabs\Smokescreen\Exception\IncludeException
206
     *
207
     * @return array
208
     */
209 17
    public function toArray(): array
210
    {
211 17
        if (!$this->resource) {
212 1
            throw new MissingResourceException('No resource has been defined to transform');
213
        }
214
215
        // Kick of serialization of the resource
216 16
        return $this->serializeResource($this->resource, $this->getIncludes());
217
    }
218
219
    /**
220
     * @return SerializerInterface
221
     */
222 15
    public function getSerializer(): SerializerInterface
223
    {
224 15
        return $this->serializer ?? new DefaultSerializer();
225
    }
226
227
    /**
228
     * Set the serializer which will be used to output the transformed resource.
229
     *
230
     * @param SerializerInterface|null $serializer
231
     *
232
     * @return $this
233
     */
234 1
    public function setSerializer(SerializerInterface $serializer = null)
235
    {
236 1
        $this->serializer = $serializer;
237
238 1
        return $this;
239
    }
240
241
    /**
242
     * Get the current includes object.
243
     *
244
     * @return Includes
245
     */
246 18
    public function getIncludes(): Includes
247
    {
248 18
        return $this->includes ?? new Includes();
249
    }
250
251
    /**
252
     * Set the Includes object used for determining included resources.
253
     *
254
     * @param Includes $includes
255
     *
256
     * @return $this
257
     */
258 1
    public function setIncludes(Includes $includes)
259
    {
260 1
        $this->includes = $includes;
261
262 1
        return $this;
263
    }
264
265
    /**
266
     * Parse the given string to generate a new Includes object.
267
     *
268
     * @param string $str
269
     *
270
     * @return $this
271
     */
272 7
    public function parseIncludes($str)
273
    {
274 7
        $this->includes = $this->getIncludeParser()->parse(!empty($str) ? $str : '');
275
276 7
        return $this;
277
    }
278
279
    /**
280
     * Return the include parser object.
281
     * If not set explicitly via setIncludeParser(), it will return the default IncludeParser object.
282
     *
283
     * @return IncludeParserInterface
284
     *
285
     * @see Smokescreen::setIncludeParser()
286
     */
287 7
    public function getIncludeParser(): IncludeParserInterface
288
    {
289 7
        return $this->includeParser ?? new IncludeParser();
290
    }
291
292
    /**
293
     * Set the include parser to handle converting a string to an Includes object.
294
     *
295
     * @param IncludeParserInterface $includeParser
296
     *
297
     * @return $this
298
     */
299 1
    public function setIncludeParser(IncludeParserInterface $includeParser)
300
    {
301 1
        $this->includeParser = $includeParser;
302
303 1
        return $this;
304
    }
305
306
    /**
307
     * Get the current relation loader.
308
     *
309
     * @return RelationLoaderInterface|null
310
     */
311 1
    public function getRelationLoader()
312
    {
313 1
        return $this->relationLoader;
314
    }
315
316
    /**
317
     * Set the relationship loader.
318
     *
319
     * @param RelationLoaderInterface $relationLoader
320
     *
321
     * @return $this
322
     */
323 1
    public function setRelationLoader(RelationLoaderInterface $relationLoader)
324
    {
325 1
        $this->relationLoader = $relationLoader;
326
327 1
        return $this;
328
    }
329
330
    /**
331
     * Returns true if a RelationLoaderInterface object has been defined.
332
     *
333
     * @return bool
334
     */
335 1
    public function hasRelationLoader(): bool
336
    {
337 1
        return $this->relationLoader !== null;
338
    }
339
340
    /**
341
     * @param ResourceInterface|array|mixed $resource
342
     * @param Includes                      $includes
343
     *
344
     * @throws \Rexlabs\Smokescreen\Exception\InvalidSerializerException
345
     * @throws \Rexlabs\Smokescreen\Exception\UnhandledResourceType
346
     * @throws \Rexlabs\Smokescreen\Exception\InvalidTransformerException
347
     * @throws \Rexlabs\Smokescreen\Exception\IncludeException
348
     *
349
     * @return array|mixed
350
     */
351 16
    protected function serializeResource($resource, Includes $includes): array
352
    {
353 16
        if ($resource instanceof ResourceInterface) {
354 14
            if (!$resource->hasTransformer()) {
355
                // Try to resolve a transformer for a resource that does not have one assigned.
356 9
                $transformer = $this->resolveTransformerForResource($resource);
357 9
                $resource->setTransformer($transformer);
358
            }
359
360
            // Load relations for any resource which implements the interface.
361 14
            $this->loadRelations($resource);
362
        }
363
364
        // Build the output by recursively transforming each resource.
365 16
        $output = null;
366 16
        if ($resource instanceof Collection) {
367 4
            $output = $this->serializeCollection($resource, $includes);
368 15
        } elseif ($resource instanceof Item) {
369 13
            $output = $this->serializeItem($resource, $includes);
370 3
        } elseif (\is_array($resource)) {
371 1
            $output = $resource;
372 2
        } elseif (\is_object($resource) && method_exists($resource, 'toArray')) {
373 1
            $output = $resource->toArray();
374
        } else {
375 1
            throw new UnhandledResourceType('Unable to serialize resource of type '.\gettype($resource));
376
        }
377
378 15
        return $output;
379
    }
380
381
    /**
382
     * Fire the relation loader (if defined) for this resource.
383
     *
384
     * @param ResourceInterface $resource
385
     */
386 14
    protected function loadRelations(ResourceInterface $resource)
387
    {
388 14
        if ($this->relationLoader !== null) {
389 1
            $this->relationLoader->load($resource);
390
        }
391 14
    }
392
393
    /**
394
     * @param Collection $collection
395
     * @param Includes   $includes
396
     *
397
     * @throws \Rexlabs\Smokescreen\Exception\InvalidSerializerException
398
     * @throws \Rexlabs\Smokescreen\Exception\UnhandledResourceType
399
     * @throws \Rexlabs\Smokescreen\Exception\InvalidTransformerException
400
     * @throws \Rexlabs\Smokescreen\Exception\IncludeException
401
     *
402
     * @return array
403
     */
404 4
    protected function serializeCollection(Collection $collection, Includes $includes): array
405
    {
406
        // Get the globally set serializer (resource may override).
407 4
        $defaultSerializer = $this->getSerializer();
408
409
        // Collection resources implement IteratorAggregate ... so that's nice.
410 4
        $items = [];
411 4
        foreach ($collection as $item) {
412
            // $item might be a Model or an array etc.
413 4
            $items[] = $this->transformItem($item, $collection->getTransformer(), $includes);
414
        }
415
416
        // The collection can have a custom serializer defined.
417 4
        $serializer = $collection->getSerializer() ?? $defaultSerializer;
418
419 4
        if ($serializer instanceof SerializerInterface) {
420
            // Serialize via object implementing SerializerInterface
421 2
            $output = $serializer->collection($collection->getResourceKey(), $items);
422 2
            if ($collection->hasPaginator()) {
423 2
                $output = array_merge($output, $serializer->paginator($collection->getPaginator()));
424
            }
425 2
        } elseif (\is_callable($serializer)) {
426
            // Serialize via a callable/closure
427 1
            $output = $serializer($collection->getResourceKey(), $items);
428
        } else {
429
            // Serialization disabled for this resource
430 1
            $output = $items;
431
        }
432
433 4
        return $output;
434
    }
435
436
    /**
437
     * Apply transformation to the item.
438
     *
439
     * @param mixed                                    $item
440
     * @param mixed|TransformerInterface|callable|null $transformer
441
     * @param Includes                                 $includes
442
     *
443
     * @throws \Rexlabs\Smokescreen\Exception\InvalidSerializerException
444
     * @throws \Rexlabs\Smokescreen\Exception\UnhandledResourceType
445
     * @throws \Rexlabs\Smokescreen\Exception\InvalidTransformerException
446
     * @throws \Rexlabs\Smokescreen\Exception\IncludeException
447
     *
448
     * @return array
449
     */
450 14
    protected function transformItem($item, $transformer, Includes $includes): array
451
    {
452 14
        if ($transformer === null) {
453
            // No transformation can be applied.
454 7
            return (array) $item;
455
        }
456 8
        if (\is_callable($transformer)) {
457
            // Callable should simply return an array.
458 3
            return (array) $transformer($item);
459
        }
460
461
        // Only these keys may be mapped
462 7
        $availableIncludeKeys = $transformer->getAvailableIncludes();
463
464
        // Wanted includes is a either the explicit includes requested, or the defaults for the transformer.
465 7
        $wantIncludeKeys = $includes->baseKeys() ?: $transformer->getDefaultIncludes();
466
467
        // Find the keys that are declared in the $includes of the transformer
468 7
        $mappedIncludeKeys = array_filter($wantIncludeKeys, function ($includeKey) use ($availableIncludeKeys) {
469 6
            return \in_array($includeKey, $availableIncludeKeys, true);
470 7
        });
471
472
        // We can consider our props anything that has not been mapped.
473 7
        $filterProps = array_filter($wantIncludeKeys, function ($includeKey) use ($mappedIncludeKeys) {
474 6
            return !\in_array($includeKey, $mappedIncludeKeys, true);
475 7
        });
476
477
        // Were any filter props explicitly provided?
478
        // If not, see if defaults were provided from the transformer.
479 7
        if (empty($filterProps)) {
480
            // No explicit props provided
481 7
            $defaultProps = $transformer->getDefaultProps();
482 7
            if (!empty($defaultProps)) {
483 1
                $filterProps = $defaultProps;
484
            }
485
        }
486
487
        // Get the base data from the transformation
488 7
        $data = $transformer->getTransformedData($item);
489
490
        // Filter the sparse field-set
491 7
        if (!empty($filterProps)) {
492 1
            $filteredData = array_filter($data, function ($key) use ($filterProps) {
493 1
                return \in_array($key, $filterProps, true);
494 1
            }, ARRAY_FILTER_USE_KEY);
495
496
            // We must always have some data after filtering
497
            // If our filtered data is empty, we should just ignore it
498 1
            if (!empty($filteredData)) {
499 1
                $data = $filteredData;
500
            }
501
        }
502
503
        // Add includes to the payload
504 7
        $includeMap = $transformer->getIncludeMap();
505 7
        foreach ($mappedIncludeKeys as $includeKey) {
506 5
            $resource = $this->executeTransformerInclude($transformer, $includeKey, $includeMap[$includeKey], $item);
507
508 5
            if ($resource instanceof ResourceInterface) {
509
                // Resource object
510 4
                ArrayHelper::mutate(
511
                    $data,
512 4
                    $resource->getResourceKey() ?: $includeKey,
513 4
                    $resource->getData() ? $this->serializeResource($resource, $includes->splice($includeKey)) : null
514
                );
515
            } else {
516
                // Plain old array
517 1
                ArrayHelper::mutate(
518
                    $data,
519 1
                    $includeKey,
520 5
                    $this->serializeResource($resource, $includes->splice($includeKey))
521
                );
522
            }
523
        }
524
525 7
        return $data;
526
    }
527
528
    /**
529
     * Execute the transformer.
530
     *
531
     * @param mixed  $transformer
532
     * @param string $includeKey
533
     * @param array  $includeDefinition
534
     * @param mixed  $item
535
     *
536
     * @throws \Rexlabs\Smokescreen\Exception\UnhandledResourceType
537
     * @throws \Rexlabs\Smokescreen\Exception\InvalidTransformerException
538
     * @throws \Rexlabs\Smokescreen\Exception\IncludeException
539
     *
540
     * @return ResourceInterface
541
     */
542 5
    protected function executeTransformerInclude($transformer, $includeKey, $includeDefinition, $item)
543
    {
544
        // Transformer explicitly provided an include method
545 5
        $method = $includeDefinition['method'];
546 5
        if (method_exists($transformer, $method)) {
547 3
            return $transformer->$method($item);
548
        }
549
550
        // Otherwise try handle the include automatically
551 2
        return $this->autoWireInclude($includeKey, $includeDefinition, $item);
552
    }
553
554
    /**
555
     * @param string $includeKey
556
     * @param array  $includeDefinition
557
     * @param        $item
558
     *
559
     * @throws \Rexlabs\Smokescreen\Exception\InvalidTransformerException
560
     * @throws \Rexlabs\Smokescreen\Exception\IncludeException
561
     *
562
     * @return Collection|Item
563
     */
564 2
    protected function autoWireInclude($includeKey, $includeDefinition, $item)
565
    {
566
        // Get the included data
567 2
        $data = null;
568 2
        if (\is_array($item) || $item instanceof \ArrayAccess) {
569 1
            $data = $item[$includeKey] ?? null;
570 1
        } elseif (\is_object($item)) {
571 1
            $data = $item->$includeKey ?? null;
572
        } else {
573
            throw new IncludeException("Cannot auto-wire include for $includeKey: Cannot get include data");
574
        }
575
576 2
        if (!empty($includeDefinition['resource_type']) && $includeDefinition['resource_type'] === 'collection') {
577 1
            return new Collection($data);
578
        }
579
580
        // Assume unless declared, that the resource is an item.
581 2
        return new Item($data);
582
    }
583
584
    /**
585
     * Applies the serializer to the Item resource.
586
     *
587
     * @param Item     $item
588
     * @param Includes $includes
589
     *
590
     * @throws \Rexlabs\Smokescreen\Exception\InvalidSerializerException
591
     * @throws \Rexlabs\Smokescreen\Exception\UnhandledResourceType
592
     * @throws \Rexlabs\Smokescreen\Exception\InvalidTransformerException
593
     * @throws \Rexlabs\Smokescreen\Exception\IncludeException
594
     *
595
     * @return array
596
     */
597 13
    protected function serializeItem(Item $item, Includes $includes): array
598
    {
599
        // Get the globally set serializer (resource may override)
600 13
        $defaultSerializer = $this->getSerializer();
601
602
        // The collection can have a custom serializer defined
603 13
        $serializer = $item->getSerializer() ?? $defaultSerializer;
604 13
        $isSerializerInterface = $serializer instanceof SerializerInterface;
605
606
        // Transform the item data
607 13
        $itemData = $this->transformItem($item->getData(), $item->getTransformer(), $includes);
608
609
        // Serialize the item data
610 13
        if ($isSerializerInterface) {
611
            // Serialize via object implementing SerializerInterface
612 11
            $output = $serializer->item($item->getResourceKey(), $itemData);
613 2
        } elseif (\is_callable($serializer)) {
614
            // Serialize via a callable/closure
615 1
            $output = $serializer($item->getResourceKey(), $itemData);
616
        } else {
617
            // No serialization
618 1
            $output = $itemData;
619
        }
620
621 13
        return $output;
622
    }
623
624
    /**
625
     * Resolve the transformer to be used for a resource.
626
     * Returns an interface, callable or null when a transformer cannot be resolved.
627
     *
628
     * @param $resource
629
     *
630
     * @return TransformerInterface|mixed|null
631
     */
632 9
    protected function resolveTransformerForResource($resource)
633
    {
634 9
        $transformer = null;
635
636 9
        if ($this->transformerResolver !== null) {
637 2
            $transformer = $this->transformerResolver->resolve($resource);
638
        }
639
640 9
        return $transformer;
641
    }
642
643
    /**
644
     * @return TransformerResolverInterface|null
645
     */
646 1
    public function getTransformerResolver()
647
    {
648 1
        return $this->transformerResolver;
649
    }
650
651
    /**
652
     * Set the transformer resolve to user.
653
     *
654
     * @param TransformerResolverInterface|null $transformerResolver
655
     *
656
     * @return $this
657
     */
658 3
    public function setTransformerResolver(TransformerResolverInterface $transformerResolver = null)
659
    {
660 3
        $this->transformerResolver = $transformerResolver;
661
662 3
        return $this;
663
    }
664
}
665