Completed
Push — master ( 1cb9cd...ecf884 )
by Korvin
05:21 queued 01:49
created

Scope   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 515
Duplicated Lines 5.05 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 98.08%

Importance

Changes 0
Metric Value
wmc 51
lcom 1
cbo 6
dl 26
loc 515
ccs 153
cts 156
cp 0.9808
rs 7.92
c 0
b 0
f 0

25 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A embedChildScope() 0 4 1
A getScopeIdentifier() 0 4 1
A getIdentifier() 0 6 1
A getParentScopes() 0 4 1
A getResource() 0 4 1
A getManager() 0 4 1
A pushParentScope() 0 4 1
A setParentScopes() 0 6 1
A isRequested() 13 13 2
A isExcluded() 13 13 2
C toArray() 0 61 10
A jsonSerialize() 0 4 1
A toJson() 0 4 1
A transformPrimitiveResource() 0 22 4
A executeResourceTransformers() 0 25 5
A serializeResource() 0 14 3
A fireTransformer() 0 21 3
A fireIncludedTransformers() 0 6 2
A transformerHasIncludes() 0 11 3
A isRootScope() 0 4 1
A filterFieldsets() 0 16 2
A getFilterFieldset() 0 4 1
A hasFilterFieldset() 0 4 1
A getResourceType() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Scope often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Scope, and based on these observations, apply Extract Interface, too.

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 implements \JsonSerializable
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 Phil Sturgeon
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 Phil Sturgeon
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 Jason Daly
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 Jason Daly
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
     * @return array
297
     */
298 41
    public function jsonSerialize()
299
    {
300 41
        return $this->toArray();
301
    }
302
303
  /**
304
     * Convert the current data for this scope to JSON.
305
     *
306
     * @param int $options
307
     *
308
     * @return string
309
     */
310 40
    public function toJson($options = 0)
311
    {
312 40
        return json_encode($this, $options);
313
    }
314
315
    /**
316
     * Transformer a primitive resource
317
     *
318
     * @return mixed
319
     */
320 2
    public function transformPrimitiveResource()
321
    {
322 2
        if (! ($this->resource instanceof Primitive)) {
323
            throw new InvalidArgumentException(
324
                'Argument $resource should be an instance of League\Fractal\Resource\Primitive'
325
            );
326
        }
327
328 2
        $transformer = $this->resource->getTransformer();
329 2
        $data = $this->resource->getData();
330
331 2
        if (null === $transformer) {
332
            $transformedData = $data;
333 2
        } elseif (is_callable($transformer)) {
334 2
            $transformedData = call_user_func($transformer, $data);
335 2
        } else {
336 1
            $transformer->setCurrentScope($this);
337 1
            $transformedData = $transformer->transform($data);
338
        }
339
340 2
        return $transformedData;
341
    }
342
343
    /**
344
     * Execute the resources transformer and return the data and included data.
345
     *
346
     * @internal
347
     *
348
     * @return array
349
     */
350 62
    protected function executeResourceTransformers()
351
    {
352 62
        $transformer = $this->resource->getTransformer();
353 62
        $data = $this->resource->getData();
354
355 62
        $transformedData = $includedData = [];
356
357 62
        if ($this->resource instanceof Item) {
358 45
            list($transformedData, $includedData[]) = $this->fireTransformer($transformer, $data);
359 62
        } elseif ($this->resource instanceof Collection) {
360 31
            foreach ($data as $value) {
361 30
                list($transformedData[], $includedData[]) = $this->fireTransformer($transformer, $value);
362 31
            }
363 37
        } elseif ($this->resource instanceof NullResource) {
364 5
            $transformedData = null;
365 5
            $includedData = [];
366 5
        } else {
367 2
            throw new InvalidArgumentException(
368
                'Argument $resource should be an instance of League\Fractal\Resource\Item'
369
                .' or League\Fractal\Resource\Collection'
370 2
            );
371
        }
372
373 60
        return [$transformedData, $includedData];
374
    }
375
376
    /**
377
     * Serialize a resource
378
     *
379
     * @internal
380
     *
381
     * @param SerializerAbstract $serializer
382
     * @param mixed              $data
383
     *
384
     * @return array
385
     */
386 60
    protected function serializeResource(SerializerAbstract $serializer, $data)
