Completed
Push — master ( e9a4c1...1f7593 )
by Raffael
09:08 queued 01:05
created

Factory   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 338
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Test Coverage

Coverage 50.68%

Importance

Changes 0
Metric Value
wmc 43
lcom 1
cbo 8
dl 0
loc 338
ccs 74
cts 146
cp 0.5068
rs 8.96
c 0
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A getSchema() 0 24 3
A validate() 0 18 1
A addTo() 0 27 2
A updateIn() 0 39 4
A deleteFrom() 0 14 2
A getAllFrom() 0 25 4
C watchFrom() 0 60 12
A initResource() 0 8 1
A loadSpecification() 0 11 2
A prepareQuery() 0 19 5
A calcOffset() 0 12 6

How to fix   Complexity   

Complex Class

Complex classes like Factory often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Factory, and based on these observations, apply Extract Interface, too.

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);
0 ignored issues
show
Deprecated Code introduced by
The method MongoDB\Collection::count() has been deprecated with message: 1.4

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
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
            $total = $collection->count($query);
0 ignored issues
show
Deprecated Code introduced by
The method MongoDB\Collection::count() has been deprecated with message: 1.4

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
253
254
            if ($offset !== null && $total === 0) {
255
                $offset = null;
256
            } elseif ($offset < 0 && $total >= $offset * -1) {
257
                $offset = $total + $offset;
258
            }
259
260
            $result = $collection->find($query, [
261
                'projection' => ['history' => 0],
262
                'skip' => $offset,
263
                'limit' => $limit,
264
                'sort' => $sort,
265
            ]);
266
267
            foreach ($result as $resource) {
268
                $bound = $build->call($this, $resource);
269
270
                if ($bound === null) {
271
                    continue;
272
                }
273
274
                yield (string) $resource['_id'] => [
275
                    'insert',
276
                    $bound,
277
                ];
278
            }
279
        }
280
281
        for ($stream->rewind(); true; $stream->next()) {
282
            if (!$stream->valid()) {
283
                continue;
284
            }
285
286
            $event = $stream->current();
287
            $bound = $build->call($this, $event['fullDocument']);
288
289
            if ($bound === null) {
290
                continue;
291
            }
292
293
            yield (string) $event['fullDocument']['_id'] => [
294
                $event['operationType'],
295
                $bound,
296
            ];
297
        }
298
    }
299
300
    /**
301
     * Build.
302
     */
303 12
    public function initResource(ResourceInterface $resource)
304
    {
305 12
        $this->logger->debug('initialized resource ['.$resource->getId().'] as ['.get_class($resource).']', [
306 12
            'category' => get_class($this),
307
        ]);
308
309 12
        return $resource;
310
    }
311
312
    /**
313
     * Load openapi specs.
314
     */
315 20
    protected function loadSpecification(): array
316
    {
317 20
        if ($this->cache->has('openapi')) {
318
            return $this->cache->get('openapi');
319
        }
320
321 20
        $data = Yaml::parseFile(self::SPEC);
322 20
        $this->cache->set('openapi', $data);
323
324 20
        return $data;
325
    }
326
327
    /**
328
     * Add fullDocument prefix to keys.
329
     */
330
    protected function prepareQuery(array $query): array
331
    {
332
        $new = [];
333
        foreach ($query as $key => $value) {
334
            switch ($key) {
335
                case '$and':
336
                case '$or':
337
                    foreach ($value as $sub_key => $sub) {
338
                        $new[$key][$sub_key] = $this->prepareQuery($sub);
339
                    }
340
341
                break;
342
                default:
343
                    $new['fullDocument.'.$key] = $value;
344
            }
345
        }
346
347
        return $new;
348
    }
349
350
    /**
351
     * Calculate offset.
352
     */
353 6
    protected function calcOffset(int $total, ?int $offset = null): ?int
354
    {
355 6
        if ($offset !== null && $total === 0) {
356
            $offset = 0;
357 6
        } elseif ($offset < 0 && $total >= $offset * -1) {
358
            $offset = $total + $offset;
359 6
        } elseif ($offset < 0) {
360
            $offset = 0;
361
        }
362
363 6
        return $offset;
364
    }
365
}
366