Completed
Push — master ( b4b95a...6bab26 )
by Dmitry
01:47
created

Temporal::override()   B

Complexity

Conditions 6
Paths 12

Size

Total Lines 45
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 45
rs 8.439
c 0
b 0
f 0
cc 6
eloc 26
nc 12
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 getLinks($entity, $id, $date)
16
    {
17
        $links = $this->getData($entity, $id, $date, '_temporal_link_aggregate');
18
        foreach ($links as $i => $source) {
19
            $link = array_key_exists(1, $source) ? ['data' => $source[1]] : [];
20
            foreach ($source[0] as $spaceId => $entityId) {
21
                $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...
22
                $link[$spaceName] = $entityId;
23
            }
24
            $links[$i] = $link;
25
        }
26
        return $links;
27
    }
28
29
    public function getState($entity, $id, $date)
30
    {
31
        return $this->getData($entity, $id, $date, '_temporal_override_aggregate');
32
    }
33
34
    private function getData($entity, $id, $date, $space)
35
    {
36
        $entity = $this->entityNameToId($entity);
37
        $date = $this->getTimestamp($date);
38
39
        $rows = $this->mapper->getClient()->getSpace($space)
40
            ->select([$entity, $id, $date], 0, 1, 0, 4) // [key, index, limit, offset, iterator = LE]
41
            ->getData();
42
43
        if (count($rows)) {
44
            $state = $this->mapper->findOne($space, [
45
                'entity' => $entity,
46
                'id' => $id,
47
                'begin' => $rows[0][2]
48
            ]);
49
            if (!$state->end || $state->end >= $date) {
50
                return $state->data;
51
            }
52
        }
53
54
        return [];
55
    }
56
57
    public function override(array $override)
58
    {
59
        $override = $this->parseConfig($override);
60
61
        foreach ($override as $k => $v) {
62
            if (!in_array($k, ['entity', 'id', 'begin', 'end', 'data'])) {
63
                $override['entity'] = $k;
64
                $override['id'] = $v;
65
                unset($override[$k]);
66
            }
67
        }
68
69
        if (!array_key_exists('entity', $override)) {
70
            throw new Exception("no entity defined");
71
        }
72
73
        // set entity id
74
        $override['entity'] = $this->entityNameToId($override['entity']);
75
76
        $override['actor'] = $this->actor;
77
        $override['timestamp'] = Carbon::now()->timestamp;
78
79
        $this->initSchema('override');
80
        $this->mapper->create('_temporal_override', $override);
81
82
        $params = [
83
            'entity' => $override['entity'],
84
            'id'     => $override['id'],
85
        ];
86
87
        $changeaxis = [];
88
89
        foreach ($this->mapper->find('_temporal_override', $params) as $i => $override) {
90
            if (!array_key_exists($override->timestamp, $changeaxis)) {
91
                $changeaxis[$override->timestamp] = [];
92
            }
93
            $changeaxis[$override->timestamp][] = [
94
                'begin' => $override->begin,
95
                'end' => $override->end,
96
                'data' => $override->data,
97
            ];
98
        }
99
100
        $this->updateAggregation('override', $params, $changeaxis);
101
    }
102
103
    public function link(array $link)
