acts_as_state_machine is a great plugin really useful when you want to add constraints and behavior to your model objects.
Note : Those who don’t know what this plugin is all about should stop reading right there or risk being completely lost.
One thing that seems impossible to do with the plugin is to have variable “initial states” for an object. Say for example that you have a User model that needs to act as a state machine. New users have to fill up a signup form to gain access to the application. These new users will be “pending” until an administrator approve them (enabled) or refuse them (disabled)
class User < ActiveRecord::Base
acts_as_state_machine :initial => :pending
state :pending
state :enabled
state :disabled
event :enable do
transitions :from => :pending, :to => :enabled
transitions :from => :disabled, :to => :enabled
end
event :disable do
transitions :from => :pending, :to => :disabled
transitions :from => :enabled, :to => :disabled
end
end
So far so good. But what if an administrator can also create a new user? In this case we would’nt want the user to be “pending”, we want him to be “enabled” right away.
I experimented a similar scenario, so I tried a few things to bypass the initial state :
#FAILED ATTEMPT #1
def create
user = User.new(params[:user])
user.state = :enabled
user.save
end
Too bad, so I tried this :
#FAILED ATTEMPT #2
def create
User.initial_state = :enabled
user = User.new(params[:user])
user.save
end
Of course, the following worked :
#SUCCESSFUL BUT SUCKY ATTEMPT
def create
user = User.new(params[:user])
user.save
user.enable!
end
Yuk… there had to be something better.
Finally, I learned that initial_state was an inheritable attribute. I didn’t know what those were all about so I did some googling. The only good explanation I found is this one.
Anyway, I was able to override the initial state of my user object by doing this :
#SUCCESSFUL ATTEMPT!
def create
User.write_inheritable_attribute :initial_state, :enabled
user = User.new(params[:user])
user.save
end
It’s not pretty I know, but at least it gets the job done. I wonder if the fact that you cannot “easily” change the initial state programmatically is a missing feature that should be added in a future version… or if it’s only my understanding of a “state machine” that sucks. Any thoughts?
Hey Frank,
I think the best way is:
def create
user = User.new(params[:user])
user.enable!
end
enable! calls save, no hacks and pretty explicit.
Changing a class attribute like this is not good practice and would prevent your app from being multi-thread amongst other things.
Marc, thanks for your input. Feel a bit silly now 😉
However I still wonder why it isn’t possible to bypass the initial state specified in the model. I’d prefer if I could just send the "state" attribute among the rest like that : User.new(:name => "blabla", :state => "enabled")
Hmm… interesting. I started experiencing some weird deadlock errors as soon as I tried what you said Marc. I am too lazy to check the plugin code but I would guess it does something special with the “!” events. Or maybe it’s something completely unrelated… still checking the issue.
Greetings,
I believe the reason you can’t do that is that you’d be jumping to a middle state of a state machine. Essentially you’d be bypassing any code that was set up to happen during transitions.
The system needs to trigger when you transition from (for example) :free to :paid, so it can provision you with the appropriate features.
In order to trigger on transition between states, you have to have a consistent starting state.
Think of it as stateful integrity, a term that has only two Google hits, so I consider myself to have made it up just now, for just this purpose. 🙂
— Morgan
Cyberfox,
good call, it makes sense. So basically I should redesign my “user state machine” to have just 1 initial state.
Maybe have the following states :
All new users could be : new (initial)
Unapproved users : unapproved
Enabled users : enabled
Disabled users : disabled
And the events :
approve! (from :unapproved to :enabled)
enable! (from :new to :enabled)
disable! (from :enabled to :disabled and from :unapproved to :disabled)
I guess that would be better than having more than 1 initial states. Any thoughts?
Hello Frank,
I am bit confused
Isn’t it good way to use
“User.write_inheritable_attribute :initial_state, :enabled”
Because I want to assign the specific state (e.g. :xyz) initially only in few cases. And in other cases when i migrate from any other state to :xyz and have to perform some events which are not required in first case.
Therefore I cannot use
User.xyz! as this performs all the event actions when I do this.
So can anyone suggest me a good way to do this.
“User.write_inheritable_attribute :initial_state, :xyz” serves my purpose but as someone has already said its not a good practice.