Page 1 of 2

Wizards and MVC

Posted: Mon Jan 24, 2011 7:10 am
by VladSun
How do you guys write your wizard pages in a way that fits the MVC approach? I'm not interested in passing the current model state. I'm more interested in the M-V-C relations defined.

The problem I'm facing is that the Controller doesn't know the logic flow to follow - it's in the Model. I.e. if you have a Model that needs $b property to be set in the next step (an additional one), only and if only the $a property is set in the current step, the Model should tell it somehow to the Controller, which in turn must know which exact View to render depending on the model state.

It seems like some of the business logic goes into the Controller. And the Controller itself behaves just like a "router" with a routing table fetched from the Model.

Using the State pattern will help constructing a good Model(s) class, but it won't solve the problem I've described.

I imagine the Model interface should be something like this:

Code: Select all

interface IWizardModel
{
	public function reset();
	public function nextState();
	public function prevState();
	public function finish();

	public function getCurrentState();

	public function saveDataForCurrentState();

	public function isNextAvail();
	public function isPrevAvail();
	public function isFinishAvail();
}
I think that in the Controller I should create a look up table for the Views to be loaded according to the $model->getCurrentState() value. Also, there should be an subcontroller which knows how to call $model->saveDataForCurrentState() (e.g. POST/GET data passed).

Re: Wizards and MVC

Posted: Mon Jan 24, 2011 8:49 am
by VladSun
Also, I think the "WizardModel" should be a separate object with an instance of the "BusinessModel" in case the BusinessModel is configured by using none wizard interface.

Re: Wizards and MVC

Posted: Mon Jan 24, 2011 10:37 am
by VladSun
I am thinking about having an getNextStepModel() method in each "step-submodel". So, the WizardModel will need just the initial steep-model (and step-history) in order to perform Next/Prev actions.

Re: Wizards and MVC

Posted: Mon Jan 24, 2011 5:03 pm
by VladSun
I've been looking into the Spring AbstractWizardFormController, but I'm not sure all of the Wizard logic should be in the Controller because, as I said above, it really depends on the Model state.

The AbstractWizardFormController even has a getPageCount() (Return the number of wizard pages) method which confuses me a lot. I think a Wizard, in general, should be "multi-path" Wizard, not just a constant, predefined sequence of pages...

Re: Wizards and MVC

Posted: Mon Jan 24, 2011 7:14 pm
by Jonah Bron
This probably doesn't help you much, but I remember when reading POEAA coming across a pattern that was very useful for wizards. Unfortunately I don't recall which one it was, or if it was used as a replacement for MVC, or to be used in conjunction with it.

:banghead:

Re: Wizards and MVC

Posted: Mon Jan 24, 2011 11:48 pm
by josh
I've found that everyone is tempted to abstract the crap out of their wizard pages. Usually a controller for the wizard with an action for each screen is fine, and then hard code the logic somewhere for action forwarding. When I've tried the 'getCurrentStep()', 'getNextStep()' methods its led to a lot of boiler plate code that didn't help. So much easier to just have stepOne(), stepTwo(), stepThree() and use regular action forwarding. Makes for nicer URLs too.

The pattern Jonah alludes to is 'Application Controller'. That's what I'm talking about though, its not worth it for simple wizards in my opinion. Its more for when the order of the actions is very un-predictable, or must vary for each user for example. When you have a pre-determined order of actions, its overkill.

Re: Wizards and MVC

Posted: Tue Jan 25, 2011 1:07 am
by Zyxist
I don't think there is a problem. Take a look:

Code: Select all

// Some controller code
try
{
    $model->setNextState($dataArray);
}
catch(DependencyException $exception)
{
   // oops, the model can't handle our request!
}
You can inform the controller about the problem with exceptions, and controllers do not have to know anything. Model provides a programming interface; if it is correctly written and you think there is some business logic in controller, you should be able to wrap it in a model method and call from the controller code.

By the way, forms are generally a tricky issue in MVC, because a complete form requires to "extend" all of the MVC elements, so it is not so trivial to make an interface for it that is both compatible with the pattern and simple-to-use. Fortunately, it is possible.

Re: Wizards and MVC

