Passed
Push — master ( 6b2979...464731 )
by Alex
04:00 queued 11s
created

CynicDeserialiser::isEntryProcessed()   C

Complexity

Conditions 12
Paths 16

Size

Total Lines 30
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
eloc 19
nc 16
nop 2
dl 0
loc 30
rs 6.9666
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) {
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
            /** @var ODataLink $link */
91
            $hasExpanded = null !== $link->getExpandedResult();
92
93
            $isEntry = ($link->getExpandedResult() ? $link->getExpandedResult()->getData() : null)
94
                       instanceof ODataEntry;
95
96
            if ($hasExpanded) {
97
                if ($isEntry) {
98
                    $this->isEntryOK($link->getExpandedResult()->getEntry());
99
                } else {
100
                    foreach ($link->getExpandedResult()->getFeed()->getEntries() as $expanded) {
101
                        $this->isEntryOK($expanded);
102
                    }
103
                }
104
            }
105
        }
106
107
        $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

107
        $set = $this->getMetaProvider()->resolveResourceSet(/** @scrutinizer ignore-type */ $payload->resourceSetName);
Loading history...
108
        if (null === $set) {
109
            $msg = 'Specified resource set could not be resolved';
110
            throw new InvalidArgumentException($msg);
111
        }
112
        return true;
113
    }
114
115
    /**
116
     * @return IMetadataProvider
117
     */
118
    protected function getMetaProvider()
119
    {
120
        return $this->metaProvider;
121
    }
122
123
    /**
124
     * @param  ODataEntry                $content
125
     * @throws ODataException
126
     * @throws ReflectionException
127
     * @throws Exception
128
     * @throws InvalidOperationException
129
     * @return array
130
     */
131
    protected function processEntryContent(ODataEntry &$content)
132
    {
133
        assert(null === $content->id || is_string($content->id), 'Entry id must be null or string');
134
135
        $isCreate = null === $content->id || empty($content->id);
136
        $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

136
        $set      = $this->getMetaProvider()->resolveResourceSet(/** @scrutinizer ignore-type */ $content->resourceSetName);
Loading history...
137
        assert($set instanceof ResourceSet, get_class($set));
138
        $type       = $set->getResourceType();
139
        $properties = $this->getDeserialiser()->bulkDeserialise($type, $content);
140
        $properties = (object)$properties;
141
142
        if ($isCreate) {
143
            $result = $this->getWrapper()->createResourceforResourceSet($set, null, $properties);
144
            assert(isset($result), get_class($result));
145
            $key     = $this->generateKeyDescriptor($type, $result);
146
            $keyProp = $key->getODataProperties();
147
            foreach ($keyProp as $keyName => $payload) {
148
                $content->propertyContent[$keyName] = $payload;
149
            }
150
        } else {
151
            $key = $this->generateKeyDescriptor($type, $content->propertyContent, $content->id);
152
            assert($key instanceof KeyDescriptor, get_class($key));
153
            $source = $this->getWrapper()->getResourceFromResourceSet($set, $key);
154
            assert(isset($source), get_class($source));
155
            $result = $this->getWrapper()->updateResource($set, $source, $key, $properties);
156
        }
157
        if (!$key instanceof KeyDescriptor) {
158
            throw new InvalidOperationException(get_class($key));
159
        }
160
        $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...
161
162
        $numLinks = count($content->links);
163
        for ($i = 0; $i < $numLinks; $i++) {
164
            $this->processLink($content->links[$i], $set, $result);
165
        }
166
167
        return [$set, $result];
168
    }
169
170
    /**
171
     * @return ModelDeserialiser
172
     */
173
    protected function getDeserialiser()
174
    {
175
        return $this->cereal;
176
    }
177
178
    /**
179
     * @return ProvidersWrapper
180
     */
181
    protected function getWrapper()
182
    {
183
        return $this->wrapper;
184
    }
185
186
    /**
187
     * @param  ResourceEntityType          $type
188
     * @param  ODataPropertyContent|object $result
189
     * @param  string|null                 $id
190
     * @throws ReflectionException
191
     * @throws ODataException
192
     * @return null|KeyDescriptor
193
     */
