Completed
Push — 1.0.x ( 209591...7a1bb8 )
by Korvin
04:54
created

Scope::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 5
cts 5
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 3
crap 1
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\MetaDataInterface;
18
use League\Fractal\Resource\Primitive;
19
use League\Fractal\Resource\NullResource;
20
use League\Fractal\Resource\ResourceInterface;
21
use League\Fractal\Serializer\SerializerAbstract;
22
23
/**
24
 * Scope
25
 *
26
 * The scope class acts as a tracker, relating a specific resource in a specific
27
 * context. For example, the same resource could be attached to multiple scopes.
28
 * There are root scopes, parent scopes and child scopes.
29
 */
30
class Scope
31
{
32
    /**
33
     * @var array
34
     */
35
    protected $availableIncludes = [];
36
37
    /**
38
     * @var string|null
39
     */
40
    protected $scopeIdentifier;
41
42
    /**
43
     * @var \League\Fractal\Manager
44
     */
45
    protected $manager;
46
47
    /**
48
     * @var ResourceInterface
49
     */
50
    protected $resource;
51
52
    /**
53
     * @var array
54
     */
55
    protected $parentScopes = [];
56
57
    /**
58
     * Create a new scope instance.
59
     *
60
     * @param Manager           $manager
61
     * @param ResourceInterface $resource
62
     * @param string            $scopeIdentifier
63
     *
64
     * @return void
65
     */
66 76
    public function __construct(Manager $manager, ResourceInterface $resource, $scopeIdentifier = null)
67
    {
68 76
        $this->manager = $manager;
69 76
        $this->resource = $resource;
70 76
        $this->scopeIdentifier = $scopeIdentifier;
71 76
    }
72
73
    /**
74
     * Embed a scope as a child of the current scope.
75
     *
76
     * @internal
77
     *
78
     * @param string            $scopeIdentifier
79
     * @param ResourceInterface $resource
80
     *
81
     * @return \League\Fractal\Scope
82
     */
83 36
    public function embedChildScope($scopeIdentifier, $resource)
84
    {
85 36
        return $this->manager->createData($resource, $scopeIdentifier, $this);
86
    }
87
88
    /**
89
     * Get the current identifier.
90
     *
91
     * @return string
92
     */
93 38
    public function getScopeIdentifier()
94
    {
95 38
        return (string) $this->scopeIdentifier;
96
    }
97
98
    /**
99
     * Get the unique identifier for this scope.
100
     *
101
     * @param string $appendIdentifier
102
     *
103
     * @return string
104
     */
105 32
    public function getIdentifier($appendIdentifier = null)
106
    {
107 32
        $identifierParts = array_merge($this->parentScopes, [$this->scopeIdentifier, $appendIdentifier]);
108
109 32
        return implode('.', array_filter($identifierParts));
110
    }
111
112
    /**
113
     * Getter for parentScopes.
114
     *
115
     * @return mixed
116
     */
117 38
    public function getParentScopes()
118
    {
119 38
        return $this->parentScopes;
120
    }
121
122
    /**
123
     * Getter for resource.
124
     *
125
     * @return ResourceInterface
126
     */
127 33
    public function getResource()
128
    {
129 33
        return $this->resource;
130
    }
131
132
    /**
133
     * Getter for manager.
134
     *
135
     * @return \League\Fractal\Manager
136
     */
137 32
    public function getManager()
138
    {
139 32
        return $this->manager;
140
    }
141
142
    /**
143
     * Is Requested.
144
     *
145
     * Check if - in relation to the current scope - this specific segment is allowed.
146
     * That means, if a.b.c is requested and the current scope is a.b, then c is allowed. If the current
147
     * scope is a then c is not allowed, even if it is there and potentially transformable.
148
     *
149
     * @internal
150
     *
151
     * @param string $checkScopeSegment
152
     *
153
     * @return bool Returns the new number of elements in the array.
154
     */
155 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...
156
    {
157 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...
158 21
            $scopeArray = array_slice($this->parentScopes, 1);
159 21
            array_push($scopeArray, $this->scopeIdentifier, $checkScopeSegment);
160
        } else {
161 44
            $scopeArray = [$checkScopeSegment];
162
        }
163
164 44
        $scopeString = implode('.', (array) $scopeArray);
165
166 44
        return in_array($scopeString, $this->manager->getRequestedIncludes());
167
    }
