Completed
Push — master ( 2d873c...4c1051 )
by Dmitry
01:47
created

Temporal::updateOverrideAggregation()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 25
rs 8.439
c 0
b 0
f 0
cc 5
eloc 15
nc 4
nop 2
1
<?php
2
3
namespace Tarantool\Mapper\Plugin;
4
5
use Carbon\Carbon;
6
use Exception;
7
use Tarantool\Mapper\Mapper;
8
use Tarantool\Mapper\Entity;
9
use Tarantool\Mapper\Plugin;
10
11
class Temporal extends Plugin
12
{
13
    private $actor;
14
    private $timestamps = [];
15
16
    public function getLinksLog($entity, $entityId, $filter = [])
17
    {
18
        $this->initSchema('link');
19
20
        $entity = $this->entityNameToId($entity);
21
22
        $nodes = $this->mapper->find('_temporal_link', [
23
            'entity' => $entity,
24
            'entityId' => $entityId,
25
        ]);
26
27
        $links = [];
28
29
        foreach ($nodes as $node) {
30
            foreach ($this->getLeafs($node) as $leaf) {
31
                $entityName = $this->entityIdToName($leaf->entity);
32
                $link = [
33
                    $entityName => $leaf->entityId,
34
                    'begin'     => $leaf->begin,
35
                    'end'       => $leaf->end,
36
                    'timestamp' => $leaf->timestamp,
37
                    'actor'     => $leaf->actor,
38
                    'idle'      => property_exists($leaf, 'idle') ? $leaf->idle : 0,
39
                ];
40
41
                $current = $leaf;
42 View Code Duplication
                while ($current->parent) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
43
                    $current = $this->mapper->findOne('_temporal_link', $current->parent);
44
                    $entityName = $this->entityIdToName($current->entity);
45
                    $link[$entityName] = $current->entityId;
46
                }
47
48
                if (count($filter)) {
49
                    foreach ($filter as $required) {
50
                        if (!array_key_exists($required, $link)) {
51
                            continue 2;
52
                        }
53
                    }
54
                }
55
                $links[] = $link;
56
            }
57
        }
58
59
        return $links;
60
    }
61
62
    public function getLinks($entity, $id, $date)
63
    {
64
        $this->initSchema('link');
65
66
        $links = $this->getData($entity, $id, $date, '_temporal_link_aggregate');
67
        foreach ($links as $i => $source) {
68
            $link = array_key_exists(1, $source) ? ['data' => $source[1]] : [];
69
            foreach ($source[0] as $spaceId => $entityId) {
70
                $spaceName = $this->mapper->findOne('_temporal_entity', $spaceId)->name;
0 ignored issues
show
Documentation introduced by
$spaceId is of type integer|string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
71
                $link[$spaceName] = $entityId;
72
            }
73
            $links[$i] = $link;
74
        }
75
        return $links;
76
    }
77
78
    public function getState($entity, $id, $date)
79
    {
80
        $this->initSchema('override');
81
82
        return $this->getData($entity, $id, $date, '_temporal_override_aggregate');
83
    }
84
85
    private function getData($entity, $id, $date, $space)
86
    {
87
        $entity = $this->entityNameToId($entity);
88
        $date = $this->getTimestamp($date);
89
90
        $rows = $this->mapper->getClient()->getSpace($space)
91
            ->select([$entity, $id, $date], 0, 1, 0, 4) // [key, index, limit, offset, iterator = LE]
92
            ->getData();
93
94
        if (count($rows) && $rows[0][0] == $entity && $rows[0][1] == $id) {
95
            $state = $this->mapper->findOne($space, [
96
                'entity' => $entity,
97
                'id' => $id,
98
                'begin' => $rows[0][2]
99
            ]);
100
            if (!$state->end || $state->end >= $date) {
101
                return $state->data;
102
            }
103
        }
104
105
        return [];
106
    }
107
108
    public function getOverrides($entityName, $id)
109
    {
110
        return $this->mapper->find('_temporal_override', [
111
            'entity' => $this->entityNameToId($entityName),
112
            'id' => $id,
113
        ]);
114
    }
