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.
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 -
- the stages needed to be performed (in case the callback returns true) if parent stage is a valid stage.
might be a simple function call, an object method call (i.e. just an "ordinary" callback
in case the stage (and all of its substages) should be performed or false if not.
field is used to construct the appropriate objects and run them when the stage is reached. I think, the
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.).
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 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.