194
    protected function generateKeyDescriptor(ResourceEntityType $type, $result, $id = null)
195
    {
196
        $isOData = $result instanceof ODataPropertyContent;
197
        $keyProp = $type->getKeyProperties();
198
        if (null === $id) {
199
            $keyPredicate = '';
200
            foreach ($keyProp as $prop) {
201
                $iType = $prop->getInstanceType();
202
                assert($iType instanceof IType, get_class($iType));
203
                $keyName = $prop->getName();
204
                $rawKey  = $isOData ? $result[$keyName]->getValue() : $result->{$keyName};
205
                $keyVal  = $iType->convertToOData(strval($rawKey));
206
                assert(isset($keyVal), 'Key property ' . $keyName . ' must not be null');
207
                $keyPredicate .= $keyName . '=' . $keyVal . ', ';
208
            }
209
            $keyPredicate[strlen($keyPredicate) - 2] = ' ';
210
        } else {
211
            $idBits       = explode('/', $id);
212
            $keyRaw       = $idBits[count($idBits) - 1];
213
            $rawBits      = explode('(', $keyRaw, 2);
214
            $rawBits      = explode(')', $rawBits[count($rawBits) - 1]);
215
            $keyPredicate = $rawBits[0];
216
        }
217
        $keyPredicate = trim($keyPredicate);
218
        /** @var KeyDescriptor|null $keyDesc */
219
        $keyDesc  = null;
220
        $isParsed = KeyDescriptor::tryParseKeysFromKeyPredicate($keyPredicate, $keyDesc);
221
        assert(true === $isParsed, 'Key descriptor not successfully parsed');
222
        $keyDesc->validate($keyPredicate, $type);
223
        // this is deliberate - ODataEntry/Feed has the structure we need for processing, and we're inserting
224
        // keyDescriptor objects in id fields to indicate the given record has been processed
225
        return $keyDesc;
226
    }
227
228
    /**
229
     * @param ODataLink   $link
230
     * @param ResourceSet $sourceSet
231
     * @param $source
232
     * @throws InvalidOperationException
233
     * @throws ODataException
234
     * @throws ReflectionException
235
     */
236
    protected function processLink(ODataLink &$link, ResourceSet $sourceSet, $source)
237
    {
238
        $hasUrl     = null !== $link->getUrl();
239
        $result     = $link->getExpandedResult() ? $link->getExpandedResult()->getData() : null;
240
        $hasPayload = isset($result);
241
        assert(
242
            null == $result || $result instanceof ODataEntry || $result instanceof ODataFeed,
243
            (null === $result ? 'null' : get_class($result))
244
        );
245
        $isFeed = ($link->getExpandedResult() ? $link->getExpandedResult()->getData() : null) instanceof ODataFeed;
246
247
        // if nothing to hook up, bail out now
248
        if (!$hasUrl && !$hasPayload) {
249
            return;
250
        }
251
252
        if ($isFeed) {
253
            $this->processLinkFeed($link, $sourceSet, $source, $hasUrl, $hasPayload);
254
        } else {
255
            $this->processLinkSingleton($link, $sourceSet, $source, $hasUrl, $hasPayload);
256
        }
257
        return;
258
    }
259
260
    /**
261
     * @param ODataLink   $link
262
     * @param ResourceSet $sourceSet
263
     * @param $source
264
     * @param  bool                      $hasUrl
265
     * @param  bool                      $hasPayload
266
     * @throws InvalidOperationException
267
     * @throws ODataException
268
     * @throws ReflectionException
269
     * @throws Exception
270
     */
271
    protected function processLinkFeed(ODataLink &$link, ResourceSet $sourceSet, $source, $hasUrl, $hasPayload)