115
116
    public function override(array $override)
117
    {
118
        $override = $this->parseConfig($override);
119
120
        foreach ($override as $k => $v) {
121
            if (!in_array($k, ['entity', 'id', 'begin', 'end', 'data'])) {
122
                $override['entity'] = $k;
123
                $override['id'] = $v;
124
                unset($override[$k]);
125
            }
126
        }
127
128
        if (!array_key_exists('entity', $override)) {
129
            throw new Exception("no entity defined");
130
        }
131
132
        // set entity id
133
        $entityName = $override['entity'];
134
        $override['entity'] = $this->entityNameToId($entityName);
135
        $override['actor'] = $this->actor;
136
        $override['timestamp'] = Carbon::now()->timestamp;
137
138
        $this->initSchema('override');
139
        $this->mapper->create('_temporal_override', $override);
140
141
        $this->updateOverrideAggregation($entityName, $override['id']);
142
    }
143
144
    public function setLinkIdle($id, $flag)
145
    {
146
        $link = $this->mapper->findOrFail('_temporal_link', $id);
147
148
        $idled = property_exists($link, 'idle') && $link->idle > 0;
149
        if ($idled && !$flag || !$idled && $flag) {
150
            return $this->toggleLinkIdle($link);
151
        }
152
    }
153
154
    public function toggleLinkIdle(Entity $link)
155
    {
156
        if (property_exists($link, 'idle') && $link->idle) {
157
            $link->idle = 0;
0 ignored issues
show
Bug introduced by
The property idle does not seem to exist in Tarantool\Mapper\Entity.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
158
        } else {
159
            $link->idle = time();
160
        }
161
        $link->save();
162
163
        $this->updateLinkAggregation($link);
164
    }
165
166
    public function setOverrideIdle($entity, $id, $begin, $actor, $timestamp, $flag)
167
    {
168
        $override = $this->mapper->findOrFail('_temporal_override', [
169
            'entity' => $this->entityNameToId($entity),
170
            'id' => $id,
171
            'begin' => $begin,
172
            'actor' => $actor,
173
            'timestamp' => $timestamp,
174
        ]);
175
        $idled = property_exists($override, 'idle') && $override->idle > 0;
176
        if ($idled && !$flag || !$idled && $flag) {
177
            return $this->toggleOverrideIdle($entity, $id, $begin, $actor, $timestamp);
178
        }
179
    }
180
181
    public function toggleOverrideIdle($entity, $id, $begin, $actor, $timestamp)
182
    {
183
        $override = $this->mapper->findOrFail('_temporal_override', [
184
            'entity' => $this->entityNameToId($entity),
185
            'id' => $id,
186
            'begin' => $begin,
187
            'actor' => $actor,
188
            'timestamp' => $timestamp,
189
        ]);
190
191
        if (property_exists($override, 'idle') && $override->idle) {
192
            $override->idle = 0;
193
        } else {
194
            $override->idle = time();
195
        }
196
        $override->save();
197
198
        $this->updateOverrideAggregation($entity, $id);
199
    }
200
201
    public function updateOverrideAggregation($entity, $id)
202
    {
203
        $params = [
204
            'entity' => $this->entityNameToId($entity),
205
            'id'     => $id,
206
        ];
207
208
        $changeaxis = [];
209
210
        foreach ($this->mapper->find('_temporal_override', $params) as $i => $override) {
211
            if (property_exists($override, 'idle') && $override->idle) {
212
                continue;
213
            }
214
            if (!array_key_exists($override->begin, $changeaxis)) {
215
                $changeaxis[$override->begin] = [];
216
            }
217
            $changeaxis[$override->begin][] = [
218
                'begin' => $override->begin,
219
                'end' => $override->end,
220
                'data' => $override->data,
221
            ];
222
        }
223
224
        $this->updateAggregation('override', $params, $changeaxis);
225
    }
226
227
    public function link(array $link)
