Passed
Push — master ( 40186e...120ad6 )
by bader
02:24
created

Visits::recordCountry()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace awssat\Visits;
4
5
use Carbon\Carbon;
6
use Illuminate\Database\Eloquent\Model;
7
use Illuminate\Support\Facades\Redis;
8
use Spatie\Referer\Referer;
9
10
class Visits
11
{
12
    /**
13
     * @var mixed
14
     */
15
    protected $ipSeconds;
16
    /**
17
     * @var null
18
     */
19
    protected $subject;
20
    /**
21
     * @var bool|mixed
22
     */
23
    protected $fresh = false;
24
    /**
25
     * @var null
26
     */
27
    protected $country = null;
28
    /**
29
     * @var null
30
     */
31
    protected $referer = null;
32
    /**
33
     * @var mixed
34
     */
35
    protected $periods;
36
    /**
37
     * @var Keys
38
     */
39
    protected $keys;
40
    /**
41
     * @var Redis
42
     */
43
    public $redis;
44
45
    /**
46
     * Visits constructor.
47
     * @param $subject
48
     * @param string $tag|null
49
     */
50
    public function __construct($subject = null, $tag = 'visits')
51
    {
52
        $config = config('visits');
53
        $this->redis = Redis::connection($config['connection']);
54
        $this->periods = $config['periods'];
55
        $this->ipSeconds = $config['remember_ip'];
56
        $this->fresh = $config['always_fresh'];
57
        $this->subject = $subject;
58
        $this->keys = new Keys($subject, $tag);
59
60
        $this->periodsSync();
61
    }
62
63
    /**
64
     * @param $attribute
65
     * @return $this
66
     */
67
    public function __get($attribute)
68
    {
69
        if($this->keys->instanceOfModel && $relation = $this->subject->$attribute) {
70
            $this->keys->append($attribute, $relation->{$relation->getKeyName()});
71
        }
72
73
        return $this;
74
    }
75
76
    /**
77
     * @param $subject
78
     * @return $this
79
     */
80
    public function by($subject)
81
    {
82
        if($subject instanceof Model) {
83
            $this->keys->append(strtolower(str_singular(class_basename(get_class($subject)))),
84
                $subject->{$subject->getKeyName()});
85
        } else if (is_array($subject)) {
86
            $subject = collect($subject);
87
            $this->keys->append($subject->flip()->first(), $subject->first());
88
        } else {
89
            $this->keys->append('custom', $subject);
90
        }
91
92
        return $this;
93
    }
94
95
    /**
96
     * Return fresh cache from database
97
     * @return $this
98
     */
99
    public function fresh()
100
    {
101
        $this->fresh = true;
102
103
        return $this;
104
    }
105
106
    /**
107
     * set x seconds for ip expiration
108
     *
109
     * @param $seconds
110
     * @return $this
111
     */
112
    public function seconds($seconds)
113
    {
114
        $this->ipSeconds = $seconds;
115
116
        return $this;
117
    }
118
119
120
    /**
121
     * @param $country
122
     * @return $this
123
     */
124
    public function country($country)
125
    {
126
        $this->country = $country;
127
128
        return $this;
129
    }
130
131
132
    /**
133
     * @param $referer
134
     * @return $this
135
     */
136
    public function referer($referer)
137
    {
138
        $this->referer = $referer;
139
140
        return $this;
141
    }
142
143
    /**
144
     * Change period
145
     *
146
     * @param $period
147
     * @return $this
148
     */
149
    public function period($period)
150
    {
151
        if (in_array($period, array_keys($this->periods))) {
152
            $this->keys->visits = $this->keys->period($period);
153
        }
154
155
        return $this;
156
    }
157
158
    /**
159
     * Sync periods times
160
     */
161
    protected function periodsSync()
162
    {
163
        foreach ($this->periods as $period) {
164
            $periodKey = $this->keys->period($period);
165
166
            if ($this->noExpiration($periodKey)) {
167
                $expireInSeconds = $this->newExpiration($period);
168
                $this->redis->incrby($periodKey . '_total', 0);
169
                $this->redis->zincrby($periodKey, 0, 0);
170
                $this->redis->expire($periodKey, $expireInSeconds);
171
                $this->redis->expire($periodKey . '_total', $expireInSeconds);
172
            }
173
        }
174
    }
175
176
    /**
177
     * @param $periodKey
178
     * @return bool
179
     */
180
    protected function noExpiration($periodKey)
181
    {
182
        return $this->redis->ttl($periodKey) == -1 || !$this->redis->exists($periodKey);
183
    }
184
185
    /**
186
     * @param $period
187
     * @return int
188
     */
189
    protected function newExpiration($period)
190
    {
191
        $expireInSeconds = 0;
192
193
        switch ($period) {
194
            case 'day':
195
                $expireInSeconds = Carbon::now()->endOfDay()->timestamp - Carbon::now()->timestamp;
196
                break;
197
            case 'week':
198
                $expireInSeconds = Carbon::now()->endOfWeek()->timestamp - Carbon::now()->timestamp;
199
                break;
200
            case 'month':
201
                $expireInSeconds = Carbon::now()->endOfMonth()->timestamp - Carbon::now()->timestamp;
202
                break;
203
            case 'year':
204
                $expireInSeconds = Carbon::now()->endOfYear()->timestamp - Carbon::now()->timestamp;
205
                break;
206
        }
207
208
        return $expireInSeconds + 1;
209
    }
210
211
    /**
212
     * Reset methods
213
     *
214
     * @param $method
215
     * @param string $args
216
     * @return Reset
217
     */
218
    public function reset($method = 'visits', $args = '')
219
    {
220
        return new Reset($this, $method, $args);
221
    }
222
223
    /**
224
     * Fetch all time trending subjects.
225
     *
226
     * @param int $limit
227
     * @param bool $isLow
228
     * @return \Illuminate\Support\Collection|array
229
     */
230
    public function top($limit = 5, $isLow = false)
231
    {
232
        $cacheKey = $this->keys->cache($limit, $isLow);
233
        $cachedList = $this->cachedList($limit, $cacheKey);
234
        $visitsIds = $this->getVisits($limit, $this->keys->visits, $isLow);
235
236
        if($visitsIds === $cachedList->pluck('id')->toArray() && ! $this->fresh) {
237
            return $cachedList;
238
        }
239
240
        return $this->freshList($cacheKey, $visitsIds);
241
    }
242
243
244
    /**
245
     * Top/low countries
246
     *
247
     * @param int $limit
248
     * @param bool $isLow
249
     * @return mixed
250
     */
251
    public function countries($limit = -1, $isLow = false)
252
    {
253
        $range = $isLow ? 'zrange' : 'zrevrange';
254
255
        return $this->redis->$range($this->keys->visits . "_countries:{$this->keys->id}", 0, $limit, 'WITHSCORES');
256
    }
257
258
    /**
259
     * top/lows refs
260
     *
261
     * @param int $limit
262
     * @param bool $isLow
263
     * @return mixed
264
     */
265
    public function refs($limit = -1, $isLow = false)
266
    {
267
        $range = $isLow ? 'zrange' : 'zrevrange';
268
269
        return $this->redis->$range($this->keys->visits . "_referers:{$this->keys->id}", 0, $limit, 'WITHSCORES');
270
    }
271
272
    /**
273
     * Fetch lowest subjects.
274
     *
275
     * @param int $limit
276
     * @return \Illuminate\Support\Collection|array
277
     */
278
    public function low($limit = 5)
279
    {
280
        return $this->top($limit, true);
281
    }
282
283
    /**
284
     * Check for the ip is has been recorded before
285
     *
286
     * @return bool
287
     * @internal param $subject
288
     */
289
    public function recordedIp()
290
    {
291
        return ! $this->redis->set($this->keys->ip(request()->ip()), true, 'EX', $this->ipSeconds, 'NX');
292
    }
293
294
    /**
295
     * Get visits of model instance.
296
     *
297
     * @return mixed
298
     * @internal param $subject
299
     */
300
    public function count()
301
    {
302
        if ($this->country) {
303
            return $this->redis->zscore($this->keys->visits . "_countries:{$this->keys->id}", $this->country);
304
        } else if ($this->referer) {
305
            return $this->redis->zscore($this->keys->visits . "_referers:{$this->keys->id}", $this->referer);
306
        }
307
308
        return intval(
309
            (!$this->keys->instanceOfModel) ?
310
                $this->redis->get($this->keys->visits . '_total') :
311
                $this->redis->zscore($this->keys->visits, $this->keys->id)
312
        );
313
    }
314
315
    /**
316
     * use diffForHumans to show diff
317
     * @param $period
318
     * @return Carbon
319
     */
320
    public function timeLeft($period = false)
321
    {
322
        return Carbon::now()->addSeconds($this->redis->ttl(
323
            $period ? $this->keys->period($period) : $this->keys->ip(request()->ip())
324
        ));
325
    }
326
327
328
    /**
329
     * @param $inc
330
     */
331
    protected function recordCountry($inc)
332
    {
333
        $this->redis->zincrby($this->keys->visits . "_countries:{$this->keys->id}", $inc, $this->getCountry());
334
    }
335
336
    /**
337
     * @param $inc
338
     */
339
    protected function recordRefer($inc)
340
    {
341
        $referer = app(Referer::class)->get();
342
        $this->redis->zincrby($this->keys->visits . "_referers:{$this->keys->id}", $inc, $referer);
343
    }
344
345
    /**
346
     * @param $inc
347
     */
348
    protected function recordPeriods($inc)
349
    {
350
        foreach ($this->periods as $period) {
351
            $periodKey = $this->keys->period($period);
352
353
            $this->redis->zincrby($periodKey, $inc, $this->keys->id);
354
            $this->redis->incrby($periodKey . '_total', $inc);
355
        }
356
    }
357
358
    /**
359
     * Increment a new/old subject to the cache cache.
360
     *
361
     * @param int $inc
362
     * @param bool $force
363
     * @param bool $periods
364
     * @param bool $country
365
     * @param bool $refer
366
     */
367
    public function increment($inc = 1, $force = false, $periods = true, $country = true, $refer = true)
368
    {
369
        if ($force || !$this->recordedIp()) {
370
            $this->redis->zincrby($this->keys->visits, $inc, $this->keys->id);
371
            $this->redis->incrby($this->keys->visits . '_total', $inc);
372
373
            foreach (['country', 'refer', 'periods'] as $method) {
374
                $$method && $this->{'record'. studly_case($method)}($inc);
375
            }
376
        }
377
    }
378
379
380
    /**
381
     * @param int $inc
382
     * @param bool $periods
383
     */
384
    public function forceIncrement($inc = 1, $periods = true)
385
    {
386
        $this->increment($inc, true, $periods);
387
    }
388
389
    /**
390
     * Decrement a new/old subject to the cache cache.
391
     *
392
     * @param int $dec
393
     * @param bool $force
394
     */
395
    public function decrement($dec = 1, $force = false)
396
    {
397
        $this->increment(-$dec, $force);
398
    }
399
400
    /**
401
     * @param int $dec
402
     * @param bool $periods
403
     */
404
    public function forceDecrement($dec = 1, $periods = true)
405
    {
406
        $this->increment(-$dec, true, $periods);
407
    }
408
409
    /**
410
     * @param $limit
411
     * @param $visitsKey
412
     * @param bool $isLow
413
     * @return mixed
414
     */
415
    protected function getVisits($limit, $visitsKey, $isLow = false)
416
    {
417
        $range = $isLow ? 'zrange' : 'zrevrange';
418
419
        return array_map('intval', $this->redis->$range($visitsKey, 0, $limit - 1));
420
    }
421
422
    /**
423
     * @param $cacheKey
424
     * @param $visitsIds
425
     * @return mixed
426
     */
427
    protected function freshList($cacheKey, $visitsIds)
428
    {
429
        if (count($visitsIds)) {
430
            $this->redis->del($cacheKey);
431
432
            return ($this->subject)::whereIn($this->keys->primary, $visitsIds)
433
                ->get()
434
                ->sortBy(function ($subject) use ($visitsIds) {
435
                    return array_search($subject->{$this->keys->primary}, $visitsIds);
436
                })->each(function ($subject) use ($cacheKey) {
437
                    $this->redis->rpush($cacheKey, serialize($subject));
438
                });
439
        }
440
441
        return [];
442
    }
443
444
    /**
445
     * @param $limit
446
     * @param $cacheKey
447
     * @return \Illuminate\Support\Collection|array
448
     */
449
    protected function cachedList($limit, $cacheKey)
450
    {
451
        return collect(
452
            array_map('unserialize', $this->redis->lrange($cacheKey, 0, $limit - 1))
453
        );
454
    }
455
456
457
    /**
458
     *  Gets visitor country code
459
     * @return mixed|string
460
     */
461
    public function getCountry()
462
    {
463
        return strtolower(geoip()->getLocation()->iso_code);
464
    }
465
466
    /**
467
     * @param $period®
468
     * @param int $time
469
     * @return bool
470
     */
471
    public function expireAt($period, $time)
472
    {
473
        $periodKey = $this->keys->period($period);
474
        return $this->redis->expire($periodKey, $time);
475
    }
476
}
477