Completed
Push — master ( 316baf...2178d1 )
by Raffael
67:25 queued 62:39
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 Generator;
16
use MongoDB\BSON\ObjectId;
17
use MongoDB\BSON\ObjectIdInterface;
18
use MongoDB\BSON\UTCDateTime;
19
use MongoDB\Collection;
20
use MongoDB\Database;
21
use Psr\Log\LoggerInterface;
22
23
class Factory
24
{
25
    /**
26
     * Logger.
27
     *
28
     * @var LoggerInterface
29
     */
30
    protected $logger;
31
32
    /**
33
     * Database.
34
     *
35
     * @var Database
36
     */
37
    protected $db;
38
39
    /**
40
     * Initialize.
41
     */
42 22
    public function __construct(Database $db, LoggerInterface $logger)
43
    {
44 22
        $this->db = $db;
45 22
        $this->logger = $logger;
46 22
    }
47
48
    /**
49
     * Add resource.
50
     */
51 16
    public function addTo(Collection $collection, array $resource, bool $simulate = false): ObjectIdInterface
52
    {
53 16
        $ts = new UTCDateTime();
54
        $resource += [
55 16
            'created' => $ts,
56 16
            'changed' => $ts,
57 16
            'version' => 1,
58
        ];
59
60 16
        $this->logger->debug('add new resource to ['.$collection->getCollectionName().']', [
61 16
            'category' => get_class($this),
62 16
            'resource' => $resource,
63
        ]);
64
65 16
        if ($simulate === true) {
66
            return new ObjectId();
67
        }
68
69 16
        $result = $collection->insertOne($resource);
70 16
        $id = $result->getInsertedId();
71
72 16
        $this->logger->info('created new resource ['.$id.'] in ['.$collection->getCollectionName().']', [
73 16
            'category' => get_class($this),
74
        ]);
75
76 16
        return $id;
77
    }
78
79
    /**
80
     * Update resource.
81
     */
82
    public function updateIn(Collection $collection, ResourceInterface $resource, array $update, bool $simulate = false): bool
83
    {
84
        $this->logger->debug('update resource ['.$resource->getId().'] in ['.$collection->getCollectionName().']', [
85
            'category' => get_class($this),
86
            'update' => $update,
87
        ]);
88
89
        $op = [
90
            '$set' => $update,
91
        ];
92
93
        if (!isset($update['data']) || $resource->getData() === $update['data']) {
94
            $this->logger->info('resource ['.$resource->getId().'] version ['.$resource->getVersion().'] in ['.$collection->getCollectionName().'] is already up2date', [
95
                'category' => get_class($this),
96
            ]);
97
        } else {
98
            $this->logger->info('add new history record for resource ['.$resource->getId().'] in ['.$collection->getCollectionName().']', [
99
                'category' => get_class($this),
100
            ]);
101
102
            $op['$set']['changed'] = new UTCDateTime();
103
            $op += [
104
                '$addToSet' => ['history' => array_intersect_key($resource->toArray(), array_flip(['data', 'version', 'changed', 'description']))],
105
                '$inc' => ['version' => 1],
106
            ];
107
        }
108
109
        if ($simulate === true) {
110
            return true;
111
        }
112
113
        $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...
114
115
        $this->logger->info('updated resource ['.$resource->getId().'] in ['.$collection->getCollectionName().']', [
116
            'category' => get_class($this),
117
        ]);
118
119
        return true;
120
    }
121
122
    /**
123
     * Delete resource.
124
     */
125 2
    public function deleteFrom(Collection $collection, ObjectIdInterface $id, bool $simulate = false): bool
126
    {
127 2
        $this->logger->info('delete resource ['.$id.'] from ['.$collection->getCollectionName().']', [
128 2
            'category' => get_class($this),
129
        ]);
130
131 2
        if ($simulate === true) {
132
            return true;
133
        }
134
135 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...
136
137 2
        return true;
138
    }
139
140
    /**
141
     * Get all.
142
     */
143 6
    public function getAllFrom(Collection $collection, ?array $query = null, ?int $offset = null, ?int $limit = null, ?array $sort = null, ?Closure $build = null): Generator
144
    {
145 6
        if ($build === null) {
146 3
            $build = function ($resource) {
147 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...
148 3
            };
149
        }
150
151 6
        $total = $collection->count($query);
152 6
        $offset = $this->calcOffset($total, $offset);
153
154 6
        if (empty($sort)) {
155 6
            $sort = ['$natural' => -1];
156
        }
157
158 6
        $result = $collection->find($query, [
159 6
            'projection' => ['history' => 0],
160 6
            'skip' => $offset,
161 6
            'limit' => $limit,
162 6
            'sort' => $sort,
163
        ]);
164
165 6
        foreach ($result as $resource) {
166 6
            yield (string) $resource['_id'] => $build->call($this, $resource);
167
        }
168
169 6
        return $total;
170
    }
171
172
    /**
173
     * Change stream.
174
     */
175
    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
176
    {
177
        if ($build === null) {
178
            $build = function ($resource) {
179
                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...
180
            };
181
        }
182
183
        $pipeline = $query;
184
        if (!empty($pipeline)) {
185
            $pipeline = [['$match' => $query]];
186
        }
187
188
        $stream = $collection->watch($pipeline, [
189
            'resumeAfter' => $after,
190
        ]);
191
192
        if ($existing === true) {
193
            $query = $this->prepareQuery($query);
194
            $total = $collection->count($query);
195
196
            if ($offset !== null && $total === 0) {
197
                $offset = null;
198
            } elseif ($offset < 0 && $total >= $offset * -1) {
199
                $offset = $total + $offset;
200
            }
201
202
            $result = $collection->find($query, [
203
                'projection' => ['history' => 0],
204
                'skip' => $offset,
205
                'limit' => $limit,
206
                'sort' => $sort,
207
            ]);
208
209
            foreach ($result as $resource) {
210
                yield (string) $resource['_id'] => [
211
                    'insert',
212
                    $build->call($this, $resource),
213
                ];
214
            }
215
        }
216
217
        for ($stream->rewind(); true; $stream->next()) {
218
            if (!$stream->valid()) {
219
                continue;
220
            }
221
222
            $event = $stream->current();
223
            yield (string) $event['fullDocument']['_id'] => [
224
                $event['operationType'],
225
                $build->call($this, $event['fullDocument']),
226
            ];
227
        }
228
    }
229
230
    /**
231
     * Build.
232
     */
233 10
    public function initResource(ResourceInterface $resource)
234
    {
235 10
        $this->logger->debug('initialized resource ['.$resource->getId().'] as ['.get_class($resource).']', [
236 10
            'category' => get_class($this),
237
        ]);
238
239 10
        return $resource;
240
    }
241
242
    /**
243
     * Remove fullDocument prefix from keys.
244
     */
245
    protected function prepareQuery(array $query): array
246
    {
247
        $filter = $query;
248
        if (isset($query['$and'])) {
249
            $query = $query['$and'][0];
250
        }
251
252
        $new = [];
253
        foreach ($query as $key => $value) {
254
            $new[substr($key, 13)] = $value;
255
        }
256
257
        if (isset($filter['$and'])) {
258
            $filter['$and'][0] = $new;
259
260
            return $filter;
261
        }
262
263
        return $new;
264
    }
265
266
    /**
267
     * Calculate offset.
268
     */
269 6
    protected function calcOffset(int $total, ?int $offset = null): ?int
270
    {
271 6
        if ($offset !== null && $total === 0) {
272
            $offset = 0;
273 6
        } elseif ($offset < 0 && $total >= $offset * -1) {
274
            $offset = $total + $offset;
275 6
        } elseif ($offset < 0) {
276
            $offset = 0;
277
        }
278
279 6
        return $offset;
280
    }
281
}
282