387
    {
388 60
        $resourceKey = $this->resource->getResourceKey();
389
390 60
        if ($this->resource instanceof Collection) {
391 31
            return $serializer->collection($resourceKey, $data);
392
        }
393
394 47
        if ($this->resource instanceof Item) {
395 45
            return $serializer->item($resourceKey, $data);
396
        }
397
398 5
        return $serializer->null();
399
    }
400
401
    /**
402
     * Fire the main transformer.
403
     *
404
     * @internal
405
     *
406
     * @param TransformerAbstract|callable $transformer
407
     * @param mixed                        $data
408
     *
409
     * @return array
410
     */
411 58
    protected function fireTransformer($transformer, $data)
412
    {
413 58
        $includedData = [];
414
415 58
        if (is_callable($transformer)) {
416 6
            $transformedData = call_user_func($transformer, $data);
417 6
        } else {
418 52
            $transformer->setCurrentScope($this);
419 52
            $transformedData = $transformer->transform($data);
420
        }
421
422 58
        if ($this->transformerHasIncludes($transformer)) {
423 49
            $includedData = $this->fireIncludedTransformers($transformer, $data);
424 49
            $transformedData = $this->manager->getSerializer()->mergeIncludes($transformedData, $includedData);
425 49
        }
426
427
        //Stick only with requested fields
428 58
        $transformedData = $this->filterFieldsets($transformedData);
429
430 58
        return [$transformedData, $includedData];
431
    }
432
433
    /**
434
     * Fire the included transformers.
435
     *
436
     * @internal
437
     *
438
     * @param \League\Fractal\TransformerAbstract $transformer
439
     * @param mixed                               $data
440
     *
441
     * @return array
442
     */
443 49
    protected function fireIncludedTransformers($transformer, $data)
444
    {
445 49
        $this->availableIncludes = $transformer->getAvailableIncludes();
446
447 49
        return $transformer->processIncludedResources($this, $data) ?: [];
448
    }
449
450
    /**
451
     * Determine if a transformer has any available includes.
452
     *
453
     * @internal
454
     *
455
     * @param TransformerAbstract|callable $transformer
456
     *
457
     * @return bool
458
     */
459 58
    protected function transformerHasIncludes($transformer)
460
    {
461 58
        if (! $transformer instanceof TransformerAbstract) {
462 6
            return false;
463
        }
464
465 52
        $defaultIncludes = $transformer->getDefaultIncludes();
466 52
        $availableIncludes = $transformer->getAvailableIncludes();
467
468 52
        return ! empty($defaultIncludes) || ! empty($availableIncludes);
469
    }
470
471
    /**
472
     * Check, if this is the root scope.
473
     *
474
     * @return bool
475
     */
476 38
    protected function isRootScope()
477
    {
478 38
        return empty($this->parentScopes);
479
    }
480
481
    /**
482
     * Filter the provided data with the requested filter fieldset for
483
     * the scope resource
484
     *
485
     * @internal
486
     *
487
     * @param array  $data
488
     *
489
     * @return array
490
     */
491 58
    protected function filterFieldsets(array $data)
492
    {
493 58
        if (!$this->hasFilterFieldset()) {
494 54
            return $data;
495
        }
496 10
        $serializer = $this->manager->getSerializer();
497 10
        $requestedFieldset = iterator_to_array($this->getFilterFieldset());
498
        //Build the array of requested fieldsets with the mandatory serializer fields
499 10
        $filterFieldset = array_flip(
500 10
            array_merge(
501 10
                $serializer->getMandatoryFields(),
502
                $requestedFieldset
503 10
            )
504 10
        );
505 10
        return array_intersect_key($data, $filterFieldset);
506
    }
507
508
    /**
509
     * Return the requested filter fieldset for the scope resource
510
     *
511
     * @internal
512
     *
513
     * @return \League\Fractal\ParamBag|null
514
     */
515 58
    protected function getFilterFieldset()
516
    {
517 58
        return $this->manager->getFieldset($this->getResourceType());
518
    }
519
520
    /**
521
     * Check if there are requested filter fieldsets for the scope resource.
522
     *
523
     * @internal
524
     *
525
     * @return bool
526
     */
527 58
    protected function hasFilterFieldset()
528
    {
529 58
        return $this->getFilterFieldset() !== null;
530
    }
531
532
    /**
533
     * Return the scope resource type.
534
     *
535
     * @internal
536
     *
537
     * @return string
538
     */
539 58
    protected function getResourceType()
540
    {
541 58
        return $this->resource->getResourceKey();
542
    }
543
}
544