naucon/form

该包为PHP提供表单组件,用于处理和验证HTML表单。还包括一个表单辅助工具,用于将表单渲染为HTML标记(PHP或Smarty模板)。

5.0.0 2024-07-16 09:51 UTC

This package is auto-updated.

Last update: 2024-09-16 10:12:34 UTC


README

关于

该包为PHP提供表单组件,用于处理和验证HTML表单。还包括一个表单辅助工具,用于将表单渲染为HTML标记(PHP或Smarty模板)。

在网站开发中最常见的任务之一是表单。此包可以帮助您以简单的方式处理和验证表单,以便维护和安全。

该包可以集成到任何PHP网站应用程序中(至少PHP5.5+)。

功能

  • 通过getter和setter将表单数据绑定到实体
  • 通过实体中定义的验证规则验证表单数据
  • 通过同步器令牌(也称为安全令牌或XSRF/CSRF令牌)来保护表单,以防止XSRF/CSRF漏洞
  • 支持表单集合
  • 翻译表单标签和表单违规
  • 表单辅助工具以渲染HTML标记
  • 使用输出过滤渲染HTML表单以防止XSS漏洞
  • 表单辅助工具的Smarty插件

兼容性

  • PHP 7.1 - 7.4

安装

通过Composer安装最新版本

composer require naucon/form

示例

启动内置的web服务器以查看示例

cd examples
php -S 127.0.0.1:3000

在浏览器中打开url

http://127.0.0.1:3000/index.html

开始使用

1. 实体

首先,我们需要一个实体。实体可以是一个具有getter和setter的普通PHP类。类的属性必须通过getter和setter或公开访问。提交的表单数据将映射到该实体的一个实例。

<?php
	class User
	{
	    protected $username;
	    protected $email;
	    protected $newsletter;

	    public function getUsername()
	    {
		    return $this->username;
		}

		public function setUsername($username)
		{
		    $this->username = $username;
		}

	    public function getEmail()
	    {
	        return $this->email;
	    }

		public function setEmail($email)
		{
		    $this->email = $email;
		}

	    public function getNewsletter()
	    {
		    return $this->newsletter;
		}

	    public function setNewsletter($newsletter)
	    {
		    $this->newsletter = $newsletter;
		}
	}
}

创建实体的一个实例并将其分配给您的表单。

require_once 'User.php';
$user = new User();

2. 表单绑定

接下来,我们获取表单管理器类的实例并创建一个表单实例,该实例执行实际的表单处理。

将实体作为第一个参数添加到表单类中。

	use Naucon\Form\FormManager;
	$formManager = new FormManager();
    $form = $formManager->createForm($user, 'yourform');

作为第二个参数提供表单的名称或命名空间。在上面的示例中,我们使用yourform。这个唯一的名称类似于命名空间或键,以确保您的表单不会与其他表单冲突。

CSRF-Token需要一个会话。所以请确保您已经启动了会话。

<?php
	session_start();

	use Naucon\Form\FormManager;
	$formManager = new FormManager();
    $form = $formManager->createForm($user, 'yourform');
	...

现在我们可以绑定提交的表单数据。因此,我们调用bind()方法并添加提交的表单数据,例如$_POST或任何其他数组作为参数。为了确保表单正常工作,您应该始终调用bind()方法,即使没有提交表单数据。使用isBound()您可以验证提交的表单数据是否已正确绑定到实体。

如果实体已绑定,您可以使用实体执行任何其他操作,例如保存到数据库。

	$form->bind(isset($_POST) ? $_POST : null);

	if ($form->isBound()) {
	    // some action, like saving the data to database
	}

确保关闭会话以将会话数据写入会话存储。

	// here rendered the form html!

	session_write_close();
?>

3. XSRF/CSRF保护

bind()提交时,只有在提交了同步器令牌(也称为安全令牌或xsrf/csrf令牌)并且该令牌有效时,才会处理数据。此令牌通过在表单呈现时设置随机令牌到会话中,防止XSRF/CSRF攻击。在表单中,该令牌添加到隐藏字段中。当表单数据提交时,该令牌是有效负载的一部分,并将与会话中先前存储的令牌进行比较。只有当这两个令牌相同,绑定才会被处理。

