Completed
Push — master ( b64fc1...5651bd )
by Raffael
16:20 queued 08:39
created

Factory::addTo()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 2.0011

Importance

Changes 0
Metric Value
dl 0
loc 27
ccs 14
cts 15
cp 0.9333
rs 9.488
c 0
b 0
f 0
cc 2
nc 2
nop 3
crap 2.0011
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * tubee
7
 *
8
 * @copyright   Copryright (c) 2017-2019 gyselroth GmbH (https://gyselroth.com)
9
 * @license     GPL-3.0 https://opensource.org/licenses/GPL-3.0
10
 */
11
12
namespace Tubee\Resource;
13
14
use Closure;
15
use Garden\Schema\ArrayRefLookup;
16
use Garden\Schema\Schema;
17
use Generator;
18
use InvalidArgumentException;
19
use MongoDB\BSON\ObjectId;
20
use MongoDB\BSON\ObjectIdInterface;
21
use MongoDB\BSON\UTCDateTime;
22
use MongoDB\Collection;
23
use MongoDB\Database;
24
use Psr\Log\LoggerInterface;
25
use Psr\SimpleCache\CacheInterface;
26
use Symfony\Component\Yaml\Yaml;
27
28
class Factory
29
{
30
    /**
31
     * OpenAPI.
32
     */
33
    const SPEC = __DIR__.'/../Rest/v1/openapi.yml';
34
35
    /**
36
     * Logger.
37
     *
38
     * @var LoggerInterface
39
     */
40
    protected $logger;
41
42
    /**
43
     * Database.
44
     *
45
     * @var Database
46
     */
47
    protected $db;
48
49
    /**
50
     * Cache.
51
     *
52
     * @var CacheInterface
53
     */
54
    protected $cache;
55
56
    /**
57
     * Initialize.
58
     */
59 26
    public function __construct(LoggerInterface $logger, CacheInterface $cache)
60
    {
61 26
        $this->logger = $logger;
62 26
        $this->cache = $cache;
63 26
    }
64
65
    /**
66
     * Get resource schema.
67
     */
68 20
    public function getSchema(string $kind): Schema
69
    {
70 20
        if ($this->cache->has($kind)) {
71
            $this->logger->debug('found resource kind ['.$kind.'] in cache', [
72
                'category' => get_class($this),
73
            ]);
74
75
            return $this->cache->get($kind);
76
        }
77
78 20
        $spec = $this->loadSpecification();
79 20
        $key = 'core.v1.'.$kind;
80
81 20
        if (!isset($spec['components']['schemas'][$key])) {
82 1
            throw new InvalidArgumentException('provided resource kind is invalid');
83
        }
84
85 19
        $schema = new Schema($spec['components']['schemas'][$key]);
86 19
        $schema->setRefLookup(new ArrayRefLookup($spec));
87 19
        $schema->setFlags(Schema::VALIDATE_EXTRA_PROPERTY_EXCEPTION);
88 19
        $this->cache->set($kind, $schema);
89
90 19
        return $schema;
91
    }
92
93
    /**
94
     * Validate resource.
95
     */
96 18
    public function validate(array $resource): array
97
    {
98 18
        $this->logger->debug('validate resource [{resource}] against schema', [
99 18
            'category' => get_class($this),
100 18
            'resource' => $resource,
101
        ]);
102
103 18
        $resource = $this->getSchema($resource['kind'])->validate($resource, [
104 18
            'request' => true,
105
        ]);
106
107 17
        $this->logger->debug('clean resource [{resource}]', [
108 17
            'category' => get_class($this),
109 17
            'resource' => $resource,
110
        ]);
111
112 17
        return $resource;
113
    }
114
115
    /**
116
     * Add resource.
117
     */
118 15
    public function addTo(Collection $collection, array $resource, bool $simulate = false): ObjectIdInterface
119
    {
120 15
        $ts = new UTCDateTime();
121
        $resource += [
122 15
            'created' => $ts,
123 15
            'changed' => $ts,
124 15
            'version' => 1,
125
        ];
126
127 15
        $this->logger->debug('add new resource to ['.$collection->getCollectionName().']', [
128 15
            'category' => get_class($this),
129 15
            'resource' => $resource,
130
        ]);
131
132 15
        if ($simulate === true) {
133
            return new ObjectId();
134
        }
135
136 15
        $result = $collection->insertOne($resource);
137 15
        $id = $result->getInsertedId();
138
139 15
        $this->logger->info('created new resource ['.$id.'] in ['.$collection->getCollectionName().']', [
140 15
            'category' => get_class($this),
141
        ]);
142
143 15
        return $id;
144
    }
145
146
    /**
147
     * Update resource.
148
     */
149
    public function updateIn(Collection $collection, ResourceInterface $resource, array $update, bool $simulate = false): bool
150
    {
151
        $this->logger->debug('update resource ['.$resource->getId().'] in ['.$collection->getCollectionName().']', [
152
            'category' => get_class($this),
153
            'update' => $update,
154
        ]);
155
156
        $op = [
157
            '$set' => $update,
158
        ];
159
160
        if (!isset($update['data']) || $resource->getData() === $update['data']) {
161
            $this->logger->info('resource ['.$resource->getId().'] version ['.$resource->getVersion().'] in ['.$collection->getCollectionName().'] is already up2date', [
162
                'category' => get_class($this),
163
            ]);
164
        } else {
165
            $this->logger->info('add new history record for resource ['.$resource->getId().'] in ['.$collection->getCollectionName().']', [
166
                'category' => get_class($this),
167
            ]);
168
169
            $op['$set']['changed'] = new UTCDateTime();
170
            $op += [
171
                '$addToSet' => ['history' => array_intersect_key($resource->toArray(), array_flip(['data', 'version', 'changed', 'description']))],
172
                '$inc' => ['version' => 1],
173
            ];
174
        }
175
176
        if ($simulate === true) {
177
            return true;
178
        }
179
180
        $result = $collection->updateOne(['_id' => $resource->getId()], $op);
0 ignored issues
show
Unused Code introduced by
$result is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
181
182
        $this->logger->info('updated resource ['.$resource->getId().'] in ['.$collection->getCollectionName().']', [
183
            'category' => get_class($this),
184
        ]);
185
186
        return true;
187
    }
188
189
    /**
190
     * Delete resource.
191
     */
192 1
    public function deleteFrom(Collection $collection, ObjectIdInterface $id, bool $simulate = false): bool
193
    {
194 1
        $this->logger->info('delete resource ['.$id.'] from ['.$collection->getCollectionName().']', [
195 1
            'category' => get_class($this),
196
        ]);
197
198 1
        if ($simulate === true) {
199
            return true;
200
        }
201
202 1
        $result = $collection->deleteOne(['_id' => $id]);
0 ignored issues
show
Unused Code introduced by
$result is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
203
204 1
        return true;
205
    }
206
207
    /**
208
     * Get all.
209
     */
210 6
    public function getAllFrom(Collection $collection, ?array $query = null, ?int $offset = null, ?int $limit = null, ?array $sort = null, ?Closure $build = null): Generator
211
    {
212 6
        $total = 0;
213
214 6
        if ($limit !== null) {
215 2
            $total = $collection->count($query);
216
        }
217
218 6
        $offset = $this->calcOffset($total, $offset);
219 6
        $result = $collection->find($query, [
220 6
            'projection' => ['history' => 0],
221 6
            'skip' => $offset,
222 6
            'limit' => $limit,
223 6
            'sort' => $sort,
224
        ]);
225
226 6
        foreach ($result as $resource) {
227 6
            $result = $build->call($this, $resource);
228 6
            if ($result !== null) {
229 6
                yield (string) $resource['_id'] => $result;
230
            }
231
        }
232
233 6
        return $total;
234
    }
235
236
    /**
237
     * Change stream.
238
     */
239
    public function watchFrom(Collection $collection, ?ObjectIdInterface $after = null, bool $existing = true, ?array $query = [], ?Closure $build = null, ?int $offset = null, ?int $limit = null, ?array $sort = null): Generator
240
    {
241
        $pipeline = $query;
242
        if (!empty($pipeline)) {
243
            $pipeline = [['$match' => $this->prepareQuery($query)]];
244
        }
245
246
        $stream = $collection->watch($pipeline, [
247
            'resumeAfter' => $after,
248
            'fullDocument' => 'updateLookup',
249
        ]);
250
251
        if ($existing === true) {
252
            if (empty($sort)) {
253
                $sort = ['created' => 1];
254
            }
255
256
            $total = $collection->count($query);
257
258
            if ($offset !== null && $total === 0) {
259
                $offset = null;
260
            } elseif ($offset < 0 && $total >= $offset * -1) {
261
                $offset = $total + $offset;
262
            }
263
264
            $result = $collection->find($query, [
265
                'projection' => ['history' => 0],
266
                'skip' => $offset,
267
                'limit' => $limit,
268
                'sort' => $sort,
269
            ]);
270
271
            foreach ($result as $resource) {
272
                $bound = $build->call($this, $resource);
273
274
                if ($bound === null) {
275
                    continue;
276
                }
277
278
                yield (string) $resource['_id'] => [
279
                    'insert',
280
                    $bound,
281
                ];
282
            }
283
        }
284
285
        for ($stream->rewind(); true; $stream->next()) {
286
            if (!$stream->valid()) {
287
                continue;
288
            }
289
290
            $event = $stream->current();
291
            $bound = $build->call($this, $event['fullDocument']);
292
293
            if ($bound === null) {
294
                continue;
295
            }
296
297
            yield (string) $event['fullDocument']['_id'] => [
298
                $event['operationType'],
299
                $bound,
300
            ];
301
        }
302
    }
303
304
    /**
305
     * Build.
306
     */
307 12
    public function initResource(ResourceInterface $resource)
308
    {
309 12
        $this->logger->debug('initialized resource ['.$resource->getId().'] as ['.get_class($resource).']', [
310 12
            'category' => get_class($this),
311
        ]);
312
313 12
        return $resource;
314
    }
315
316
    /**
317
     * Load openapi specs.
318
     */
319 20
    protected function loadSpecification(): array
320
    {
321 20
        if ($this->cache->has('openapi')) {
322
            return $this->cache->get('openapi');
323
        }
324
325 20
        $data = Yaml::parseFile(self::SPEC);
326 20
        $this->cache->set('openapi', $data);
327
328 20
        return $data;
329
    }
330
331
    /**
332
     * Add fullDocument prefix to keys.
333
     */
334
    protected function prepareQuery(array $query): array
335
    {
336
        $new = [];
337
        foreach ($query as $key => $value) {
338
            switch ($key) {
339
                case '$and':
340
                case '$or':
341
                    foreach ($value as $sub_key => $sub) {
342
                        $new[$key][$sub_key] = $this->prepareQuery($sub);
343
                    }
344
345
                break;
346
                default:
347
                    $new['fullDocument.'.$key] = $value;
348
            }
349
        }
350
351
        return $new;
352
    }
353
354
    /**
355
     * Calculate offset.
356
     */
357 6
    protected function calcOffset(int $total, ?int $offset = null): ?int
358
    {
359 6
        if ($offset !== null && $total === 0) {
360
            $offset = 0;
361 6
        } elseif ($offset < 0 && $total >= $offset * -1) {
362
            $offset = $total + $offset;
363 6
        } elseif ($offset < 0) {
364
            $offset = 0;
365
        }
366
367 6
        return $offset;
368
    }
369
}
370