168
169
    /**
170
     * Is Excluded.
171
     *
172
     * Check if - in relation to the current scope - this specific segment should
173
     * be excluded. That means, if a.b.c is excluded and the current scope is a.b,
174
     * then c will not be allowed in the transformation whether it appears in
175
     * the list of default or available, requested includes.
176
     *
177
     * @internal
178
     *
179
     * @param string $checkScopeSegment
180
     *
181
     * @return bool
182
     */
183 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...
184
    {
185 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...
186 7
            $scopeArray = array_slice($this->parentScopes, 1);
187 7
            array_push($scopeArray, $this->scopeIdentifier, $checkScopeSegment);
188
        } else {
189 32
            $scopeArray = [$checkScopeSegment];
190
        }
191
192 32
        $scopeString = implode('.', (array) $scopeArray);
193
194 32
        return in_array($scopeString, $this->manager->getRequestedExcludes());
195
    }
196
197
    /**
198
     * Push Parent Scope.
199
     *
200
     * Push a scope identifier into parentScopes
201
     *
202
     * @internal
203
     *
204
     * @param string $identifierSegment
205
     *
206
     * @return int Returns the new number of elements in the array.
207
     */
208 1
    public function pushParentScope($identifierSegment)
209
    {
210 1
        return array_push($this->parentScopes, $identifierSegment);
211
    }
212
213
    /**
214
     * Set parent scopes.
215
     *
216
     * @internal
217
     *
218
     * @param string[] $parentScopes Value to set.
219
     *
220
     * @return $this
221
     */
222 37
    public function setParentScopes($parentScopes)
223
    {
224 37
        $this->parentScopes = $parentScopes;
225
226 37
        return $this;
227
    }
228
229
    /**
230
     * Convert the current data for this scope to an array.
231
     *
232
     * @return array|null
233
     */
234 70
    public function toArray()
235
    {
236 70
        list($rawData, $rawIncludedData) = $this->executeResourceTransformers();
237
238 69
        $serializer = $this->manager->getSerializer();
239
240 69
        $data = $this->serializeResource($serializer, $rawData);
241
242
        // If the serializer wants the includes to be side-loaded then we'll
243
        // serialize the included data and merge it with the data.
244 68
        if ($serializer->sideloadIncludes()) {
245
            //Filter out any relation that wasn't requested
246 40
            $rawIncludedData = array_map(array($this, 'filterFieldsets'), $rawIncludedData);
247
248 40
            $includedData = $serializer->includedData($this->resource, $rawIncludedData);
249
250
            // If the serializer wants to inject additional information
251
            // about the included resources, it can do so now.
252 40
            $data = $serializer->injectData($data ?: [], $rawIncludedData);
253
254 40
            if ($this->isRootScope()) {
255
                // If the serializer wants to have a final word about all
256
                // the objects that are sideloaded, it can do so now.
257 40
                $includedData = $serializer->filterIncludes(
258 40
                    $includedData,
259 40
                    $data
260
                );
261
            }
262
263 40
            $data = $data + $includedData;
264
        }
265
266 68
        if (!empty($this->availableIncludes)) {
267 47
            $data = $serializer->injectAvailableIncludeData($data ?: [], $this->availableIncludes);
268
        }
269
270 68
        if ($this->resource instanceof Collection) {
271 31
            if ($this->resource->hasCursor()) {
272 1
                $pagination = $serializer->cursor($this->resource->getCursor());
273 30
            } elseif ($this->resource->hasPaginator()) {
274 4
                $pagination = $serializer->paginator($this->resource->getPaginator());
275
            }
276
277 31
            if (! empty($pagination)) {
278 5
                $this->resource->setMetaValue((string) key($pagination), current($pagination));
279
            }
280
        }
281
282
        // Pull out all of OUR metadata and any custom meta data to merge with the main level data
283 68
        $meta = [];
284 68
        if ($this->resource instanceof MetaDataInterface) {
285 68
            $meta = $serializer->meta($this->resource->getMeta());
286
        }
287
288
        // in case of returning NullResource we should return null and not to go with array_merge
289 68
        if (is_null($data)) {
290 2
            if (!empty($meta)) {
291 1
                return $meta;
292
            }
293 1
            return null;
294
        }
295
296 67
        return $data + $meta;
297
    }