<form method="post">
	<input type=text name="_csrf_token" value="<?php echo $form->getSynchronizerToken(); ?>" />

标记如下所示

<form method="post" id="yourform">
	<input type="hidden" name="_csrf_token" value="67187c7e5abeb31b93339ab5cbb263b6" />

可以通过调用带有参数falsesetSynchronizerTokenValidation()方法来禁用XSRF/CSRF保护。

$formManager->setSynchronizerTokenValidation(false);

或者

$form->setSynchronizerTokenValidation(false);

4. 验证

为了验证提交的表单数据,我们使用Symfony验证组件。约束(验证规则)在静态方法loadValidatorMetadata()中定义,可以是yml文件、xml文件或实体中的注解。

如何定义约束和可用的验证器,请参阅Symfony验证文档:https://symfony.ac.cn/doc/2.8/validation.html

在我们的例子中,我们在User实体中添加了一个静态方法loadValidatorMetadata()

public static function loadValidatorMetadata(ClassMetadata $metadata)
{
    $metadata->addPropertyConstraint('username', new Assert\NotBlank());
    $metadata->addPropertyConstraint('username', new Assert\Length(
        array('min' => 5, 'max' => 50)
    ));
    $metadata->addPropertyConstraint('email', new Assert\Email());
    $metadata->addPropertyConstraint('age', new Assert\Type('numeric'));
}

您还可以使用yml、xml文件或注解。有关如何定义约束的更多信息,请参阅Symfony验证文档:https://symfony.ac.cn/doc/current/components/validator.html

    $options['config_paths'] = array(__DIR__ . '/Resources/config/');

    $formManager = new FormManager($options);

如果您需要为特定用例排除某些验证约束,或者您希望仅通过配置应用条件子集,则可以使用验证组来完成

    $options['validation_groups'] = array('Default', 'registration');

如果在表单选项中提供了验证组名称数组,则只有在验证表单时标记了这些组名称的约束才会被应用。有关验证组配置的更多信息,请参阅https://symfony.ac.cn/doc/current/validation/groups.html文档。

在调用bind()isBound()之后,我们调用isValid()。换句话说,我们在绑定实体后进行验证。

	$form->bind(isset($_POST) ? $_POST : null);

	if ($form->isBound() 
		&& $form->isValid()) {
	    // some action, like saving the data to database
	}
	
	// here rendered the form html!

	session_write_close();
?>

验证错误存储在FormError实例中。它们可以通过以下方法访问

$form->getErrors()
$entityContainer->hasError('username')
$entityContainer->getError('username')
$entityContainer->getError('username')->getMessage();

$entityContainerEntityContainer的实例。它不是实体UserEntityContainer包含实体。可以通过调用EntityContainer::getEntity()来访问。

如果您的表单中只有一个实体,您可以通过调用来访问EntityContainer

$form->getFirstEntityContainer()

如果您的表单包含多个实体,您可以遍历EntityContainer

foreach ($form->getEntityContainerIterator() as $entityContainer) {
    ...
}

实体容器也可以通过表单实例进行迭代。

foreach ($form as $entityContainer) {
    ...
}

5. 翻译

为了翻译表单标签和验证违规,我们使用symfony翻译组件。

要更改当前语言,请调用setLocales

$form->getTranslator()->setLocale('de_DE');

您可以在配置中指定当前语言和备用语言

$options = [
    'locale' => 'en_EN',
    'fallback_locales' => array('en_EN'),
];

$formManager = new FormManager($options);

默认情况下,将加载表单和symfony组件的约束的默认翻译。要添加表单标签或自己的约束的翻译,您必须指定一个或多个路径,以查找这些翻译

$paths = array(realpath(__DIR__) . '/Resources/translations');
$options['translator_paths' => $paths];

$formManager = new FormManager($options);

6. 表单渲染

使用表单助手

使用表单助手,您可以轻松渲染HTML表单字段、标签、按钮,无需担心字段名称和值。

以下表单助手可用,并且可以通过FormHelper实例或smarty插件访问。

  • 选择
    • "checkbox"
    • "radio"
    • "select"
  • 字段
    • "error"
    • "hidden"
    • "label"
    • "password"
    • "text"
    • "textarea"
  • 标签
    • "errors"
    • "reset"
    • "submit"

限制:目前表单助手不考虑不同的HTML标准,如HTML4.01、xhtml、HTML5等。还有一些HTML标记符号缺失。

使用PHP模板的FormHelper

首先,您必须创建FormHelper类的实例。表单助手需要一个Form实例的实例。

<?php
	use Naucon\Form\FormHelper;
    $formHelper = new FormHelper($form);

表单助手包含以下方法来渲染表单元素

  • formStart($method='post', $action=null, $enctype=null, $options=array())
  • formEnd()
  • formField($helperName, $propertyName, array $options=array())
  • formChoice($helperName, $propertyName, $choices, array $options=array())
  • formTag($helperName, $content=null, array $options=array())

使用FormHelper渲染表单的示例

echo $formHelper->formStart();
echo $formHelper->formTag('errors');

foreach ($formHelper as $entityContainer) {
?>
    <fieldset>
        <?php echo $formHelper->formField('label', 'username'); ?>
        <?php echo $formHelper->formField('text', 'username', array('maxlength' => 32)); ?>
    	<?php echo $formHelper->formField('error', 'username'); ?>
        <br/>
        <?php echo $formHelper->formField('label', 'email'); ?>
        <?php echo $formHelper->formField('text', 'email'); ?>
        <?php echo $formHelper->formField('error', 'email'); ?>
        <br/>
        <?php echo $formHelper->formField('label', 'newsletter'); ?>
        <?php echo $formHelper->formField('checkbox', 'newsletter', 1); ?>
        <?php echo $formHelper->formField('error', 'newsletter'); ?>
        <br/>
    </fieldset>
    <fieldset>
        <?php echo $formHelper->formTag('submit', 'Submit'); ?>
        <?php echo $formHelper->formTag('reset', 'Reset'); ?>
    </fieldset>
<?php
}
echo $formHelper->formEnd();
使用Smarty3模板的FormHelper

首先,我们必须将表单助手smarty插件添加到smarty中。

$smarty->setPluginsDir('vendor/Naucon/Form/Helper/view/smarty');

然后,我们必须将表单实例分配给smarty。

$smarty->assign('form', $form);

表单包包含以下块和功能插件

如何在smarty模板中渲染表单的示例

{ncform from=$form}
	{ncform_tag type='errors'}
	<fieldset>
		{ncform_field type='label' field='username'}
		{ncform_field type='text' field='username' maxlength=32}
		{ncform_field type='error' field='username'}
		<br/>
		{ncform_field type='label' field='email'}
		{ncform_field type='text' field='email'}
		{ncform_field type='error' field='email'}
		<br/>
		{ncform_field type='label' field='newsletter'}
		{ncform_choice type='radio' field='newsletter' value=1}
		{ncform_field type='error' field='newsletter'}
		<br/>
	</fieldset>
	<fieldset>
		{ncform_tag type='submit' value='Submit'}
		{ncform_tag type='reset' value='Reset'}
	</fieldset>
{/ncform}

上述示例渲染以下HTML标记

<form method="post" id="yourform">
	<input type="hidden" name="_ncToken" value="67187c7e5abeb31b93339ab5cbb263b6" />
	<fieldset>
		<label for="yourform_username">username</label>
		<input type="text" name="yourform[username]" value="" id="yourform_username" maxlength="32" />
		<br/>
		<label for="yourform_email">email</label>
		<input type="text" name="yourform[email]" value="" id="yourform_email" />
		<br/>
		<label for="yourform_newsletter">newsletter</label>
		<input type="checkbox" name="yourform[newsletter]" value="1" id="yourform_newsletter" />
		<br/>
	</fieldset>
	<fieldset>
		<input type="submit" value="Submit" />
	 	<input type="reset" value="Reset" />
	</fieldset>
</form>

也可以使用{ncform_field}元素来输出输入字段的仅部分,例如id、name、value

<input type="text" name="{ncform_field type='name' field='email'}" value="{ncform_field type='value' field='email'}" id="{ncform_field type='id' field='email'}" maxlength="32" />
FormHelper与Twig模板

首先,您需要注册twig扩展

use Naucon\Form\Helper\Twig\NcFormExtension;
$twig = new \Twig\Environment($loader);
$twig->addExtension(new NcFormExtension());

示例

 {{ ncform_start(form, method='post', action='some-action', enctype='some type', {furtherOptions:'option'}) }}
 {{ ncform_field(form, 'text', 'activation_code', { style: 'some style', id: 'some id', value: 'some value', maxlength: 'some lenght', class: 'css class', required: 'required', 'data-attribute': 'some attribute'}) }}
 {{ ncform_end(form) }}
没有表单辅助器

没有表单辅助器,您必须自己将表单字段添加到HTML标记中。请务必设置正确的name属性 - 否则,表单数据无法绑定。

name属性以表单名称yourform开头,后面跟着括号内的属性名称[username]

<input type="text" name="yourform[username]" value="" />
<input type="text" name="yourform[email]" value="" />
<input type="checkbox" name="yourform[newsletter]" value="1" />

在具有多个实体的表单中,name属性必须还包含括号内的实体名称[0],位于表单和属性名称之间

<input type="text" name="yourform[0][username]" value="" />
<input type="text" name="yourform[1][username]" value="" />

或者,您也可以要求EntityContainer实例提供name属性和值 - 如果您喜欢。

<label>Username</label>
<input type="text" name="<?php echo $entityContainer->getFormName('username'); ?>" value="<?php echo $entityContainer->getFormValue('username'); ?>" />
<?php
    if ($formEntity->hasError('username')) {
        echo 'WRONG: ' . $formEntity->getError('username')->getMessage();
    }
?>
<br/>

<label>First name</label>
<input type="text" name="<?php echo $entityContainer->getFormName('firstname'); ?>" value="<?php echo $entityContainer->getFormValue('firstname'); ?>" />
<?php
    if ($formEntity->hasError('firstname')) {
        echo 'WRONG: ' . $formEntity->getError('firstname')->getMessage();
    }
?>
<br/>

表单集合

要处理(绑定和验证)多个表单,您可以使用表单集合。可以通过从FormManager调用createFormCollection()来创建表单集合。第一个参数添加实体数组。第二个参数是表单名称。实体不必具有相同的名称 - 但在以下示例中我们这样做。

$products = array(new Product(), new Product(), new Product());

use Naucon\Form\FormManager;
$formManager = new FormManager();
$form = $formManager->createFormCollection($products, 'yourforms');
$form->bind($_POST);

要渲染表单,我们像这样遍历表单实体

foreach ($form->getEntityContainerIterator() as $entityContainer) {

可以使用collection_type选项来指定表单集合的行为。值可以是以下选项之一:FormCollectionInterface::COLLECTION_TYPE_ALL、FormCollectionInterface::COLLECTION_TYPE_MANY、FormCollectionInterface::COLLECTION_TYPE_ONE。

  • FormCollectionInterface::COLLECTION_TYPE_ALL:有效载荷必须绑定所有表单实体
  • FormCollectionInterface::COLLECTION_TYPE_ANY:有效载荷可以绑定任何匹配的表单实体
  • FormCollectionInterface::COLLECTION_TYPE_ONE:有效载荷必须绑定一个定义的表单实体。哪个定义通过表单选项。
  • FormCollectionInterface::COLLECTION_TYPE_MANY:有效载荷必须绑定一个或多个表单实体。哪些定义通过表单选项。

集合类型之一

例如,您有两个具有不同字段集的表单,用户必须从中选择。我们会使用COLLECTION_TYPE_ONE或COLLECTION_TYPE_MANY。添加一个带有集合类型的第三个参数。

$creditCardEntity = new CreditCard();
$directDebitEntity = new DirectDebit();
$paymentMethods = array('cc' => $creditCardEntity, 'dd' => $directDebitEntity);

use Naucon\Form\FormManager;
use Naucon\Form\FormCollectionInterface;
$formManager = new FormManager();
$forms = $formManager->createFormCollection($paymentMethods, 'payment_forms', FormCollectionInterface::COLLECTION_TYPE_ONE);
$forms->bind($_POST);

对于每个表单实体,我们添加一个单选按钮或复选框,如下所示

foreach ($forms->getEntityContainerIterator() as $entityContainer) {

    if ($entityContainer->getName()=='cc') {
?>
        <fieldset>
            <input type="radio" name="<?php echo $forms->getFormOptionName() ?>" value="<?php echo $entityContainer->getName() ?>" <?php if ($forms->isFormOptionSelected($entityContainer->getName())) echo ' checked="checked"' ?> />
            <label>Credit Card</label>
            <br/>
        </fieldet>

使用辅助器,它将看起来像这样

$formHelper = new FormHelper($form);

echo $formHelper->formStart();
echo $formHelper->formTag('errors');

foreach ($formHelper as $formEntity) {
    if ($formEntity->getName()=='cc') {
?>
    <fieldset>
    	<?php echo $formHelper->formOption('radio', 'cc' ); ?>

在smarty中,它将看起来像这样

{ncform from=$form}
    {ncform_tag type='errors'}
    {foreach from=$formHelper item="formEntity"}
        {if $formEntity->getName()=='cc'}
            <fieldset>
                {ncform_option type='radio' choice='cc'}

要定义默认表单选项,请调用FormCollection::setFormOptionDefaultValues(array $defaultValues)FormCollection::addFormOptionDefaultValues($formEntityName)

$forms->setFormOptionDefaultValues(array('cc'))

或者

$forms->addFormOptionDefaultValues('dd');

配置

可以通过在创建表单管理器或表单实例时添加参数来配置表单。

$formManager = new FormManager($options);

或者

$form = $formManager->createForm($entity, 'testform', $options);

默认选项如下所示

$options = [
    'csrf_parameter' => '_csrf_token',
    'csrf_protection' => true,
    'collection_type' => FormCollectionInterface::COLLECTION_TYPE_ALL,
    'locale' => 'en_EN',
    'fallback_locales' => array('en_EN'),
    'translator_paths' => array(),
    'config_paths' => array()
];
  • csrf_parameter = 指定csrf隐藏输入字段的name属性
  • csrf_protection = 启用/禁用csrf保护
  • collection_type = 查看表单集合文档
  • locale = 默认语言
  • fallback_locales = 如果在当前语言中找不到翻译,则回退语言
  • translator_paths = 指定翻译文件的位置
  • config_paths = 指定验证文件的位置

表单钩子

通过表单实体的钩子,您可以影响表单处理。在以下示例中,我们使用预验证钩子从已绑定的属性中删除空格。我们还向后验证钩子添加了额外的验证。

class Address
{
    protected $streetName;
    protected $streetNumber;
    protected $postalCode;
    protected $town;
    protected $country;

   ...

    public function prevalidatorHook(FormHook $formHook)
    {
        $this->streetName = trim($this->streetName);
        $this->streetNumber = trim($this->streetNumber);
        $this->postalCode = trim($this->postalCode);
        $this->town = trim($this->town);
        $this->country = trim($this->country);
    }

    public function postvalidatorHook(FormHook $formHook)
    {
        if ($this->postalCode != '54321') {
            $formHook->setError('postal_code', 'has unexpected value');
        }
    }

    ...

钩子基本上是表单实体中的一个具有定义的命名转换的方法。如果存在于表单实体中,则在处理时调用它。

  • postbindHook(在实体绑定之后调用)
  • prevalidatorHook(在验证实体之前调用)
  • postvalidatorHook(在实体验证后调用)

钩子接收一个FormHook实例,该实例允许您以受限的方式访问实体容器以更改错误

  • getErrors()
  • hasErrors()
  • getError($key)
  • hasError($key)
  • setError($key, $message)

路线图

  • 解耦翻译、验证器与配置(更小,更多选项)
  • 测试使用alpha实体名称/集合的标签
  • 测试编码值
  • 将表单错误重命名为违规
  • 表单集合选项的违规(包括翻译)
  • csrf验证的违规(包括翻译)
  • 绑定和验证的状态机
  • 添加smarty块插件以限制表单输出为第一个或最后一个实体(用于表格和按钮)。
  • 添加smarty块插件以限制到定义的实体(表单集合)。