HELLO, I’M SERHAT AND THIS IS MY FANCY TITLE.

Rails - Callbacks

Kullanıcılardan herhangi biri hesabını iptal ettiği zaman adminlere bilgilendirme maili gitmesi, bir işlem tetiklendiğinde - başka bir işlemin de gerçekleşmesi veya bir model nesnesi oluşturduğunuzda, başka bir model nesnesinin de onla ilişkili olarak oluşması gibi durumları tanımlarken ihtiyaç duyduğumuz metodların en sık kullanılanları Rails'te 6 tanedir. Bunlar:

  • before_create
  • after_create
  • before_save
  • after_save
  • before_destroy
  • after_destroy

Zaten bu ifadelerin ne iş yaptığı isimlerinden de anlaşıldığı için tekrarlamayacağım.

before ile başlayan herhangi bir callback false döndürdüğü taktirde, uygulamanızın çalışma sürecini durdurabileceği için dikkatli kullanılmalıdır.

def before_create
  false
end

İlerde uygulamanız dallanıp budaklandığında modeliniz kayıt işlemi yapmıyorsa öncelikli olarak before_* ifadelerini kontrol etmek faydalı olabilir.

Örneğin kullanıcılardan biri blog yazınıza yorum yazdığı zaman, yazının yazarına bilgilendirme emaili gitmesini istiyorsunuz varsayalım. Comment modeli içerisinde böyle bir durumu aşağıdaki gibi kurgularız:

class Comment < ActiveRecord::Base
  belongs_to :article

  validates_presence_of :name, :email, :body
  validate :article_should_be_published

  def article_should_be_published
    errors.add(:article_id, "is not published yet") if article&& !article.published?
  end

  def after_create
    puts "We will notify the author in Chapter 9"
  end
end

create işleminden sonra yapılmasını istediğiniz işleri doğrudan after_create metodu içerisine yazabilirsiniz. Ancak bu yöntem oldukça pratik olmasına rağmen, farklı işlemleri yapmak için çalışan kodları ard arda yazdığınızda kodunuzun okunabilirliği azalacaktır. Bu yüzden aşağıdaki gibi bir yol izleyerek her bir eylemi ayrı metod olarak tanımlamak ve daha sonra bunları after_create'ten sonra virgülle ayırarak çağırarak daha mantıklı olur:

class Comment < ActiveRecord::Base
  belongs_to :article

  validates_presence_of :name, :email, :body
  validate :article_should_be_published

  after_create :email_article_author

  def article_should_be_published
    errors.add(:article_id, "is not published yet") if article && !article.published?
  end

  def email_article_author
    puts "We will notify #{article.user.email} in Chapter 9"
  end
end

User Modelinin Güncellenmesi

User modelimizin altında bulunan "password" alanı, şifreleri plain-text olarak muhafaza ettiği için güvenlik zaaflarına sebebiyet verebilir. Bu tür hassas dataları her zaman encrypt etmek gereklidir. Bu yüzden öncelikle veritabanında ki "password" alanını "hashed_password" olarak tekrar adlandıralım:

$ rails generate migration rename_password_to_hashed_password

Şimdi migration dosyamızı hazırlayalım:

class RenamePasswordToHashedPassword < ActiveRecord::Migration
  def change
    rename_column :users, :password, :hashed_password
  end
end

Migration'ı çalıştıralım:

$ rake db:migrate

Şimdi ise User modelimizi bu değişikliğe uygun şekilde hazırlayalım:

require 'digest'
class User < ActiveRecord::Base

  attr_accessor :password

  # Validations
  validates_uniqueness_of :email
  validates_length_of :email, :within => 5..50
  validates_format_of :email, :with => /^[^@][\w.-]+@[\w.-]+[.][a-z]{2,4}$/i
  validates_confirmation_of :password
  validates_length_of :password, :within => 4..20
  validates_presence_of :password, :if => :password_required?

  # Relations
  has_one :profile
  has_many :articles, ->{order('published_at DESC, title ASC')}, :dependent => :nullify
  has_many :replies, :through => :articles, :source => :comments

  # Callbacks
  before_save :encrypt_new_password

  # Authentication
  def self.authenticate(email, password)
      user = find_by_email(email)
      return user if user && user.authenticated?(password)
  end

  def authenticated?(password)
      self.hashed_password == encrypt(password)
  end

  protected

  def encrpyt_new_password
      return if password.blank?
      self.hashed_password = encrypt(password)
  end

  def password_required?
      hashed_password.blank? || password.present?
  end

  def encrypt(string)
      Digest::SHA1.hexdigest(string)
  end
end

Ruby'nin built-in kütüphanelerinden biri olan Digest ile şifreleri hash'leyebiliriz ancak bu örnekte kullanılan SHA1 şifreleme algoritmasının production ortamı için pekte kullanışlı olduğu söylenemez. Production için BCrypt kullanımı düşünülebilir.