104
    {
105
        $link = $this->parseConfig($link);
106
107
        $this->initSchema('link');
108
109
        ksort($link);
110
        $node = null;
111
        foreach ($link as $entity => $id) {
112
            if (in_array($entity, ['begin', 'end', 'data'])) {
113
                continue;
114
            }
115
            $spaceId = $this->entityNameToId($entity);
116
117
            $params = [
118
                'entity'   => $spaceId,
119
                'entityId' => $id,
120
                'parent'   => $node ? $node->id : 0,
121
            ];
122
            $node = $this->mapper->findOrCreate('_temporal_link', $params);
123
        }
124
125
        if (!$node || !$node->parent) {
126
            throw new Exception("Invalid link configuration");
127
        }
128
129
        $node->begin = $link['begin'];
130
        $node->end = $link['end'];
131
        $node->actor = $this->actor;
132
        $node->timestamp = Carbon::now()->timestamp;
133
        if (array_key_exists('data', $link)) {
134
            $node->data = $link['data'];
135
        }
136
137
        $node->save();
138
139
        foreach ($link as $entity => $id) {
140
            if (in_array($entity, ['begin', 'end', 'data'])) {
141
                continue;
142
            }
143
            $spaceId = $this->entityNameToId($entity);
144
            $source = $this->mapper->find('_temporal_link', [
145
                'entity'   => $spaceId,
146
                'entityId' => $id,
147
            ]);
148
149
            $leafs = [];
150
            foreach ($source as $node) {
151
                foreach ($this->getLeafs($node) as $detail) {
152
                    $leafs[] = $detail;
153
                }
154
            }
155
156
            $changeaxis = [];
157
158
            foreach ($leafs as $leaf) {
159
                $current = $leaf;
160
                $ref = [];
161
162
                while ($current) {
163
                    if ($current->entity != $spaceId) {
164
                        $ref[$current->entity] = $current->entityId;
165
                    }
166
                    if ($current->parent) {
167
                        $current = $this->mapper->findOne('_temporal_link', $current->parent);
168
                    } else {
169
                        $current = null;
170
                    }
171
                }
172
173
                $data = [$ref];
174
                if (property_exists($leaf, 'data') && $leaf->data) {
175
                    $data[] = $leaf->data;
176
                }
177
178
                if (!array_key_exists($leaf->timestamp, $changeaxis)) {
179
                    $changeaxis[$leaf->timestamp] = [];
180
                }
181
                $changeaxis[$leaf->timestamp][] = [
182
                    'begin' => $leaf->begin,
183
                    'end' => $leaf->end,
184
                    'data' => $data
185
                ];
186
            }
187
188
            $params = [
189
                'entity' => $spaceId,
190
                'id'     => $id,
191
            ];
192
193
            $this->updateAggregation('link', $params, $changeaxis);
194
        }
195
    }
196
197
    public function setActor($actor)
198
    {
199
        $this->actor = $actor;
200
        return $this;
201
    }
202
203
    private function getLeafs($link)
204
    {
205
        if ($link->timestamp) {
206
            return [$link];
207
        }
208
209
        $leafs = [];
210
        foreach ($this->mapper->find('_temporal_link', ['parent' => $link->id]) as $child) {
211
            foreach ($this->getLeafs($child) as $leaf) {
212
                $leafs[] = $leaf;
213
            }
214
        }
215
        return $leafs;
216
    }
217
218
    private function getTimestamp($string)
219
    {
220
        if (!array_key_exists($string, $this->timestamps)) {
221
            $this->timestamps[$string] = Carbon::parse($string)->timestamp;
222
        }
223
        return $this->timestamps[$string];
224
    }
225
226
    private function parseConfig(array $data)
227
    {
228
        if (!$this->actor) {
229
            throw new Exception("actor is undefined");
230
        }
231
232
        if (array_key_exists('actor', $data)) {
233
            throw new Exception("actor is defined");
234
        }
235
236
        if (array_key_exists('timestamp', $data)) {
237
            throw new Exception("timestamp is defined");
238
        }
239
240
        foreach (['begin', 'end'] as $field) {
241
            if (array_key_exists($field, $data)) {
242
                if (is_string($data[$field])) {
243
                    $data[$field] = $this->getTimestamp($data[$field]);
244
                }
245
            } else {
246
                $data[$field] = 0;
247
            }
248
        }
249
250
        return $data;
251
    }
252
253
    private function updateAggregation($type, $params, $changeaxis)
254
    {
255
        $isLink = $type === 'link';
256
        $space = $isLink ? '_temporal_link_aggregate' : '_temporal_override_aggregate';
257
258
        $timeaxis = [];
259
        foreach ($changeaxis as $timestamp => $changes) {
260
            foreach ($changes as $change) {
261
                foreach (['begin', 'end'] as $field) {
262
                    if (!array_key_exists($change[$field], $timeaxis)) {
263
                        $timeaxis[$change[$field]] = [
264
                            'begin' => $change[$field],
265
                            'end'   => $change[$field],
266
                            'data'  => [],
267
                        ];
268
                    }
269
                }
270
            }
271
        }
272
273
        ksort($changeaxis);
274
        ksort($timeaxis);
275
276
        $nextSliceId = null;
277
        foreach (array_reverse(array_keys($timeaxis)) as $timestamp) {
278
            if ($nextSliceId) {
279
                $timeaxis[$timestamp]['end'] = $nextSliceId;
280
            } else {
281
                $timeaxis[$timestamp]['end'] = 0;
282
            }
283
            $nextSliceId = $timestamp;
284
        }
285
286
        foreach ($this->mapper->find($space, $params) as $state) {
287
            $this->mapper->remove($state);
288
        }
289
290
        $states = [];
291
        foreach ($timeaxis as $state) {
292
            foreach ($changeaxis as $changes) {
293
                foreach ($changes as $change) {
294
                    if ($change['begin'] > $state['begin']) {
295
                        // future override
296
                        continue;
297
                    }
298
                    if ($change['end'] && ($change['end'] < $state['end'] || !$state['end'])) {
299
                        // complete override
300
                        continue;
301
                    }
302
                    if ($isLink) {
303
                        $state['data'][] = $change['data'];
304
                    } else {
305
                        $state['data'] = array_merge($state['data'], $change['data']);
306
                    }
307
                }
308
            }
309
            if (count($state['data'])) {
310
                $states[] = array_merge($state, $params);
311
            }
312
        }
313
314
        // merge states
315
        $clean = $isLink;
316
        while (!$clean) {
317
            $clean = true;
318
            foreach ($states as $i => $state) {
319
                if (array_key_exists($i+1, $states)) {
320
                    $next = $states[$i+1];
321
                    if (!count(array_diff_assoc($state['data'], $next['data']))) {
322
                        $states[$i]['end'] = $next['end'];
323
                        unset($states[$i+1]);
324
                        $states = array_values($states);
325
                        $clean = false;
326
                        break;
327
                    }
328
                }
329
            }
330
        }
331
332
        foreach ($states as $state) {
333
            $this->mapper->create($space, $state);
334
        }
335
    }