Posted: Tue Jan 25, 2011 5:37 am
by VladSun
@josh - Yes, I'm talking about the "the order of the actions is very un-predictable" case.
@Zyxist - it looks OK, but how would I know what should be in the $dataArray? The data might contains POST/GET/COOKIE/FILES data ... Also, how does the Controller know which View to load for the next stage?

Suppose we have a:

Code: Select all

class A_Model
{
	public $a;
	public $b;
	public $c;
	public $d;

	...

	protected function checkValidState()
	{
		if (!empty($b) and empty($a))
			throw new Exception('"A" must be set in case "B" is used.');

		if (!empty($c) and empty($a))
			throw new Exception('"A" must be set in case "C" is used.');

		if (empty($a) and empty($c) and !empty($d))
			throw new Exception('"A" and "C" must be set in case "D" is used.');
	}

	public function run()
	{
		$this->checkValidState();
		...
	}
}
We "configure" the object and then call run() which in turn tries to validate state first.

Now, if the object configuration is made by using a single form (i.e. a single View and a single Action Controller) it's somehow trivial (though there might be some business logic leaks into the View).

Not so easy in a multipath Wizard ...

Re: Wizards and MVC

Posted: Wed Jan 26, 2011 2:55 am
by Christopher
An Application Controller is really not that complicated. It is really just a formalism of the state/transition system that Josh covered. The states are methods like Josh's stepOne(), stepTwo(), stepThree() and the transitions are rules of some sort. The Application Controller just calls the correct method based on the state and the transition rules that apply to that state. Typically in PHP the state is stored in the session. A sequence of forms for data collection or registration are common. Checkout is another common wizard sequence. But there are certainly common cases where the process can span multiple sessions, so state information is stored in the database. I have built a number of application and CRM type sequences that track state over time.

Re: Wizards and MVC

Posted: Wed Jan 26, 2011 4:20 am
by josh
Yeah basically a declarative way to add rule objects onto some stack.

"if x=y and z=1; then load controller action B, view A" (or whatever)
...
...
and you'd have a huge list of rules.

An example of what this is bad for is a "next, next, next" style of wizard where there is a pre-determined order. Another example of a bad application is using this to deal with "has a user confirmed their email" (that can be handled with a simple conditional, and the rules would be too much indirection). A good example is an application that business people use to negotiate. Maybe there's a bunch of legal rules that must vary from state to state, industry to industry. In that kind of application it makes sense to abstract the rules.

The pattern could be overly simplified and stated as "put everything in a 'model' and have your controllers ask the model what to do"

Re: Wizards and MVC

Posted: Wed Jan 26, 2011 6:42 am
by VladSun
OK, it's very similar to the idea I'm implementing now :) Though, I've decided to implement it client-side in JS :)

I'll post the code for critique.

Re: Wizards and MVC

Posted: Thu Jan 27, 2011 2:49 am
by Christopher
josh wrote:and you'd have a huge list of rules.

An example of what this is bad for is a "next, next, next" style of wizard where there is a pre-determined order. Another example of a bad application is using this to deal with "has a user confirmed their email" (that can be handled with a simple conditional, and the rules would be too much indirection). A good example is an application that business people use to negotiate. Maybe there's a bunch of legal rules that must vary from state to state, industry to industry. In that kind of application it makes sense to abstract the rules.
I'm probably not understanding the examples you are giving. In my experience there are very few rules and they are not in a list. Each rule is associated with a state and determines if a transition can occur. I am not sure the difference between a "simple conditional" and a rule.

Re: Wizards and MVC

Posted: Thu Jan 27, 2011 4:43 am
by josh
By simple conditional I mean an if statement in the controller. If user is logged in, forward to dashboard action, else, forward to login/register.

By rules / the examples I gave I mean for example if your application helps people do legal work, the "flow" may vary based on the local law of the user. Rules based on state like you say (both meanings of state, the Geographical state in my example, but more importantly State meaning "data" )

Another way of describing it is "rules" could be items your end user/administrator could edit, as a non-programmer. The rules don't have to be stored in code, they could come from a database.

Re: Wizards and MVC