228
    {
229
        $link = $this->parseConfig($link);
230
231
        $this->initSchema('link');
232
233
        $config = [];
234
        foreach ($link as $entity => $id) {
235
            if (!in_array($entity, ['begin', 'end', 'data'])) {
236
                $config[$entity] = $id;
237
            }
238
        }
239
240
        ksort($config);
241
        $node = null;
242
243
        foreach (array_keys($config) as $i => $entity) {
244
            $id = $config[$entity];
245
            $spaceId = $this->entityNameToId($entity);
246
            $params = [
247
                'entity'   => $spaceId,
248
                'entityId' => $id,
249
                'parent'   => $node ? $node->id : 0,
250
            ];
251
            if (count($config) == $i+1) {
252
                $params['begin'] = $link['begin'];
253
                $params['timestamp'] = 0;
254
            }
255
            $node = $this->mapper->findOrCreate('_temporal_link', $params);
256
        }
257
258
        if (!$node || !$node->parent) {
259
            throw new Exception("Invalid link configuration");
260
        }
261
262
        $node->begin = $link['begin'];
263
        $node->end = $link['end'];
264
        $node->actor = $this->actor;
265
        $node->timestamp = Carbon::now()->timestamp;
266
        if (array_key_exists('data', $link)) {
267
            $node->data = $link['data'];
268
        }
269
270
        $node->save();
271
272
        $this->updateLinkAggregation($node);
273
    }
274
275
    public function updateLinkAggregation(Entity $node)
276
    {
277
        $todo = [
278
            $this->entityIdToName($node->entity) => $node->entityId,
0 ignored issues
show
Bug introduced by
The property entity does not seem to exist in Tarantool\Mapper\Entity.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
Bug introduced by
The property entityId does not seem to exist in Tarantool\Mapper\Entity.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
279
        ];
280
281
        $current = $node;
282 View Code Duplication
        while ($current->parent) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
283
            $current = $this->mapper->findOne('_temporal_link', ['id' => $current->parent]);
284
            $todo[$this->entityIdToName($current->entity)] = $current->entityId;
285
        }
286
287
        foreach ($todo as $entity => $id) {
288
289
            $spaceId = $this->entityNameToId($entity);
290
            $source = $this->mapper->find('_temporal_link', [
291
                'entity'   => $spaceId,
292
                'entityId' => $id,
293
            ]);
294
295
            $leafs = [];
296
            foreach ($source as $node) {
297
                foreach ($this->getLeafs($node) as $detail) {
298
                    $leafs[] = $detail;
299
                }
300
            }
301
302
            $changeaxis = [];
303
304
            foreach ($leafs as $leaf) {
305
                $current = $leaf;
306
                $ref = [];
307
308
                if (property_exists($leaf, 'idle') && $leaf->idle) {
309
                    continue;
310
                }
311
312
                while ($current) {
313
                    if ($current->entity != $spaceId) {
314
                        $ref[$current->entity] = $current->entityId;
315
                    }
316
                    if ($current->parent) {
317
                        $current = $this->mapper->findOne('_temporal_link', $current->parent);
318
                    } else {
319
                        $current = null;
320
                    }
321
                }
322
323
                $data = [$ref];
324
                if (property_exists($leaf, 'data') && $leaf->data) {
325
                    $data[] = $leaf->data;
326
                }
327
328
                if (!array_key_exists($leaf->timestamp, $changeaxis)) {
329
                    $changeaxis[$leaf->timestamp] = [];
330
                }
331
                $changeaxis[$leaf->timestamp][] = [
332
                    'begin' => $leaf->begin,
333
                    'end' => $leaf->end,
334
                    'data' => $data
335
                ];
336
            }
337
338
            $params = [
339
                'entity' => $spaceId,
340
                'id'     => $id,
341
            ];
342
343
            $this->updateAggregation('link', $params, $changeaxis);
344
        }
345
    }
346
347
    public function setActor($actor)
348
    {
349
        $this->actor = $actor;
350
        return $this;
351
    }
352
353
    private function getLeafs($link)
354
    {
355
        if ($link->timestamp) {
356
            return [$link];
357
        }
358
359
        $leafs = [];
360
        foreach ($this->mapper->find('_temporal_link', ['parent' => $link->id]) as $child) {
361
            foreach ($this->getLeafs($child) as $leaf) {
362
                $leafs[] = $leaf;
363
            }
364
        }
365
        return $leafs;
366
    }
