state_machines-activerecord

repository·master·Indexed 19 days ago

https://github.com/state-machines/state_machines-activerecord

An integration for the state_machines gem providing support for ActiveRecord 7.2+ models. It features database transactions, automatic saving, state-driven validations, and intelligent conflict resolution with Rails enums. The library automatically generates transparent scopes (with_state, without_state) and supports integer-backed state attributes with optional auto-conversion.

Tokens
4.2K
Snippets
9
Records
16
Agent score
62%

What's inside state_machines-activerecord

  1. Handle integer-backed state attributes

    master

    The gem provides two ways to handle integer columns used for states:

    Automatic Conversion (Default)

    If states do not declare explicit integer values, the gem converts them transparently. Application code reads state names (strings), but the database stores integers (mapped by definition order).

    # Database stores 1, but code sees "approved"
    order.status = :approved
    order.status # => "approved"

    Explicit Integer Values

    If states declare explicit values, the gem maintains classic raw-integer behavior. Reading the attribute returns the integer, and status_name returns the symbol.

    # state :pending, value: 0
    # state :approved, value: 1
    
    order.status      # => 1
    order.status_name # => :approved

    Disabling Auto-Conversion

    To disable all type conversion and use standard ActiveRecord integer handling, set this in an initializer before defining state machines:

    # config/initializers/state_machines.rb
    StateMachines::Integrations::ActiveRecord.auto_convert_integer_state_attributes = false
    class Order < ApplicationRecord
      state_machine :status, initial: :pending do
        state :pending
        state :approved
      end
    end
    
    order = Order.create!
    order.status = :approved
    order.status # => "approved"
    # The database stores 1.
  2. Integrate state machines with Rails enums

    master

    If an ActiveRecord model uses a Rails enum and a state_machine on the same attribute, the gem automatically detects the conflict and resolves it by prefixing state machine methods to prevent collisions with Rails enum methods.

    Auto-Detection Requirements

    • The state machine attribute must match an existing Rails enum attribute.
    • Auto-detection is enabled by default.

    Conflict Resolution Behavior

    When integration is detected, the gem:

    1. Preserves original Rails enum methods (e.g., pending?, processing!).
    2. Generates prefixed state machine methods (e.g., status_pending?).
    3. Creates prefixed scope methods (e.g., Order.status_pending).

    Configuration Options

    • prefix (default: true): Adds a prefix to generated methods.
    • suffix (default: false): Uses suffixes instead of prefixes.
    • scopes (default: true): Controls whether state machine scopes are generated.
    class Order < ApplicationRecord
      # Rails enum definition
      enum :status, { pending: 0, processing: 1, completed: 2, cancelled: 3 }
      
      # State machine on the same attribute
      state_machine :status do
        state :pending, :processing, :completed, :cancelled
        
        event :process do
          transition pending: :processing
        end
        
        event :complete do
          transition processing: :completed
        end
        
        event :cancel do
          transition [:pending, :processing] => :cancelled
        end
      end
    end
  3. Define state-driven validations

    master

    You can define validations that only run when the machine is in specific states.

    Important Caveat: Due to ActiveRecord constraints, if you define a custom validator for multiple states, it may only execute for the last state in the list. To ensure the validation runs for all specified states, wrap the validation call in a block.

    Correct Pattern for Multiple States

    class Vehicle < ApplicationRecord
      state_machine do
        state :first_gear, :second_gear do
          # Use a block to ensure it runs for both states
          validate {|vehicle| vehicle.speed_is_legal}
        end
      end
    end
    class Vehicle < ApplicationRecord
      state_machine do
        state :first_gear, :second_gear do
          validate {|vehicle| vehicle.speed_is_legal}
        end
      end
    end
  4. Handle state transitions securely via Strong Parameters

    master

    State machines in ActiveRecord create public event attributes (e.g., state_event) to allow transitions via mass-assignment. To prevent malicious users from triggering transitions through web forms, you must use Rails' Strong Parameters to permit only intended attributes.

    Protecting Events

    If you want to prevent users from tampering with events, exclude the event attribute from your permitted parameters:

    class VehiclesController < ApplicationController
      def vehicle_params
        # Exclude state_event to prevent tampering
        params.require(:vehicle).permit(:color, :make, :model)
      end
    end

    Implementing Public and Private Machines

    If you need certain events to be triggerable via mass-assignment while others remain protected, you can define two separate state machines that target the same underlying attribute:

    class Vehicle < ApplicationRecord
      # Private machine for internal logic
      state_machine do
        # Define private events here
      end
    
      # Public machine for mass-assignment/external control
      state_machine :public_state, :attribute => :state do
        # Define public events here
      end
    end
  5. Use transactions and rollback behavior in transitions

    master

    Every state transition in ActiveRecord is wrapped in a database transaction. If a transition fails (e.g., due to a before_transition callback returning false or a validation error), the changes made during that transition will be rolled back.

    Disabling Transactions

    If you want to disable automatic transactions for a specific machine, set :use_transactions => false:

    class Vehicle < ApplicationRecord
      state_machine :initial => :parked, :use_transactions => false do
        # ...
      end
    end

    Important Note on Callbacks

    • before_transition: If this halts the chain, the transaction rolls back.
    • after_transition: If an after callback halts the chain, the transition has already occurred and the transaction is not rolled back.
  6. How the Integer type handler manages state-to-integer conversion

    master

    The StateMachines::Type::Integer class is a custom ActiveRecord::Type::Value designed for state machine attributes backed by integer columns. It manages the bidirectional conversion between state name strings (used by the state machine logic) and integer values (stored in the database).

    Mapping Logic

    • Implicit Mapping: States without an explicit integer value are mapped by their index position in the states collection (0, 1, 2, ...).
    • Explicit Mapping: States with an explicit integer value (e.g., state :pending, value: 2) use that specific integer directly.
    • Passthrough Mode: If every named state in the machine has an explicit integer value, the type enters "passthrough mode." In this mode, it delegates to the column's original integer type, preserving raw-integer behavior (e.g., record.status returns the integer, while record.status_name returns the symbol).

    Conversion Lifecycle

    • deserialize(value): Converts an integer from the database into a state name string.
    • cast(value): Converts an assigned value (Symbol, String, or Integer) into the in-memory state name string.
    • serialize(value): Converts the in-memory state name string back into an integer for database storage.
  7. Understand the callback execution order

    master

    When using the default :save action, state machine callbacks are interleaved with ActiveRecord callbacks in the following order:

    1. save (ActiveRecord)
    2. Begin transaction
    3. before_transition (StateMachines)
    4. valid (ActiveRecord)
    5. before_validation (ActiveRecord)
    6. validate (ActiveRecord)
    7. after_validation (ActiveRecord)
    8. before_save (ActiveRecord)
    9. before_create (ActiveRecord)
    10. create (ActiveRecord)
    11. after_create (ActiveRecord)
    12. after_save (ActiveRecord)
    13. after_transition (StateMachines)
    14. End transaction
    15. after_commit (ActiveRecord)
    16. after_transition (with after_commit: true)
    17. after_commit (ActiveRecord)
  8. Integrate state machines with ActiveRecord

    master

    To use state machines within an ActiveRecord model, define a state_machine block inside your class. By default, transitions are triggered by the :save action, meaning calling .save or .save! on the record will attempt to execute any pending state transitions.

    Default Behavior

    When a transition is triggered via the :save action, the record's state attribute is updated. If other attributes were modified on the record prior to the transition, those changes will also be persisted.

    class Vehicle < ApplicationRecord
      state_machine :initial => :parked do
        event :ignite do
          transition :parked => :idling
        end
      end
    end
    
    # Usage
    vehicle = Vehicle.create
    vehicle.name = 'Ford Explorer'
    vehicle.ignite # Returns true and prepares transition
    vehicle.save    # Persists both the name change and the state transition
    class Vehicle < ApplicationRecord
      state_machine :initial => :parked do
        event :ignite do
          transition :parked => :idling
        end
      end
    end
  9. Define a state machine in an ActiveRecord model

    master

    You can define a state machine directly within an ActiveRecord class. The integration provides support for database transactions, automatic saving, named scopes, and validation errors.

    Example implementation:

    class Vehicle < ApplicationRecord
      state_machine :initial => :parked do
        before_transition :parked => any - :parked, :do => :put_on_seatbelt
        after_transition any => :parked do |vehicle, transition|
          vehicle.seatbelt = 'off'
        end
        around_transition :benchmark
    
        event :ignite do
          transition :parked => :idling
        end
    
        state :first_gear, :second_gear do
          validates :seatbelt_on, presence: true
        end
      end
    
      def put_on_seatbelt
        ...
      end
    
      def benchmark
        ...
        yield
        ...
      end
    end
  10. Use generated state scopes

    master

    The integration automatically generates scopes based on your states (assuming the default column name is state).

    Available Scopes

    • with_state(:name) or with_states(:name1, :name2)
    • without_state(:name) or without_states(:name1, :name2)

    Transparent Scopes

    Passing nil to a state scope returns all records, which is useful for search filters:

    Vehicle.with_state(nil)                            # Returns all vehicles
    Vehicle.with_state(params[:state])                 # Returns all vehicles if params[:state] is nil
    Vehicle.where(color: 'red').with_state(nil)        # Returns all red vehicles (chainable)
    Vehicle.with_state(:parked)                         # also plural #with_states
    Vehicle.without_states(:first_gear, :second_gear)   # also singular #without_state
  11. Access state machine methods and scopes with Rails enum integration

    master

    When using the Rails enum integration described above, use the following method patterns:

    Original Rails Enum Methods (Preserved)

    • order.pending? (Predicate)
    • order.processing! (Bang method to set state)
    • Order.pending (Scope)

    Generated State Machine Methods (Prefixed)

    • order.status_pending? (Predicate)
    • order.status_processing! (Raises RuntimeError - placeholder for conflict resolution)
    • Order.status_pending (Scope)
    • Order.not_status_pending (Negative scope)