Completed
Push — master ( 3f0c6a...394e06 )
by De
01:56
created

Detector   B

Complexity

Total Complexity 48

Size/Duplication

Total Lines 341
Duplicated Lines 11.14 %

Coupling/Cohesion

Components 3
Dependencies 2

Importance

Changes 35
Bugs 1 Features 14
Metric Value
wmc 48
c 35
b 1
f 14
lcom 3
cbo 2
dl 38
loc 341
rs 8.4865

25 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A setKey() 0 5 1
A getKey() 0 8 3
B check() 0 25 4
A registerProcessorNamespace() 0 5 1
A declareProcessor() 0 5 1
A addProcssor() 0 7 1
A isProcessorDeclared() 0 4 1
A getProcessorClassName() 0 13 3
B getProcessor() 0 24 5
A registerCollectorNamespace() 0 5 1
A getCollectorClassName() 19 19 4
B createCollector() 0 27 3
A getStorageClassName() 19 19 4
A createStorage() 0 14 2
A registerStorageNamespace() 0 5 1
A on() 0 10 3
A onCheckPassed() 0 6 1
A onCheckFailed() 0 6 1
A isUnchecked() 0 4 1
A isPassed() 0 4 1
A isFailed() 0 4 1
A hasState() 0 4 1
A subscribe() 0 5 1
A trigger() 0 10 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Detector 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 Detector, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Sokil\FraudDetector;
4
5
use Sokil\DataType\PriorityList;
6
use Symfony\Component\EventDispatcher\EventDispatcher;
7
use Sokil\FraudDetector\Processor\ProcessorInterface;
8
use Sokil\FraudDetector\Collector\CollectorInterface;
9
10
class Detector
11
{
12
    const STATE_UNCHECKED   = 'unckecked';
13
    const STATE_PASSED      = 'checkPassed';
14
    const STATE_FAILED      = 'checkFailed';
15
16
    private $state = self::STATE_UNCHECKED;
17
18
    /**
19
     *
20
     * @var mixed key to identify unique user
21
     */
22
    private $key;
23
24
    /**
25
     *
26
     * @var \Sokil\DataType\PriorityList
27
     */
28
    private $processorDeclarationList;
29
30
    private $processorList = array();
31
32
    private $processorNamespaces = array(
33
        '\Sokil\FraudDetector\Processor',
34
    );
35
36
    private $collectorNamespaces = array(
37
        '\Sokil\FraudDetector\Collector',
38
    );
39
40
    private $storageNamespaces = array(
41
        '\Sokil\FraudDetector\Storage',
42
    );
43
44
    /**
45
     *
46
     * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
47
     */
48
    private $eventDispatcher;
49
50
    public function __construct()
51
    {
52
        $this->processorDeclarationList = new PriorityList();
53
        $this->eventDispatcher = new EventDispatcher();
54
    }
55
56
    /**
57
     * Key that uniquely identify user
58
     * @param type $key
59
     * @return \Sokil\FraudDetector\Detector
60
     */
61
    public function setKey($key)
62
    {
63
        $this->key = $key;
64
        return $this;
65
    }
66
67
    public function getKey()
68
    {
69
        if(!$this->key) {
70
            $this->key = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null;
71
        }
72
73
        return $this->key;
74
    }
75
76
    /**
77
     * Check if request is not fraud
78
     */
79
    public function check()
80
    {
81
        // check all conditions
82
        /* @var $processor \Sokil\FraudDetector\ProcessorInterface */
83
        foreach($this->processorDeclarationList->getKeys() as $processorName) {
84
            $processor = $this->getProcessor($processorName);
85
86
            if($processor->isPassed()) {
87
                $processor->afterCheckPassed();
88
                $this->trigger(self::STATE_PASSED . ':' . $processorName);
89
                
90
                // check passed if all processors passes their cheks
91
                if ($this->state === self::STATE_UNCHECKED) {
92
                    $this->state = self::STATE_PASSED;
93
                }
94
            } else {
95
                $processor->afterCheckFailed();
96
                $this->trigger(self::STATE_FAILED . ':' . $processorName);
97
                // if any processor failed - all check failed
98
                $this->state = self::STATE_FAILED;
99
            }
100
        }
101
102
        $this->trigger($this->state);
103
    }
104
105
    public function registerProcessorNamespace($namespace)
106
    {
107
        $this->processorNamespaces[] = rtrim($namespace, '\\');
108
        return $this;
109
    }
110
111
    /**
112
     * Add processor identified by its name.
113
     * If processor already added, it will be replaced by new instance.
114
     *
115
     * @param string $name name of processor
116
     * @param callable $callable configurator callable
117
     * @return \Sokil\FraudDetector\Detector
118
     */
119
    public function declareProcessor($name, $callable = null, $priority = 0)
120
    {
121
        $this->processorDeclarationList->set($name, $callable, $priority);
0 ignored issues
show
Documentation introduced by
$callable is of type callable|null, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
122
        return $this;
123
    }
124
125
    public function addProcssor($name, ProcessorInterface $processor, $priority = 0)
126
    {
127
        $this->declareProcessor($name, null, $priority);
128
        $this->processorList[$name] = $processor;
129
130
        return $this;
131
    }
132
133
    public function isProcessorDeclared($name)
134
    {
135
        return $this->processorDeclarationList->has($name);
136
    }
137
138
    /**
139
     * Factory method to create new check condition
140
     *
141
     * @param string $name name of check condition
142
     * @return \Sokil\FraudDetector\ProcessorInterface
143
     * @throws \Exception
144
     */
145
    private function getProcessorClassName($name)
146
    {
147
        $className = ucfirst($name) . 'Processor';
148
149
        foreach($this->processorNamespaces as $namespace) {
150
            $fullyQualifiedClassName = $namespace . '\\' . $className;
151
            if(class_exists($fullyQualifiedClassName)) {
152
                return $fullyQualifiedClassName;
153
            }
154
        }
155
156
        throw new \Exception('Class ' . $fullyQualifiedClassName . ' not found');
157
    }
158
159
    public function getProcessor($processorName)
160
    {
161
        if(isset($this->processorList[$processorName])) {
162
            return $this->processorList[$processorName];
163
        }
164
165
        // create processor
166
        $processorClassName = $this->getProcessorClassName($processorName);
167
        $processor =  new $processorClassName($this);
168
169
        if (!($processor instanceof ProcessorInterface)) {
170
            throw new \Exception('Processor must inherit ProcessorInterface');
171
        }
172
173
        // configure processor
174
        $configuratorCallable = $this->processorDeclarationList->get($processorName);
175
        if($configuratorCallable && is_callable($configuratorCallable)) {
176
            call_user_func($configuratorCallable, $processor, $this);
177
        }
178
179
        $this->processorList[$processorName] = $processor;
180
181
        return $processor;
182
    }
183
184
    public function registerCollectorNamespace($namespace)
185
    {
186
        $this->collectorNamespaces[] = rtrim($namespace, '\\');
187
        return $this;
188
    }
189
190 View Code Duplication
    private function getCollectorClassName($type)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
191
    {
192
        if(false == strpos($type, '_')) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing strpos($type, '_') of type integer to the boolean false. If you are specifically checking for 0, consider using something more explicit like === 0 instead.
Loading history...
193
            $className = ucfirst($type);
194
        } else {
195
            $className = implode('', array_map('ucfirst', explode('_', $type)));
196
        }
197
198
        $className .= 'Collector';
199
200
        foreach($this->collectorNamespaces as $namespace) {
201
            $fullyQualifiedClassName = $namespace . '\\' . $className;
202
            if(class_exists($fullyQualifiedClassName)) {
203
                return $fullyQualifiedClassName;
204
            }
205
        }
206
207
        throw new \Exception('Class ' . $fullyQualifiedClassName . ' not found');
208
    }
