在会话和后退按钮中存储表单


Store forms in session and back button

我正在尝试实现以下场景:

1. user display the page addBook.php
2. user starts filling the form
3. but when he wants to select the book Author from the Author combo box, the Author is not yet created in the database so the user clicks a link to add a new Author
5. user is redirected to addAuthor.php
6. the user fill the form and when he submits it, he goes back to addBook.php with all the previous data already present and the new Author selected.

事情是:我有一些场景,其中有不止一个级别的递归。(例如:添加图书=>添加作者=>添加国家/地区)

我该怎么做?

At step #3, the link submit the form so that I can save it in session.
To handle recursion, I can use a Stack and push the current from on the Stack each time I click a link. And pop the last form of the Stack when the user completes the action correctly or click a cancel button.

我的问题是:

如何处理浏览器的后退按钮?如果用户不是点击"取消"按钮,而是点击后退按钮,我怎么会知道我需要弹出最后一个元素

你知道实现这一点的一些常见模式吗?

您必须在客户端上使用javascript并挂接到窗口卸载事件,序列化表单并将答案发送到服务器,服务器将其保存在会话中。

$(window).unload(function() {
     $.ajax({
          url   : 'autosave.php',
          data : $('#my_form').serialize()
     });
});

服务器上的

// autosave.php
$_SESSION['autosave_data'] = $_POST['autosave_data'];
// addbook.php
if (isset($_SESSION['autosave_data'])) {
    // populate the fields
}

这是我为解决问题而开发的解决方案。

因为这个问题不是客户端的问题,而是服务器端的问题。以下是我在项目中使用的php类:

首先是堆栈功能的主要类。需要在session_start之前完成包含,因为对象将存储在会话中

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;
    }
}

然后定义抽象StackHandler类

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) {
    }
}

然后在页面顶部添加以下行。根据您的需要调整处理程序。

// 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
        }
    }
}

我希望这能帮助其他人。