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.