# Models - DHH Rails Style ## Concerns for Horizontal Behavior Models heavily use concerns. A typical Card model includes 14+ concerns: ```ruby class Card < ApplicationRecord include Assignable include Attachments include Broadcastable include Closeable include Colored include Eventable include Golden include Mentions include Multistep include Pinnable include Postponable include Readable include Searchable include Taggable include Watchable end ``` Each concern is self-contained with associations, scopes, and methods. **Naming:** Adjectives describing capability (`Closeable`, `Publishable`, `Watchable`) ## State as Records, Not Booleans Instead of boolean columns, create separate records: ```ruby # Instead of: closed: boolean is_golden: boolean postponed: boolean # Create records: class Card::Closure < ApplicationRecord belongs_to :card belongs_to :creator, class_name: "User" end class Card::Goldness < ApplicationRecord belongs_to :card belongs_to :creator, class_name: "User" end class Card::NotNow < ApplicationRecord belongs_to :card belongs_to :creator, class_name: "User" end ``` **Benefits:** - Automatic timestamps (when it happened) - Track who made changes - Easy filtering via joins and `where.missing` - Enables rich UI showing when/who **In the model:** ```ruby module Closeable extend ActiveSupport::Concern included do has_one :closure, dependent: :destroy end def closed? closure.present? end def close(creator: Current.user) create_closure!(creator: creator) end def reopen closure&.destroy end end ``` **Querying:** ```ruby Card.joins(:closure) # closed cards Card.where.missing(:closure) # open cards ``` ## Callbacks - Used Sparingly Only 38 callback occurrences across 30 files in Fizzy. Guidelines: **Use for:** - `after_commit` for async work - `before_save` for derived data - `after_create_commit` for side effects **Avoid:** - Complex callback chains - Business logic in callbacks - Synchronous external calls ```ruby class Card < ApplicationRecord after_create_commit :notify_watchers_later before_save :update_search_index, if: :title_changed? private def notify_watchers_later NotifyWatchersJob.perform_later(self) end end ``` ## Scope Naming Standard scope names: ```ruby class Card < ApplicationRecord scope :chronologically, -> { order(created_at: :asc) } scope :reverse_chronologically, -> { order(created_at: :desc) } scope :alphabetically, -> { order(title: :asc) } scope :latest, -> { reverse_chronologically.limit(10) } # Standard eager loading scope :preloaded, -> { includes(:creator, :assignees, :tags) } # Parameterized scope :indexed_by, ->(column) { order(column => :asc) } scope :sorted_by, ->(column, direction = :asc) { order(column => direction) } end ``` ## Plain Old Ruby Objects POROs namespaced under parent models: ```ruby # app/models/event/description.rb class Event::Description def initialize(event) @event = event end def to_s # Presentation logic for event description end end # app/models/card/eventable/system_commenter.rb class Card::Eventable::SystemCommenter def initialize(card) @card = card end def comment(message) # Business logic end end # app/models/user/filtering.rb class User::Filtering # View context bundling end ``` **NOT used for service objects.** Business logic stays in models. ## Method Naming **Verbs** - Actions that change state: ```ruby card.close card.reopen card.gild # make golden card.ungild board.publish board.archive ``` **Predicates** - Queries derived from state: ```ruby card.closed? # closure.present? card.golden? # goldness.present? board.published? ``` **Avoid** generic setters: ```ruby # Bad card.set_closed(true) card.update_golden_status(false) # Good card.close card.ungild ``` ## Validation Philosophy Minimal validations on models. Use contextual validations on form/operation objects: ```ruby # Model - minimal class User < ApplicationRecord validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP } end # Form object - contextual class Signup include ActiveModel::Model attr_accessor :email, :name, :terms_accepted validates :email, :name, presence: true validates :terms_accepted, acceptance: true def save return false unless valid? User.create!(email: email, name: name) end end ``` **Prefer database constraints** over model validations for data integrity: ```ruby # migration add_index :users, :email, unique: true add_foreign_key :cards, :boards ``` ## Let It Crash Philosophy Use bang methods that raise exceptions on failure: ```ruby # Preferred - raises on failure @card = Card.create!(card_params) @card.update!(title: new_title) @comment.destroy! # Avoid - silent failures @card = Card.create(card_params) # returns false on failure if @card.save # ... end ``` Let errors propagate naturally. Rails handles ActiveRecord::RecordInvalid with 422 responses. ## Default Values with Lambdas Use lambda defaults for associations with Current: ```ruby class Card < ApplicationRecord belongs_to :creator, class_name: "User", default: -> { Current.user } belongs_to :account, default: -> { Current.account } end class Comment < ApplicationRecord belongs_to :commenter, class_name: "User", default: -> { Current.user } end ``` Lambdas ensure dynamic resolution at creation time. ## Rails 7.1+ Model Patterns **Normalizes** - clean data before validation: ```ruby class User < ApplicationRecord normalizes :email, with: ->(email) { email.strip.downcase } normalizes :phone, with: ->(phone) { phone.gsub(/\D/, "") } end ``` **Delegated Types** - replace polymorphic associations: ```ruby class Message < ApplicationRecord delegated_type :messageable, types: %w[Comment Reply Announcement] end # Now you get: message.comment? # true if Comment message.comment # returns the Comment Message.comments # scope for Comment messages ``` **Store Accessor** - structured JSON storage: ```ruby class User < ApplicationRecord store :settings, accessors: [:theme, :notifications_enabled], coder: JSON end user.theme = "dark" user.notifications_enabled = true ``` ## Concern Guidelines - **50-150 lines** per concern (most are ~100) - **Cohesive** - related functionality only - **Named for capabilities** - `Closeable`, `Watchable`, not `CardHelpers` - **Self-contained** - associations, scopes, methods together - **Not for mere organization** - create when genuine reuse needed **Touch chains** for cache invalidation: ```ruby class Comment < ApplicationRecord belongs_to :card, touch: true end class Card < ApplicationRecord belongs_to :board, touch: true end ``` When comment updates, card's `updated_at` changes, which cascades to board. **Transaction wrapping** for related updates: ```ruby class Card < ApplicationRecord def close(creator: Current.user) transaction do create_closure!(creator: creator) record_event(:closed) notify_watchers_later end end end ```