Completed
Pull Request — master (#119)
by Toby
04:40
created

Document::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
ccs 1
cts 1
cp 1
cc 1
eloc 2
nc 1
nop 1
crap 1
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
use LogicException;
16
17
class Document implements JsonSerializable
18
{
19
    use LinksTrait;
20
    use MetaTrait;
21
22
    const MEDIA_TYPE = 'application/vnd.api+json';
23
24
    /**
25
     * The data object.
26
     *
27
     * @var ResourceInterface|ResourceInterface[]|null
28
     */
29
    protected $data;
30
31
    /**
32
     * The errors array.
33
     *
34
     * @var array|null
35
     */
36
    protected $errors;
37
38
    /**
39
     * The jsonapi array.
40
     *
41
     * @var array|null
42
     */
43
    protected $jsonapi;
44
45
    /**
46
     * Relationships to include.
47
     * 
48
     * @var array
49
     */
50
    protected $include = [];
51
52 12
    /**
53
     * Sparse fieldsets.
54 12
     * 
55 12
     * @var array
56
     */
57
    protected $fields = [];
58
59
    /**
60
     * @param ResourceInterface|ResourceInterface[] $data
0 ignored issues
show
Documentation introduced by
Should the type for parameter $data not be ResourceInterface|ResourceInterface[]|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
61
     */
62
    public function __construct($data = null)
63
    {
64
        $this->data = $data;
65 9
    }
66
67 9
    /**
68
     * Get the data object.
69 9
     * 
70 9
     * @return ResourceInterface|ResourceInterface[]|null $data
71
     */
72
    public function getData()
73
    {
74 9
        return $this->data;
75 3
    }
76 3
77 9
    /**
78 9
     * Set the data object.
79
     *
80
     * @param ResourceInterface|ResourceInterface[]|null $data
81 9
     *
82 3
     * @return $this
83
     */
84 3
    public function setData($data)
85
    {
86
        $this->data = $data;
87
88 3
        return $this;
89
    }
90
91
    /**
92 3
     * Get the errors array.
93
     * 
94
     * @return array|null $errors
95
     */
96 3
    public function getErrors()
97 3
    {
98 9
        return $this->errors;
99 9
    }
100
101 9
    /**
102
     * Set the errors array.
103
     *
104 3
     * @param array|null $errors
105 9
     *
106
     * @return $this
107 9
     */
108
    public function setErrors(array $errors = null)
109
    {
110
        $this->errors = $errors;
111
112
        return $this;
113
    }
114
115
    /**
116 3
     * Get the jsonapi array.
117
     * 
118 3
     * @return array|null $jsonapi
119 3
     */
120
    public function getJsonapi()
121 3
    {
122
        return $this->jsonapi;
123
    }
124 3
125
    /**
126
     * Set the jsonapi array.
127 3
     *
128
     * @param array|null $jsonapi
129
     *
130
     * @return $this
131
     */
132
    public function setJsonapi(array $jsonapi = null)
133
    {
134
        $this->jsonapi = $jsonapi;
135
136
        return $this;
137
    }
138
139
    /**
140
     * Get the relationships to include.
141
     * 
142
     * @return array $include
143
     */
144
    public function getInclude()
145
    {
146
        return $this->include;
147
    }
148
149
    /**
150
     * Set the relationships to include.
151
     * 
152
     * @param array $include
153
     *
154
     * @return $this
155
     */
156
    public function setInclude(array $include)
157
    {
158
        $this->include = $include;
159
160
        return $this;
161
    }
162
163
    /**
164
     * Get the sparse fieldsets.
165
     * 
166
     * @return array $fields
167
     */
168
    public function getFields()
169
    {
170
        return $this->fields;
171
    }
172
173
    /**
174
     * Set the sparse fieldsets.
175
     * 
176
     * @param array $fields
177 12
     *
178
     * @return $this
179 12
     */
180
    public function setFields(array $fields)
181 12
    {
182
        $this->fields = $fields;
183
184
        return $this;
185 12
    }
186 9
187
    /**
188 9
     * Build the JSON-API document as an array.
189
     *
190 9
     * @return array
191 3
     */
192 3
    public function toArray()
193 3
    {
194 3
        $document = [];
195 9
196
        if ($this->links) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->links 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...
197 12
            $document['links'] = $this->links;
198
        }
199
200
        if ($this->data) {
201 12
            $isCollection = is_array($this->data);
202
203
            // Build a multi-dimensional map of all of the distinct resources
204
            // that are present in the document, indexed by type and ID. This is
205 12
            // done by recursively looping through each of the resources and
206
            // their included relationships. We do this so that any resources
207
            // that are duplicated may be merged back into a single instance.
208
            $map = [];
209 12
            $resources = $isCollection ? $this->data : [$this->data];
210
211
            $this->addResourcesToMap($map, $resources, $this->include);
212
213
            // Now extract the document's primary resource(s) from the resource
214
            // map, and flatten the map's remaining resources to be included in
215
            // the document's "included" array.
216
            foreach ($resources as $resource) {
217 6
                $type = $resource->getType();
218
                $id = $resource->getId();
219 6
220
                $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...
221
                unset($map[$type][$id]);
222
            }
223
224
            $included = call_user_func_array('array_merge', $map);
225
226
            $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...
227
228
            if ($included) {
229
                $document['included'] = $included;
230
            }
231
        }
232
233
        if ($this->meta) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->meta 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...
234
            $document['meta'] = $this->meta;
235
        }
236
237
        if ($this->errors) {
238
            $document['errors'] = $this->errors;
239
        }
240
241
        if ($this->jsonapi) {
242
            $document['jsonapi'] = $this->jsonapi;
243
        }
244
245
        return $document;
246
    }