367
368
    private function getTimestamp($string)
369
    {
370
        if (!array_key_exists($string, $this->timestamps)) {
371
            if (strlen($string) == 8 && is_numeric($string)) {
372
                $this->timestamps[$string] = Carbon::createFromFormat('Ymd', $string)->setTime(0, 0, 0)->timestamp;
373
            } else {
374
                $this->timestamps[$string] = Carbon::parse($string)->timestamp;
375
            }
376
        }
377
        return $this->timestamps[$string];
378
    }
379
380
    private function parseConfig(array $data)
381
    {
382
        if (!$this->actor) {
383
            throw new Exception("actor is undefined");
384
        }
385
386
        if (array_key_exists('actor', $data)) {
387
            throw new Exception("actor is defined");
388
        }
389
390
        if (array_key_exists('timestamp', $data)) {
391
            throw new Exception("timestamp is defined");
392
        }
393
394
        foreach (['begin', 'end'] as $field) {
395
            if (array_key_exists($field, $data) && strlen($data[$field])) {
396
                if (strlen($data[$field]) == 8 || is_string($data[$field])) {
397
                    $data[$field] = $this->getTimestamp($data[$field]);
398
                }
399
            } else {
400
                $data[$field] = 0;
401
            }
402
        }
403
404
        return $data;
405
    }
406
407
    private function updateAggregation($type, $params, $changeaxis)
408
    {
409
        $isLink = $type === 'link';
410
        $space = $isLink ? '_temporal_link_aggregate' : '_temporal_override_aggregate';
411
412
        $timeaxis = [];
413
        foreach ($changeaxis as $timestamp => $changes) {
414
            foreach ($changes as $change) {
415
                foreach (['begin', 'end'] as $field) {
416
                    if (!array_key_exists($change[$field], $timeaxis)) {
417
                        $timeaxis[$change[$field]] = [
418
                            'begin' => $change[$field],
419
                            'end'   => $change[$field],
420
                            'data'  => [],
421
                        ];
422
                    }
423
                }
424
            }
425
        }
426
427
        ksort($changeaxis);
428
        ksort($timeaxis);
429
430
        $nextSliceId = null;
431
        foreach (array_reverse(array_keys($timeaxis)) as $timestamp) {
432
            if ($nextSliceId) {
433
                $timeaxis[$timestamp]['end'] = $nextSliceId;
434
            } else {
435
                $timeaxis[$timestamp]['end'] = 0;
436
            }
437
            $nextSliceId = $timestamp;
438
        }
439
440
        foreach ($this->mapper->find($space, $params) as $state) {
441
            $this->mapper->remove($state);
442
        }
443
444
        $states = [];
445
        foreach ($timeaxis as $state) {
446
            foreach ($changeaxis as $changes) {
447
                foreach ($changes as $change) {
448
                    if ($change['begin'] > $state['begin']) {
449
                        // future override
450
                        continue;
451
                    }
452
                    if ($change['end'] && ($change['end'] < $state['end'] || !$state['end'])) {
453
                        // complete override
454
                        continue;
455
                    }
456
                    if ($isLink) {
457
                        $state['data'][] = $change['data'];
458
                    } else {
459
                        $state['data'] = array_merge($state['data'], $change['data']);
460
                    }
461
                }
462
            }
463
            if (count($state['data'])) {
464
                $states[] = array_merge($state, $params);
465
            }
466
        }
467
468
        // merge states
469
        $clean = $isLink;
470
        while (!$clean) {
471
            $clean = true;
472
            foreach ($states as $i => $state) {
473
                if (array_key_exists($i+1, $states)) {
474
                    $next = $states[$i+1];
475
                    if (json_encode($state['data']) == json_encode($next['data'])) {
476
                        $states[$i]['end'] = $next['end'];
477
                        unset($states[$i+1]);
478
                        $states = array_values($states);
479
                        $clean = false;
480
                        break;
481
                    }
482
                }
483
            }
484
        }
485
486
        foreach ($states as $state) {
487
            $this->mapper->create($space, $state);
488
        }
489
    }
