state_machines-activerecord
state_machines-activerecord copied to clipboard
Transient attributes quirks
I have a state machine on a class that inherits from ActiveRecord::Base
that's something like this:
state_machine :state do
state :one, :two do
validate { |record| record.validate_has_gubbins }
end
state :three
event :one_to_two do
transition one: :two # , if: ->(m) { m.gubbins.present? }
end
event :two_to_three do
transition two: :three
end
end
It's based on an attribute that's not persisted to the database - a transient attribute. I define this with an attr_accessor
and set its initial value in an after_initialize
hook. The initial value is figured out from the current state of the record.
This is a useful approach because the state machine can be changed without a significant negative impact on existing records - if a new state were introduced between one
and two
, for example, two
would still be stored in the database and would leave all two
records in that state, whereas if it's dynamically figured out, they'd just move back to one
with all data intact. It's just as useful for validating records at different stages while being a little more flexible where you need it.
There are a couple of quirks with this approach at the moment, though:
- The validator I've added up there doesn't run. I'm following the advice here to set the validation on both the
one
andtwo
states. The fact that the validator doesn't run then means I need to use the commented part to protect that transition. I did some experimenting myself and thestate
block does get called, but thevalidate
block does not. -
initial:
doesn't work. I tried setting it to a lambda and a symbol, and I also tried playing withinitialize_state_machine(force: true)
. The fix as mentioned is to set its initial value in anafter_initialize
hook as you would any other transient attribute, but it'd be better to have it within the state machine declaration.
I've since changed approaches - I'm now using a dynamic state machine as per the docs. Thus far, I think I'd recommend that, but I've got the following observations:
- The state machine is on the
Machine
class rather than theobject
that gets passed in toMachine#new
. You'll need to allow the machine to recognize the parent object if you want to use that in conditional transitions. Aim for something likeif: ->(machine) { machine.parent. [...] }
- You'll also need to set the
initial
state based on the parent object.
Tangentially related, but with transient attributes, use the ActiveRecord Attributes API.