298
299
    /**
300
     * Convert the current data for this scope to JSON.
301
     *
302
     * @param int $options
303
     *
304
     * @return string
305
     */
306 40
    public function toJson($options = 0)
307
    {
308 40
        $result = json_encode($this->toArray(), $options);
309 40
        return $result ?: '';
310
    }
311
312
    /**
313
     * Transformer a primitive resource
314
     *
315
     * @return mixed
316
     */
317 2
    public function transformPrimitiveResource()
318
    {
319 2
        if (! ($this->resource instanceof Primitive)) {
320
            throw new InvalidArgumentException(
321
                'Argument $resource should be an instance of League\Fractal\Resource\Primitive'
322
            );
323
        }
324
325 2
        $transformer = $this->resource->getTransformer();
326 2
        $data = $this->resource->getData();
327
328 2
        if (null === $transformer) {
329
            $transformedData = $data;
330 2
        } elseif (is_callable($transformer)) {
331 2
            $transformedData = call_user_func($transformer, $data);
332
        } else {
333 1
            $transformer->setCurrentScope($this);
334 1
            $transformedData = $transformer->transform($data);
335
        }
336
337 2
        return $transformedData;
338
    }
339
340
    /**
341
     * Execute the resources transformer and return the data and included data.
342
     *
343
     * @internal
344
     *
345
     * @return array
346
     */
347 62
    protected function executeResourceTransformers()
348
    {
349 62
        $transformer = $this->resource->getTransformer();
350 62
        $data = $this->resource->getData();
351
352 62
        $transformedData = $includedData = [];
353
354 62
        if ($this->resource instanceof Item) {
355 45
            list($transformedData, $includedData[]) = $this->fireTransformer($transformer, $data);
356 37
        } elseif ($this->resource instanceof Collection) {
357 31
            foreach ($data as $value) {
358 30
                list($transformedData[], $includedData[]) = $this->fireTransformer($transformer, $value);
359
            }
360 7
        } elseif ($this->resource instanceof NullResource) {
361 5
            $transformedData = null;
362 5
            $includedData = [];
363
        } else {
364 2
            throw new InvalidArgumentException(
365
                'Argument $resource should be an instance of League\Fractal\Resource\Item'
366 2
                . ' or League\Fractal\Resource\Collection'
367
            );
368
        }
369
370 60
        return [$transformedData, $includedData];
371
    }
372
373
    /**
374
     * Serialize a resource
375
     *
376
     * @internal
377
     *
378
     * @param SerializerAbstract $serializer
379
     * @param mixed              $data
380
     *
381
     * @return array|null
382
     */
383 60
    protected function serializeResource(SerializerAbstract $serializer, $data)
384
    {
385 60
        $resourceKey = $this->resource->getResourceKey();
386
387 60
        if ($this->resource instanceof Collection) {
388 31
            return $serializer->collection($resourceKey, $data);
389
        }
390
391 47
        if ($this->resource instanceof Item) {
392 45
            return $serializer->item($resourceKey, $data);
393
        }
394
395 5
        return $serializer->null();
396
    }
397
398
    /**
399
     * Fire the main transformer.
400
     *
401
     * @internal
402
     *
403
     * @param TransformerAbstract|callable|null $transformer
404
     * @param mixed                        $data
405
     *
406
     * @return array
407
     */
408 58
    protected function fireTransformer($transformer, $data)
