Im folgenden möchte ich eine mögliche Realisierung eines Multi Step Form Controllers vorstellen.
Die Ursprungsanforderungen, deren Resultat dieser Controller ist, waren etwas speziel aber nicht ungewöhlich:
- Felder des Formulars sind nur zum Teil direkt Properties eines Models zuzuordnen.
- Einige Felder gehören direkt zum Model A anderen zum Model B und wiederum andere gehören zu gar keinen Model.
- Es gibt die Möglichkeit innerhalb der Schritt vor und zurück zu Springen.
- Die Felder haben Teilweise Validierungen.
- Evtl Models werden in der finalen Action aus den Feldern des Formulars zusammengebaut.
Grunsätzlich gibt es also einige Punkte, die bedacht werden müssen:
- Beim Ruecksprung von bspw. Schritt 3 auf Schritt 2 muss sichergestellt werden, dass die Argumente die Schritt 1 erwartet rekonstruiert werden, da diese logischerweise nicht mehr im Request enthalten sind.
- Da die finale Action valide Daten erwartet und erneute komplett Validierung kompliziert und redundant wäre muss sichergestellt werden, das die final Action nicht direkt (z.B. durch manipulation der GET Parameter) angesprungen wird.
- Sollte auf einen Schritt die Session „austimen“, z.B. Weil der User zu lang AFK war oder der betreffende Schritt per Bookmark angesprungen wurde., so muss auf den ersten Schritt redirected werden.
- Da es keine direkte associatin zu Models gibt bzw. das Formular mehrer Models handhabt. Muss man mit namebased input Feldern (<f:form.textbox name=“whatever“ value=“{whatever}“ />) arbeiten.
Ein bischen Code sagt mehr als Tausend Worte daher hier erstmal der komplette Controller:
abstract class Tx_XXX_Controller_AbstractMultiStepFormController extends Tx_Extbase_MVC_Controller_ActionController { /** * Session storage key used * @var string */ protected $sessionDataStorageKey; /** * The session data container * @var array */ protected $sessionData = array(); /** * The actual form data * @var array */ protected $formData; /** * A list of action names that have been passed successfully. * @var array */ protected $passedActionMethodNames; /** * A list of action names that have to be passed before the final action can be executed. * @var array */ protected $mandatoryActionMethodNames; /** * The action methode name of the first action. * @var string */ protected $firstActionMethodName; /** * The action methode name of the first action. * @var string */ protected $finalActionMethodName; /** * Handles session/argument interaction to ensure correct validation. Furthermore * takes care of mandatory actions. * @return void */ protected function initializeAction() { $this->loadSessionData(); if ($this->isFinalAction() && !$this->passedMandatoryActions()) { $this->redirect($this->firstActionMethodName); } if ($this->formData === NULL) { if(!$this->isFirstAction()) { $this->redirect($this->firstActionMethodName); } else { $this->formData = array(); } } $requestArguments = $this->request->getArguments(); foreach ($this->arguments->getArgumentNames() as $argumentName) { if (array_key_exists($argumentName, $requestArguments)) { $this->formData[$argumentName] = $requestArguments[$argumentName]; } else { if(array_key_exists($argumentName, $this->formData)) { $requestArguments[$argumentName] = (String)$this->formData[$argumentName]; $_POST['tx_' . strtolower($this->extensionName) . '_' . strtolower($this->request->getPluginName())][$argumentName] = (String)$this->formData[$argumentName]; } } } $this->request->setArguments($requestArguments); $this->storeSessionData(); } /** * We use initializeView to determin if a certain action has been reached * and all it's validation has been passed successful. * @return void */ protected function initializeView() { $this->passedActionMethodNames[$this->actionMethodName] = TRUE; $this->storeSessionData(); } /** * Returns if this is the first action. * @return boolean */ protected function isFirstAction() { return $this->actionMethodName === $this->firstActionMethodName; } /** * Returns if this is the final action. * @return boolean */ protected function isFinalAction() { return $this->actionMethodName === $this->finalActionMethodName; } /** * Checks if all mandatory actions have been passed. * @return boolean */ protected function passedMandatoryActions() { foreach($this->mandatoryActionMethodNames as $mandatoryActionMethodeName) { if ($this->passedActionMethodNames[$mandatoryActionMethodeName] === NULL) { return FALSE; } } return TRUE; } /** * Loads data from session and populates formData and passedActionMethodeNames. * @return void */ protected function loadSessionData() { $this->sessionData = $GLOBALS['TSFE']->fe_user->getKey('ses', $this->sessionDataStorageKey); $this->formData = $this->sessionData['formData']; $this->passedActionMethodNames = $this->sessionData['passedActionMethodNames']; } /** * Stores data to session. Including formData and passedActionMethodeNames. * @return void */ protected function storeSessionData() { $this->sessionData['formData'] = $this->formData; $this->sessionData['passedActionMethodNames'] = $this->passedActionMethodNames; $GLOBALS['TSFE']->fe_user->setKey('ses', $this->sessionDataStorageKey, $this->sessionData); $GLOBALS['TSFE']->fe_user->storeSessionData(); } protected function clearSessionData() { $this->sessionData = array(); $GLOBALS['TSFE']->fe_user->setKey('ses', $this->sessionDataStorageKey, $this->sessionData); $GLOBALS['TSFE']->fe_user->storeSessionData(); } } |
Eine Ausführliche Erklärung folgt dann im nächsten Teil.
Eine entsprechende Implementierung muesste dann so aussehen:
class Tx_XXX_Controller_WhateverController extends Tx_XXX_Controller_AbstractMultiStepFormController { protected $sessionDataStorageKey = 'tx_xxx_whatever_form'; protected $firstActionMethodName = 'step1Action'; protected $finalActionMethodName = 'submitAction'; protected $mandatoryActionMethodNames = array('step1Action', 'step2Action', 'step3Action', 'summaryAction'); /** * @param string $name * @param string $firstname * @return string A html form */ public function step1Action($name = NULL, $firstname) { $this->view->assignMultiple(array( 'name' => $name, 'firstname' => $firstname); } /** * @param string $name * @param string $firstname * @param string $email * @param string $street * @param string $streetNumber * @validate $name notEmpty; * @validate $firstname notEmpty; * @return string A html form */ public function step2Action($name, $firstname $email = NULL, $street = NULL, $streetNumber = NULL) { $this->view->assignMultiple(array( 'email' => $email, 'street' => $street, 'streetNumber' => $streetNumber)); } /** * @param string $email * @param string $street * @param string $streetNumber * @param string $zip * @param string $location * @validate $email notEmpty; * @validate $street notEmpty; * @validate $streetNumber notEmpty; */ public function step3Action($email, $street, $streetNumber, $zip = NULL, $location = NULL) { $this->view->assignMultiple(array( 'zip' => $zip, 'location' => $location); } /** * @param string $zip * @param string $location * @validate $zip notEmpty; * @validate $location notEmpty; */ public function summary($zip, $location) { $this->view->assign('formData' => $this->formData); } public function submitAction() { // do something ... } } |