209
210
    /*
211
     * @param int $requestNumber maximum number of allowed requests
212
     * @param int $timeInterval time interval in seconds
213
     */
214
    public function createCollector(
215
        $type,
216
        $namespace,
217
        $requestNumber,
218
        $timeInterval,
219
        $configuratorCallable = null
220
    ) {
221
        $className = $this->getCollectorClassName($type);
222
223
        $collector = new $className(
224
            $this->getKey() . ':' . $namespace,
225
            $requestNumber,
226
            $timeInterval
227
        );
228
229
        if (!($collector instanceof CollectorInterface)) {
230
            throw new \Exception('Collector must inherit CollectorInterface');
231
        }
232
233
        // configure
234
        if(is_callable($configuratorCallable)) {
235
            call_user_func($configuratorCallable, $collector, $this);
236
        }
237
238
        return $collector;
239
240
    }
241
242 View Code Duplication
    private function getStorageClassName($type)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
243
    {
244
        if(false == strpos($type, '_')) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing strpos($type, '_') of type integer to the boolean false. If you are specifically checking for 0, consider using something more explicit like === 0 instead.
Loading history...
245
            $className = ucfirst($type);
246
        } else {
247
            $className = implode('', array_map('ucfirst', explode('_', $type)));
248
        }
249
250
        $className .= 'Storage';
