Completed
Push — master ( c8b0a5...59e33a )
by Dmitry
01:58
created

Temporal::getTimestamp()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 4
nc 2
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 override(array $override)
16
    {
17
        if (!$this->actor) {
18
            throw new Exception("actor is undefined");
19
        }
20
21
        if (array_key_exists('actor', $override)) {
22
            throw new Exception("actor override is defined");
23
        }
24
25
        if (array_key_exists('timestamp', $override)) {
26
            throw new Exception("timestamp override is defined");
27
        }
28
29
        foreach (['begin', 'end'] as $field) {
30
            if (array_key_exists($field, $override)) {
31
                if (is_string($override[$field])) {
32
                    $override[$field] = $this->getTimestamp($override[$field]);
33
                }
34
            } else {
35
                $override[$field] = 0;
36
            }
37
        }
38
39
        foreach ($override as $k => $v) {
40
            if (!in_array($k, ['entity', 'id', 'begin', 'end', 'data'])) {
41
                $override['entity'] = $k;
42
                $override['id'] = $v;
43
                unset($override[$k]);
44
            }
45
        }
46
47
        if (!array_key_exists('entity', $override)) {
48
            throw new Exception("no entity defined");
49
        }
50
51
        if (!$this->mapper->getSchema()->hasSpace($override['entity'])) {
52
            throw new Exception("invalid entity " . $override['entity']);
53
        }
54
55
        $space = $this->mapper->getSchema()->getSpace($override['entity']);
56
57
        // set entity id
58
        $override['entity'] = $space->getId();
59
60
        $override['actor'] = $this->actor;
61
        $override['timestamp'] = Carbon::now()->timestamp;
62
63
        $this->initSchema('override');
64
        $this->mapper->create('_override', $override);
65
        $this->updateState($override);
66
    }
67
68
    public function setActor($actor)
69
    {
70
        $this->actor = $actor;
71
        return $this;
72
    }
73
74
    public function getState($entity, $id, $date)
75
    {
76
        if (!$this->mapper->getSchema()->hasSpace($entity)) {
77
            throw new Exception("invalid entity: " . $entity);
78
        }
79
80
        $entity = $this->mapper->getSchema()->getSpace($entity)->getId();
81
        $date = $this->getTimestamp($date);
82
83
        $rows = $this->mapper->getClient()->getSpace('_override_aggregate')
84
            ->select([$entity, $id, $date], 0, 1, 0, 4) // [key, index, limit, offset, iterator = LE]
85
            ->getData();
86
87
        if (count($rows)) {
88
            $state = $this->mapper->findOne('_override_aggregate', [
89
                'entity' => $entity,
90
                'id' => $id,
91
                'begin' => $rows[0][2]
92
            ]);
93
            if (!$state->end || $state->end >= $date) {
94
                return $state->data;
95
            }
96
        }
97
98
        return [];
99
    }
100
101
    private function updateState($params)
102
    {
103
        $params = [
104
            'entity' => $params['entity'],
105
            'id'     => $params['id'],
106
        ];
107
108
        $timeaxis = [];
109
        $changeaxis = [];
110
111
        foreach ($this->mapper->find('_override', $params) as $i => $override) {
112
            foreach (['begin', 'end'] as $field) {
113
                if (!array_key_exists($override->$field, $timeaxis)) {
114
                    $timeaxis[$override->$field] = [
115
                        'begin' => $override->$field,
116
                        'end'   => $override->$field,
117
                        'data'  => [],
118
                    ];
119
                }
120
            }
121
122
            if (!array_key_exists($override->timestamp, $changeaxis)) {
123
                $changeaxis[$override->timestamp] = [];
124
            }
125
            $changeaxis[$override->timestamp][] = $override;
126
        }
127
128
        ksort($changeaxis);
129
        ksort($timeaxis);
130
131
        $nextSliceId = null;
132
        foreach (array_reverse(array_keys($timeaxis)) as $timestamp) {
133
            if ($nextSliceId) {
134
                $timeaxis[$timestamp]['end'] = $nextSliceId;
135
            } else {
136
                $timeaxis[$timestamp]['end'] = 0;
137
            }
138
            $nextSliceId = $timestamp;
139
        }
140
141
        foreach ($this->mapper->find('_override_aggregate', $params) as $state) {
142
            $this->mapper->remove($state);
143
        }
144
145
        $states = [];
146
        foreach ($timeaxis as $state) {
147
            foreach ($changeaxis as $overrides) {
148
                foreach ($overrides as $override) {
149
                    if ($override->begin > $state['begin']) {
150
                        // future override
151
                        continue;
152
                    }
153
                    if ($override->end && ($override->end < $state['end'] || !$state['end'])) {
154
                        // complete override
155
                        continue;
156
                    }
157
158
                    $state['data'] = array_merge($state['data'], $override->data);
159
                }
160
            }
161
            if (count($state['data'])) {
162
                $states[] = array_merge($state, $params);
163
            }
164
        }
165
166
        // merge states
167
        $clean = false;
168
        while (!$clean) {
169
            $clean = true;
170
            foreach ($states as $i => $state) {
171
                if (array_key_exists($i+1, $states)) {
172
                    $next = $states[$i+1];
173
                    if (!count(array_diff_assoc($state['data'], $next['data']))) {
174
                        $states[$i]['end'] = $next['end'];
175
                        unset($states[$i+1]);
176
                        $states = array_values($states);
177
                        $clean = false;
178
                        break;
179
                    }
180
                }
181
            }
182
        }
183
184
        foreach ($states as $state) {
185
            $this->mapper->create('_override_aggregate', $state);
186
        }
187
    }
188
189
    private function initSchema($name)
190
    {
191
        switch ($name) {
192
            case 'override':
193
                return $this->mapper->getSchema()->once(__CLASS__.'_states', function (Mapper $mapper) {
194
                    $mapper->getSchema()
195
                        ->createSpace('_override', [
196
                            'entity'     => 'unsigned',
197
                            'id'         => 'unsigned',
198
                            'begin'      => 'unsigned',
199
                            'end'        => 'unsigned',
200
                            'timestamp'  => 'unsigned',
201
                            'actor'      => 'unsigned',
202
                            'data'       => '*',
203
                        ])->addIndex([
204
                            'fields' => ['entity', 'id', 'begin', 'timestamp', 'actor']
205
                        ]);
206
207
                    $mapper->getSchema()
208
                        ->createSpace('_override_aggregate', [
209
                            'entity'     => 'unsigned',
210
                            'id'         => 'unsigned',
211
                            'begin'      => 'unsigned',
212
                            'end'        => 'unsigned',
213
                            'data'       => '*',
214
                        ])
215
                        ->addIndex([
216
                            'fields' => ['entity', 'id', 'begin'],
217
                        ]);
218
                });
219
        }
220
221
        throw new Exception("Invalid schema $name");
222
    }
223
224
    private function getTimestamp($string)
225
    {
226
        if (!array_key_exists($string, $this->timestamps)) {
227
            $this->timestamps[$string] = Carbon::parse($string)->timestamp;
228
        }
229
        return $this->timestamps[$string];
230
    }
231
}
232