Completed
Push — master ( f1a3d7...431755 )
by Matt
07:09
created

Scope::serializeResource()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 7
cts 7
cp 1
rs 9.7998
c 0
b 0
f 0
cc 3
nc 3
nop 2
crap 3
1
<?php
2
3
/*
4
 * This file is part of the League\Fractal package.
5
 *
6
 * (c) Phil Sturgeon <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 **/
11
12
namespace League\Fractal;
13
14
use InvalidArgumentException;
15
use League\Fractal\Resource\Collection;
16
use League\Fractal\Resource\Item;
17
use League\Fractal\Resource\Primitive;
18
use League\Fractal\Resource\NullResource;
19
use League\Fractal\Resource\ResourceInterface;
20
use League\Fractal\Serializer\SerializerAbstract;
21
22
/**
23
 * Scope
24
 *
25
 * The scope class acts as a tracker, relating a specific resource in a specific
26
 * context. For example, the same resource could be attached to multiple scopes.
27
 * There are root scopes, parent scopes and child scopes.
28
 */
29
class Scope
30
{
31
    /**
32
     * @var array
33
     */
34
    protected $availableIncludes = [];
35
36
    /**
37
     * @var string
38
     */
39
    protected $scopeIdentifier;
40
41
    /**
42
     * @var \League\Fractal\Manager
43
     */
44
    protected $manager;
45
46
    /**
47
     * @var ResourceInterface
48
     */
49
    protected $resource;
50
51
    /**
52
     * @var array
53
     */
54
    protected $parentScopes = [];
55
56
    /**
57
     * Create a new scope instance.
58
     *
59
     * @param Manager           $manager
60
     * @param ResourceInterface $resource
61
     * @param string            $scopeIdentifier
62
     *
63
     * @return void
64
     */
65 76
    public function __construct(Manager $manager, ResourceInterface $resource, $scopeIdentifier = null)
66
    {
67 76
        $this->manager = $manager;
68 76
        $this->resource = $resource;
69 76
        $this->scopeIdentifier = $scopeIdentifier;
70 76
    }
71
72
    /**
73
     * Embed a scope as a child of the current scope.
74
     *
75
     * @internal
76
     *
77
     * @param string            $scopeIdentifier
78
     * @param ResourceInterface $resource
79
     *
80
     * @return \League\Fractal\Scope
81
     */
82 36
    public function embedChildScope($scopeIdentifier, $resource)
83
    {
84 36
        return $this->manager->createData($resource, $scopeIdentifier, $this);
85
    }
86
87
    /**
88
     * Get the current identifier.
89
     *
90
     * @return string
91
     */
92 38
    public function getScopeIdentifier()
93
    {
94 38
        return $this->scopeIdentifier;
95
    }
96
97
    /**
98
     * Get the unique identifier for this scope.
99
     *
100
     * @param string $appendIdentifier
101
     *
102
     * @return string
103
     */
104 32
    public function getIdentifier($appendIdentifier = null)
105
    {
106 32
        $identifierParts = array_merge($this->parentScopes, [$this->scopeIdentifier, $appendIdentifier]);
107
108 32
        return implode('.', array_filter($identifierParts));
109
    }
110
111
    /**
112
     * Getter for parentScopes.
113
     *
114
     * @return mixed
115
     */
116 38
    public function getParentScopes()
117
    {
118 38
        return $this->parentScopes;
119
    }
120
121
    /**
122
     * Getter for resource.
123
     *
124
     * @return ResourceInterface
125
     */
126 33
    public function getResource()
127
    {
128 33
        return $this->resource;
129
    }
130
131
    /**
132
     * Getter for manager.
133
     *
134
     * @return \League\Fractal\Manager
135
     */
136 32
    public function getManager()
137
    {
138 32
        return $this->manager;
139
    }
140
141
    /**
142
     * Is Requested.
143
     *
144
     * Check if - in relation to the current scope - this specific segment is allowed.
145
     * That means, if a.b.c is requested and the current scope is a.b, then c is allowed. If the current
146
     * scope is a then c is not allowed, even if it is there and potentially transformable.
147
     *
148
     * @internal
149
     *
150
     * @param string $checkScopeSegment
151
     *
152
     * @return bool Returns the new number of elements in the array.
153
     */
154 44 View Code Duplication
    public function isRequested($checkScopeSegment)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
155
    {
156 44
        if ($this->parentScopes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->parentScopes of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
157 21
            $scopeArray = array_slice($this->parentScopes, 1);
158 21
            array_push($scopeArray, $this->scopeIdentifier, $checkScopeSegment);
159 21
        } else {
160 44
            $scopeArray = [$checkScopeSegment];
161
        }
162
163 44
        $scopeString = implode('.', (array) $scopeArray);
164
165 44
        return in_array($scopeString, $this->manager->getRequestedIncludes());
166
    }
167
168
    /**
169
     * Is Excluded.
170
     *
171
     * Check if - in relation to the current scope - this specific segment should
172
     * be excluded. That means, if a.b.c is excluded and the current scope is a.b,
173
     * then c will not be allowed in the transformation whether it appears in
174
     * the list of default or available, requested includes.
175
     *
176
     * @internal
177
     *
178
     * @param string $checkScopeSegment
179
     *
180
     * @return bool
181
     */
182 32 View Code Duplication
    public function isExcluded($checkScopeSegment)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
183
    {
184 32
        if ($this->parentScopes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->parentScopes of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
185 7
            $scopeArray = array_slice($this->parentScopes, 1);
186 7
            array_push($scopeArray, $this->scopeIdentifier, $checkScopeSegment);
187 7
        } else {
188 32
            $scopeArray = [$checkScopeSegment];
189
        }
190
191 32
        $scopeString = implode('.', (array) $scopeArray);
192
193 32
        return in_array($scopeString, $this->manager->getRequestedExcludes());
194
    }
195
196
    /**
197
     * Push Parent Scope.
198
     *
199
     * Push a scope identifier into parentScopes
200
     *
201
     * @internal
202
     *
203
     * @param string $identifierSegment
204
     *
205
     * @return int Returns the new number of elements in the array.
206
     */
207 1
    public function pushParentScope($identifierSegment)
208
    {
209 1
        return array_push($this->parentScopes, $identifierSegment);
210
    }
211
212
    /**
213
     * Set parent scopes.
214
     *
215
     * @internal
216
     *
217
     * @param string[] $parentScopes Value to set.
218
     *
219
     * @return $this
220
     */
221 37
    public function setParentScopes($parentScopes)
222
    {
223 37
        $this->parentScopes = $parentScopes;
224
225 37
        return $this;
226
    }
227
228
    /**
229
     * Convert the current data for this scope to an array.
230
     *
231
     * @return array
232
     */
233 70
    public function toArray()
234
    {
235 70
        list($rawData, $rawIncludedData) = $this->executeResourceTransformers();
236
237 69
        $serializer = $this->manager->getSerializer();
238
239 69
        $data = $this->serializeResource($serializer, $rawData);
240
241
        // If the serializer wants the includes to be side-loaded then we'll
242
        // serialize the included data and merge it with the data.
243 68
        if ($serializer->sideloadIncludes()) {
244
            //Filter out any relation that wasn't requested
245 40
            $rawIncludedData = array_map(array($this, 'filterFieldsets'), $rawIncludedData);
246
247 40
            $includedData = $serializer->includedData($this->resource, $rawIncludedData);
248
249
            // If the serializer wants to inject additional information
250
            // about the included resources, it can do so now.
251 40
            $data = $serializer->injectData($data, $rawIncludedData);
252
253 40
            if ($this->isRootScope()) {
254
                // If the serializer wants to have a final word about all
255
                // the objects that are sideloaded, it can do so now.
256 40
                $includedData = $serializer->filterIncludes(
257 40
                    $includedData,
258
                    $data
259 40
                );
260 40
            }
261
262 40
            $data = $data + $includedData;
263 40
        }
264
265 68
        if (!empty($this->availableIncludes)) {
266 47
            $data = $serializer->injectAvailableIncludeData($data, $this->availableIncludes);
267 47
        }
268
269 68
        if ($this->resource instanceof Collection) {
270 31
            if ($this->resource->hasCursor()) {
271 1
                $pagination = $serializer->cursor($this->resource->getCursor());
272 31
            } elseif ($this->resource->hasPaginator()) {
273 4
                $pagination = $serializer->paginator($this->resource->getPaginator());
274 4
            }
275
276 31
            if (! empty($pagination)) {
277 5
                $this->resource->setMetaValue(key($pagination), current($pagination));
278 5
            }
279 31
        }
280
281
        // Pull out all of OUR metadata and any custom meta data to merge with the main level data
282 68
        $meta = $serializer->meta($this->resource->getMeta());
283
284
        // in case of returning NullResource we should return null and not to go with array_merge
285 68
        if (is_null($data)) {
286 2
            if (!empty($meta)) {
287 1
                return $meta;
288
            }
289 1
            return null;
290
        }
291
292 67
        return $data + $meta;
293
    }
294
295
    /**
296
     * Convert the current data for this scope to JSON.
297
     *
298
     * @param int $options
299
     *
300
     * @return string
301
     */
302 40
    public function toJson($options = 0)
303
    {
304 40
        return json_encode($this->toArray(), $options);
305
    }
306
307
    /**
308
     * Transformer a primitive resource
309
     *
310
     * @return mixed
311
     */
312 2
    public function transformPrimitiveResource()
313
    {
314 2
        if (! ($this->resource instanceof Primitive)) {
315
            throw new InvalidArgumentException(
316
                'Argument $resource should be an instance of League\Fractal\Resource\Primitive'
317
            );
318
        }
319
320 2
        $transformer = $this->resource->getTransformer();
321 2
        $data = $this->resource->getData();
322
323 2
        if (null === $transformer) {
324
            $transformedData = $data;
325 2
        } elseif (is_callable($transformer)) {
326 2
            $transformedData = call_user_func($transformer, $data);
327 2
        } else {
328 1
            $transformer->setCurrentScope($this);
329 1
            $transformedData = $transformer->transform($data);
330
        }
331
332 2
        return $transformedData;
333
    }
334
335
    /**
336
     * Execute the resources transformer and return the data and included data.
337
     *
338
     * @internal
339
     *
340
     * @return array
341
     */
342 62
    protected function executeResourceTransformers()
343
    {
344 62
        $transformer = $this->resource->getTransformer();
345 62
        $data = $this->resource->getData();
346
347 62
        $transformedData = $includedData = [];
348
349 62
        if ($this->resource instanceof Item) {
350 45
            list($transformedData, $includedData[]) = $this->fireTransformer($transformer, $data);
351 62
        } elseif ($this->resource instanceof Collection) {
352 31
            foreach ($data as $value) {
353 30
                list($transformedData[], $includedData[]) = $this->fireTransformer($transformer, $value);
354 31
            }
355 37
        } elseif ($this->resource instanceof NullResource) {
356 5
            $transformedData = null;
357 5
            $includedData = [];
358 5
        } else {
359 2
            throw new InvalidArgumentException(
360
                'Argument $resource should be an instance of League\Fractal\Resource\Item'
361
                .' or League\Fractal\Resource\Collection'
362 2
            );
363
        }
364
365 60
        return [$transformedData, $includedData];
366
    }
367
368
    /**
369
     * Serialize a resource
370
     *
371
     * @internal
372
     *
373
     * @param SerializerAbstract $serializer
374
     * @param mixed              $data
375
     *
376
     * @return array
377
     */
378 60
    protected function serializeResource(SerializerAbstract $serializer, $data)
379
    {
380 60
        $resourceKey = $this->resource->getResourceKey();
381
382 60
        if ($this->resource instanceof Collection) {
383 31
            return $serializer->collection($resourceKey, $data);
384
        }
385
386 47
        if ($this->resource instanceof Item) {
387 45
            return $serializer->item($resourceKey, $data);
388
        }
389
390 5
        return $serializer->null();
391
    }
392
393
    /**
394
     * Fire the main transformer.
395
     *
396
     * @internal
397
     *
398
     * @param TransformerAbstract|callable $transformer
399
     * @param mixed                        $data
400
     *
401
     * @return array
402
     */
403 58
    protected function fireTransformer($transformer, $data)
404
    {
405 58
        $includedData = [];
406
407 58
        if (is_callable($transformer)) {
408 6
            $transformedData = call_user_func($transformer, $data);
409 6
        } else {
410 52
            $transformer->setCurrentScope($this);
411 52
            $transformedData = $transformer->transform($data);
412
        }
413
414 58
        if ($this->transformerHasIncludes($transformer)) {
415 49
            $includedData = $this->fireIncludedTransformers($transformer, $data);
416 49
            $transformedData = $this->manager->getSerializer()->mergeIncludes($transformedData, $includedData);
417 49
        }
418
419
        //Stick only with requested fields
420 58
        $transformedData = $this->filterFieldsets($transformedData);
421
422 58
        return [$transformedData, $includedData];
423
    }
424
425
    /**
426
     * Fire the included transformers.
427
     *
428
     * @internal
429
     *
430
     * @param \League\Fractal\TransformerAbstract $transformer
431
     * @param mixed                               $data
432
     *
433
     * @return array
434
     */
435 49
    protected function fireIncludedTransformers($transformer, $data)
436
    {
437 49
        $this->availableIncludes = $transformer->getAvailableIncludes();
438
439 49
        return $transformer->processIncludedResources($this, $data) ?: [];
440
    }
441
442
    /**
443
     * Determine if a transformer has any available includes.
444
     *
445
     * @internal
446
     *
447
     * @param TransformerAbstract|callable $transformer
448
     *
449
     * @return bool
450
     */
451 58
    protected function transformerHasIncludes($transformer)
452
    {
453 58
        if (! $transformer instanceof TransformerAbstract) {
454 6
            return false;
455
        }
456
457 52
        $defaultIncludes = $transformer->getDefaultIncludes();
458 52
        $availableIncludes = $transformer->getAvailableIncludes();
459
460 52
        return ! empty($defaultIncludes) || ! empty($availableIncludes);
461
    }
462
463
    /**
464
     * Check, if this is the root scope.
465
     *
466
     * @return bool
467
     */
468 38
    protected function isRootScope()
469
    {
470 38
        return empty($this->parentScopes);
471
    }
472
473
    /**
474
     * Filter the provided data with the requested filter fieldset for
475
     * the scope resource
476
     *
477
     * @internal
478
     *
479
     * @param array  $data
480
     *
481
     * @return array
482
     */
483 58
    protected function filterFieldsets(array $data)
484
    {
485 58
        if (!$this->hasFilterFieldset()) {
486 54
            return $data;
487
        }
488 10
        $serializer = $this->manager->getSerializer();
489 10
        $requestedFieldset = iterator_to_array($this->getFilterFieldset());
490
        //Build the array of requested fieldsets with the mandatory serializer fields
491 10
        $filterFieldset = array_flip(
492 10
            array_merge(
493 10
                $serializer->getMandatoryFields(),
494
                $requestedFieldset
495 10
            )
496 10
        );
497 10
        return array_intersect_key($data, $filterFieldset);
498
    }
499
500
    /**
501
     * Return the requested filter fieldset for the scope resource
502
     *
503
     * @internal
504
     *
505
     * @return \League\Fractal\ParamBag|null
506
     */
507 58
    protected function getFilterFieldset()
508
    {
509 58
        return $this->manager->getFieldset($this->getResourceType());
510
    }
511
512
    /**
513
     * Check if there are requested filter fieldsets for the scope resource.
514
     *
515
     * @internal
516
     *
517
     * @return bool
518
     */
519 58
    protected function hasFilterFieldset()
520
    {
521 58
        return $this->getFilterFieldset() !== null;
522
    }
523
524
    /**
525
     * Return the scope resource type.
526
     *
527
     * @internal
528
     *
529
     * @return string
530
     */
531 58
    protected function getResourceType()
532
    {
533 58
        return $this->resource->getResourceKey();
534
    }
535
}
536