272
    {
273
        assert(
274
            $link->getExpandedResult()->getData() instanceof ODataFeed,
275
            get_class($link->getExpandedResult()->getData())
276
        );
277
        $propName = $link->getTitle();
278
279
        // if entries is empty, bail out - nothing to do
280
        $numEntries = count($link->getExpandedResult()->getFeed()->getEntries());
281
        if (0 === $numEntries) {
282
            return;
283
        }
284
        // check that each entry is of consistent resource set after checking it hasn't been processed
285
        $first = $link->getExpandedResult()->getFeed()->getEntries()[0]->resourceSetName;
286
        if ($link->getExpandedResult()->getFeed()->getEntries()[0]->id instanceof KeyDescriptor) {
287
            return;
288
        }
289
        for ($i = 1; $i < $numEntries; $i++) {
290
            if ($first !== $link->getExpandedResult()->getFeed()->getEntries()[$i]->resourceSetName) {
291
                $msg = 'All entries in given feed must have same resource set';
292
                throw new InvalidArgumentException($msg);
293
            }
294
        }
295
296
        $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

296
        $targSet = $this->getMetaProvider()->resolveResourceSet(/** @scrutinizer ignore-type */ $first);
Loading history...
297
        assert($targSet instanceof ResourceSet);
298
        $targType = $targSet->getResourceType();
299
        assert($targType instanceof ResourceEntityType);
300
        $instanceType = $targType->getInstanceType();
301
        assert($instanceType instanceof ReflectionClass);
302
        $targObj = $instanceType->newInstanceArgs();
303
304
        // assemble payload
305
        $data = [];
306
        $keys = [];
307
        for ($i = 0; $i < $numEntries; $i++) {
308
            $data[] = $this->getDeserialiser()->bulkDeserialise(
309
                $targType,
310
                $link->getExpandedResult()->getFeed()->getEntries()[$i]
311
            );
312
            $keys[] = $hasUrl ? $this->generateKeyDescriptor(
313
                $targType,
314
                $link->getExpandedResult()->getFeed()->getEntries()[$i]->propertyContent
315
            ) : null;
316
        }
317
318
        // creation
319
        if (!$hasUrl && $hasPayload) {
320
            $bulkResult = $this->getWrapper()->createBulkResourceforResourceSet($targSet, $data);
321
            assert(is_array($bulkResult));
322
            for ($i = 0; $i < $numEntries; $i++) {
323
                $targEntityInstance = $bulkResult[$i];
324
                $this->getWrapper()->hookSingleModel($sourceSet, $source, $targSet, $targEntityInstance, $propName);
325
                $key                                                        = $this->generateKeyDescriptor($targType, $targEntityInstance);
326
                $link->getExpandedResult()->getFeed()->getEntries()[$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...
327
            }
328
        }
329
        // update
330
        if ($hasUrl && $hasPayload) {
331
            $bulkResult = $this->getWrapper()->updateBulkResource($targSet, $targObj, $keys, $data);
332
            for ($i = 0; $i < $numEntries; $i++) {
333
                $targEntityInstance = $bulkResult[$i];
334
                $this->getWrapper()->hookSingleModel($sourceSet, $source, $targSet, $targEntityInstance, $propName);
335
                $link->getExpandedResult()->getFeed()->getEntries()[$i]->id = $keys[$i];
336
            }
337
        }
338
        assert(isset($bulkResult) && is_array($bulkResult));
339
340
        for ($i = 0; $i < $numEntries; $i++) {
341
            assert($link->getExpandedResult()->getFeed()->getEntries()[$i]->id instanceof KeyDescriptor);
342
            $numLinks = count($link->getExpandedResult()->getFeed()->getEntries()[$i]->links);
343
            for ($j = 0; $j < $numLinks; $j++) {
344
                $this->processLink($link->getExpandedResult()->getFeed()->getEntries()[$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...
345
            }
346
        }
347
348
        return;
349
    }
350
351
    /**
352
     * @param ODataLink   $link
353
     * @param ResourceSet $sourceSet
354
     * @param $source
355
     * @param $hasUrl
356
     * @param $hasPayload
357
     * @throws InvalidOperationException
358
     * @throws ODataException
359
     * @throws ReflectionException
360
     */
361
    protected function processLinkSingleton(ODataLink &$link, ResourceSet $sourceSet, $source, $hasUrl, $hasPayload)
362
    {
363
        /** @var ODataEntry|null $result */
364
        $result = $link->getExpandedResult() ? $link->getExpandedResult()->getEntry() : null;
365
        assert(
366
            null === $result || $result instanceof ODataEntry,
367
            (null === $result ? 'null' : get_class($result))
368
        );
369
370
        if ($hasUrl) {
371
            $urlBitz      = explode('/', $link->getUrl());
372
            $rawPredicate = $urlBitz[count($urlBitz) - 1];
373
            $rawPredicate = explode('(', $rawPredicate);
374
            $setName      = $rawPredicate[0];
375
            $rawPredicate = trim($rawPredicate[count($rawPredicate) - 1], ')');
376
            $targSet      = $this->getMetaProvider()->resolveResourceSet($setName);
377
            assert(null !== $targSet, get_class($targSet));
378
            $type = $targSet->getResourceType();
379
        } else {
380
            $type = $this->getMetaProvider()->resolveResourceType($result->type->getTerm());
381
        }
382
383
        // if link result has already been processed, bail out
384
        if (null !== $result || null !== $link->getUrl()) {
385
            $isUrlKey = $link->getUrl() instanceof KeyDescriptor;
386
            $isIdKey  = $result instanceof ODataEntry &&
387
                $result->id instanceof KeyDescriptor;
388
            if ($isUrlKey || $isIdKey) {
389
                if ($isIdKey) {
390
                }
391
                return;
392
            }
393
        }
394
        assert(null === $result || !$result->id instanceof KeyDescriptor);
395
        assert(null === $link->getUrl() || is_string($link->getUrl()));
396
397
        assert($type instanceof ResourceEntityType, get_class($type));
398
        $propName = $link->getTitle();
399
400
        /** @var KeyDescriptor|null $keyDesc */
401
        $keyDesc = null;
402
403
        if ($hasUrl) {
404
            assert(isset($rawPredicate));
405
            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...
406
            $keyDesc->validate($rawPredicate, $type);
407
            assert(null !== $keyDesc, 'Key description must not be null');
408
        }
409
410
        // hooking up to existing resource
411
        if ($hasUrl && !$hasPayload) {
412
            assert(isset($targSet));
413
            assert(isset($keyDesc));
414
            $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...
415
            assert(isset($target));
416
            $this->getWrapper()->hookSingleModel($sourceSet, $source, $targSet, $target, $propName);
417
            return;
418
        }
419
        // creating new resource
420
        if (!$hasUrl && $hasPayload) {
421
            list($targSet, $target) = $this->processEntryContent($result);
422
            assert(isset($target));
423
            $key        = $this->generateKeyDescriptor($type, $result->propertyContent);
424
            $link->setUrl($key->generateRelativeUri($targSet));
425
            $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...
426
            $this->getWrapper()->hookSingleModel($sourceSet, $source, $targSet, $target, $propName);
427
            return;
428
        }
429
        // updating existing resource and connecting to it
430
        list($targSet, $target) = $this->processEntryContent($result);
431
        assert(isset($target));
432
        $result->id = $keyDesc;
433
        $this->getWrapper()->hookSingleModel($sourceSet, $source, $targSet, $target, $propName);
434
        return;
435
    }
436
437
    /**
438
     * @param  ODataEntry $payload
439
     * @param  int        $depth
440
     * @return bool
441
     */
442
    protected function isEntryProcessed(ODataEntry $payload, $depth = 0)
443
    {
444
        assert(is_int($depth) && 0 <= $depth && 100 >= $depth, 'Maximum recursion depth exceeded');
445
        if (!$payload->id instanceof KeyDescriptor) {
446
            return false;
447
        }
448
        foreach ($payload->links as $link) {
449
            $expand = $link->getExpandedResult() ? $link->getExpandedResult()->getData() : null;
450
            if (null === $expand) {
451
                continue;
452
            }
453
            if ($expand instanceof ODataEntry) {
454
                if (!$this->isEntryProcessed($expand, $depth + 1)) {
455
                    return false;
456
                } else {
457
                    continue;
458
                }
459
            }
460
            if ($expand instanceof ODataFeed) {
461
                foreach ($expand->getEntries() as $entry) {
462
                    if (!$this->isEntryProcessed($entry, $depth + 1)) {
463
                        return false;
464
                    }
465
                }
466
                continue;
467
            }
468
            assert(false, 'Expanded result cannot be processed');
469
        }
470
471
        return true;
472
    }
473
}
474