From 7968b09fea0df0d969dd0d828046da9509fd78fb Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Wed, 1 Oct 2008 18:36:52 +0000 Subject: [PATCH] ENHANCEMENT Added MultiForm->getSavedSteps() and MultiForm->getSavedStepByClass() ENHANCEMENT Added MultiFormStep->validateStep() for custom validation routines ENHANCEMENT Changed MultiForm->save() to use $form->getData() instead of $data to ensure that all fields are saveable into a DataObject (had trouble with ConfirmedPasswordField returning array instead of string) --- code/MultiForm.php | 173 +++++++++++++++++++++++++---------------- code/MultiFormStep.php | 63 +++++++++------ 2 files changed, 149 insertions(+), 87 deletions(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index bc883e1..4a9b524 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -54,7 +54,7 @@ abstract class MultiForm extends Form { 'executeForm', 'MultiFormSessionID', 'SecurityID' - ); + ); /** * Start the MultiForm instance. @@ -83,9 +83,8 @@ abstract class MultiForm extends Form { // Set up the actions for the current step $actions = $this->actionsFor($currentStep); - // Set up validation (if necessary) {@TODO find a better way instead - // of hardcoding a check for action_prev in order to prevent validation - // when hitting the back button + // Set up validation (if necessary) + // @todo find a better way instead of hardcoding a check for action_prev in order to prevent validation when hitting the back button $validator = null; if(empty($_REQUEST['action_prev'])) { if($this->getCurrentStep()->getValidator()) { @@ -183,8 +182,8 @@ abstract class MultiForm extends Form { */ protected function setSession() { // If there's a MultiFormSessionID variable set, find that, otherwise create a new session - if(isset($_GET['MultiFormSessionID'])) { - $this->session = $this->getSessionRecord($_GET['MultiFormSessionID']); + if(isset($_GET['MultiFormSessionID'])) { + $this->session = $this->getSessionRecord($_GET['MultiFormSessionID']); } // If there was no session found, create a new one instead @@ -192,24 +191,59 @@ abstract class MultiForm extends Form { $this->session = new MultiFormSession(); $this->session->write(); } - + // Create encrypted identification to the session instance if it doesn't exist if(!$this->session->Hash) { $this->session->Hash = sha1($this->session->ID . '-' . microtime()); $this->session->write(); } - } - - /** - * Return an instance of MultiFormSession. - * - * @param string $hash The unique, encrypted hash to identify the session - * @return MultiFormSession - */ - function getSessionRecord($hash) { - $SQL_hash = Convert::raw2sql($hash); - return DataObject::get_one('MultiFormSession', "Hash = '$SQL_hash' AND IsComplete = 0"); - } + } + + /** + * Return an instance of MultiFormSession. + * + * @param string $hash The unique, encrypted hash to identify the session + * @return MultiFormSession + */ + function getSessionRecord($hash) { + $SQL_hash = Convert::raw2sql($hash); + return DataObject::get_one('MultiFormSession', "Hash = '$SQL_hash' AND IsComplete = 0"); + } + + /** + * Get all steps saved in the database for the currently active session, + * in the order they were saved, oldest to newest (automatically ordered by ID). + * If you want a full chain of steps regardless if they've already been saved + * to the database, use {@link getAllStepsLinear()}. + * + * @return DataObjectSet|boolean A set of MultiFormStep subclasses + */ + function getSavedSteps() { + return DataObject::get( + 'MultiFormStep', + sprintf("SessionID = '%s'", + $this->session->ID + ) + ); + } + + /** + * Get a step which was previously saved to the database in the current session. + * Caution: This might cause unexpected behaviour if you have multiple steps + * in your chain with the same classname. + * + * @param string $className Classname of a {@link MultiFormStep} subclass + * @return MultiFormStep + */ + function getSavedStepByClass($className) { + return DataObject::get_one( + 'MultiFormStep', + sprintf("SessionID = '%s' AND ClassName = '%s'", + $this->session->ID, + Convert::raw2sql($className) + ) + ); + } /** * Build a FieldSet of the FormAction fields for the given step. @@ -241,7 +275,7 @@ abstract class MultiForm extends Form { } else { $actions->push(new FormAction('next', _t('MultiForm.NEXT', 'Next'))); } - + // If there is a previous step defined, add the back button if($step->getPreviousStep() && $step->canGoBack()) { // If there is a next step, insert the action before the next action @@ -284,16 +318,16 @@ abstract class MultiForm extends Form { * so you can create your own functionality which handles saving * of all the data collected through each step of the form. * - * @param array $data The request data returned from the form + * @param array $data The request data returned from the form * @param object $form The form that the action was called on */ - public function finish($data, $form) { - if(!$this->getCurrentStep()->isFinalStep()) { - Director::redirectBack(); - return false; - } - - // Save the form data for the current step + public function finish($data, $form) { + if(!$this->getCurrentStep()->isFinalStep()) { + Director::redirectBack(); + return false; + } + + // Save the form data for the current step $this->save($data); } @@ -303,24 +337,30 @@ abstract class MultiForm extends Form { * Saves the current step session data to the database, creates the * new step based on getNextStep() of the current step (or fetches * an existing one), resets the current step to the next step, - * then redirects to the newly set step. - * - * @param array $data The request data returned from the form + * then redirects to the newly set step. + * + * @param array $data The request data returned from the form * @param object $form The form that the action was called on */ public function next($data, $form) { - if(!$this->getCurrentStep()->getNextStep()) { + // Get the next step class + $nextStepClass = $this->getCurrentStep()->getNextStep(); + + if(!$nextStepClass) { + Director::redirectBack(); + return false; + } + + // custom validation (use MultiFormStep->getValidator() for built-in functionality) + if(!$this->getCurrentStep()->validateStep($data, $form)) { Director::redirectBack(); return false; } - // Get the next step class - $nextStepClass = $this->getCurrentStep()->getNextStep(); - // Save the form data for the current step - $this->save($data); + $this->save($form->getData()); - // Determine whether we can use a step already in the DB, or have to create a new one + // Determine whether we can use a step already in the DB, or have to create a new one if(!$nextStep = DataObject::get_one($nextStepClass, "SessionID = {$this->session->ID}")) { $nextStep = new $nextStepClass(); $nextStep->SessionID = $this->session->ID; @@ -340,9 +380,9 @@ abstract class MultiForm extends Form { * * Retrieves the previous step class, finds the record for that * class in the DB, and sets the current step to that step found. - * Finally, it redirects to that step. - * - * @param array $data The request data returned from the form + * Finally, it redirects to that step. + * + * @param array $data The request data returned from the form * @param object $form The form that the action was called on */ public function prev($data, $form) { @@ -381,7 +421,7 @@ abstract class MultiForm extends Form { $currentStep = $this->getCurrentStep(); if(is_array($data)) { foreach($data as $field => $value) { - if(in_array($field, $this->stat('ignored_fields')) || self::is_action_field($field)) { + if(in_array($field, $this->stat('ignored_fields'))) { unset($data[$field]); } } @@ -395,8 +435,8 @@ abstract class MultiForm extends Form { /** * Add the MultiFormSessionID variable to the URL on form submission. * This is a means to persist the session, by adding it's identification - * to the URL, which ties it back to this MultiForm instance. - * + * to the URL, which ties it back to this MultiForm instance. + * * @return string */ function FormAction() { @@ -407,13 +447,13 @@ abstract class MultiForm extends Form { return $action; } - /** + /** * Determine the steps to show in a linear fashion, starting from the - * first step. We run a recursive function passing the steps found - * by reference to get a listing of the steps. + * first step. We run {@link getAllStepsRecursive} passing the steps found + * by reference to get a listing of the steps. * - * @return DataObjectSet - */ + * @return DataObjectSet + */ public function getAllStepsLinear() { $stepsFound = new DataObjectSet(); @@ -436,6 +476,9 @@ abstract class MultiForm extends Form { * Recursively run through steps using the getNextStep() method on each step * to determine what the next step is, gathering each step along the way. * We stop on the last step, and return the results. + * If a step in the chain was already saved to the database in the current + * session, its used - otherwise a singleton of this step is used. + * Caution: Doesn't consider branching for steps which aren't in the database yet. * * @param $step Subclass of MultiFormStep to find the next step of * @param $stepsFound $stepsFound DataObjectSet reference, the steps found to call back on @@ -470,7 +513,7 @@ abstract class MultiForm extends Form { } else { return $stepsFound; } - } + } /** * Number of steps already completed (excluding currently started step). @@ -506,26 +549,26 @@ abstract class MultiForm extends Form { */ public function getCompletedPercent() { return (float)$this->CompletedStepCount * 100 / $this->TotalStepCount; - } + } - /** - * Determines whether the field is an action. This checks the string name of the - * field, and not the actual field object of one. The actual checking is done - * by doing a string check to see if "action_" is prefixed to the name of the - * field. For example, in the form system: FormAction('next', 'Next') field + /** + * Determines whether the field is an action. This checks the string name of the + * field, and not the actual field object of one. The actual checking is done + * by doing a string check to see if "action_" is prefixed to the name of the + * field. For example, in the form system: FormAction('next', 'Next') field * gives an ID of "action_next" * * The assumption here is the ID we're checking against has the prefix that we're - * looking for, otherwise this won't work. - * - * @param string $fieldName The name of the field to check is an action - * @param string $prefix The prefix of the string to check for, default is "action_" - * @return boolean - */ - public static function is_action_field($fieldName, $prefix = 'action_') { - if(substr((string)$fieldName, 0, strlen($prefix)) == $prefix) return true; - } + * looking for, otherwise this won't work. + * + * @param string $fieldName The name of the field to check is an action + * @param string $prefix The prefix of the string to check for, default is "action_" + * @return boolean + */ + public static function is_action_field($fieldName, $prefix = 'action_') { + if(substr((string)$fieldName, 0, strlen($prefix)) == $prefix) return true; + } -} +} ?> \ No newline at end of file diff --git a/code/MultiFormStep.php b/code/MultiFormStep.php index 7729fe0..58bc91e 100644 --- a/code/MultiFormStep.php +++ b/code/MultiFormStep.php @@ -1,7 +1,7 @@ -httpSubmission()}. * * @return Validator */ public function getValidator() { return false; } - - /** + + /** * Accessor method for $this->title * - * @return string Title of this step - */ - public function getTitle() { - return $this->title; - } + * @return string Title of this step + */ + public function getTitle() { + return $this->title; + } /** * Gets a direct link to this step (only works @@ -157,6 +158,24 @@ class MultiFormStep extends DataObject { $this->write(); } + /** + * Custom validation for a step. In most cases, it should be sufficient + * to have built-in validation through the {@link Validator} class + * on the {@link getValidator()} method. + * Use {@link Form->sessionMessage()} to feed back validation messages + * to the user. Please don't redirect from this method, + * this is taken care of in {@link next()}. + * + * @usedby next() + * + * @param array $data Request data + * @param Form $form + * @return boolean Validation success + */ + public function validateStep($data, $form) { + return true; + } + /** * Returns the first value of $next_step * @@ -274,19 +293,19 @@ class MultiFormStep extends DataObject { public function isFinalStep() { return $this->stat('is_final_step'); } - - /** - * Determines whether the currently viewed step is the current step set in the session. - * This assumes you are checking isCurrentStep() against a data record of a MultiFormStep - * subclass, otherwise it doesn't work. An example of this is using a singleton instance - it won't - * work because there's no data. - * - * @return boolean - */ - public function isCurrentStep() { - return ($this->class == $this->Session()->CurrentStep()->class) ? true : false; - } -} + /** + * Determines whether the currently viewed step is the current step set in the session. + * This assumes you are checking isCurrentStep() against a data record of a MultiFormStep + * subclass, otherwise it doesn't work. An example of this is using a singleton instance - it won't + * work because there's no data. + * + * @return boolean + */ + public function isCurrentStep() { + return ($this->class == $this->Session()->CurrentStep()->class) ? true : false; + } + +} ?> \ No newline at end of file