Passed
Pull Request — master (#269)
by Christopher
03:14
created

CynicDeserialiser::isEntryOK()   C

Complexity

Conditions 12
Paths 101

Size

Total Lines 38
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
eloc 24
nc 101
nop 1
dl 0
loc 38
rs 6.9583
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace POData\ObjectModel;
6
7
use Exception;
8
use InvalidArgumentException;
9
use POData\Common\InvalidOperationException;
10
use POData\Common\ODataException;
11
use POData\Providers\Metadata\IMetadataProvider;
12
use POData\Providers\Metadata\ResourceEntityType;
13
use POData\Providers\Metadata\ResourceSet;
14
use POData\Providers\Metadata\Type\IType;
15
use POData\Providers\ProvidersWrapper;
16
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\KeyDescriptor;
17
use ReflectionClass;
18
use ReflectionException;
19
20
/**
21
 * Class CynicDeserialiser.
22
 * @package POData\ObjectModel
23
 */
24
class CynicDeserialiser
25
{
26
    /**
27
     * @var IMetadataProvider
28
     */
29
    private $metaProvider;
30
31
    /**
32
     * @var ProvidersWrapper
33
     */
34
    private $wrapper;
35
36
    /**
37
     * @var ModelDeserialiser
38
     */
39
    private $cereal;
40
41
    /**
42
     * CynicDeserialiser constructor.
43
     * @param IMetadataProvider $meta
44
     * @param ProvidersWrapper  $wrapper
45
     */
46
    public function __construct(IMetadataProvider $meta, ProvidersWrapper $wrapper)
47
    {
48
        $this->metaProvider = $meta;
49
        $this->wrapper      = $wrapper;
50
        $this->cereal       = new ModelDeserialiser();
51
    }
52
53
    /**
54
     * @param  ODataEntry                $payload
55
     * @throws ODataException
56
     * @throws ReflectionException
57
     * @throws InvalidOperationException
58
     * @return mixed
59
     */
60
    public function processPayload(ODataEntry &$payload)
61
    {
62
        $entryOk = $this->isEntryOK($payload);
63
        if (!$entryOk) {
0 ignored issues
show
introduced by
The condition $entryOk is always true.
Loading history...
64
            throw new InvalidOperationException('Payload not OK');
65
        }
66
        list($sourceSet, $source) = $this->processEntryContent($payload);
67
        if (!$sourceSet instanceof ResourceSet) {
0 ignored issues
show
introduced by
$sourceSet is always a sub-type of POData\Providers\Metadata\ResourceSet.
Loading history...
68
            throw new InvalidOperationException('$sourceSet not instanceof ResourceSet');
69
        }
70
        $numLinks = count($payload->links);
71
        for ($i = 0; $i < $numLinks; $i++) {
72
            $this->processLink($payload->links[$i], $sourceSet, $source);
73
        }
74
        if (!$this->isEntryProcessed($payload)) {
75
            throw new InvalidOperationException('Payload not processed');
76
        }
77
        return $source;
78
    }
79
80
    /**
81
     * Check if supplied ODataEntry is well-formed.
82
     *
83
     * @param  ODataEntry $payload
84
     * @return bool
85
     */
86
    protected function isEntryOK(ODataEntry $payload)
87
    {
88
        // check links
89
        foreach ($payload->links as $link) {
90
            $hasUrl      = null !== $link->getUrl();
91
            $hasExpanded = isset($link->expandedResult);
0 ignored issues
show
Bug introduced by
The property expandedResult is declared private in POData\ObjectModel\ODataLink and cannot be accessed from this context.
Loading history...
92
            if ($hasUrl) {
93
                if (!is_string($link->getUrl())) {
94
                    $msg = 'Url must be either string or null';
95
                    throw new InvalidArgumentException($msg);
96
                }
97
            }
98
            if ($hasExpanded) {
99
                $isGood = $link->expandedResult instanceof ODataEntry || $link->expandedResult instanceof ODataFeed;
0 ignored issues
show
introduced by
$link->expandedResult is always a sub-type of POData\ObjectModel\ODataFeed.
Loading history...
100
                if (!$isGood) {
101
                    $msg = 'Expanded result must null, or be instance of ODataEntry or ODataFeed';
102
                    throw new InvalidArgumentException($msg);
103
                }
104
            }
105
            $isEntry = ($link->getExpandedResult() ? $link->getExpandedResult()->getData() : null) instanceof ODataEntry;
106
107
            if ($hasExpanded) {
108
                if ($isEntry) {
109
                    $this->isEntryOK($link->expandedResult);
110
                } else {
111
                    foreach ($link->expandedResult->entries as $expanded) {
112
                        $this->isEntryOK($expanded);
113
                    }
114
                }
115
            }
116
        }
117
118
        $set = $this->getMetaProvider()->resolveResourceSet($payload->resourceSetName);
0 ignored issues
show
Bug introduced by
It seems like $payload->resourceSetName can also be of type null; however, parameter $name of POData\Providers\Metadat...r::resolveResourceSet() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

118
        $set = $this->getMetaProvider()->resolveResourceSet(/** @scrutinizer ignore-type */ $payload->resourceSetName);
Loading history...
119
        if (null === $set) {
120
            $msg = 'Specified resource set could not be resolved';
121
            throw new InvalidArgumentException($msg);
122
        }
123
        return true;
124
    }
125
126
    /**
127
     * @return IMetadataProvider
128
     */
129
    protected function getMetaProvider()
130
    {
131
        return $this->metaProvider;
132
    }
133
134
    /**
135
     * @param  ODataEntry                $content
136
     * @throws ODataException
137
     * @throws ReflectionException
138
     * @throws Exception
139
     * @throws InvalidOperationException
140
     * @return array
141
     */
142
    protected function processEntryContent(ODataEntry &$content)
143
    {
144
        assert(null === $content->id || is_string($content->id), 'Entry id must be null or string');
145
146
        $isCreate = null === $content->id || empty($content->id);
147
        $set      = $this->getMetaProvider()->resolveResourceSet($content->resourceSetName);
0 ignored issues
show
Bug introduced by
It seems like $content->resourceSetName can also be of type null; however, parameter $name of POData\Providers\Metadat...r::resolveResourceSet() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

147
        $set      = $this->getMetaProvider()->resolveResourceSet(/** @scrutinizer ignore-type */ $content->resourceSetName);
Loading history...
148
        assert($set instanceof ResourceSet, get_class($set));
149
        $type       = $set->getResourceType();
150
        $properties = $this->getDeserialiser()->bulkDeserialise($type, $content);
151
        $properties = (object)$properties;
152
153
        if ($isCreate) {
154
            $result = $this->getWrapper()->createResourceforResourceSet($set, null, $properties);
155
            assert(isset($result), get_class($result));
156
            $key     = $this->generateKeyDescriptor($type, $result);
157
            $keyProp = $key->getODataProperties();
158
            foreach ($keyProp as $keyName => $payload) {
159
                $content->propertyContent->properties[$keyName] = $payload;
160
            }
161
        } else {
162
            $key = $this->generateKeyDescriptor($type, $content->propertyContent, $content->id);
163
            assert($key instanceof KeyDescriptor, get_class($key));
164
            $source = $this->getWrapper()->getResourceFromResourceSet($set, $key);
165
            assert(isset($source), get_class($source));
166
            $result = $this->getWrapper()->updateResource($set, $source, $key, $properties);
167
        }
168
        if (!$key instanceof KeyDescriptor) {
169
            throw new InvalidOperationException(get_class($key));
170
        }
171
        $content->id = $key;
0 ignored issues
show
Documentation Bug introduced by
It seems like $key of type POData\UriProcessor\Reso...entParser\KeyDescriptor is incompatible with the declared type null|string of property $id.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
172
173
        $numLinks = count($content->links);
174
        for ($i = 0; $i < $numLinks; $i++) {
175
            $this->processLink($content->links[$i], $set, $result);
176
        }
177
178
        return [$set, $result];
179
    }
180
181
    /**
182
     * @return ModelDeserialiser
183
     */
184
    protected function getDeserialiser()
185
    {
186
        return $this->cereal;
187
    }
188
189
    /**
190
     * @return ProvidersWrapper
191
     */
192
    protected function getWrapper()
193
    {
194
        return $this->wrapper;
195
    }
196
197
    /**
198
     * @param  ResourceEntityType          $type
199
     * @param  ODataPropertyContent|object $result
200
     * @param  string|null                 $id
201
     * @throws ReflectionException
202
     * @throws ODataException
203
     * @return null|KeyDescriptor
204
     */
205
    protected function generateKeyDescriptor(ResourceEntityType $type, $result, $id = null)
206
    {
207
        $isOData = $result instanceof ODataPropertyContent;
208
        $keyProp = $type->getKeyProperties();
209
        if (null === $id) {
210
            $keyPredicate = '';
211
            foreach ($keyProp as $prop) {
212
                $iType = $prop->getInstanceType();
213
                assert($iType instanceof IType, get_class($iType));
214
                $keyName = $prop->getName();
215
                $rawKey  = $isOData ? $result->properties[$keyName]->value : $result->{$keyName};
216
                $keyVal  = $iType->convertToOData(strval($rawKey));
217
                assert(isset($keyVal), 'Key property ' . $keyName . ' must not be null');
218
                $keyPredicate .= $keyName . '=' . $keyVal . ', ';
219
            }
220
            $keyPredicate[strlen($keyPredicate) - 2] = ' ';
221
        } else {
222
            $idBits       = explode('/', $id);
223
            $keyRaw       = $idBits[count($idBits) - 1];
224
            $rawBits      = explode('(', $keyRaw, 2);
225
            $rawBits      = explode(')', $rawBits[count($rawBits) - 1]);
226
            $keyPredicate = $rawBits[0];
227
        }
228
        $keyPredicate = trim($keyPredicate);
229
        /** @var KeyDescriptor|null $keyDesc */
230
        $keyDesc  = null;
231
        $isParsed = KeyDescriptor::tryParseKeysFromKeyPredicate($keyPredicate, $keyDesc);
232
        assert(true === $isParsed, 'Key descriptor not successfully parsed');
233
        $keyDesc->validate($keyPredicate, $type);
234
        // this is deliberate - ODataEntry/Feed has the structure we need for processing, and we're inserting
235
        // keyDescriptor objects in id fields to indicate the given record has been processed
236
        return $keyDesc;
237
    }
238
239
    /**
240
     * @param ODataLink   $link
241
     * @param ResourceSet $sourceSet
242
     * @param $source
243
     * @throws InvalidOperationException
244
     * @throws ODataException
245
     * @throws ReflectionException
246
     */
247
    protected function processLink(ODataLink &$link, ResourceSet $sourceSet, $source)
248
    {
249
        $hasUrl     = null !== $link->getUrl();
250
        $result     = $link->getExpandedResult() ? $link->getExpandedResult()->getData() : null;
251
        $hasPayload = isset($result);
252
        assert(
253
            null == $result || $result instanceof ODataEntry || $result instanceof ODataFeed,
254
            (null === $result ? 'null' : get_class($result))
255
        );
256
        $isFeed = ($link->getExpandedResult() ? $link->getExpandedResult()->getData() : null) instanceof ODataFeed;
257
258
        // if nothing to hook up, bail out now
259
        if (!$hasUrl && !$hasPayload) {
0 ignored issues
show
introduced by
The condition $hasUrl is always true.
Loading history...
260
            return;
261
        }
262
263
        if ($isFeed) {
264
            $this->processLinkFeed($link, $sourceSet, $source, $hasUrl, $hasPayload);
265
        } else {
266
            $this->processLinkSingleton($link, $sourceSet, $source, $hasUrl, $hasPayload);
267
        }
268
        return;
269
    }
270
271
    /**
272
     * @param ODataLink   $link
273
     * @param ResourceSet $sourceSet
274
     * @param $source
275
     * @param  bool                      $hasUrl
276
     * @param  bool                      $hasPayload
277
     * @throws InvalidOperationException
278
     * @throws ODataException
279
     * @throws ReflectionException
280
     * @throws Exception
281
     */
282
    protected function processLinkFeed(ODataLink &$link, ResourceSet $sourceSet, $source, $hasUrl, $hasPayload)
283
    {
284
        assert(
285
            $link->getExpandedResult()->getData() instanceof ODataFeed,
286
            get_class($link->getExpandedResult()->getData())
287
        );
288
        $propName = $link->getTitle();
289
290
        // if entries is empty, bail out - nothing to do
291
        $numEntries = count($link->getExpandedResult()->getFeed()->entries);
292
        if (0 === $numEntries) {
293
            return;
294
        }
295
        // check that each entry is of consistent resource set after checking it hasn't been processed
296
        $first = $link->getExpandedResult()->getFeed()->entries[0]->resourceSetName;
297
        if ($link->getExpandedResult()->getFeed()->entries[0]->id instanceof KeyDescriptor) {
298
            return;
299
        }
300
        for ($i = 1; $i < $numEntries; $i++) {
301
            if ($first !== $link->getExpandedResult()->getFeed()->entries[$i]->resourceSetName) {
302
                $msg = 'All entries in given feed must have same resource set';
303
                throw new InvalidArgumentException($msg);
304
            }
305
        }
306
307
        $targSet = $this->getMetaProvider()->resolveResourceSet($first);
0 ignored issues
show
Bug introduced by
It seems like $first can also be of type null; however, parameter $name of POData\Providers\Metadat...r::resolveResourceSet() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

307
        $targSet = $this->getMetaProvider()->resolveResourceSet(/** @scrutinizer ignore-type */ $first);
Loading history...
308
        assert($targSet instanceof ResourceSet);
309
        $targType = $targSet->getResourceType();
310
        assert($targType instanceof ResourceEntityType);
311
        $instanceType = $targType->getInstanceType();
312
        assert($instanceType instanceof ReflectionClass);
313
        $targObj = $instanceType->newInstanceArgs();
314
315
        // assemble payload
316
        $data = [];
317
        $keys = [];
318
        for ($i = 0; $i < $numEntries; $i++) {
319
            $data[] = $this->getDeserialiser()->bulkDeserialise(
320
                $targType,
321
                $link->getExpandedResult()->getFeed()->entries[$i]
322
            );
323
            $keys[] = $hasUrl ? $this->generateKeyDescriptor(
324
                $targType,
325
                $link->getExpandedResult()->getFeed()->entries[$i]->propertyContent
326
            ) : null;
327
        }
328
329
        // creation
330
        if (!$hasUrl && $hasPayload) {
331
            $bulkResult = $this->getWrapper()->createBulkResourceforResourceSet($targSet, $data);
332
            assert(is_array($bulkResult));
333
            for ($i = 0; $i < $numEntries; $i++) {
334
                $targEntityInstance = $bulkResult[$i];
335
                $this->getWrapper()->hookSingleModel($sourceSet, $source, $targSet, $targEntityInstance, $propName);
336
                $key                                   = $this->generateKeyDescriptor($targType, $targEntityInstance);
337
                $link->getExpandedResult()->getFeed()->entries[$i]->id = $key;
0 ignored issues
show
Documentation Bug introduced by
It seems like $key can also be of type POData\UriProcessor\Reso...entParser\KeyDescriptor. However, the property $id is declared as type null|string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
338
            }
339
        }
340
        // update
341
        if ($hasUrl && $hasPayload) {
342
            $bulkResult = $this->getWrapper()->updateBulkResource($targSet, $targObj, $keys, $data);
343
            for ($i = 0; $i < $numEntries; $i++) {
344
                $targEntityInstance = $bulkResult[$i];
345
                $this->getWrapper()->hookSingleModel($sourceSet, $source, $targSet, $targEntityInstance, $propName);
346
                $link->getExpandedResult()->getFeed()->entries[$i]->id = $keys[$i];
347
            }
348
        }
349
        assert(isset($bulkResult) && is_array($bulkResult));
350
351
        for ($i = 0; $i < $numEntries; $i++) {
352
            assert($link->getExpandedResult()->getFeed()->entries[$i]->id instanceof KeyDescriptor);
353
            $numLinks = count($link->getExpandedResult()->getFeed()->entries[$i]->links);
354
            for ($j = 0; $j < $numLinks; $j++) {
355
                $this->processLink($link->getExpandedResult()->getFeed()->entries[$i]->links[$j], $targSet, $bulkResult[$i]);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $bulkResult does not seem to be defined for all execution paths leading up to this point.
Loading history...
356
            }
357
        }
358
359
        return;
360
    }
361
362
    /**
363
     * @param ODataLink   $link
364
     * @param ResourceSet $sourceSet
365
     * @param $source
366
     * @param $hasUrl
367
     * @param $hasPayload
368
     * @throws InvalidOperationException
369
     * @throws ODataException
370
     * @throws ReflectionException
371
     */
372
    protected function processLinkSingleton(ODataLink &$link, ResourceSet $sourceSet, $source, $hasUrl, $hasPayload)
373
    {
374
        /** @var ODataEntry|null $result */
375
        $result = $link->getExpandedResult() ? $link->getExpandedResult()->getEntry() : null;
376
        assert(
377
            null === $result || $result instanceof ODataEntry,
378
            (null === $result ? 'null' : get_class($result))
379
        );
380
381
        if ($hasUrl) {
382
            $urlBitz      = explode('/', $link->getUrl());
383
            $rawPredicate = $urlBitz[count($urlBitz) - 1];
384
            $rawPredicate = explode('(', $rawPredicate);
385
            $setName      = $rawPredicate[0];
386
            $rawPredicate = trim($rawPredicate[count($rawPredicate) - 1], ')');
387
            $targSet      = $this->getMetaProvider()->resolveResourceSet($setName);
388
            assert(null !== $targSet, get_class($targSet));
389
            $type = $targSet->getResourceType();
390
        } else {
391
            $type = $this->getMetaProvider()->resolveResourceType($result->type->getTerm());
392
        }
393
394
        // if link result has already been processed, bail out
395
        if (null !== $result || null !== $link->getUrl()) {
0 ignored issues
show
introduced by
The condition null !== $link->getUrl() is always true.
Loading history...
396
            $isUrlKey = $link->getUrl() instanceof KeyDescriptor;
397
            $isIdKey  = $result instanceof ODataEntry &&
398
                $result->id instanceof KeyDescriptor;
399
            if ($isUrlKey || $isIdKey) {
400
                if ($isIdKey) {
401
                }
402
                return;
403
            }
404
        }
405
        assert(null === $result || !$result->id instanceof KeyDescriptor);
406
        assert(null === $link->getUrl() || is_string($link->getUrl()));
407
408
        assert($type instanceof ResourceEntityType, get_class($type));
409
        $propName = $link->getTitle();
410
411
        /** @var KeyDescriptor|null $keyDesc */
412
        $keyDesc = null;
413
414
        if ($hasUrl) {
415
            assert(isset($rawPredicate));
416
            KeyDescriptor::tryParseKeysFromKeyPredicate($rawPredicate, $keyDesc);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $rawPredicate does not seem to be defined for all execution paths leading up to this point.
Loading history...
417
            $keyDesc->validate($rawPredicate, $type);
418
            assert(null !== $keyDesc, 'Key description must not be null');
419
        }
420
421
        // hooking up to existing resource
422
        if ($hasUrl && !$hasPayload) {
423
            assert(isset($targSet));
424
            assert(isset($keyDesc));
425
            $target = $this->getWrapper()->getResourceFromResourceSet($targSet, $keyDesc);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $targSet does not seem to be defined for all execution paths leading up to this point.
Loading history...
426
            assert(isset($target));
427
            $this->getWrapper()->hookSingleModel($sourceSet, $source, $targSet, $target, $propName);
428
            return;
429
        }
430
        // creating new resource
431
        if (!$hasUrl && $hasPayload) {
432
            list($targSet, $target) = $this->processEntryContent($result);
433
            assert(isset($target));
434
            $key        = $this->generateKeyDescriptor($type, $result->propertyContent);
435
            $link->setUrl($key->generateRelativeUri($targSet));
436
            $result->id = $key;
0 ignored issues
show
Documentation Bug introduced by
It seems like $key can also be of type POData\UriProcessor\Reso...entParser\KeyDescriptor. However, the property $id is declared as type null|string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
437
            $this->getWrapper()->hookSingleModel($sourceSet, $source, $targSet, $target, $propName);
438
            return;
439
        }
440
        // updating existing resource and connecting to it
441
        list($targSet, $target) = $this->processEntryContent($result);
442
        assert(isset($target));
443
        $result->id = $keyDesc;
444
        $this->getWrapper()->hookSingleModel($sourceSet, $source, $targSet, $target, $propName);
445
        return;
446
    }
447
448
    /**
449
     * @param  ODataEntry $payload
450
     * @param  int        $depth
451
     * @return bool
452
     */
453
    protected function isEntryProcessed(ODataEntry $payload, $depth = 0)
454
    {
455
        assert(is_int($depth) && 0 <= $depth && 100 >= $depth, 'Maximum recursion depth exceeded');
456
        if (!$payload->id instanceof KeyDescriptor) {
457
            return false;
458
        }
459
        foreach ($payload->links as $link) {
460
            $expand = $link->getExpandedResult() ? $link->getExpandedResult()->getData() : null;
461
            if (null === $expand) {
462
                continue;
463
            }
464
            if ($expand instanceof ODataEntry) {
465
                if (!$this->isEntryProcessed($expand, $depth + 1)) {
466
                    return false;
467
                } else {
468
                    continue;
469
                }
470
            }
471
            if ($expand instanceof ODataFeed) {
472
                foreach ($expand->entries as $entry) {
473
                    if (!$this->isEntryProcessed($entry, $depth + 1)) {
474
                        return false;
475
                    }
476
                }
477
                continue;
478
            }
479
            assert(false, 'Expanded result cannot be processed');
480
        }
481
482
        return true;
483
    }
484
}
485