251
252
        foreach($this->storageNamespaces as $namespace) {
253
            $fullyQualifiedClassName = $namespace . '\\' . $className;
254
            if(class_exists($fullyQualifiedClassName)) {
255
                return $fullyQualifiedClassName;
256
            }
257
        }
258
259
        throw new \Exception('Class ' . $fullyQualifiedClassName . ' not found');
260
    }
261
262
    public function createStorage($type, $configuratorCallable = null)
263
    {
264
        $className = $this->getStorageClassName($type);
265
266
        $storage = new $className();
267
268
        // configure
269
        if(is_callable($configuratorCallable)) {
270
            call_user_func($configuratorCallable, $storage);
271
        }
272
273
        return $storage;
274
275
    }
276
277
    public function registerStorageNamespace($namespace)
278
    {
279
        $this->storageNamespaces[] = rtrim($namespace, '\\');
280
        return $this;
281
    }
282
283
    private function on($stateName, $callable)
284
    {
285
        if($this->hasState(self::STATE_UNCHECKED)) {
286
            $this->subscribe($stateName, $callable);
287
        } elseif($this->hasState($stateName)) {
288
            call_user_func($callable);
289
        }
290
291
        return $this;
292
    }
293
294
    public function onCheckPassed($callable)
295
    {
296
        $this->on(self::STATE_PASSED, $callable);
297
298
        return $this;
299
    }
300
301
    public function onCheckFailed($callable)
302
    {
303
        $this->on(self::STATE_FAILED, $callable);
304
305
        return $this;
306
    }
307
308
    public function isUnchecked()
309
    {
310
        return $this->hasState(self::STATE_UNCHECKED);
311
    }
312
313
    public function isPassed()
314
    {
315
        return $this->hasState(self::STATE_PASSED);
316
    }
317
318
    public function isFailed()
319
    {
320
        return $this->hasState(self::STATE_FAILED);
321
    }
322
323
    private function hasState($state)
324
    {
325
        return $this->state === $state;
326
    }
327
328
    public function subscribe($eventName, $callable, $priority = 0)
329
    {
330
        $this->eventDispatcher->addListener($eventName, $callable, $priority);
331
        return $this;
332
    }
333
334
    /**
335
     *
336
     * @param string $eventName
337
     * @param mixed $target
338
     * @return \Sokil\FraudDetector\Event
339
     */
340
    public function trigger($eventName, $target = null)
341
    {
342
        $event = new Event();
343
344
        if($target) {
345
            $event->setTarget($target);
346
        }
347
348
        return $this->eventDispatcher->dispatch($eventName, $event);
349
    }
350
}
351