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

Factory::watchFrom()   C

Complexity

Conditions 13
Paths 56

Size

Total Lines 64

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 182

Importance

Changes 0
Metric Value
dl 0
loc 64
ccs 0
cts 38
cp 0
rs 6.0787
c 0
b 0
f 0
cc 13
nc 56
nop 8
crap 182

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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