├── README.md ├── CHANGELOG.md ├── src ├── Models │ └── Visit.php ├── LaravelPopularityServiceProvider.php ├── Traits │ ├── Visitable.php │ ├── SetsPendingIntervals.php │ └── FiltersByPopularityTimeframe.php └── PendingVisit.php ├── database ├── factories │ └── ArticleFactory.php └── migrations │ └── create_visits_table.php.stub ├── LICENSE.md └── composer.json /README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-popularity` will be documented in this file. 4 | 5 | ## 1.0.0 - 202X-XX-XX 6 | 7 | - initial release 8 | -------------------------------------------------------------------------------- /src/Models/Visit.php: -------------------------------------------------------------------------------- 1 | 'json', 15 | ]; 16 | 17 | public function visitable() 18 | { 19 | return $this->morphTo(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /database/factories/ArticleFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->sentence(10) 16 | ]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /database/migrations/create_visits_table.php.stub: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->morphs('visitable'); 14 | $table->json('data'); 15 | $table->timestamps(); 16 | }); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /src/LaravelPopularityServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('laravel-popularity') 19 | ->hasMigration('create_visits_table'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Traits/Visitable.php: -------------------------------------------------------------------------------- 1 | withCount('visits as visit_count_total'); 21 | } 22 | 23 | public function visits() 24 | { 25 | return $this->morphMany(Visit::class, 'visitable'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Traits/SetsPendingIntervals.php: -------------------------------------------------------------------------------- 1 | interval = now()->subHour(); 12 | 13 | return $this; 14 | } 15 | 16 | public function dailyInterval() 17 | { 18 | $this->interval = now()->subDay(); 19 | 20 | return $this; 21 | } 22 | 23 | public function weeklyInterval() 24 | { 25 | $this->interval = now()->subWeek(); 26 | 27 | return $this; 28 | } 29 | 30 | public function monthlyInterval() 31 | { 32 | $this->interval = now()->subMonth(); 33 | 34 | return $this; 35 | } 36 | 37 | public function customInterval(Carbon $interval) 38 | { 39 | $this->interval = $interval; 40 | 41 | return $this; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) codecourse 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/PendingVisit.php: -------------------------------------------------------------------------------- 1 | dailyInterval(); 20 | } 21 | 22 | public function withIp($ip = null) 23 | { 24 | $this->attributes['ip'] = $ip ?? request()->ip(); 25 | 26 | return $this; 27 | } 28 | 29 | public function withUser($user = null) 30 | { 31 | if ($user) { 32 | $this->attributes['user_id'] = $user->id; 33 | } 34 | 35 | return $this; 36 | } 37 | 38 | public function withData($data) 39 | { 40 | $this->attributes = array_merge($this->attributes, $data); 41 | 42 | return $this; 43 | } 44 | 45 | protected function buildJsonColumns() 46 | { 47 | return collect($this->attributes) 48 | ->mapWithKeys(function ($value, $index) { 49 | return ['data->' . $index => $value]; 50 | }) 51 | ->toArray(); 52 | } 53 | 54 | protected function shouldBeLoggedAgain(Visit $visit) 55 | { 56 | return ! $visit->wasRecentlyCreated && $visit->created_at->lt($this->interval); 57 | } 58 | 59 | public function __destruct() 60 | { 61 | $visit = $this->model->visits()->latest()->firstOrCreate($this->buildJsonColumns(), [ 62 | 'data' => $this->attributes, 63 | ]); 64 | 65 | $visit->when($this->shouldBeLoggedAgain($visit), function () use ($visit) { 66 | $visit->replicate()->save(); 67 | }); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codecourse/laravel-popularity", 3 | "description": "A package to track the popularity of your Laravel models.", 4 | "keywords": [ 5 | "codecourse", 6 | "laravel", 7 | "laravel-popularity" 8 | ], 9 | "homepage": "https://github.com/codecourse/laravel-popularity", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Codecourse", 14 | "email": "alex@codecourse.com", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.0", 20 | "spatie/laravel-package-tools": "^1.9.2" 21 | }, 22 | "require-dev": { 23 | "nunomaduro/collision": "^6.1", 24 | "orchestra/testbench": "^7.4.0", 25 | "pestphp/pest": "^1.21", 26 | "pestphp/pest-plugin-laravel": "^1.2", 27 | "phpunit/phpunit": "^9.5", 28 | "spatie/laravel-ray": "^1.26" 29 | }, 30 | "autoload": { 31 | "psr-4": { 32 | "Codecourse\\LaravelPopularity\\": "src", 33 | "Codecourse\\LaravelPopularity\\Database\\Factories\\": "database/factories" 34 | } 35 | }, 36 | "autoload-dev": { 37 | "psr-4": { 38 | "Codecourse\\LaravelPopularity\\Tests\\": "tests" 39 | } 40 | }, 41 | "scripts": { 42 | "test": "vendor/bin/pest", 43 | "test-coverage": "vendor/bin/pest coverage" 44 | }, 45 | "config": { 46 | "sort-packages": true, 47 | "allow-plugins": { 48 | "pestphp/pest-plugin": true 49 | } 50 | }, 51 | "extra": { 52 | "laravel": { 53 | "providers": [ 54 | "Codecourse\\LaravelPopularity\\LaravelPopularityServiceProvider" 55 | ], 56 | "aliases": { 57 | "LaravelPopularity": "Codecourse\\LaravelPopularity\\Facades\\LaravelPopularity" 58 | } 59 | } 60 | }, 61 | "minimum-stability": "dev", 62 | "prefer-stable": true 63 | } 64 | -------------------------------------------------------------------------------- /src/Traits/FiltersByPopularityTimeframe.php: -------------------------------------------------------------------------------- 1 | withTotalVisitCount()->orderBy('visit_count_total', 'desc'); 13 | } 14 | 15 | public function scopePopularLastDays(Builder $query, $days) 16 | { 17 | $query->popularBetween(now()->subDays($days), now()); 18 | } 19 | 20 | public function scopePopularThisWeek(Builder $query) 21 | { 22 | $query->popularBetween(now()->startOfWeek(), now()->endOfWeek()); 23 | } 24 | 25 | public function scopePopularLastWeek(Builder $query) 26 | { 27 | $query->popularBetween( 28 | $startOfLastWeek = now()->subDays(7)->startOfWeek(), 29 | $startOfLastWeek->copy()->endOfWeek() 30 | ); 31 | } 32 | 33 | public function scopePopularThisMonth(Builder $query) 34 | { 35 | $query->popularBetween(now()->startOfMonth(), now()->endofMonth()); 36 | } 37 | 38 | public function scopePopularLastMonth(Builder $query) 39 | { 40 | $query->popularBetween( 41 | now()->startOfMonth()->subMonthWithoutOverflow(), 42 | now()->subMonthWithoutOverflow()->endOfMonth(), 43 | ); 44 | } 45 | 46 | public function scopePopularThisYear(Builder $query) 47 | { 48 | $query->popularBetween(now()->startOfYear(), now()->endOfYear()); 49 | } 50 | 51 | public function scopePopularLastYear(Builder $query) 52 | { 53 | $query->popularBetween( 54 | now()->startOfYear()->subYearWithoutOverflow(), 55 | now()->subYearWithoutOverflow()->endOfYear(), 56 | ); 57 | } 58 | 59 | public function scopePopularBetween(Builder $query, Carbon $from, Carbon $to) 60 | { 61 | $query->whereHas('visits', $this->betweenScope($from, $to)) 62 | ->withCount([ 63 | 'visits as visit_count' => $this->betweenScope($from, $to), 64 | ]); 65 | } 66 | 67 | protected function betweenScope(Carbon $from, Carbon $to) 68 | { 69 | return function ($query) use ($from, $to) { 70 | $query->whereBetween('created_at', [$from, $to]); 71 | }; 72 | } 73 | } 74 | --------------------------------------------------------------------------------