feat: silverstripe 5 support

This commit is contained in:
Will Rossiter 2023-09-06 20:35:07 +12:00
parent 27552089c1
commit 3d5995526c
No known key found for this signature in database
GPG Key ID: 7FD2A809B22259EF
8 changed files with 192 additions and 273 deletions

11
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,11 @@
name: CI
on:
push:
pull_request:
workflow_dispatch:
jobs:
ci:
name: CI
uses: silverstripe/gha-ci/.github/workflows/ci.yml@v1

View File

@ -1,37 +0,0 @@
language: php
dist: trusty
env:
global:
- COMPOSER_ROOT_VERSION=4.0.x-dev
matrix:
include:
- php: 5.6
env: DB=MYSQL PHPCS_TEST=1 PHPUNIT_TEST=1
- php: 7.0
env: DB=PGSQL PHPUNIT_TEST=1
- php: 7.1
env: DB=MYSQL PHPUNIT_COVERAGE_TEST=1
- php: 7.2
env: DB=MYSQL PHPUNIT_TEST=1
before_script:
# Init PHP
- phpenv rehash
- phpenv config-rm xdebug.ini
# Install composer dependencies
- composer validate
- composer require --no-update silverstripe/recipe-core:1.0.x-dev
- if [[ $DB == PGSQL ]]; then composer require --no-update silverstripe/postgresql:2.0.x-dev; fi
- composer install --prefer-dist --no-interaction --no-progress --no-suggest --optimize-autoloader --verbose --profile
script:
- if [[ $PHPUNIT_TEST ]]; then vendor/bin/phpunit; fi
- if [[ $PHPUNIT_COVERAGE_TEST ]]; then phpdbg -qrr vendor/bin/phpunit --coverage-clover=coverage.xml; fi
- if [[ $PHPCS_TEST ]]; then vendor/bin/phpcs --standard=vendor/silverstripe/framework/phpcs.xml.dist src/ tests/; fi
after_success:
- if [[ $PHPUNIT_COVERAGE_TEST ]]; then bash <(curl -s https://codecov.io/bash) -f coverage.xml; fi

View File

@ -1,34 +0,0 @@
0.3:
API CHANGE Changed MultiFormStep::$next_steps to public, and MultiFormStep::$is_final_step to public
API CHANGE MultiForm::$start_step is now declared as public, not protected, due to changes in Object::get_static()
ENHANCEMENT Custom text for back/next and submit buttons on a per-step basis
BUGFIX Checking $this->Data exists before trying to unserialize it
BUGFIX #4480 MultiForm::next() and MultiForm::prev() now save the form data before checking the next or previous step
API CHANGE Added currentSessionHash property to MultiForm which stores the currently opened session by the user
API CHANGE Renamed MultiForm::getSessionRecord() to getCurrentSession() and removed the parameter, since this wasn't done in a very OOP manner
ENHANCEMENT: enabled MultiForm to use customised action (actions_exempt_from_validation).
Changed has been done in the constructor to read the static variables (incl. overwritten variables).
BUGFIX: MultiFormStep::saveInto() needs the current Controller as the new $form Controller to work
ENHANCEMENT Added language files
BUGFIX If the step doesn't exist, don't attempt to delete it
BUGFIX Ensure that any relations to MultiFormStep are destroyed before calling delete()
ENHANCEMENT Removed hack of specific action to bypass validation and allow specifying actions to be exempt through a public static variable
0.2:
- ENHANCEMENT Updated entities and added german translation for multiform
- ENHANCEMENT Making multiform module translatable
- ENHANCEMENT Added MultiFormStep->saveInto() to simulate Form->saveInto()
- ENHANCEMENT Made MultiForm->prev() do the same behaviour for saving data
- ENHANCEMENT Added MultiForm->getSavedSteps()
- BUGFIX Removing url_type which isnt very useful
- BUGFIX $this->form wasn't accessible on MultiFormStep
- ENHANCEMENT Correct use of parent::construct() so that fields, actions
- BUGFIX SQL injection possibility fix on MultiForm->getSessionRecordByID()
- BUGFIX Disable security token inherited from Form, which isn't required
- BUGFIX Made MultiFormPurgeTask greatly simplified, and workable
- ENHANCEMENT Allowed static $ignored_fields to be overloaded on subclass of MultiForm
- BUGFIX Use $nextStep->Link and $prevStep->Link() for prev() and next() on MultiForm
- API CHANGE Ticket #2562 - Cleaner instanciation of MultiForm subclass without having to call ->init()
0.1:
- initial release

191
README.md
View File

@ -15,46 +15,46 @@ individual implementation can be customized to the project requirements.
## Maintainer Contact ## Maintainer Contact
* Sean Harvey (Nickname: sharvey, halkyon) <sean (at) silverstripe (dot) com> - Sean Harvey (Nickname: sharvey, halkyon) <sean (at) silverstripe (dot) com>
* Ingo Schommer (Nickname: chillu) <ingo (at) silverstripe (dot) com> - Ingo Schommer (Nickname: chillu) <ingo (at) silverstripe (dot) com>
## Requirements ## Requirements
* SilverStripe ^4.0 - SilverStripe ^5.
**Note:** For a SilverStripe 3.x compatible version, please use [the 1.x release line](https://github.com/silverstripe/silverstripe-multiform/tree/1.3). **Note:** For a SilverStripe 4.x or 3.x compatible version, please use `^2` or `^1` tagged version
## What it does do ## What it does do
* Abstracts fields, actions and validation to each individual step. - Abstracts fields, actions and validation to each individual step.
* Maintains flow control automatically, so it knows which steps are ahead and - Maintains flow control automatically, so it knows which steps are ahead and
behind. It also can retrieve an entire step process from start to finish, useful behind. It also can retrieve an entire step process from start to finish, useful
for a step list. for a step list.
* Persists data by storing it in the session for each step, once it's - Persists data by storing it in the session for each step, once it's
completed. The session is saved into the database. completed. The session is saved into the database.
* Allows customisation of next, previous steps, saving, loading and - Allows customisation of next, previous steps, saving, loading and
finalisation of the entire step process finalisation of the entire step process
* Allows for basic ability to branch a step by overloading the next step method - Allows for basic ability to branch a step by overloading the next step method
with logic to switch based on a condition (e.g. a checkbox, or dropdown in the with logic to switch based on a condition (e.g. a checkbox, or dropdown in the
field data). field data).
* Ties a user logged in who is using the step process (if applicable). This - Ties a user logged in who is using the step process (if applicable). This
means you can build extended security or logging. means you can build extended security or logging.
* Basic flexibility on the URL presented to the user when they are using the - Basic flexibility on the URL presented to the user when they are using the
forms. By default it stores an encrypted hash of the session in the URL, but you forms. By default it stores an encrypted hash of the session in the URL, but you
can reference it by the ID instead. It's recommend that additional security, can reference it by the ID instead. It's recommend that additional security,
such as checking the user who first started the session be applied if you want such as checking the user who first started the session be applied if you want
to reference by ID. to reference by ID.
## What it doesn't do ## What it doesn't do
* Automatically handle relation saving, e.g. MembershipForm manages the Member - Automatically handle relation saving, e.g. MembershipForm manages the Member
* Provide a complete package out of the box (you must write a bit of code using - Provide a complete package out of the box (you must write a bit of code using
the tutorial!) the tutorial!)
* Automatically determine what to do at the end of the process, and where to - Automatically determine what to do at the end of the process, and where to
save it save it
* Provide nicely presented URLs of each step (an enhancement, for the future) - Provide nicely presented URLs of each step (an enhancement, for the future)
Note: The *multiform* directory should sit in your SilverStripe root project Note: The _multiform_ directory should sit in your SilverStripe root project
directory in the file system as a sibling of cms and framework. directory in the file system as a sibling of cms and framework.
## Reporting bugs ## Reporting bugs
@ -74,7 +74,7 @@ before.
If you are not familiar with SilverStripe, it is highly recommended you run If you are not familiar with SilverStripe, it is highly recommended you run
through the tutorials before attempting to start with this one. through the tutorials before attempting to start with this one.
* [View a listing of all available tutorials](http://doc.silverstripe.org/tutorials) - [View a listing of all available tutorials](http://doc.silverstripe.org/tutorials)
### 1. Installing ### 1. Installing
@ -88,9 +88,9 @@ composer require silverstripe/multiform
### 2. Create subclass of MultiForm ### 2. Create subclass of MultiForm
First of all, we need to create a new subclass of *MultiForm*. First of all, we need to create a new subclass of _MultiForm_.
For the above example, our multi-form will be called *SurveyForm* For the above example, our multi-form will be called _SurveyForm_
```php ```php
use SilverStripe\MultiForm\Forms\MultiForm; use SilverStripe\MultiForm\Forms\MultiForm;
@ -144,7 +144,7 @@ To get more than one step, each step needs to know what it's next step is in
order to use flow control in our system. order to use flow control in our system.
To let the step know what step is next in the process, we do the same as setting To let the step know what step is next in the process, we do the same as setting
the `$start_step` variable *SurveyForm*, but we call it `$next_steps`. the `$start_step` variable _SurveyForm_, but we call it `$next_steps`.
```php ```php
use SilverStripe\MultiForm\Models\MultiFormStep; use SilverStripe\MultiForm\Models\MultiFormStep;
@ -166,7 +166,7 @@ class PersonalDetailsStep extends MultiFormStep
``` ```
At the very least, each step also has to have a `getFields()` method returning At the very least, each step also has to have a `getFields()` method returning
a *FieldSet* with some form field objects. These are the fields that the form a _FieldSet_ with some form field objects. These are the fields that the form
will render for the given step. will render for the given step.
Keep in mind that our multi-form also requires an end point. This step is the Keep in mind that our multi-form also requires an end point. This step is the
@ -188,17 +188,17 @@ class OrganisationDetailsStep extends MultiFormStep
### 5. Run database integrity check ### 5. Run database integrity check
We need to run *dev/build?flush=1* now, so that the classes are available to the We need to run _dev/build?flush=1_ now, so that the classes are available to the
SilverStripe manifest builder, and to ensure that the database is up to date SilverStripe manifest builder, and to ensure that the database is up to date
with all the latest tables. So you can go ahead and do that. with all the latest tables. So you can go ahead and do that.
*Note: Whenever you add a new step, you **MUST** run dev/build?flush=1 or you _Note: Whenever you add a new step, you **MUST** run dev/build?flush=1 or you
may receive errors.* may receive errors._
However, we've forgotten one thing. We need to create a method on a page-type so However, we've forgotten one thing. We need to create a method on a page-type so
that the form can be rendered into a given template. that the form can be rendered into a given template.
So, if we want to render our multi-form as `$SurveyForm` in the *Page.ss* So, if we want to render our multi-form as `$SurveyForm` in the _Page.ss_
template, we need to create a SurveyForm method (function) on the controller: template, we need to create a SurveyForm method (function) on the controller:
```php ```php
@ -228,32 +228,23 @@ class PageController extends ContentController
``` ```
The `SurveyForm()` function will create a new instance of our subclass of The `SurveyForm()` function will create a new instance of our subclass of
MultiForm, which in this example, is *SurveyForm*. This in turn will then set MultiForm, which in this example, is _SurveyForm_. This in turn will then set
up all the form fields, actions, and validation available to each step, as well up all the form fields, actions, and validation available to each step, as well
as the session. as the session.
You can of course, put the *SurveyForm* method on any controller class you You can of course, put the _SurveyForm_ method on any controller class you
like. like.
Your template should look something like this, to render the form in: Your template should look something like this, to render the form in:
```html ```html
<div id="content"> <div id="content">
<% if $Content %> <% if $Content %> $Content <% end_if %> <% if $SurveyForm %> $SurveyForm <%
$Content end_if %> <% if $Form %> $Form <% end_if %>
<% end_if %>
<% if $SurveyForm %>
$SurveyForm
<% end_if %>
<% if $Form %>
$Form
<% end_if %>
</div> </div>
``` ```
In this case, the above template example is a *sub-template* inside the *Layout* 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 directory for the templates. Note that we have also included `$Form`, so
standard forms are still able to be used alongside our multi-step form. standard forms are still able to be used alongside our multi-step form.
@ -264,8 +255,8 @@ useful, out of the box.
Two of them, as of the time of writing this are: Two of them, as of the time of writing this are:
* Progress list (multiform/templates/Includes/MultiFormProgressList.ss) - Progress list (multiform/templates/Includes/MultiFormProgressList.ss)
* Progress complete percent (multiform/templates/Includes/MultiFormProgressPercent.ss) - Progress complete percent (multiform/templates/Includes/MultiFormProgressPercent.ss)
They are designed to be used either by themselves, or alongside each other. They are designed to be used either by themselves, or alongside each other.
For example, the percentage could compliment the progress list to give an For example, the percentage could compliment the progress list to give an
@ -277,9 +268,7 @@ To include these with our instance of multiform, we just need to add an
For example: For example:
```html ```html
<% with $SurveyForm %> <% with $SurveyForm %> <% include MultiFormProgressList %> <% end_with %>
<% include MultiFormProgressList %>
<% end_with %>
``` ```
This means the included template is rendered within the scope of the This means the included template is rendered within the scope of the
@ -290,20 +279,9 @@ Putting it together, we might have something looking like this:
```html ```html
<div id="content"> <div id="content">
<% if $Content %> <% if $Content %> $Content <% end_if %> <% if $SurveyForm %> <% with
$Content $SurveyForm %> <% include MultiFormProgressList %> <% end_with %>
<% end_if %> $SurveyForm <% end_if %> <% if $Form %> $Form <% end_if %>
<% if $SurveyForm %>
<% with $SurveyForm %>
<% include MultiFormProgressList %>
<% end_with %>
$SurveyForm
<% end_if %>
<% if $Form %>
$Form
<% end_if %>
</div> </div>
``` ```
@ -312,29 +290,27 @@ specific to your project, just create a new "Include" template inside your own
project templates directory, and include that instead. Some helpful methods to project templates directory, and include that instead. Some helpful methods to
use on the MultiForm would be: use on the MultiForm would be:
- `AllStepsLinear()` (which also makes use of `getAllStepsRecursive()` to
* `AllStepsLinear()` (which also makes use of `getAllStepsRecursive()` to produce a list of steps)
produce a list of steps) - `getCompletedStepCount()`
* `getCompletedStepCount()` - `getTotalStepCount()`
* `getTotalStepCount()` - `getCompletedPercent()`
* `getCompletedPercent()`
The default progress indicators make use of the above functions in the The default progress indicators make use of the above functions in the
templates. templates.
To use a custom method of your own, simply create a new method on your subclass To use a custom method of your own, simply create a new method on your subclass
of MultiForm. In this example, *SurveyForm* would be the one to customise. of MultiForm. In this example, _SurveyForm_ would be the one to customise.
This new method you create would then become available in the progress indicator This new method you create would then become available in the progress indicator
template. template.
### 7. Loading values from other steps ### 7. Loading values from other steps
There are several use cases where you want to pre-populate a value based on the submission value of another step. There are several use cases where you want to pre-populate a value based on the submission value of another step.
There are two methods supporting this: There are two methods supporting this:
* `getValueFromOtherStep()` loads any submitted value from another step from the session - `getValueFromOtherStep()` loads any submitted value from another step from the session
* `copyValueFromOtherStep()` saves you the repeated work of adding the same lines of code again and again. - `copyValueFromOtherStep()` saves you the repeated work of adding the same lines of code again and again.
Here is an example of how to populate the email address from step 1 in step2 : Here is an example of how to populate the email address from step 1 in step2 :
@ -387,9 +363,9 @@ progress through successfully, we need to customise what happens at the end of
the last step. the last step.
On the final step, the `finish()` method is called to finalise all the data On the final step, the `finish()` method is called to finalise all the data
from the steps we completed. This method can be found on *MultiForm*. However, from the steps we completed. This method can be found on _MultiForm_. However,
we cannot automatically save each step, because we don't know where to save it. we cannot automatically save each step, because we don't know where to save it.
So, we must write some code on our subclass of *MultiForm*, overloading So, we must write some code on our subclass of _MultiForm_, overloading
`finish()` to tell it what to do at the end. `finish()` to tell it what to do at the end.
Here is an example of what we could do here: Here is an example of what we could do here:
@ -467,6 +443,7 @@ class Organisation extends DataObject
]; ];
} }
``` ```
#### Warning #### Warning
If you're dealing with sensitive data, it's best to delete the session and step If you're dealing with sensitive data, it's best to delete the session and step
@ -498,18 +475,18 @@ In order, when you have a page with a multi-form rendering into it, it chooses
which template to render that form in this order, within the context of the which template to render that form in this order, within the context of the
MultiForm class: MultiForm class:
* $this->getCurrentStep()->class (the current step class) - $this->getCurrentStep()->class (the current step class)
* MultiFormStep - MultiFormStep
* $this->class (your subclass of MultiForm) - $this->class (your subclass of MultiForm)
* MultiForm - MultiForm
* Form - Form
More than likely, you'll want the first one to be available when the form More than likely, you'll want the first one to be available when the form
renders. To that effect, you can start placing templates in the renders. To that effect, you can start placing templates in the
*templates/Includes* directory for your project. You need to name them the same _templates/Includes_ directory for your project. You need to name them the same
as the class name for each step. For example, if you want *MembershipForm*, a as the class name for each step. For example, if you want _MembershipForm_, a
subclass of *MultiFormStep* to have it's own template, you would put subclass of _MultiFormStep_ to have it's own template, you would put
*MembershipForm.ss* into that directory, and run *?flush=1*. _MembershipForm.ss_ into that directory, and run _?flush=1_.
If you'd like a pre-existing template on how to customise the form step, have a If you'd like a pre-existing template on how to customise the form step, have a
look at Form.ss that's found within the framework module. Use that template, as look at Form.ss that's found within the framework module. Use that template, as
@ -542,6 +519,7 @@ class MyStep extends MultiFormStep
... ...
} }
``` ```
### Validation ### Validation
To define validation on a step-by-step basis, please define getValidator() and To define validation on a step-by-step basis, please define getValidator() and
@ -571,10 +549,10 @@ class MyStep extends MultiFormStep
`finish()` is the final call in the process. At this step, all the form data `finish()` is the final call in the process. At this step, all the form data
would most likely be unserialized, and saved to the database in whatever way the would most likely be unserialized, and saved to the database in whatever way the
developer sees fit. By default, we have a `finish()` method on *MultiForm* which developer sees fit. By default, we have a `finish()` method on _MultiForm_ which
serializes the last step form data into the database, and that's it. serializes the last step form data into the database, and that's it.
`finish()` should be overloaded onto your subclass of *MultiForm*, and `finish()` should be overloaded onto your subclass of _MultiForm_, and
`parent::finish()` should be called first, otherwise the last step form data `parent::finish()` should be called first, otherwise the last step form data
won't be saved. won't be saved.
@ -627,38 +605,39 @@ $this->session->delete();
### Expiring old session data ### Expiring old session data
Included with the MultiForm module is a class called *MultiFormPurgeTask*. This Included with the MultiForm module is a class called _MultiFormPurgeTask_. This
task can be used to purge expired session data on a regular basis. The date of task can be used to purge expired session data on a regular basis. The date of
expiry can be customised, and is given a default of 7 days to delete sessions expiry can be customised, and is given a default of 7 days to delete sessions
after their creation. after their creation.
You can run the task from the URL, by using http://mysite.com/dev/tasks/MultiFormPurgeTask?flush=1 You can run the task from the URL, by using http://mysite.com/dev/tasks/MultiFormPurgeTask?flush=1
MultiFormPurgeTask is a subclass of *BuildTask*, so can be run using the [SilverStripe CLI tools](http://doc.silverstripe.org/framework/en/topics/commandline). MultiFormPurgeTask is a subclass of _BuildTask_, so can be run using the [SilverStripe CLI tools](http://doc.silverstripe.org/framework/en/topics/commandline).
One way of automatically running this on a UNIX based machine is by cron. One way of automatically running this on a UNIX based machine is by cron.
## TODO ## TODO
* Code example on how to use `$form->saveInto()` with MultiForm, as it doesn't have all steps in the $form context at `finish()` - Code example on how to use `$form->saveInto()` with MultiForm, as it doesn't have all steps in the $form context at `finish()`
* Allowing a user to click a link, and have an email sent to them with the current state, so they can come back and use the form exactly where they left off - Allowing a user to click a link, and have an email sent to them with the current state, so they can come back and use the form exactly where they left off
* Possibly allow for different means to persist data, such as the browser session cache instead of the database. - Possibly allow for different means to persist data, such as the browser session cache instead of the database.
* Different presentation of the URL to identify each step. - Different presentation of the URL to identify each step.
* Allow customisation of `prev()` and `next()` on each step. Currently you can only customise for the entire MultiForm subclass. There is a way to customise on a per step basis, which could be described in a small recipe. - Allow customisation of `prev()` and `next()` on each step. Currently you can only customise for the entire MultiForm subclass. There is a way to customise on a per step basis, which could be described in a small recipe.
* More detailed explanation, and recipe example on how to make branched multistep forms. For example, clicking a different action takes you to an alternative next step than the one defined in `$next_steps`
- More detailed explanation, and recipe example on how to make branched multistep forms. For example, clicking a different action takes you to an alternative next step than the one defined in `$next_steps`
## Related ## Related
* [Form](/form) - [Form](/form)
* [Form field types](http://doc.silverstripe.org/form-field-types)
* [Tutorials](/tutorials) - [Form field types](http://doc.silverstripe.org/form-field-types)
* [Tutorial 3 - Forms](http://doc.silverstripe.org/framework/en/tutorials/3-forms)
* [Templates](http://doc.silverstripe.org/framework/en/reference/templates) - [Tutorials](/tutorials)
- [Tutorial 3 - Forms](http://doc.silverstripe.org/framework/en/tutorials/3-forms)
- [Templates](http://doc.silverstripe.org/framework/en/reference/templates)

View File

@ -1 +0,0 @@
<?php

View File

@ -18,16 +18,14 @@
} }
], ],
"require": { "require": {
"silverstripe/framework": "^4" "silverstripe/framework": "^5"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^5.7", "phpunit/phpunit": "^9.6"
"squizlabs/php_codesniffer": "^3.0",
"silverstripe/versioned": "^1"
}, },
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "2.x-dev" "dev-main": "3.x-dev"
} }
}, },
"autoload": { "autoload": {

View File

@ -1,7 +1,10 @@
<phpunit bootstrap="vendor/silverstripe/framework/tests/bootstrap.php" colors="true"> <?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/silverstripe/cms/tests/bootstrap.php" colors="true">
<testsuites>
<testsuite name="Default"> <testsuite name="Default">
<directory>tests/</directory> <directory>tests/</directory>
</testsuite> </testsuite>
</testsuites>
<filter> <filter>
<whitelist addUncoveredFilesFromWhitelist="true"> <whitelist addUncoveredFilesFromWhitelist="true">
<directory suffix=".php">src/</directory> <directory suffix=".php">src/</directory>

View File

@ -44,7 +44,7 @@ class MultiFormTest extends FunctionalTest
*/ */
protected $form; protected $form;
protected function setUp() protected function setUp(): void
{ {
parent::setUp(); parent::setUp();
@ -122,17 +122,17 @@ class MultiFormTest extends FunctionalTest
Config::modify()->set(MultiForm::class, 'get_var', 'SuperSessionID'); Config::modify()->set(MultiForm::class, 'get_var', 'SuperSessionID');
$form = $this->controller->Form(); $form = $this->controller->Form();
$this->assertContains( $this->assertStringContainsString(
'SuperSessionID', 'SuperSessionID',
$form->config()->get('ignored_fields'), $form->config()->get('ignored_fields'),
'GET var wasn\'t added to ignored fields' 'GET var wasn\'t added to ignored fields'
); );
$this->assertContains( $this->assertStringContainsString(
'SuperSessionID', 'SuperSessionID',
$form->FormAction(), $form->FormAction(),
"Form action doesn't contain correct session ID parameter" "Form action doesn't contain correct session ID parameter"
); );
$this->assertContains( $this->assertStringContainsString(
'SuperSessionID', 'SuperSessionID',
$form->getCurrentStep()->Link(), $form->getCurrentStep()->Link(),
"Form step doesn't contain correct session ID parameter" "Form step doesn't contain correct session ID parameter"