Completed
Pull Request — master (#119)
by Toby
65:37 queued 63:37
created

Document::mergeResources()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 33
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 6.0087

Importance

Changes 0
Metric Value
dl 0
loc 33
ccs 15
cts 16
cp 0.9375
rs 8.439
c 0
b 0
f 0
cc 6
eloc 12
nc 6
nop 3
crap 6.0087
1
<?php
2
3
/*
4
 * This file is part of JSON-API.
5
 *
6
 * (c) Toby Zerner <[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 Tobscure\JsonApi;
13
14
use JsonSerializable;
15
16
class Document implements JsonSerializable
17
{
18
    use LinksTrait;
19
    use SelfLinkTrait;
20
    use PaginationLinksTrait;
21
    use MetaTrait;
22
23
    const MEDIA_TYPE = 'application/vnd.api+json';
24
    const DEFAULT_API_VERSION = '1.0';
25
26
    /**
27
     * The primary data.
28
     *
29
     * @var ResourceInterface|ResourceInterface[]|null
30
     */
31
    protected $data;
32
33
    /**
34
     * The errors array.
35
     *
36
     * @var Error[]|null
37
     */
38
    protected $errors;
39
40
    /**
41
     * The jsonapi array.
42
     *
43
     * @var array|null
44
     */
45
    protected $jsonapi;
46
47
    /**
48
     * Relationships to include.
49
     *
50
     * @var array
51
     */
52
    protected $include = [];
53
54
    /**
55
     * Sparse fieldsets.
56
     *
57
     * @var array
58
     */
59
    protected $fields = [];
60
61
    /**
62
     * Use named constructors instead.
63
     */
64 24
    private function __construct()
65
    {
66 24
    }
67
68
    /**
69
     * @param ResourceInterface|ResourceInterface[] $data
70
     *
71
     * @return self
72
     */
73 18
    public static function fromData($data)
74
    {
75 18
        $document = new self;
76 18
        $document->setData($data);
77
78 18
        return $document;
79
    }
80
81
    /**
82
     * @param array $meta
83
     *
84
     * @return self
85
     */
86 3
    public static function fromMeta(array $meta)
87
    {
88 3
        $document = new self;
89 3
        $document->replaceMeta($meta);
90
91 3
        return $document;
92
    }
93
94
    /**
95
     * @param Error[] $errors
96
     *
97
     * @return self
98
     */
99 3
    public static function fromErrors(array $errors)
100
    {
101 3
        $document = new self;
102 3
        $document->setErrors($errors);
103
104 3
        return $document;
105
    }
106
107
    /**
108
     * Get the primary data.
109
     *
110
     * @return ResourceInterface|ResourceInterface[]|null $data
111
     */
112
    public function getData()
113
    {
114
        return $this->data;
115
    }
116
117
    /**
118
     * Set the data object.
119
     *
120
     * @param ResourceInterface|ResourceInterface[]|null $data
121
     */
122 18
    public function setData($data)
123
    {
124 18
        $this->data = $data;
125 18
    }
126
127
    /**
128
     * Get the errors array.
129
     *
130
     * @return Error[]|null $errors
131
     */
132
    public function getErrors()
133
    {
134
        return $this->errors;
135
    }
136
137
    /**
138
     * Set the errors array.
139
     *
140
     * @param Error[]|null $errors
141
     */
142 3
    public function setErrors(array $errors = null)
143
    {
144 3
        $this->errors = $errors;
145 3
    }
146
147
    /**
148
     * Set the jsonapi version.
149
     *
150
     * @param string $version
151
     */
152
    public function setApiVersion($version)
153
    {
154
        $this->jsonapi['version'] = $version;
155
    }
156
157
    /**
158
     * Set the jsonapi meta information.
159
     *
160
     * @param array $meta
161
     */
162
    public function setApiMeta(array $meta)
163
    {
164
        $this->jsonapi['meta'] = $meta;
165
    }
166
167
    /**
168
     * Get the relationships to include.
169
     *
170
     * @return string[] $include
171
     */
172
    public function getInclude()
173
    {
174
        return $this->include;
175
    }
176
177
    /**
178
     * Set the relationships to include.
179
     *
180
     * @param string[] $include
181
     */
182 3
    public function setInclude(array $include)
183
    {
184 3
        $this->include = $include;
185 3
    }
186
187
    /**
188
     * Get the sparse fieldsets.
189
     *
190
     * @return array[] $fields
191
     */
192
    public function getFields()
193
    {
194
        return $this->fields;
195
    }
196
197
    /**
198
     * Set the sparse fieldsets.
199
     *
200
     * @param array[] $fields
201
     */
202 3
    public function setFields(array $fields)
203
    {
204 3
        $this->fields = $fields;
205 3
    }
206
207
    /**
208
     * Serialize for JSON usage.
209
     *
210
     * @return array
211
     */
212 24
    public function jsonSerialize()
213
    {
214
        $document = [
215 24
            'links' => $this->links,
216 24
            'meta' => $this->meta,
217 24
            'errors' => $this->errors,
218 24
            'jsonapi' => $this->jsonapi
219 24
        ];
220
221 24
        if ($this->data) {
222 15
            $isCollection = is_array($this->data);
223
224
            // Build a multi-dimensional map of all of the distinct resources
225
            // that are present in the document, indexed by type and ID. This is
226
            // done by recursively looping through each of the resources and
227
            // their included relationships. We do this so that any resources
228
            // that are duplicated may be merged back into a single instance.
229 15
            $map = [];
230 15
            $resources = $isCollection ? $this->data : [$this->data];
231
232 15
            $this->mergeResources($map, $resources, $this->include);
233
234
            // Now extract the document's primary resource(s) from the resource
235
            // map, and flatten the map's remaining resources to be included in
236
            // the document's "included" array.
237 15
            foreach ($resources as $resource) {
238 15
                $type = $resource->getType();
239 15
                $id = $resource->getId();
240
241 15
                if (isset($map[$type][$id])) {
242 15
                    $primary[] = $map[$type][$id];
0 ignored issues
show
Coding Style Comprehensibility introduced by
$primary was never initialized. Although not strictly required by PHP, it is generally a good practice to add $primary = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
243 15
                    unset($map[$type][$id]);
244 15
                }
245 15
            }
246
247 15
            $document['data'] = $isCollection ? $primary : $primary[0];
0 ignored issues
show
Bug introduced by
The variable $primary does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
248 15
            $document['included'] = call_user_func_array('array_merge', $map);
249 15
        }
250
251 24
        return array_filter($document);
252
    }
253
254
    /**
255
     * Build the JSON-API document and encode it as a JSON string.
256
     *
257
     * @return string
258
     */
259
    public function __toString()
260
    {
261
        return json_encode($this->jsonSerialize());
262
    }
263
264
    /**
265
     * Recursively add the given resources and their relationships to a map.
266
     *
267
     * @param array &$map The map to merge resources into.
268
     * @param ResourceInterface[] $resources
269
     * @param array $include An array of relationship paths to include.
270
     */
271 15
    private function mergeResources(array &$map, array $resources, array $include)
272
    {
273
        // Index relationship paths so that we have a list of the direct
274
        // relationships that will be included on these resources, and arrays
275
        // of their respective nested relationships.
276 15
        $include = $this->indexRelationshipPaths($include);
277
278 15
        foreach ($resources as $resource) {
279 15
            $relationships = [];
280
281
            // Get each of the relationships we're including on this resource,
282
            // and add their resources (and their relationships, and so on) to
283
            // the map.
284 15
            foreach ($include as $name => $nested) {
285 3
                if (! ($relationship = $resource->getRelationship($name))) {
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $relationship is correct as $resource->getRelationship($name) (which targets Tobscure\JsonApi\Resourc...face::getRelationship()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
286
                    continue;
287
                }
288
289 3
                $relationships[$name] = $relationship;
290
291 3
                if ($data = $relationship->getData()) {
292 3
                    $children = is_array($data) ? $data : [$data];
293
294 3
                    $this->mergeResources($map, $children, $nested);
295 3
                }
296 15
            }
297
298
            // Serialize the resource into an array and add it to the map. If
299
            // it is already present, its properties will be merged into the
300
            // existing resource.
301 15
            $this->mergeResource($map, $resource, $relationships);
302 15
        }
303 15
    }
304
305
    /**
306
     * Merge the given resource into a resource map.
307
     *
308
     * If it is already present in the map, its properties will be merged into
309
     * the existing resource.
310
     *
311
     * @param array &$map
312
     * @param ResourceInterface $resource
313
     * @param Relationship[] $relationships
314
     */
315 15
    private function mergeResource(array &$map, ResourceInterface $resource, array $relationships)
316
    {
317 15
        $type = $resource->getType();
318 15
        $id = $resource->getId();
319 15
        $meta = $resource->getMeta();
320 15
        $links = $resource->getLinks();
321
322 15
        $fields = isset($this->fields[$type]) ? $this->fields[$type] : null;
323
324 15
        $attributes = $resource->getAttributes($fields);
325
326 15
        if ($fields) {
327 3
            $keys = array_flip($fields);
328
329 3
            $attributes = array_intersect_key($attributes, $keys);
330 3
            $relationships = array_intersect_key($relationships, $keys);
331 3
        }
332
333 15
        $props = array_filter(compact('attributes', 'relationships', 'links', 'meta'));
334
335 15
        if (empty($map[$type][$id])) {
336 15
            $map[$type][$id] = compact('type', 'id') + $props;
337 15
        } else {
338 3
            $map[$type][$id] = array_replace_recursive($map[$type][$id], $props);
339
        }
340 15
    }
341
342
    /**
343
     * Index relationship paths by top-level relationships.
344
     *
345
     * Given an array of relationship paths such as:
346
     *
347
     * ['user', 'user.employer', 'user.employer.country', 'comments']
348
     *
349
     * Returns an array with key-value pairs of top-level relationships and
350
     * their nested relationships:
351
     *
352
     * ['user' => ['employer', 'employer.country'], 'comments' => []]
353
     *
354
     * @param string[] $paths
355
     *
356
     * @return array[]
357
     */
358 15
    private function indexRelationshipPaths(array $paths)
359
    {
360 15
        $tree = [];
361
362 15
        foreach ($paths as $path) {
363 3
            list($primary, $nested) = array_pad(explode('.', $path, 2), 2, null);
364
365 3
            if (! isset($tree[$primary])) {
366 3
                $tree[$primary] = [];
367 3
            }
368
369 3
            if ($nested) {
370 3
                $tree[$primary][] = $nested;
371 3
            }
372 15
        }
373
374 15
        return $tree;
375
    }
376
}
377