2022.09.14

Better Rails: 3 Filters giúp Controller của bạn sang xịn mịn hơn rất nhiều

3 filters trong Rails

Các bạn khi bắt đầu làm việc với Rails hầu hết đều làm việc với 7 actions chuẩn khi tạo một Controller mới: index, new, show, create, edit, update, destroy. Sau một thời gian làm việc, các bạn sẽ để ý thấy một điều rằng, các bạn thường sẽ muốn dùng đi dùng lại một đoạn tính năng tương tự nhau ở các actions hoặc thậm chí là các controllers. Trong bài viết này chúng ta sẽ cùng đi chung với nhau trên con đường giải quyết vấn đề này với một tính năng rất hay: Filters.

Bonus: Trong bài viết, chúng ta sẽ dùng một chút metaprogramming để tạo ra đoạn code giúp bỏ đi rất nhiều đoạn code lặp đi lặp lại nhé.

Trong bài viết chúng ta sẽ đi qua các phần sau:

  • Tìm hiểu về Filters trong Rails,
  • Áp dụng Filters để share code giữa các action trong cùng một Controller
  • Sử dụng metaprogramming để share code giữa các controller

Bắt đầu thôi. Let’s go.

3 loại filters trong Rails

Đầu tiên chúng ta sẽ nói về tính năng Filters mà Rails cung cấp. Rails cung cấp cho chúng ta 3 loại filters:

  • before_filter: chạy trước khi request được gửi đến action trong controller
  • after_filter: chạy sau khi action xử lý request
  • around_filter: bọc xung quanh action, chủ yếu sử dụng cho xử lý ngoại lệ (exception)
3 filters trong Rails
3 filters trong Rails

Cùng tìm hiểu kỹ hơn nhé.

before_filter

Đây là Filter được sử dụng phổ biến nhất. Có 2 cách để gọi một before_filter.

Cách thứ nhất là sử dụng một anonymous block:

class ArticlesController < ApplicationController

  before_filter do

    @article = Article.find(params[:id]) if params[:id]

  end

  #...

Đoạn code bên trên sẽ được thực thi trước tất cả request đến bất kì action nào bên trong controller. Vậy nếu như chỉ muốn áp dụng đoạn code đó cho action newedit thôi thì làm thế nào? Một chút nữa chúng ta sẽ nói đến cách để scope phạm vi của filter.

Cách viết thứ hai sau đây là cách mà mình ưa thích hơn vì sự gọn gàng của nó: sử dụng tên của method.

class ArticlesController < ApplicationController

  before_filter :load_article

  # Actions...

  private

  def load_article

    @article = Article.find(params[:id]) if params[:id]

  end

end

Các bạn có thể để ý thấy mình để method load_article là private method. Bởi vì filter này chỉ được sử dụng bên trong controller, và sẽ không được truy cập bởi router để map vào một đường dẫn nào, vì vậy nên tốt hơn hết, chúng ta sẽ để nó là private method. 

after_filter 

Sau khi tìm hiểu về before_filter bên trên, chắc hẳn các bạn cũng đã đoán ra after_filter sử dụng như thế nào rồi. Nó hoạt động với cơ chế tương tự như before_filter, chỉ khác là được thực thi sau khi action của controller được thực thi. Bạn có thể sử dụng nó để lọc hoặc mask các trường thông tin nhạy cảm trước khi trả kết quả về cho client chẳng hạn.

around_filter

Đây là filter hiếm khi được sử dụng và cũng hơi khó hiểu một chút xíu. Cách dùng của nó như sau:

around_filter :wrap_actions

def wrap_actions

  begin

    # wrap action code will be here 

    yield # after yeild, action code will be run

  rescue
    render text: "It broke!" # if there is any exception throw by action code, reach here
  end

end

Như mình đã comment trong đoạn code trên, khi nào yield được gọi, code bên trong action của controller sẽ được thực thi. Trong quá trình thực thi của action, nếu có exception xảy ra, exception sẽ được bắt ở đoạn code rescue của around_filter bên trên. Đoạn code trên là một trong những ứng dụng của around_filter: bắt exception.

Scope phạm vi của filter: only và except