490
491
    private function entityNameToId($name)
492
    {
493
        if (!$this->mapper->hasPlugin(Sequence::class)) {
494
            $this->mapper->addPlugin(Sequence::class);
495
        }
496
497
        $this->mapper->getSchema()->once(__CLASS__.'@entity', function (Mapper $mapper) {
0 ignored issues
show
Unused Code introduced by
The parameter $mapper is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
498
            $this->mapper->getSchema()
499
                ->createSpace('_temporal_entity', [
500
                    'id'   => 'unsigned',
501
                    'name' => 'str',
502
                ])
503
                ->addIndex(['id'])
504
                ->addIndex(['name']);
505
        });
506
507
        return $this->mapper->findOrCreate('_temporal_entity', compact('name'))->id;
508
    }
509
510
    private function entityIdToName($id)
511
    {
512
        return $this->mapper->findOne('_temporal_entity', compact('id'))->name;
513
    }
514
515
    private function initSchema($name)
516
    {
517
        switch ($name) {
518
            case 'override':
519
                $this->mapper->getSchema()->once(__CLASS__.'@states', function (Mapper $mapper) {
520
                    $mapper->getSchema()
521
                        ->createSpace('_temporal_override', [
522
                            'entity'     => 'unsigned',
523
                            'id'         => 'unsigned',
524
                            'begin'      => 'unsigned',
525
                            'end'        => 'unsigned',
526
                            'timestamp'  => 'unsigned',
527
                            'actor'      => 'unsigned',
528
                            'data'       => '*',
529
                        ])
530
                        ->addIndex(['entity', 'id', 'begin', 'timestamp', 'actor']);
531
532
                    $mapper->getSchema()
533
                        ->createSpace('_temporal_override_aggregate', [
534
                            'entity'     => 'unsigned',
535
                            'id'         => 'unsigned',
536
                            'begin'      => 'unsigned',
537
                            'end'        => 'unsigned',
538
                            'data'       => '*',
539
                        ])
540
                        ->addIndex(['entity', 'id', 'begin']);
541
                });
542
                $this->mapper->getSchema()->once(__CLASS__.'@override-idle', function (Mapper $mapper) {
543
                    $mapper->getSchema()->getSpace('_temporal_override')->addProperty('idle', 'unsigned');
544
                });
545
                return;
546
547
            case 'link':
548
                $this->mapper->getSchema()->once(__CLASS__.'@link', function (Mapper $mapper) {
549
                    $mapper->getSchema()
550
                        ->createSpace('_temporal_link', [
551
                            'id'        => 'unsigned',
552
                            'parent'    => 'unsigned',
553
                            'entity'    => 'unsigned',
554
                            'entityId'  => 'unsigned',
555
                            'begin'     => 'unsigned',
556
                            'end'       => 'unsigned',
557
                            'timestamp' => 'unsigned',
558
                            'actor'     => 'unsigned',
559
                            'data'      => '*',
560
                        ])
561
                        ->addIndex(['id'])
562
                        ->addIndex(['entity', 'entityId', 'parent', 'begin', 'timestamp', 'actor'])
563
                        ->addIndex([
564
                            'fields' => 'parent',
565
                            'unique' => false,
566
                        ]);
567
568
                    $mapper->getSchema()
569
                        ->createSpace('_temporal_link_aggregate', [
570
                            'entity' => 'unsigned',
571
                            'id'     => 'unsigned',
572
                            'begin'  => 'unsigned',
573
                            'end'    => 'unsigned',
574
                            'data'   => '*',
575
                        ])
576
                        ->addIndex(['entity', 'id', 'begin']);
577
                });
578
                $this->mapper->getSchema()->once(__CLASS__.'@link-idle', function (Mapper $mapper) {
579
                    $mapper->getSchema()->getSpace('_temporal_link')->addProperty('idle', 'unsigned');
580
                });
581
                return;
582
        }
583
584
        throw new Exception("Invalid schema $name");
585
    }
586
}
587