Completed
Pull Request — master (#9)
by Jodie
02:26
created

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