From 43cc780627c5a55d78e01cbd58e3946a20a235e0 Mon Sep 17 00:00:00 2001 From: Franco Springveldt Date: Thu, 7 Sep 2017 08:47:35 +1200 Subject: [PATCH] FIX ran linter and removed php tags from README --- README.md | 477 +++---- _config.php | 1 - lang/ar_SA.php | 8 +- lang/bg_BG.php | 8 +- lang/bs_BA.php | 8 +- lang/cs_CZ.php | 8 +- lang/da_DK.php | 8 +- lang/de_DE.php | 8 +- lang/en_US.php | 26 +- lang/eo_XX.php | 8 +- lang/es_419.php | 9 +- lang/es_AR.php | 8 +- lang/es_MX.php | 8 +- lang/et_EE.php | 8 +- lang/fr_FR.php | 8 +- lang/id_ID.php | 8 +- lang/is_IS.php | 8 +- lang/it_IT.php | 8 +- lang/ja_JP.php | 8 +- lang/ms_MY.php | 8 +- lang/nb_NO.php | 8 +- lang/nl_NL.php | 8 +- lang/pl_PL.php | 8 +- lang/pt_PT.php | 8 +- lang/sr_RS.php | 8 +- lang/sv_SE.php | 8 +- lang/tr_TR.php | 8 +- lang/zh_CN.php | 8 +- src/extensions/MultiFormObjectDecorator.php | 94 +- src/model/MultiForm.php | 1399 ++++++++++--------- src/model/MultiFormSession.php | 101 +- src/model/MultiFormStep.php | 908 ++++++------ src/tasks/MultiFormPurgeTask.php | 78 +- tests/MultiFormObjectDecoratorTest.php | 65 +- tests/MultiFormTest.php | 219 +-- 35 files changed, 1797 insertions(+), 1772 deletions(-) diff --git a/README.md b/README.md index 50d1a95..e4ca932 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ Using [Composer](https://getcomposer.org/), you can install multiform into your SilverStripe site using this command (while in the directory where your site is currently located) - composer require "silverstripe/multiform:*" +`composer require "silverstripe/multiform:*"` ### 2. Create subclass of MultiForm @@ -91,13 +91,11 @@ First of all, we need to create a new subclass of *MultiForm*. For the above example, our multi-form will be called *SurveyForm* - :::php - 'Thank you for your submission', - 'Content' => '

You have successfully submitted the form!

' - ); - } - - // ... - - } +} +class Page_Controller extends ContentController { + +// ... + + // + private static $allowed_actions = array( + 'SurveyForm', + 'finished' + ); + + public function SurveyForm() { + return new SurveyForm($this, 'Form'); + } + + public function finished() { + return array( + 'Title' => 'Thank you for your submission', + 'Content' => '

You have successfully submitted the form!

' + ); + } + +// ... + +} +``` The `SurveyForm()` function will create a new instance our subclass of MultiForm, which in this example, is *SurveyForm*. This in turn will then set @@ -242,21 +230,21 @@ like. Your template should look something like this, to render the form in: - :::html -
- <% if $Content %> - $Content - <% end_if %> - - <% if $SurveyForm %> - $SurveyForm - <% end_if %> - - <% if $Form %> - $Form - <% end_if %> -
- +```html +
+ <% if $Content %> + $Content + <% end_if %> + + <% if $SurveyForm %> + $SurveyForm + <% end_if %> + + <% if $Form %> + $Form + <% end_if %> +
+``` In this case, the above template example is a *sub-template* inside the *Layout* directory for the templates. Note that we have also included `$Form`, so @@ -281,11 +269,11 @@ To include these with our instance of multiform, we just need to add an For example: - :::html - <% with $SurveyForm %> - <% include MultiFormProgressList %> - <% end_with %> - +```html +<% with $SurveyForm %> + <% include MultiFormProgressList %> +<% end_with %> +``` This means the included template is rendered within the scope of the SurveyForm instance returned, instead of the top level controller context. @@ -293,24 +281,24 @@ This gives us the data to show the progression of the steps. Putting it together, we might have something looking like this: - :::html -
- <% if $Content %> - $Content - <% end_if %> - - <% if $SurveyForm %> - <% with $SurveyForm %> - <% include MultiFormProgressList %> - <% end_with %> - - $SurveyForm - <% end_if %> - <% if $Form %> - $Form - <% end_if %> -
+```html +
+ <% if $Content %> + $Content + <% end_if %> + + <% if $SurveyForm %> + <% with $SurveyForm %> + <% include MultiFormProgressList %> + <% end_with %> + $SurveyForm + <% end_if %> + <% if $Form %> + $Form + <% end_if %> +
+``` Feel free to play around with the progress indicators. If you need something specific to your project, just create a new "Include" template inside your own @@ -343,37 +331,35 @@ based on the submission value of another step. There are two methods supporting Here is an example of how to populate the email address from step 1 in step2 : - :::php - copyValueFromOtherStep($fields, 'Step1', 'Email'); - - return $fields; - } - } + // set the email field to the input from Step 1 + $this->copyValueFromOtherStep($fields, 'Step1', 'Email'); + return $fields; + } +} +``` ### 8. Finishing it up @@ -389,56 +375,54 @@ So, we must write some code on our subclass of *MultiForm*, overloading Here is an example of what we could do here: - :::php - session->ID}" - ); - - if($steps) { - foreach($steps as $step) { - if($step->class == 'SurveyFormPersonalDetailsStep') { - $member = new Member(); - $data = $step->loadData(); + $steps = DataObject::get( + 'MultiFormStep', + "SessionID = {$this->session->ID}" + ); + + if($steps) { + foreach($steps as $step) { + if($step->class == 'SurveyFormPersonalDetailsStep') { + $member = new Member(); + $data = $step->loadData(); - if($data) { - $member->update($data); - $member->write(); - } - } - - if($step->class == 'SurveyOrganisationDetailsStep') { - $organisation = new Organisation(); - $data = $step->loadData(); + if($data) { + $member->update($data); + $member->write(); + } + } - if($data) { - $organisation->update($data); + if($step->class == 'SurveyOrganisationDetailsStep') { + $organisation = new Organisation(); + $data = $step->loadData(); - if($member && $member->ID) { - $organisation->MemberID = $member->ID; - } + if($data) { + $organisation->update($data); - $organisation->write(); - } - } - // Shows the step data (unserialized by loadData) - // Debug::show($step->loadData()); - } - } + if($member && $member->ID) { + $organisation->MemberID = $member->ID; + } - $this->controller->redirect($this->controller->Link() . 'finished'); - } - } + $organisation->write(); + } + } + // Shows the step data (unserialized by loadData) + // Debug::show($step->loadData()); + } + } + $this->controller->redirect($this->controller->Link() . 'finished'); + } +} +``` #### 9. Organisation data model @@ -449,17 +433,14 @@ groups in SilverStripe) so we need to create it: This example has been chosen as a separate DataObject but you may wish to change the code and add the data to the Member class instead. - :::php - session->delete(); - +```php +$this->session->delete(); +``` This will also go through each of it's steps and delete them as well. @@ -517,24 +498,24 @@ be something different based on a user's choice of input during the step, you can override getNextStep() on any given step to manually override what the next step should be. An example: - :::php - class MyStep extends MultiFormStep - - // ... - - public function getNextStep() { - $data = $this->loadData(); - if(@$data['Gender'] == 'Male') { - return 'TestThirdCase1Step'; - } else { - return 'TestThirdCase2Step'; - } - } - - // ... - - } +```php +class MyStep extends MultiFormStep +// ... + + public function getNextStep() { + $data = $this->loadData(); + if(@$data['Gender'] == 'Male') { + return 'TestThirdCase1Step'; + } else { + return 'TestThirdCase2Step'; + } + } + +// ... + +} +``` ### Validation To define validation on a step-by-step basis, please define getValidator() and @@ -543,22 +524,22 @@ validation see [:form](http://doc.silverstripe.org/form-validation). e.g. - :::php - class MyStep extends MultiFormStep { - - ... - - public function getValidator() { - return new RequiredFields(array( - 'Name', - 'Email' - )); - } - - ... - - } +```php +class MyStep extends MultiFormStep { + ... + + public function getValidator() { + return new RequiredFields(array( + 'Name', + 'Email' + )); + } + + ... + +} +``` ### finish() @@ -573,29 +554,27 @@ won't be saved. For example: - :::php - filter(array( - "SessionID" => $this->session->ID - )); + public static $start_step = 'SurveyFormPersonalDetailsStep'; - if($steps) { - foreach($steps as $step) { - // Shows the step data (unserialized by loadData) - Debug::show($step->loadData()); - } - } - } - } + public function finish($data, $form) { + parent::finish($data, $form); + $steps = MultiFormStep::get()->filter(array( + "SessionID" => $this->session->ID + )); + + if($steps) { + foreach($steps as $step) { + // Shows the step data (unserialized by loadData) + Debug::show($step->loadData()); + } + } + } +} +``` The above is a sample bit of code that simply fetches all the steps in the database that were saved. Further refinement could include getting steps only @@ -613,9 +592,9 @@ idea to immediately delete this data after the user has submitted. This can be easily achieved by adding the following line at the end of your `finish()` method on your MultiForm subclass. - :::php - $this->session->delete(); - +```php +$this->session->delete(); +``` ### Expiring old session data diff --git a/_config.php b/_config.php index a4abe2d..b3d9bbc 100644 --- a/_config.php +++ b/_config.php @@ -1,2 +1 @@ \ No newline at end of file diff --git a/lang/bg_BG.php b/lang/bg_BG.php index eeed11d..f11b98c 100644 --- a/lang/bg_BG.php +++ b/lang/bg_BG.php @@ -10,10 +10,10 @@ i18n::include_locale_file('modules: multiform', 'en_US'); global $lang; -if(array_key_exists('bg_BG', $lang) && is_array($lang['bg_BG'])) { - $lang['bg_BG'] = array_merge($lang['en_US'], $lang['bg_BG']); +if (array_key_exists('bg_BG', $lang) && is_array($lang['bg_BG'])) { + $lang['bg_BG'] = array_merge($lang['en_US'], $lang['bg_BG']); } else { - $lang['bg_BG'] = $lang['en_US']; + $lang['bg_BG'] = $lang['en_US']; } $lang['bg_BG']['MultiForm']['BACK'] = 'Назад'; @@ -23,5 +23,3 @@ $lang['bg_BG']['MultiFormSession']['plural_name'] = '(никакви)'; $lang['bg_BG']['MultiFormSession']['singular_name'] = '(никакво)'; $lang['bg_BG']['MultiFormStep']['plural_name'] = '(никакви)'; $lang['bg_BG']['MultiFormStep']['singular_name'] = '(никакво)'; - -?> \ No newline at end of file diff --git a/lang/bs_BA.php b/lang/bs_BA.php index 9ce2a26..1bb4b7a 100644 --- a/lang/bs_BA.php +++ b/lang/bs_BA.php @@ -10,10 +10,10 @@ i18n::include_locale_file('modules: multiform', 'en_US'); global $lang; -if(array_key_exists('bs_BA', $lang) && is_array($lang['bs_BA'])) { - $lang['bs_BA'] = array_merge($lang['en_US'], $lang['bs_BA']); +if (array_key_exists('bs_BA', $lang) && is_array($lang['bs_BA'])) { + $lang['bs_BA'] = array_merge($lang['en_US'], $lang['bs_BA']); } else { - $lang['bs_BA'] = $lang['en_US']; + $lang['bs_BA'] = $lang['en_US']; } $lang['bs_BA']['MultiFormSession']['db_Hash'] = 'Hash'; @@ -24,5 +24,3 @@ $lang['bs_BA']['MultiFormSession']['singular_name'] = '(ništa)'; $lang['bs_BA']['MultiFormStep']['db_Data'] = 'Podaci'; $lang['bs_BA']['MultiFormStep']['plural_name'] = '(ništa)'; $lang['bs_BA']['MultiFormStep']['singular_name'] = '(ništa)'; - -?> \ No newline at end of file diff --git a/lang/cs_CZ.php b/lang/cs_CZ.php index 42f1857..ebeafed 100644 --- a/lang/cs_CZ.php +++ b/lang/cs_CZ.php @@ -10,10 +10,10 @@ i18n::include_locale_file('modules: multiform', 'en_US'); global $lang; -if(array_key_exists('cs_CZ', $lang) && is_array($lang['cs_CZ'])) { - $lang['cs_CZ'] = array_merge($lang['en_US'], $lang['cs_CZ']); +if (array_key_exists('cs_CZ', $lang) && is_array($lang['cs_CZ'])) { + $lang['cs_CZ'] = array_merge($lang['en_US'], $lang['cs_CZ']); } else { - $lang['cs_CZ'] = $lang['en_US']; + $lang['cs_CZ'] = $lang['en_US']; } $lang['cs_CZ']['MultiForm']['BACK'] = 'Zpět'; @@ -27,5 +27,3 @@ $lang['cs_CZ']['MultiFormSession']['singular_name'] = '(žádný)'; $lang['cs_CZ']['MultiFormStep']['db_Data'] = 'Data'; $lang['cs_CZ']['MultiFormStep']['plural_name'] = '(žádný)'; $lang['cs_CZ']['MultiFormStep']['singular_name'] = '(žádný)'; - -?> \ No newline at end of file diff --git a/lang/da_DK.php b/lang/da_DK.php index 9ef3bb1..4a3e435 100644 --- a/lang/da_DK.php +++ b/lang/da_DK.php @@ -10,10 +10,10 @@ i18n::include_locale_file('modules: multiform', 'en_US'); global $lang; -if(array_key_exists('da_DK', $lang) && is_array($lang['da_DK'])) { - $lang['da_DK'] = array_merge($lang['en_US'], $lang['da_DK']); +if (array_key_exists('da_DK', $lang) && is_array($lang['da_DK'])) { + $lang['da_DK'] = array_merge($lang['en_US'], $lang['da_DK']); } else { - $lang['da_DK'] = $lang['en_US']; + $lang['da_DK'] = $lang['en_US']; } $lang['da_DK']['MultiFormSession']['db_Hash'] = 'Havelåge'; @@ -24,5 +24,3 @@ $lang['da_DK']['MultiFormSession']['singular_name'] = '(ingen)'; $lang['da_DK']['MultiFormStep']['db_Data'] = 'Data'; $lang['da_DK']['MultiFormStep']['plural_name'] = '(ingen)'; $lang['da_DK']['MultiFormStep']['singular_name'] = '(none)'; - -?> \ No newline at end of file diff --git a/lang/de_DE.php b/lang/de_DE.php index 73232b4..8ad2e62 100644 --- a/lang/de_DE.php +++ b/lang/de_DE.php @@ -10,10 +10,10 @@ i18n::include_locale_file('modules: multiform', 'en_US'); global $lang; -if(array_key_exists('de_DE', $lang) && is_array($lang['de_DE'])) { - $lang['de_DE'] = array_merge($lang['en_US'], $lang['de_DE']); +if (array_key_exists('de_DE', $lang) && is_array($lang['de_DE'])) { + $lang['de_DE'] = array_merge($lang['en_US'], $lang['de_DE']); } else { - $lang['de_DE'] = $lang['en_US']; + $lang['de_DE'] = $lang['en_US']; } $lang['de_DE']['MultiForm']['BACK'] = 'Zurück'; @@ -27,5 +27,3 @@ $lang['de_DE']['MultiFormSession']['singular_name'] = 'Multi-Formular'; $lang['de_DE']['MultiFormStep']['db_Data'] = 'Daten'; $lang['de_DE']['MultiFormStep']['plural_name'] = 'Multi-Formular-Schritte'; $lang['de_DE']['MultiFormStep']['singular_name'] = 'Multi-Formular-Schritt'; - -?> \ No newline at end of file diff --git a/lang/en_US.php b/lang/en_US.php index 37832ff..5cbbfb8 100644 --- a/lang/en_US.php +++ b/lang/en_US.php @@ -6,24 +6,22 @@ $lang['en_US']['MultiForm']['BACK'] = 'Back'; $lang['en_US']['MultiForm']['NEXT'] = 'Next'; $lang['en_US']['MultiForm']['SUBMIT'] = 'Submit'; $lang['en_US']['MultiFormSession']['PLURALNAME'] = array( - 'Multi Form Sessions', - 50, - 'Pural name of the object, used in dropdowns and to generally identify a collection of this object in the interface' + 'Multi Form Sessions', + 50, + 'Pural name of the object, used in dropdowns and to generally identify a collection of this object in the interface' ); $lang['en_US']['MultiFormSession']['SINGULARNAME'] = array( - 'Multi Form Session', - 50, - 'Singular name of the object, used in dropdowns and to generally identify a single object in the interface' + 'Multi Form Session', + 50, + 'Singular name of the object, used in dropdowns and to generally identify a single object in the interface' ); $lang['en_US']['MultiFormStep']['PLURALNAME'] = array( - 'Multi Form Steps', - 50, - 'Pural name of the object, used in dropdowns and to generally identify a collection of this object in the interface' + 'Multi Form Steps', + 50, + 'Pural name of the object, used in dropdowns and to generally identify a collection of this object in the interface' ); $lang['en_US']['MultiFormStep']['SINGULARNAME'] = array( - 'Multi Form Step', - 50, - 'Singular name of the object, used in dropdowns and to generally identify a single object in the interface' + 'Multi Form Step', + 50, + 'Singular name of the object, used in dropdowns and to generally identify a single object in the interface' ); - -?> \ No newline at end of file diff --git a/lang/eo_XX.php b/lang/eo_XX.php index 732acaa..3fd7083 100644 --- a/lang/eo_XX.php +++ b/lang/eo_XX.php @@ -10,10 +10,10 @@ i18n::include_locale_file('modules: multiform', 'en_US'); global $lang; -if(array_key_exists('eo_XX', $lang) && is_array($lang['eo_XX'])) { - $lang['eo_XX'] = array_merge($lang['en_US'], $lang['eo_XX']); +if (array_key_exists('eo_XX', $lang) && is_array($lang['eo_XX'])) { + $lang['eo_XX'] = array_merge($lang['en_US'], $lang['eo_XX']); } else { - $lang['eo_XX'] = $lang['en_US']; + $lang['eo_XX'] = $lang['en_US']; } $lang['eo_XX']['MultiForm']['BACK'] = 'Retro'; @@ -27,5 +27,3 @@ $lang['eo_XX']['MultiFormSession']['singular_name'] = '(neniu)'; $lang['eo_XX']['MultiFormStep']['db_Data'] = 'Datumoj'; $lang['eo_XX']['MultiFormStep']['plural_name'] = '(neniu)'; $lang['eo_XX']['MultiFormStep']['singular_name'] = '(neniu)'; - -?> \ No newline at end of file diff --git a/lang/es_419.php b/lang/es_419.php index 3e853d7..7a49f61 100644 --- a/lang/es_419.php +++ b/lang/es_419.php @@ -10,11 +10,8 @@ i18n::include_locale_file('modules: multiform', 'en_US'); global $lang; -if(array_key_exists('es_', $lang) && is_array($lang['es_'])) { - $lang['es_'] = array_merge($lang['en_US'], $lang['es_']); +if (array_key_exists('es_', $lang) && is_array($lang['es_'])) { + $lang['es_'] = array_merge($lang['en_US'], $lang['es_']); } else { - $lang['es_'] = $lang['en_US']; + $lang['es_'] = $lang['en_US']; } - - -?> \ No newline at end of file diff --git a/lang/es_AR.php b/lang/es_AR.php index 03c4768..6bc9c5e 100644 --- a/lang/es_AR.php +++ b/lang/es_AR.php @@ -10,10 +10,10 @@ i18n::include_locale_file('modules: multiform', 'en_US'); global $lang; -if(array_key_exists('es_AR', $lang) && is_array($lang['es_AR'])) { - $lang['es_AR'] = array_merge($lang['en_US'], $lang['es_AR']); +if (array_key_exists('es_AR', $lang) && is_array($lang['es_AR'])) { + $lang['es_AR'] = array_merge($lang['en_US'], $lang['es_AR']); } else { - $lang['es_AR'] = $lang['en_US']; + $lang['es_AR'] = $lang['en_US']; } $lang['es_AR']['MultiForm']['BACK'] = 'Volver'; @@ -27,5 +27,3 @@ $lang['es_AR']['MultiFormSession']['singular_name'] = '(ninguno)'; $lang['es_AR']['MultiFormStep']['db_Data'] = 'Datos'; $lang['es_AR']['MultiFormStep']['plural_name'] = '(ninguno)'; $lang['es_AR']['MultiFormStep']['singular_name'] = '(ninguno)'; - -?> \ No newline at end of file diff --git a/lang/es_MX.php b/lang/es_MX.php index 6689c66..f790cbc 100644 --- a/lang/es_MX.php +++ b/lang/es_MX.php @@ -10,10 +10,10 @@ i18n::include_locale_file('modules: multiform', 'en_US'); global $lang; -if(array_key_exists('es_MX', $lang) && is_array($lang['es_MX'])) { - $lang['es_MX'] = array_merge($lang['en_US'], $lang['es_MX']); +if (array_key_exists('es_MX', $lang) && is_array($lang['es_MX'])) { + $lang['es_MX'] = array_merge($lang['en_US'], $lang['es_MX']); } else { - $lang['es_MX'] = $lang['en_US']; + $lang['es_MX'] = $lang['en_US']; } $lang['es_MX']['MultiForm']['BACK'] = 'Atrás'; @@ -27,5 +27,3 @@ $lang['es_MX']['MultiFormSession']['singular_name'] = '(ningún)'; $lang['es_MX']['MultiFormStep']['db_Data'] = 'Datos'; $lang['es_MX']['MultiFormStep']['plural_name'] = '(ningunos)'; $lang['es_MX']['MultiFormStep']['singular_name'] = '(ningún)'; - -?> \ No newline at end of file diff --git a/lang/et_EE.php b/lang/et_EE.php index 9311d8b..7f836c6 100644 --- a/lang/et_EE.php +++ b/lang/et_EE.php @@ -10,10 +10,10 @@ i18n::include_locale_file('modules: multiform', 'en_US'); global $lang; -if(array_key_exists('et_EE', $lang) && is_array($lang['et_EE'])) { - $lang['et_EE'] = array_merge($lang['en_US'], $lang['et_EE']); +if (array_key_exists('et_EE', $lang) && is_array($lang['et_EE'])) { + $lang['et_EE'] = array_merge($lang['en_US'], $lang['et_EE']); } else { - $lang['et_EE'] = $lang['en_US']; + $lang['et_EE'] = $lang['en_US']; } $lang['et_EE']['MultiForm']['BACK'] = 'Tagasi'; @@ -27,5 +27,3 @@ $lang['et_EE']['MultiFormSession']['singular_name'] = '(none)'; $lang['et_EE']['MultiFormStep']['db_Data'] = 'Andmed'; $lang['et_EE']['MultiFormStep']['plural_name'] = '(none)'; $lang['et_EE']['MultiFormStep']['singular_name'] = '(none)'; - -?> \ No newline at end of file diff --git a/lang/fr_FR.php b/lang/fr_FR.php index 5aecb8d..0486d56 100644 --- a/lang/fr_FR.php +++ b/lang/fr_FR.php @@ -10,10 +10,10 @@ i18n::include_locale_file('modules: multiform', 'en_US'); global $lang; -if(array_key_exists('fr_FR', $lang) && is_array($lang['fr_FR'])) { - $lang['fr_FR'] = array_merge($lang['en_US'], $lang['fr_FR']); +if (array_key_exists('fr_FR', $lang) && is_array($lang['fr_FR'])) { + $lang['fr_FR'] = array_merge($lang['en_US'], $lang['fr_FR']); } else { - $lang['fr_FR'] = $lang['en_US']; + $lang['fr_FR'] = $lang['en_US']; } $lang['fr_FR']['MultiForm']['BACK'] = 'Retour'; @@ -27,5 +27,3 @@ $lang['fr_FR']['MultiFormSession']['singular_name'] = '(aucun)'; $lang['fr_FR']['MultiFormStep']['db_Data'] = 'Data'; $lang['fr_FR']['MultiFormStep']['plural_name'] = '(aucun)'; $lang['fr_FR']['MultiFormStep']['singular_name'] = '(aucun)'; - -?> \ No newline at end of file diff --git a/lang/id_ID.php b/lang/id_ID.php index 54362d2..f784d53 100644 --- a/lang/id_ID.php +++ b/lang/id_ID.php @@ -10,10 +10,10 @@ i18n::include_locale_file('modules: multiform', 'en_US'); global $lang; -if(array_key_exists('id_ID', $lang) && is_array($lang['id_ID'])) { - $lang['id_ID'] = array_merge($lang['en_US'], $lang['id_ID']); +if (array_key_exists('id_ID', $lang) && is_array($lang['id_ID'])) { + $lang['id_ID'] = array_merge($lang['en_US'], $lang['id_ID']); } else { - $lang['id_ID'] = $lang['en_US']; + $lang['id_ID'] = $lang['en_US']; } $lang['id_ID']['MultiForm']['BACK'] = 'Kembali'; @@ -26,5 +26,3 @@ $lang['id_ID']['MultiFormSession']['singular_name'] = '(tidak ada)'; $lang['id_ID']['MultiFormStep']['db_Data'] = 'Data'; $lang['id_ID']['MultiFormStep']['plural_name'] = '(tidak ada)'; $lang['id_ID']['MultiFormStep']['singular_name'] = '(tidak ada)'; - -?> \ No newline at end of file diff --git a/lang/is_IS.php b/lang/is_IS.php index e8d422e..63f80d4 100644 --- a/lang/is_IS.php +++ b/lang/is_IS.php @@ -10,10 +10,10 @@ i18n::include_locale_file('modules: multiform', 'en_US'); global $lang; -if(array_key_exists('is_IS', $lang) && is_array($lang['is_IS'])) { - $lang['is_IS'] = array_merge($lang['en_US'], $lang['is_IS']); +if (array_key_exists('is_IS', $lang) && is_array($lang['is_IS'])) { + $lang['is_IS'] = array_merge($lang['en_US'], $lang['is_IS']); } else { - $lang['is_IS'] = $lang['en_US']; + $lang['is_IS'] = $lang['en_US']; } $lang['is_IS']['MultiFormSession']['plural_name'] = '(ekkert)'; @@ -21,5 +21,3 @@ $lang['is_IS']['MultiFormSession']['singular_name'] = '(ekkert)'; $lang['is_IS']['MultiFormStep']['db_Data'] = 'Gögn'; $lang['is_IS']['MultiFormStep']['plural_name'] = '(ekkert)'; $lang['is_IS']['MultiFormStep']['singular_name'] = '(ekkert)'; - -?> \ No newline at end of file diff --git a/lang/it_IT.php b/lang/it_IT.php index b5980fe..f658361 100644 --- a/lang/it_IT.php +++ b/lang/it_IT.php @@ -10,10 +10,10 @@ i18n::include_locale_file('modules: multiform', 'en_US'); global $lang; -if(array_key_exists('it_IT', $lang) && is_array($lang['it_IT'])) { - $lang['it_IT'] = array_merge($lang['en_US'], $lang['it_IT']); +if (array_key_exists('it_IT', $lang) && is_array($lang['it_IT'])) { + $lang['it_IT'] = array_merge($lang['en_US'], $lang['it_IT']); } else { - $lang['it_IT'] = $lang['en_US']; + $lang['it_IT'] = $lang['en_US']; } $lang['it_IT']['MultiForm']['BACK'] = 'Indietro'; @@ -27,5 +27,3 @@ $lang['it_IT']['MultiFormSession']['singular_name'] = '(nessuno)'; $lang['it_IT']['MultiFormStep']['db_Data'] = 'Dati'; $lang['it_IT']['MultiFormStep']['plural_name'] = '(nessuno)'; $lang['it_IT']['MultiFormStep']['singular_name'] = '(nessuno)'; - -?> \ No newline at end of file diff --git a/lang/ja_JP.php b/lang/ja_JP.php index 4bebaf3..1ee4235 100644 --- a/lang/ja_JP.php +++ b/lang/ja_JP.php @@ -10,10 +10,10 @@ i18n::include_locale_file('modules: multiform', 'en_US'); global $lang; -if(array_key_exists('ja_JP', $lang) && is_array($lang['ja_JP'])) { - $lang['ja_JP'] = array_merge($lang['en_US'], $lang['ja_JP']); +if (array_key_exists('ja_JP', $lang) && is_array($lang['ja_JP'])) { + $lang['ja_JP'] = array_merge($lang['en_US'], $lang['ja_JP']); } else { - $lang['ja_JP'] = $lang['en_US']; + $lang['ja_JP'] = $lang['en_US']; } $lang['ja_JP']['MultiForm']['BACK'] = '戻る'; @@ -21,5 +21,3 @@ $lang['ja_JP']['MultiForm']['NEXT'] = '次へ'; $lang['ja_JP']['MultiForm']['SUBMIT'] = '送信'; $lang['ja_JP']['MultiFormSession']['db_Hash'] = 'ハッシュ'; $lang['ja_JP']['MultiFormStep']['db_Data'] = 'データ'; - -?> \ No newline at end of file diff --git a/lang/ms_MY.php b/lang/ms_MY.php index f43ee01..b46c4dd 100644 --- a/lang/ms_MY.php +++ b/lang/ms_MY.php @@ -10,10 +10,10 @@ i18n::include_locale_file('modules: multiform', 'en_US'); global $lang; -if(array_key_exists('ms_MY', $lang) && is_array($lang['ms_MY'])) { - $lang['ms_MY'] = array_merge($lang['en_US'], $lang['ms_MY']); +if (array_key_exists('ms_MY', $lang) && is_array($lang['ms_MY'])) { + $lang['ms_MY'] = array_merge($lang['en_US'], $lang['ms_MY']); } else { - $lang['ms_MY'] = $lang['en_US']; + $lang['ms_MY'] = $lang['en_US']; } $lang['ms_MY']['MultiFormSession']['db_Hash'] = 'Cincangan'; @@ -24,5 +24,3 @@ $lang['ms_MY']['MultiFormSession']['singular_name'] = '(tiada)'; $lang['ms_MY']['MultiFormStep']['db_Data'] = 'Data'; $lang['ms_MY']['MultiFormStep']['plural_name'] = '(tiada)'; $lang['ms_MY']['MultiFormStep']['singular_name'] = '(tiada)'; - -?> \ No newline at end of file diff --git a/lang/nb_NO.php b/lang/nb_NO.php index 91a50ae..40ab72e 100644 --- a/lang/nb_NO.php +++ b/lang/nb_NO.php @@ -10,10 +10,10 @@ i18n::include_locale_file('modules: multiform', 'en_US'); global $lang; -if(array_key_exists('nb_NO', $lang) && is_array($lang['nb_NO'])) { - $lang['nb_NO'] = array_merge($lang['en_US'], $lang['nb_NO']); +if (array_key_exists('nb_NO', $lang) && is_array($lang['nb_NO'])) { + $lang['nb_NO'] = array_merge($lang['en_US'], $lang['nb_NO']); } else { - $lang['nb_NO'] = $lang['en_US']; + $lang['nb_NO'] = $lang['en_US']; } $lang['nb_NO']['MultiForm']['BACK'] = 'Forrige'; @@ -27,5 +27,3 @@ $lang['nb_NO']['MultiFormSession']['singular_name'] = '(ingen)'; $lang['nb_NO']['MultiFormStep']['db_Data'] = 'Data'; $lang['nb_NO']['MultiFormStep']['plural_name'] = '(ingen)'; $lang['nb_NO']['MultiFormStep']['singular_name'] = '(ingen)'; - -?> \ No newline at end of file diff --git a/lang/nl_NL.php b/lang/nl_NL.php index cb7c7e8..3b6cf87 100644 --- a/lang/nl_NL.php +++ b/lang/nl_NL.php @@ -10,10 +10,10 @@ i18n::include_locale_file('modules: multiform', 'en_US'); global $lang; -if(array_key_exists('nl_NL', $lang) && is_array($lang['nl_NL'])) { - $lang['nl_NL'] = array_merge($lang['en_US'], $lang['nl_NL']); +if (array_key_exists('nl_NL', $lang) && is_array($lang['nl_NL'])) { + $lang['nl_NL'] = array_merge($lang['en_US'], $lang['nl_NL']); } else { - $lang['nl_NL'] = $lang['en_US']; + $lang['nl_NL'] = $lang['en_US']; } $lang['nl_NL']['MultiForm']['BACK'] = 'Terug'; @@ -27,5 +27,3 @@ $lang['nl_NL']['MultiFormSession']['singular_name'] = '(geen)'; $lang['nl_NL']['MultiFormStep']['db_Data'] = 'Data'; $lang['nl_NL']['MultiFormStep']['plural_name'] = '(geen)'; $lang['nl_NL']['MultiFormStep']['singular_name'] = '(geen)'; - -?> \ No newline at end of file diff --git a/lang/pl_PL.php b/lang/pl_PL.php index a5ee74b..06b90a6 100644 --- a/lang/pl_PL.php +++ b/lang/pl_PL.php @@ -10,10 +10,10 @@ i18n::include_locale_file('modules: multiform', 'en_US'); global $lang; -if(array_key_exists('pl_PL', $lang) && is_array($lang['pl_PL'])) { - $lang['pl_PL'] = array_merge($lang['en_US'], $lang['pl_PL']); +if (array_key_exists('pl_PL', $lang) && is_array($lang['pl_PL'])) { + $lang['pl_PL'] = array_merge($lang['en_US'], $lang['pl_PL']); } else { - $lang['pl_PL'] = $lang['en_US']; + $lang['pl_PL'] = $lang['en_US']; } $lang['pl_PL']['MultiForm']['BACK'] = 'Wstecz'; @@ -27,5 +27,3 @@ $lang['pl_PL']['MultiFormSession']['singular_name'] = '(brak)'; $lang['pl_PL']['MultiFormStep']['db_Data'] = 'Dane'; $lang['pl_PL']['MultiFormStep']['plural_name'] = '(brak)'; $lang['pl_PL']['MultiFormStep']['singular_name'] = '(brak)'; - -?> \ No newline at end of file diff --git a/lang/pt_PT.php b/lang/pt_PT.php index 524dc95..dc4aa7d 100644 --- a/lang/pt_PT.php +++ b/lang/pt_PT.php @@ -10,10 +10,10 @@ i18n::include_locale_file('modules: multiform', 'en_US'); global $lang; -if(array_key_exists('pt_PT', $lang) && is_array($lang['pt_PT'])) { - $lang['pt_PT'] = array_merge($lang['en_US'], $lang['pt_PT']); +if (array_key_exists('pt_PT', $lang) && is_array($lang['pt_PT'])) { + $lang['pt_PT'] = array_merge($lang['en_US'], $lang['pt_PT']); } else { - $lang['pt_PT'] = $lang['en_US']; + $lang['pt_PT'] = $lang['en_US']; } $lang['pt_PT']['MultiFormSession']['db_IsComplete'] = 'Está completa'; @@ -23,5 +23,3 @@ $lang['pt_PT']['MultiFormSession']['singular_name'] = '(nenhum)'; $lang['pt_PT']['MultiFormStep']['db_Data'] = 'Dados'; $lang['pt_PT']['MultiFormStep']['plural_name'] = '(nenhum)'; $lang['pt_PT']['MultiFormStep']['singular_name'] = '(nenhum)'; - -?> \ No newline at end of file diff --git a/lang/sr_RS.php b/lang/sr_RS.php index a9aa411..991ed0d 100644 --- a/lang/sr_RS.php +++ b/lang/sr_RS.php @@ -10,10 +10,10 @@ i18n::include_locale_file('modules: multiform', 'en_US'); global $lang; -if(array_key_exists('sr_RS', $lang) && is_array($lang['sr_RS'])) { - $lang['sr_RS'] = array_merge($lang['en_US'], $lang['sr_RS']); +if (array_key_exists('sr_RS', $lang) && is_array($lang['sr_RS'])) { + $lang['sr_RS'] = array_merge($lang['en_US'], $lang['sr_RS']); } else { - $lang['sr_RS'] = $lang['en_US']; + $lang['sr_RS'] = $lang['en_US']; } $lang['sr_RS']['MultiFormSession']['db_IsComplete'] = 'ЈеЗавршен'; @@ -22,5 +22,3 @@ $lang['sr_RS']['MultiFormSession']['singular_name'] = '(без)'; $lang['sr_RS']['MultiFormStep']['db_Data'] = 'Подаци'; $lang['sr_RS']['MultiFormStep']['plural_name'] = '(без)'; $lang['sr_RS']['MultiFormStep']['singular_name'] = '(без)'; - -?> \ No newline at end of file diff --git a/lang/sv_SE.php b/lang/sv_SE.php index ef01aa4..f51b787 100644 --- a/lang/sv_SE.php +++ b/lang/sv_SE.php @@ -10,10 +10,10 @@ i18n::include_locale_file('modules: multiform', 'en_US'); global $lang; -if(array_key_exists('sv_SE', $lang) && is_array($lang['sv_SE'])) { - $lang['sv_SE'] = array_merge($lang['en_US'], $lang['sv_SE']); +if (array_key_exists('sv_SE', $lang) && is_array($lang['sv_SE'])) { + $lang['sv_SE'] = array_merge($lang['en_US'], $lang['sv_SE']); } else { - $lang['sv_SE'] = $lang['en_US']; + $lang['sv_SE'] = $lang['en_US']; } $lang['sv_SE']['MultiForm']['BACK'] = 'Tillbaka'; @@ -24,5 +24,3 @@ $lang['sv_SE']['MultiFormSession']['plural_name'] = '(ingen)'; $lang['sv_SE']['MultiFormStep']['db_Data'] = 'Data'; $lang['sv_SE']['MultiFormStep']['plural_name'] = '(inga)'; $lang['sv_SE']['MultiFormStep']['singular_name'] = '(ingen)'; - -?> \ No newline at end of file diff --git a/lang/tr_TR.php b/lang/tr_TR.php index 9d29af8..e998f87 100644 --- a/lang/tr_TR.php +++ b/lang/tr_TR.php @@ -10,10 +10,10 @@ i18n::include_locale_file('modules: multiform', 'en_US'); global $lang; -if(array_key_exists('tr_TR', $lang) && is_array($lang['tr_TR'])) { - $lang['tr_TR'] = array_merge($lang['en_US'], $lang['tr_TR']); +if (array_key_exists('tr_TR', $lang) && is_array($lang['tr_TR'])) { + $lang['tr_TR'] = array_merge($lang['en_US'], $lang['tr_TR']); } else { - $lang['tr_TR'] = $lang['en_US']; + $lang['tr_TR'] = $lang['en_US']; } $lang['tr_TR']['MultiForm']['BACK'] = 'Geri'; @@ -27,5 +27,3 @@ $lang['tr_TR']['MultiFormSession']['singular_name'] = '(hiçbiri)'; $lang['tr_TR']['MultiFormStep']['db_Data'] = 'Veri'; $lang['tr_TR']['MultiFormStep']['plural_name'] = '(hiçbiri)'; $lang['tr_TR']['MultiFormStep']['singular_name'] = '(hiçbiri)'; - -?> \ No newline at end of file diff --git a/lang/zh_CN.php b/lang/zh_CN.php index 120de77..c350786 100644 --- a/lang/zh_CN.php +++ b/lang/zh_CN.php @@ -10,10 +10,10 @@ i18n::include_locale_file('modules: multiform', 'en_US'); global $lang; -if(array_key_exists('zh_CN', $lang) && is_array($lang['zh_CN'])) { - $lang['zh_CN'] = array_merge($lang['en_US'], $lang['zh_CN']); +if (array_key_exists('zh_CN', $lang) && is_array($lang['zh_CN'])) { + $lang['zh_CN'] = array_merge($lang['en_US'], $lang['zh_CN']); } else { - $lang['zh_CN'] = $lang['en_US']; + $lang['zh_CN'] = $lang['en_US']; } $lang['zh_CN']['MultiForm']['BACK'] = '区域'; @@ -27,5 +27,3 @@ $lang['zh_CN']['MultiFormSession']['singular_name'] = '单名称'; $lang['zh_CN']['MultiFormStep']['db_Data'] = '数据'; $lang['zh_CN']['MultiFormStep']['plural_name'] = '多名称'; $lang['zh_CN']['MultiFormStep']['singular_name'] = '单名称'; - -?> \ No newline at end of file diff --git a/src/extensions/MultiFormObjectDecorator.php b/src/extensions/MultiFormObjectDecorator.php index 3168538..3423a65 100644 --- a/src/extensions/MultiFormObjectDecorator.php +++ b/src/extensions/MultiFormObjectDecorator.php @@ -14,56 +14,58 @@ * * @package multiform */ -class MultiFormObjectDecorator extends DataExtension { +class MultiFormObjectDecorator extends DataExtension +{ - private static $db = array( - 'MultiFormIsTemporary' => 'Boolean', - ); + private static $db = array( + 'MultiFormIsTemporary' => 'Boolean', + ); - private static $has_one = array( - 'MultiFormSession' => 'MultiFormSession', - ); + private static $has_one = array( + 'MultiFormSession' => 'MultiFormSession', + ); - /** - * Augment any queries to MultiFormObjectDecorator and only - * return anything that isn't considered temporary. - */ - public function augmentSQL(SQLQuery &$query) { - $where = $query->getWhere(); - if(!$where && !$this->wantsTemporary($query)) { - $from = array_values($query->getFrom()); - $query->addWhere("{$from[0]}.\"MultiFormIsTemporary\" = '0'"); - return; - } + /** + * Augment any queries to MultiFormObjectDecorator and only + * return anything that isn't considered temporary. + */ + public function augmentSQL(SQLQuery &$query) + { + $where = $query->getWhere(); + if (!$where && !$this->wantsTemporary($query)) { + $from = array_values($query->getFrom()); + $query->addWhere("{$from[0]}.\"MultiFormIsTemporary\" = '0'"); + return; + } - if( - strpos($where[0], ".`ID` = ") === false - && strpos($where[0], ".ID = ") === false - && strpos($where[0], "ID = ") !== 0 - && !$this->wantsTemporary($query) - ) { - $from = array_values($query->getFrom()); - $query->addWhere("{$from[0]}.\"MultiFormIsTemporary\" = '0'"); - } - } + if (strpos($where[0], ".`ID` = ") === false + && strpos($where[0], ".ID = ") === false + && strpos($where[0], "ID = ") !== 0 + && !$this->wantsTemporary($query) + ) { + $from = array_values($query->getFrom()); + $query->addWhere("{$from[0]}.\"MultiFormIsTemporary\" = '0'"); + } + } - /** - * Determines if the current query is supposed - * to be exempt from the automatic filtering out - * of temporary records. - * - * @param SQLQuery $query - * @return boolean - */ - protected function wantsTemporary($query) { - foreach($query->getWhere() as $whereClause) { - $from = array_values($query->getFrom()); - // SQLQuery will automatically add double quotes and single quotes to values, so check against that. - if($whereClause == "{$from[0]}.\"MultiFormIsTemporary\" = '1'") { - return true; - } - } + /** + * Determines if the current query is supposed + * to be exempt from the automatic filtering out + * of temporary records. + * + * @param SQLQuery $query + * @return boolean + */ + protected function wantsTemporary($query) + { + foreach ($query->getWhere() as $whereClause) { + $from = array_values($query->getFrom()); + // SQLQuery will automatically add double quotes and single quotes to values, so check against that. + if ($whereClause == "{$from[0]}.\"MultiFormIsTemporary\" = '1'") { + return true; + } + } - return false; - } + return false; + } } diff --git a/src/model/MultiForm.php b/src/model/MultiForm.php index 8a3978a..547fdd7 100644 --- a/src/model/MultiForm.php +++ b/src/model/MultiForm.php @@ -14,686 +14,721 @@ * * @package multiform */ -abstract class MultiForm extends Form { - - /** - * A session object stored in the database, to identify and store - * data for this MultiForm instance. - * - * @var MultiFormSession - */ - protected $session; - - /** - * The current encrypted MultiFormSession identification. - * - * @var string - */ - protected $currentSessionHash; - - /** - * Defines which subclass of {@link MultiFormStep} should be the first - * step in the multi-step process. - * - * @var string Classname of a {@link MultiFormStep} subclass - */ - public static $start_step; - - /** - * Set the casting for these fields. - * - * @var array - */ - private static $casting = array( - 'CompletedStepCount' => 'Int', - 'TotalStepCount' => 'Int', - 'CompletedPercent' => 'Float' - ); - - /** - * @var string - */ - private static $get_var = 'MultiFormSessionID'; - - /** - * These fields are ignored when saving the raw form data into session. - * This ensures only field data is saved, and nothing else that's useless - * or potentially dangerous. - * - * @var array - */ - public static $ignored_fields = array( - 'url', - 'executeForm', - 'SecurityID' - ); - - /** - * Any of the actions defined in this variable are exempt from - * being validated. - * - * This is most useful for the "Back" (action_prev) action, as - * you typically don't validate the form when the user is going - * back a step. - * - * @var array - */ - public static $actions_exempt_from_validation = array( - 'action_prev' - ); - - /** - * @var string - */ - protected $displayLink; - - /** - * Flag which is being used in getAllStepsRecursive() to allow adding the completed flag on the steps - * - * @var boolean - */ - protected $currentStepHasBeenFound = false; - - /** - * Start the MultiForm instance. - * - * @param Controller instance $controller Controller this form is created on - * @param string $name The form name, typically the same as the method name - */ - public function __construct($controller, $name) { - // First set the controller and name manually so they are available for - // field construction. - $this->controller = $controller; - $this->name = $name; - - // Set up the session for this MultiForm instance - $this->setSession(); - - // Get the current step available (Note: either returns an existing - // step or creates a new one if none available) - $currentStep = $this->getCurrentStep(); - - // Set the step returned above as the current step - $this->setCurrentStep($currentStep); - - // Set the form of the step to this form instance - $currentStep->setForm($this); - - // Set up the fields for the current step - $fields = $currentStep->getFields(); - - // Set up the actions for the current step - $actions = $this->actionsFor($currentStep); - - // Set up validation (if necessary) - $validator = null; - $applyValidation = true; - - $actionNames = static::$actions_exempt_from_validation; - - if($actionNames) { - foreach ($actionNames as $exemptAction) { - if(!empty($_REQUEST[$exemptAction])) { - $applyValidation = false; - break; - } - } - } - - // Apply validation if the current step requires validation (is not exempt) - if($applyValidation) { - if($currentStep->getValidator()) { - $validator = $currentStep->getValidator(); - } - } - - // Give the fields, actions, and validation for the current step back to the parent Form class - parent::__construct($controller, $name, $fields, $actions, $validator); - - $getVar = $this->config()->get_var; - - // Set a hidden field in our form with an encrypted hash to identify this session. - $this->fields->push(new HiddenField($getVar, false, $this->session->Hash)); - - // If there is saved data for the current step, we load it into the form it here - //(CAUTION: loadData() MUST unserialize first!) - if($data = $currentStep->loadData()) { - $this->loadDataFrom($data); - } - - // Disable security token - we tie a form to a session ID instead - $this->disableSecurityToken(); - - self::$ignored_fields[] = $getVar; - } - - /** - * Accessor method to $this->controller. - * - * @return Controller this MultiForm was instanciated on. - */ - public function getController() { - return $this->controller; - } - - /** - * Returns the get_var to the template engine - * - * @return string - */ - public function getGetVar() { - return $this->config()->get_var; - } - - /** - * Get the current step. - * - * If StepID has been set in the URL, we attempt to get that record - * by the ID. Otherwise, we check if there's a current step ID in - * our session record. Failing those cases, we assume that the form has - * just been started, and so we create the first step and return it. - * - * @return MultiFormStep subclass - */ - public function getCurrentStep() { - $startStepClass = static::$start_step; - - // Check if there was a start step defined on the subclass of MultiForm - if(!isset($startStepClass)) user_error( - 'MultiForm::init(): Please define a $start_step on ' . $this->class, - E_USER_ERROR - ); - - // Determine whether we use the current step, or create one if it doesn't exist - $currentStep = null; - $StepID = $this->controller->request->getVar('StepID'); - if(isset($StepID)) { - $currentStep = DataObject::get_one( - 'MultiFormStep', - array( - 'SessionID' => $this->session->ID, - 'ID' => $StepID - ) - ); - } elseif($this->session->CurrentStepID) { - $currentStep = $this->session->CurrentStep(); - } - - // Always fall back to creating a new step (in case the session or request data is invalid) - if(!$currentStep || !$currentStep->ID) { - $currentStep = Object::create($startStepClass); - $currentStep->SessionID = $this->session->ID; - $currentStep->write(); - $this->session->CurrentStepID = $currentStep->ID; - $this->session->write(); - $this->session->flushCache(); - } - - if($currentStep) $currentStep->setForm($this); - - return $currentStep; - } - - /** - * Set the step passed in as the current step. - * - * @param MultiFormStep $step A subclass of MultiFormStep - * @return boolean The return value of write() - */ - protected function setCurrentStep($step) { - $this->session->CurrentStepID = $step->ID; - $step->setForm($this); - - return $this->session->write(); - } - - /** - * Accessor method to $this->session. - * - * @return MultiFormSession - */ - public function getSession() { - return $this->session; - } - - /** - * Set up the session. - * - * If MultiFormSessionID isn't set, we assume that this is a new - * multiform that requires a new session record to be created. - * - * @TODO Fix the fact you can continually refresh and create new records - * if MultiFormSessionID isn't set. - * - * @TODO Not sure if we should bake the session stuff directly into MultiForm. - * Perhaps it would be best dealt with on a separate class? - */ - protected function setSession() { - $this->session = $this->getCurrentSession(); - - // If there was no session found, create a new one instead - if(!$this->session) { - $this->session = new MultiFormSession(); - } - - // 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(); - } - } - - /** - * Set the currently used encrypted hash to identify - * the MultiFormSession. - * - * @param string $hash Encrypted identification to session - */ - public function setCurrentSessionHash($hash) { - $this->currentSessionHash = $hash; - $this->setSession(); - } - - /** - * Return the currently used {@link MultiFormSession} - * @return MultiFormSession|boolean FALSE - */ - public function getCurrentSession() { - if(!$this->currentSessionHash) { - $this->currentSessionHash = $this->controller->request->getVar($this->config()->get_var); - - if(!$this->currentSessionHash) { - return false; - } - } - - $this->session = MultiFormSession::get()->filter(array( - "Hash" => $this->currentSessionHash, - "IsComplete" => 0 - ))->first(); - - return $this->session; - } - - /** - * 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()}. - * - * @param string $filter SQL WHERE statement - * @return DataObjectSet|boolean A set of MultiFormStep subclasses - */ - public function getSavedSteps($filter = null) { - $filter .= ($filter) ? ' AND ' : ''; - $filter .= sprintf("\"SessionID\" = '%s'", $this->session->ID); - return DataObject::get('MultiFormStep', $filter); - } - - /** - * 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 - */ - public function getSavedStepByClass($className) { - return DataObject::get_one( - 'MultiFormStep', - sprintf("\"SessionID\" = '%s' AND \"ClassName\" = '%s'", - $this->session->ID, - Convert::raw2sql($className) - ) - ); - } - - /** - * Build a FieldList of the FormAction fields for the given step. - * - * If the current step is the final step, we push in a submit button, which - * calls the action {@link finish()} to finalise the submission. Otherwise, - * we push in a next button which calls the action {@link next()} to determine - * where to go next in our step process, and save any form data collected. - * - * If there's a previous step (a step that has the current step as it's next - * step class), then we allow a previous button, which calls the previous action - * to determine which step to go back to. - * - * If there are any extra actions defined in MultiFormStep->getExtraActions() - * then that set of actions is appended to the end of the actions FieldSet we - * have created in this method. - * - * @param $currentStep Subclass of MultiFormStep - * @return FieldList of FormAction objects - */ - public function actionsFor($step) { - // Create default multi step actions (next, prev), and merge with extra actions, if any - $actions = (class_exists('FieldList')) ? new FieldList() : new FieldSet(); - - // If the form is at final step, create a submit button to perform final actions - // The last step doesn't have a next button, so add that action to any step that isn't the final one - if($step->isFinalStep()) { - $actions->push(new FormAction('finish', $step->getSubmitText())); - } else { - $actions->push(new FormAction('next', $step->getNextText())); - } - - // 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 - if($step->getNextStep()) { - $actions->insertBefore($prev = new FormAction('prev', $step->getPrevText()), 'action_next'); - // Assume that this is the last step, insert the action before the finish action - } else { - $actions->insertBefore($prev = new FormAction('prev', $step->getPrevText()), 'action_finish'); - } - //remove browser validation from prev action - $prev->setAttribute("formnovalidate", "formnovalidate"); - } - - // Merge any extra action fields defined on the step - $actions->merge($step->getExtraActions()); - - return $actions; - } - - /** - * Return a rendered version of this form, with a specific template. - * Looks through the step ancestory templates (MultiFormStep, current step - * subclass template) to see if one is available to render the form with. If - * any of those don't exist, look for a default Form template to render - * with instead. - * - * @return SSViewer object to render the template with - */ - public function forTemplate() { - $return = $this->renderWith(array( - $this->getCurrentStep()->class, - 'MultiFormStep', - $this->class, - 'MultiForm', - 'Form' - )); - - $this->clearMessage(); - - return $return; - } - - /** - * This method saves the data on the final step, after submitting. - * It should always be overloaded with parent::finish($data, $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 object $form The form that the action was called on - */ - public function finish($data, $form) { - // Save the form data for the current step - $this->save($data); - - if(!$this->getCurrentStep()->isFinalStep()) { - $this->controller->redirectBack(); - return false; - } - - if(!$this->getCurrentStep()->validateStep($data, $form)) { - Session::set("FormInfo.{$form->FormName()}.data", $form->getData()); - $this->controller->redirectBack(); - return false; - } - } - - /** - * Determine what to do when the next action is called. - * - * 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 - * @param object $form The form that the action was called on - */ - public function next($data, $form) { - // Save the form data for the current step - $this->save($form->getData()); - - // Get the next step class - $nextStepClass = $this->getCurrentStep()->getNextStep(); - - if(!$nextStepClass) { - $this->controller->redirectBack(); - return false; - } - - // Perform custom step validation (use MultiFormStep->getValidator() for - // built-in functionality). The data needs to be manually saved on error - // so the form is re-populated. - if(!$this->getCurrentStep()->validateStep($data, $form)) { - Session::set("FormInfo.{$form->FormName()}.data", $form->getData()); - $this->controller->redirectBack(); - return false; - } - - // validation succeeded so we reset it to remove errors and messages - $this->resetValidation(); - - // 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 = Object::create($nextStepClass); - $nextStep->SessionID = $this->session->ID; - $nextStep->write(); - } - - // Set the next step found as the current step - $this->setCurrentStep($nextStep); - - // Redirect to the next step - $this->controller->redirect($nextStep->Link()); - } - - /** - * Determine what to do when the previous action is called. - * - * 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 - * @param object $form The form that the action was called on - */ - public function prev($data, $form) { - // Save the form data for the current step - $this->save($form->getData()); - - // Get the previous step class - $prevStepClass = $this->getCurrentStep()->getPreviousStep(); - - if(!$prevStepClass && !$this->getCurrentStep()->canGoBack()) { - $this->controller->redirectBack(); - return false; - } - - // Get the previous step of the class instance returned from $currentStep->getPreviousStep() - $prevStep = DataObject::get_one($prevStepClass, "\"SessionID\" = {$this->session->ID}"); - - // Set the current step as the previous step - $this->setCurrentStep($prevStep); - - // Redirect to the previous step - $this->controller->redirect($prevStep->Link()); - } - - /** - * Save the raw data given back from the form into session. - * - * Take the submitted form data for the current step, removing - * any key => value pairs that shouldn't be saved, then saves - * the data into the session. - * - * @param array $data An array of data to save - */ - protected function save($data) { - $currentStep = $this->getCurrentStep(); - if(is_array($data)) { - foreach($data as $field => $value) { - if(in_array($field, static::$ignored_fields)) { - unset($data[$field]); - } - } - $currentStep->saveData($data); - } - return; - } - - // ############ Misc ############ - - /** - * 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. - * - * @return string - */ - public function FormAction() { - $action = parent::FormAction(); - $action .= (strpos($action, '?')) ? '&' : '?'; - $action .= "{$this->config()->get_var}={$this->session->Hash}"; - - return $action; - } - - /** - * Returns the link to the page where the form is displayed. The user is - * redirected to this link with a session param after each step is - * submitted. - * - * @return string - */ - public function getDisplayLink() { - return $this->displayLink ? $this->displayLink : Controller::curr()->Link(); - } - - /** - * Set the link to the page on which the form is displayed. - * - * The link defaults to the controllers current link. However if the form - * is displayed inside an action the display link must be explicitly set. - * - * @param string $link - */ - public function setDisplayLink($link) { - $this->displayLink = $link; - } - - /** - * Determine the steps to show in a linear fashion, starting from the - * first step. We run {@link getAllStepsRecursive} passing the steps found - * by reference to get a listing of the steps. - * - * @return DataObjectSet of MultiFormStep instances - */ - public function getAllStepsLinear() { - $stepsFound = (class_exists('ArrayList')) ? new ArrayList() : new DataObjectSet(); - - $firstStep = DataObject::get_one(static::$start_step, "\"SessionID\" = {$this->session->ID}"); - $firstStep->LinkingMode = ($firstStep->ID == $this->getCurrentStep()->ID) ? 'current' : 'link'; - $firstStep->setForm($this); - $stepsFound->push($firstStep); - - // mark the further steps as non-completed if the first step is the current - if ($firstStep->ID == $this->getCurrentStep()->ID) { - $this->currentStepHasBeenFound = true; - } else { - $firstStep->addExtraClass('completed'); - } - - $this->getAllStepsRecursive($firstStep, $stepsFound); - - return $stepsFound; - } - - /** - * 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 - * @return DataObjectSet of MultiFormStep instances - */ - protected function getAllStepsRecursive($step, &$stepsFound) { - // Find the next step to the current step, the final step has no next step - if(!$step->isFinalStep()) { - if($step->getNextStep()) { - // Is this step in the DB? If it is, we use that - $nextStep = $step->getNextStepFromDatabase(); - if(!$nextStep) { - // If it's not in the DB, we use a singleton instance of it instead - - // - this step hasn't been accessed yet - $nextStep = singleton($step->getNextStep()); - } - - // once the current steps has been found we won't add the completed class anymore. - if ($nextStep->ID == $this->getCurrentStep()->ID) $this->currentStepHasBeenFound = true; - - $nextStep->LinkingMode = ($nextStep->ID == $this->getCurrentStep()->ID) ? 'current' : 'link'; - - // add the completed class - if (!$this->currentStepHasBeenFound) $nextStep->addExtraClass('completed'); - - $nextStep->setForm($this); - - // Add the array data, and do a callback - $stepsFound->push($nextStep); - $this->getAllStepsRecursive($nextStep, $stepsFound); - } - // Once we've reached the final step, we just return what we've collected - } else { - return $stepsFound; - } - } - - /** - * Number of steps already completed (excluding currently started step). - * The way we determine a step is complete is to check if it has the Data - * field filled out with a serialized value, then we know that the user has - * clicked next on the given step, to proceed. - * - * @TODO Not sure if it's entirely appropriate to check if Data is set as a - * way to determine a step is "completed". - * - * @return int - */ - public function getCompletedStepCount() { - $steps = DataObject::get('MultiFormStep', "\"SessionID\" = {$this->session->ID} && \"Data\" IS NOT NULL"); - - return $steps ? $steps->Count() : 0; - } - - /** - * Total number of steps in the shortest path (only counting straight path without any branching) - * The way we determine this is to check if each step has a next_step string variable set. If it's - * anything else (like an array, for defining multiple branches) then it gets counted as a single step. - * - * @return int - */ - public function getTotalStepCount() { - return $this->getAllStepsLinear() ? $this->getAllStepsLinear()->Count() : 0; - } - - /** - * Percentage of steps completed (excluding currently started step) - * - * @return float - */ - public function getCompletedPercent() { - return (float) $this->getCompletedStepCount() * 100 / $this->getTotalStepCount(); - } +abstract class MultiForm extends Form +{ + + /** + * A session object stored in the database, to identify and store + * data for this MultiForm instance. + * + * @var MultiFormSession + */ + protected $session; + + /** + * The current encrypted MultiFormSession identification. + * + * @var string + */ + protected $currentSessionHash; + + /** + * Defines which subclass of {@link MultiFormStep} should be the first + * step in the multi-step process. + * + * @var string Classname of a {@link MultiFormStep} subclass + */ + public static $start_step; + + /** + * Set the casting for these fields. + * + * @var array + */ + private static $casting = array( + 'CompletedStepCount' => 'Int', + 'TotalStepCount' => 'Int', + 'CompletedPercent' => 'Float' + ); + + /** + * @var string + */ + private static $get_var = 'MultiFormSessionID'; + + /** + * These fields are ignored when saving the raw form data into session. + * This ensures only field data is saved, and nothing else that's useless + * or potentially dangerous. + * + * @var array + */ + public static $ignored_fields = array( + 'url', + 'executeForm', + 'SecurityID' + ); + + /** + * Any of the actions defined in this variable are exempt from + * being validated. + * + * This is most useful for the "Back" (action_prev) action, as + * you typically don't validate the form when the user is going + * back a step. + * + * @var array + */ + public static $actions_exempt_from_validation = array( + 'action_prev' + ); + + /** + * @var string + */ + protected $displayLink; + + /** + * Flag which is being used in getAllStepsRecursive() to allow adding the completed flag on the steps + * + * @var boolean + */ + protected $currentStepHasBeenFound = false; + + /** + * Start the MultiForm instance. + * + * @param Controller instance $controller Controller this form is created on + * @param string $name The form name, typically the same as the method name + */ + public function __construct($controller, $name) + { + // First set the controller and name manually so they are available for + // field construction. + $this->controller = $controller; + $this->name = $name; + + // Set up the session for this MultiForm instance + $this->setSession(); + + // Get the current step available (Note: either returns an existing + // step or creates a new one if none available) + $currentStep = $this->getCurrentStep(); + + // Set the step returned above as the current step + $this->setCurrentStep($currentStep); + + // Set the form of the step to this form instance + $currentStep->setForm($this); + + // Set up the fields for the current step + $fields = $currentStep->getFields(); + + // Set up the actions for the current step + $actions = $this->actionsFor($currentStep); + + // Set up validation (if necessary) + $validator = null; + $applyValidation = true; + + $actionNames = static::$actions_exempt_from_validation; + + if ($actionNames) { + foreach ($actionNames as $exemptAction) { + if (!empty($_REQUEST[$exemptAction])) { + $applyValidation = false; + break; + } + } + } + + // Apply validation if the current step requires validation (is not exempt) + if ($applyValidation) { + if ($currentStep->getValidator()) { + $validator = $currentStep->getValidator(); + } + } + + // Give the fields, actions, and validation for the current step back to the parent Form class + parent::__construct($controller, $name, $fields, $actions, $validator); + + $getVar = $this->config()->get_var; + + // Set a hidden field in our form with an encrypted hash to identify this session. + $this->fields->push(new HiddenField($getVar, false, $this->session->Hash)); + + // If there is saved data for the current step, we load it into the form it here + //(CAUTION: loadData() MUST unserialize first!) + if ($data = $currentStep->loadData()) { + $this->loadDataFrom($data); + } + + // Disable security token - we tie a form to a session ID instead + $this->disableSecurityToken(); + + self::$ignored_fields[] = $getVar; + } + + /** + * Accessor method to $this->controller. + * + * @return Controller this MultiForm was instanciated on. + */ + public function getController() + { + return $this->controller; + } + + /** + * Returns the get_var to the template engine + * + * @return string + */ + public function getGetVar() + { + return $this->config()->get_var; + } + + /** + * Get the current step. + * + * If StepID has been set in the URL, we attempt to get that record + * by the ID. Otherwise, we check if there's a current step ID in + * our session record. Failing those cases, we assume that the form has + * just been started, and so we create the first step and return it. + * + * @return MultiFormStep subclass + */ + public function getCurrentStep() + { + $startStepClass = static::$start_step; + + // Check if there was a start step defined on the subclass of MultiForm + if (!isset($startStepClass)) { + user_error( + 'MultiForm::init(): Please define a $start_step on ' . $this->class, + E_USER_ERROR + ); + } + + // Determine whether we use the current step, or create one if it doesn't exist + $currentStep = null; + $StepID = $this->controller->request->getVar('StepID'); + if (isset($StepID)) { + $currentStep = DataObject::get_one( + 'MultiFormStep', + array( + 'SessionID' => $this->session->ID, + 'ID' => $StepID + ) + ); + } elseif ($this->session->CurrentStepID) { + $currentStep = $this->session->CurrentStep(); + } + + // Always fall back to creating a new step (in case the session or request data is invalid) + if (!$currentStep || !$currentStep->ID) { + $currentStep = Object::create($startStepClass); + $currentStep->SessionID = $this->session->ID; + $currentStep->write(); + $this->session->CurrentStepID = $currentStep->ID; + $this->session->write(); + $this->session->flushCache(); + } + + if ($currentStep) { + $currentStep->setForm($this); + } + + return $currentStep; + } + + /** + * Set the step passed in as the current step. + * + * @param MultiFormStep $step A subclass of MultiFormStep + * @return boolean The return value of write() + */ + protected function setCurrentStep($step) + { + $this->session->CurrentStepID = $step->ID; + $step->setForm($this); + + return $this->session->write(); + } + + /** + * Accessor method to $this->session. + * + * @return MultiFormSession + */ + public function getSession() + { + return $this->session; + } + + /** + * Set up the session. + * + * If MultiFormSessionID isn't set, we assume that this is a new + * multiform that requires a new session record to be created. + * + * @TODO Fix the fact you can continually refresh and create new records + * if MultiFormSessionID isn't set. + * + * @TODO Not sure if we should bake the session stuff directly into MultiForm. + * Perhaps it would be best dealt with on a separate class? + */ + protected function setSession() + { + $this->session = $this->getCurrentSession(); + + // If there was no session found, create a new one instead + if (!$this->session) { + $this->session = new MultiFormSession(); + } + + // 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(); + } + } + + /** + * Set the currently used encrypted hash to identify + * the MultiFormSession. + * + * @param string $hash Encrypted identification to session + */ + public function setCurrentSessionHash($hash) + { + $this->currentSessionHash = $hash; + $this->setSession(); + } + + /** + * Return the currently used {@link MultiFormSession} + * @return MultiFormSession|boolean FALSE + */ + public function getCurrentSession() + { + if (!$this->currentSessionHash) { + $this->currentSessionHash = $this->controller->request->getVar($this->config()->get_var); + + if (!$this->currentSessionHash) { + return false; + } + } + + $this->session = MultiFormSession::get()->filter(array( + "Hash" => $this->currentSessionHash, + "IsComplete" => 0 + ))->first(); + + return $this->session; + } + + /** + * 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()}. + * + * @param string $filter SQL WHERE statement + * @return DataObjectSet|boolean A set of MultiFormStep subclasses + */ + public function getSavedSteps($filter = null) + { + $filter .= ($filter) ? ' AND ' : ''; + $filter .= sprintf("\"SessionID\" = '%s'", $this->session->ID); + return DataObject::get('MultiFormStep', $filter); + } + + /** + * 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 + */ + public function getSavedStepByClass($className) + { + return DataObject::get_one( + 'MultiFormStep', + sprintf( + "\"SessionID\" = '%s' AND \"ClassName\" = '%s'", + $this->session->ID, + Convert::raw2sql($className) + ) + ); + } + + /** + * Build a FieldList of the FormAction fields for the given step. + * + * If the current step is the final step, we push in a submit button, which + * calls the action {@link finish()} to finalise the submission. Otherwise, + * we push in a next button which calls the action {@link next()} to determine + * where to go next in our step process, and save any form data collected. + * + * If there's a previous step (a step that has the current step as it's next + * step class), then we allow a previous button, which calls the previous action + * to determine which step to go back to. + * + * If there are any extra actions defined in MultiFormStep->getExtraActions() + * then that set of actions is appended to the end of the actions FieldSet we + * have created in this method. + * + * @param $currentStep Subclass of MultiFormStep + * @return FieldList of FormAction objects + */ + public function actionsFor($step) + { + // Create default multi step actions (next, prev), and merge with extra actions, if any + $actions = (class_exists('FieldList')) ? new FieldList() : new FieldSet(); + + // If the form is at final step, create a submit button to perform final actions + // The last step doesn't have a next button, so add that action to any step that isn't the final one + if ($step->isFinalStep()) { + $actions->push(new FormAction('finish', $step->getSubmitText())); + } else { + $actions->push(new FormAction('next', $step->getNextText())); + } + + // 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 + if ($step->getNextStep()) { + $actions->insertBefore($prev = new FormAction('prev', $step->getPrevText()), 'action_next'); + // Assume that this is the last step, insert the action before the finish action + } else { + $actions->insertBefore($prev = new FormAction('prev', $step->getPrevText()), 'action_finish'); + } + //remove browser validation from prev action + $prev->setAttribute("formnovalidate", "formnovalidate"); + } + + // Merge any extra action fields defined on the step + $actions->merge($step->getExtraActions()); + + return $actions; + } + + /** + * Return a rendered version of this form, with a specific template. + * Looks through the step ancestory templates (MultiFormStep, current step + * subclass template) to see if one is available to render the form with. If + * any of those don't exist, look for a default Form template to render + * with instead. + * + * @return SSViewer object to render the template with + */ + public function forTemplate() + { + $return = $this->renderWith(array( + $this->getCurrentStep()->class, + 'MultiFormStep', + $this->class, + 'MultiForm', + 'Form' + )); + + $this->clearMessage(); + + return $return; + } + + /** + * This method saves the data on the final step, after submitting. + * It should always be overloaded with parent::finish($data, $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 object $form The form that the action was called on + */ + public function finish($data, $form) + { + // Save the form data for the current step + $this->save($data); + + if (!$this->getCurrentStep()->isFinalStep()) { + $this->controller->redirectBack(); + return false; + } + + if (!$this->getCurrentStep()->validateStep($data, $form)) { + Session::set("FormInfo.{$form->FormName()}.data", $form->getData()); + $this->controller->redirectBack(); + return false; + } + } + + /** + * Determine what to do when the next action is called. + * + * 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 + * @param object $form The form that the action was called on + */ + public function next($data, $form) + { + // Save the form data for the current step + $this->save($form->getData()); + + // Get the next step class + $nextStepClass = $this->getCurrentStep()->getNextStep(); + + if (!$nextStepClass) { + $this->controller->redirectBack(); + return false; + } + + // Perform custom step validation (use MultiFormStep->getValidator() for + // built-in functionality). The data needs to be manually saved on error + // so the form is re-populated. + if (!$this->getCurrentStep()->validateStep($data, $form)) { + Session::set("FormInfo.{$form->FormName()}.data", $form->getData()); + $this->controller->redirectBack(); + return false; + } + + // validation succeeded so we reset it to remove errors and messages + $this->resetValidation(); + + // 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 = Object::create($nextStepClass); + $nextStep->SessionID = $this->session->ID; + $nextStep->write(); + } + + // Set the next step found as the current step + $this->setCurrentStep($nextStep); + + // Redirect to the next step + $this->controller->redirect($nextStep->Link()); + } + + /** + * Determine what to do when the previous action is called. + * + * 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 + * @param object $form The form that the action was called on + */ + public function prev($data, $form) + { + // Save the form data for the current step + $this->save($form->getData()); + + // Get the previous step class + $prevStepClass = $this->getCurrentStep()->getPreviousStep(); + + if (!$prevStepClass && !$this->getCurrentStep()->canGoBack()) { + $this->controller->redirectBack(); + return false; + } + + // Get the previous step of the class instance returned from $currentStep->getPreviousStep() + $prevStep = DataObject::get_one($prevStepClass, "\"SessionID\" = {$this->session->ID}"); + + // Set the current step as the previous step + $this->setCurrentStep($prevStep); + + // Redirect to the previous step + $this->controller->redirect($prevStep->Link()); + } + + /** + * Save the raw data given back from the form into session. + * + * Take the submitted form data for the current step, removing + * any key => value pairs that shouldn't be saved, then saves + * the data into the session. + * + * @param array $data An array of data to save + */ + protected function save($data) + { + $currentStep = $this->getCurrentStep(); + if (is_array($data)) { + foreach ($data as $field => $value) { + if (in_array($field, static::$ignored_fields)) { + unset($data[$field]); + } + } + $currentStep->saveData($data); + } + return; + } + + // ############ Misc ############ + + /** + * 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. + * + * @return string + */ + public function FormAction() + { + $action = parent::FormAction(); + $action .= (strpos($action, '?')) ? '&' : '?'; + $action .= "{$this->config()->get_var}={$this->session->Hash}"; + + return $action; + } + + /** + * Returns the link to the page where the form is displayed. The user is + * redirected to this link with a session param after each step is + * submitted. + * + * @return string + */ + public function getDisplayLink() + { + return $this->displayLink ? $this->displayLink : Controller::curr()->Link(); + } + + /** + * Set the link to the page on which the form is displayed. + * + * The link defaults to the controllers current link. However if the form + * is displayed inside an action the display link must be explicitly set. + * + * @param string $link + */ + public function setDisplayLink($link) + { + $this->displayLink = $link; + } + + /** + * Determine the steps to show in a linear fashion, starting from the + * first step. We run {@link getAllStepsRecursive} passing the steps found + * by reference to get a listing of the steps. + * + * @return DataObjectSet of MultiFormStep instances + */ + public function getAllStepsLinear() + { + $stepsFound = (class_exists('ArrayList')) ? new ArrayList() : new DataObjectSet(); + + $firstStep = DataObject::get_one(static::$start_step, "\"SessionID\" = {$this->session->ID}"); + $firstStep->LinkingMode = ($firstStep->ID == $this->getCurrentStep()->ID) ? 'current' : 'link'; + $firstStep->setForm($this); + $stepsFound->push($firstStep); + + // mark the further steps as non-completed if the first step is the current + if ($firstStep->ID == $this->getCurrentStep()->ID) { + $this->currentStepHasBeenFound = true; + } else { + $firstStep->addExtraClass('completed'); + } + + $this->getAllStepsRecursive($firstStep, $stepsFound); + + return $stepsFound; + } + + /** + * 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 + * @return DataObjectSet of MultiFormStep instances + */ + protected function getAllStepsRecursive($step, &$stepsFound) + { + // Find the next step to the current step, the final step has no next step + if (!$step->isFinalStep()) { + if ($step->getNextStep()) { + // Is this step in the DB? If it is, we use that + $nextStep = $step->getNextStepFromDatabase(); + if (!$nextStep) { + // If it's not in the DB, we use a singleton instance of it instead - + // - this step hasn't been accessed yet + $nextStep = singleton($step->getNextStep()); + } + + // once the current steps has been found we won't add the completed class anymore. + if ($nextStep->ID == $this->getCurrentStep()->ID) { + $this->currentStepHasBeenFound = true; + } + + $nextStep->LinkingMode = ($nextStep->ID == $this->getCurrentStep()->ID) ? 'current' : 'link'; + + // add the completed class + if (!$this->currentStepHasBeenFound) { + $nextStep->addExtraClass('completed'); + } + + $nextStep->setForm($this); + + // Add the array data, and do a callback + $stepsFound->push($nextStep); + $this->getAllStepsRecursive($nextStep, $stepsFound); + } + // Once we've reached the final step, we just return what we've collected + } else { + return $stepsFound; + } + } + + /** + * Number of steps already completed (excluding currently started step). + * The way we determine a step is complete is to check if it has the Data + * field filled out with a serialized value, then we know that the user has + * clicked next on the given step, to proceed. + * + * @TODO Not sure if it's entirely appropriate to check if Data is set as a + * way to determine a step is "completed". + * + * @return int + */ + public function getCompletedStepCount() + { + $steps = DataObject::get('MultiFormStep', "\"SessionID\" = {$this->session->ID} && \"Data\" IS NOT NULL"); + + return $steps ? $steps->Count() : 0; + } + + /** + * Total number of steps in the shortest path (only counting straight path without any branching) + * The way we determine this is to check if each step has a next_step string variable set. If it's + * anything else (like an array, for defining multiple branches) then it gets counted as a single step. + * + * @return int + */ + public function getTotalStepCount() + { + return $this->getAllStepsLinear() ? $this->getAllStepsLinear()->Count() : 0; + } + + /** + * Percentage of steps completed (excluding currently started step) + * + * @return float + */ + public function getCompletedPercent() + { + return (float) $this->getCompletedStepCount() * 100 / $this->getTotalStepCount(); + } } diff --git a/src/model/MultiFormSession.php b/src/model/MultiFormSession.php index 3858848..dbd3cf7 100644 --- a/src/model/MultiFormSession.php +++ b/src/model/MultiFormSession.php @@ -10,59 +10,66 @@ * * @package multiform */ -class MultiFormSession extends DataObject { +class MultiFormSession extends DataObject +{ - private static $db = array( - 'Hash' => 'Varchar(40)', // cryptographic hash identification to this session - 'IsComplete' => 'Boolean' // flag to determine if this session is marked completed - ); + private static $db = array( + 'Hash' => 'Varchar(40)', // cryptographic hash identification to this session + 'IsComplete' => 'Boolean' // flag to determine if this session is marked completed + ); - private static $has_one = array( - 'Submitter' => 'Member', - 'CurrentStep' => 'MultiFormStep' - ); + private static $has_one = array( + 'Submitter' => 'Member', + 'CurrentStep' => 'MultiFormStep' + ); - private static $has_many = array( - 'FormSteps' => 'MultiFormStep' - ); + private static $has_many = array( + 'FormSteps' => 'MultiFormStep' + ); - /** - * Mark this session as completed. - * - * This sets the flag "IsComplete" to true, - * and writes the session back. - */ - public function markCompleted() { - $this->IsComplete = 1; - $this->write(); - } + /** + * Mark this session as completed. + * + * This sets the flag "IsComplete" to true, + * and writes the session back. + */ + public function markCompleted() + { + $this->IsComplete = 1; + $this->write(); + } - /** - * These actions are performed when write() is called on this object. - */ - public function onBeforeWrite() { - // save submitter if a Member is logged in - $currentMember = Member::currentUser(); - if(!$this->SubmitterID && $currentMember) $this->SubmitterID = $currentMember->ID; + /** + * These actions are performed when write() is called on this object. + */ + public function onBeforeWrite() + { + // save submitter if a Member is logged in + $currentMember = Member::currentUser(); + if (!$this->SubmitterID && $currentMember) { + $this->SubmitterID = $currentMember->ID; + } - parent::onBeforeWrite(); - } + parent::onBeforeWrite(); + } - /** - * These actions are performed when delete() is called on this object. - */ - public function onBeforeDelete() { - // delete dependent form steps and relation - $steps = $this->FormSteps(); - if($steps) foreach($steps as $step) { - if($step && $step->exists()) { - $steps->remove($step); - $step->delete(); - $step->destroy(); - } - } - - parent::onBeforeDelete(); - } + /** + * These actions are performed when delete() is called on this object. + */ + public function onBeforeDelete() + { + // delete dependent form steps and relation + $steps = $this->FormSteps(); + if ($steps) { + foreach ($steps as $step) { + if ($step && $step->exists()) { + $steps->remove($step); + $step->delete(); + $step->destroy(); + } + } + } + parent::onBeforeDelete(); + } } diff --git a/src/model/MultiFormStep.php b/src/model/MultiFormStep.php index 49ebe6d..f08adf7 100644 --- a/src/model/MultiFormStep.php +++ b/src/model/MultiFormStep.php @@ -9,457 +9,489 @@ * * @package multiform */ -class MultiFormStep extends DataObject { +class MultiFormStep extends DataObject +{ - private static $db = array( - 'Data' => 'Text' // stores serialized maps with all session information - ); + private static $db = array( + 'Data' => 'Text' // stores serialized maps with all session information + ); - private static $has_one = array( - 'Session' => 'MultiFormSession' - ); + private static $has_one = array( + 'Session' => 'MultiFormSession' + ); - /** - * Centerpiece of the flow control for the form. - * - * If set to a string, you have a linear form flow - * If set to an array, you should use {@link getNextStep()} - * to enact flow control and branching to different form - * steps, most likely based on previously set session data - * (e.g. a checkbox field or a dropdown). - * - * @var array|string - */ - public static $next_steps; - - /** - * Each {@link MultiForm} subclass needs at least - * one step which is marked as the "final" one - * and triggers the {@link MultiForm->finish()} - * method that wraps up the whole submission. - * - * @var boolean - */ - public static $is_final_step = false; - - /** - * This variable determines whether a user can use - * the "back" action from this step. - * - * @TODO This does not check if the arbitrarily chosen step - * using the step indicator is actually a previous step, so - * unless you remove the link from the indicator template, or - * type in StepID=23 to the address bar you can still go back - * using the step indicator. - * - * @var boolean - */ - protected static $can_go_back = true; - - /** - * Title of this step. - * - * Used for the step indicator templates. - * - * @var string - */ - protected $title; - - /** - * Form class that this step is directly related to. - * - * @var MultiForm subclass - */ - protected $form; - - /** - * List of additional CSS classes for this step - * - * @var array $extraClasses - */ - protected $extraClasses = array(); - - /** - * Temporary cache to increase the performance for repeated look ups. - * - * @var array $cache - */ - protected $step_data_cache = array(); - - /** - * Form fields to be rendered with this step. - * (Form object is created in {@link MultiForm}. - * - * This function needs to be implemented on your - * subclasses of MultiFormStep. - * - * @return FieldList - */ - public function getFields() { - user_error('Please implement getFields on your MultiFormStep subclass', E_USER_ERROR); - } - - /** - * Additional form actions to be added to this step. - * (Form object is created in {@link MultiForm}. - * - * Note: This is optional, and is to be implemented - * on your subclasses of MultiFormStep. - * - * @return FieldList - */ - public function getExtraActions() { - return (class_exists('FieldList')) ? new FieldList() : new FieldSet(); - } - - /** - * Get a validator specific to this form. - * The form is automatically validated in {@link Form->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 ? $this->title : $this->class; - } - - /** - * Gets a direct link to this step (only works - * if you're allowed to skip steps, or this step - * has already been saved to the database - * for the current {@link MultiFormSession}). - * - * @return string Relative URL to this step - */ - public function Link() { - $form = $this->form; - return Controller::join_links($form->getDisplayLink(), "?{$form->config()->get_var}={$this->Session()->Hash}"); - } - - /** - * Unserialize stored session data and return it. - * This is used for loading data previously saved - * in session back into the form. - * - * You need to overload this method onto your own - * step if you require custom loading. An example - * would be selective loading specific fields, leaving - * others that are not required. - * - * @return array - */ - public function loadData() { - return ($this->Data && is_string($this->Data)) ? unserialize($this->Data) : array(); - } - - /** - * Save the data for this step into session, serializing it first. - * - * To selectively save fields, instead of it all, this - * method would need to be overloaded on your step class. - * - * @param array $data The processed data from save() on {@link MultiForm} - */ - public function saveData($data) { - $this->Data = serialize($data); - $this->write(); - } - - /** - * Save the data on this step into an object, - * similiar to {@link Form->saveInto()} - by building - * a stub form from {@link getFields()}. This is necessary - * to trigger each {@link FormField->saveInto()} method - * individually, rather than assuming that all data - * serialized through {@link saveData()} can be saved - * as a simple value outside of the original FormField context. - * - * @param DataObject $obj - */ - public function saveInto($obj) { - $form = new Form( - Controller::curr(), - 'Form', - $this->getFields(), - ((class_exists('FieldList')) ? new FieldList() : new FieldSet()) - ); - $form->loadDataFrom($this->loadData()); - $form->saveInto($obj); - return $obj; - } - - /** - * 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()}. - * - * @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 - * - * @return string Classname of a {@link MultiFormStep} subclass - */ - public function getNextStep() { - $nextSteps = static::$next_steps; - - // Check if next_steps have been implemented properly if not the final step - if(!$this->isFinalStep()) { - if(!isset($nextSteps)) user_error('MultiFormStep->getNextStep(): Please define at least one $next_steps on ' . $this->class, E_USER_ERROR); - } - - if(is_string($nextSteps)) { - return $nextSteps; - } elseif(is_array($nextSteps) && count($nextSteps)) { - // custom flow control goes here - return $nextSteps[0]; - } else { - return false; - } - } - - /** - * Returns the next step to the current step in the database. - * - * This will only return something if you've previously visited - * the step ahead of the current step, and then gone back a step. - * - * @return MultiFormStep|boolean - */ - public function getNextStepFromDatabase() { - if($this->SessionID && is_numeric($this->SessionID)) { - $nextSteps = static::$next_steps; - - if(is_string($nextSteps)) { - return DataObject::get_one($nextSteps, "\"SessionID\" = {$this->SessionID}"); - } elseif(is_array($nextSteps)) { - return DataObject::get_one($nextSteps[0], "\"SessionID\" = {$this->SessionID}"); - } else { - return false; - } - } - } - - /** - * Accessor method for self::$next_steps - * - * @return string|array - */ - public function getNextSteps() { - return static::$next_steps; - } - - /** - * Returns the previous step, if there is one. - * - * To determine if there is a previous step, we check the database to see if there's - * a previous step for this multi form session ID. - * - * @return string Classname of a {@link MultiFormStep} subclass - */ - public function getPreviousStep() { - $steps = DataObject::get('MultiFormStep', "\"SessionID\" = {$this->SessionID}", '"LastEdited" DESC'); - if($steps) { - foreach($steps as $step) { - $step->setForm($this->form); - - if($step->getNextStep()) { - if($step->getNextStep() == $this->class) { - return $step->class; - } - } - } - } - } - - /** - * Retrieves the previous step class record from the database. - * - * This will only return a record if you've previously been on the step. - * - * @return MultiFormStep subclass - */ - public function getPreviousStepFromDatabase() { - if($prevStepClass = $this->getPreviousStep()) { - return DataObject::get_one($prevStepClass, "\"SessionID\" = {$this->SessionID}"); - } - } - - /** - * Get the text to the use on the button to the previous step. - * @return string - */ - public function getPrevText() { - return _t('MultiForm.BACK', 'Back'); - } - - /** - * Get the text to use on the button to the next step. - * @return string - */ - public function getNextText() { - return _t('MultiForm.NEXT', 'Next'); - } - - /** - * Get the text to use on the button to submit the form. - * @return string - */ - public function getSubmitText() { - return _t('MultiForm.SUBMIT', 'Submit'); - } - - /** - * Sets the form that this step is directly related to. - * - * @param MultiForm subclass $form - */ - public function setForm($form) { - $this->form = $form; - } - - /** - * @return Form - */ - public function getForm() { - return $this->form; - } - - // ##################### Utility #################### - - /** - * Determines whether the user is able to go back using the "action_back" - * Determines whether the user is able to go back using the "action_back" - * Determines whether the user is able to go back using the "action_back" - * form action, based on the boolean value of $can_go_back. - * - * @return boolean - */ - public function canGoBack() { - return static::$can_go_back; - } - - /** - * Determines whether this step is the final step in the multi-step process or not, - * based on the variable $is_final_step - which must be defined on at least one step. - * - * @return boolean - */ - public function isFinalStep() { - return static::$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; - } - - /** - * Add a CSS-class to the step. If needed, multiple classes can be added by delimiting a string with spaces. - * - * @param string $class A string containing a classname or several class names delimited by a space. - * @return MultiFormStep - */ - public function addExtraClass($class) { - // split at white space - $classes = preg_split('/\s+/', $class); - foreach($classes as $class) { - // add classes one by one - $this->extraClasses[$class] = $class; - } - return $this; - } - - /** - * Remove a CSS-class from the step. Multiple classes names can be passed through as a space delimited string. - * - * @param string $class - * @return MultiFormStep - */ - public function removeExtraClass($class) { - // split at white space - $classes = preg_split('/\s+/', $class); - foreach ($classes as $class) { - // unset one by one - unset($this->extraClasses[$class]); - } - return $this; - } - - /** - * @return string - */ - public function getExtraClasses() { - return join(' ', array_keys($this->extraClasses)); - } - - /** - * Returns the submitted value, if any, of any steps. - * - * @param string $fromStep (classname) - * @param string $key + /** + * Centerpiece of the flow control for the form. * - * @return mixed - */ - public function getValueFromOtherStep($fromStep, $key) { - // load the steps in the cache, if this one doesn't exist - if (!array_key_exists('steps_' . $fromStep, $this->step_data_cache)) { - $steps = MultiFormStep::get()->filter('SessionID', $this->form->session->ID); + * If set to a string, you have a linear form flow + * If set to an array, you should use {@link getNextStep()} + * to enact flow control and branching to different form + * steps, most likely based on previously set session data + * (e.g. a checkbox field or a dropdown). + * + * @var array|string + */ + public static $next_steps; - if($steps) { - foreach($steps as $step) { - $this->step_data_cache['steps_' . $step->ClassName] = $step->loadData(); + /** + * Each {@link MultiForm} subclass needs at least + * one step which is marked as the "final" one + * and triggers the {@link MultiForm->finish()} + * method that wraps up the whole submission. + * + * @var boolean + */ + public static $is_final_step = false; + + /** + * This variable determines whether a user can use + * the "back" action from this step. + * + * @TODO This does not check if the arbitrarily chosen step + * using the step indicator is actually a previous step, so + * unless you remove the link from the indicator template, or + * type in StepID=23 to the address bar you can still go back + * using the step indicator. + * + * @var boolean + */ + protected static $can_go_back = true; + + /** + * Title of this step. + * + * Used for the step indicator templates. + * + * @var string + */ + protected $title; + + /** + * Form class that this step is directly related to. + * + * @var MultiForm subclass + */ + protected $form; + + /** + * List of additional CSS classes for this step + * + * @var array $extraClasses + */ + protected $extraClasses = array(); + + /** + * Temporary cache to increase the performance for repeated look ups. + * + * @var array $cache + */ + protected $step_data_cache = array(); + + /** + * Form fields to be rendered with this step. + * (Form object is created in {@link MultiForm}. + * + * This function needs to be implemented on your + * subclasses of MultiFormStep. + * + * @return FieldList + */ + public function getFields() + { + user_error('Please implement getFields on your MultiFormStep subclass', E_USER_ERROR); + } + + /** + * Additional form actions to be added to this step. + * (Form object is created in {@link MultiForm}. + * + * Note: This is optional, and is to be implemented + * on your subclasses of MultiFormStep. + * + * @return FieldList + */ + public function getExtraActions() + { + return (class_exists('FieldList')) ? new FieldList() : new FieldSet(); + } + + /** + * Get a validator specific to this form. + * The form is automatically validated in {@link Form->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 ? $this->title : $this->class; + } + + /** + * Gets a direct link to this step (only works + * if you're allowed to skip steps, or this step + * has already been saved to the database + * for the current {@link MultiFormSession}). + * + * @return string Relative URL to this step + */ + public function Link() + { + $form = $this->form; + return Controller::join_links($form->getDisplayLink(), "?{$form->config()->get_var}={$this->Session()->Hash}"); + } + + /** + * Unserialize stored session data and return it. + * This is used for loading data previously saved + * in session back into the form. + * + * You need to overload this method onto your own + * step if you require custom loading. An example + * would be selective loading specific fields, leaving + * others that are not required. + * + * @return array + */ + public function loadData() + { + return ($this->Data && is_string($this->Data)) ? unserialize($this->Data) : array(); + } + + /** + * Save the data for this step into session, serializing it first. + * + * To selectively save fields, instead of it all, this + * method would need to be overloaded on your step class. + * + * @param array $data The processed data from save() on {@link MultiForm} + */ + public function saveData($data) + { + $this->Data = serialize($data); + $this->write(); + } + + /** + * Save the data on this step into an object, + * similiar to {@link Form->saveInto()} - by building + * a stub form from {@link getFields()}. This is necessary + * to trigger each {@link FormField->saveInto()} method + * individually, rather than assuming that all data + * serialized through {@link saveData()} can be saved + * as a simple value outside of the original FormField context. + * + * @param DataObject $obj + */ + public function saveInto($obj) + { + $form = new Form( + Controller::curr(), + 'Form', + $this->getFields(), + ((class_exists('FieldList')) ? new FieldList() : new FieldSet()) + ); + $form->loadDataFrom($this->loadData()); + $form->saveInto($obj); + return $obj; + } + + /** + * 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()}. + * + * @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 + * + * @return string Classname of a {@link MultiFormStep} subclass + */ + public function getNextStep() + { + $nextSteps = static::$next_steps; + + // Check if next_steps have been implemented properly if not the final step + if (!$this->isFinalStep()) { + if (!isset($nextSteps)) { + user_error('MultiFormStep->getNextStep(): Please define at least one $next_steps on ' . $this->class, E_USER_ERROR); + } + } + + if (is_string($nextSteps)) { + return $nextSteps; + } elseif (is_array($nextSteps) && count($nextSteps)) { + // custom flow control goes here + return $nextSteps[0]; + } else { + return false; + } + } + + /** + * Returns the next step to the current step in the database. + * + * This will only return something if you've previously visited + * the step ahead of the current step, and then gone back a step. + * + * @return MultiFormStep|boolean + */ + public function getNextStepFromDatabase() + { + if ($this->SessionID && is_numeric($this->SessionID)) { + $nextSteps = static::$next_steps; + + if (is_string($nextSteps)) { + return DataObject::get_one($nextSteps, "\"SessionID\" = {$this->SessionID}"); + } elseif (is_array($nextSteps)) { + return DataObject::get_one($nextSteps[0], "\"SessionID\" = {$this->SessionID}"); + } else { + return false; + } + } + } + + /** + * Accessor method for self::$next_steps + * + * @return string|array + */ + public function getNextSteps() + { + return static::$next_steps; + } + + /** + * Returns the previous step, if there is one. + * + * To determine if there is a previous step, we check the database to see if there's + * a previous step for this multi form session ID. + * + * @return string Classname of a {@link MultiFormStep} subclass + */ + public function getPreviousStep() + { + $steps = DataObject::get('MultiFormStep', "\"SessionID\" = {$this->SessionID}", '"LastEdited" DESC'); + if ($steps) { + foreach ($steps as $step) { + $step->setForm($this->form); + + if ($step->getNextStep()) { + if ($step->getNextStep() == $this->class) { + return $step->class; + } } } - } + } + } - // check both as PHP isn't recursive - if(isset($this->step_data_cache['steps_' . $fromStep])) { - if(isset($this->step_data_cache['steps_' . $fromStep][$key])) { + /** + * Retrieves the previous step class record from the database. + * + * This will only return a record if you've previously been on the step. + * + * @return MultiFormStep subclass + */ + public function getPreviousStepFromDatabase() + { + if ($prevStepClass = $this->getPreviousStep()) { + return DataObject::get_one($prevStepClass, "\"SessionID\" = {$this->SessionID}"); + } + } + + /** + * Get the text to the use on the button to the previous step. + * @return string + */ + public function getPrevText() + { + return _t('MultiForm.BACK', 'Back'); + } + + /** + * Get the text to use on the button to the next step. + * @return string + */ + public function getNextText() + { + return _t('MultiForm.NEXT', 'Next'); + } + + /** + * Get the text to use on the button to submit the form. + * @return string + */ + public function getSubmitText() + { + return _t('MultiForm.SUBMIT', 'Submit'); + } + + /** + * Sets the form that this step is directly related to. + * + * @param MultiForm subclass $form + */ + public function setForm($form) + { + $this->form = $form; + } + + /** + * @return Form + */ + public function getForm() + { + return $this->form; + } + + // ##################### Utility #################### + + /** + * Determines whether the user is able to go back using the "action_back" + * Determines whether the user is able to go back using the "action_back" + * Determines whether the user is able to go back using the "action_back" + * form action, based on the boolean value of $can_go_back. + * + * @return boolean + */ + public function canGoBack() + { + return static::$can_go_back; + } + + /** + * Determines whether this step is the final step in the multi-step process or not, + * based on the variable $is_final_step - which must be defined on at least one step. + * + * @return boolean + */ + public function isFinalStep() + { + return static::$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; + } + + /** + * Add a CSS-class to the step. If needed, multiple classes can be added by delimiting a string with spaces. + * + * @param string $class A string containing a classname or several class names delimited by a space. + * @return MultiFormStep + */ + public function addExtraClass($class) + { + // split at white space + $classes = preg_split('/\s+/', $class); + foreach ($classes as $class) { + // add classes one by one + $this->extraClasses[$class] = $class; + } + return $this; + } + + /** + * Remove a CSS-class from the step. Multiple classes names can be passed through as a space delimited string. + * + * @param string $class + * @return MultiFormStep + */ + public function removeExtraClass($class) + { + // split at white space + $classes = preg_split('/\s+/', $class); + foreach ($classes as $class) { + // unset one by one + unset($this->extraClasses[$class]); + } + return $this; + } + + /** + * @return string + */ + public function getExtraClasses() + { + return join(' ', array_keys($this->extraClasses)); + } + + /** + * Returns the submitted value, if any, of any steps. + * + * @param string $fromStep (classname) + * @param string $key + * + * @return mixed + */ + public function getValueFromOtherStep($fromStep, $key) + { + // load the steps in the cache, if this one doesn't exist + if (!array_key_exists('steps_' . $fromStep, $this->step_data_cache)) { + $steps = MultiFormStep::get()->filter('SessionID', $this->form->session->ID); + + if ($steps) { + foreach ($steps as $step) { + $this->step_data_cache['steps_' . $step->ClassName] = $step->loadData(); + } + } + } + + // check both as PHP isn't recursive + if (isset($this->step_data_cache['steps_' . $fromStep])) { + if (isset($this->step_data_cache['steps_' . $fromStep][$key])) { return $this->step_data_cache['steps_' . $fromStep][$key]; } } return null; - } + } - /** - * allows to get a value from another step copied over - * - * @param FieldList $fields - * @param string $formStep - * @param string $fieldName - * @param string $fieldNameTarget (optional) - */ - public function copyValueFromOtherStep(FieldList $fields, $formStep, $fieldName, $fieldNameTarget = null) { - // if a target field isn't defined use the same fieldname - if (!$fieldNameTarget) $fieldNameTarget = $fieldName; + /** + * allows to get a value from another step copied over + * + * @param FieldList $fields + * @param string $formStep + * @param string $fieldName + * @param string $fieldNameTarget (optional) + */ + public function copyValueFromOtherStep(FieldList $fields, $formStep, $fieldName, $fieldNameTarget = null) + { + // if a target field isn't defined use the same fieldname + if (!$fieldNameTarget) { + $fieldNameTarget = $fieldName; + } - $fields->fieldByName($fieldNameTarget)->setValue($this->getValueFromOtherStep($formStep, $fieldName)); - } + $fields->fieldByName($fieldNameTarget)->setValue($this->getValueFromOtherStep($formStep, $fieldName)); + } } diff --git a/src/tasks/MultiFormPurgeTask.php b/src/tasks/MultiFormPurgeTask.php index dfe2d87..c761e98 100644 --- a/src/tasks/MultiFormPurgeTask.php +++ b/src/tasks/MultiFormPurgeTask.php @@ -12,44 +12,48 @@ * * @package multiform */ -class MultiFormPurgeTask extends BuildTask { +class MultiFormPurgeTask extends BuildTask +{ - /** - * Days after which sessions expire and - * are automatically deleted. - * - * @var int - */ - public static $session_expiry_days = 7; + /** + * Days after which sessions expire and + * are automatically deleted. + * + * @var int + */ + public static $session_expiry_days = 7; - /** - * Run this cron task. - * - * Go through all MultiFormSession records that - * are older than the days specified in $session_expiry_days - * and delete them. - */ - public function run($request) { - $sessions = $this->getExpiredSessions(); - $delCount = 0; - if($sessions) foreach($sessions as $session) { - $session->delete(); - $delCount++; - } - echo $delCount . ' session records deleted that were older than ' . self::$session_expiry_days . ' days.'; - } - - /** - * Return all MultiFormSession database records that are older than - * the days specified in $session_expiry_days - * - * @return DataObjectSet - */ - protected function getExpiredSessions() { - return DataObject::get( - 'MultiFormSession', - "DATEDIFF(NOW(), \"MultiFormSession\".\"Created\") > " . self::$session_expiry_days - ); - } + /** + * Run this cron task. + * + * Go through all MultiFormSession records that + * are older than the days specified in $session_expiry_days + * and delete them. + */ + public function run($request) + { + $sessions = $this->getExpiredSessions(); + $delCount = 0; + if ($sessions) { + foreach ($sessions as $session) { + $session->delete(); + $delCount++; + } + } + echo $delCount . ' session records deleted that were older than ' . self::$session_expiry_days . ' days.'; + } + /** + * Return all MultiFormSession database records that are older than + * the days specified in $session_expiry_days + * + * @return DataObjectSet + */ + protected function getExpiredSessions() + { + return DataObject::get( + 'MultiFormSession', + "DATEDIFF(NOW(), \"MultiFormSession\".\"Created\") > " . self::$session_expiry_days + ); + } } diff --git a/tests/MultiFormObjectDecoratorTest.php b/tests/MultiFormObjectDecoratorTest.php index f935f93..15f9b8a 100644 --- a/tests/MultiFormObjectDecoratorTest.php +++ b/tests/MultiFormObjectDecoratorTest.php @@ -1,44 +1,45 @@ array('MultiFormObjectDecorator') - ); + protected $requiredExtensions = array( + 'MultiFormObjectDecorator_DataObject' => array('MultiFormObjectDecorator') + ); - protected $extraDataObjects = array( - 'MultiFormObjectDecorator_DataObject' - ); + protected $extraDataObjects = array( + 'MultiFormObjectDecorator_DataObject' + ); - public function testTemporaryDataFilteredQuery() { - $records = MultiFormObjectDecorator_DataObject::get() - ->map('Name') - ->toArray(); + public function testTemporaryDataFilteredQuery() + { + $records = MultiFormObjectDecorator_DataObject::get() + ->map('Name') + ->toArray(); - $this->assertContains('Test 1', $records); - $this->assertContains('Test 2', $records); - $this->assertNotContains('Test 3', $records); + $this->assertContains('Test 1', $records); + $this->assertContains('Test 2', $records); + $this->assertNotContains('Test 3', $records); + } - } - - public function testTemporaryDataQuery() { - $records = MultiFormObjectDecorator_DataObject::get() - ->filter(array('MultiFormIsTemporary' => 1)) - ->map('Name') - ->toArray(); - - $this->assertNotContains('Test 1', $records); - $this->assertNotContains('Test 2', $records); - $this->assertContains('Test 3', $records); - } + public function testTemporaryDataQuery() + { + $records = MultiFormObjectDecorator_DataObject::get() + ->filter(array('MultiFormIsTemporary' => 1)) + ->map('Name') + ->toArray(); + $this->assertNotContains('Test 1', $records); + $this->assertNotContains('Test 2', $records); + $this->assertContains('Test 3', $records); + } } -class MultiFormObjectDecorator_DataObject extends DataObject { - - private static $db = array( - 'Name' => 'Varchar' - ); +class MultiFormObjectDecorator_DataObject extends DataObject +{ + private static $db = array( + 'Name' => 'Varchar' + ); } diff --git a/tests/MultiFormTest.php b/tests/MultiFormTest.php index fbadf2e..589a485 100644 --- a/tests/MultiFormTest.php +++ b/tests/MultiFormTest.php @@ -17,162 +17,181 @@ * @package multiform * @subpackage tests */ -class MultiFormTest extends FunctionalTest { +class MultiFormTest extends FunctionalTest +{ - public static $fixture_file = 'multiform/tests/MultiFormTest.yml'; + public static $fixture_file = 'multiform/tests/MultiFormTest.yml'; - protected $controller; + protected $controller; - public function setUp() { - parent::setUp(); + public function setUp() + { + parent::setUp(); - $this->controller = new MultiFormTest_Controller(); - $this->form = $this->controller->Form(); - } + $this->controller = new MultiFormTest_Controller(); + $this->form = $this->controller->Form(); + } - public function testInitialisingForm() { - $this->assertTrue(is_numeric($this->form->getCurrentStep()->ID) && ($this->form->getCurrentStep()->ID > 0)); - $this->assertTrue(is_numeric($this->form->getSession()->ID) && ($this->form->getSession()->ID > 0)); - $this->assertEquals('MultiFormTest_StepOne', $this->form->getStartStep()); - } + public function testInitialisingForm() + { + $this->assertTrue(is_numeric($this->form->getCurrentStep()->ID) && ($this->form->getCurrentStep()->ID > 0)); + $this->assertTrue(is_numeric($this->form->getSession()->ID) && ($this->form->getSession()->ID > 0)); + $this->assertEquals('MultiFormTest_StepOne', $this->form->getStartStep()); + } - public function testSessionGeneration() { - $this->assertTrue($this->form->session->ID > 0); - } + public function testSessionGeneration() + { + $this->assertTrue($this->form->session->ID > 0); + } - public function testMemberLogging() { - // Grab any user to fake being logged in as, and ensure that after a session is written it has - // that user as the submitter. - $userId = Member::get_one("Member")->ID; - $this->session()->inst_set('loggedInAs', $userId); + public function testMemberLogging() + { + // Grab any user to fake being logged in as, and ensure that after a session is written it has + // that user as the submitter. + $userId = Member::get_one("Member")->ID; + $this->session()->inst_set('loggedInAs', $userId); - $session = $this->form->session; - $session->write(); + $session = $this->form->session; + $session->write(); - $this->assertEquals($userId, $session->SubmitterID); - } + $this->assertEquals($userId, $session->SubmitterID); + } - public function testSecondStep() { - $this->assertEquals('MultiFormTest_StepTwo', $this->form->getCurrentStep()->getNextStep()); - } + public function testSecondStep() + { + $this->assertEquals('MultiFormTest_StepTwo', $this->form->getCurrentStep()->getNextStep()); + } - public function testParentForm() { - $currentStep = $this->form->getCurrentStep(); - $this->assertEquals($currentStep->getForm()->class, $this->form->class); - } + public function testParentForm() + { + $currentStep = $this->form->getCurrentStep(); + $this->assertEquals($currentStep->getForm()->class, $this->form->class); + } - public function testTotalStepCount() { - $this->assertEquals(3, $this->form->getAllStepsLinear()->Count()); - } + public function testTotalStepCount() + { + $this->assertEquals(3, $this->form->getAllStepsLinear()->Count()); + } - public function testCompletedSession() { - $this->form->setCurrentSessionHash($this->form->session->Hash); - $this->assertInstanceOf('MultiFormSession', $this->form->getCurrentSession()); - $this->form->session->markCompleted(); - $this->assertNull($this->form->getCurrentSession()); - } + public function testCompletedSession() + { + $this->form->setCurrentSessionHash($this->form->session->Hash); + $this->assertInstanceOf('MultiFormSession', $this->form->getCurrentSession()); + $this->form->session->markCompleted(); + $this->assertNull($this->form->getCurrentSession()); + } - public function testIncorrectSessionIdentifier() { - $this->form->setCurrentSessionHash('sdfsdf3432325325sfsdfdf'); // made up! + public function testIncorrectSessionIdentifier() + { + $this->form->setCurrentSessionHash('sdfsdf3432325325sfsdfdf'); // made up! - // A new session is generated, even though we made up the identifier - $this->assertInstanceOf('MultiFormSession', $this->form->session); - } + // A new session is generated, even though we made up the identifier + $this->assertInstanceOf('MultiFormSession', $this->form->session); + } - function testCustomGetVar() { - Config::nest(); - Config::inst()->update('MultiForm', 'get_var', 'SuperSessionID'); + function testCustomGetVar() + { + Config::nest(); + Config::inst()->update('MultiForm', 'get_var', 'SuperSessionID'); - $form = $this->controller->Form(); - $this->assertContains('SuperSessionID', $form::$ignored_fields, "GET var wasn't added to ignored fields"); - $this->assertContains('SuperSessionID', $form->FormAction(), "Form action doesn't contain correct session + $form = $this->controller->Form(); + $this->assertContains('SuperSessionID', $form::$ignored_fields, "GET var wasn't added to ignored fields"); + $this->assertContains('SuperSessionID', $form->FormAction(), "Form action doesn't contain correct session ID parameter"); - $this->assertContains('SuperSessionID', $form->getCurrentStep()->Link(), "Form step doesn't contain correct + $this->assertContains('SuperSessionID', $form->getCurrentStep()->Link(), "Form step doesn't contain correct session ID parameter"); - Config::unnest(); -} - + Config::unnest(); + } } /** * @package multiform * @subpackage tests */ -class MultiFormTest_Controller extends Controller implements TestOnly { +class MultiFormTest_Controller extends Controller implements TestOnly +{ - public function Link() { - return 'MultiFormTest_Controller'; - } + public function Link() + { + return 'MultiFormTest_Controller'; + } - public function Form($request = null) { - $form = new MultiFormTest_Form($this, 'Form'); - $form->setHTMLID('MultiFormTest_Form'); - return $form; - } + public function Form($request = null) + { + $form = new MultiFormTest_Form($this, 'Form'); + $form->setHTMLID('MultiFormTest_Form'); + return $form; + } } /** * @package multiform * @subpackage tests */ -class MultiFormTest_Form extends MultiForm implements TestOnly { +class MultiFormTest_Form extends MultiForm implements TestOnly +{ - public static $start_step = 'MultiFormTest_StepOne'; - - public function getStartStep() { - return self::$start_step; - } + public static $start_step = 'MultiFormTest_StepOne'; + public function getStartStep() + { + return self::$start_step; + } } /** * @package multiform * @subpackage tests */ -class MultiFormTest_StepOne extends MultiFormStep implements TestOnly { +class MultiFormTest_StepOne extends MultiFormStep implements TestOnly +{ - public static $next_steps = 'MultiFormTest_StepTwo'; + public static $next_steps = 'MultiFormTest_StepTwo'; - public function getFields() { - $class = (class_exists('FieldList')) ? 'FieldList' : 'FieldSet'; - return new $class( - new TextField('FirstName', 'First name'), - new TextField('Surname', 'Surname'), - new EmailField('Email', 'Email address') - ); - } + public function getFields() + { + $class = (class_exists('FieldList')) ? 'FieldList' : 'FieldSet'; + return new $class( + new TextField('FirstName', 'First name'), + new TextField('Surname', 'Surname'), + new EmailField('Email', 'Email address') + ); + } } - + /** * @package multiform * @subpackage tests */ -class MultiFormTest_StepTwo extends MultiFormStep implements TestOnly { +class MultiFormTest_StepTwo extends MultiFormStep implements TestOnly +{ - public static $next_steps = 'MultiFormTest_StepThree'; + public static $next_steps = 'MultiFormTest_StepThree'; - public function getFields() { - $class = (class_exists('FieldList')) ? 'FieldList' : 'FieldSet'; - return new $class( - new TextareaField('Comments', 'Tell us a bit about yourself...') - ); - } + public function getFields() + { + $class = (class_exists('FieldList')) ? 'FieldList' : 'FieldSet'; + return new $class( + new TextareaField('Comments', 'Tell us a bit about yourself...') + ); + } } - + /** * @package multiform * @subpackage tests */ -class MultiFormTest_StepThree extends MultiFormStep implements TestOnly { +class MultiFormTest_StepThree extends MultiFormStep implements TestOnly +{ - public static $is_final_step = true; - - public function getFields() { - $class = (class_exists('FieldList')) ? 'FieldList' : 'FieldSet'; - return new $class( - new TextField('Test', 'Anything else you\'d like to tell us?') - ); - } + public static $is_final_step = true; + public function getFields() + { + $class = (class_exists('FieldList')) ? 'FieldList' : 'FieldSet'; + return new $class( + new TextField('Test', 'Anything else you\'d like to tell us?') + ); + } }