Completed
Push — master ( 19607d...3f0c6a )
by De
01:50
created

Detector::registerStorageNamespace()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 5
rs 9.4286
cc 1
eloc 3
nc 1
nop 1
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
                $this->state = self::STATE_PASSED;
90
            } else {
91
                $processor->afterCheckFailed();
92
                $this->trigger(self::STATE_FAILED . ':' . $processorName);
93
                $this->state = self::STATE_FAILED;
94
                break;
95
            }
96
        }
97
98
        $this->trigger($this->state);
99
    }
100
101
    public function registerProcessorNamespace($namespace)
102
    {
103
        $this->processorNamespaces[] = rtrim($namespace, '\\');
104
        return $this;
105
    }
106
107
    /**
108
     * Add processor identified by its name.
109
     * If processor already added, it will be replaced by new instance.
110
     *
111
     * @param string $name name of processor
112
     * @param callable $callable configurator callable
113
     * @return \Sokil\FraudDetector\Detector
114
     */
115
    public function declareProcessor($name, $callable = null, $priority = 0)
116
    {
117
        $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...
118
        return $this;
119
    }
120
121
    public function addProcssor($name, ProcessorInterface $processor, $priority = 0)
122
    {
123
        $this->declareProcessor($name, null, $priority);
124
        $this->processorList[$name] = $processor;
125
126
        return $this;
127
    }
128
129
    public function isProcessorDeclared($name)
130
    {
131
        return $this->processorDeclarationList->has($name);
132
    }
133
134
    /**
135
     * Factory method to create new check condition
136
     *
137
     * @param string $name name of check condition
138
     * @return \Sokil\FraudDetector\ProcessorInterface
139
     * @throws \Exception
140
     */
141
    private function getProcessorClassName($name)
142
    {
143
        $className = ucfirst($name) . 'Processor';
144
145
        foreach($this->processorNamespaces as $namespace) {
146
            $fullyQualifiedClassName = $namespace . '\\' . $className;
147
            if(class_exists($fullyQualifiedClassName)) {
148
                return $fullyQualifiedClassName;
149
            }
150
        }
151
152
        throw new \Exception('Class ' . $fullyQualifiedClassName . ' not found');
153
    }
154
155
    public function getProcessor($processorName)
156
    {
157
        if(isset($this->processorList[$processorName])) {
158
            return $this->processorList[$processorName];
159
        }
160
161
        // create processor
162
        $processorClassName = $this->getProcessorClassName($processorName);
163
        $processor =  new $processorClassName($this);
164
165
        if (!($processor instanceof ProcessorInterface)) {
166
            throw new \Exception('Processor must inherit ProcessorInterface');
167
        }
168
169
        // configure processor
170
        $configuratorCallable = $this->processorDeclarationList->get($processorName);
171
        if($configuratorCallable && is_callable($configuratorCallable)) {
172
            call_user_func($configuratorCallable, $processor, $this);
173
        }
174
175
        $this->processorList[$processorName] = $processor;
176
177
        return $processor;
178
    }
179
180
    public function registerCollectorNamespace($namespace)
181
    {
182
        $this->collectorNamespaces[] = rtrim($namespace, '\\');
183
        return $this;
184
    }
185
186 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...
187
    {
188
        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...
189
            $className = ucfirst($type);
190
        } else {
191
            $className = implode('', array_map('ucfirst', explode('_', $type)));
192
        }
193
194
        $className .= 'Collector';
195
196
        foreach($this->collectorNamespaces as $namespace) {
197
            $fullyQualifiedClassName = $namespace . '\\' . $className;
198
            if(class_exists($fullyQualifiedClassName)) {
199
                return $fullyQualifiedClassName;
200
            }
201
        }
202
203
        throw new \Exception('Class ' . $fullyQualifiedClassName . ' not found');
204
    }
205
206
    /*
207
     * @param int $requestNumber maximum number of allowed requests
208
     * @param int $timeInterval time interval in seconds
209
     */
210
    public function createCollector(
211
        $type,
212
        $namespace,
213
        $requestNumber,
214
        $timeInterval,
215
        $configuratorCallable = null
216
    ) {
217
        $className = $this->getCollectorClassName($type);
218
219
        $collector = new $className(
220
            $this->getKey() . ':' . $namespace,
221
            $requestNumber,
222
            $timeInterval
223
        );
224
225
        if (!($collector instanceof CollectorInterface)) {
226
            throw new \Exception('Collector must inherit CollectorInterface');
227
        }
228
229
        // configure
230
        if(is_callable($configuratorCallable)) {
231
            call_user_func($configuratorCallable, $collector, $this);
232
        }
233
234
        return $collector;
235
236
    }
237
238 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...
239
    {
240
        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...
241
            $className = ucfirst($type);
242
        } else {
243
            $className = implode('', array_map('ucfirst', explode('_', $type)));
244
        }
245
246
        $className .= 'Storage';
247
248
        foreach($this->storageNamespaces as $namespace) {
249
            $fullyQualifiedClassName = $namespace . '\\' . $className;
250
            if(class_exists($fullyQualifiedClassName)) {
251
                return $fullyQualifiedClassName;
252
            }
253
        }
254
255
        throw new \Exception('Class ' . $fullyQualifiedClassName . ' not found');
256
    }
257
258
    public function createStorage($type, $configuratorCallable = null)
259
    {
260
        $className = $this->getStorageClassName($type);
261
262
        $storage = new $className();
263
264
        // configure
265
        if(is_callable($configuratorCallable)) {
266
            call_user_func($configuratorCallable, $storage);
267
        }
268
269
        return $storage;
270
271
    }
272
273
    public function registerStorageNamespace($namespace)
274
    {
275
        $this->storageNamespaces[] = rtrim($namespace, '\\');
276
        return $this;
277
    }
278
279
    private function on($stateName, $callable)
280
    {
281
        if($this->hasState(self::STATE_UNCHECKED)) {
282
            $this->subscribe($stateName, $callable);
283
        } elseif($this->hasState($stateName)) {
284
            call_user_func($callable);
285
        }
286
287
        return $this;
288
    }
289
290
    public function onCheckPassed($callable)
291
    {
292
        $this->on(self::STATE_PASSED, $callable);
293
294
        return $this;
295
    }
296
297
    public function onCheckFailed($callable)
298
    {
299
        $this->on(self::STATE_FAILED, $callable);
300
301
        return $this;
302
    }
303
304
    public function isUnchecked()
305
    {
306
        return $this->hasState(self::STATE_UNCHECKED);
307
    }
308
309
    public function isPassed()
310
    {
311
        return $this->hasState(self::STATE_PASSED);
312
    }
313
314
    public function isFailed()
315
    {
316
        return $this->hasState(self::STATE_FAILED);
317
    }
318
319
    private function hasState($state)
320
    {
321
        return $this->state === $state;
322
    }
323
324
    public function subscribe($eventName, $callable, $priority = 0)
325
    {
326
        $this->eventDispatcher->addListener($eventName, $callable, $priority);
327
        return $this;
328
    }
329
330
    /**
331
     *
332
     * @param string $eventName
333
     * @param mixed $target
334
     * @return \Sokil\FraudDetector\Event
335
     */
336
    public function trigger($eventName, $target = null)
337
    {
338
        $event = new Event();
339
340
        if($target) {
341
            $event->setTarget($target);
342
        }
343
344
        return $this->eventDispatcher->dispatch($eventName, $event);
345
    }
346
}