Completed
Push — master ( 70ee55...1a8571 )
by Dmitry
01:50
created

Temporal::parseConfig()   D

Complexity

Conditions 9
Paths 7

Size

Total Lines 26
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 26
rs 4.909
c 0
b 0
f 0
cc 9
eloc 14
nc 7
nop 1
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\Plugin;
9
10
class Temporal extends Plugin
11
{
12
    private $actor;
13
    private $timestamps = [];
14
15
    public function getLinksLog($entity, $entityId, $filter = [])
16
    {
17
        $this->initSchema('link');
18
19
        $entity = $this->entityNameToId($entity);
20
21
        $nodes = $this->mapper->find('_temporal_link', [
22
            'entity' => $entity,
23
            'entityId' => $entityId,
24
        ]);
25
26
        $links = [];
27
28
        foreach ($nodes as $node) {
29
            foreach ($this->getLeafs($node) as $leaf) {
30
                $entityName = $this->entityIdToName($leaf->entity);
31
                $link = [
32
                    $entityName => $leaf->entityId,
33
                    'begin'     => $leaf->begin,
34
                    'end'       => $leaf->end,
35
                    'timestamp' => $leaf->timestamp,
36
                    'actor'     => $leaf->actor,
37
                ];
38
39
                $current = $leaf;
40
                while ($current->parent) {
41
                    $current = $this->mapper->findOne('_temporal_link', $current->parent);
42
                    $entityName = $this->entityIdToName($current->entity);
43
                    $link[$entityName] = $current->entityId;
44
                }
45
46
                if (count($filter)) {
47
                    foreach ($filter as $required) {
48
                        if (!array_key_exists($required, $link)) {
49
                            continue 2;
50
                        }
51
                    }
52
                }
53
                $links[] = $link;
54
            }
55
        }
56
57
        return $links;
58
    }
59
60
    public function getLinks($entity, $id, $date)
61
    {
62
        $this->initSchema('link');
63
64
        $links = $this->getData($entity, $id, $date, '_temporal_link_aggregate');
65
        foreach ($links as $i => $source) {
66
            $link = array_key_exists(1, $source) ? ['data' => $source[1]] : [];
67
            foreach ($source[0] as $spaceId => $entityId) {
68
                $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...
69
                $link[$spaceName] = $entityId;
70
            }
71
            $links[$i] = $link;
72
        }
73
        return $links;
74
    }
75
76
    public function getState($entity, $id, $date)
77
    {
78
        $this->initSchema('override');
79
80
        return $this->getData($entity, $id, $date, '_temporal_override_aggregate');
81
    }
82
83
    private function getData($entity, $id, $date, $space)
84
    {
85
        $entity = $this->entityNameToId($entity);
86
        $date = $this->getTimestamp($date);
87
88
        $rows = $this->mapper->getClient()->getSpace($space)
89
            ->select([$entity, $id, $date], 0, 1, 0, 4) // [key, index, limit, offset, iterator = LE]
90
            ->getData();
91
92
        if (count($rows) && $rows[0][0] == $entity && $rows[0][1] == $id) {
93
            $state = $this->mapper->findOne($space, [
94
                'entity' => $entity,
95
                'id' => $id,
96
                'begin' => $rows[0][2]
97
            ]);
98
            if (!$state->end || $state->end >= $date) {
99
                return $state->data;
100
            }
101
        }
102
103
        return [];
104
    }
105
106
    public function getOverrides($entityName, $id)
107
    {
108
        return $this->mapper->find('_temporal_override', [
109
            'entity' => $this->entityNameToId($entityName),
110
            'id' => $id,
111
        ]);
112
    }
113
114
    public function override(array $override)
115
    {
116
        $override = $this->parseConfig($override);
117
118
        foreach ($override as $k => $v) {
119
            if (!in_array($k, ['entity', 'id', 'begin', 'end', 'data'])) {
120
                $override['entity'] = $k;
121
                $override['id'] = $v;
122
                unset($override[$k]);
123
            }
124
        }
125
126
        if (!array_key_exists('entity', $override)) {
127
            throw new Exception("no entity defined");
128
        }
129
130
        // set entity id
131
        $entityName = $override['entity'];
132
        $override['entity'] = $this->entityNameToId($entityName);
133
        $override['actor'] = $this->actor;
134
        $override['timestamp'] = Carbon::now()->timestamp;
135
136
        $this->initSchema('override');
137
        $this->mapper->create('_temporal_override', $override);
138
139
        $this->updateOverrideAggregation($entityName, $override['id']);
140
    }
141
142
    public function setOverrideIdle($entity, $id, $begin, $actor, $timestamp, $flag)
143
    {
144
        $override = $this->mapper->findOrFail('_temporal_override', [
145
            'entity' => $this->entityNameToId($entity),
146
            'id' => $id,
147
            'begin' => $begin,
148
            'actor' => $actor,
149
            'timestamp' => $timestamp,
150
        ]);
151
        $idled = property_exists($override, 'idle') && $override->idle > 0;
152
        if ($idled && !$flag || !$idled && $flag) {
153
            return $this->toggleOverrideIdle($entity, $id, $begin, $actor, $timestamp);
154
        }
155
    }
156
157
    public function toggleOverrideIdle($entity, $id, $begin, $actor, $timestamp)
158
    {
159
        $override = $this->mapper->findOrFail('_temporal_override', [
160
            'entity' => $this->entityNameToId($entity),
161
            'id' => $id,
162
            'begin' => $begin,
163
            'actor' => $actor,
164
            'timestamp' => $timestamp,
165
        ]);
166
167
        if (property_exists($override, 'idle') && $override->idle) {
168
            $override->idle = 0;
169
        } else {
170
            $override->idle = time();
171
        }
172
        $override->save();
173
174
        $this->updateOverrideAggregation($entity, $id);
175
    }
176
177
    public function updateOverrideAggregation($entity, $id)
178
    {
179
        $params = [
180
            'entity' => $this->entityNameToId($entity),
181
            'id'     => $id,
182
        ];
183
184
        $changeaxis = [];
185
186
        foreach ($this->mapper->find('_temporal_override', $params) as $i => $override) {
187
            if (property_exists($override, 'idle') && $override->idle) {
188
                continue;
189
            }
190
            if (!array_key_exists($override->begin, $changeaxis)) {
191
                $changeaxis[$override->begin] = [];
192
            }
193
            $changeaxis[$override->begin][] = [
194
                'begin' => $override->begin,
195
                'end' => $override->end,
196
                'data' => $override->data,
197
            ];
198
        }
199
200
        $this->updateAggregation('override', $params, $changeaxis);
201
    }
202
203
    public function link(array $link)
204
    {
205
        $link = $this->parseConfig($link);
206
207
        $this->initSchema('link');
208
209
        ksort($link);
210
        $node = null;
211
        foreach ($link as $entity => $id) {
212
            if (in_array($entity, ['begin', 'end', 'data'])) {
213
                continue;
214
            }
215
            $spaceId = $this->entityNameToId($entity);
216
217
            $params = [
218
                'entity'   => $spaceId,
219
                'entityId' => $id,
220
                'parent'   => $node ? $node->id : 0,
221
            ];
222
            $node = $this->mapper->findOrCreate('_temporal_link', $params);
223
        }
224
225
        if (!$node || !$node->parent) {
226
            throw new Exception("Invalid link configuration");
227
        }
228
229
        $node->begin = $link['begin'];
230
        $node->end = $link['end'];
231
        $node->actor = $this->actor;
232
        $node->timestamp = Carbon::now()->timestamp;
233
        if (array_key_exists('data', $link)) {
234
            $node->data = $link['data'];
235
        }
236
237
        $node->save();
238
239
        foreach ($link as $entity => $id) {
240
            if (in_array($entity, ['begin', 'end', 'data'])) {
241
                continue;
242
            }
243
            $spaceId = $this->entityNameToId($entity);
244
            $source = $this->mapper->find('_temporal_link', [
245
                'entity'   => $spaceId,
246
                'entityId' => $id,
247
            ]);
248
249
            $leafs = [];
250
            foreach ($source as $node) {
251
                foreach ($this->getLeafs($node) as $detail) {
252
                    $leafs[] = $detail;
253
                }
254
            }
255
256
            $changeaxis = [];
257
258
            foreach ($leafs as $leaf) {
259
                $current = $leaf;
260
                $ref = [];
261
262
                while ($current) {
263
                    if ($current->entity != $spaceId) {
264
                        $ref[$current->entity] = $current->entityId;
265
                    }
266
                    if ($current->parent) {
267
                        $current = $this->mapper->findOne('_temporal_link', $current->parent);
268
                    } else {
269
                        $current = null;
270
                    }
271
                }
272
273
                $data = [$ref];
274
                if (property_exists($leaf, 'data') && $leaf->data) {
275
                    $data[] = $leaf->data;
276
                }
277
278
                if (!array_key_exists($leaf->timestamp, $changeaxis)) {
279
                    $changeaxis[$leaf->timestamp] = [];
280
                }
281
                $changeaxis[$leaf->timestamp][] = [
282
                    'begin' => $leaf->begin,
283
                    'end' => $leaf->end,
284
                    'data' => $data
285
                ];
286
            }
287
288
            $params = [
289
                'entity' => $spaceId,
290
                'id'     => $id,
291
            ];
292
293
            $this->updateAggregation('link', $params, $changeaxis);
294
        }
295
    }
296
297
    public function setActor($actor)
298
    {
299
        $this->actor = $actor;
300
        return $this;
301
    }
302
303
    private function getLeafs($link)
304
    {
305
        if ($link->timestamp) {
306
            return [$link];
307
        }
308
309
        $leafs = [];
310
        foreach ($this->mapper->find('_temporal_link', ['parent' => $link->id]) as $child) {
311
            foreach ($this->getLeafs($child) as $leaf) {
312
                $leafs[] = $leaf;
313
            }
314
        }
315
        return $leafs;
316
    }
317
318
    private function getTimestamp($string)
319
    {
320
        if (!array_key_exists($string, $this->timestamps)) {
321
            if (strlen($string) == 8 && is_numeric($string)) {
322
                $this->timestamps[$string] = Carbon::createFromFormat('Ymd', $string)->setTime(0, 0, 0)->timestamp;
323
            } else {
324
                $this->timestamps[$string] = Carbon::parse($string)->timestamp;
325
            }
326
        }
327
        return $this->timestamps[$string];
328
    }
329
330
    private function parseConfig(array $data)
331
    {
332
        if (!$this->actor) {
333
            throw new Exception("actor is undefined");
334
        }
335
336
        if (array_key_exists('actor', $data)) {
337
            throw new Exception("actor is defined");
338
        }
339
340
        if (array_key_exists('timestamp', $data)) {
341
            throw new Exception("timestamp is defined");
342
        }
343
344
        foreach (['begin', 'end'] as $field) {
345
            if (array_key_exists($field, $data) && strlen($data[$field])) {
346
                if (strlen($data[$field]) == 8 || is_string($data[$field])) {
347
                    $data[$field] = $this->getTimestamp($data[$field]);
348
                }
349
            } else {
350
                $data[$field] = 0;
351
            }
352
        }
353
354
        return $data;
355
    }
356
357
    private function updateAggregation($type, $params, $changeaxis)
358
    {
359
        $isLink = $type === 'link';
360
        $space = $isLink ? '_temporal_link_aggregate' : '_temporal_override_aggregate';
361
362
        $timeaxis = [];
363
        foreach ($changeaxis as $timestamp => $changes) {
364
            foreach ($changes as $change) {
365
                foreach (['begin', 'end'] as $field) {
366
                    if (!array_key_exists($change[$field], $timeaxis)) {
367
                        $timeaxis[$change[$field]] = [
368
                            'begin' => $change[$field],
369
                            'end'   => $change[$field],
370
                            'data'  => [],
371
                        ];
372
                    }
373
                }
374
            }
375
        }
376
377
        ksort($changeaxis);
378
        ksort($timeaxis);
379
380
        $nextSliceId = null;
381
        foreach (array_reverse(array_keys($timeaxis)) as $timestamp) {
382
            if ($nextSliceId) {
383
                $timeaxis[$timestamp]['end'] = $nextSliceId;
384
            } else {
385
                $timeaxis[$timestamp]['end'] = 0;
386
            }
387
            $nextSliceId = $timestamp;
388
        }
389
390
        foreach ($this->mapper->find($space, $params) as $state) {
391
            $this->mapper->remove($state);
392
        }
393
394
        $states = [];
395
        foreach ($timeaxis as $state) {
396
            foreach ($changeaxis as $changes) {
397
                foreach ($changes as $change) {
398
                    if ($change['begin'] > $state['begin']) {
399
                        // future override
400
                        continue;
401
                    }
402
                    if ($change['end'] && ($change['end'] < $state['end'] || !$state['end'])) {
403
                        // complete override
404
                        continue;
405
                    }
406
                    if ($isLink) {
407
                        $state['data'][] = $change['data'];
408
                    } else {
409
                        $state['data'] = array_merge($state['data'], $change['data']);
410
                    }
411
                }
412
            }
413
            if (count($state['data'])) {
414
                $states[] = array_merge($state, $params);
415
            }
416
        }
417
418
        // merge states
419
        $clean = $isLink;
420
        while (!$clean) {
421
            $clean = true;
422
            foreach ($states as $i => $state) {
423
                if (array_key_exists($i+1, $states)) {
424
                    $next = $states[$i+1];
425
                    if (json_encode($state['data']) == json_encode($next['data'])) {
426
                        $states[$i]['end'] = $next['end'];
427
                        unset($states[$i+1]);
428
                        $states = array_values($states);
429
                        $clean = false;
430
                        break;
431
                    }
432
                }
433
            }
434
        }
435
436
        foreach ($states as $state) {
437
            $this->mapper->create($space, $state);
438
        }
439
    }
440
441
    private function entityNameToId($name)
442
    {
443
        if (!$this->mapper->hasPlugin(Sequence::class)) {
444
            $this->mapper->addPlugin(Sequence::class);
445
        }
446
447
        $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...
448
            $this->mapper->getSchema()
449
                ->createSpace('_temporal_entity', [
450
                    'id'   => 'unsigned',
451
                    'name' => 'str',
452
                ])
453
                ->addIndex(['id'])
454
                ->addIndex(['name']);
455
        });