Posted: Thu Jan 27, 2011 10:23 am
by Jenk
josh wrote:I've found that everyone is tempted to abstract the crap out of their wizard pages. Usually a controller for the wizard with an action for each screen is fine, and then hard code the logic somewhere for action forwarding. When I've tried the 'getCurrentStep()', 'getNextStep()' methods its led to a lot of boiler plate code that didn't help. So much easier to just have stepOne(), stepTwo(), stepThree() and use regular action forwarding. Makes for nicer URLs too.

The pattern Jonah alludes to is 'Application Controller'. That's what I'm talking about though, its not worth it for simple wizards in my opinion. Its more for when the order of the actions is very un-predictable, or must vary for each user for example. When you have a pre-determined order of actions, its overkill.
This.

We always go for this approach within anything that fits the "Wizard" pattern, which is nothing more than "a sequence of events needed to complete a task".

Re: Wizards and MVC

Posted: Thu Jan 27, 2011 5:42 pm
by VladSun
After some hard development I think I got my solution. Though it's in JS, I think the same approach may be used in PHP.

So... let's say we have a form page that configures the Model. I need to have the ability to configure the Model by either of the interfaces - the "old" form interface and the new wizard interface. So, I create a Configurator Model - i.e. an object that knows exactly how the Model needs to be configured. The Configurator collects user's input, may be asked questions about what's needed and what's not needed, etc. Finally, the Configurator puts the collected data into the Model. The Model doesn't care about how this data was collected. It just throws exceptions on invalid input.

The Configurator itself may be a FormConfigurator, a WizardConfigurator, etc.

Let's concentrate on wizards :)
Another class is defined - a WizardRouter. A WizardRouter implements two main methods: getCurrentStage() (I call it stage, not step :) ) and nextStage() - i.e. it implements Iterator like interface. It is also passed a object path-finder object (Strategy pattern). I've implemented 2 strategies. The first one uses a "routing table" - a hierarchical and static (predefined) collection. Every item in this collection contains 3 fields - action - a string, a callback that will be used to check whether this step will be performed or not, and substages - the stages needed to be performed (in case the callback returns true) if parent stage is a valid stage.

Every callback might be a simple function call, an object method call (i.e. just an "ordinary" callback :) ) This function should return true in case the stage (and all of its substages) should be performed or false if not.

The action field is used to construct the appropriate objects and run them when the stage is reached. I think, the action field should be passed to a ActionControllerFactory (in my JS case - a ComponentFactory). The objects created by this factory should be initialized with the current Configurator state (or the Configurator itself).

When the WizardRouter nextStage() method returns false we call the finish() method of Configurator. It in turn configres the Model with the collected data.

I haven't implemented the "Previous" functionality, but I think I'll do it by using the history (undo/redo) related patterns (like Command, Memento etc.).

There are also some nasty features like "dirty" prev/next :)

I've made a small demo of the router strategy (in JS):
(tests included)

Code: Select all

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
	<head>
	</head>
<body>

	<pre id="prediv"></pre>

<script type="text/javascript" >

function build(up, stages)
{
	if (!stages)
		return;

	for(var i=0; i< stages.length; i++)
	{
		stages[i].up	= up;
		stages[i].prev	= stages[i-1] ? stages[i-1] : null;
		stages[i].next	= stages[i+1] ? stages[i+1] : null;

		build(stages[i], stages[i].stages);
	}
}

function getNextStage(current, routes)
{
	if (current == null)
	{
		current = routes[0];
		if (current.run !== false)
			return current;
	}

	if (current.stages && current.run !== false)
	{
		for (var i=0; i < current.stages.length; i++)
			if (current.stages[i].run !== false)
				return current.stages[i];
	}

	while (current.next !== null)
	{
		if (current.next.run === false)
			current = current.next;
		else
			return current.next;
	}

	while (current.up !== null)
	{
		current = current.up;
		while (current.next !== null)
		{
			if (current.next.run === false)
				current = current.next;
			else
				return current.next;
		}
	}

	return null;
}

// TESTS

var tests = [];