Şimdi hazırladığımız User modelini satır satır inceleyelim:

  • require 'digest' => Şifreleri encrypt edebilmek için Ruby'nin built-in kütüphanelerinden biri olan Digest'i çağırdık.

  • attr_accessor :password => Burada Ruby'ye "password" için reader ve writer metodları oluşturmasını söyledik çünkü veritabanımızda artık "password" diye bir alan bulunmuyor ve bu yüzden de "password" isminde bir metod Active Record tarafından otomatik olarak oluşturulmuyor. Sonuç olarak "password"ü hala bir şekilde encrypt edilmeden önce set etmeye ihtiyacımız olduğu için kendi "niteleyicimizi (attribute)" oluşturduk. Bu niteleyici herhangi bir model niteleyicisi gibi çalışmasına rağmen, model kaydedildiğinde veritabanına kayıt edilmez.

  • before_save :encrypt_new_password => Buradaki before_save callback'i Active Record'a, kayıt yapmadan önce encrypt_new_password metodunu çalıştırmasını söylüyor. Burada kayıt yapmaktan kasıt hem create hemde update işlemi.

  • encrypt_new_password => Bu metod sayesinde yalnızca "password" alanı dolu ise şifrenin hashlenmesi sağlanır. Aksi halde mevcut olan şifre korunur. Böylece, kullanıcı şifresini güncellemek istemediğinde şifresini encrypt etmemiş oluruz. Eğer password alanı boş ise, return if password.blank? ile metoddan işlem yapmadan dönebiliriz. Ancak password alanı dolu ise metodumuz self.hashed_password = encrypt(password) ile girilen password'ü şifreyecektir.

  • encrypt => Bu metod Digest kütüphanesini kullanarak, ona gönderilen veriyi SHA1 ile şifreler. Ayrıca şifreleme sonucunu yani hash'i döndürür.

  • password_required? => Kullandığımız validasyonları pratikleştirmek için hazırladığımız bir metodtur. Bu metod sayesinde hashed_password niteleyicisinin boş olup olmadığını - veya "password" erişicisinin (accessor) yeni bir şifrenin oluşturulmasında kullanılıp kullanılmadığını kontrol ettirebiliriz.

  • self.authenticate => Metodun ismine bakarak bunun bir class metodu olduğunu yani instance yerine doğrudan bir class üzeride çalıştığını söyleyebiliriz. Yani bu metoda bir instance üzerinden değil, doğrudan class üzerinden erişiriz. Örneklemek gerekirse; @user = User.new & @user.authenticate şeklinde erişmek yerine doğrudan self.authenticate şeklinde erişim sağlarız. Authenticate metodumuz biri e-mail adresi diğeri ise plain-text şifre olmak üzere iki tane argüman alıyor ve find_by_email metodu sayesinde ilgili e-mail adresi ile eşleşen kullanıcının bulunabilmesini sağlıyor. Eğer metod sayesinde kullanıcı bulunduysa zaten user değişkenine atanıyor, bulunamadıysa bu değişken nil olarak kalıyor. Metod içerisinde belirtilen return user if user && user.authenticated?(password) ifadesi sebebiyle metodumuz ancak user bulunabildiğinde ve bu user authenticated olduğunda true döndürecektir.

  • authenticated? => Bu metod basitçe girilen password'ü önce hash'ler, daha sonra ise saklanmış olan mevcut hash ile karşılaştırır. Eğer bunlar eşleşiyorsa true döndürür, eşleşmiyor ise false döndürür.

Hazırladığımız uygulamayı test edelim:

>> user = User.first
  => #<User id: 1, email: "user@example.com", ..>

>> user.password = "secret"
  => "secret"

>> user.password_confirmation = "secret"
  => "secret"

>> user.save
  => true

>> user.hashed_password
  => "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4"

>> User.authenticate("user@example.com", "secret")
  => #<User id: 1, email: "user@example.com", ...>

>> User.authenticate("user@example.com", "secret2")
  => nil

>> second_user = User.last
  => #<User id: 2, email: "mary@example.com", ...>

>> second_user.update_attributes(:password => "secret", :password_confirmation => "secret")
  => true

>> User.authenticate("mary@example.com", "secret")
  => #<User id: 2, email: "mary@example.com", ...>

Sürecin Özeti

  • Authenticate metoduna email adresi ve plain-text şifre olmak üzere iki parametre gönderiyoruz.
  • Gönderdiğimiz şifre metod tarafından hashlenir ve veritabanında hash'li olarak tutulan şifreyle karşılaştırır.
  • Eğer hash'ler eşleşirse authentication başarılı olur, eşleşmez ise başarısız olarak nil döndürür.

Son olarak, eklediğimiz bu yeni özellikler için db/seeds.rb dosyamızı düzenleyelim:

user = User.create :email => "msdundars@gmail.com", :password => "123456", :password_confirmation => "123456"
user.profile.create :user_id => 1, :name => "M.Serhat Dündar", :birthday => "08-09-1990", :bio => "Super-human", :color => "Black", :twitter => "msdundars"
Category.create [
  {:name => "Eğitim Bilimleri"},
  {:name => "Genel"},
  {:name => "Seyehat Notları"},
  {:name => "Avusturya"},
  {:name => "Danimarka"},
  {:name => "Estonya"},
  {:name => "İsveç"},
  {:name => "Letonya"},
  {:name => "Macaristan"},
  {:name => "Slovakya"},
  {:name => "Tavsiyeler"},
  {:name => "Yunanistan"},
  {:name => "Teknik Yazılar"},
  {:name => "Front-End"},
  {:name => "Güvenlik"},
  {:name => "Hosting"},
  {:name => "Linux"},
  {:name => "MySQL"},
  {:name => "PHP"},
  {:name => "Programlama"},
  {:name => "Python"},
  {:name => "Rails"},
  {:name => "Ruby"}
]
user.articles.create :title => "Hello World!", :body => "Hi, this is a brand new blog! I am gonna write about Rails in this blog", :published_at => Date.today, :excerpt => "Hi, this is my first post", :location => "Samsun"

Comment.create :name => 'Annoying Guest Commenter', :email => 'spammer@gmail.com', :body => 'Great article!', :article_id => 1

Başarılar.


Share this post!


Blog Comments powered by Disqus.