336
337
    private function entityNameToId($name)
338
    {
339
        if (!$this->mapper->hasPlugin(Sequence::class)) {
340
            $this->mapper->addPlugin(Sequence::class);
341
        }
342
343
        $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...
344
            $this->mapper->getSchema()
345
                ->createSpace('_temporal_entity', [
346
                    'id'   => 'unsigned',
347
                    'name' => 'str',
348
                ])
349
                ->addIndex(['id'])
350
                ->addIndex(['name']);
351
        });
352
353
        return $this->mapper->findOrCreate('_temporal_entity', compact('name'))->id;
354
    }
355
356
    private function entityIdToName($id)
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
357
    {
358
        return $this->mapper->findOne('_temporal_entity', compact('id'))->name;
359
    }
360
361
    private function initSchema($name)
362
    {
363
        switch ($name) {
364
            case 'override':
365
                return $this->mapper->getSchema()->once(__CLASS__.'@states', function (Mapper $mapper) {
366
                    $mapper->getSchema()
367
                        ->createSpace('_temporal_override', [
368
                            'entity'     => 'unsigned',
369
                            'id'         => 'unsigned',
370
                            'begin'      => 'unsigned',
371
                            'end'        => 'unsigned',
372
                            'timestamp'  => 'unsigned',
373
                            'actor'      => 'unsigned',
374
                            'data'       => '*',
375
                        ])
376
                        ->addIndex(['entity', 'id', 'begin', 'timestamp', 'actor']);
377
378
                    $mapper->getSchema()
379
                        ->createSpace('_temporal_override_aggregate', [
380
                            'entity'     => 'unsigned',
381
                            'id'         => 'unsigned',
382
                            'begin'      => 'unsigned',
383
                            'end'        => 'unsigned',
384
                            'data'       => '*',
385
                        ])
386
                        ->addIndex(['entity', 'id', 'begin']);
387
                });
388
389
            case 'link':
390
                return $this->mapper->getSchema()->once(__CLASS__.'@link', function (Mapper $mapper) {
391
                    $mapper->getSchema()
392
                        ->createSpace('_temporal_link', [
393
                            'id'        => 'unsigned',
394
                            'parent'    => 'unsigned',
395
                            'entity'    => 'unsigned',
396
                            'entityId'  => 'unsigned',
397
                            'begin'     => 'unsigned',
398
                            'end'       => 'unsigned',
399
                            'timestamp' => 'unsigned',
400
                            'actor'     => 'unsigned',
401
                            'data'      => '*',
402
                        ])
403
                        ->addIndex(['id'])
404
                        ->addIndex(['entity', 'entityId', 'parent', 'begin', 'timestamp', 'actor'])
405
                        ->addIndex([
406
                            'fields' => 'parent',
407
                            'unique' => false,
408
                        ]);
409
410
                    $mapper->getSchema()
411
                        ->createSpace('_temporal_link_aggregate', [
412
                            'entity' => 'unsigned',
413
                            'id'     => 'unsigned',
414
                            'begin'  => 'unsigned',
415
                            'end'    => 'unsigned',
416
                            'data'   => '*',
417
                        ])
418
                        ->addIndex(['entity', 'id', 'begin']);
419
                });
420
        }
421
422
        throw new Exception("Invalid schema $name");
423
    }
424
}
425