Nếu để mặc định, các đoạn code Filter ở trên sẽ được thực thi trước tất cả request đến bất kì action nào bên trong controller. Vậy nếu như chỉ muốn áp dụng đoạn code đó cho action newedit thôi thì làm thế nào? Sau đây mình sẽ nói về cách scope phạm vi của Filter tới action mà các bạn mong muốn thôi: sử dụng onlyexcept.

Cả 3 filter bên trên đều nhận option :only:except

  • :only: danh sách các actions mà filter đó sẽ chạy cùng
  • :except: danh sách các actions mà filter đó sẽ không chạy cùng

Ví dụ, ở đoạn code ví dụ cho before_filter bên trên, các bạn có thể bỏ điều kiện check if params[:id] nếu như bạn scope filter này tới các action sẽ luôn có params[:id], ví dụ như :show, :edit, :update, :destroy.  Cách làm như sau:

class ArticlesController < ApplicationController

  before_filter :load_article, only: [:show, :edit, :update, :destroy]

  # Actions...

private

  def load_article

    @article = Article.find(params[:id])

  end

end

Hoặc các bạn cũng có thể sử dụng :except như sau:

class ArticlesController < ApplicationController

  before_filter :load_article, except: [:index, :new, :create]

  #...

 

Như vậy là chúng ta đã đi qua 3 loại filter được sử dụng trong Rails và cũng đã tìm hiểu làm sao để config các actions mà chúng ta muốn chạy filter bên trong controller. Sau đây sẽ tới phần thú vị nhất, share các filter này giữa các controller luôn. Bật mí, chúng ta sẽ sử dụng một chút metaprogramming.

Sharing Filters

Thông thường, filter sẽ được sử dụng để share code giữa các action bên trong một controller cụ thể. Tuy nhiên, chúng ta còn có thể share chúng giữa các controller, sử dụng OOP (lập trình hướng đối tượng).

Share filter giữa các controller thông qua ApplicationController

Cách thông thường nhất để tái sử dụng filter giữa các controller đó là di chuyển đoạn code filter này ra ApplicationController. Bởi vì tất cả các controller khác của chúng ta kế thừa từ ApplicationController, các controller này sẽ có quyền truy cập đến các filter này.

 

class ApplicationController < ActionController::Base

  protect_from_forgery

  private

  def load_article

    @article = Article.find(params[:id])

  end

end



class ArticlesController < ApplicationController

  before_filter :load_article, only: [:show, :edit, :update, :destroy]

  # Actions...

end

 

Nhưng các bạn có thấy điều gì bất thường ở trong đoạn code trên hay không? Đúng rồi, ngoài controller ArticlesController ra, các controller khác đâu cần biến @article làm gì. Vậy có cách nào để:

  • ArticlesController thì tạo ra biến @article
  • PostsController thì tạo ra biến @post
  • OrdersController thì tạo ra biến @order
  • … 

hay không?

Sử dụng một chút metaprogramming, chúng ta có thể làm được. Chúng ta sẽ sửa method load_article lại thành find_resource. Trong method này, sử dụng params[:controller] để suy ra model tương ứng, kiểu như sau:

  • ArticlesController -> Model: Article -> biến: @article
  • PostsController -> Model: Post -> biến: @post
  • OrdersController -> Model: Order -> biến: @order

 

class ApplicationController < ActionController::Base

  protect_from_forgery

  private

  def find_resource

    class_name = params[:controller].singularize

    klass = class_name.camelize.constantize

    self.instance_variable_set "@" + class_name, klass.find(params[:id])

  end

end


class ArticlesController < ApplicationController

  before_filter :find_resource, only: [:show, :edit, :update, :destroy]

  # Actions...

end

Như vậy qua bài viết này, mình cùng với các bạn đã đi qua các kỹ thuật áp dụng Filter trong Rails để khiến code trở nên ngắn gọn và clean hơn. Hi vọng bài viết giúp ích phần nào đó cho các bạn trên con đường sử dụng framework Ruby on Rails trong công việc của mình. Các bạn có chia sẻ nào thêm có thể để dưới phần bình luận nhé.

References:

Subscribe
Notify of
0 Comments
Inline Feedbacks
View all comments