Completed
Push — master ( c66249...0aee22 )
by Alex
14s queued 12s
created

CynicDeserialiser::processLink()   B

Complexity

Conditions 7
Paths 3

Size

Total Lines 22
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 7
eloc 14
c 4
b 0
f 0
nc 3
nop 3
dl 0
loc 22
rs 8.8333
1
<?php
2
3
namespace POData\ObjectModel;
4
5
use POData\Common\InvalidOperationException;
6
use POData\Providers\Metadata\IMetadataProvider;
7
use POData\Providers\Metadata\ResourceEntityType;
8
use POData\Providers\Metadata\ResourceSet;
9
use POData\Providers\Metadata\Type\IType;
10
use POData\Providers\ProvidersWrapper;
11
use POData\Providers\Query\IQueryProvider;
12
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\KeyDescriptor;
13
14
/**
15
 * Class CynicDeserialiser
16
 * @package POData\ObjectModel
17
 */
18
class CynicDeserialiser
19
{
20
    /**
21
     * @var IMetadataProvider
22
     */
23
    private $metaProvider;
24
25
    /**
26
     * @var ProvidersWrapper
27
     */
28
    private $wrapper;
29
30
    /**
31
     * @var ModelDeserialiser
32
     */
33
    private $cereal;
34
35
    /**
36
     * CynicDeserialiser constructor.
37
     * @param IMetadataProvider $meta
38
     * @param ProvidersWrapper $wrapper
39
     */
40
    public function __construct(IMetadataProvider $meta, ProvidersWrapper $wrapper)
41
    {
42
        $this->metaProvider = $meta;
43
        $this->wrapper = $wrapper;
44
        $this->cereal = new ModelDeserialiser();
45
    }
46
47
    /**
48
     * @param ODataEntry $payload
49
     * @return mixed
50
     * @throws InvalidOperationException
51
     * @throws \POData\Common\ODataException
52
     * @throws \ReflectionException
53
     */
54
    public function processPayload(ODataEntry &$payload)
55
    {
56
        $entryOk = $this->isEntryOK($payload);
57
        if (!$entryOk) {
58
            throw new InvalidOperationException('Payload not OK');
59
        }
60
        list($sourceSet, $source) = $this->processEntryContent($payload);
61
        if (!$sourceSet instanceof ResourceSet) {
0 ignored issues
show
introduced by
$sourceSet is always a sub-type of POData\Providers\Metadata\ResourceSet.
Loading history...
62
            throw new InvalidOperationException('$sourceSet not instanceof ResourceSet');
63
        }
64
        $numLinks = count($payload->links);
65
        for ($i = 0; $i < $numLinks; $i++) {
66
            $this->processLink($payload->links[$i], $sourceSet, $source);
67
        }
68
        if (!$this->isEntryProcessed($payload)) {
69
            throw new InvalidOperationException('Payload not processed');
70
        }
71
        return $source;
72
    }
73
74
    /**
75
     * Check if supplied ODataEntry is well-formed.
76
     *
77
     * @param ODataEntry $payload
78
     * @return bool
79
     */
80
    protected function isEntryOK(ODataEntry $payload)
81
    {
82
        // check links
83
        foreach ($payload->links as $link) {
84
            $hasUrl = isset($link->url);
85
            $hasExpanded = isset($link->expandedResult);
86
            if ($hasUrl) {
87
                if (!is_string($link->url)) {
88
                    $msg = 'Url must be either string or null';
89
                    throw new \InvalidArgumentException($msg);
90
                }
91
            }
92
            if ($hasExpanded) {
93
                $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...
94
                if (!$isGood) {
95
                    $msg = 'Expanded result must null, or be instance of ODataEntry or ODataFeed';
96
                    throw new \InvalidArgumentException($msg);
97
                }
98
            }
99
            $isEntry = $link->expandedResult instanceof ODataEntry;
100
101
            if ($hasExpanded) {
102
                if ($isEntry) {
103
                    $this->isEntryOK($link->expandedResult);
104
                } else {
105
                    foreach ($link->expandedResult->entries as $expanded) {
106
                        $this->isEntryOK($expanded);
107
                    }
108
                }
109
            }
110
        }
111
112
        $set = $this->getMetaProvider()->resolveResourceSet($payload->resourceSetName);
113
        if (null === $set) {
114
            $msg = 'Specified resource set could not be resolved';
115
            throw new \InvalidArgumentException($msg);
116
        }
117
        return true;
118
    }
119
120
    /**
121
     * @param ODataEntry $payload
122
     * @param int $depth
123
     * @return bool
124
     */
125
    protected function isEntryProcessed(ODataEntry $payload, $depth = 0)
126
    {
127
        assert(is_int($depth) && 0 <= $depth && 100 >= $depth, 'Maximum recursion depth exceeded');
128
        if (!$payload->id instanceof KeyDescriptor) {
0 ignored issues
show
introduced by
$payload->id is never a sub-type of POData\UriProcessor\Reso...entParser\KeyDescriptor.
Loading history...
129
            return false;
130
        }
131
        foreach ($payload->links as $link) {
132
            $expand = $link->expandedResult;
133
            if (null === $expand) {
134
                continue;
135
            }
136
            if ($expand instanceof ODataEntry) {
137
                if (!$this->isEntryProcessed($expand, $depth + 1)) {
138
                    return false;
139
                } else {
140
                    continue;
141
                }
142
            }
143
            if ($expand instanceof ODataFeed) {
144
                foreach ($expand->entries as $entry) {
145
                    if (!$this->isEntryProcessed($entry, $depth + 1)) {
146
                        return false;
147
                    }
148
                }
149
                continue;
150
            }
151
            assert(false, 'Expanded result cannot be processed');
152
        }
153
154
        return true;
155
    }
156
157
    /**
158
     * @param ODataEntry $content
159
     * @return array
160
     * @throws InvalidOperationException
161
     * @throws \POData\Common\ODataException
162
     * @throws \ReflectionException
163
     * @throws \Exception
164
     */
165
    protected function processEntryContent(ODataEntry &$content)
166
    {
167
        assert(null === $content->id || is_string($content->id), 'Entry id must be null or string');
168
169
        $isCreate = null === $content->id || empty($content->id);
170
        $set = $this->getMetaProvider()->resolveResourceSet($content->resourceSetName);
171
        assert($set instanceof ResourceSet, get_class($set));
172
        $type = $set->getResourceType();
173
        $properties = $this->getDeserialiser()->bulkDeserialise($type, $content);
174
        $properties = (object) $properties;
175
176
        if ($isCreate) {
177
            $result = $this->getWrapper()->createResourceforResourceSet($set, null, $properties);
178
            assert(isset($result), get_class($result));
179
            $key = $this->generateKeyDescriptor($type, $result);
180
            $keyProp = $key->getODataProperties();
181
            foreach ($keyProp as $keyName => $payload) {
182
                $content->propertyContent->properties[$keyName] = $payload;
183
            }
184
        } else {
185
            $key = $this->generateKeyDescriptor($type, $content->propertyContent, $content->id);
186
            assert($key instanceof KeyDescriptor, get_class($key));
187
            $source = $this->getWrapper()->getResourceFromResourceSet($set, $key);
188
            assert(isset($source), get_class($source));
189
            $result = $this->getWrapper()->updateResource($set, $source, $key, $properties);
190
        }
191
        if (!$key instanceof KeyDescriptor) {
192
            throw new InvalidOperationException(get_class($key));
193
        }
194
        $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 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...
195
196
        $numLinks = count($content->links);
197
        for ($i = 0; $i < $numLinks; $i++) {
198
            $this->processLink($content->links[$i], $set, $result);
199
        }
200
201
        return [$set, $result];
202
    }
203
204
    /**
205
     * @return IMetadataProvider
206
     */
207
    protected function getMetaProvider()
208
    {
209
        return $this->metaProvider;
210
    }
211
212
    /**
213
     * @return ProvidersWrapper
214
     */
215
    protected function getWrapper()
216
    {
217
        return $this->wrapper;
218
    }
219
220
    /**
221
     * @return ModelDeserialiser
222
     */
223
    protected function getDeserialiser()
224
    {
225
        return $this->cereal;
226
    }
227
228
    /**
229
     * @param  ResourceEntityType $type
230
     * @param  ODataPropertyContent|object $result
231
     * @param  string|null $id
232
     * @return null|KeyDescriptor
233
     * @throws \POData\Common\ODataException
234
     * @throws \ReflectionException
235
     */
236
    protected function generateKeyDescriptor(ResourceEntityType $type, $result, $id = null)
237
    {
238
        $isOData = $result instanceof ODataPropertyContent;
239
        $keyProp = $type->getKeyProperties();
240
        if (null === $id) {
241
            $keyPredicate = '';
242
            foreach ($keyProp as $prop) {
243
                $iType = $prop->getInstanceType();
244
                assert($iType instanceof IType, get_class($iType));
245
                $keyName = $prop->getName();
246
                $rawKey = $isOData ? $result->properties[$keyName]->value : $result->$keyName;
247
                $keyVal = $iType->convertToOData($rawKey);
248
                assert(isset($keyVal), 'Key property ' . $keyName . ' must not be null');
249
                $keyPredicate .= $keyName . '=' . $keyVal . ', ';
250
            }
251
            $keyPredicate[strlen($keyPredicate) - 2] = ' ';
252
        } else {
253
            $idBits = explode('/', $id);
254
            $keyRaw = $idBits[count($idBits)-1];
255
            $rawBits = explode('(', $keyRaw, 2);
256
            $rawBits = explode(')', $rawBits[count($rawBits)-1]);
257
            $keyPredicate = $rawBits[0];
258
        }
259
        $keyPredicate = trim($keyPredicate);
260
        /** @var KeyDescriptor|null $keyDesc */
261
        $keyDesc = null;
262
        $isParsed = KeyDescriptor::tryParseKeysFromKeyPredicate($keyPredicate, $keyDesc);
263
        assert(true === $isParsed, 'Key descriptor not successfully parsed');
264
        $keyDesc->validate($keyPredicate, $type);
265
        // this is deliberate - ODataEntry/Feed has the structure we need for processing, and we're inserting
266
        // keyDescriptor objects in id fields to indicate the given record has been processed
267
        return $keyDesc;
268
    }
269
270
    /**
271
     * @param ODataLink $link
272
     * @param ResourceSet $sourceSet
273
     * @param $source
274
     * @throws InvalidOperationException
275
     * @throws \POData\Common\ODataException
276
     * @throws \ReflectionException
277
     */
278
    protected function processLink(ODataLink &$link, ResourceSet $sourceSet, $source)
279
    {
280
        $hasUrl = isset($link->url);
281
        $result = $link->expandedResult;
282
        $hasPayload = isset($result);
283
        assert(
284
            null == $result || $result instanceof ODataEntry || $result instanceof ODataFeed,
285
            (null === $result ? 'null' : get_class($result))
286
        );
287
        $isFeed = $link->expandedResult instanceof ODataFeed;
288
289
        // if nothing to hook up, bail out now
290
        if (!$hasUrl && !$hasPayload) {
291
            return;
292
        }
293
294
        if ($isFeed) {
295
            $this->processLinkFeed($link, $sourceSet, $source, $hasUrl, $hasPayload);
296
        } else {
297
            $this->processLinkSingleton($link, $sourceSet, $source, $hasUrl, $hasPayload);
298
        }
299
        return;
300
    }
301
302
    /**
303
     * @param ODataLink $link
304
     * @param ResourceSet $sourceSet
305
     * @param $source
306
     * @param $hasUrl
307
     * @param $hasPayload
308
     * @throws InvalidOperationException
309
     * @throws \POData\Common\ODataException
310
     * @throws \ReflectionException
311
     */
312
    protected function processLinkSingleton(ODataLink &$link, ResourceSet $sourceSet, $source, $hasUrl, $hasPayload)
313
    {
314
        /** @var ODataEntry|null $result */
315
        $result = $link->expandedResult;
316
        assert(
317
            null === $result || $result instanceof ODataEntry,
318
            (null === $result ? 'null' : get_class($result))
319
        );
320
        // if link result has already been processed, bail out
321
        if (null !== $result || null !== $link->url) {
322
            $isUrlKey = $link->url instanceof KeyDescriptor;
323
            $isIdKey = $result instanceof ODataEntry &&
324
                       $result->id instanceof KeyDescriptor;
0 ignored issues
show
introduced by
$result->id is never a sub-type of POData\UriProcessor\Reso...entParser\KeyDescriptor.
Loading history...
325
            if ($isUrlKey || $isIdKey) {
0 ignored issues
show
introduced by
The condition $isIdKey is always false.
Loading history...
326
                if ($isIdKey) {
327
                    $link->url = $result->id;
328
                }
329
                return;
330
            }
331
        }
332
        assert(null === $result || !$result->id instanceof KeyDescriptor);
333
        assert(null === $link->url || is_string($link->url));
334
        if ($hasUrl) {
335
            $urlBitz = explode('/', $link->url);
336
            $rawPredicate = $urlBitz[count($urlBitz) - 1];
337
            $rawPredicate = explode('(', $rawPredicate);
338
            $setName = $rawPredicate[0];
339
            $rawPredicate = trim($rawPredicate[count($rawPredicate) - 1], ')');
340
            $targSet = $this->getMetaProvider()->resolveResourceSet($setName);
341
            assert(null !== $targSet, get_class($targSet));
342
            $type = $targSet->getResourceType();
343
        } else {
344
            $type = $this->getMetaProvider()->resolveResourceType($result->type->term);
345
        }
346
        assert($type instanceof ResourceEntityType, get_class($type));
347
        $propName = $link->title;
348
349
        /** @var KeyDescriptor|null $keyDesc */
350
        $keyDesc = null;
351
352
        if ($hasUrl) {
353
            assert(isset($rawPredicate));
354
            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...
355
            $keyDesc->validate($rawPredicate, $type);
356
            assert(null !== $keyDesc, 'Key description must not be null');
357
        }
358
359
        // hooking up to existing resource
360
        if ($hasUrl && !$hasPayload) {
361
            assert(isset($targSet));
362
            assert(isset($keyDesc));
363
            $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...
364
            assert(isset($target));
365
            $this->getWrapper()->hookSingleModel($sourceSet, $source, $targSet, $target, $propName);
366
            $link->url = $keyDesc;
367
            return;
368
        }
369
        // creating new resource
370
        if (!$hasUrl && $hasPayload) {
371
            list($targSet, $target) = $this->processEntryContent($result);
372
            assert(isset($target));
373
            $key = $this->generateKeyDescriptor($type, $result->propertyContent);
374
            $link->url = $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 $url is declared as type 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...
375
            $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 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...
376
            $this->getWrapper()->hookSingleModel($sourceSet, $source, $targSet, $target, $propName);
377
            return;
378
        }
379
        // updating existing resource and connecting to it
380
        list($targSet, $target) = $this->processEntryContent($result);
381
        assert(isset($target));
382
        $link->url = $keyDesc;
383
        $result->id = $keyDesc;
384
        $this->getWrapper()->hookSingleModel($sourceSet, $source, $targSet, $target, $propName);
385
        return;
386
    }
387
388
    /**
389
     * @param ODataLink $link
390
     * @param ResourceSet $sourceSet
391
     * @param $source
392
     * @param bool $hasUrl
393
     * @param bool $hasPayload
394
     * @throws InvalidOperationException
395
     * @throws \POData\Common\ODataException
396
     * @throws \ReflectionException
397
     * @throws \Exception
398
     */
399
    protected function processLinkFeed(ODataLink &$link, ResourceSet $sourceSet, $source, $hasUrl, $hasPayload)
400
    {
401
        assert(
402
            $link->expandedResult instanceof ODataFeed,
403
            get_class($link->expandedResult)
404
        );
405
        $propName = $link->title;
406
407
        // if entries is empty, bail out - nothing to do
408
        $numEntries = count($link->expandedResult->entries);
409
        if (0 === $numEntries) {
410
            return;
411
        }
412
        // check that each entry is of consistent resource set after checking it hasn't been processed
413
        $first = $link->expandedResult->entries[0]->resourceSetName;
414
        if ($link->expandedResult->entries[0]->id instanceof KeyDescriptor) {
0 ignored issues
show
introduced by
$link->expandedResult->entries[0]->id is never a sub-type of POData\UriProcessor\Reso...entParser\KeyDescriptor.
Loading history...
415
            return;
416
        }
417
        for ($i = 1; $i < $numEntries; $i++) {
418
            if ($first !== $link->expandedResult->entries[$i]->resourceSetName) {
419
                $msg = 'All entries in given feed must have same resource set';
420
                throw new \InvalidArgumentException($msg);
421
            }
422
        }
423
424
        $targSet = $this->getMetaProvider()->resolveResourceSet($first);
425
        assert($targSet instanceof ResourceSet);
426
        $targType = $targSet->getResourceType();
427
        assert($targType instanceof ResourceEntityType);
428
        $instanceType = $targType->getInstanceType();
429
        assert($instanceType instanceof \ReflectionClass);
430
        $targObj = $instanceType->newInstanceArgs();
431
432
        // assemble payload
433
        $data = [];
434
        $keys = [];
435
        for ($i = 0; $i < $numEntries; $i++) {
436
            $data[] = $this->getDeserialiser()->bulkDeserialise(
437
                $targType,
438
                $link->expandedResult->entries[$i]
439
            );
440
            $keys[] = $hasUrl ? $this->generateKeyDescriptor(
441
                $targType,
442
                $link->expandedResult->entries[$i]->propertyContent
443
            ) : null;
444
        }
445
446
        // creation
447
        if (!$hasUrl && $hasPayload) {
448
            $bulkResult = $this->getWrapper()->createBulkResourceforResourceSet($targSet, $data);
449
            assert(is_array($bulkResult));
450
            for ($i = 0; $i < $numEntries; $i++) {
451
                $targEntityInstance = $bulkResult[$i];
452
                $this->getWrapper()->hookSingleModel($sourceSet, $source, $targSet, $targEntityInstance, $propName);
453
                $key = $this->generateKeyDescriptor($targType, $targEntityInstance);
454
                $link->expandedResult->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 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...
455
            }
456
        }
457
        // update
458
        if ($hasUrl && $hasPayload) {
459
            $bulkResult = $this->getWrapper()->updateBulkResource($targSet, $targObj, $keys, $data);
460
            for ($i = 0; $i < $numEntries; $i++) {
461
                $targEntityInstance = $bulkResult[$i];
462
                $this->getWrapper()->hookSingleModel($sourceSet, $source, $targSet, $targEntityInstance, $propName);
463
                $link->expandedResult->entries[$i]->id = $keys[$i];
464
            }
465
        }
466
        assert(isset($bulkResult) && is_array($bulkResult));
467
468
        for ($i = 0; $i < $numEntries; $i++) {
469
            assert($link->expandedResult->entries[$i]->id instanceof KeyDescriptor);
470
            $numLinks = count($link->expandedResult->entries[$i]->links);
471
            for ($j = 0; $j < $numLinks; $j++) {
472
                $this->processLink($link->expandedResult->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...
473
            }
474
        }
475
476
        return;
477
    }
478
}
479