+20

Laravel Page View Counter

Ngày xưa làm project training về website order thức ăn mình làm chức năng đếm số lượt xem sản phẩm như sau: Thêm một column là count_views vào bảng products rồi xữ lý (trong controller) tăng count_views lên 1 và update vào database mỗi lần người dùng click vào trang chi tiết sản phẩm. Đúng là khi xưa ta bé ta ngu, cách làm này rất không đúng và risk của nó thì chắc các bạn đã biết rồi, mình không nói thêm nữa. Hôm nay mình viết bài này để chia sẽ các bạn cách làm chức năng đếm số lượt xem trang một cách đúng đắn hơn, sử dụng Events và Middleware trong Laravel. Nếu bạn nào muốn dùng package để làm chức năng này thì có thể sử dụng package này. Nếu bạn quyết định dùng package thì bạn có thể dừng đọc bài này ở đây được rồi 😃

OK, chúng ta cùng nhau bắt đầu nhé Đầu tiên chúng ta sẽ tạo table posts sử dụng lệnh quen thuộc sau:

php artisan make:migration create_posts_table --create="posts"

Tiếp đến là thêm một số fields vào table posts như sau:

public function up()
{
    Schema::create('posts', function (Blueprint $table)
    {
        $table->increments('id');
        $table->string('title');
        $table->text('content');
        $table->integer('view_count')->default(0);
        $table->timestamps();
    });
}

Tiếp theo, chạy lệnh sau để nó sinh table posts trong Database nhé

php artisan migrate

Chúng ta sẽ tạo tiếp Model Post, Controller PostsController và Route.

namespace App\Demo;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    protected $fillable = [ 'title', 'content' ];
}

php artisan make:controller PostsController

Route::resource('posts', 'PostsController');

Để tăng số lượt xem của post mình cần sử dụng Event trong Laravel, nó cho phép chúng ta kích hoạt các hành động ứng với các sự kiện tương ứng. Có thể bạn đã sử dụng nó khi làm việc với Eloquent rồi phải không nào, nó đặt biệt hữu dụng trong trường hợp bạn muốn validate Model khi các hành động created,updated… được thực hiện. Các bạn có thể tham khảo thêm tại link này nhé. Vậy thì làm cách nào để chúng ta có thể apply nó vào chức năng đếm số lượt xem bài post của chúng ta. Đầu tiên chúng ta sẽ tạo một Event mà nó sẽ được kích hoạt mỗi khi người dùng view bài post. Muốn tính lượt xem cho page nào thì ta chỉ cần hook Event này vào là xong.Với cách làm này chúng ta đang break xữ lý logic ra khỏi controller và viết nó trong một class chuyên biệt hơn, việc này làm cho code trở nên clear và dễ maintain hơn nhiều phải không nào. Bây giờ thì controller sẽ đơn giản và ngắn gọn như sau:

use App\Demo\Post;

public function show($id)
{
    $post = Post::find($id);
    Event::fire('posts.view', $post);

    return View::make('posts.show')->withPost($post);
}

Laravel cho phép chúng ta đăng ký một lớp đóng vai trò lắng nghe sự kiện và mặc định nó sẽ gọi phương thức handle của lớp này để xữ lý mỗi khi sự kiện được kích hoạt.

namespace App\Demo\Events;

use App\Demo\Post;

class ViewPostHandler
{
    public function handle(Post $post)
    {
        $post->increment('view_count');
    }
}

Tiếp theo là bước đăng ký. Bạn có thể đăng ký trong phương thức boot của EventServiceProvider. Event::listen('posts.view', 'App\Demo\Events\ViewPostHandler');

Bây giờ mỗi lúc bài post được hiển thị thì sự kiện "post.view" được kích hoạt và nó sẽ tăng số lượt view trang lên 1.Nhưng điều gì sẽ xãy ra nếu người dùng vào xem một bài post và nhấn F5 liên tục. Số lượt view sẽ tăng liên tục, không ổn, chúng ta sẽ sử dụng Session và Middleware để giải quyết vấn đề này. Các bạn sửa lại class ViewPostHandler như sau nhé

namespace App\Demo\Events;

use App\Demo\Post;
use Illuminate\Session\Store;

class ViewPostHandler
{
    private $session;

    public function __construct(Store $session)
    {
        $this->session = $session;
    }

    public function handle(Post $post)
	{
	    if (!$this->isPostViewed($post))
	    {
	        $post->increment('view_count');
	        $this->storePost($post);
	    }
	}

	private function isPostViewed($post)
	{
	    $viewed = $this->session->get('viewed_posts', []);

	    return array_key_exists($post->id, $viewed);
	}

	private function storePost($post)
	{
	    $key = 'viewed_posts.' . $post->id;

	    $this->session->put($key, time());
	}
}

Tiếp theo chúng ta sẽ xây dựng middleware Filter, với middleware này thì chúng ta chỉ cho phép tăng biến đếm view_count sau một khoảng thời gian nhất định (ở đây mình để 1 giờ)

