This is the solution I developed to answer my problem.
As the problem was not a client side problem but truly a server side one. Following the php classes I used in my project:
First the main class of the stack functionality. The inclusion need to be done before the session_start as the object will be stored in the session
class Stack {
private $stack;
private $currentPosition;
private $comeFromCancelledAction = false;
public function __construct() {
$this->clear();
}
/* ----------------------------------------------------- */
/* PUBLICS METHODS */
/* ----------------------------------------------------- */
/**
* Clear the stack history
*/
public function clear() {
$this->stack = array();
$this->currentPosition = -1;
}
/**
* get the current position of the stack
*/
public function getCurrentPosition() {
return $this->currentPosition;
}
/**
* Add a new element on the stack
* Increment the current position
*
* @param $url the url to add on the stack
* @param $data optionnal, the data that could be stored with this $url
*/
public function add($url, &$data = array()) {
if (count($this->stack) != $this->currentPosition) {
// the currentPosition is not the top of the stack
// need to slice the array to discard dirty urls
$this->stack = array_slice($this->stack, 0, $this->currentPosition+1);
}
$this->currentPosition++;
$this->stack[] = array('url' => $url, 'data' => $data, 'previousData' => null, 'linked_data' => null);
}
/**
* Add the stack position parameter in the URL and do a redirect
* Exit the current script.
*/
public function redirect() {
header('location:'.$this->addStackParam($this->getUrl($this->currentPosition)), 301);
exit;
}
/**
* get the URL of a given position
* return null if the position is not valid
*/
public function getUrl($position) {
if (isset($this->stack[$position])) {
return $this->stack[$position]['url'];
} else {
return null;
}
}
/**
* get the Data of a given position
* return a reference of the data
*/
public function &getData($position) {
if (isset($this->stack[$position])) {
return $this->stack[$position]['data'];
} else {
return null;
}
}
/**
* Update the context of the current position
*/
public function storeCurrentData(&$data) {
$this->stack[$this->currentPosition]['data'] = $data;
}
/**
* store some data that need to be fixed in sub flow
* (for example the id of the parent object)
*/
public function storeLinkedData($data) {
$this->stack[$this->currentPosition]['linked_data'] = $data;
}
/**
* Update the context of the current position
*/
public function storePreviousData(&$data) {
$this->stack[$this->currentPosition]['previousData'] = $data;
}
/**
* Compute all linked data for every positions before the current one and return an array
* containing all keys / values
* Should be called in sub flow to fixed some data.
*
* Example: if you have tree pages: dad.php, mum.php and child.php
* when creating a "child" object from a "dad", the dad_id should be fixed
* but when creating a "child" object from a "mum", the mum_id should be fixed and a combo for choosing a dad should be displayed
*/
public function getLinkedData() {
$totalLinkedData = array();
for($i = 0; $i < $this->currentPosition; $i++) {
$linkedData = $this->stack[$i]['linked_data'];
if ($linkedData != null && count($linkedData) > 0) {
foreach($linkedData as $key => $value) {
$totalLinkedData[$key] = $value;
}
}
}
return $totalLinkedData;
}
/**
* Main method of the Stack class.
* Should be called on each page before any output as this method should do redirects.
*
* @param $handler StackHandler object that will be called at each step of the stack process
* Let the caller to be notified when something appens.
* @return the data
*/
public function initialise(StackHandler $handler) {
if (!isset($_GET['stack']) || !ctype_digit($_GET['stack'])) {
// no stack info, acces the page directly
$this->clear();
$this->add($this->getCurrentUrl()); //add the ?stack=<position number>
$this->storeLinkedData($handler->getLinkedData());
$this->redirect(); //do a redirect to the same page
} else {
// $_GET['stack'] is set and is a number
$position = $_GET['stack'];
if ($this->currentPosition == $position) {
// ok the user stay on the same page
// or just comme from the redirection
if (!empty($_POST['action'])) {
// user submit a form and need to do an action
if ($_POST['action'] == 'cancel') {
$currentData = array_pop($this->stack);
$this->currentPosition--;
$handler->onCancel($currentData);
// redirect to the next page with ?stack=<current position + 1>
$this->redirect();
} else {
// store the action for future use
$this->stack[$this->currentPosition]['action'] = $_POST['action'];
$currentData = $this->getData($this->currentPosition);
list($currentData, $nextUrl) = $handler->onAction($currentData, $_POST['action']);
// store current form for future use
$this->storeCurrentData($currentData);
// add the new page on the stack
$this->add($nextUrl);
// redirect to the next page with ?stack=<current position + 1>
$this->redirect();
}
} else if (isset($this->stack[$this->currentPosition]['action'])) {
// no action, and an action exists for this position
$currentData = $this->getData($this->currentPosition);
$action = $this->stack[$this->currentPosition]['action'];
if ($this->comeFromCancelledAction) {
//we return from a cancelled action
$currentData = $handler->onReturningFromCancelledAction($action, $currentData);
$this->comeFromCancelledAction = false;
} else {
$previousData = $this->getPreviousData();
if ($previousData != null) {
//we return from a sucessful action
$currentData = $handler->onReturningFromSuccesAction($action, $currentData, $previousData);
$this->resetPreviousData();
}
}
$this->storeCurrentData( $currentData );
}
$currentData = $this->getData($this->currentPosition);
if ($currentData == null) {
$currentData = $handler->getInitialData();
$this->storeCurrentData( $currentData );
}
return $currentData;
} else if ($this->getUrl($position) == $this->getCurrentUrl()) {
// seems that the user pressed the back or next button of the browser
// set the current position
$this->currentPosition = $position;
return $this->getData($position);
} else {
// the position does not exist or the url is incorrect
// redirect to the last known position
$this->redirect();
}
}
}
/**
* call this method after completing an action and need to redirect to the previous page.
* If you need to give some data to the previous action, use $dataForPreviousAction
*/
public function finishAction($dataForPreviousAction = null) {
$pop = array_pop($this->stack);
$this->currentPosition--;
$this->storePreviousData($dataForPreviousAction);
$this->redirect();
}
/* ----------------------------------------------------- */
/* PRIVATE METHODS */
/* ----------------------------------------------------- */
/**
* get the previous data for the current position
* used when a sub flow finish an action to give some data to the parent flow
*/
private function &getPreviousData() {
if (isset($this->stack[$this->currentPosition])) {
return $this->stack[$this->currentPosition]['previousData'];
} else {
return null;
}
}
/**
* get the current url without the stack parameter
*
* Attention: this method calls "basename" on PHP_SELF do strip the folder structure
* and assume that every pages are in the same directory.
*
* The "stack" parameter is removed from the query string
*
* Example: for the page "http://myserver.com/path/to/a.php?id=1&stack=2"
* PHP_SELF will be: /path/to/a.php
* QUERY_STRING wille be: id=1&stack=2
* This method will return: "a.php?id=1"
*/
private function getCurrentUrl() {
$basename = basename($_SERVER['PHP_SELF']);
if ($_SERVER['QUERY_STRING'] != '') {
return $basename.$this->removeQueryStringKey('?'.$_SERVER['QUERY_STRING'], 'stack');
} else {
return $basename;
}
}
/**
* add the "stack" parameter in an url
*/
private function addStackParam($url) {
return $url . (strpos($url, '?') === false ? '?' : '&') . 'stack=' . $this->currentPosition;
}
/**
* Usefull private method to remove a key=value from a query string.
*/
private function removeQueryStringKey($url, $key) {
$url = preg_replace('/(?:&|(\?))'.$key.'=[^&]*(?(1)&|)?/i', "$1", $url);
return $url != '?' ? $url : '';
}
/**
* reset the previous data so that the data are not used twice
*/
private function resetPreviousData() {
$this->stack[$this->currentPosition]['previousData'] = null;
}
}
Then define the abstract StackHandler class
abstract class StackHandler {
/**
* return the initial data to store for this current page
*/
public function &getInitialData() {
return null;
}
/**
* return an array containing the key/values that need to be fixed in sub flows
*/
public function getLinkedData() {
return null;
}
/**
* user ask to go to a sub page
*/
public function onAction(&$currentData, $action) {
$currentData = $_POST;
$nextUrl = $_POST['action'];
return array($currentData, $nextUrl);
}
public function onCancel(&$currentData) {
}
public function onReturningFromCancelledAction($action, &$currentData) {
}
public function onReturningFromSuccesAction($action, &$currentData, $previousData) {
}
}
Then add the following lines at the top of your pages. Adapt the handler it to fit your needs.
// be sure that a stack object exist in the session
if (!isset($_SESSION['stack'])) {
$_SESSION['stack'] = new Stack();
}
$myDad = $_SESSION['stack']->initialise(new DadStackHandler());
class DadStackHandler extends StackHandler {
/**
* return the initial data to store for this current page
*/
public function &getInitialData() {
if(! empty($_GET['id_dad']) && ctype_digit($_GET['id_dad'])){
// update
$myDad = new Dad($_GET['id_dad']);
} else {
// creation
$myDad = new Dad();
}
return $myDad;
}
/**
* return an array containing the key/values that need to be fixed in sub flows
*/
public function getLinkedData() {
$linkedData = array();
if (! empty($_GET['id_dad']) && ctype_digit($_GET['id_dad'])) {
$linkedData['id_dad'] = $_GET['id_dad'];
}
return $linkedData;
}
/**
* user ask to go to a sub page
*/
public function onAction(&$myDad, $action) {
//in order not to loose user inputs, save them in the current data
$myDad->name = $_POST['name'];
$nextUrl = null;
// find the next url based on the action name
if ($action == 'child') {
$nextUrl = 'child.php';
}
return array($myDad, $nextUrl);
}
public function onCancel(&$myDad) {
// probably nothing to do, leave the current data untouched
// or update current data
return $myDad;
}
public function onReturningFromCancelledAction($action, &$myDad) {
// probably nothing to do, leave the current data untouched
// called when returning from child.php
return $myDad;
}
public function onReturningFromSuccesAction($action, &$myDad, $newId) {
// update the id of the foreign field if needed
// or update the current data
// not a good example as in real life child should be a list and not a foreign key
// $myDad->childId = $newId;
$myDad->numberOfChildren++;
return $myDad;
}
}
...
if (user submit form and all input are correct) {
if ($myDad->save()) {
// the user finish an action, so we should redirect him to the previous one
if ($_SESSION['stack']->getCurrentPosition() > 0) {
$_SESSION['stack']->finishAction($myDad->idDad);
} else {
// default redirect, redirect to the same page in view more or redirect to a list page
}
}
}
I hope this could help others.