Completed
Push — master ( ebc071...9dcd83 )
by Tobias
29:49 queued 29:28
created

src/Scope.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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)
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
        } 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)
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
        } 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
                );
260
            }
261
262 40
            $data = $data + $includedData;
263
        }
264
265 68
        if (!empty($this->availableIncludes)) {
266 47
            $data = $serializer->injectAvailableIncludeData($data, $this->availableIncludes);
267
        }
268
269 68
        if ($this->resource instanceof Collection) {
270 31
            if ($this->resource->hasCursor()) {
271 1
                $pagination = $serializer->cursor($this->resource->getCursor());
272 30
            } elseif ($this->resource->hasPaginator()) {
273 4
                $pagination = $serializer->paginator($this->resource->getPaginator());
274
            }
275
276 31
            if (! empty($pagination)) {
277 5
                $this->resource->setMetaValue(key($pagination), current($pagination));
278
            }
279
        }
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
        } 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 37
        } elseif ($this->resource instanceof Collection) {
360 31
            foreach ($data as $value) {
361 30
                list($transformedData[], $includedData[]) = $this->fireTransformer($transformer, $value);
362
            }
363 7
        } elseif ($this->resource instanceof NullResource) {
364 5
            $transformedData = null;
365 5
            $includedData = [];
366
        } else {
367 2
            throw new InvalidArgumentException(
368
                'Argument $resource should be an instance of League\Fractal\Resource\Item'
369 2
                .' or League\Fractal\Resource\Collection'
370
            );
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
        } 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
        }
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
            )
504
        );
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