Passed
Push — master ( 3896ee...e50616 )
by Raffael
05:45
created

Factory::watchFrom()   C

Complexity

Conditions 11
Paths 48

Size

Total Lines 54

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 132

Importance

Changes 0
Metric Value
dl 0
loc 54
ccs 0
cts 34
cp 0
rs 6.8569
c 0
b 0
f 0
cc 11
nc 48
nop 8
crap 132

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.io
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 Symfony\Component\Yaml\Yaml;
26
27
class Factory
28
{
29
    const SPEC = __DIR__.'/../Rest/v1/openapi.yml';
30
31
    /**
32
     * Logger.
33
     *
34
     * @var LoggerInterface
35
     */
36
    protected $logger;
37
38
    /**
39
     * Database.
40
     *
41
     * @var Database
42
     */
43
    protected $db;
44
45
    /**
46
     * Storage.
47
     *
48
     * @var array
49
     */
50
    protected static $storage;
51
52
    /**
53
     * Initialize.
54
     */
55 22
    public function __construct(Database $db, LoggerInterface $logger)
56
    {
57 22
        $this->db = $db;
58 22
        $this->logger = $logger;
59 22
    }
60
61
    /**
62
     * Get resource schema.
63
     */
64 16
    public static function getSchema(string $kind): Schema
65
    {
66 16
        if (isset(self::$storage[$kind])) {
67 14
            return self::$storage[$kind];
68
        }
69
70 2
        $spec = self::loadSpecification();
71
72 2
        if (!isset($spec['components']['schemas'][$kind])) {
73
            throw new InvalidArgumentException('Provided resource kind is invalid');
74
        }
75
76 2
        $schema = new Schema($spec['components']['schemas'][$kind]);
77 2
        $schema->setRefLookup(new ArrayRefLookup($spec));
78 2
        $schema->setFlags(Schema::VALIDATE_EXTRA_PROPERTY_EXCEPTION);
79 2
        self::$storage[$kind] = $schema;
80
81 2
        return $schema;
82
    }
83
84
    /**
85
     * Validate resource.
86
     */
87 16
    public function validate(array $resource): array
88
    {
89 16
        $this->logger->debug('validate resource ['.$resource['kind'].'] against schema', [
90 16
            'category' => get_class($this),
91
        ]);
92
93 16
        return self::getSchema($resource['kind'])->validate($resource);
94
    }
95
96
    /**
97
     * Add resource.
98
     */
99 16
    public function addTo(Collection $collection, array $resource, bool $simulate = false): ObjectIdInterface
100
    {
101 16
        $ts = new UTCDateTime();
102
        $resource += [
103 16
            'created' => $ts,
104 16
            'changed' => $ts,
105 16
            'version' => 1,
106
        ];
107
108 16
        $this->logger->debug('add new resource to ['.$collection->getCollectionName().']', [
109 16
            'category' => get_class($this),
110 16
            'resource' => $resource,
111
        ]);
112
113 16
        if ($simulate === true) {
114
            return new ObjectId();
115
        }
116
117 16
        $result = $collection->insertOne($resource);
118 16
        $id = $result->getInsertedId();
119
120 16
        $this->logger->info('created new resource ['.$id.'] in ['.$collection->getCollectionName().']', [
121 16
            'category' => get_class($this),
122
        ]);
123
124 16
        return $id;
125
    }
126
127
    /**
128
     * Update resource.
129
     */
130
    public function updateIn(Collection $collection, ResourceInterface $resource, array $update, bool $simulate = false): bool
131
    {
132
        $this->logger->debug('update resource ['.$resource->getId().'] in ['.$collection->getCollectionName().']', [
133
            'category' => get_class($this),
134
            'update' => $update,
135
        ]);
136
137
        $op = [
138
            '$set' => $update,
139
        ];
140
141
        if (!isset($update['data']) || $resource->getData() === $update['data']) {
142
            $this->logger->info('resource ['.$resource->getId().'] version ['.$resource->getVersion().'] in ['.$collection->getCollectionName().'] is already up2date', [
143
                'category' => get_class($this),
144
            ]);
145
        } else {
146
            $this->logger->info('add new history record for resource ['.$resource->getId().'] in ['.$collection->getCollectionName().']', [
147
                'category' => get_class($this),
148
            ]);
149
150
            $op['$set']['changed'] = new UTCDateTime();
151
            $op += [
152
                '$addToSet' => ['history' => array_intersect_key($resource->toArray(), array_flip(['data', 'version', 'changed', 'description']))],
153
                '$inc' => ['version' => 1],
154
            ];
155
        }
156
157
        if ($simulate === true) {
158
            return true;
159
        }
160
161
        $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...
162
163
        $this->logger->info('updated resource ['.$resource->getId().'] in ['.$collection->getCollectionName().']', [
164
            'category' => get_class($this),
165
        ]);
166
167
        return true;
168
    }
169
170
    /**
171
     * Delete resource.
172
     */
173 2
    public function deleteFrom(Collection $collection, ObjectIdInterface $id, bool $simulate = false): bool
174
    {
175 2
        $this->logger->info('delete resource ['.$id.'] from ['.$collection->getCollectionName().']', [
176 2
            'category' => get_class($this),
177
        ]);
178
179 2
        if ($simulate === true) {
180
            return true;
181
        }
182
183 2
        $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...
184
185 2
        return true;
186
    }
187
188
    /**
189
     * Get all.
190
     */
191 6
    public function getAllFrom(Collection $collection, ?array $query = null, ?int $offset = null, ?int $limit = null, ?array $sort = null, ?Closure $build = null): Generator
192
    {
193 6
        if ($build === null) {
194 3
            $build = function ($resource) {
195 3
                return $this->build($resource);
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Tubee\Resource\Factory as the method build() does only exist in the following sub-classes of Tubee\Resource\Factory: Tubee\AccessRole\Factory, Tubee\AccessRule\Factory, Tubee\Collection\Factory, Tubee\DataObjectRelation\Factory, Tubee\DataObject\Factory, Tubee\Endpoint\Factory, Tubee\Job\Factory, Tubee\Log\Factory, Tubee\Process\Factory, Tubee\ResourceNamespace\Factory, Tubee\Secret\Factory, Tubee\User\Factory, Tubee\Workflow\Factory. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
196 3
            };
197
        }
198
199 6
        $total = $collection->count($query);
200 6
        $offset = $this->calcOffset($total, $offset);
201
202 6
        if (empty($sort)) {
203 6
            $sort = ['$natural' => -1];
204
        }
205
206 6
        $result = $collection->find($query, [
207 6
            'projection' => ['history' => 0],
208 6
            'skip' => $offset,
209 6
            'limit' => $limit,
210 6
            'sort' => $sort,
211
        ]);
212
213 6
        foreach ($result as $resource) {
214 6
            yield (string) $resource['_id'] => $build->call($this, $resource);
215
        }
216
217 6
        return $total;
218
    }
219
220
    /**
221
     * Change stream.
222
     */
223
    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
224
    {
225
        if ($build === null) {
226
            $build = function ($resource) {
227
                return $this->build($resource);
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Tubee\Resource\Factory as the method build() does only exist in the following sub-classes of Tubee\Resource\Factory: Tubee\AccessRole\Factory, Tubee\AccessRule\Factory, Tubee\Collection\Factory, Tubee\DataObjectRelation\Factory, Tubee\DataObject\Factory, Tubee\Endpoint\Factory, Tubee\Job\Factory, Tubee\Log\Factory, Tubee\Process\Factory, Tubee\ResourceNamespace\Factory, Tubee\Secret\Factory, Tubee\User\Factory, Tubee\Workflow\Factory. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
228
            };
229
        }
230
231
        $pipeline = $query;
232
        if (!empty($pipeline)) {
233
            $pipeline = [['$match' => $query]];
234
        }
235
236
        $stream = $collection->watch($pipeline, [
237
            'resumeAfter' => $after,
238
        ]);
239
240
        if ($existing === true) {
241
            $query = $this->prepareQuery($query);
242
            $total = $collection->count($query);
243
244
            if ($offset !== null && $total === 0) {
245
                $offset = null;
246
            } elseif ($offset < 0 && $total >= $offset * -1) {
247
                $offset = $total + $offset;
248
            }
249
250
            $result = $collection->find($query, [
251
                'projection' => ['history' => 0],
252
                'skip' => $offset,
253
                'limit' => $limit,
254
                'sort' => $sort,
255
            ]);
256
257
            foreach ($result as $resource) {
258
                yield (string) $resource['_id'] => [
259
                    'insert',
260
                    $build->call($this, $resource),
261
                ];
262
            }
263
        }
264
265
        for ($stream->rewind(); true; $stream->next()) {
266
            if (!$stream->valid()) {
267
                continue;
268
            }
269
270
            $event = $stream->current();
271
            yield (string) $event['fullDocument']['_id'] => [
272
                $event['operationType'],
273
                $build->call($this, $event['fullDocument']),
274
            ];
275
        }
276
    }
277
278
    /**
279
     * Build.
280
     */
281 10
    public function initResource(ResourceInterface $resource)
282
    {
283 10
        $this->logger->debug('initialized resource ['.$resource->getId().'] as ['.get_class($resource).']', [
284 10
            'category' => get_class($this),
285
        ]);
286
287 10
        return $resource;
288
    }
289
290
    /**
291
     * Load openapi specs.
292
     */
293 2
    protected static function loadSpecification(): array
294
    {
295 2
        if (apcu_exists(self::SPEC)) {
296
            //    return apcu_fetch(self::SPEC);
297
        }
298
299 2
        $data = Yaml::parseFile(self::SPEC);
300 2
        apcu_store(self::SPEC, $data);
301
302 2
        return $data;
303
    }
304
305
    /**
306
     * Remove fullDocument prefix from keys.
307
     */
308
    protected function prepareQuery(array $query): array
309
    {
310
        $filter = $query;
311
        if (isset($query['$and'])) {
312
            $query = $query['$and'][0];
313
        }
314
315
        $new = [];
316
        foreach ($query as $key => $value) {
317
            $new[substr($key, 13)] = $value;
318
        }
319
320
        if (isset($filter['$and'])) {
321
            $filter['$and'][0] = $new;
322
323
            return $filter;
324
        }
325
326
        return $new;
327
    }
328
329
    /**
330
     * Calculate offset.
331
     */
332 6
    protected function calcOffset(int $total, ?int $offset = null): ?int
333
    {
334 6
        if ($offset !== null && $total === 0) {
335
            $offset = 0;
336 6
        } elseif ($offset < 0 && $total >= $offset * -1) {
337
            $offset = $total + $offset;
338 6
        } elseif ($offset < 0) {
339
            $offset = 0;
340
        }
341
342 6
        return $offset;
343
    }
344
}
345