456
457
        return $this->mapper->findOrCreate('_temporal_entity', compact('name'))->id;
458
    }
459
460
    private function entityIdToName($id)
461
    {
462
        return $this->mapper->findOne('_temporal_entity', compact('id'))->name;
463
    }
464
465
    private function initSchema($name)
466
    {
467
        switch ($name) {
468
            case 'override':
469
                $this->mapper->getSchema()->once(__CLASS__.'@states', function (Mapper $mapper) {
470
                    $mapper->getSchema()
471
                        ->createSpace('_temporal_override', [
472
                            'entity'     => 'unsigned',
473
                            'id'         => 'unsigned',
474
                            'begin'      => 'unsigned',
475
                            'end'        => 'unsigned',
476
                            'timestamp'  => 'unsigned',
477
                            'actor'      => 'unsigned',
478
                            'data'       => '*',
479
                        ])
480
                        ->addIndex(['entity', 'id', 'begin', 'timestamp', 'actor']);
481
482
                    $mapper->getSchema()
483
                        ->createSpace('_temporal_override_aggregate', [
484
                            'entity'     => 'unsigned',
485
                            'id'         => 'unsigned',
486
                            'begin'      => 'unsigned',
487
                            'end'        => 'unsigned',
488
                            'data'       => '*',
489
                        ])
490
                        ->addIndex(['entity', 'id', 'begin']);
491
                });
492
                $this->mapper->getSchema()->once(__CLASS__.'@override-idle', function (Mapper $mapper) {
493
                    $mapper->getSchema()->getSpace('_temporal_override')->addProperty('idle', 'unsigned');
494
                });
495
                return;
496
497
            case 'link':
498
                return $this->mapper->getSchema()->once(__CLASS__.'@link', function (Mapper $mapper) {
499
                    $mapper->getSchema()
500
                        ->createSpace('_temporal_link', [
501
                            'id'        => 'unsigned',
502
                            'parent'    => 'unsigned',
503
                            'entity'    => 'unsigned',
504
                            'entityId'  => 'unsigned',
505
                            'begin'     => 'unsigned',
506
                            'end'       => 'unsigned',
507
                            'timestamp' => 'unsigned',
508
                            'actor'     => 'unsigned',
509
                            'data'      => '*',
510
                        ])
511
                        ->addIndex(['id'])
512
                        ->addIndex(['entity', 'entityId', 'parent', 'begin', 'timestamp', 'actor'])
513
                        ->addIndex([
514
                            'fields' => 'parent',
515
                            'unique' => false,
516
                        ]);
517
518
                    $mapper->getSchema()
519
                        ->createSpace('_temporal_link_aggregate', [
520
                            'entity' => 'unsigned',
521
                            'id'     => 'unsigned',
522
                            'begin'  => 'unsigned',
523
                            'end'    => 'unsigned',
524
                            'data'   => '*',
525
                        ])
526
                        ->addIndex(['entity', 'id', 'begin']);
527
                });
528
        }
529
530
        throw new Exception("Invalid schema $name");
531
    }
532
}
533