ผจญภัยใน will_paginate เพื่อเพิ่ม first_item ใน RailsSpace (ภาค 3)
ต่อจากตอนที่แล้ว…ว่า Rails เหมือนกับจะไม่โหลด class WillPaginate::Collection จาก gem ขึ้นมาเพื่อ extend definition ที่เราสร้างไว้ใน lib/will_paginate/collection.rb เลยทดลองเปลี่ยนโค้ดโดยใช้ class_eval แทน
WillPaginate::Collection.class_eval do def first_item offset + 1 end end
เพราะคิดว่าการเรียกใช้ class จะทำให้ Rails โหลด gem ให้ แต่วิธีนี้มีสองปัญหา คือหนึ่ง Rails ไม่ได้โหลด gem ให้และสอง การที่ไม่ได้ define class แบบ class Collection ทำให้ Rails ยิง error ว่า
Expected /Users...rails_space/lib/will_paginate/collection.rb to define WillPaginate::Collection /opt/.../activesupport-2.3.2/lib/active_support/dependencies.rb:426:in `load_missing_constant'
จาก stack trace เราสรุปได้ว่า Rails รับผิดชอบในการโหลด dependencies ต่างๆ ไม่ใช่ Ruby เพราะฉะนั้น Rails สามารถที่เลือกโหลดหรือไม่โหลดไฟล์ได้ ข้อสังเกตุที่สองคือวิธีการโหลด Collection class นั้นต่างกับวิธีการโหลด class อื่นๆที่มาจาก gem ของ Collection class เป็นแบบนี้
/...activesupport-2.3.2/lib/active_support/dependencies.rb:426:in `load_missing_constant' /...activesupport-2.3.2/lib/active_support/dependencies.rb:80:in `const_missing' /...rails_space/lib/will_paginate/collection.rb:1 /...activesupport-2.3.2/lib/active_support/dependencies.rb:380:in `load_without_new_constant_marking' /...activesupport-2.3.2/lib/active_support/dependencies.rb:380:in `load_file' /...activesupport-2.3.2/lib/active_support/dependencies.rb:521:in `new_constants_in' /...activesupport-2.3.2/lib/active_support/dependencies.rb:379:in `load_file' /...activesupport-2.3.2/lib/active_support/dependencies.rb:259:in `require_or_load' /...activesupport-2.3.2/lib/active_support/dependencies.rb:425:in `load_missing_constant' /...activesupport-2.3.2/lib/active_support/dependencies.rb:80:in `const_missing' /...rails_space/lib/will_paginate/collection.rb:1 /...rubygems/custom_require.rb:31:in `gem_original_require' /...rubygems/custom_require.rb:31:in `require' /...activesupport-2.3.2/lib/active_support/dependencies.rb:156:in `require' /...activesupport-2.3.2/lib/active_support/dependencies.rb:521:in `new_constants_in' /...activesupport-2.3.2/lib/active_support/dependencies.rb:156:in `require'
ส่วนของ class อื่นๆจะประมาณนี้
/...gems/mislav-will_paginate-2.3.8/lib/will_paginate/array.rb:1 /...rubygems/custom_require.rb:31:in `gem_original_require' /...rubygems/custom_require.rb:31:in `require' /...activesupport-2.3.2/lib/active_support/dependencies.rb:156:in `require' /...activesupport-2.3.2/lib/active_support/dependencies.rb:521:in `new_constants_in' /...activesupport-2.3.2/lib/active_support/dependencies.rb:156:in `require'
จะเห็นได้ว่า แทนที่จะโหลด Collection class จาก gem จริงๆแล้ว Rails พยายามโหลด Collection มาจาก lib/will_paginate/collection.rb ของเราต่างหาก
หลังจากอุตส่าห์บ้าลองนู่นนี่อยู่นาน เพิ่งนึกได้ว่ามีหนังสือ The Rails Way อยู่ เลยลองเปิดดูบทแรก ปรากฏว่านี่มันเป็นพฤติกรรมที่มีมาตั้งแต่ Rails 1.2 แล้วนี่หว่า – -”
คือ Rails จะโหลด class หรือ module ที่ยังไม่ถูกโหลดขึ้นมาโดยอัตโนมัติ โดยพยายามหาจาก load_paths ที่ define ตอน Rails บู๊ตขึ้นมา สำหรับกรณีนี้ สรุปว่าปัญหาน่าจะเกิดจากการที่ /lib อยู่ใน load_paths ก่อน gem path นั่นเอง ลองดูได้จาก
$ script/console >> $:
(คลื่นแทรก: ลอง google ดู rails auto-loading ปรากฏว่ามีคนเจอปัญหาแบบเดียวกันเดี๊ยเลย ที่นี่)
เอาล่ะ หลังจากบ้าๆบอๆมานาน ก็ถึงวิธีแก้ เพราะไม่อยากมีภาคต่อแล้ว มันยาวเกิน
วิธีแก้วิธีแบบแรกก็คือใช้โค้ดแบบในลิงก์คนที่เจอปัญหาแบบเดียวกันนี่แหละ เพิ่มโค้ดนี้ต่อท้ายสุดใน environment.rb
module WpCollectionExtensions
def first_item
offset + 1
end
end
WillPaginate::Collection.instance_eval { include WpCollectionExtensions }
แบบทีสองคือใช้ after_initialize ของ config ใน environment.rb ประมาณว่าเพิ่มโค้ดนี้ก่อนจบ block ใน environment.rb วิธีนี้คือการเพิ่ม hook ให้ Rails รันโค้ดใน block ที่ส่งไปหลัง initialize ทุกอย่างเสร็จเรียบร้อย
config.after_initialize do class WillPaginate::Collection def first_item offset + 1 end end end
จริงๆแล้วสองวิธีนี้มันก็ไม่ต่างกันเท่าไรหรอก เพราะว่ามันรอจน Rails บู๊ตจบกระบวนความแล้วก็เอาโค้ดมาใช้
วิธีที่สามคือสร้างไฟล์ไว้ใน config/initializers แล้วเอาโค้ดไปใส่ไว้ในนี้แทน เพราะ Rails โหลดไฟล์ที่มีนามสกุล .rb ใน initializers นี้ด้วย เช่น สร้างไฟล์ชื่อ config/initializers/extend_will_paginate.rb แล้วใส่โค้ดเดียวกันก็ได้
class WillPaginate::Collection def first_item offset + 1 end end
เป็นอันจบซีรี่ส์โคตรยาวซะที จริงๆคงมีวิธีอื่นอีก แต่แค่นี้ก็เวิร์คแล้ว ไม่รู้จะหามันเพิ่มไปทำไมอีก