diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/composer.json b/composer.json index 600642e..ea4ae52 100644 --- a/composer.json +++ b/composer.json @@ -17,13 +17,13 @@ ], "require": { "php": "^8.0", - "illuminate/support": "^8.0|^9.0|^10.0|^11.0", - "jaybizzle/crawler-detect": "^1.2" + "illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0", + "jaybizzle/crawler-detect": "^1.3" }, "require-dev": { "liip/rmt": "^1.7", - "orchestra/testbench": "^6.0|^7.0|^8.0", - "phpunit/phpunit": "^9.0" + "orchestra/testbench": "^6.0|^7.0|^8.0|^9.0", + "phpunit/phpunit": "^9.0|^10.0|^11.0" }, "autoload": { "psr-4": { diff --git a/database/factories/PageViewFactory.php b/database/factories/PageViewFactory.php index 8c07d61..be0b302 100644 --- a/database/factories/PageViewFactory.php +++ b/database/factories/PageViewFactory.php @@ -19,7 +19,7 @@ class PageViewFactory extends Factory * * @return array */ - public function definition() + public function definition(): array { return [ 'session_id' => $this->faker->word, diff --git a/database/migrations/2023_03_29_135458_create_page_views_table.php b/database/migrations/2023_03_29_135458_create_page_views_table.php index 0c98909..8bde4ce 100644 --- a/database/migrations/2023_03_29_135458_create_page_views_table.php +++ b/database/migrations/2023_03_29_135458_create_page_views_table.php @@ -11,7 +11,7 @@ * * @return void */ - public function up() + public function up(): void { $tableName = config('laravel-analytics.db_prefix') . 'page_views'; @@ -21,7 +21,7 @@ public function up() $table->string('path')->index(); $table->string('user_agent')->nullable(); $table->string('ip')->nullable(); - $table->string('referer')->nullable()->index(); + $table->text('referer')->nullable()->index(); $table->string('county')->nullable()->index(); $table->string('city')->nullable(); $table->string('page_model_type')->nullable(); @@ -38,7 +38,7 @@ public function up() * * @return void */ - public function down() + public function down(): void { Schema::dropIfExists(config('laravel-analytics.db_prefix') . 'page_views'); } diff --git a/routes/api.php b/routes/api.php new file mode 100644 index 0000000..6bbe2f0 --- /dev/null +++ b/routes/api.php @@ -0,0 +1,14 @@ +pageViewRepository = $pageViewRepository; + } + + public function getPageViewsPerDays(): JsonResponse + { + return response()->json( + $this->pageViewRepository->getByDateGroupedByDays(Carbon::today()->subDays(28)) + ); + } + + public function getPageViewsPerPaths(): JsonResponse + { + return response()->json( + $this->pageViewRepository->getByDateGroupedByPath(Carbon::today()->subDays(28)) + ); + } + + public function getPageViewsLast28Days(): JsonResponse { - return $pageViewRepository->getByDateGroupedByDays(Carbon::today()->subDays(28)); + return response()->json( + $this->pageViewRepository->getLast28Days() + ); } - public function getPageViewsPerPaths(Request $request, PageViewRepository $pageViewRepository) + public function getPageViewsLast3Months(): JsonResponse { - return $pageViewRepository->getByDateGroupedByPath(Carbon::today()->subDays(28)); + return response()->json( + $this->pageViewRepository->getLast3Months() + ); + } + + public function getPageViewsLast6Months(): JsonResponse + { + return response()->json( + $this->pageViewRepository->getLast6Months() + ); + } + + public function getPageViewsLastYear(): JsonResponse + { + return response()->json( + $this->pageViewRepository->getLastYear() + ); + } + + public function getPageViewsCustomRange(Request $request): JsonResponse + { + $startDate = Carbon::parse($request->query('start_date', now()->subMonth())); + $endDate = Carbon::parse($request->query('end_date', now())); + + return response()->json( + $this->pageViewRepository->getCustomRange($startDate, $endDate) + ); } -} \ No newline at end of file +} diff --git a/src/Http/Middleware/Analytics.php b/src/Http/Middleware/Analytics.php index a2dd116..e288c5f 100644 --- a/src/Http/Middleware/Analytics.php +++ b/src/Http/Middleware/Analytics.php @@ -2,9 +2,9 @@ namespace WdevRs\LaravelAnalytics\Http\Middleware; +use Closure; use Illuminate\Database\Eloquent\Model; use Illuminate\Http\Request; -use Closure; use Illuminate\Support\Str; use Jaybizzle\CrawlerDetect\CrawlerDetect; use Throwable; @@ -17,53 +17,54 @@ public function handle(Request $request, Closure $next) $response = $next($request); try { - if (!$request->isMethod('GET')) { + if (!$this->shouldTrack($request)) { return $response; } - if ($request->isJson()) { - return $response; - } - - $userAgent = $request->userAgent(); + $this->storePageView($request); + } catch (Throwable $e) { + report($e); + } - if (is_null($userAgent)) { - return $response; - } + return $response; + } - /** @var CrawlerDetect $crawlerDetect */ - $crawlerDetect = app(CrawlerDetect::class); + private function shouldTrack(Request $request): bool + { + if (!$request->isMethod('GET') || $request->isJson()) { + return false; + } - if ($crawlerDetect->isCrawler($userAgent)) { - return $response; - } + $userAgent = $request->userAgent(); + if (is_null($userAgent) || app(CrawlerDetect::class)->isCrawler($userAgent)) { + return false; + } - /** @var PageView $pageView */ - $pageView = PageView::make([ - 'session_id' => session()->getId(), - 'path' => $request->path(), - 'user_agent' => Str::substr($userAgent, 0, 255), - 'ip' => $request->headers->get('X-Forwarded-For') ? $request->headers->get('X-Forwarded-For') : $request->ip(), - 'referer' => $request->headers->get('referer'), - ]); + return true; + } - $parameters = $request->route()?->parameters(); - $model = null; + private function storePageView(Request $request): void + { + $pageView = new PageView([ + 'session_id' => session()->getId(), + 'path' => $request->path(), + 'user_agent' => Str::limit($request->userAgent(), 255), + 'ip' => $request->header('X-Forwarded-For', $request->ip()), + 'referer' => $request->header('referer'), + ]); - if (!is_null($parameters)) { - $model = reset($parameters); - } + if ($model = $this->getRouteModel($request)) { + $pageView->pageModel()->associate($model); + } - if (is_a($model, Model::class)) { - $pageView->pageModel()->associate($model); - } + $pageView->save(); + } - $pageView->save(); + private function getRouteModel(Request $request): ?Model + { + $parameters = $request->route()?->parameters(); + $model = $parameters ? reset($parameters) : null; - return $response; - } catch (Throwable $e) { - report($e); - return $response; - } + return $model instanceof Model ? $model : null; } } diff --git a/src/LaravelAnalytics.php b/src/LaravelAnalytics.php deleted file mode 100644 index 1f2cff9..0000000 --- a/src/LaravelAnalytics.php +++ /dev/null @@ -1,23 +0,0 @@ -loadTranslationsFrom(__DIR__.'/../resources/lang', 'laravel-analytics'); - // $this->loadViewsFrom(__DIR__.'/../resources/views', 'laravel-analytics'); + // Load package migrations $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); -// $this->loadRoutesFrom(__DIR__.'/routes.php'); + + // Load package routes + $this->loadRoutesFrom(__DIR__.'/../routes/api.php'); if ($this->app->runningInConsole()) { $this->publishes([ @@ -29,32 +28,20 @@ public function boot() __DIR__.'/../resources/js/components' => resource_path('js/vendor/laravel-analytics/components'), ], 'components'); - // Publishing assets. - /*$this->publishes([ - __DIR__.'/../resources/assets' => public_path('vendor/laravel-analytics'), - ], 'assets');*/ - - // Publishing the translation files. - /*$this->publishes([ - __DIR__.'/../resources/lang' => resource_path('lang/vendor/laravel-analytics'), - ], 'lang');*/ - - // Registering package commands. - // $this->commands([]); } } /** * Register the application services. */ - public function register() + public function register(): void { // Automatically apply the package configuration $this->mergeConfigFrom(__DIR__.'/../config/laravel-analytics.php', 'laravel-analytics'); - // Register the main class to use with the facade - $this->app->singleton('laravel-analytics', function () { - return new LaravelAnalytics; + //Add methods to get data through Analytics facade + $this->app->singleton('analytics', function ($app) { + return $app->make(PageViewRepository::class); }); } } diff --git a/src/Models/PageView.php b/src/Models/PageView.php index 6ffe09f..ce5e1f8 100644 --- a/src/Models/PageView.php +++ b/src/Models/PageView.php @@ -6,17 +6,43 @@ use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\MorphTo; class PageView extends Model { use HasFactory; - protected $guarded = ['id']; + + protected $table; + + protected $guarded = ['id']; // Prevent mass assignment for ID + + protected $fillable = [ + 'session_id', + 'path', + 'user_agent', + 'ip', + 'referer', + 'county', + 'city', + 'page_model_type', + 'page_model_id', + 'created_at', + 'updated_at', + ]; + + protected $casts = [ + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; protected static function newFactory(): Factory { return PageViewFactory::new(); } + /** + * @param array $attributes + */ public function __construct(array $attributes = []) { $this->table = config('laravel-analytics.db_prefix') . 'page_views'; @@ -24,8 +50,12 @@ public function __construct(array $attributes = []) parent::__construct($attributes); } - - public function pageModel() + /** + * Get the related page model. + * + * @return MorphTo + */ + public function pageModel(): MorphTo { return $this->morphTo(); } diff --git a/src/Repositories/PageViewRepository.php b/src/Repositories/PageViewRepository.php index 96bf621..9b1449f 100644 --- a/src/Repositories/PageViewRepository.php +++ b/src/Repositories/PageViewRepository.php @@ -9,18 +9,17 @@ class PageViewRepository { - public function getByDate(Carbon $date): Collection { return PageView::query() - ->where('created_at', '>=', $date) - ->get(); + ->where('created_at', '>=', $date) + ->get(); } public function getByDateGroupedByPath(Carbon $date): Collection { return PageView::query() - ->selectRaw('COUNT(id) as count, path' ) + ->selectRaw('COUNT(id) as count, path') ->where('created_at', '>=', $date) ->groupBy('path') ->orderByDesc('count') @@ -31,9 +30,9 @@ public function getByDateGroupedByPath(Carbon $date): Collection public function getByDateGroupedByDays(Carbon $date): Collection { return PageView::query() - ->select(DB::raw('DATE(created_at) as date'), DB::raw('COUNT(id) as count') ) + ->selectRaw('DATE(created_at) as date, COUNT(id) as count') ->where('created_at', '>=', $date) - ->groupBy('date') + ->groupBy(DB::raw('DATE(created_at)')) ->orderBy('date') ->pluck('count', 'date'); } @@ -41,12 +40,40 @@ public function getByDateGroupedByDays(Carbon $date): Collection public function getVisitorsByDateGroupedByDays(Carbon $date): Collection { return PageView::query() - ->select(DB::raw('DATE(created_at) as date'), DB::raw('COUNT(DISTINCT session_id) as count') ) + ->selectRaw('DATE(created_at) as date, COUNT(DISTINCT session_id) as count') ->where('created_at', '>=', $date) - ->groupBy( 'date') + ->groupBy(DB::raw('DATE(created_at)')) ->orderBy('date') ->pluck('count', 'date'); } + public function getLast28Days(): Collection + { + return $this->getByDateGroupedByDays(Carbon::today()->subDays(28)); + } + + public function getLast3Months(): Collection + { + return $this->getByDateGroupedByDays(Carbon::today()->subMonths(3)); + } + + public function getLast6Months(): Collection + { + return $this->getByDateGroupedByDays(Carbon::today()->subMonths(6)); + } + + public function getLastYear(): Collection + { + return $this->getByDateGroupedByDays(Carbon::today()->subYear()); + } -} \ No newline at end of file + public function getCustomRange(Carbon $startDate, Carbon $endDate): Collection + { + return PageView::query() + ->selectRaw('DATE(created_at) as date, COUNT(id) as count') + ->whereBetween('created_at', [$startDate, $endDate]) + ->groupBy(DB::raw('DATE(created_at)')) + ->orderBy('date') + ->pluck('count', 'date'); + } +} diff --git a/tests/Controllers/AnalyticsControllerTest.php b/tests/Controllers/AnalyticsControllerTest.php index 8ae3358..9c02a8d 100644 --- a/tests/Controllers/AnalyticsControllerTest.php +++ b/tests/Controllers/AnalyticsControllerTest.php @@ -13,8 +13,6 @@ class AnalyticsControllerTest extends TestCase protected function setUp(): void { parent::setUp(); - - LaravelAnalytics::routes(); } public function testItCanGetPageViewsPerDay() diff --git a/tests/PageViewRepositoryTest.php b/tests/PageViewRepositoryTest.php index 042a8ef..8b0dbd7 100644 --- a/tests/PageViewRepositoryTest.php +++ b/tests/PageViewRepositoryTest.php @@ -31,7 +31,7 @@ public function testItCanGetPageViewDataByDate() $this->assertCount(10, $analyticsData); } - public function testItCanGetPageViewDataByDateGrouppedByPath() + public function testItCanGetPageViewDataByDateGroupedByPath() { $pageViewRepository = app(PageViewRepository::class); @@ -57,7 +57,7 @@ public function testItCanGetPageViewDataByDateGrouppedByPath() $this->assertEquals(5, $analyticsData['test/2']); } - public function testItCanGetPageViewDataByDateGrouppedByDays() + public function testItCanGetPageViewDataByDateGroupedByDays() { $pageViewRepository = app(PageViewRepository::class); diff --git a/tests/TestCase.php b/tests/TestCase.php index 9a677cf..2efeaf8 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -16,7 +16,7 @@ protected function setUp(): void $this->artisan('migrate', ['--database' => 'testing'])->run(); } - protected function getPackageProviders($app) + protected function getPackageProviders($app): array { return [ LaravelAnalyticsServiceProvider::class