tests.push(
{
	title	: 'Tree, L1 - 3 stages, L2 - 1 stage, L3 - 1 stage',
	route:
	[
		{
			action : '1'
		},
		{
			action : '2'
		},
		{
			action : '3',
			stages :
			[
				{
					action : '3.1',
					stages	:
					[
						{
							action	: '3.1.1'
						}
					]
				},
				{
					run	: false,
					action : '3.2'
				},
				{
					action : '3.3'
				}
			]
		}
	],

	expectedPath : ['1', '2', '3', '3.1', '3.1.1', '3.3']
});

tests.push(
{
	title	: 'Tree, all stages disabled',
	route:
	[
		{
			run	: false,
			action : '1'
		},
		{
			run	: false,
			action : '2'
		},
		{
			run	: false,
			action : '3',
			stages :
			[
				{
					action : '3.1',
					stages	:
					[
						{
							action	: '3.1.1'
						}
					]
				},
				{
					run	: false,
					action : '3.2'
				},
				{
					run	: false,
					action : '3.3'
				}
			]
		}
	],

	expectedPath : []
});

tests.push(
{
	title	: 'Tree, L1 - 3 stages, L2 - 1 stage, L3 - 1 stage, L1 last two stages disabled',
	route:
	[
		{
			action : '1'
		},
		{
			action : '2'
		},
		{
			action : '3',
			stages :
			[
				{
					action : '3.1',
					stages	:
					[
						{
							action	: '3.1.1'
						}
					]
				},
				{
					run	: false,
					action : '3.2'
				},
				{
					run	: false,
					action : '3.3'
				}
			]
		}
	],
	expectedPath : ['1', '2', '3', '3.1', '3.1.1']
});

tests.push(
{
	title	: 'Tree. L1 -3 stages, L2 - 3 stages, L3 - 1 stage',
	route:
	[
		{
			action : '1'
		},
		{
			action : '2'
		},
		{
			action : '3',
			stages :
			[
				{
					action : '3.1',
					stages	:
					[
						{
							action	: '3.1.1'
						}
					]
				},
				{
					action : '3.2'
				},
				{
					action : '3.3'
				}
			]
		}
	],

	expectedPath : ['1', '2', '3', '3.1', '3.1.1', '3.2', '3.3']
});

tests.push(
{
	title	: 'Deep tree, L1 - 3 stages, L2 - 1 substage, L3 one (second) of two substages disabled',
	route:
	[
		{
			action : '1'
		},
		{
			action : '2'
		},
		{
			action : '3',
			stages :
			[
				{
					action : '3.1',
					stages	:
					[
						{
							action	: '3.1.1'
						},
						{
							run	: false,
							action	: '3.1.2'
						}
					]
				},
				{
					action : '3.2'
				},
				{
					action : '3.3'
				}
			]
		}
	],

	expectedPath : ['1', '2', '3', '3.1', '3.1.1','3.2', '3.3']
});

tests.push(
{
	title	: 'Deep tree, L1 - 3 stages, L2 - 1 substage, L3 one (first) of two substages disabled',
	route:
	[
		{
			action : '1'
		},
		{
			action : '2'
		},
		{
			action : '3',
			stages :
			[
				{
					action : '3.1',
					stages	:
					[
						{
							run	: false,
							action	: '3.1.1'
						},
						{
							action	: '3.1.2'
						}
					]
				},
				{
					action : '3.2'
				},
				{
					action : '3.3'
				}
			]
		}
	],

	expectedPath : ['1', '2', '3', '3.1', '3.1.2','3.2', '3.3']
});

tests.push(
{
	title	: 'Deep tree, 3 stages, 1 substage with all substages disabled',
	route:
	[
		{
			action : '1'
		},
		{
			action : '2'
		},
		{
			action : '3',
			stages :
			[
				{
					action : '3.1',
					stages	:
					[
						{
							run	: false,
							action	: '3.1.1'
						},
						{
							run	: false,
							action	: '3.1.2'
						}
					]
				},
				{
					action : '3.2'
				},
				{
					action : '3.3'
				}
			]
		}
	],

	expectedPath : ['1', '2', '3', '3.1', '3.2', '3.3']
});

tests.push(
{
	title	: 'Linear, 4 stages, 3rd disabled',
	route:
	[
		{
			action : '1'
		},
		{
			action : '2'
		},
		{
			action : '3',
			run	: false
		},
		{
			action	: '4'
		}
	],

	expectedPath : ['1', '2', '4']
});

