---
title: "نقدّم ActiveForm لـ Ruby on Rails"
description: "أستخدم form objects منذ وقت طويل وأكرر الأنماط نفسها مرة بعد مرة، فطوّرت هذه المكتبة الصغيرة لتتولى العمل التأسيسي عنكم."
canonical: https://oozou.com/ar/blog/introducing-activeform-for-ruby-on-rails-50
locale: ar
date: 2020-07-19
author: "OOZOU Team"
---

# نقدّم ActiveForm لـ Ruby on Rails

> أستخدم form objects منذ وقت طويل، ووجدت نفسي أكرر الأنماط نفسها مرة بعد مرة، فطوّرت هذه المكتبة الصغيرة لتتولى العمل التأسيسي عنكم.

Published: 2020-07-19 · Author: OOZOU Team (Engineering) · 10 min read

# ما هي form objects؟

تناولت حالات استخدام form objects في [هذا المقال عن التخلص من النماذج المتضخمة (fat models)](https://oozou.com/blog/ensuring-maintainability-in-large-rails-apps-fat-controllers-done-right-34?preview_token=dUGoP6F5iWuBKq8gU45tMVHp)، لكنني أردت هنا التعمق أكثر في كيفية استخدامي لها وسببه. فأنا مقتنع تمامًا بفائدة هذه الكائنات، وقدرتها على تجريد المنطق وعزله ببساطة وفعالية لا مثيل لها في رأيي.
التعريف الأساسي للـ form object أنه صنف (class) يحتوي على attributes قابلة للكتابة، وعمليات تحقق (validations)، ومنطق لحفظ هذه الـ attributes في كائنات ActiveRecord. ويمكن أن تتضمن هذه الـ forms آثارًا جانبية أخرى، مثل تشغيل background jobs وإرسال رسائل البريد الإلكتروني والإشعارات الفورية (push notifications) وغيرها.
وأبسط طريقة لفهم الفكرة أن تعتبروها تمثيلًا لـ action في الـ controller، يُنقل إليه كل منطق الأعمال الذي يجري في تلك الـ action. ويمكن أيضًا عرضها أو تقديمها أو تحويلها (serialize) مثل كائنات ActiveRecord.

وكما في النموذج الورقي أعلاه، هناك سبب آخر قد يدفعنا إلى استخدام الـ forms: فهي أكثر منطقية من الناحية المفاهيمية. إذ كثيرًا ما تحتوي الـ endpoints على منطق وبيانات تخص أكثر من كائن ActiveRecord. ومع الـ forms يمكننا رسم حدود حول مفهوم معيّن، فمثلًا نموذج الإقرار الضريبي يجمع بيانات المستخدم وبيانات الدخل وبيانات الاستقطاعات وغيرها. ومع TaxReturnForm يمكننا تغليف هذا المنطق وتحويله إلى مفهوم ملموس داخل تطبيقاتنا.
وربما نحتاج إلى التحقق من أن المستخدم أدخل رقم هويته عند تقديم الإقرار الضريبي، لا عند تحديث بريده الإلكتروني في الحساب. ومع الـ forms يمكننا حصر هذه الـ validations في الكائنات الصحيحة، فتصبح التطبيقات أنظف والمسؤوليات أوضح. ولا حاجة بعد الآن إلى attributes مخصصة تخبر نماذج ActiveRecord متى تشغّل validation ومتى تتجاوزها، فهذا معرَّف ضمنيًا.

# لنرَ الكود!

لننظر في كيفية كتابة الـ forms باستخدام ActiveForm

- تحتوي وحدة ActiveForm::Base على كل المنطق اللازم لإعداد أدوات الـ form المساعدة.
- تحتوي وحدة ActiveForm::SimpleFormHelpers على منطق إضافي يتيح لـ simple_form اكتشاف أنواع الحقول تلقائيًا.
- تتضمن ActiveRecord::Base بعض الدوال المتعلقة بتحويل الأنواع (typecasting) غير المتوفرة بعد في ActiveModel::Attributes، ولحسن الحظ يسهل إعدادها. ويمكنكم بدلًا من ذلك تحديد خيار 'as:' في حقول simple_form، مثل `<%= f.input :date_of_birth, as: :date %>`

```ruby
class UserRegistrationForm
  include ActiveForm::Base
  include ActiveForm::SimpleFormHelpers

  attr_accessor :user

  attribute :first_name
  attribute :last_name
  attribute :email
  attribute :password

  with_options presence: true do
    validates :first_name
    validates :last_name
    validates :email, format: { with: Devise.email_regexp, allow_blank: true }
    validates :password, length: { minimum: 8, maximum: 100 }
  end

  validate :email_unique?, if: -> { email.present? }

  def process
    return false unless valid?

    self.user = User.create(attributes)
    UserRegistrationJob.perform_later(user)
    UserMailer.registration_confirmation(user).deliver_later
    true
  end

  # uniqueness validations need to be done manually but we can still use the
  # default messages
  private def email_unique?
    return true unless User.exists?(email: email)

    errors.add(:email, :taken)
  end
end
```

**هل ما زلت بحاجة إلى strong params؟** يأتي ActiveForm مع قائمة سماح (allow-list) للـ attributes افتراضيًا. فأي attribute في القائمة مسموح به، وأي attribute معرَّف كـ `attr_{accessor,reader,writer}` لن يُملأ إذا مُرّر ضمن الـ params. وهذا يعني أننا لم نعد بحاجة إلى strong_params في الـ controllers، لأن الـ form تعرّف بوضوح ما تتوقعه وتحمينا بحكم تصميمها.**ماذا لو احتجت إلى تمرير resource؟** في المثال أعلاه ننشئ مستخدمًا، لكن ربما نريد تحديث مستخدم موجود. في هذه الحالة قد نرغب في تمرير current_user، ويمكننا ذلك بتعريف attr_accessor ثم تمرير current_user عند تهيئة الـ form. على سبيل المثال:

```ruby
UserEditForm.new(params: params, user: current_user)
```

**لماذا process وليس save؟** الأمر متروك لكم تمامًا، لكن من الجيد الالتزام بأسلوب موحّد داخل الفريق لتجنب الالتباس. بدأت باستخدام save، ثم وجدت أن هناك حالات لا تحفظ فيها الـ form أي شيء، كأن تكتفي بتشغيل job أو إرسال إشعار فوري. ووجدت أن process تناسب حالات أكثر، فاعتمدتها. وهي عادةً الدالة العامة (public) الوحيدة في الـ forms التي أكتبها.**كيف أتعامل مع الإخفاق عند حفظ عدة كائنات؟** أنصح باستخدام كتل ActiveRecord::Base.transaction لأي منطق يحدّث أكثر من جدول. لكن بعض الإجراءات أحيانًا لا يمكن التراجع عنها، وهذا للأسف أمر لا مفر منه. فمثلًا، لنفترض أننا نرسل رسائل بريد إلكتروني أثناء استدعاء process. إذا أرسلنا الرسالة قبل حفظ السجل ثم فشل الحفظ، فماذا نفعل؟ لا يمكننا استرجاع الرسالة. وعلى المطوّر حل هذه المسألة في كل حالة على حدة. عادةً أحفظ كل ما أستطيع أولًا، ثم أستدعي الآثار الجانبية بعد ذلك، فإذا فشلت أتعامل معها في مكان آخر وأعيد المحاولة عند الحاجة.**لماذا عليّ كتابة uniqueness validations بنفسي؟** يقوم ActiveRecord ببعض السحر داخليًا لتعمل uniqueness validations. فعند التحقق من التفرد على نسخة AR يكون self هو الكائن نفسه، فتعرف Ruby أي attribute تقارنه بالسجلات الأخرى في قاعدة البيانات. وهذا مهم عند التحقق من تفرد سجل محفوظ مسبقًا: فإذا كنتم تكتفون بالتحقق من وجود قيمة الـ attribute في قاعدة البيانات، فعليكم في حالة السجل المحفوظ أن تستثنوا الكائن الحالي من البحث. ومع أن تنفيذ ذلك في ActiveForm ممكن تقنيًا، فإنه ليس مباشرًا. وكتابة uniqueness validations بأنفسكم سهلة، لذلك رأيت أن من الأفضل ترك ذلك للمطوّر

# كيف نستخدم هذا في الـ controllers؟

```ruby
class RegistrationsController
  def new
    @form = UserRegistrationForm.new
  end

  def create
    @form = UserRegistrationForm.new(params: params[:form])

    if @form.process
      redirect_to welcome_path, notice: 'Welcome, your registration was successful!'
    else
      render :new
    end
  end
end
```

تهيئة الـ forms بسيطة، إذ يمكنكم تمرير الـ params عبر الـ keyword argument المسمّى params. وتذكروا أننا لم نعد بحاجة إلى strong params، لأن الـ forms ترفض المعاملات غير الصالحة نيابةً عنا، فيمكننا تمرير الـ params الخام مباشرة (مع مراعاة أن تمرروا params الخاصة بالـ form لا الـ params الجذرية، فإذا سمّيتم الـ instance variable باسم form، يمكنكم دائمًا تمرير params[:form]).
ويمكننا عرض الـ forms في الـ views، وبما أننا نضمّن ActiveForm::Validations يمكننا أيضًا عرض رسائل الخطأ. وستبدو الـ controllers مألوفة جدًا كما لو كنا نستخدم نماذج ActiveRecord، لكننا نستفيد من عزل المنطق الخاص بكل طلب بدلًا من حشره في نموذج ActiveRecord يُستخدم في عدة controllers أو actions.
ومع أنه يمكنكم نقل منطق مصادقة المستخدم إلى الـ forms، فإن Devise مستخدمة على نطاق واسع وهناك طرق راسخة لإدارة المصادقة. لذلك أنصح باتباع الطريقة المعتادة: استدعوا authenticate_user في الـ controllers، ثم مرّروا current_user إلى الـ form كـ resource. والأمر نفسه ينطبق على الـ policies، فأنا أحب pundit وأواصل استخدامها خارج الـ forms، ولا أرى بأسًا في ذلك.

# كيف يبدو عرض الـ form؟

```erb
# app/views/registrations/new.html.erb
<%= simple_form_for @form, url: registrations_path, method: :post do |f| %>
  <%= f.input :first_name %>
  <%= f.input :last_name %>
  <%= f.input :email %>
  <%= f.input :password %>
  <%= f.submit 'Register' %>
<% end %>
```

لا مفاجآت هنا، فهو يكاد يطابق forms الخاصة بـ ActiveRecord. وأنا أحب simple_form كثيرًا، لذلك أضفت بعض الأدوات المساعدة لتحديد نوع الحقل بشكل صحيح، ويمكنكم استخدامها بتضمين وحدة ActiveForm::SimpleFormHelpers في الـ form.
لاحظوا أننا نحدد الـ url والـ method صراحةً. والسبب أن ActiveRecord يوفر الدالة persisted? التي تستخدمها form_for وsimple_form_for لمعرفة ما إذا كان السجل جديدًا أم موجودًا، فتغيّر الـ method تلقائيًا من post إلى put وتغيّر نص زر الإرسال. وأفضّل التصريح بذلك لأنه أوضح وأقل اعتمادًا على السحر. لكن إن شئتم، يمكنكم بسهولة تعريف هذه الدالة في الـ forms: يكفي تعريف def persisted? وإرجاع true أو false.

# هل هذا كل شيء؟

نعم تقريبًا. إنها أداة صغيرة تحمل إمكانات كبيرة لبناء تطبيقات سهلة الصيانة.
وهناك بعض المزايا الإضافية أيضًا.
**الـ callbacks: before_initilization وafter_initialization**أستخدم هذه الـ callbacks كثيرًا للبحث عن attributes أو resources أو تهيئتها، على سبيل المثال:

```ruby
attr_accessor :user, :device

attribute :device_uid, :uuid

before_initialization -> { self.device = user.device if user.present? }
before_initialization -> { self.device_uid = SecureRandom.uuid }
```

**حقول التاريخ**تتعامل Rails مع حقول التاريخ بطريقة مختلفة عن الحقول الأخرى. فعند إنشاء date select تقسم Rails الحقل إلى 3 قوائم اختيار، وتقسم حقول الوقت إلى 5: السنة والشهر واليوم والساعة والدقيقة. وتُرسل هذه القيم إلى الـ backend لا بصيغة `params: { 'date' => '01/01/2001' }` بل بصيغة `params: { 'date(1i)' => '01', 'date(2i)' => '01', 'date(3i)' => '2001' }`. ويحوّل ActiveForm هذه التواريخ والأوقات تلقائيًا بحسب نوع الـ attribute، فمثلًا إذا وُجد attribute :date, :date فسيحاول دمج هذه الـ params في كائن تاريخ قبل إسناده إلى الـ form.**الـ attributes المنطقية (Boolean)**تأتي الـ attributes المنطقية افتراضيًا مع دالة بديلة تنتهي بعلامة استفهام. فكلنا نعرف أن أعمدة boolean في ActiveRecord لها دالتا وصول، مثل admin وadmin?، وسيتصرف ActiveForm بالطريقة نفسها، فمثلًا attribute :admin, boolean سينشئ اسمًا بديلًا admin? للـ attribute المسمى admin.**دالة 'attributes'**تُحوَّل نتيجة ActiveRecord::Base#attributes إلى indifferent_access، بخلاف ActiveModel::Model#attributes. وهذا يعني أن استدعاء attributes[:email] يفشل، بينما attribute['email'] لا يفشل. لذلك يعيد ActiveForm تعريف attributes ويستدعي indifferent_access على النتيجة، فنستطيع الوصول إليها بمفاتيح نصية أو symbols.**قابلية توسيع ActiveModel::Attributes**
يمكن توسيع ActiveModel::Attributes، فتستطيعون إنشاء أنواعكم الخاصة بسهولة. وقد أنشأت كثيرًا منها طوال فترة استخدامي لهذه المكتبة. فمثلًا لديّ EmailType يحوّل قيم البريد الإلكتروني إلى أحرف صغيرة تلقائيًا، فلا مزيد من عناوين البريد بأحرف مختلطة (ويمكنكم بالطبع فعل ذلك في postgres أيضًا). [ولا تنسوا تسجيل أنواعكم لدى Rails في initializer](https://api.rubyonrails.org/classes/ActiveRecord/Type.html#method-c-register)

```ruby
class EmailType < ActiveModel::Type::String
  def cast(value)
    return nil unless value.is_a?(String) && value.present?
    super(value.downcase)
  end
end
```

# هل هذه gem؟

أرى في الوقت الحالي أن تحويل ActiveForm إلى gem سابق لأوانه، فلا تزال هناك أمور تحتاج إلى تحسين وحسم. وسأخصص يومًا ما مزيدًا من الوقت لجعلها قابلة للتوسيع وإطلاقها كـ gem، أما الآن فأرى ذلك تعقيدًا لا داعي له.
وإذا أردتم استخدامها، [فنزّلوها من حسابنا على GitHub](https://github.com/oozou/active_form)، واستوردوها إلى تطبيقكم، وابدأوا ببناء form objects خاصة بكم. ونرحب بآرائكم تمامًا، كما نقبل الـ pull requests. وسأواصل استخدام form objects ورفع التغييرات إلى المستودع حين أرى أنها مفيدة وذات صلة للجميع.
وسبب عدم تحويل ActiveForm إلى gem أن المنطق المخصص الخارج عن Rails core صغير نسبيًا، ومع أن ActiveForm تناسب حاليًا 99% من حالات استخدامي، فإنني أعدّلها أحيانًا بحسب متطلبات المنتج. واستيراد المكتبة يدويًا إلى app/lib/active_form/ يجعل التعديل أبسط بكثير. كما استفدت من أكبر عدد ممكن من وحدات Rails لتسهيل الصيانة، فيكفي أن أحدّث Rails لأحصل على التحديثات مجانًا. وبفضل الاعتماد على وحدات Rails core، فهي مكتبة صغيرة جدًا: لا تحتوي وحدة Base إلا على 10 دوال!
ويمكنكم الاطلاع على الكود المصدري في المستودع أعلاه، وهو ملفان مع كثير من التعليقات. وإذا احتجتم إلى مساعدة في فهم أي جزء، فلا تترددوا في التواصل معي، ويسعدني شرح ما لم يتضح لكم. والتعليقات تشرح الكثير، فأرجو ألا تحتاجوا إلى ذلك. وتواصلوا معي أيضًا إذا كانت لديكم أفكار لتحسينها.

---

Contact OOZOU: hello@oozou.com · +66 2544 0000 · https://oozou.com/ar/contact

More for AI agents: https://oozou.com/llms.txt · every page has a markdown twin at its URL plus ".md"
