Completed
Push — master ( 066d89...19607d )
by De
01:54
created

src/Detector.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace Sokil\FraudDetector;
4
5
use Sokil\DataType\PriorityList;
6
use Symfony\Component\EventDispatcher\EventDispatcher;
7
8
class Detector
9
{
10
    const STATE_UNCHECKED   = 'unckecked';
11
    const STATE_PASSED      = 'checkPassed';
12
    const STATE_FAILED      = 'checkFailed';
13
14
    private $state = self::STATE_UNCHECKED;
15
16
    /**
17
     *
18
     * @var mixed key to identify unique user
19
     */
20
    private $key;
21
22
    /**
23
     *
24
     * @var \Sokil\DataType\PriorityList
25
     */
26
    private $processorDeclarationList;
27
28
    private $processorList = array();
29
30
    private $processorNamespaces = array(
31
        '\Sokil\FraudDetector\Processor',
32
    );
33
34
    private $collectorNamespaces = array(
35
        '\Sokil\FraudDetector\Processor\RequestRate\Collector',
36
    );
37
38
    /**
39
     *
40
     * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
41
     */
42
    private $eventDispatcher;
43
44
    public function __construct()
45
    {
46
        $this->processorDeclarationList = new PriorityList();
47
        $this->eventDispatcher = new EventDispatcher();
48
    }
49
50
    /**
51
     * Key that uniquely identify user
52
     * @param type $key
53
     * @return \Sokil\FraudDetector\Detector
54
     */
55
    public function setKey($key)
56
    {
57
        $this->key = $key;
58
        return $this;
59
    }
60
61
    public function getKey()
62
    {
63
        if(!$this->key) {
64
            $this->key = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null;
65
        }
66
67
        return $this->key;
68
    }
69
70
    /**
71
     * Check if request is not fraud
72
     */
73
    public function check()
74
    {
75
        // check all conditions
76
        /* @var $processor \Sokil\FraudDetector\ProcessorInterface */
77
        foreach($this->processorDeclarationList->getKeys() as $processorName) {
78
            $processor = $this->getProcessor($processorName);
79
80
            if($processor->isPassed()) {
81
                $processor->afterCheckPassed();
82
                $this->trigger(self::STATE_PASSED . ':' . $processorName);
83
                $this->state = self::STATE_PASSED;
84
            } else {
85
                $processor->afterCheckFailed();
86
                $this->trigger(self::STATE_FAILED . ':' . $processorName);
87
                $this->state = self::STATE_FAILED;
88
                break;
89
            }
90
        }
91
92
        $this->trigger($this->state);
93
    }
94
95
    public function registerProcessorNamespace($namespace)
96
    {
97
        $this->processorNamespaces[] = rtrim($namespace, '\\');
98
        return $this;
99
    }
100
101
    /**
102
     * Add processor identified by its name.
103
     * If processor already added, it will be replaced by new instance.
104
     *
105
     * @param string $name name of processor
106
     * @param callable $callable configurator callable
107
     * @return \Sokil\FraudDetector\Detector
108
     */
109
    public function declareProcessor($name, $callable = null, $priority = 0)
110
    {
111
        $this->processorDeclarationList->set($name, $callable, $priority);
112
        return $this;
113
    }
114
115
    public function addProcssor($name, ProcessorInterface $processor, $priority = 0)
116
    {
117
        $this->declareProcessor($name, null, $priority);
118
        $this->processorList[$name] = $processor;
119
120
        return $this;
121
    }
122
123
    public function isProcessorDeclared($name)
124
    {
125
        return $this->processorDeclarationList->has($name);
126
    }
127
128
    /**
129
     * Factory method to create new check condition
130
     *
131
     * @param string $name name of check condition
132
     * @return \Sokil\FraudDetector\ProcessorInterface
133
     * @throws \Exception
134
     */
135
    private function getProcessorClassName($name)
136
    {
137
        $className = ucfirst($name) . 'Processor';
138
139 View Code Duplication
        foreach($this->processorNamespaces as $namespace) {
0 ignored issues
show
This code seems to be duplicated across 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...
140
            $fullyQualifiedClassName = $namespace . '\\' . $className;
141
            if(class_exists($fullyQualifiedClassName)) {
142
                return $fullyQualifiedClassName;
143
            }
144
        }
145
146
        throw new \Exception('Class ' . $fullyQualifiedClassName . ' not found');
147
    }
148
149
    public function getProcessor($processorName)
150
    {
151
        if(isset($this->processorList[$processorName])) {
152
            return $this->processorList[$processorName];
153
        }
154
155
        // create processor
156
        $processorClassName = $this->getProcessorClassName($processorName);
157
        $processor =  new $processorClassName($this);
158
159
        if (!($processor instanceof ProcessorInterface)) {
160
            throw new \Exception('Processor must inherit ProcessorInterface');
161
        }
162
163
        // configure processor
164
        $configuratorCallable = $this->processorDeclarationList->get($processorName);
165
        if($configuratorCallable && is_callable($configuratorCallable)) {
166
            call_user_func($configuratorCallable, $processor);
167
        }
168
169
        $this->processorList[$processorName] = $processor;
170
171
        return $processor;
172
    }
173
174
    public function registerCollectorNamespace($namespace)
175
    {
176
        $this->collectorNamespaces[] = rtrim($namespace, '\\');
177
        return $this;
178
    }
179
180
    public function getCollectorClassName($type)
181
    {
182
        if(false == strpos($type, '_')) {
183
            $className = ucfirst($type);
184
        } else {
185
            $className = implode('', array_map('ucfirst', explode('_', $type)));
186
        }
187
188
        $className .= 'Collector';
189
190 View Code Duplication
        foreach($this->collectorNamespaces as $namespace) {
0 ignored issues
show
This code seems to be duplicated across 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
            $fullyQualifiedClassName = $namespace . '\\' . $className;
192
            if(class_exists($fullyQualifiedClassName)) {
193
                return $fullyQualifiedClassName;
194
            }
195
        }
196
197
        throw new \Exception('Class ' . $fullyQualifiedClassName . ' not found');
198
    }
199
200
    private function on($stateName, $callable)
201
    {
202
        if($this->hasState(self::STATE_UNCHECKED)) {
203
            $this->subscribe($stateName, $callable);
204
        } elseif($this->hasState($stateName)) {
205
            call_user_func($callable);
206
        }
207
208
        return $this;
209
    }
210
211
    public function onCheckPassed($callable)
212
    {
213
        $this->on(self::STATE_PASSED, $callable);
214
215
        return $this;
216
    }
217
218
    public function onCheckFailed($callable)
219
    {
220
        $this->on(self::STATE_FAILED, $callable);
221
222
        return $this;
223
    }
224
225
    public function isUnchecked()
226
    {
227
        return $this->hasState(self::STATE_UNCHECKED);
228
    }
229
230
    public function isPassed()
231
    {
232
        return $this->hasState(self::STATE_PASSED);
233
    }
234
235
    public function isFailed()
236
    {
237
        return $this->hasState(self::STATE_FAILED);
238
    }
239
240
    private function hasState($state)
241
    {
242
        return $this->state === $state;
243
    }
244
245
    public function subscribe($eventName, $callable, $priority = 0)
246
    {
247
        $this->eventDispatcher->addListener($eventName, $callable, $priority);
248
        return $this;
249
    }
250
251
    /**
252
     *
253
     * @param string $eventName
254
     * @param mixed $target
255
     * @return \Sokil\FraudDetector\Event
256
     */
257
    public function trigger($eventName, $target = null)
258
    {
259
        $event = new Event();
260
261
        if($target) {
262
            $event->setTarget($target);
263
        }
264
265
        return $this->eventDispatcher->dispatch($eventName, $event);
266
    }
267
}