409
    {
410 58
        $includedData = [];
411
412 58
        if (is_null($transformer)) {
413
            return [$data, $includedData];
414
        }
415
416 58
        if (is_callable($transformer)) {
417 6
            $transformedData = call_user_func($transformer, $data);
418
        } else {
419 52
            $transformer->setCurrentScope($this);
420 52
            $transformedData = $transformer->transform($data);
421
422 52
            if ($this->transformerHasIncludes($transformer)) {
423 49
                $includedData = $this->fireIncludedTransformers($transformer, $data);
424 49
                $transformedData = $this->manager->getSerializer()->mergeIncludes($transformedData, $includedData);
425
            }
426
        }
427
428
        //Stick only with requested fields
429 58
        $transformedData = $this->filterFieldsets($transformedData);
430
431 58
        return [$transformedData, $includedData];
432
    }
433
434
    /**
435
     * Fire the included transformers.
436
     *
437
     * @internal
438
     *
439
     * @param \League\Fractal\TransformerAbstract $transformer
440
     * @param mixed                               $data
441
     *
442
     * @return array
443
     */
444 49
    protected function fireIncludedTransformers($transformer, $data)
445
    {
446 49
        $this->availableIncludes = $transformer->getAvailableIncludes();
447
448 49
        return $transformer->processIncludedResources($this, $data) ?: [];
449
    }
450
451
    /**
452
     * Determine if a transformer has any available includes.
453
     *
454
     * @internal
455
     *
456
     * @param TransformerAbstract|callable $transformer
457
     *
458
     * @return bool
459
     */
460 52
    protected function transformerHasIncludes($transformer)
461
    {
462 52
        if (! $transformer instanceof TransformerAbstract) {
463
            return false;
464
        }
465
466 52
        $defaultIncludes = $transformer->getDefaultIncludes();
467 52
        $availableIncludes = $transformer->getAvailableIncludes();
468
469 52
        return ! empty($defaultIncludes) || ! empty($availableIncludes);
470
    }
471
472
    /**
473
     * Check, if this is the root scope.
474
     *
475
     * @return bool
476
     */
477 38
    protected function isRootScope()
478
    {
479 38
        return empty($this->parentScopes);
480
    }
481
482
    /**
483
     * Filter the provided data with the requested filter fieldset for
484
     * the scope resource
485
     *
486
     * @internal
487
     *
488
     * @param array  $data
489
     *
490
     * @return array
491
     */
492 58
    protected function filterFieldsets(array $data)
493
    {
494 58
        if (!$this->hasFilterFieldset()) {
495 54
            return $data;
496
        }
497 10
        $serializer = $this->manager->getSerializer();
498 10
        $bag = $this->getFilterFieldset();
499 10
        $requestedFieldset = $bag ? iterator_to_array($bag) : [];
500
        //Build the array of requested fieldsets with the mandatory serializer fields
501 10
        $filterFieldset = array_flip(
502 10
            array_merge(
503 10
                $serializer->getMandatoryFields(),
504 10
                $requestedFieldset
505
            )
506
        );
507 10
        return array_intersect_key($data, $filterFieldset);
508
    }
509
510
    /**
511
     * Return the requested filter fieldset for the scope resource
512
     *
513
     * @internal
514
     *
515
     * @return \League\Fractal\ParamBag|null
516
     */
517 58
    protected function getFilterFieldset()
518
    {
519 58
        return $this->manager->getFieldset($this->getResourceType());
520
    }
521
522
    /**
523
     * Check if there are requested filter fieldsets for the scope resource.
524
     *
525
     * @internal
526
     *
527
     * @return bool
528
     */
529 58
    protected function hasFilterFieldset()
530
    {
531 58
        return $this->getFilterFieldset() !== null;
532
    }
533
534
    /**
535
     * Return the scope resource type.
536
     *
537
     * @internal
538
     *
539
     * @return string
540
     */
541 58
    protected function getResourceType()
542
    {
543 58
        return $this->resource->getResourceKey();
544
    }
545
}
546