247
248
    /**
249
     * Build the JSON-API document and encode it as a JSON string.
250
     *
251
     * @return string
252
     */
253
    public function __toString()
254
    {
255
        return json_encode($this->toArray());
256
    }
257
258
    /**
259
     * Serialize for JSON usage.
260
     *
261
     * @return array
262
     */
263
    public function jsonSerialize()
264
    {
265
        return $this->toArray();
266
    }
267
268
    /**
269
     * Recursively add the given resources and their relationships to a map.
270
     * 
271
     * @param array &$map The map to merge resources into.
272
     * @param ResourceInterface[] $resources
273
     * @param array $include An array of relationship paths to include.
274
     */
275
    private function addResourcesToMap(array &$map, array $resources, array $include)
276
    {
277
        // Index relationship paths so that we have a list of the direct
278
        // relationships that will be included on these resources, and arrays
279
        // of their respective nested relationships.
280
        $include = $this->indexRelationshipPaths($include);
281
282
        foreach ($resources as $resource) {
283
            $relationships = [];
284
285
            // Get each of the relationships we're including on this resource,
286
            // and add their resources (and their relationships, and so on) to
287
            // the map.
288
            foreach ($include as $name => $nested) {
289
                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...
290
                    continue;
291
                }
292
293
                $relationships[$name] = $relationship;
294
295
                if ($data = $relationship->getData()) {
296
                    $children = is_array($data) ? $data : [$data];
297
298
                    $this->addResourcesToMap($map, $children, $nested);
299
                }
300
            }
301
302
            // Serialize the resource into an array and add it to the map. If
303
            // it is already present, its properties will be merged into the
304
            // existing resource.
305
            $this->addResourceToMap($map, $resource, $relationships);
306
        }
307
    }
308
309
    /**
310
     * Serialize the given resource as an array and add it to the given map.
311
     *
312
     * If it is already present in the map, its properties will be merged into
313
     * the existing array.
314
     * 
315
     * @param array &$map
316
     * @param ResourceInterface $resource
317
     * @param Relationship[] $resource
318
     */
319
    private function addResourceToMap(array &$map, ResourceInterface $resource, array $relationships)
320
    {
321
        $type = $resource->getType();
322
        $id = $resource->getId();
323
324
        if (empty($map[$type][$id])) {
325
            $map[$type][$id] = [
326
                'type' => $type,
327
                'id' => $id
328
            ];
329
        }
330
331
        $array = &$map[$type][$id];
332
        $fields = $this->getFieldsForType($type);
333
334 View Code Duplication
        if ($meta = $resource->getMeta()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
335
            $array['meta'] = array_replace_recursive(isset($array['meta']) ? $array['meta'] : [], $meta);
336
        }
337
338 View Code Duplication
        if ($links = $resource->getLinks()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
339
            $array['links'] = array_replace_recursive(isset($array['links']) ? $array['links'] : [], $links);
340
        }
341
342
        if ($attributes = $resource->getAttributes($fields)) {
343
            if ($fields) {
344
                $attributes = array_intersect_key($attributes, array_flip($fields));
345
            }
346
            if ($attributes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $attributes 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...
347
                $array['attributes'] = array_replace_recursive(isset($array['attributes']) ? $array['attributes'] : [], $attributes);
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 133 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
348
            }
349
        }
350
351
        if ($relationships && $fields) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $relationships 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...
352
            $relationships = array_intersect_key($relationships, array_flip($fields));
353
        }
354
        if ($relationships) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $relationships 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...
355
            $relationships = array_map(function ($relationship) {
356
                return $relationship->toArray();
357
            }, $relationships);
358
359
            $array['relationships'] = array_replace_recursive(isset($array['relationships']) ? $array['relationships'] : [], $relationships);
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 141 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
360
        }
361
    }
362
363
    /**
364
     * Index relationship paths by top-level relationships.
365
     *
366
     * Given an array of relationship paths such as:
367
     *
368
     * ['user', 'user.employer', 'user.employer.country', 'comments']
369
     *
370
     * Returns an array with key-value pairs of top-level relationships and
371
     * their nested relationships:
372
     *
373
     * ['user' => ['employer', 'employer.country'], 'comments' => []]
374
     *
375
     * @param array $paths
376
     *
377
     * @return array
378
     */
379
    private function indexRelationshipPaths(array $paths)
380
    {
381
        $tree = [];
382
383
        foreach ($paths as $path) {
384
            list($primary, $nested) = array_pad(explode('.', $path, 2), 2, null);
385
386
            if (! isset($tree[$primary])) {
387
                $tree[$primary] = [];
388
            }
389
390
            if ($nested) {
391
                $tree[$primary][] = $nested;
392
            }
393
        }
394
395
        return $tree;
396
    }
397
398
    /**
399
     * Get the fields that should be included for resources of the given type.
400
     * 
401
     * @param string $type
402
     * 
403
     * @return array|null
404
     */
405
    private function getFieldsForType($type)
406
    {
407
        return isset($this->fields[$type]) ? $this->fields[$type] : null;
408
    }
409
}
410