Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

DoeJohn's avatar

Counting Views of Articles From the Same User

Hi,

I created a simple application that counts the views of an article. In the database articles table has a column view_count. Also I have a defined event & listener that increments view_count every time when some article is viewed:

App\Events\ ArticleWasViewed.php:

class ArticleWasViewed extends Event
{
    use SerializesModels;

    public $article;

    public function __construct(Article $article)
    {
        $this->article = $article;
    }

    public function broadcastOn()
    {
        return [];
    }
}

App\Listeners\ IncrementArticleViewCount.php:

class IncrementArticleViewCount
{
    public function __construct()  { }

    public function handle(ArticleWasViewed $event)
    {
        $event->article->increment('view_count');
    }
}

And in ArticleController.php@show:

    public function show($id)
    {
        $article = $this->articles->find($id);

        \Event::fire(new ArticleWasViewed($article));

        return view('articles.show', compact('article', ));
    }

And this works fine - every time when someone visits (view) some article - view_count is increased by 1. But if the same user visits the same article again and again (for example, he refreshes the page) - view_count will be increased by 1 every time. So, if one user refreshes the page 1000 times - that article will get 1,000 new views.

How to solve this, what is the most common solution? And how to do it with Laravel? I see that YouTube works like this:

It will count as views if you watch more than 20% of the video and then refresh, but like the other guys said once you hit 300 "Views" google's algorithm takes over and will see that all the views are from the same I.P. address and will not count any more views after that in the best case scenario. In the worst case senario, your account gets banned and all your videos are deleted for viewhacking.

Once you hit 300 "Views" google's algorithm takes over and will see that all the views are from the same I.P. address and will not count any more views after that in the best case scenario.

Maybe that's not the best solution?

Correct me if I'm wrong, but I think that vBulletin always counts everything - when you refresh the page - it will count it, and there is no protection like YouTube has...

Also, here http://blog.stidges.com/post/implementing-a-page-view-counter-in-laravel this "problem" is solved by using route filters & letting the session entry to expire after a given time:

To tackle this problem, we will make use of route filters. We want to call a filter on every page load that checks whether some viewed posts can be cleared from the session. Again, we will be using a dedicated class for handling this filter. This allows us to inject the session instance into the class and use it to do our cleaning. By default, when assigning a class to handle a filter, Laravel will call the 'filter' method on this class. Read more about using route filters here.

But, using route filters in Laravel 5 is not the best way, right?

0 likes
4 replies
jekinney's avatar

For me I sent a separate table along with view_count column. If your data is mostly dynamic then you can use polymorphic. If not store the full url.

Lets say the proverbial blog and you want to count views on a article:

 id - integer
user_id - integer
ip - string
views - integer //optional
 viewable_id - integer
 viewable_type - string

So a middleware is set to check if view count needs to be set and incremented. Fire an queued event or job to run it in the background.

$article = Article::with('views')->where('slug', $this->slug)->first();
if(auth()->check()) {
    //see if it contains the user_id from auth()->id(), if found you can increment optional user view count, otherwise done
    // if not found, insert new row and increment main model's view count
}
// otherwise guest, check IP in IP column, if found can in increment optional user view count, otherwise done
// insert new row with user_id set to 0 (means guest)  increment main model's view count

Instead of always hitting the data base you could load the user's view history in a cache on login and check that, and if guest also start a cache when they hit a page that you want view counts. This way you can update both (if using events use two listeners, one for db and one to update cache).

This also gives you a few more data options. Which user has viewed an article the most. Which articles does a user like to view a lot etc (analytic data to keep your user's engaged and tailor articles towards that). You also will have a separate guest view count, which you can assume that more then likely they found the article via search or social media and went to your site to read it.

nothing will be 100% until IPv6 comes out, even then you can't control some one with 20 devices hitting the page 20 times, but it minimizes it. 20 is better then 200.

1 like
DoeJohn's avatar

@jekinney Thank you. So if I understand well:

  1. Along with view_count column we will have a separate table named, for example, views;

  2. I'm not sure if I understood all of the columns of that table:

    • views = the number of views made by a specific user (user_id or IP) for a specific article (viewable_id = article_id), right ?
$article = Article::with('views')->where('slug', $this->slug)->first();
  • This basically means: in middleware, we first need to find article that is currently being viewed (slug = $this->slug)? How and from where we get $this->slug?
  • We also get all of the records from views table where is actually viewable_id = article_id (with('views')), right?
mikebarwick's avatar

I'd just store a session variable. Expire after a day (or 12hrs), etc. Check if the session exists, if it doesn't, increment. etc. Doesn't need to be complex logic for this type of protection.

For example: Session::put($article_id, $value);

6 likes
jekinney's avatar

@FilipLaracasts

My example was using polymorphic relationship so your view count could expand to anything:

https://laravel.com/docs/4.2/eloquent#polymorphic-relations

Similar to the likes example but you would be setting users by IP and/or user_id. Like I said I would store the path too if you wanted

To get the path: https://laravel.com/docs/4.2/requests#request-information

//full path/url

Request::path(); or Request::url();

// Segment or part of the path
// say the url is http://localhost.dev/article/slug-of-article-title/show
$slug = Request::segment(2);
dd($slug); // should output 'slug-of-article'
1 like

Please or to participate in this conversation.