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

CynicDeserialiser   F

Complexity

Total Complexity 84

Size/Duplication

Total Lines 452
Duplicated Lines 0 %

Importance

Changes 5
Bugs 1 Features 0
Metric Value
eloc 230
c 5
b 1
f 0
dl 0
loc 452
rs 2
wmc 84

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A processPayload() 0 18 5
A getDeserialiser() 0 3 1
D processLinkSingleton() 0 74 18
A generateKeyDescriptor() 0 32 4
B processLink() 0 22 9
B processEntryContent() 0 37 7
C processLinkFeed() 0 78 16
A getMetaProvider() 0 3 1
C isEntryProcessed() 0 30 12
B isEntryOK() 0 31 9
A getWrapper() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like CynicDeserialiser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use CynicDeserialiser, and based on these observations, apply Extract Interface, too.

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
            $hasUrl      = null !== $link->getUrl();
91
            $hasExpanded = null !== $link->getExpandedResult();
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
            $isEntry = ($link->getExpandedResult() ? $link->getExpandedResult()->getData() : null) instanceof ODataEntry;
99
100
            if ($hasExpanded) {
101
                if ($isEntry) {
102
                    $this->isEntryOK($link->getExpandedResult()->getEntry());
103
                } else {
104
                    foreach ($link->getExpandedResult()->getFeed()->entries as $expanded) {
105
                        $this->isEntryOK($expanded);
106
                    }
107
                }
108
            }
109
        }
110
111
        $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

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

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

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