namespace App\Http\Middleware;

use Closure;
use Illuminate\Session\Store;
use Session;

class Filter
{
    private $session;

    public function __construct(Store $session)
    {
        $this->session = $session;
    }

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $posts = $this->getViewedPosts();

        if (!is_null($posts))
        {
            $posts = $this->cleanExpiredViews($posts);
            $this->storePosts($posts);
        }

        return $next($request);
    }

    private function getViewedPosts()
    {
        return $this->session->get('viewed_posts', null);
    }

    private function cleanExpiredViews($posts)
    {
        $time = time();

        // Let the views expire after one hour.
        $throttleTime = 3600;

        return array_filter($posts, function ($timestamp) use ($time, $throttleTime)
        {
            return ($timestamp + $throttleTime) > $time;
        });
    }

    private function storePosts($posts)
    {
        $this->session->put('viewed_posts', $posts);
    }
}

Bước cuối cùng là đăng ký middleware này trong Kernel và thêm middleware vào cho route 'filter' => \App\Http\Middleware\Filter::class,

Route::group(['middleware' => 'filter'], function() {
    Route::resource('posts', 'PostsController');
});

Như vậy là mình đã hoàn thành xong chức năng đếm số lượt xem trang rồi, khá đơn giản phải không nào, cảm ơn các bạn đã đọc.


All rights reserved

Bình luận

Đăng nhập để bình luận
Avatar
@dinhdanh
thg 5 17, 2017 9:42 SA

Mình có câu hỏi như này, phần mà bạn trình bày ở trên khác gì so với thuật toán bạn đưa ra ban đầu: "Thêm một column là count_views vào bảng products rồi xữ lý (trong controller) tăng count_views lên 1 và update vào database mỗi lần người dùng click vào trang chi tiết sản phẩm. " Mình đọc qua bài thấy cũng là thêm 1 cột đếm trong db, xử lý tăng lên 1 khi có view mới(áp dụng thêm session để giải quyết vấn đề view ảo(refresh page)). Phần xử lý thay vì nằm trong controller thì bạn sử dụng middware và event của laravel. Thực ra việc đặt phần xử lý trong controller theo mình là không sai, chỉ là ở đây bạn chia nhỏ ra để code dễ xem hơn, phần này nghiêng về design pattern hơn là thuật toán.

Avatar
@conglb
thg 8 1, 2017 8:46 SA

Mình cũng có cảm giác giống bac Đình Danh. Thuật toán ban đầu vốn là bug lớn khi cứ F5 thì đếm, và qua bài này thì chúng ta vá lỗi đó bằng cách thêm Session & midd . Code rất chân phương dễ hiểu. tks bạn.

Avatar
@quangduy90
thg 8 13, 2017 6:26 SA

đếm lượt views giờ người ta sài redis, chứ mỗi lần có khách là mỗi lần update thì ko ổn

Avatar
@vanbaodo
thg 10 19, 2017 7:42 SA

hóng tust của bác viết về cái này 😃

Avatar
@quangduy90
thg 10 28, 2017 9:10 SA

google đầy đó bác x_x

Avatar
@simple1805
thg 9 5, 2017 6:28 SA

Cho mình hỏi class ViewPostHandler viết trong forder tự tạo hay viết ở đâu . Thanks bạn

Avatar

Trong thư mục App\Demo\Events rồi tạo class ...

Avatar
@KmasterYC
thg 10 19, 2017 8:53 SA

Giờ người dùng clear cookie rồi F5 tính sao đây bạn ^^ 😸

Avatar
@cutyn01
thg 12 22, 2022 12:35 CH

a cho e hỏi cái Event::fire thì phải trheem cái class nào trong controller vậy. e toàn bị báo lỗi này Method Illuminate\Events\Dispatcher::fire does not exist. a chỉ giúp e với. thank a

Avatar
@vietthangif
thg 12 21, 2018 9:12 SA
Avatar
@x2team
thg 9 18, 2019 1:07 SA

Tại sao mỗi lần mình load trang thì biến view_count tăng lên 2 chứ ko phải là 1 nhỉ? Và biến view_count hiển thị ở view lúc nào cũng lớn hơn cột view_count trong database 1 đơn vị là như nào nhỉ? Mong mọi người giúp đỡ 😦

Lỗi mình giống như post này: https://stackoverflow.com/questions/30645309/laravel-5-incrementing-by-2-instead-by-1

Avatar
@spaussio
thg 7 9, 2021 8:50 SA

Event::fire('posts.view', $post);

fire này sao e làm toàn báo lỗi nhỉ?

Avatar

bởi vì Laravel bạn đang dùng là bản mới, cơ chế event, listener không còn giống trong bài viết, bài viết đang xài hình như là laravel 5. thì phải, hiện tại comment này đã ra mắt 9. rồi

Avatar
@cutyn01
thg 12 22, 2022 12:48 CH

@LoveIsABeautifulPain phải khắc phục lỗi đó như nào ạ, e đang dùng laravel 8

Avatar
+20
Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí