silverstripe/multiform

SilverStripe 多步骤表单,具有流程控制和状态持久化

安装: 48 189

依赖项: 5

建议者: 0

安全性: 0

星级: 31

关注者: 13

分支: 32

开放问题: 1

类型:silverstripe-vendormodule

2.0.2 2023-09-06 08:23 UTC

README

Scrutinizer Code Quality Code Coverage

简介

MultiForm 是一个 SilverStripe 模块,允许对表单进行流程控制,并自动根据每个步骤类的配置变量确定步骤过程。它增强了 SilverStripe 中现有的 Form 类。

模块的目标是提供比功能更大的灵活性,以便每个单独的实现都可以根据项目需求进行定制。

维护者联系方式

  • Sean Harvey(昵称:sharvey,halkyon)<sean (at) silverstripe (dot) com>
  • Ingo Schommer(昵称:chillu)<ingo (at) silverstripe (dot) com>

要求

  • SilverStripe ^5.

注意:对于与 SilverStripe 4.x 或 3.x 兼容的版本,请使用 ^2^1 标记的版本

它做什么

  • 将字段、操作和验证抽象到每个单独的步骤中。
  • 自动维护流程控制,因此它知道哪些步骤在前和在后。它还可以从开始到结束检索整个步骤过程,这对于步骤列表很有用。
  • 通过在每个步骤完成后将其存储到会话中,以持久化数据。会话保存到数据库中。
  • 允许自定义下一个、上一个步骤、保存、加载和整个步骤过程的最终化
  • 允许通过覆盖下一个步骤方法并添加基于条件的逻辑(例如,字段数据中的复选框或下拉菜单)来基本分支步骤。
  • 将使用步骤过程登录的用户绑定(如果适用)。这意味着您可以构建扩展的安全或日志记录。
  • 在用户使用表单时,对用户显示的 URL 具有基本的灵活性。默认情况下,它将加密的会话哈希存储在 URL 中,但您也可以通过 ID 引用它。如果您想通过 ID 引用,建议应用额外的安全措施,例如检查首次启动会话的用户。

它不做什么

  • 自动处理关系保存,例如 MembershipForm 管理成员
  • 提供完整的开箱即用软件包(您必须使用教程编写一些代码!)
  • 自动确定过程结束时要做什么以及在哪里保存
  • 提供每个步骤的精美呈现的 URL(未来的增强功能)

注意:multiform 目录应位于您的 SilverStripe 根项目目录中文件系统中的 cms 和 framework 的同级目录中。

报告错误

如果您发现应修复以供未来版本使用的错误,请请在此处创建一个条目 https://github.com/silverstripe/silverstripe-multiform/issues

这有助于确保我们未来发布更少的错误软件!

教程

假设开始此教程的开发者具有 SilverStripe 的中级知识,了解 "run dev/build?flush=1" 的含义,并且之前在 SilverStripe 中编写过一些自定义 PHP 代码。

如果您不熟悉 SilverStripe,强烈建议您在尝试此教程之前运行教程。

1. 安装

使用Composer,您可以通过以下命令将多表单安装到您的SilverStripe网站上(在您的网站当前所在目录中):

composer require silverstripe/multiform

2. 创建MultiForm的子类

首先,我们需要创建一个新的MultiForm子类。

对于上述示例,我们的多表单将被称为SurveyForm

use SilverStripe\MultiForm\Forms\MultiForm;

class SurveyForm extends MultiForm
{

}

3. 设置第一步

现在我们已经创建了新的MultiForm子类步骤,我们需要定义哪个表单将成为我们的多步骤过程中的第一步。

每个表单步骤都必须是MultiFormStep的子类。这样,多表单可以识别这个步骤是我们步骤过程的一部分,而不是一个标准的表单。

例如,如果我们有一个第一步用于收集表单用户的个人详细信息,那么我们可能会有这个类

use SilverStripe\MultiForm\Models\MultiFormStep;

class PersonalDetailsStep extends MultiFormStep
{

}

现在我们已经定义了表单的第一步,我们需要回到我们的MultiForm子类SurveyForm,并告诉它SurveyFormPersonalDetailsStep是第一步。

use SilverStripe\MultiForm\Forms\MultiForm;

class SurveyForm extends MultiForm
{
    private static $start_step = PersonalDetailsStep::class;
}

4. 定义下一步和最后一步

我们已经成功地设置了多表单的基本结构,但它不是很实用,因为只有一个步骤!

为了有多个步骤,每个步骤都需要知道它的下一步是什么,以便在我们的系统中使用流程控制。

为了让步骤知道过程中下一步是什么,我们与设置$start_step变量SurveyForm的方式相同,但我们称之为$next_steps

use SilverStripe\MultiForm\Models\MultiFormStep;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\TextField;

class PersonalDetailsStep extends MultiFormStep
{
    private static $next_steps = OrganisationDetailsStep::class;

    public function getFields()
    {
        return FieldList::create(
            TextField::create('FirstName', 'First name'),
            TextField::create('Surname', 'Surname')
        );
    }
}

至少,每个步骤还必须有一个返回一个包含一些表单字段对象的FieldSetgetFields()方法。这些是表单将针对给定步骤渲染的字段。

请注意,我们的多表单还需要一个终点。这个步骤是最后的步骤,需要设置另一个变量,让多表单系统知道这是最后一步。

所以,如果我们假设我们的过程中的最后一步是OrganisationDetailsStep,那么我们可以这样做

use SilverStripe\MultiForm\Models\MultiFormStep;

class OrganisationDetailsStep extends MultiFormStep
{
    private static $is_final_step = true;

    ...
}

5. 运行数据库完整性检查

现在我们需要运行dev/build?flush=1,这样类就可以供SilverStripe清单构建器使用,并确保数据库与所有最新的表保持最新。所以你可以继续做。

注意:每次您添加一个新步骤时,您必须运行dev/build?flush=1,否则您可能会收到错误。

然而,我们忘记了一件事。我们需要在页面类型上创建一个方法,以便将表单渲染到给定的模板中。

所以,如果我们想将多表单作为$SurveyForm渲染到Page.ss模板中,我们需要在控制器上创建一个SurveyForm方法(函数)

use SilverStripe\CMS\Controllers\ContentController;
use SilverStripe\ORM\FieldType\DBHTMLText;

class PageController extends ContentController
{
    private static $allowed_actions = [
        'SurveyForm',
        'finished'
    ];

    public function SurveyForm()
    {
        return SurveyForm::create($this, 'SurveyForm');
    }

    public function finished()
    {
        return [
            'Title' => 'Thank you for your submission',
            'Content' => DBHTMLText::create('<p>You have successfully submitted the form!</p>')
        ];
    }
}

SurveyForm()函数将创建我们MultiForm子类的一个新实例,在本例中是SurveyForm。然后,这将设置每个步骤可用的所有表单字段、操作和验证,以及会话。

当然,您可以将SurveyForm方法放在您喜欢的任何控制器类中。

您的模板应该看起来像这样,以渲染表单

<div id="content">
    <% if $Content %> $Content <% end_if %> <% if $SurveyForm %> $SurveyForm <%
    end_if %> <% if $Form %> $Form <% end_if %>
</div>

在这种情况下,上面的模板示例是位于Layout目录中的模板的子模板。注意,我们还包括了$Form,这样标准表单仍然可以与我们的多步骤表单一起使用。

6. 添加步骤指示器

默认情况下,我们包含了一些基本进度指示器,这些指示器可能是开箱即用的。

截至撰写本文时,其中两个是

  • 进度列表(multiform/templates/Includes/MultiFormProgressList.ss)
  • 进度完成百分比(multiform/templates/Includes/MultiFormProgressPercent.ss)

它们设计用来独立使用,或与其他组件一起使用。例如,百分比可以与进度列表相结合,以显示完成状态。

要将这些功能包含在我们的多表单实例中,我们只需在模板中添加一个<% include %>语句。

例如

<% with $SurveyForm %> <% include MultiFormProgressList %> <% end_with %>

这意味着包含的模板将在返回的SurveyForm实例的作用域内渲染,而不是顶级控制器上下文。这为我们提供了显示步骤进度的数据。

组合起来,可能会是这样的

<div id="content">
    <% if $Content %> $Content <% end_if %> <% if $SurveyForm %> <% with
    $SurveyForm %> <% include MultiFormProgressList %> <% end_with %>
    $SurveyForm <% end_if %> <% if $Form %> $Form <% end_if %>
</div>

请随意尝试进度指示器。如果您需要特定于项目的功能,只需在您的项目模板目录内创建一个新的“包含”模板,并用它来替代。在MultiForm上可用的某些有用方法是

  • AllStepsLinear()(它还使用了getAllStepsRecursive()来生成步骤列表)
  • getCompletedStepCount()
  • getTotalStepCount()
  • getCompletedPercent()

默认进度指示器在模板中使用了上述功能。

要使用您自己的自定义方法,只需在MultiForm子类上创建一个新的方法。在这个例子中,SurveyForm是需要自定义的。您创建的这个新方法将随后在进度指示器模板中可用。

7. 从其他步骤加载值

有几个用例中,您希望根据另一个步骤的提交值预先填充一个值。有两个方法支持这一功能

  • getValueFromOtherStep()从会话中加载另一个步骤提交的任何值
  • copyValueFromOtherStep()节省了您反复添加相同代码行的工作。

以下是如何在步骤2中从步骤1填充电子邮件地址的示例

use SilverStripe\MultiForm\Models\MultiFormStep;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\EmailField;

class Step1 extends MultiFormStep
{
    private static $next_steps = Step2::class;

    public function getFields()
    {
        return FieldList::create(
            EmailField::create('Email', 'Your email')
        );
    }
}
use SilverStripe\MultiForm\Models\MultiFormStep;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\EmailField;

class Step2 extends MultiFormStep
{
    private static $next_steps = Step3::class;

    public function getFields()
    {
        $fields = FieldList::create(
            EmailField::create('Email', 'E-mail'),
            EmailField::create('Email2', 'Verify E-Mail')
        );

        // set the email field to the input from Step 1
        $this->copyValueFromOtherStep($fields, 'Step1', 'Email');

        return $fields;
    }
}

8. 完成工作

现在我们已经建立了一个结构,可以收集每一步的数据并成功通过,我们需要自定义最后一步结束时的行为。

在最后一步,调用finish()方法以最终确定我们完成的步骤中的所有数据。此方法可在MultiForm中找到。但是,我们无法自动保存每个步骤,因为我们不知道保存的位置。因此,我们必须在MultiForm的子类上编写一些代码,重载finish()以告知它结束时该做什么。

以下是一个我们可以在这里进行的示例

use SilverStripe\MultiForm\Forms\MultiForm;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\EmailField;

class SurveyForm extends MultiForm
{
   private static $start_step = PersonalDetailsStep::class;

   public function finish($data, $form)
   {
      parent::finish($data, $form);

      $steps = MultiFormStep::get()->filter([
        "SessionID" => $this->session->ID
      ]);

      if ($steps) {
         foreach ($steps as $step) {
            if ($step->ClassName == PersonalDetailsStep::class) {
               $member = Member::create();
               $data = $step->loadData();

               if ($data) {
                  $member->update($data);
                  $member->write();
               }
            }

            if ($step->ClassName == OrganisationDetailsStep::class) {
               $organisation = Organisation::create();
               $data = $step->loadData();

               if ($data) {
                  $organisation->update($data);

                  if ($member && $member->ID) {
                    $organisation->MemberID = $member->ID;
                  }

                  $organisation->write();
               }
            }
            // Shows the step data (unserialized by loadData)
            // Debug::show($step->loadData());
         }
      }

      $this->controller->redirect($this->controller->Link() . 'finished');
   }
}

9. 组织数据模型

上述示例中提到的Organisation类目前不存在(与负责银Stripe成员组的现有Member()类不同),因此我们需要创建它

此示例已选择作为单独的数据对象,但您可能希望更改代码并将数据添加到Member类中。

use  SilverStripe\ORM\DataObject;

class Organisation extends DataObject
{
    private static $db = [
        // Add your Organisation fields here
    ];
}

警告

如果您正在处理敏感数据,最好在表单成功提交后立即删除会话和步骤数据。

您可以通过在MultiForm子类上调用此方法来删除它

$this->session->delete();

这将遍历每个步骤并删除它们。

自定义

虽然多表单系统提供了一套合理的默认设置,但它并不神奇地为您做所有事情,这意味着您需要自定义其某些方面。以下是一些可以自定义的有用方法(尽管您可以从MultiForm和MultiFormStep中技术上重载任何可用功能)

模板

这个系统的最好之处在于可以自定义每个表单步骤模板。

按照顺序,当您有一个包含多表单渲染的页面时,它将根据MultiForm类的上下文顺序选择要渲染的模板

  • $this->getCurrentStep()->class(当前步骤类)
  • MultiFormStep
  • $this->class(您的MultiForm子类)
  • MultiForm
  • Form

很可能,你希望在表单渲染时第一个就可用。为此,你可以开始在你的项目目录中放置模板到 templates/Includes 目录。你需要将它们的名称与每个步骤的类名相同。例如,如果你想 MembershipForm,一个 MultiFormStep 的子类有它自己的模板,你会在那个目录中放入 MembershipForm.ss,并运行 ?flush=1

如果你想查看如何自定义表单步骤的现有模板,请查看框架模块中找到的 Form.ss。使用该模板作为你项目模板中新 MembershipForm.ss 模板的基础。

有关更多信息,请查看表单文档

getNextStep()

如果你想要覆盖下一步(例如,如果你想根据用户在步骤中的输入选择不同的下一步),你可以覆盖任何给定步骤的 getNextStep() 来手动覆盖下一步应该是什么。以下是一个示例

class MyStep extends MultiFormStep
{
   ...

   public function getNextStep()
   {
      $data = $this->loadData();
      if(isset($data['Gender']) && $data['Gender'] == 'Male') {
         return TestThirdCase1Step::class;
      } else {
         return TestThirdCase2Step::class;
      }
   }

   ...
}

验证

为了逐个步骤定义验证,请定义 getValidator() 并返回一个验证器对象,如 RequiredFields - 有关表单验证的更多信息请查看 :form

例如:

class MyStep extends MultiFormStep
{
   ...

   public function getValidator()
   {
      return RequiredFields::create(array(
         'Name',
         'Email'
      ));
   }

   ...
}

finish()

finish() 是过程中的最终调用。在这个步骤中,所有表单数据很可能会反序列化,并以开发者认为合适的方式保存到数据库中。默认情况下,我们在 MultiForm 上有一个 finish() 方法,它将最后一步表单数据序列化到数据库,这就是全部。

finish() 应该被覆盖到你的 MultiForm 子类中,并且应该首先调用 parent::finish(),否则最后一步的表单数据将不会被保存。

例如

use SilverStripe\Dev\Debug;
use SilverStripe\MultiForm\Forms\MultiForm;
use SilverStripe\MultiForm\Models\MultiFormStep;

class SurveyForm extends MultiForm
{
   private static $start_step = PersonalDetailsStep::class;

   public function finish($data, $form)
   {
      parent::finish($data, $form);

    $steps = MultiFormStep::get()->filter(['SessionID' => $this->session->ID]);

      if($steps) {
         foreach ($steps as $step) {
            // Shows the step data (unserialized by loadData)
            Debug::show($step->loadData());
         }
      }
   }
}

上面的代码是一个简单的示例,它只是从数据库中检索所有已保存的步骤。进一步的改进可能包括仅当数据(序列化的原始表单数据)被设置时获取步骤,因为上面的示例不尊重步骤的分支(给定表单步骤上的多个下一步)。

最佳实践

提交后删除会话

如果你处理的是敏感数据,例如信用卡字段或不应在会话数据库中保留的个人字段,那么在用户提交后立即删除这些数据是一个好主意。

这可以通过在你的 MultiForm 子类中的 finish() 方法的末尾添加以下行轻松实现。

$this->session->delete();

过期旧会话数据

MultiForm 模块中包含一个名为 MultiFormPurgeTask 的类。这个任务可以用来定期清除过期会话数据。到期日期可以自定义,默认为创建会话后 7 天删除会话。

你可以通过使用 http://mysite.com/dev/tasks/MultiFormPurgeTask?flush=1 从 URL 运行此任务。

MultiFormPurgeTask 是 BuildTask 的子类,因此可以使用 SilverStripe CLI 工具 运行。

在基于 UNIX 的机器上自动运行此任务的一种方法是使用 cron。

TODO

  • 有关如何使用 $form->saveInto() 与 MultiForm 的代码示例,因为在 finish() 上下文中 $form 没有所有步骤

  • 允许用户点击链接,并给他们发送包含当前状态的电子邮件,这样他们就可以回到并使用表单从他们离开的地方继续

  • 可能允许不同的方法来持久化数据,例如使用浏览器会话缓存而不是数据库。

  • 以不同的方式呈现 URL 以识别每个步骤。

  • 允许在每个步骤中自定义 prev()next()。目前您只能为整个 MultiForm 子类进行自定义。有一种按步骤自定义的方法,可以简要描述为一个小食谱。

  • 更详细的解释,以及如何创建分支多步骤表单的食谱示例。例如,点击不同的操作将带您进入由 $next_steps 定义的替代下一步。

相关