tests.push(
{
	title	: 'Linear, 4 stages, first 3 disabled',
	route:
	[
		{
			run	: false,
			action : '1'
		},
		{
			run	: false,
			action : '2'
		},
		{
			run	: false,
			action : '3'
		},
		{
			action	: '4'
		}
	],

	expectedPath : ['4']
});

tests.push(
{
	title	: 'Deep tree, 4 stages, first disabled',
	route:
	[
		{
			run	: false,
			action : '1',
			stages :
			[
				{
					action : '1.1',
					stages	:
					[
						{
							action	: '1.1.1'
						},
						{
							action	: '1.1.2'
						}
					]
				},
				{
					action : '1.2'
				},
				{
					action : '1.3'
				}
			]
		},
		{
			action : '2'
		},
		{
			action : '3'
		},
		{
			action	: '4'
		}
	],

	expectedPath : ['2', '3', '4']
});

tests.push(
{
	title	: 'Deep tree, mixed',
	route:
	[
		{
			action : '1',
			stages :
			[
				{
					action : '1.1',
					stages	:
					[
						{
							action	: '1.1.1'
						},
						{
							run	: false,
							action	: '1.1.2'
						}
					]
				},
				{
					run	: false,
					action : '1.2'
				},
				{
					action : '1.3'
				}
			]
		},
		{
			run	: false,
			action : '2'
		},
		{
			action : '3',
			stages :
			[
				{
					action : '3.1'
				},
				{
					run	: false,
					action : '3.2'
				},
				{
					action : '3.3',
					stages	:
					[
						{
							run	: false,
							action	: '3.3.1'
						},
						{
							action	: '3.3.2'
						}
					]
				}
			]
		},
		{
			action	: '4'
		}
	],

	expectedPath : ['1', '1.1', '1.1.1', '1.3', '3', '3.1', '3.3', '3.3.2', '4']
});

tests.push(
{
	title	: 'Single stage',
	route:
	[
		{
			action : '1'
		}
	],

	expectedPath : ['1']
});

tests.push(
{
	title	: 'Linear, 4 stages, last disabled',
	route:
	[
		{
			action : '1'
		},
		{
			action : '2'
		},
		{
			action : '3'
		},
		{
			action	: '4',
			run	: false
		}
	],

	expectedPath : ['1', '2', '3']
});

tests.push(
{
	title	: 'Linear, 4 stages first disabled',
	route:
	[
		{
			run: false,
			action : '1'
		},
		{
			action : '2'
		},
		{
			action : '3'
		},
		{
			action : '4'
		}
	],

	expectedPath : ['2', '3', '4']
});

function isEqual(path1, path2)
{
	if (path1.length != path2.length)
		return false;

	for (var i=0; i< path1.length; i++)
		if (path1[i] != path2[i])
			return false;

	return true;
}

function testBuildPath(route, expectedPath, testTitle)
{
	build(null, route);

	var current = null;
	var path = [];

	while ((current = getNextStage(current, route)))
	{
		path.push(current.action);
	}

	if (!isEqual(path, expectedPath))
	{
		document.getElementById('prediv').innerHTML += "\n[-] TEST [ " + testTitle + " ] FAILED !\n";
		document.getElementById('prediv').innerHTML += "=====================================================\n";
		document.getElementById('prediv').innerHTML += "Actual : \n" + path.join("\n");
		document.getElementById('prediv').innerHTML += "\n____________________________________________________\n\n";
		document.getElementById('prediv').innerHTML += "Expected: \n" + expectedPath.join("\n");
		document.getElementById('prediv').innerHTML += "\n=====================================================\n\n\n";
	}
	else
		document.getElementById('prediv').innerHTML += "[+] TEST " + testTitle + " - OK \n";
}

for (var i=0; i < tests.length; i++)
	testBuildPath(tests[i].route, tests[i].expectedPath, tests[i].title);


</script>
</body>
</html>
The "callbacks" are substituted with simple boolean "constants". Just for testing purposes.

The second routing strategy is something like this:
- the routing objects asks the Configurator what should be the next stage. The Configurator answers according to its state. It should be used for really, really unpredictable routes.