From e773092872e6a6c732cd0e72480f75b8d91639ac Mon Sep 17 00:00:00 2001 From: Abdulrahman Date: Sun, 13 Oct 2019 19:33:32 +0300 Subject: [PATCH 1/5] adds support of multiple data engines --- .gitignore | 2 + .styleci.yml | 0 .travis.yml | 0 CHANGELOG.md | 6 + ISSUE_TEMPLATE.md | 0 LICENSE.md | 0 README.md | 9 +- composer.json | 28 +-- .../migrations/create_visits_table.php.stub | 37 ++++ phpunit.xml | 0 src/DataEngines/DataEngine.php | 31 +++ src/DataEngines/EloquentEngine.php | 204 ++++++++++++++++++ src/DataEngines/RedisEngine.php | 126 +++++++++++ src/Keys.php | 51 +---- src/Models/Visit.php | 13 ++ src/Reset.php | 57 +++-- src/Traits/Lists.php | 65 ++---- src/Traits/Periods.php | 24 +-- src/Traits/Record.php | 15 +- src/Traits/Setters.php | 2 +- src/Visits.php | 105 +++++---- src/VisitsServiceProvider.php | 8 +- src/config/visits.php | 35 ++- src/helpers.php | 2 +- tests/Feature/EloquentPeriodsTest.php | 22 ++ tests/Feature/EloquentVisitsTest.php | 22 ++ .../{PeriodsTest.php => PeriodsTestCase.php} | 81 +++---- tests/Feature/RedisPeriodsTest.php | 37 ++++ tests/Feature/RedisVisitsTest.php | 36 ++++ .../{VisitsTest.php => VisitsTestCase.php} | 86 ++++---- tests/TestCase.php | 20 +- 31 files changed, 809 insertions(+), 315 deletions(-) mode change 100644 => 100755 .gitignore mode change 100644 => 100755 .styleci.yml mode change 100644 => 100755 .travis.yml mode change 100644 => 100755 CHANGELOG.md mode change 100644 => 100755 ISSUE_TEMPLATE.md mode change 100644 => 100755 LICENSE.md mode change 100644 => 100755 README.md mode change 100644 => 100755 composer.json create mode 100755 database/migrations/create_visits_table.php.stub mode change 100644 => 100755 phpunit.xml create mode 100755 src/DataEngines/DataEngine.php create mode 100755 src/DataEngines/EloquentEngine.php create mode 100755 src/DataEngines/RedisEngine.php mode change 100644 => 100755 src/Keys.php create mode 100755 src/Models/Visit.php mode change 100644 => 100755 src/Reset.php mode change 100644 => 100755 src/Traits/Lists.php mode change 100644 => 100755 src/Traits/Periods.php mode change 100644 => 100755 src/Traits/Record.php mode change 100644 => 100755 src/Traits/Setters.php mode change 100644 => 100755 src/Visits.php mode change 100644 => 100755 src/VisitsServiceProvider.php mode change 100644 => 100755 src/config/visits.php mode change 100644 => 100755 src/helpers.php create mode 100755 tests/Feature/EloquentPeriodsTest.php create mode 100755 tests/Feature/EloquentVisitsTest.php rename tests/Feature/{PeriodsTest.php => PeriodsTestCase.php} (67%) mode change 100644 => 100755 create mode 100755 tests/Feature/RedisPeriodsTest.php create mode 100755 tests/Feature/RedisVisitsTest.php rename tests/Feature/{VisitsTest.php => VisitsTestCase.php} (79%) mode change 100644 => 100755 mode change 100644 => 100755 tests/TestCase.php diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index e1252c3..56f0897 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ composer.lock .env .DS_Store /.vscode +.phpunit.result.cache +dump.rdb diff --git a/.styleci.yml b/.styleci.yml old mode 100644 new mode 100755 diff --git a/.travis.yml b/.travis.yml old mode 100644 new mode 100755 diff --git a/CHANGELOG.md b/CHANGELOG.md old mode 100644 new mode 100755 index a01cebc..46fcd02 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All Notable changes to `laravel-visits` will be documented in this file. Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) principles. +## 2.1.0 + +- Rewrites huge part of the package to support multiple data engines. +- Adds database's data engine support (Eloquent). + + ## 2.0.0 - Global ignore feature (can be enabled from [config/visits.php](https://github.com/awssat/laravel-visits/blob/master/src/config/visits.php#L70)) diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md old mode 100644 new mode 100755 diff --git a/LICENSE.md b/LICENSE.md old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 41626e7..f4634c5 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ Laravel Visits is a counter that can be attached to any model to track its visit - Get Top/Lowest visits per a model. - Get most visited countries, refs, OSes, and languages ... - Get visits per a period of time like a month of a year of an item or model. +- Supports multiple data engines: Redis or database (any SQL engine that Eloquent supports). ## Install Via Composer @@ -49,8 +50,9 @@ composer require awssat/laravel-visits #### Requirement - Laravel 5.5+ - PHP 7.1+ -- This package rely heavly on Redis. To use it, make sure that Redis is configured and ready. (see [Laravel Redis Configuration](https://laravel.com/docs/5.6/redis#configuration)) - +- Data engines options (can be configured from config/visits.php): + - Redis: make sure that Redis is configured and ready. (see [Laravel Redis Configuration](https://laravel.com/docs/5.6/redis#configuration)) + - Database: publish migration file: `php artisan vendor:publish --provider="Awssat\Visits\VisitsServiceProvider" --tag="migrations"` then migrate. #### Config @@ -77,11 +79,8 @@ To prvent any data loss add a new conection on `config/database.php` ``` and you can define your redis connection name on `config/visits.php` - ``` php - 'connection' => 'default' // to 'laravel-visits' - ``` diff --git a/composer.json b/composer.json old mode 100644 new mode 100755 index 1af2be2..59e3580 --- a/composer.json +++ b/composer.json @@ -18,24 +18,28 @@ } ], "require": { - "php": "~7.1", - "illuminate/support": "~5.5||^6.0", + "php": "~7.2", + "illuminate/support": "~5.5.0 || ~5.6.0 || ~5.7.0 || ~5.8.0 || ^6.0", "jaybizzle/crawler-detect": "^1.2", - "predis/predis": "^1.1", "spatie/laravel-referer": "^1.3", "torann/geoip": "^1.0" }, "require-dev": { - "illuminate/database": "~5.5||^6.0", - "orchestra/testbench": "~3.0", - "mockery/mockery": "^0.9.5 || ^1.0", - "fzaninotto/faker": "^1.6", "doctrine/dbal": "^2.5", - "phpunit/phpunit": "^6.1" + "fzaninotto/faker": "^1.6", + "illuminate/support": "~5.5.0 || ~5.6.0 || ~5.7.0 || ~5.8.0 || ^6.0", + "mockery/mockery": "^1.2", + "orchestra/testbench": "^3.5 || ^3.6 || ^3.7 || ^3.8 || ^4.0", + "predis/predis": "^1.1" + }, + "suggest": { + "predis/predis": "Needed if you are using redis as data engine of laravel-visits", + "ext-redis": "Needed if you are using redis as engine data of laravel-visits", + "illuminate/database": "Needed if you are using database as engine data of laravel-visits" }, "autoload": { "psr-4": { - "awssat\\Visits\\": "src" + "Awssat\\Visits\\": "src" }, "files": [ "src/helpers.php" @@ -43,7 +47,7 @@ }, "autoload-dev": { "psr-4": { - "awssat\\Visits\\Tests\\": "tests" + "Awssat\\Visits\\Tests\\": "tests" } }, "scripts": { @@ -57,10 +61,10 @@ }, "laravel": { "providers": [ - "awssat\\Visits\\VisitsServiceProvider" + "Awssat\\Visits\\VisitsServiceProvider" ], "aliases": { - "Visits": "awssat\\Visits\\Visits" + "Visits": "Awssat\\Visits\\Visits" } } }, diff --git a/database/migrations/create_visits_table.php.stub b/database/migrations/create_visits_table.php.stub new file mode 100755 index 0000000..30d5361 --- /dev/null +++ b/database/migrations/create_visits_table.php.stub @@ -0,0 +1,37 @@ +bigIncrements('id'); + $table->string('primary_key'); + $table->string('secondary_key')->nullable(); + $table->unsignedBigInteger('score'); + $table->json('list')->nullable(); + $table->timestamp('expired_at')->nullable(); + $table->timestamps(); + $table->unique(['primary_key', 'secondary_key']); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('visits'); + } +} diff --git a/phpunit.xml b/phpunit.xml old mode 100644 new mode 100755 diff --git a/src/DataEngines/DataEngine.php b/src/DataEngines/DataEngine.php new file mode 100755 index 0000000..f1ebb21 --- /dev/null +++ b/src/DataEngines/DataEngine.php @@ -0,0 +1,31 @@ +model = $model; + } + + public function connect(string $connection): DataEngine + { + return $this; + } + + public function setPrefix(string $prefix): DataEngine + { + $this->prefix = $prefix . ':'; + return $this; + } + + public function increment(string $key, int $value, ?string $member = null): bool + { + if (! empty($member) || is_numeric($member)) { + $row = $this->model->firstOrNew(['primary_key' => $this->prefix.$key, 'secondary_key' => $member]); + } else { + $row = $this->model->firstOrNew(['primary_key' => $this->prefix.$key, 'secondary_key' => null]); + } + + if($row->expired_at !== null && \Carbon\Carbon::now()->gt($row->expired_at)) { + $row->score = $value; + $row->expired_at = null; + } else { + $row->score += $value; + } + + return $row->save(); + } + + public function decrement(string $key, int $value, ?string $member = null): bool + { + return $this->increment($key, -$value, $member); + } + + public function delete($key, ?string $member = null): bool + { + if(is_array($key)) { + array_walk($key, function($item) { + $this->delete($item); + }); + return true; + } + + if(! empty($member) || is_numeric($member)) { + return $this->model->where(['primary_key' => $this->prefix.$key, 'secondary_key' => $member])->delete(); + } else { + return $this->model->where(['primary_key' => $this->prefix.$key])->delete(); + } + } + + public function get(string $key, ?string $member = null) + { + if(! empty($member) || is_numeric($member)) { + return $this->model->where(['primary_key' => $this->prefix.$key, 'secondary_key' => $member]) + ->where(function($q) { + return $q->where('expired_at', '>', \Carbon\Carbon::now())->orWhereNull('expired_at'); + }) + ->value('score'); + } else { + return $this->model->where(['primary_key' => $this->prefix.$key, 'secondary_key' => null]) + ->where(function($q) { + return $q->where('expired_at', '>', \Carbon\Carbon::now())->orWhereNull('expired_at'); + }) + ->value('score'); + } + } + + public function set(string $key, $value, ?string $member = null): bool + { + if(! empty($member) || is_numeric($member)) { + return $this->model->create([ + 'primary_key' => $this->prefix.$key, + 'secondary_key' => $member, + 'score' => $value, + ]) instanceof Model; + } else { + return $this->model->create([ + 'primary_key' => $this->prefix.$key, + 'score' => $value, + ]) instanceof Model; + } + } + + public function search(string $word, bool $noPrefix = true): array + { + $results = []; + + if($word == '*') { + $results = $this->model + ->where(function($q) { + return $q->where('expired_at', '>', \Carbon\Carbon::now())->orWhereNull('expired_at'); + }) + ->pluck('primary_key'); + } else { + $results = $this->model->where('primary_key', 'like', $this->prefix.str_replace('*', '%', $word)) + ->where(function($q) { + return $q->where('expired_at', '>', \Carbon\Carbon::now())->orWhereNull('expired_at'); + }) + ->pluck('primary_key'); + } + + return array_map( + function($item) use($noPrefix) { + if ($noPrefix && substr($item, 0, strlen($this->prefix)) == $this->prefix) { + return substr($item, strlen($this->prefix)); + } + + return $item; + }, + $results->toArray() ?? [] + ); + } + + public function flatList(string $key, int $limit = -1): array + { + return array_slice( + $this->model->where(['primary_key' => $this->prefix.$key, 'secondary_key' => null]) + ->where(function($q) { + return $q->where('expired_at', '>', \Carbon\Carbon::now())->orWhereNull('expired_at'); + }) + ->value('list') ?? [], 0, $limit + ); + } + + public function addToFlatList(string $key, $value): bool + { + $row = $this->model->firstOrNew(['primary_key' => $this->prefix.$key, 'secondary_key' => null]); + + if($row->expired_at !== null && \Carbon\Carbon::now()->gt($row->expired_at)) { + $row->list = (array) $value; + $row->expired_at = null; + } else { + $row->list = array_merge($row->list ?? [], (array) $value); + } + + $row->score = $row->score ?? 0; + return (bool) $row->save(); + } + + public function valueList(string $key, int $limit = -1, bool $orderByAsc = false, bool $withValues = false): array + { + $rows = $this->model->where('primary_key', $this->prefix.$key) + ->where(function($q) { + return $q->where('expired_at', '>', \Carbon\Carbon::now())->orWhereNull('expired_at'); + }) + // ->where('score', '>', 0) + ->whereNotNull('secondary_key') + ->orderBy('score', $orderByAsc ? 'asc' : 'desc') + ->when($limit > -1, function($q) use($limit) { + return $q->limit($limit+1); + })->pluck('score', 'secondary_key') ?? \Illuminate\Support\Collection::make(); + + return $withValues ? $rows->toArray() : array_keys($rows->toArray()); + } + + public function exists(string $key): bool + { + return $this->model->where(['primary_key' => $this->prefix.$key, 'secondary_key' => null]) + ->where(function($q) { + return $q->where('expired_at', '>', \Carbon\Carbon::now())->orWhereNull('expired_at'); + }) + ->exists(); + } + + public function timeLeft(string $key): int + { + $expired_at = $this->model->where(['primary_key' => $this->prefix.$key])->value('expired_at'); + + if($expired_at === null) { + return -2; + } + + $ttl = $expired_at->timestamp - \Carbon\Carbon::now()->timestamp; + return $ttl <= 0 ? -1 : $ttl; + } + + public function setExpiration(string $key, int $time): bool + { + $time = \Carbon\Carbon::now()->addSeconds($time); + + return $this->model->where(['primary_key' => $this->prefix.$key]) + ->where(function($q) { + return $q->where('expired_at', '>', \Carbon\Carbon::now())->orWhereNull('expired_at'); + }) + ->update([ + 'expired_at' => $time + ]); + } +} \ No newline at end of file diff --git a/src/DataEngines/RedisEngine.php b/src/DataEngines/RedisEngine.php new file mode 100755 index 0000000..37e398d --- /dev/null +++ b/src/DataEngines/RedisEngine.php @@ -0,0 +1,126 @@ +redis = $redis; + } + + public function connect(string $connection): DataEngine + { + $this->connection = $this->redis->connection($connection); + return $this; + } + + public function setPrefix(string $prefix): DataEngine + { + $this->prefix = $prefix . ':'; + return $this; + } + + public function increment(string $key, int $value, ?string $member = null): bool + { + if (! empty($member) || is_numeric($member)) { + $this->connection->zincrby($this->prefix.$key, $value, $member); + } else { + $this->connection->incrby($this->prefix.$key, $value); + } + + // both methods returns integer and raise an excpetion in case of an error. + return true; + } + + public function decrement(string $key, int $value, ?string $member = null): bool + { + return $this->increment($key, -$value, $member); + } + + public function delete($key, ?string $member = null): bool + { + if(is_array($key)) { + array_walk($key, function($item) { + $this->delete($item); + }); + return true; + } + + if(! empty($member) || is_numeric($member)) { + return $this->connection->zrem($this->prefix.$key, $member) > 0; + } else { + return $this->connection->del($this->prefix.$key) > 0; + } + } + + public function get(string $key, ?string $member = null) + { + if(! empty($member) || is_numeric($member)) { + return $this->connection->zscore($this->prefix.$key, $member); + } else { + return $this->connection->get($this->prefix.$key); + } + } + + public function set(string $key, $value, ?string $member = null): bool + { + if(! empty($member) || is_numeric($member)) { + return $this->connection->zAdd($this->prefix.$key, $value, $member) > 0; + } else { + return (bool) $this->connection->set($this->prefix.$key, $value); + } + } + + public function search(string $word, bool $noPrefix = true): array + { + return array_map( + function($item) use($noPrefix) { + if ($noPrefix && substr($item, 0, strlen($this->prefix)) == $this->prefix) { + return substr($item, strlen($this->prefix)); + } + + return $item; + }, + $this->connection->keys($this->prefix.$word) ?? [] + ); + } + + public function flatList(string $key, int $limit = -1): array + { + return $this->connection->lrange($this->prefix.$key, 0, $limit); + } + + public function addToFlatList(string $key, $value): bool + { + return $this->connection->rpush($this->prefix.$key, $value) !== false; + } + + public function valueList(string $key, int $limit = -1, bool $orderByAsc = false, bool $withValues = false): array + { + $range = $orderByAsc ? 'zrange' : 'zrevrange'; + + return $this->connection->$range($this->prefix.$key, 0, $limit, ['withscores' => $withValues]); + } + + public function exists(string $key): bool + { + return (bool) $this->connection->exists($this->prefix.$key); + } + + public function timeLeft(string $key): int + { + return $this->connection->ttl($this->prefix.$key); + } + + public function setExpiration(string $key, int $time): bool + { + return $this->connection->expire($this->prefix.$key, $time); + } +} \ No newline at end of file diff --git a/src/Keys.php b/src/Keys.php old mode 100644 new mode 100755 index 689f61a..4a82ec3 --- a/src/Keys.php +++ b/src/Keys.php @@ -1,14 +1,12 @@ modelName = $this->pluralModelName($subject); - $this->prefix = config('visits.redis_keys_prefix'); - $this->testing = app()->environment('testing') ? 'testing:' : ''; $this->primary = (new $subject)->getKeyName(); $this->tag = $tag; $this->visits = $this->visits(); @@ -39,18 +30,14 @@ public function __construct($subject, $tag) /** * Get cache key - * - * @return string */ public function visits() { - return "{$this->prefix}:$this->testing" . $this->modelName . "_{$this->tag}"; + return (app()->environment('testing') ? 'testing:' : '').$this->modelName."_{$this->tag}"; } /** - * Get cache key - * - * @return string + * Get cache key for total values */ public function visitsTotal() { @@ -58,63 +45,47 @@ public function visitsTotal() } /** - * @param $ip - * @return string + * ip key */ public function ip($ip) { - return $this->visits . '_' . - snake_case("recorded_ips:" . ($this->instanceOfModel ? "{$this->id}:" : '') . $ip); + return $this->visits.'_'.Str::snake( + 'recorded_ips:'.($this->instanceOfModel ? "{$this->id}:" : '') . $ip + ); } - /** - * @param $limit - * @param $isLow - * @return string + * list cache key */ public function cache($limit = '*', $isLow = false) { - $key = $this->visits . "_lists"; + $key = $this->visits.'_lists'; if ($limit == '*') { return "{$key}:*"; } - return "{$key}:" . ($isLow ? "low" : "top") . "{$limit}"; + return "{$key}:".($isLow ? 'low' : 'top').$limit; } /** - * @param $period - * @return string + * period key */ public function period($period) { return "{$this->visits}_{$period}"; } - /** - * @param $relation - * @param $id - */ public function append($relation, $id) { $this->visits .= "_{$relation}_{$id}"; } - /** - * @param $subject - * @return string - */ public function modelName($subject) { return strtolower(Str::singular(class_basename(get_class($subject)))); } - /** - * @param $subject - * @return string - */ public function pluralModelName($subject) { return strtolower(Str::plural(class_basename(is_string($subject) ? $subject : get_class($subject)))); diff --git a/src/Models/Visit.php b/src/Models/Visit.php new file mode 100755 index 0000000..ee0b5f6 --- /dev/null +++ b/src/Models/Visit.php @@ -0,0 +1,13 @@ + 'array']; + protected $dates = ['expired_at']; + +} diff --git a/src/Reset.php b/src/Reset.php old mode 100644 new mode 100755 index 2d963e4..571ccfe --- a/src/Reset.php +++ b/src/Reset.php @@ -1,18 +1,11 @@ subject); @@ -29,7 +22,6 @@ public function __construct(Visits $parent, $method, $args) /** * Reset everything - * */ public function factory() { @@ -49,57 +41,55 @@ public function factory() public function visits() { if ($this->keys->id) { - $this->redis->zrem($this->keys->visits, $this->keys->id); - $this->redis->del($this->keys->visits . "_countries:{$this->keys->id}"); - $this->redis->del($this->keys->visits . "_referers:{$this->keys->id}"); - $this->redis->del($this->keys->visits . "_OSes:{$this->keys->id}"); - $this->redis->del($this->keys->visits . "_languages:{$this->keys->id}"); + $this->connection->delete($this->keys->visits, $this->keys->id); + foreach (['countries', 'referers', 'OSes', 'languages'] as $item) { + $this->connection->delete($this->keys->visits."_{$item}:{$this->keys->id}"); + } foreach ($this->periods as $period => $_) { - $this->redis->zrem($this->keys->period($period), $this->keys->id); + $this->connection->delete($this->keys->period($period), $this->keys->id); } $this->ips(); } else { - $this->redis->del($this->keys->visits); - $this->redis->del($this->keys->visits . '_total'); + $this->connection->delete($this->keys->visits); + $this->connection->delete($this->keys->visits.'_total'); } - } public function allrefs() { - $cc = $this->redis->keys($this->keys->visits . '_referers:*'); + $cc = $this->connection->search($this->keys->visits.'_referers:*'); if (count($cc)) { - $this->redis->del($cc); + $this->connection->delete($cc); } } public function allOperatingSystems() { - $cc = $this->redis->keys($this->keys->visits . '_OSes:*'); + $cc = $this->connection->search($this->keys->visits.'_OSes:*'); if (count($cc)) { - $this->redis->del($cc); + $this->connection->delete($cc); } } public function allLanguages() { - $cc = $this->redis->keys($this->keys->visits . '_languages:*'); + $cc = $this->connection->search($this->keys->visits.'_languages:*'); if (count($cc)) { - $this->redis->del($cc); + $this->connection->delete($cc); } } public function allcountries() { - $cc = $this->redis->keys($this->keys->visits . '_countries:*'); + $cc = $this->connection->search($this->keys->visits.'_countries:*'); if (count($cc)) { - $this->redis->del($cc); + $this->connection->delete($cc); } } @@ -108,10 +98,10 @@ public function allcountries() */ public function periods() { - foreach ($this->periods as $period => $days) { + foreach ($this->periods as $period => $_) { $periodKey = $this->keys->period($period); - $this->redis->del($periodKey); - $this->redis->del($periodKey . '_total'); + $this->connection->delete($periodKey); + $this->connection->delete($periodKey.'_total'); } } @@ -121,10 +111,10 @@ public function periods() */ public function ips($ips = '*') { - $ips = $this->redis->keys($this->keys->ip($ips)); + $ips = $this->connection->search($this->keys->ip($ips)); if (count($ips)) { - $this->redis->del($ips); + $this->connection->delete($ips); } } @@ -133,9 +123,10 @@ public function ips($ips = '*') */ public function lists() { - $lists = $this->redis->keys($this->keys->cache()); + $lists = $this->connection->search($this->keys->cache()); + if (count($lists)) { - $this->redis->del($lists); + $this->connection->delete($lists); } } } diff --git a/src/Traits/Lists.php b/src/Traits/Lists.php old mode 100644 new mode 100755 index 1424eb0..a6156ae --- a/src/Traits/Lists.php +++ b/src/Traits/Lists.php @@ -1,6 +1,6 @@ keys->cache($limit, $isLow); + $cacheKey = $this->keys->cache($limit, $orderByAsc); $cachedList = $this->cachedList($limit, $cacheKey); - $visitsIds = $this->getVisitsIds($limit, $this->keys->visits, $isLow); + $visitsIds = $this->getVisitsIds($limit, $this->keys->visits, $orderByAsc); if($visitsIds === $cachedList->pluck($this->keys->primary)->toArray() && ! $this->fresh) { return $cachedList; @@ -29,58 +29,40 @@ public function top($limit = 5, $isLow = false) /** * Top/low countries - * - * @param int $limit - * @param bool $isLow - * @return mixed */ - public function countries($limit = -1, $isLow = false) + public function countries($limit = -1, $orderByAsc = false) { - $range = $isLow ? 'zrange' : 'zrevrange'; - - return $this->redis->$range($this->keys->visits . "_countries:{$this->keys->id}", 0, $limit, 'WITHSCORES'); + return $this->getSortedList('countries', $limit, $orderByAsc, true); } /** * top/lows refs - * - * @param int $limit - * @param bool $isLow - * @return mixed */ - public function refs($limit = -1, $isLow = false) + public function refs($limit = -1, $orderByAsc = false) { - $range = $isLow ? 'zrange' : 'zrevrange'; - - return $this->redis->$range($this->keys->visits . "_referers:{$this->keys->id}", 0, $limit, 'WITHSCORES'); + return $this->getSortedList('referers', $limit, $orderByAsc, true); } /** * top/lows operating systems - * - * @param int $limit - * @param bool $isLow - * @return mixed */ - public function operatingSystems($limit = -1, $isLow = false) + public function operatingSystems($limit = -1, $orderByAsc = false) { - $range = $isLow ? 'zrange' : 'zrevrange'; - - return $this->redis->$range($this->keys->visits . "_OSes:{$this->keys->id}", 0, $limit, 'WITHSCORES'); + return $this->getSortedList('OSes', $limit, $orderByAsc, true); } /** * top/lows languages - * - * @param int $limit - * @param bool $isLow - * @return mixed */ - public function languages($limit = -1, $isLow = false) + public function languages($limit = -1, $orderByAsc = false) { - $range = $isLow ? 'zrange' : 'zrevrange'; + return $this->getSortedList('languages', $limit, $orderByAsc, true); + } + - return $this->redis->$range($this->keys->visits . "_languages:{$this->keys->id}", 0, $limit, 'WITHSCORES'); + protected function getSortedList($name, $limit, $orderByAsc = false, $withValues = true) + { + return $this->connection->valueList($this->keys->visits . "_{$name}:{$this->keys->id}", $limit, $orderByAsc, $withValues); } /** @@ -101,11 +83,9 @@ public function low($limit = 5) * @param bool $isLow * @return mixed */ - protected function getVisitsIds($limit, $visitsKey, $isLow = false) + protected function getVisitsIds($limit, $visitsKey, $orderByAsc = false) { - $range = $isLow ? 'zrange' : 'zrevrange'; - - return array_map('intval', $this->redis->$range($visitsKey, 0, $limit - 1)); + return array_map('intval', $this->connection->valueList($visitsKey, $limit - 1, $orderByAsc)); } /** @@ -116,14 +96,15 @@ protected function getVisitsIds($limit, $visitsKey, $isLow = false) protected function freshList($cacheKey, $visitsIds) { if (count($visitsIds)) { - $this->redis->del($cacheKey); + + $this->connection->delete($cacheKey); return ($this->subject)::whereIn($this->keys->primary, $visitsIds) ->get() ->sortBy(function ($subject) use ($visitsIds) { return array_search($subject->{$this->keys->primary}, $visitsIds); })->each(function ($subject) use ($cacheKey) { - $this->redis->rpush($cacheKey, serialize($subject)); + $this->connection->addToFlatList($cacheKey, serialize($subject)); }); } @@ -138,7 +119,7 @@ protected function freshList($cacheKey, $visitsIds) protected function cachedList($limit, $cacheKey) { return Collection::make( - array_map('unserialize', $this->redis->lrange($cacheKey, 0, $limit - 1)) + array_map('unserialize', $this->connection->flatList($cacheKey, $limit)) ); } } diff --git a/src/Traits/Periods.php b/src/Traits/Periods.php old mode 100644 new mode 100755 index c85ccd4..f1e52ef --- a/src/Traits/Periods.php +++ b/src/Traits/Periods.php @@ -1,9 +1,10 @@ noExpiration($periodKey)) { $expireInSeconds = $this->newExpiration($period); - $this->redis->incrby($periodKey . '_total', 0); - $this->redis->zincrby($periodKey, 0, 0); - $this->redis->expire($periodKey, $expireInSeconds); - $this->redis->expire($periodKey . '_total', $expireInSeconds); + $this->connection->increment($periodKey.'_total', 0); + $this->connection->increment($periodKey, 0, 0); + $this->connection->setExpiration($periodKey, $expireInSeconds); + $this->connection->setExpiration($periodKey.'_total', $expireInSeconds); } } } - /** - * @param $periodKey - * @return bool - */ protected function noExpiration($periodKey) { - return $this->redis->ttl($periodKey) == -1 || !$this->redis->exists($periodKey); + return $this->connection->timeLeft($periodKey) == -1 || ! $this->connection->exists($periodKey); } - /** - * @param $period - * @return int - * @throws Exception - */ protected function newExpiration($period) { try { - $periodCarbon = $this->xHoursPeriod($period) ?? Carbon::now()->{'endOf' . studly_case($period)}(); + $periodCarbon = $this->xHoursPeriod($period) ?? Carbon::now()->{'endOf' . Str::studly($period)}(); } catch (Exception $e) { throw new Exception("Wrong period: `{$period}`! please update config/visits.php file."); } diff --git a/src/Traits/Record.php b/src/Traits/Record.php old mode 100644 new mode 100755 index 84cd5c8..7789674 --- a/src/Traits/Record.php +++ b/src/Traits/Record.php @@ -1,6 +1,6 @@ redis->zincrby($this->keys->visits."_countries:{$this->keys->id}", $inc, $this->getVisitorCountry()); + $this->connection->increment($this->keys->visits."_countries:{$this->keys->id}", $inc, $this->getVisitorCountry()); } /** @@ -20,7 +20,7 @@ protected function recordCountry($inc) protected function recordRefer($inc) { $referer = app(Referer::class)->get(); - $this->redis->zincrby($this->keys->visits."_referers:{$this->keys->id}", $inc, $referer); + $this->connection->increment($this->keys->visits."_referers:{$this->keys->id}", $inc, $referer); } /** @@ -28,7 +28,7 @@ protected function recordRefer($inc) */ protected function recordOperatingSystem($inc) { - $this->redis->zincrby($this->keys->visits."_OSes:{$this->keys->id}", $inc, $this->getVisitorOperatingSystem()); + $this->connection->increment($this->keys->visits."_OSes:{$this->keys->id}", $inc, $this->getVisitorOperatingSystem()); } /** @@ -36,7 +36,7 @@ protected function recordOperatingSystem($inc) */ protected function recordLanguage($inc) { - $this->redis->zincrby($this->keys->visits."_languages:{$this->keys->id}", $inc, $this->getVisitorLanguage()); + $this->connection->increment($this->keys->visits."_languages:{$this->keys->id}", $inc, $this->getVisitorLanguage()); } /** @@ -47,8 +47,8 @@ protected function recordPeriods($inc) foreach ($this->periods as $period) { $periodKey = $this->keys->period($period); - $this->redis->zincrby($periodKey, $inc, $this->keys->id); - $this->redis->incrby($periodKey.'_total', $inc); + $this->connection->increment($periodKey, $inc, $this->keys->id); + $this->connection->increment($periodKey.'_total', $inc); } } @@ -94,7 +94,6 @@ public function getVisitorOperatingSystem() */ public function getVisitorLanguage() { - return request()->getPreferredLanguage(); } } diff --git a/src/Traits/Setters.php b/src/Traits/Setters.php old mode 100644 new mode 100755 index 32575c6..3cc7255 --- a/src/Traits/Setters.php +++ b/src/Traits/Setters.php @@ -1,6 +1,6 @@ redis = Redis::connection($config['connection']); + + $this->connection = $this->determineConnection($config['engine'] ?? 'redis') + ->connect($config['connection']) + ->setPrefix($config['keys_prefix']); + + if(! $this->connection) { + return; + } + $this->periods = $config['periods']; $this->ipSeconds = $config['remember_ip']; $this->fresh = $config['always_fresh']; $this->ignoreCrawlers = $config['ignore_crawlers']; $this->globalIgnore = $config['global_ignore']; $this->subject = $subject; - $this->keys = new Keys($subject, $tag); + $this->keys = new Keys($subject, preg_replace('/[^a-z0-9_]/i', '', $tag)); $this->periodsSync(); } + protected function determineConnection($name) + { + $connections = [ + 'redis' => \Awssat\Visits\DataEngines\RedisEngine::class, + 'eloquent' => \Awssat\Visits\DataEngines\EloquentEngine::class + ]; + + if(! array_key_exists($name, $connections)) { + throw new Exception("(Laravel-Visits) The selected engine `{$name}` is not supported! Please correct this issue from config/visits.php."); + } + + return app($connections[$name]); + } + /** * @param $subject - * @return $this + * @return self */ public function by($subject) { @@ -105,7 +124,7 @@ public function by($subject) * * @param $method * @param string $args - * @return Reset + * @return \Awssat\Visits\Reset */ public function reset($method = 'visits', $args = '') { @@ -114,56 +133,57 @@ public function reset($method = 'visits', $args = '') /** * Check for the ip is has been recorded before - * * @return bool - * @internal param $subject */ public function recordedIp() { - return ! $this->redis->set($this->keys->ip(request()->ip()), true, 'EX', $this->ipSeconds, 'NX'); + if(! $this->connection->exists($this->keys->ip(request()->ip()))) { + $this->connection->set($this->keys->ip(request()->ip()), true); + $this->connection->setExpiration($this->keys->ip(request()->ip()), $this->ipSeconds); + + return false; + } + + return true; } /** - * Get visits of model instance. - * + * Get visits of model incount(stance. * @return mixed - * @internal param $subject */ public function count() { if ($this->country) { - return $this->redis->zscore($this->keys->visits . "_countries:{$this->keys->id}", $this->country); + return $this->connection->get($this->keys->visits."_countries:{$this->keys->id}", $this->country); } else if ($this->referer) { - return $this->redis->zscore($this->keys->visits . "_referers:{$this->keys->id}", $this->referer); + return $this->connection->get($this->keys->visits."_referers:{$this->keys->id}", $this->referer); } else if ($this->operatingSystem) { - return $this->redis->zscore($this->keys->visits . "_OSes:{$this->keys->id}", $this->operatingSystem); + return $this->connection->get($this->keys->visits."_OSes:{$this->keys->id}", $this->operatingSystem); } else if ($this->language) { - return $this->redis->zscore($this->keys->visits . "_languages:{$this->keys->id}", $this->language); + return $this->connection->get($this->keys->visits."_languages:{$this->keys->id}", $this->language); } return intval( - $this->keys->instanceOfModel ? - $this->redis->zscore($this->keys->visits, $this->keys->id) : - $this->redis->get($this->keys->visitsTotal()) + $this->keys->instanceOfModel + ? $this->connection->get($this->keys->visits, $this->keys->id) + : $this->connection->get($this->keys->visitsTotal()) ); } /** - * use diffForHumans to show diff - * @return Carbon + * @return integer time left in seconds */ public function timeLeft() { - return Carbon::now()->addSeconds($this->redis->ttl($this->keys->visits)); + return $this->connection->timeLeft($this->keys->visits); } /** - * use diffForHumans to show diff - * @return Carbon + * @return integer time left in seconds */ public function ipTimeLeft() { - return Carbon::now()->addSeconds($this->redis->ttl($this->keys->ip(request()->ip()))); + return $this->connection->timeLeft($this->keys->ip(request()->ip())); } protected function isCrawler() @@ -172,18 +192,15 @@ protected function isCrawler() } /** - * Increment a new/old subject to the cache. - * - * @param int $inc - * @param bool $force - * @param bool $periods + * @param int $inc value to increment + * @param bool $force force increment, skip time limit * @param array $ignore to ignore recording visits of periods, country, refer, language and operatingSystem. pass them on this array. */ public function increment($inc = 1, $force = false, $ignore = []) { if ($force || (!$this->isCrawler() && !$this->recordedIp())) { - $this->redis->zincrby($this->keys->visits, $inc, $this->keys->id); - $this->redis->incrby($this->keys->visitsTotal(), $inc); + $this->connection->increment($this->keys->visits, $inc, $this->keys->id); + $this->connection->increment($this->keys->visitsTotal(), $inc); if(is_array($this->globalIgnore) && sizeof($this->globalIgnore) > 0) { $ignore = array_merge($ignore, $this->globalIgnore); @@ -192,7 +209,7 @@ public function increment($inc = 1, $force = false, $ignore = []) //NOTE: $$method is parameter also .. ($periods,$country,$refer) foreach (['country', 'refer', 'periods', 'operatingSystem', 'language'] as $method) { if(! in_array($method, $ignore)) { - $this->{'record'.studly_case($method)}($inc); + $this->{'record'.Str::studly($method)}($inc); } } } @@ -235,6 +252,6 @@ public function forceDecrement($dec = 1, $ignore = []) public function expireAt($period, $time) { $periodKey = $this->keys->period($period); - return $this->redis->expire($periodKey, $time); + return $this->connection->setExpiration($periodKey, $time); } } diff --git a/src/VisitsServiceProvider.php b/src/VisitsServiceProvider.php old mode 100644 new mode 100755 index 649c224..f136bce --- a/src/VisitsServiceProvider.php +++ b/src/VisitsServiceProvider.php @@ -1,6 +1,6 @@ config_path('visits.php'), ], 'config'); + if (! class_exists('CreateVisitsTable')) { + $this->publishes([ + __DIR__.'/../database/migrations/create_visits_table.php.stub' => database_path('migrations/'.date('Y_m_d_His', time()).'_create_visits_table.php'), + ], 'migrations'); + } + Carbon::macro('endOfxHours', function ($xhours) { if ($xhours > 12) { throw new \Exception('12 is the maximum period in xHours feature'); diff --git a/src/config/visits.php b/src/config/visits.php old mode 100644 new mode 100755 index b277555..6ee44d8 --- a/src/config/visits.php +++ b/src/config/visits.php @@ -1,17 +1,28 @@ 'redis', + 'connection' => 'laravel-visits', + /* |-------------------------------------------------------------------------- | Counters periods |-------------------------------------------------------------------------- | - | Set time in days for each periods counter , you can leave it blank if you like + | Record visits (total) of each one of these periods in this set (can be empty) | */ 'periods' => [ - 'day', 'week', 'month', @@ -23,44 +34,32 @@ | Redis prefix |-------------------------------------------------------------------------- */ - 'redis_keys_prefix' => 'visits', + 'keys_prefix' => 'visits', /* |-------------------------------------------------------------------------- | Remember ip for x seconds of time |-------------------------------------------------------------------------- | - | Prevent counts duplication by remembering each ip has visited the page for x seconds. - | Visits from same ip will be counted after ip expire + | Will count only one visit of an IP during this specified time. | */ 'remember_ip' => 15 * 60, /* |-------------------------------------------------------------------------- - | Always make fresh top/low lists + | Always return uncached fresh top/low lists |-------------------------------------------------------------------------- */ 'always_fresh' => false, - /* - |-------------------------------------------------------------------------- - | Redis Database Connection Name - |-------------------------------------------------------------------------- - | - | When using "redis" you may specify a - | connection that should be used to manage your database storage. This should - | correspond to a connection in your database configuration options. - | - */ - 'connection' => 'laravel-visits', /* |-------------------------------------------------------------------------- | Ignore Crawlers |-------------------------------------------------------------------------- | - | Doesn't count crawlers visits + | Ignore counting crawlers visits or not | */ 'ignore_crawlers' => true, diff --git a/src/helpers.php b/src/helpers.php old mode 100644 new mode 100755 index 1e3477f..cf5d438 --- a/src/helpers.php +++ b/src/helpers.php @@ -4,6 +4,6 @@ { function visits($subject, $tag = 'visits') { - return new \awssat\Visits\Visits($subject, $tag); + return new \Awssat\Visits\Visits($subject, $tag); } } diff --git a/tests/Feature/EloquentPeriodsTest.php b/tests/Feature/EloquentPeriodsTest.php new file mode 100755 index 0000000..cb6cdad --- /dev/null +++ b/tests/Feature/EloquentPeriodsTest.php @@ -0,0 +1,22 @@ +app['config']['visits.engine'] = 'eloquent'; + $this->connection = app(\Awssat\Visits\DataEngines\EloquentEngine::class) + ->setPrefix($this->app['config']['visits.keys_prefix']); + include_once __DIR__.'/../../database/migrations/create_visits_table.php.stub'; + (new \CreateVisitsTable())->up(); + } +} diff --git a/tests/Feature/EloquentVisitsTest.php b/tests/Feature/EloquentVisitsTest.php new file mode 100755 index 0000000..a6ab895 --- /dev/null +++ b/tests/Feature/EloquentVisitsTest.php @@ -0,0 +1,22 @@ +app['config']['visits.engine'] = 'eloquent'; + $this->connection = app(\Awssat\Visits\DataEngines\EloquentEngine::class) + ->setPrefix($this->app['config']['visits.keys_prefix']); + include_once __DIR__.'/../../database/migrations/create_visits_table.php.stub'; + (new \CreateVisitsTable())->up(); + } +} diff --git a/tests/Feature/PeriodsTest.php b/tests/Feature/PeriodsTestCase.php old mode 100644 new mode 100755 similarity index 67% rename from tests/Feature/PeriodsTest.php rename to tests/Feature/PeriodsTestCase.php index e4d1101..90316d8 --- a/tests/Feature/PeriodsTest.php +++ b/tests/Feature/PeriodsTestCase.php @@ -1,20 +1,18 @@ period('3hours')->count(), ]); - sleep(1); + Carbon::setTestNow(now()->addSeconds(1)); + sleep(1);//for redis $this->assertEquals([1, 0], [ visits($post)->count(), @@ -48,9 +47,9 @@ public function x_hours_periods() /** @test */ public function day_test() { - $time = Carbon::now(); - - Carbon::setTestNow($time->endOfDay()); + Carbon::setTestNow( + $time = Carbon::now()->endOfDay() + ); $post = Post::create()->fresh(); @@ -60,18 +59,20 @@ public function day_test() $this->assertEquals([1, 1, 1], [ visits($post)->count(), visits($post)->period('day')->count(), - visits('awssat\Visits\Tests\Post')->period('day')->count(), + visits('Awssat\Visits\Tests\Post')->period('day')->count(), ]); //time until redis delete periods - $this->assertEquals(1, visits($post)->period('day') - ->timeLeft()->diffInSeconds()); + $this->assertEquals(1, visits($post)->period('day')->timeLeft()); - $this->assertEquals(1, visits('awssat\Visits\Tests\Post') - ->period('day')->timeLeft()->diffInSeconds()); + $this->assertEquals( + 1, + visits('Awssat\Visits\Tests\Post')->period('day')->timeLeft() + ); //after seconds it should be empty for week and day - sleep(1); + Carbon::setTestNow(Carbon::now()->addSeconds(1)); + sleep(1); //for redfis $this->assertEquals([1, 0,], [ visits($post)->count(), @@ -81,7 +82,6 @@ public function day_test() //he came after a 5 minute later Carbon::setTestNow(Carbon::now()->addMinutes(5)); - sleep(1); visits($post)->forceIncrement(); @@ -92,10 +92,10 @@ public function day_test() //time until redis delete periods - $this->assertEquals(1, visits($post)->period('day')->timeLeft()->diffInDays($time)); + $this->assertEquals(1, now()->addSeconds(visits($post)->period('day')->timeLeft())->diffInDays($time)); //time until redis delete periods - $this->assertEquals(1, visits('awssat\Visits\Tests\Post')->period('day')->timeLeft()->diffInDays($time)); + $this->assertEquals(1, now()->addSeconds(visits('Awssat\Visits\Tests\Post')->period('day')->timeLeft())->diffInDays($time)); } /** @test */ @@ -118,7 +118,9 @@ public function all_periods() ]); //after seconds it should be empty for week and day - sleep(1); + Carbon::setTestNow(Carbon::now()->addSeconds(1)); + sleep(1); //for redis + $this->assertEquals([1, 0, 0, 1, 1], [ visits($post)->count(), visits($post)->period('day')->count(), @@ -130,7 +132,7 @@ public function all_periods() //he came after a 5 minute later Carbon::setTestNow(Carbon::now()->endOfWeek()->addHours(1)); - sleep(1); + visits($post)->forceIncrement(); $this->assertEquals([2, 1, 1, 2, 2], [ @@ -158,36 +160,37 @@ public function total_periods() //it should be there fo breif of time $this->assertEquals([2, 2, 2, 2, 2], [ - visits('awssat\Visits\Tests\Post')->count(), - visits('awssat\Visits\Tests\Post')->period('day')->count(), - visits('awssat\Visits\Tests\Post')->period('week')->count(), - visits('awssat\Visits\Tests\Post')->period('month')->count(), - visits('awssat\Visits\Tests\Post')->period('year')->count() + visits('Awssat\Visits\Tests\Post')->count(), + visits('Awssat\Visits\Tests\Post')->period('day')->count(), + visits('Awssat\Visits\Tests\Post')->period('week')->count(), + visits('Awssat\Visits\Tests\Post')->period('month')->count(), + visits('Awssat\Visits\Tests\Post')->period('year')->count() ]); //after seconds it should be empty for week and day - sleep(1); + Carbon::setTestNow(Carbon::now()->addSeconds(1)); + sleep(1); //for redis + $this->assertEquals([2, 0, 0, 2, 2], [ - visits('awssat\Visits\Tests\Post')->count(), - visits('awssat\Visits\Tests\Post')->period('day')->count(), - visits('awssat\Visits\Tests\Post')->period('week')->count(), - visits('awssat\Visits\Tests\Post')->period('month')->count(), - visits('awssat\Visits\Tests\Post')->period('year')->count() + visits('Awssat\Visits\Tests\Post')->count(), + visits('Awssat\Visits\Tests\Post')->period('day')->count(), + visits('Awssat\Visits\Tests\Post')->period('week')->count(), + visits('Awssat\Visits\Tests\Post')->period('month')->count(), + visits('Awssat\Visits\Tests\Post')->period('year')->count() ]); //he came after a 5 minute later Carbon::setTestNow(Carbon::now()->endOfWeek()->addHours(1)); - sleep(1); visits($post2)->forceIncrement(); visits($post2)->forceIncrement(); $this->assertEquals([4, 2, 2, 4, 4], [ - visits('awssat\Visits\Tests\Post')->count(), - visits('awssat\Visits\Tests\Post')->period('day')->count(), - visits('awssat\Visits\Tests\Post')->period('week')->count(), - visits('awssat\Visits\Tests\Post')->period('month')->count(), - visits('awssat\Visits\Tests\Post')->period('year')->count() + visits('Awssat\Visits\Tests\Post')->count(), + visits('Awssat\Visits\Tests\Post')->period('day')->count(), + visits('Awssat\Visits\Tests\Post')->period('week')->count(), + visits('Awssat\Visits\Tests\Post')->period('month')->count(), + visits('Awssat\Visits\Tests\Post')->period('year')->count() ]); } } diff --git a/tests/Feature/RedisPeriodsTest.php b/tests/Feature/RedisPeriodsTest.php new file mode 100755 index 0000000..3d400e5 --- /dev/null +++ b/tests/Feature/RedisPeriodsTest.php @@ -0,0 +1,37 @@ +app['config']['database.redis.client'] = 'predis'; // phpredis also works + $this->app['config']['database.redis.options.prefix'] = ''; + $this->app['config']['database.redis.laravel-visits'] = [ + 'host' => env('REDIS_HOST', 'localhost'), + 'password' => env('REDIS_PASSWORD', null), + 'port' => env('REDIS_PORT', 6379), + 'database' => 3, + ]; + + $this->redis = Redis::connection('laravel-visits'); + + if (count($keys = $this->redis->keys($this->app['config']['visits.keys_prefix'].':testing:*'))) { + $this->redis->del($keys); + } + + + $this->connection = app(\Awssat\Visits\DataEngines\RedisEngine::class) + ->connect($this->app['config']['visits.connection']) + ->setPrefix($this->app['config']['visits.keys_prefix']); + } +} diff --git a/tests/Feature/RedisVisitsTest.php b/tests/Feature/RedisVisitsTest.php new file mode 100755 index 0000000..4604d27 --- /dev/null +++ b/tests/Feature/RedisVisitsTest.php @@ -0,0 +1,36 @@ +app['config']['database.redis.client'] = 'predis'; // phpredis also works + $this->app['config']['database.redis.options.prefix'] = ''; + $this->app['config']['database.redis.laravel-visits'] = [ + 'host' => env('REDIS_HOST', 'localhost'), + 'password' => env('REDIS_PASSWORD', null), + 'port' => env('REDIS_PORT', 6379), + 'database' => 3, + ]; + + $this->redis = Redis::connection('laravel-visits'); + + if (count($keys = $this->redis->keys($this->app['config']['visits.keys_prefix'].':testing:*'))) { + $this->redis->del($keys); + } + + + $this->connection = app(\Awssat\Visits\DataEngines\RedisEngine::class) + ->connect($this->app['config']['visits.connection']) + ->setPrefix($this->app['config']['visits.keys_prefix']); + } +} diff --git a/tests/Feature/VisitsTest.php b/tests/Feature/VisitsTestCase.php old mode 100644 new mode 100755 similarity index 79% rename from tests/Feature/VisitsTest.php rename to tests/Feature/VisitsTestCase.php index fb1ec9f..9f6503b --- a/tests/Feature/VisitsTest.php +++ b/tests/Feature/VisitsTestCase.php @@ -1,20 +1,20 @@ by($user)->increment(); } - $top_visits_overall = visits('awssat\Visits\Tests\Post') + $top_visits_overall = visits('Awssat\Visits\Tests\Post') ->top(10) ->toArray(); - $this->assertEmpty($top_visits_overall); - $top_visits = visits('awssat\Visits\Tests\Post') + $top_visits = visits('Awssat\Visits\Tests\Post') ->by($user) ->top(20) ->toArray(); @@ -82,11 +81,11 @@ public function multi_tags_storing() visits($userA, 'clicks')->increment(); visits($userA, 'clicks2')->increment(); - $keys = $this->redis->keys('visits:testing:*'); + $keys = $this->connection->search('testing:*'); - $this->assertContains('visits:testing:posts_visits', $keys); - $this->assertContains('visits:testing:posts_clicks', $keys); - $this->assertContains('visits:testing:posts_clicks2', $keys); + $this->assertContains('testing:posts_visits', $keys); + $this->assertContains('testing:posts_clicks', $keys); + $this->assertContains('testing:posts_clicks2', $keys); } /** @test */ @@ -98,7 +97,7 @@ public function multi_tags_visits() visits($userA, 'clicks')->increment(); - $this->assertEquals([1, 1, ], [visits($userA)->count(), visits($userA, 'clicks')->count()]); + $this->assertEquals([1, 1], [visits($userA)->count(), visits($userA, 'clicks')->count()]); } /** @test */ @@ -114,7 +113,7 @@ public function referer_test() visits($Post)->forceIncrement(10); - $this->assertEquals(['twitter.com' => 10, 'google.com' => 1, ], visits($Post)->refs()); + $this->assertEquals(['twitter.com' => 10, 'google.com' => 1], visits($Post)->refs()); } /** @test */ @@ -196,16 +195,15 @@ public function it_reset_counter() $post3 = Post::create()->fresh(); visits($post1)->increment(10); + visits($post1)->reset(); visits($post2)->increment(5); - visits($post3)->increment(); - visits($post1)->reset(); $this->assertEquals( [2, 3], - visits('awssat\Visits\Tests\Post')->top(5)->pluck('id')->toArray() + visits('Awssat\Visits\Tests\Post')->top(5)->pluck('id')->toArray() ); } @@ -222,10 +220,16 @@ public function reset_specific_ip() '129.0.0.2', '124.0.0.2' ]; - $key = 'visits:testing:posts_visits_recorded_ips:1:'; + + $prefix = 'testing:posts_visits_'; + $key = $prefix.'recorded_ips:1:'; foreach ($ips as $ip) { - $this->redis->set($key.$ip, true, 'EX', 15 * 60, 'NX'); + if(! $this->connection->exists($key.$ip)) { + $this->connection->set($key.$ip, true); + } else { + $this->connection->setExpiration($key.$ip, 15*60); + } } visits($post)->increment(10); @@ -237,13 +241,14 @@ public function reset_specific_ip() visits($post)->reset('ips', '127.0.0.1'); - $ips_in_redis = Collection::make($this->redis->keys(config('visits.redis_keys_prefix').':testing:posts_visits_recorded_ips:*'))->map(function ($ip) use ($key) { - return str_replace($key, '', $ip); - }); + $ips_in_db = Collection::make($this->connection->search($prefix.'recorded_ips:*')) + ->map(function ($ip){ + return substr($ip, strrpos($ip, ':') + 1); + }); - $this->assertArrayNotHasKey( + $this->assertNotContains( '127.0.0.1', - $ips_in_redis->toArray() + $ips_in_db ); visits($post)->increment(10); @@ -279,32 +284,33 @@ public function it_shows_proper_tops_and_lows() $this->assertEquals( Collection::make($arr)->sort()->reverse()->keys()->take(10)->toArray(), - visits('awssat\Visits\Tests\Post')->period('day')->top(10)->pluck('id')->toArray() + visits('Awssat\Visits\Tests\Post')->period('day')->top(10)->pluck('id')->toArray() ); $this->assertEquals( Collection::make($arr)->sort()->keys()->take(10)->toArray(), - visits('awssat\Visits\Tests\Post')->period('day')->low(11)->pluck('id')->toArray() + visits('Awssat\Visits\Tests\Post')->period('day')->low(11)->pluck('id')->toArray() ); - visits('awssat\Visits\Tests\Post')->period('day')->reset(); + visits('Awssat\Visits\Tests\Post')->period('day')->reset(); $this->assertEquals( 0, - visits('awssat\Visits\Tests\Post')->period('day')->count() + visits('Awssat\Visits\Tests\Post')->period('day')->count() ); + // dd(visits('Awssat\Visits\Tests\Post')->period('day')->top(10)); $this->assertEmpty( - visits('awssat\Visits\Tests\Post')->period('day')->top(10) + visits('Awssat\Visits\Tests\Post')->period('day')->top(10) ); $this->assertNotEmpty( - visits('awssat\Visits\Tests\Post')->top(10) + visits('Awssat\Visits\Tests\Post')->top(10) ); $this->assertEquals( Collection::make($arr)->sum(), - visits('awssat\Visits\Tests\Post')->count() + visits('Awssat\Visits\Tests\Post')->count() ); } @@ -365,7 +371,9 @@ public function it_only_record_ip_for_amount_of_time() visits($post)->seconds(1)->increment(); - sleep(visits($post)->ipTimeLeft()->diffInSeconds() + 1); + Carbon::setTestNow(Carbon::now()->addSeconds(visits($post)->ipTimeLeft() + 1)); + sleep(1);//for redis + visits($post)->increment(); @@ -382,7 +390,7 @@ public function n_minus_1_bug() visits($post)->forceIncrement(); } - $list = visits('awssat\Visits\Tests\Post')->top(5)->pluck('name'); + $list = visits('Awssat\Visits\Tests\Post')->top(5)->pluck('name'); $this->assertEquals(5, $list->count()); } @@ -404,15 +412,15 @@ public function it_list_from_cache() visits($post3)->forceIncrement(2); visits($post4)->forceIncrement(1); - $fresh = visits('awssat\Visits\Tests\Post')->top()->pluck('name'); + $fresh = visits('Awssat\Visits\Tests\Post')->top()->pluck('name'); $post5->update(['name' => 'changed']); - $cached = visits('awssat\Visits\Tests\Post')->top()->pluck('name'); + $cached = visits('Awssat\Visits\Tests\Post')->top()->pluck('name'); $this->assertEquals($fresh->first(), $cached->first()); - $fresh2 = visits('awssat\Visits\Tests\Post') + $fresh2 = visits('Awssat\Visits\Tests\Post') ->fresh() ->top() ->pluck('name'); diff --git a/tests/TestCase.php b/tests/TestCase.php old mode 100644 new mode 100755 index c579911..9717f55 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -1,10 +1,10 @@ referer = $this->app['referer']; $this->runTestMigrations(); - - $this->app['config']['database.redis.laravel-visits'] = [ - 'host' => env('REDIS_HOST', 'localhost'), - 'password' => env('REDIS_PASSWORD', null), - 'port' => env('REDIS_PORT', 6379), - 'database' => 3, - ]; - - $this->redis = Redis::connection('laravel-visits'); - - if (count($cc = $this->redis->keys('visits:testing:*'))) { - $this->redis->del($cc); - } } From 74ac357dfb839550f55e7fb3a4773f6f0834091b Mon Sep 17 00:00:00 2001 From: Abdulrahman Date: Sun, 13 Oct 2019 19:43:21 +0300 Subject: [PATCH 2/5] fallback to old config --- src/Visits.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Visits.php b/src/Visits.php index d844dd5..a4f2703 100755 --- a/src/Visits.php +++ b/src/Visits.php @@ -73,7 +73,7 @@ public function __construct($subject = null, $tag = 'visits') $this->connection = $this->determineConnection($config['engine'] ?? 'redis') ->connect($config['connection']) - ->setPrefix($config['keys_prefix']); + ->setPrefix($config['keys_prefix'] ?? $config['redis_keys_prefix'] ?? 'visits'); if(! $this->connection) { return; From cdd952e9f7fe96ba95ffe57b65f5fe12c24c6c93 Mon Sep 17 00:00:00 2001 From: Abdulrahman Date: Sun, 13 Oct 2019 20:08:50 +0300 Subject: [PATCH 3/5] add clean command --- src/Commands/CleanCommand.php | 32 ++++++++++++++++++++++++++++++++ src/VisitsServiceProvider.php | 7 +++++++ 2 files changed, 39 insertions(+) create mode 100644 src/Commands/CleanCommand.php diff --git a/src/Commands/CleanCommand.php b/src/Commands/CleanCommand.php new file mode 100644 index 0000000..35ac875 --- /dev/null +++ b/src/Commands/CleanCommand.php @@ -0,0 +1,32 @@ +cleanEloquent(); + } + } + + protected function cleanEloquent() + { + Visit::where('expired_at', '<', \Carbon\Carbon::now())->delete(); + } +} diff --git a/src/VisitsServiceProvider.php b/src/VisitsServiceProvider.php index f136bce..01befe6 100755 --- a/src/VisitsServiceProvider.php +++ b/src/VisitsServiceProvider.php @@ -2,6 +2,7 @@ namespace Awssat\Visits; +use Awssat\Visits\Commands\CleanCommand; use Illuminate\Support\Carbon; use Illuminate\Support\ServiceProvider; @@ -49,5 +50,11 @@ public function register() __DIR__.'/config/visits.php', 'visits' ); + + $this->app->bind('command.visits:clean', CleanCommand::class); + + $this->commands([ + 'command.visits:clean', + ]); } } From 551a1f6fccfbfd2408f346a0c45806938da96347 Mon Sep 17 00:00:00 2001 From: Abdulrahman Date: Sun, 13 Oct 2019 21:43:54 +0300 Subject: [PATCH 4/5] fix travis --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 87106c1..b907e49 100755 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,7 @@ language: php php: - - 7.0 - - 7.1 - 7.2 + - 7.3 before_script: - composer self-update - composer install --prefer-source --no-interaction From d270e5eac48ef1d6629e4bee35875a2ea859d15e Mon Sep 17 00:00:00 2001 From: Abdulrahman Date: Mon, 14 Oct 2019 15:06:33 +0300 Subject: [PATCH 5/5] fix conflicts --- .github/ISSUE_TEMPLATE/1_Bug_report.md | 15 ++ README.md | 196 ++++++++++++++----------- src/Traits/Periods.php | 2 +- src/Traits/Record.php | 20 ++- 4 files changed, 146 insertions(+), 87 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/1_Bug_report.md diff --git a/.github/ISSUE_TEMPLATE/1_Bug_report.md b/.github/ISSUE_TEMPLATE/1_Bug_report.md new file mode 100644 index 0000000..e689c86 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1_Bug_report.md @@ -0,0 +1,15 @@ +--- +name: "🐛 Bug Report" +about: Report a general package issue + +--- + +- Operating system and version (e.g. Ubuntu 16.04, Windows 7): +- Package Version: #.#.# +- Laravel Version: #.#.# +- Link to your project: + +### Description: + + +### Steps To Reproduce: diff --git a/README.md b/README.md index f4634c5..6e6f6b3 100755 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ ## Introduction -Laravel Visits is a counter that can be attached to any model to track its visits with useful features like IP-protection and lists caching +Laravel Visits is a counter that can be attached to any model to track its visits with useful features like IP-protection and lists caching. ## Table of Contents @@ -26,7 +26,7 @@ Laravel Visits is a counter that can be attached to any model to track its visit * [Top or Lowest list per model type](#top-or-lowest-list-per-model-type) * [Reset and clear values](#reset-and-clear-values) * [Integration with Eloquent](#integration-with-eloquent) - * [Change log](#change-log) + * [Changelog](#changelog) * [Contributing](#contributing) * [Credits](#credits) * [License](#license) @@ -38,56 +38,60 @@ Laravel Visits is a counter that can be attached to any model to track its visit - It's not limitd to one type of Model (like some packages that allow only User model). - Record per visitors and not by vistis using IP detecting, so even with refresh, visit won't duplicate (can be changed from config). - Get Top/Lowest visits per a model. -- Get most visited countries, refs, OSes, and languages ... +- Get most visited countries, refs, OSes, and languages. - Get visits per a period of time like a month of a year of an item or model. - Supports multiple data engines: Redis or database (any SQL engine that Eloquent supports). + ## Install -Via Composer -``` bash +To get started with Laravel Visits, use Composer to add the package to your project's dependencies: +```bash composer require awssat/laravel-visits ``` #### Requirement - Laravel 5.5+ -- PHP 7.1+ +- PHP 7.2+ - Data engines options (can be configured from config/visits.php): - Redis: make sure that Redis is configured and ready. (see [Laravel Redis Configuration](https://laravel.com/docs/5.6/redis#configuration)) - Database: publish migration file: `php artisan vendor:publish --provider="Awssat\Visits\VisitsServiceProvider" --tag="migrations"` then migrate. + #### Config To adjust the package to your needs, you can publish the config file to your project's config folder using: -``` + +```bash php artisan vendor:publish --provider="awssat\Visits\VisitsServiceProvider" ``` -### Note : Redis Database Name +> **Note** : Redis Database Name - By default `laravel-visits` doesn't use the default laravel redis configuration (see [issue #5](https://github.com/awssat/laravel-visits/issues/5)) -To prvent any data loss add a new conection on `config/database.php` - -``` php - - 'laravel-visits' => [ - 'host' => env('REDIS_HOST', '127.0.0.1'), - 'password' => env('REDIS_PASSWORD', null), - 'port' => env('REDIS_PORT', 6379), - 'database' => 3, // anything from 1 to 15, except 0 (or what is set in default) - ], +To prevent any data loss add a new connection on `config/database.php` +```php +'laravel-visits' => [ + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'password' => env('REDIS_PASSWORD', null), + 'port' => env('REDIS_PORT', 6379), + 'database' => 3, // anything from 1 to 15, except 0 (or what is set in default) +], ``` and you can define your redis connection name on `config/visits.php` -``` php +```php + 'connection' => 'default' // to 'laravel-visits' ``` ## Usage -It's simple. Using `visits` helper as: -``` +It's simple. +Using `visits` helper as: + +```php visits($model)->{method}() ``` Where: @@ -95,131 +99,127 @@ Where: - **{method}**: any method that is supported by this library, and they are documented below. #### Tags -- You can track multiple kinds of visits to a single model using the tags as `visits($model, 'tag1')->increment()` +- You can track multiple kinds of visits to a single model using the tags as `visits($model,'tag1')->increment()` ## Increments and Decrements #### Increment ##### One -``` php +```php visits($post)->increment(); ``` -##### More than one -``` php +##### More than one +```php visits($post)->increment(10); ``` #### Decrement ##### One -``` php +```php visits($post)->decrement(); ``` -##### More than one -``` php +##### More than one +```php visits($post)->decrement(10); ``` #### Only increment/decrement once during x seconds (based on visitor's IP) -``` php +```php visits($post)->seconds(30)->increment() ``` -- Note: this will override default config setting (once each 15 minutes per IP). +> **Note:** this will override default config setting (once each 15 minutes per IP). #### Force increment/decrement -``` php +```php visits($post)->forceIncrement(); visits($post)->forceDecrement(); ``` - This will ignore IP limitation and increment/decrement every visit. - ## An item visits #### All visits of an item -``` php +```php visits($post)->count(); ``` -- Note: $post is a row of a model, i.e. $post = Post::find(22) - +> **Note:** $post is a row of a model, i.e. $post = Post::find(22); -#### Item's visits by a period -``` php +#### Item's visits by a period +```php visits($post)->period('day')->count(); ``` ## A model class visits #### All visits of a model type -``` php -visits('App\Post')->count() +```php +visits('App\Post')->count(); ``` #### Visits of a model type in period -``` php -visits('App\Post')->period('day')->count() +```php +visits('App\Post')->period('day')->count(); ``` ## Countries of visitors -``` php -visits($post)->countries() +```php +visits($post)->countries(); ``` ## Referers of visitors -``` php -visits($post)->refs() +```php +visits($post)->refs(); ``` ## Operating Systems of visitors -``` php -visits($post)->operatingSystems() +```php +visits($post)->operatingSystems(); ``` ## Languages of visitors -``` php -visits($post)->languages() +```php +visits($post)->languages(); ``` - ## Top or Lowest list per model type #### Top/Lowest 10 -``` php -visits('App\Post')->top(10) +```php +visits('App\Post')->top(10); ``` -``` php -visits('App\Post')->low(10) +```php +visits('App\Post')->low(10); ``` #### Uncached list -``` php -visits('App\Post')->fresh()->top(10) +```php +visits('App\Post')->fresh()->top(10); ``` -- Note: you can always get uncached list by enabling `alwaysFresh` from package config. +> **Note:** you can always get uncached list by enabling `alwaysFresh` from package config. #### By a period of time -``` php -visits('App\Post')->period('month')->top(10) +```php +visits('App\Post')->period('month')->top(10); ``` - ## Reset and clear values #### Clear an item visits -``` php +```php visits($post)->reset(); ``` #### Clear an item visits of specific period -``` php -visits($post)->period('year')->reset() +```php +visits($post)->period('year')->reset(); ``` #### Clear recorded visitors' IPs -``` php +```php visits($post)->reset('ips'); -visits($post)->reset('ips', '127.0.0.1'); +visits($post)->reset('ips','127.0.0.1'); ``` @@ -236,7 +236,7 @@ visits($post)->reset('ips', '127.0.0.1'); - decade - century -you also can make your custom period by adding a carbon marco in appserviceprovider: +you can also make your custom period by adding a carbon marco in `AppServiceProvider`: ```php Carbon::macro('endOf...', function () { @@ -245,17 +245,17 @@ Carbon::macro('endOf...', function () { ``` #### Other -``` php +```php //clear all visits of the given model and its items -visits('App\Post')->reset() +visits('App\Post')->reset(); //clear all cache of the top/lowest list -visits('App\Post')->reset('lists') +visits('App\Post')->reset('lists'); //clear visits from all items of the given model in a period -visits('App\Post')->period('year')->reset() +visits('App\Post')->period('year')->reset(); //...? -visits('App\Post')->reset('factory') +visits('App\Post')->reset('factory'); //increment/decrement methods offer ignore parameter to stop recording any items of ('country', 'refer', 'periods', 'operatingSystem', 'language') -visits('App\Post')->increment(1, false, ['country']) +visits('App\Post')->increment(1, false, ['country']); ``` ## Integration with Eloquent @@ -263,22 +263,22 @@ visits('App\Post')->increment(1, false, ['country']) You can add a `visits` method to your model class: ```php - public function visits() - { - return visits($this); - } +public function visits() +{ + return visits($this); +} ``` Then you can use it as: ```php - $post = Post::find(1); - $post->visits()->increment(); - $post->visits()->count(); +$post = Post::find(1); +$post->visits()->increment(); +$post->visits()->count(); ``` -## Change log +## Changelog Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. @@ -293,8 +293,40 @@ Please see [CONTRIBUTING](CONTRIBUTING.md) and [CODE_OF_CONDUCT](CODE_OF_CONDUCT - [All Contributors][link-contributors] ## Todo -- An export command to save visits of any periods to a table on database. - +- An export command to save visits of any periods to a table on the database. + +## Contributors + +### Code Contributors + +This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)]. + + +### Financial Contributors + +Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/laravel-visits/contribute)] + +#### Individuals + + + +#### Organizations + +Support this project with your organization. +Your logo will show up here with a link to your website. +[[Contribute](https://opencollective.com/laravel-visits/contribute)] + + + + + + + + + + + + ## License The MIT License (MIT). Please see [License File](LICENSE.md) for more information. diff --git a/src/Traits/Periods.php b/src/Traits/Periods.php index f1e52ef..5acf0f4 100755 --- a/src/Traits/Periods.php +++ b/src/Traits/Periods.php @@ -3,8 +3,8 @@ namespace Awssat\Visits\Traits; use Illuminate\Support\Carbon; -use Exception; use Illuminate\Support\Str; +use Exception; trait Periods { diff --git a/src/Traits/Record.php b/src/Traits/Record.php index 7789674..bd98362 100755 --- a/src/Traits/Record.php +++ b/src/Traits/Record.php @@ -19,8 +19,7 @@ protected function recordCountry($inc) */ protected function recordRefer($inc) { - $referer = app(Referer::class)->get(); - $this->connection->increment($this->keys->visits."_referers:{$this->keys->id}", $inc, $referer); + $this->connection->increment($this->keys->visits."_referers:{$this->keys->id}", $inc, $this->getVisitorReferer()); } /** @@ -56,7 +55,7 @@ protected function recordPeriods($inc) * Gets visitor country code * @return mixed|string */ - protected function getVisitorCountry() + public function getVisitorCountry() { return strtolower(geoip()->getLocation()->iso_code); } @@ -94,6 +93,19 @@ public function getVisitorOperatingSystem() */ public function getVisitorLanguage() { - return request()->getPreferredLanguage(); + $language = request()->getPreferredLanguage(); + if (false !== $position = strpos($language, '_')) { + $language = substr($language, 0, $position); + } + return $language; + } + + /** + * Gets visitor referer + * @return mixed|string + */ + public function getVisitorReferer() + { + return app(Referer::class)->get(); } }