What is the correct syntax for performing a validation on before a transition in the state_machine gem?
I've tried the following,
before_transition :apple => :orange do
validate :validate_core
end
def validate_core
if core.things.blank?
errors.add(:core, 'must have one thing')
end
end
But I get the following error,
undefined method `validate' for #<StateMachine::Machine:0x007ffed73e0bd8>
I've also tried writing it as,
state :orange do
validate :validate_core
end
But this causes a rollback after the record is saved, which is less than ideal. I'd like to stop the state machine from transitioning into :orange
in the first place.
The core problem is that in my controller I have logic that relies on the result of object.save
. The validation I have for my state machine doesn't kick in until after the initial save, so save gets returned as true and the controller goes on to logic it shouldn't hit if the object isn't valid.
I've worked around this by testing the validity manually in addition to checking the save, but it feels like there should be a way to have the validation fire before the object saves.
The idea of that particular state machine is to embed validation declaration inside the state.
state :orange do
validate :validate_core
end
The configuration above will perform the validation :validate_core
whenever the object is transitioning to orange.
event :orangify do
transition all => :orange
end
I understand your concern about the rollback, but keep in mind that the rollback is performed in a transaction, thus it's quite cheap.
record.orangify!
Moreover, remember you can also use the non bang version that don't use exceptions.
> c.orangify
(0.3ms) BEGIN
(0.3ms) ROLLBACK
=> false
That said, if you want to use a different approach based on the before transition, then you only have to know that if the callback returns false, the transition is halted.
before_transition do
false
end
> c.orangify!
(0.2ms) BEGIN
(0.2ms) ROLLBACK
StateMachine::InvalidTransition: Cannot transition state via :cancel from :purchased (Reason(s): Transition halted)
Note that a transaction is always started, but it's likely no query will be performed if the callback is at the very beginning.
The before_transaction
accepts some params. You can yield the object and the transaction instance.
before_transition do |object, transaction|
object.validate_core
end
and indeed you can restrict it by event
before_transition all => :orange do |object, transaction|
object.validate_core # => false
end
In this case, validate_core
however is supposed to be a simple method that returns true/false. If you want to use the defined validation chain, then what comes to my mind is to invoke valid?
on the model itself.
before_transition all => :orange do |object, transaction|
object.valid?
end
However, please note that you can't run a transaction outside the scope of a transaction. In fact, if you inspect the code for perform
, you will see that callbacks are inside the transaction.
# Runs each of the collection's transitions in parallel.
#
# All transitions will run through the following steps:
# 1. Before callbacks
# 2. Persist state
# 3. Invoke action
# 4. After callbacks (if configured)
# 5. Rollback (if action is unsuccessful)
#
# If a block is passed to this method, that block will be called instead
# of invoking each transition's action.
def perform(&block)
reset
if valid?
if use_event_attributes? && !block_given?
each do |transition|
transition.transient = true
transition.machine.write(object, :event_transition, transition)
end
run_actions
else
within_transaction do
catch(:halt) { run_callbacks(&block) }
rollback unless success?
end
end
end
# ...
end
To skip the transaction, you should monkey patch state_machine so that transition methods (such as orangify!
) check whether the record is valid before transitioning.
Here's an example of what you should achieve
# Override orangify! state machine action
# If the record is valid, then perform the actual transition,
# otherwise return early.
def orangify!(*args)
return false unless self.valid?
super
end
Of course, you can't do that manually for each method, that's why you should monkey patch the library to achieve this result.
You could try to cancel the transition to the next state by doing something like this:
before_transition :apple => :orange do
if core.things.blank?
errors.add(:core, 'must have one thing')
throw :halt
end
end
This way, if core.things is blank, then an error would appear for core and the transition would be cancelled. I assume it also wouldn't make any changes to the DB. Haven't tried this code though but just read its source. Given that the code above, would likely lead to even more code to catch the exception, how about the approach below?
def orange_with_validation
if core.things.blank? && apple?
errors.add(:core, 'must have one thing')
else
#transition to orange state
orange
end
end
You could use the code above in places where you would like validation before it transitions to the orange state. This approach allows you to workaround the limitations of state_machine's callbacks. Using it in your controller which powers the wizard form would stop your form from moving to the next step due and would avoid any DB hits when it fails the validation.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With