Recently, I was tasked with creating an event registration process for an existing Drupal site. This project would move the user through a number of possible paths to the final outcome of registering (or not registering), so building it as a finite state machine (using the state design pattern) seemed a good fit and would provide a flexible, easily modified, object-oriented structure for event registration. I thought it might be worthwhile to offer a simplified version of this approach, in case anyone would like to see an example of a simple, web-based finite state machine that is driven by user interaction.
Following the Gang of Four (GoF), each state in this state machine is controlled by its own object, or as they put it, "[t]he State pattern puts all behavior associated with a particular state into one object" (GoF 307). Because of this, I will refer to these states as "state objects" or "states" interchangeably.
This example has one starting point--unknown user who is not registered--and two possible end points, if followed all the way through--known user who is registered and known user who is declined. Theoretically, we could have four possible starting points, if this example allowed users to log in--unknown user who is not registered, known (logged in) user who is not registered, known user who is registered and known user who is declined. Allowing this would just involve the creation of a few more state objects.
If you follow along in the source code for this state machine, you'll see that we start with an abstract base class (AbstractRegistrationState) that contains code that will be used by each state object. The constructor of this base class creates a container object that will let us store user data for the duration of the registration process. The base class also defines an abstract method called getState(), but we'll return to that in a minute. Finally, the destructor calls a Singleton class (TrackProgress) that uses the Reflection API so we can visually see the path each user takes through the state machine.
The base class is extended by three more abstract classes, which define three types of state objects. AbstractDecisionState is the abstract class for state objects that contain conditionals (where decisions are made); AbstractFormState is the parent of state objects that create forms that require user input; and AbstractFinalState is the parent of state objects that represent terminal points in the state machine that require no further action--in this example, these terminal points consist of either the user being registered or the user having declined.
Each of these three abstract types provides a concrete definition for the getState() method. In each case, getState() is essentially a wrapper for an abstract method that is made concrete in the state objects and where most of the real work is done. This wrapper allows us to define custom functionality for each type of state object; for example, we can see that in AbstractFormState, the getState() wrapper is used to add open and closing form tags. Other than this, getState() returns the protected variable ($newState, $newForm and $finalMsg) defined by the state object that lets the context know which state comes next. Since AbstractFinalState is always a terminal state, the destructor for that class destroys the data container created in AbstractRegistrationState.
We can look at some of the state object classes to get a sense of how they work. HasResponded is a decision class that checks if the user has registered yet. The next state object is instantiated and returned based on the result of this conditional. EmailForm is a form class that just provides a form for collecting the user's email address (remember, the opening and closing tags are provided in the parent class for this state). RegistrationMsg just provides a message that the user has successfully registered.
Now let's look at the RegistrationContext class. This is the "context" of the state machine. As defined by the GoF, the context "defines the interface of interests to clients" (306). In many cases, this means that the context will implement the same interface as the state objects, in order to provide a tool to clients to move to different states. However, in our case, we need the state machine to be able to move through the states without any guidance from the client other than user input. We can't program in advance for the client to show the user a register button or a message that they have successfully registered.
So instead of implementing the same methods as the state object classes, the RegistrationContext class has one method, stateMachine(), which kicks off the registration process by instantiating the HasResponded state object. HasResponded will always be the first state object and will let us know if the user is currently registered. If not, the conditional in HasResponded::getDecision() sets $newState to IsKnownUser. AbstractDecisionState::getState() returns the value of $newState.
Now that we've started the state machine, we can just create a simple loop that cycles us through each state object until we either need more user input or have reached a terminal state. As long as the returned value of getState() is another state object, the state machine will keep cycling through decision states. If HasResponded returns IsKnownUser as the next state object, and the user has not given us their email address yet, then the next state returned is EmailForm. At this point, EmailForm does not return a new state object, but a form definition, and so the loop ends, and the user sees a form in which to enter their email address.
That is basically it. Try it in the window below. (Note: the form below only works on strings in valid email formats, but they don't have to be real email addresses. Email addresses entered are not collected or stored in any way.) Notice that TrackProgress::trackObjects shows us the path the user takes through the state objects. The path will always lead to either a form or a final message.
Because the outcome depends upon user input, we don't know what the outcome will be, so all paths towards the outcome have to be handled within the state objects of the machine. One of the drawbacks of this is that all the conditionals are distributed across the state object classes, which could potentially get confusing. On the other hand, if we just kept all the conditionals in the RegistrationContext class, we would end up with a lot of duplicated code and would lose much of the usefulness of the state design pattern. As the GoF say, "The State pattern puts each branch of the conditional in a separate class. This lets you treat the object's state as an object in its own right that can vary independently from other objects" (306).
I hope this example has proven useful. As mentioned, this example is a greatly simplified version of the state machine we developed to handle event registration, which contains many more potential pathways and registers users for events with SOAP calls to our NetForum database. The version here only provides a few possible paths in the state machine, to give you the basic idea of how to put one together. For a much more elaborate version of a state machine in PHP, see https://github.com/rolfvreijdenberger/izzum-statemachine.
Like long procedures, large conditional statements are undesirable. They're monolithic and tend to make the code less explicit, which in turn makes them difficult to modify and extend. The State pattern offers a better way to structure state-specific code. The logic that determines the state transitions doesn't reside in monolithic if or switch statements but instead is partitioned between the State subclasses. Encapsulating each state transition and action in a class elevates the idea of an execution state to full object status. That imposes structure on the code and makes its intent clearer (307).
Gamma, Erich, Richard Helm, Ralph Johnson, and John Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Boston: Addison-Wesley, 1995.