ryunosuke/chmonos

兼容php/js验证库

v2.1.1 2024-09-28 09:51 UTC

README

描述

  • 使用宣言性描述在php/js中应用相同的规则进行验证
  • 不仅简单的验证,还可以进行“如果那个值是那个值,那么这个值是那个值”的条件设置
  • 与输入的渲染和集成,因此通常不需要值回退或标签的编写

安装

{
    "require": {
        "ryunosuke/chmonos": "dev-master"
    }
}

演示

cd /path/to/chmonoce
composer example
# access to "http://hostname:3000"

使用方法

基本

因为进行js检查,所以需要一些静态文件。 script 目录以下适用。

  • validator.js: js检查的核心js文件
  • validator-error.js: 错误显示的js文件
  • validator-error.css: 错误显示的css文件

此外,js检查的错误显示是完全独立的,因此可以相对容易地进行替换。 上面的两个文件负责错误显示。通过更改这些文件可以自定义错误显示。

在php方面,通过以下方式以数组形式声明性定义规则,创建Form实例,渲染即可获得<form>标签。 在此表单中,通过submit使用validate方法进行输入值的验证(js验证通常在屏幕内自动进行)。

表单有一些选项。

// Form インスタンスを作成
$form = new Form([/* ルールについては後述 */], [
    'nonce'      => '',           // 生成される script タグの nonce 属性を指定します(CSP 用です。不要なら指定不要です)
    'inputClass' => Input::class, // UI 要素の検証やレンダリングに使用する Input クラス名を指定します。基本的には指定不要です
    'vuejs'      => false,        // レンダリングが vuejs に適した形になります
]);

// POST でバリデーション
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $form->validateOrThrow($_POST);
}

// レンダリング
$form->form([/* form の属性*/]); // form 開きタグ
$form->form();                   // form 閉じタグ(引数無しで閉じる)

这只是一个基本的使用方法。 example中几乎使用了所有功能,请参考。

规则的定义

将以下关联数组传递给表单的规则。 虽然有很多,但大多数都有默认值,所以只需要指定一部分(最终,空数组也可以工作)。

提供null而不是数组时,该元素将被忽略,就像它不存在一样。

$form = new Form([
    'element_name1' => [
        'title'                 => '',         // 画面に表示される際の論理名を指定します
        'normalize'             => [],         // 入力値のプレフィルタです(後述)
        'condition'             => [],         // 入力条件を配列で指定します(後述)
        'options'               => [],         // checkbox や radio などの選択肢を配列で指定します。キーが値、値が表示文字列です
        'invalids'              => [],         // 無効 options を指定します(後述)
        'invalid-option-prefix' => "\x18",     // 無効 options を表すプレフィックスを指定します(後述)
        'datalist'              => [],         // datalist を生成します。現状では「自動 Condition が行われない options」という仕様です
        'event'                 => ['change'], // js チェックが走るイベント名を指定します(後述)
        'propagate'             => [],         // js チェックのイベントの伝播先を指定します(後述)
        'message'               => [],         // エラーメッセージを指定します(後述)
        'phantom'               => [],         // 疑似データソースを指定します(後述)
        'attribute'             => [],         // その他のカスタムデータです。html レンダリングされる際のタグ属性にもなります
        'inputs'                => [],         // ネスト構造を再帰的に記述します(後述)
        'checkmode'             => [           // サーバー・クライアントサイドのどちらで検証を行うか指定します(非推奨。将来的に削除されます)
            'server' => true,        // server を true にするとサーバーサイドで検証が行われます
            'client' => true,        // client を true にするとクライアントサイドで検証が行われます
        ], 
        'wrapper'               => null,       // input 要素をラップする span class を指定します。未指定だとラップしません
        'grouper'               => null,       // input 要素郡(同じ名前の radio/checkbox)をラップする span class を指定します。未指定だとラップしません
        'invisible'             => false,      // 不可視状態で検証を行うかを指定します
        'ignore'                => false,      // 検証や依存関係の値としては全て有効ですが最終的な結果から除くかを指定します(後述)
        'trimming'              => true,       // 値のトリミングを行うかを指定します
        'needless'              => [],         // 必須ではない場合の属性を指定します(後述)
        'autocond'              => true,       // 一部の入力条件を自動設定するかを指定します(後述)
        'delimiter'             => null,       // 配列⇔文字列の区切り文字を指定します(後述)
        'multiple'              => null,       // 複数入力か否かを指定します(後述)
        'dependent'             => true,       // 自動伝播設定を行うかを指定します(後述)
        'pseudo'                => true,       // ダミー hidden を生成するかを指定します(後述)
        'nullable'              => false,      // 値が null の場合でも default 値を使用するか指定します
        // 'default'               => null,       // 値が飛んでこなかった時のデフォルト値を指定します(後述)
        // 'fixture'               => null,       // Form/Context の getFixture が呼ばれた時に固定値として使用されます(後述)
    ],
    'element_name2' => [/* 構造は同じ */],
    // ・・・
]);

有一些选项比较复杂,所以会进行深入探讨。

normalize

在验证之前,值会被回调到闭包中。

仅php的设置。js侧不使用。基本上,只在服务器端进行值过滤或转换,但没有这样的用途。

condition

指定输入条件。

指定是条件类 => [参数]的形式。

$rule = [
    'condition' => [
        'Requires' => [],
        'Decimal'  => [3, 2],
        'Range'    => ['min' => -200, 'max' => 200],
    ],
];

这样,“输入必填”、“小数xxx.yy”、“范围-200 ~ 200”表示。

有几种指定方式,以下都是正确的指定。

$rule = [
    'condition' => [
        'Range'      => [-200, 200],          // 上述の条件クラス => [パラメータ]
        Range::class => [-200, 200],          // FQSEN 指定
        '-200~+200'  => new Range(-200, 200), // インスタンス直接指定
    ],
];

基本上是条件类 => [参数]。这很容易处理后续处理。 但是,“想要使用多个相同的Condition(如Compare)作为参数”时,无法处理这种情况,因此需要选择其他指定方法。

各个Condition的参数请参考源代码或DocComment。 构造函数的参数直接使用。

带有Child后缀的Condition是type=array专用的。 带有此后缀的Condition基本上用于设置自身行/列的列的串刺性条件。 例如,“行元素包含特定值”、“值重复检查”等。

如果值是null且键是数值,则该元素将被忽略,就像它不存在一样。

message

自定义错误消息。

错误消息具有“form => input => condition => type => message”的层次结构(表单具有输入,输入具有条件,条件具有错误数组)。 指定此结构的type => message部分。

$rule = [
    'message' => [
        'RequireInvalidText'       => '入力必須です',
        Requires::INVALID_MULTIPLE => '選択必須です',
    ],
];

这样就可以自定义错误消息。 如果不指定,则使用默认消息(虽然尽量设置得像样)。 上面是直接指定,下面是常数指定,但推荐使用常数指定以备更改。

但是,这种指定会在“想要使用多个相同的Condition(如Compare)作为参数”的情况下导致所有消息都相同。 如果想要单独更改消息,则可以通过传递嵌套数组来从condition => type => message的层次结构指定,从而可以指定。

$rule = [
    'message' => [
        'condition1' => [
            'RequireInvalidText'       => '入力必須です1',
            Requires::INVALID_MULTIPLE => '選択必須です1',
        ],
        'condition2' => [
            'RequireInvalidText'       => '入力必須です2',
            Requires::INVALID_MULTIPLE => '選択必須です2',
        ],
    ],
];

condition1或condition2是condition数组键的相同。

此外,可以在消息中嵌入${_name}这样的字符串,并将其替换为同名的类字段。 例如,内置的Decimal Condition是“请输入整数部分为${_int}位,小数部分为${_dec}位以下的数字”。

这种语法模仿了js的模板字面量,因此可以嵌入如${implode(",", _types)}这样的表达式。 一些Condition中使用它,请参考。

另外,使用此键更改错误消息只会影响该元素。 由于js的容量减少,结构上“有公共消息,如果更改则仅使用个别消息”的机制。 如果要更改整个公共消息,则可以重新编写基本类或使用AbstractCondition::setMessages

错误消息不进行任何html转义,请注意。 转义是错误显示插件的职责,本体侧不接触这种设计。

options

指定checkbox、radio、select等的选项。 1级嵌套被视为optgroup。

指定options时,由于autocond功能,将自动附加InArray条件。 指定invalids时,由于autocond功能,将自动附加NotInArray条件。 在这种情况下,标签文本将被此处指定覆盖。

指定invalid-option-prefix时,带有该前缀的元素将以invalids的行为。 将元素的前缀删除。

键是值(1级嵌套被视为optgroup的标题)。 值是标签,但可以接受stdClass。 在stdClass的情况下,labelinvalid将被特别处理,label用于标签,invalid用于上述中指定的invalid-option-prefix的值。 其他属性是html属性值。 因此,以下代码(几乎是)等价。

$rule = [
    'options'    => [
        10 => '電話',
        20 => 'メール',
        30 => "FAX",
    ],
    'invalids'   => [
        30 => "FAX(無効状態)", // これは invalids 機能により NotInArray 条件で不正な値とみなされる
    ],
];
$rule = [
    'options'    => [
        10 => '電話',
        20 => 'メール',
        30 => "\x18FAX", // これは invalid-option-prefix 機能により NotInArray 条件で不正な値とみなされる
    ],
];
$rule = [
    'options'    => [
        10 => (object) ['label' => '電話', 'data-attr' => '10'],
        20 => (object) ['label' => 'メール', 'data-attr' => '20'],
        30 => (object) ['label' => 'FAX', 'data-attr' => '30', 'invalid' => true], // これは autocond 機能により NotInArray 条件で不正な値とみなされる
    ],
];

不同之处在于option可以添加data-attr这样的属性值。

为什么需要这样的规范,是因为“当前选择项由于某种原因消失了”这样的规范可以忍受。 例如,从主数据中删除,从数据库中逻辑删除等(上述中的30:FAX这样的选择项被删除)。

html的select在候选项不正确的情况下(value为30:FAX但30:FAX没有option的状态)有“自动选择最上面的”这样的规范,如果不考虑这一点直接实现,打开画面后只需保存就可以将值改为10:电话,这种事情就会发生。 radio/checkbox不会发生这样的事情,但在许多情况下会导致必需错误,并且当前的选择项是什么的信息会丢失,便利性会降低。 在这种情况下,通过invalids提供无效候选项或通过prefix提供,同时通过NoInArray进行错误处理,可以保持当前选择项的同时进行错误处理。

如果选择项是公共的、类似于主数据的情况,则invalid-option-prefix很好。 通过在定义位置附加"\x18"可以全局无效化。 如果从数据库等动态构建,则使用invalids很好。 在获取options的同时获取invalids可以无效化。

event

指定js验证运行的事件名。 基本是change或input的二选一。 特殊规范是,可以像jQuery命名空间一样使用change.modifier并通过点分隔符添加修饰符来为该事件添加特殊属性。

noerror

如果指定为event.noerror,则“当出现错误时不会触发”这种行为。 反之,“仅在错误解除时触发”。

例如,['change', 'input.noerror']将“更改时会出现错误,但输入时不会出现错误”。 这是为了避免在输入过程中出现错误时屏幕变得嘈杂或打扰而采取的措施。

尝试输入电子邮件地址时,在输入到时出错确实很烦人。换句话说,无法添加input。另一方面,当需要修正hogera@example..com的输入错误时,在删除.时错误就会消失,这会更友好。换句话说,需要添加input。在这种情况下,使用['change', 'input.noerror']可以实现期望的行为。

norequire

使用event.norequire时,表示“在因依赖关系(propagate, dependent 也参考)引起的验证时,不输出必要错误”的行为。

例如,在希望连络方法: ○电话 ○FAX / 电话号码[      ], FAX[      ]这样的UI中,在勾选电话的瞬间电话号码就会变成必要错误,这是过早的。因为用户还没有输入,甚至还没有意识到有一个电话号码这个项目。换句话说,使用propagate, dependent解除依赖关系后,普通的验证事件也不会传播。这个修饰符仅用于抑制必要错误(Requires)。

这是一个相当狭义用途并且有破坏性变更的计划。具体来说,有计划更改“如果DOM顺序、坐标在后面则不进行验证”的行为。这是在作者需要时新创建的修饰符,不是面向大众的,所以原则上不推荐使用。

propagate

指定在执行验证的元素时一起进行验证的元素。

例如,在像☑ 必须输入: [      ]这样的UI中,当勾选复选框时,可以使得事件从复选框传播到文本框。但是,由于规则会从一定程度上自动设置,所以通常不需要显式指定。

有关这一点,请参阅后续的dependent

phantom

将其他字段的值合并为自身的值。

例如,有三个字段[年] [月] [日],如果想要将自身值作为[年月日]进行日期验证,就可以这样做。

$rule = [
    'phantom' => ['%04d/%02d/%02d', 'year', 'month', 'day'],
];

这样就会构建为年月日。换句话说,vsprintf($phantom[0], ...array_slice($phantom, 1))生成的值将成为自身的值。

inputs

描述嵌套元素。

$rule = [
    'inputs' => [
        'child_element_name1' => [/* 構造は同じ */],
        'child_element_name2' => [/* 構造は同じ */],
        // ・・・
    ],
];

这样,input元素就可以生成具有层次结构的html,这在“在屏幕内按下按钮则元素集会追加”的情况下使用。但是,它会变得相当复杂,并且几乎必须使用context/template/vuefor。

请注意,可以嵌套到2层。超过2层嵌套的行为尚未定义。

ignore

指定是否从最终结果中排除。

在Compare、phantom等中,有时会出现“最终结果中不需要,但规则中需要”的元素。例如

  • 使用Compare时,想要与当前日期和时间比较,所以需要now元素。
  • 在Requires中,内部需要ID
  • 想要通过Ajax抛出依赖值。

这样的情况。这些元素可能只是为了解决依赖关系或为了其他元素而存在,通常最终验证值中不需要。

此外,此属性具有省略属性,如果像@element_name一样在元素名前附加@,则将自动视为ignore: true

needless

指定非必需时的html input属性。

“非必需”是指“条件性Requires已指定但与条件不一致”。

※ “必需”时必需,但“非必需”时不是不可输入 ※ “不可输入”时不可输入,但“可输入”时不是必需 ※ 也就是说,这不是二值逻辑,而是“必需”“任意”“不可输入”的三值逻辑,所以仅使用Requires是不够的

“非必需时不可输入”“非必需时从显示中删除”这样的需求很少但很常见,所以为了这样的需求而嵌入。

指定可以指定任意属性,但实际上可能是以下3个属性之一。或者,可以通过将data-属性传递给css来作为数据属性进行传递。

  • readonly: 不可输入但可见且可选择的输入类型可能存在
  • disabled: 不可输入但可见且可选择,值不会跳过(这可能也是期望的)
  • inert: 不可输入但可见且不可选择

它们都有各自的特点,所以应该根据用途来选择。假设是关联数组,但如果是上述这样的逻辑属性,则可以用单字符串或连续数组进行指定。

此外,由于css的:has非常实用,所以

.wrapper:has(input[inert]) {
}

等,就可以在js之外实现删除或隐藏块等操作。因此,即使指定了needless,也仅限于属性赋值,DOM结构上完全不进行操作。

autocond

如果可以从规则描述中自动设置condition,则进行设置。例如

  • 当有选项时,设置InArray condition
  • 当可以导出maxlength时,设置StringLength condition

将自动设置。如果已设置相同的condition,则不进行覆盖。

此处理由Input类的_setAutoXXX检测并在自动调用。换句话说,通过创建名为_setAutoHoge的方法来扩展condition的自动设置。

可以通过数组指定要进行的自动设置。例如,使用true/false指定时表示全设置/全未设置。

$rule = [
    'autocond' => [
        'InArray'      => true,
        'StringLength' => false,
    ],
];

这意味着仅设置“当有选项时设置InArray condition”。如果Input类存在_setAutoHoge方法,则指定['Hoge' => false]则Hoge也将排除。未指定的condition被视为true。

除了true之外,还可以传递闭包,这会返回自动生成的Condition实例作为参数。这当需要设置validationLevel或message时很有用。

dependent

指定依赖项。

propagate是对立面。propagate指定的是“自身的事件传播的元素(自身 -> 相手)”,而此dependent指定的是“向自身传播事件的元素(对方 -> 自身)”。虽然没有太大区别,但在父到子、子到父的情况下指定方式略有不同。另外,设置为true时,会根据规则自动合并对方的propagate

例如,在像☑ 必须输入: [      ]这样的UI中,当勾选复选框时,可以使得事件从复选框传播到文本框。本应针对☑ 必须输入设置propagate,但如果对[      ]设置了Requires(checked)的条件,则可以自动推导出“勾选则必需”的关联。true是设置或未设置的标志。

true单值也可以指定为['elemname', true]的数组。这意味着“elemname和自动设置”。

delimiter

指定数组与字符串之间的分隔符。

指定分隔符后,自动将multiple设置为true。然后,当有字符串值传入时,将自动使用分隔符进行数组化。

主要用途是“URL缩短”这种狭义功能。在许多情况下,使用方括号[]进行多选选择会导致URL变长,有时可能会超过浏览器限制(约2000个字符)。

例如,在多选框中,如果通过something_id[]这样的名称选择100个项目,那么仅此一项就足以将URL变为something_id[]=10001&something_id[]=10002&...(约40个字符),全部加起来约2000个字符(排除url编码和&符号)。在这种情况下,通过此选项指定例如,作为分隔符,则变为something_id=10001,10002,...(13+6*100=613)个字符,大大缩短了URL。

转换是隐式进行的。原则上,不需要利用方注意。

multiple

允许值的多值。

  • select: 附加multiple属性
  • 文件:具有多个属性
  • 包括上述内容,除了 radio 外的所有类型的名称都变为 name[]

默认值为 null,在某种程度上会自动设置(例如,默认值是数组时)。

在渲染 input 时,在 hidden 之前直接插入。

由于 html 中的 checkbox 在未选中时值不会丢失,因此始终使用规则默认值。设置此选项后,即使在未选中时也可以跳过空字符串或空数组(在 multiple 的情况下)。

此外,在空字符串跳过的初始值中,将使用此键指定的值。指定 true 则为空数组,否则与指定空字符串相同。

原则上为 checkbox 专用,但对于 multiple select 也适用。其他元素则简单地忽略。

默认

指定值丢失时的默认值。

明确定义则使用该值。如果没有指定,则根据规则自动计算。例如,如果指定了 inputs,则为 [];如果指定了 options,则为第一个值。

由于这种自动处理可能会改变兼容性,因此请尽可能明确定义。

请注意,默认情况下,“值丢失时”适用。不是 isset 或 strlen,而是在真的没有时适用。简而言之,是 array_key_exists 判断。但是,由于对于 checkbox 来说有点不方便,可以使用 pseudo 生成假的 hidden。

nullable 设置为 true 后,即使值为 null,也会使用默认值。简而言之,是 isset 判断。

pseudo 和 default 的区别如下。

  • pseudo: checkbox/select 未选中时的默认值
  • default: setValues 的初始值、值丢失时的初始值、template/context 中的初始值
固定

Form/Context 中有 getFixture 方法,可以自动生成“将能够通过 Form/Context 验证的值”。但这只是自动生成,因此直接使用时可能无法通过验证。

指定此 fixture 后,将不会自动生成,而是明确定义该值。传递闭包时将调用它。这是为了满足“在测试中根本不想生成 fixture 值”的情况。

其他规则

规则不会根据上述以外的既定键进行筛选。只需使用适当的键传递值即可通过 $input->hoge 进行引用。

这是出于考虑扩展性的原因,但在未来的 verup 中,如果键发生冲突,则兼容性可能会崩溃,因此请使用前缀等处理。

渲染

使用创建的 $form 对象按以下方式渲染。

// form 開きタグ
$form->form([/* form の属性*/]);
// label タグ
$form->label('element_name', [/* label の属性 */]);
// input タグ
$form->input('element_name', [/* input の属性 */]);
// form 閉じタグ(引数無しで閉じる)
$form->form();

如果使用 vuejs 的功能,则需要将 ['vuejs' => true] 传递给 /* form 的属性*/。这样就会输出 v-model 或 :data-vinput-id 等,在 vuejs 中也可以正常工作。

属性基本上只提供 name=value 的关联数组。有一种特殊处理,即“值是 false 的情况下,该属性将不存在”。这旨在考虑诸如 readonly 等逻辑属性。

form 标签的属性设置得相当好。例如,如果有 file 元素,则会变成 multipart/form-data 的 post。但是,在渲染时,如果明确指定,则始终优先考虑它。

input 标签的属性有一些特殊属性(括号内是默认属性)。

  • 公共
    • name: 根据规则定义自动设置。指定也会被忽略
    • id: 没有指定时,将自动设置。id <=> for 关联
    • type: html 中的 input type 属性。textarea 或 select 也可以统一使用。没有指定时,会根据规则自动推断
    • class: 指定的是这样,但 validatable 一定会附加
  • type=select 的情况下
    • option_attrs([]): 指定每个 option 的属性关联数组
  • type=checkbox 的情况下
    • multiple(false): 与规则数组中的 multiple 具有相同的效果,并将 [] 附加到 name 属性
    • wrapper(null): 与规则数组中的 wrapper 具有相同的效果,将 input 标签用 span 拉包
    • grouper(null): 与规则数组中的 grouper 具有相同的效果,将同名 input 标签用 span 进行分组
    • labeled("right"): 生成 label 标签作为子元素。"outer" 则作为父元素生成
    • label_attrs([]): labeled 有效时指定 label 侧的属性关联数组
    • format(null): 使用 sprintf 格式化最终的 html。例如,用 div 包围 <div>%s</div>
    • separator(null): 多个元素时的分隔符。例如,将选择项用换行符分隔 separator => '<br>'
  • type=radio 的情况下
    • multiple 以外与 checkbox 相同

label 标签的属性没有特别需要说明的。如果有,则 for 和 class 会自动设置(指定的情况下,它优先)。

已提供用于文件拖放 UI 的文件,并准备了 .vfile-dropzone 这样的 DOM,以便可以在其中拖放文件。file 目标如下

  • 作为子元素存在
  • for 属性指定了 id

也就是说,label ⇔ input 的关系相同(尽管违反 W3C,但可以忽略)。指定 file 的 files 属性将被更新,并且将触发 change 事件。换句话说,它与常规选择的情况完全相同。

无论找到文件还是找不到文件,.vfile-dropzone 都将触发 filedrop 事件。这可以在不使用 file 的情况下进行预览等。

验证

在提交渲染的 form 标签时,将进行如下验证。

// 普通に検証
$isvalid = $form->validate($_POST);
$messages = $form->getMessages();

// 駄目だった場合に例外
$form->validateOrThrow($_POST);

如前所述,有两种方法。

  • validate
    • 以 bool 返回验证结果
    • 通过 getMessages 接收错误消息
    • 参数是通过引用传递的,因此请勿直接传递 $_POST
  • validateOrThrow
    • 如果验证失败,则抛出 ValidationException
    • 返回新的值

基本上推荐使用 validateOrThrow。引用传递难以处理,验证失败时的处理是单一的,抛出异常后,可以在框架等的公共处理部分处理它,因此就足够了。

此外,引用传递或返回新值是由于本库同时进行值正规化的思想。例如,隐藏规则外的键、将 type=file 设置为文件路径、用默认值填充等,“仅仅验证值”是不方便的,而“得到正确的值”则很方便。也就是说,“在验证的同时得到完全有效的值”是概念上的存在。

如下所示,可以添加不基于 condition 的自定义错误。

// 完全カスタムエラーの追加
$form->error('element_name', 'エラーメッセージ');

无论如何,错误处理是必要的。在通过 validate 后的 form 的渲染中,已经实现了错误显示或值回退,因此基本上不需要做任何事情。

如果需要显式获取错误,请使用 getMessagesgetFlatMessages 等方法。

context 和 template 和 vuefor

通过规则 inputs 指定可以表达嵌套结构。设置 inputs 后,将生成 element[-1][subelement] 这样的 name 属性,可以定义“与特定字段相关联的字段集”。为了渲染它,需要使用以下 context/template/vuefor 方法。

以下所述,除非另有说明

[
    'parent' => [
        'inputs' => [
            'child1' => [
                'title'     => '要素1',
            ],
            'child2' => [
                'title'     => '要素2',
            ],
        ]
    ]
]

假设有一个描述该规则的文档。

上下文

根据指定的序列生成字段集。开闭与form相同,有参数时开闭,无参数时关闭。

<?= $form->form([]) ?>
    <?= $form->context('parent', null) ?>
    <?= $form->input('child1') ?>
    <?= $form->input('child2') ?>
    <?= $form->context() ?>
    
    <?= $form->context('parent', 1) ?>
    <?= $form->input('child1') ?>
    <?= $form->input('child2') ?>
    <?= $form->context() ?>
<?= $form->form() ?>

那么

<form>
    <input name="parent[__index][child1]">
    <input name="parent[__index][child2]">
    
    <input name="parent[1][child1]">
    <input name="parent[1][child2]">
</form>

将生成这样的标签。通过context方法的第二个参数指定序列,如果指定null,则使用特殊键__index进行渲染。

通常,context方法被放置在<template>标签内部。通过这种方式获取的template标签,通过传递给chmonos.birth,可以得到完整的DOM Node,然后只需要将其添加到需要的位置即可。

更具体的实际使用方法如下。

<?= $form->form([]) ?>
    <ul id="parent">
    <?php foreach($form->parent->getValue() as $index => $value): ?>
        <?= $form->context('parent', $index) ?>
        <li>
            <?= $form->input('child1') ?>
            <?= $form->input('child2') ?>
        </li>
        <?= $form->context() ?>
    <?php endforeach ?>
    </ul>
    <template id="template" data-vtemplate-name="parent">
        <?= $form->context('parent', null) ?>
        <li>
            <?= $form->input('child1') ?>
            <?= $form->input('child2') ?>
        </li>
        <?= $form->context() ?>
    </template>
    <script>
        document.querySelector('#append-button').addEventListener('click', function() {
            document.querySelector('#parent').appendChild(chmonos.birth(document.querySelector('#template')));
        });
    </script>
<?= $form->form() ?>

在渲染时生成与设置值对应的字段集,然后当按钮被点击时,会生成新的DOM。、index和事件等是通过birth设置的,所以上面的代码是最小的满足这种行为的代码。

template

上述的context方法会导致大量的模板和重复代码。由于实际的数据渲染和模板渲染是分离的,动态元素的使用方法也相对固定化,所以有了template方法来实现一定程度的自动化。

<?= $form->form([]) ?>
    <ul>
        <?= $form->template('parent') ?>
        <li>
            <span data-vnode="">this is ${child1}</span><?= $form->input('child1') ?>
            <span data-vnode="">this is ${child2}</span><?= $form->input('child2') ?>
        </li>
        <?= $form->template() ?>
    </ul>
    <script>
        document.querySelector('#append-button').addEventListener('click', function() {
            chmonos.spawn('parent');
        });
    </script>
<?= $form->form() ?>

去除了值部分的foreach或template部分,非常简洁。另外,新增部分只需要简单地调用spawn即可。

尽管生成的HTML和行为与使用context方法时几乎相同,但实际上template方法只是自动汇总了context中的模板代码。

虽然不能做太复杂的事情,但如果只是想简单地添加动态元素,template会更方便。

通过提供属性data-vnode,可以使用值进行渲染。虽然不是vuejs那样的双向绑定,但足以用于简单地嵌入文本。

data-vnode的值预期将来可能会改变行为或传递选项。目前,不应提供任何值。

实现是动态执行js的模板字面量。如果输入了危险的字符串,可能会导致严重后果,因此请务必注意。

vuefor

基本上与context相同。将js变量名传递到context中指定为index的位置。

<?= $form->form(['vuejs' => true]) ?>
    <div v-for="(row, index) in rows">
    <?= $form->vuefor('parent', 'row', 'index') ?>
    <?= $form->input('child1') ?>
    <?= $form->input('child2') ?>
    <?= $form->vuefor() ?>
    </div>
<?= $form->form() ?>

上述中,本库使用特殊属性输出,带有: (v-bind)的属性。v-model也会输出,所以当vuejs的数据被更改时,就会生成适当的DOM。要传递v-model的修饰符,请指定属性v-model.modifier

更具体的实际使用方法如下。

<ul id="application">
    <input v-on:click="append" type="button" value="追加">
    <?= $form->form(['id' => 'vuejs_form', 'vuejs' => true]) ?>
        <div v-for="(child, index) in parent">
            <?= $form->vuefor('parent', 'child', 'index') ?>
            <?= $form->input('child1') ?>
            <?= $form->input('child2', ['v-model.modifier' => 'number']) ?>
            <?= $form->vuefor() ?>
        </div>
        <input v-on:click="remove(index)" type="button" value="削除">
    <?= $form->form() ?>
</ul>
<script>
    document.addEventListener('DOMContentLoaded', function () {
        const vuejs_chmonos = document.getElementById('vuejs_form').chmonos;
        const app = new Vue({
            el: '#application',
            data: function () {
                return vuejs_chmonos.data;
            },
            methods: {
                append: function () {
                    this.rows.push(Object.assign({}, vuejs_chmonos.defaults.parent));
                },
                remove: function (index) {
                    this.parent.splice(index, 1);
                },
            },
            mounted: function () {
                this.$nextTick(function () {
                    vuejs_chmonos.initialize();
                });
            },
        });
    });
</script>

简而言之,本库只是自动输出“本库可以运行的属性”。context/template会自动初始化,但vuefor不会,因此需要在mounted中显式调用。请参考example。

但是,这只是一个辅助功能。如果想要大量使用vuejs,那么这个功能并不适合。此外,这也是一个实验性功能,不在兼容性维护范围内。这个功能可能会进行破坏性更改。

应该使用哪个

首先,用一句话概括context、template和vuefor的行为

  • context: 用php渲染(因此可以输出template,所以使用方需要很好地处理)
  • template: 用js渲染(因此php侧的灵活性有限)
  • vuefor: 用vuejs渲染(仅输出特殊属性)

适合的情景是:“没有动态添加(只有现有数据渲染,不会逐渐增加)”时使用context,“有动态添加”时使用template,“使用vuejs”时则适合使用vuefor。

错误处理

默认情况下,只要设置了condition,利用方就无需做任何事情。

如果需要自定义错误显示,请监听元素上的validated。这是在验证完成时触发的,事件参数包含错误对象(发生了什么错误)。基本上,本库的工作就是在上述事件触发之前,而如何显示则是管外事。

此外,验证过的元素会设置以下内容。这与上述错误显示无关,是常规操作。

  • validation_warning类切换
  • validation_error类切换
  • validation_ok类切换
  • warningTypes/errorTypes属性赋值

命名空间和目录注册

当使用字符串指定Condition时,如果使用标准以外的(非内置的)Condition,则需要注册命名空间和目录。以下是如何创建任意Condition并将其嵌入的方法。

命名空间和目录优先于规定的内容。也就是说,可以改变现有同名Condition的行为。

// 名前空間とディレクトリを登録します
\ryunosuke\chmonos\Condition\AbstractCondition::setNamespace($namespace, $directory);
// 登録したディレクトリを漁って $outdir に javascript コードを生成します
\ryunosuke\chmonos\Condition\AbstractCondition::outputJavascript($outdir);

另外,如果outdir下有phpjs目录,则将其中的js文件合并。如果需要包含没有的函数或需要覆盖的函数,请将其放置在phpjs中。

添加自定义验证

通过上述注册,可以使用字符串指定Condition,因此可以在该目录中创建一个继承自AbstractCondition的类来自定义验证。基本上,通过查看内置的类(如Uri等简单类)就可以知道应该编写什么,但有一些规则。

getJavascriptCode方法

返回js中的验证代码。如果返回值中包含// @validationcode:inject这样的注释,则下面的validate方法的内容将被注入,形成一个完整的js代码。

也就是说,如果不包含// @validationcode:inject,则完全作为js代码运行。相反,如果包含// @validationcode:inject,则可以进行变量设置或预处理等“js特有的预处理”。

但是,使用机会并不多。实际上,在内置类中也几乎不用。在需要php/js处理差异的情况下使用。以内置的Unique为例(Unique涉及层次结构,因此几乎不可能进行通用代码化)。

prevalidate方法

validate方法之前执行的php处理。虽然可以使用getJavascriptCode进行js的预处理,但不能进行php的预处理。当需要在php中执行预处理时使用。

这也很少使用,在内置类中也几乎不用。如果语法相同,也可以在validate方法中通过$context['lang']进行分支来编写。Unique是一个例子(请参考Unique)。

validate方法

编写验证处理的主体。

有关各个参数的详细信息将在后面简要介绍,但原则上可以通过参考内置类来补充。暂且重要的是,此方法中的代码作为js执行这一点。

由于php和js的语言语法非常相似,因此通过一些小的改动就可以相当容易地执行。通过库吸收这些小的改动,可以几乎完全通过php代码编写来执行js代码。

  • 无需变量声明
    • 在php层中卷起来并声明var
  • 可以使用php的多种函数
    • 实现几乎依赖于locutus
  • 将一些非兼容语法函数化
    • 因此,需要通过函数调用use、foreach、cast等

validate传入以下参数。

  • $value: 检查的值本身
  • $fields: 与值本身不同的、通过getFields得到的另一个值。除非非常复杂,否则通常是空数组
  • $params: 从自身下划线开始的前缀属性数组
  • $consts: 自身的类常量数组
  • $error: 错误通知闭包。使用此闭包进行错误通知
  • $context: 包含语法、执行语言、实例等执行上下文的数组

简单来说,在PHP层中,可以用self或$this引用的对象在JS中无法引用,因此大多数情况都需要通过参数传递。然而,$error相当特殊,在PHP端它只传递消息键或消息,但在JS中还有以下行为。

  • $error(string):添加单个错误消息。这是基本用法。
  • $error(Object):添加多个错误消息。很少使用。
  • $error(Promise):添加在Promise解决后的消息。
    • 例如在Ajax获取错误消息时需要用到。Promise内部可以像使用$error(string)一样使用。
  • $error(null):删除错误消息。
  • $error(undefined):对错误不进行任何操作(既不添加也不删除)。

这是由于PHP是“得到消息就好”的层面,而JS则需要显示或进行异步操作,这是层面的差异。尽管如此,对于简单的条件,应该只使用$error(字符串)。这方面的例子可以参考内置的AjaxImageSize

错误警告

调用Condition的setValidationLevel为"warning"时,该Condition将被警告化,服务器检查将被跳过。客户端检查将显示警告,但不会被处理为错误。可以直接POST。

警告处理可以通过下面的addCustomValidation添加“warning”。

addCustomValidation

如果不需要服务器端验证,则form.chmonos有addCustomValidation方法,使用它可以实现JS的事前和事后验证。

// 事前検証
document.getElementById('form-id').chmonos.addCustomValidation(function(promises) {
    // DOM を追加したり hidden を設定したり
}, 'before');
// 事後検証
document.getElementById('form-id').chmonos.addCustomValidation(function(promises) {
    // ajax で一意制をちぇっくしたり
}, 'after');
// 警告イベント
document.getElementById('form-id').chmonos.addCustomValidation(function(promises) {
    promises.push(!confirm("警告が出ているが本当によい?"));
}, 'warning');

分别调用注册在验证处理前后的function。回调中的this表示form元素本身。唯一参数是Promise数组,可以向该数组添加Promise以进行延迟执行处理。(通常不使用Promise,直接显示错误消息等就足够了。Promise用于异步处理或Ajax等。)

before/after是时间上的差异,没有其他差异,但有共同规范,“返回false将中断”的规范(类似于preventDefault)。在before中返回false将阻止其他before事件和之后的常规验证处理执行。当然,after也不会执行。在after中返回false将阻止其他after事件执行。无论如何,返回false将使验证结果为false。

before可以用于特殊UI的值验证、完全的前提条件验证等。after可以用于文件元素或异步通信的唯一性检查等。warning只在“没有错误但有警告”时触发。在这里可以添加确认对话框等,实现“有警告但强行POST”等功能。

由于这仅是JS的检查,因此请勿使用它进行重要验证。例如,“既然外部键已经保护,所以只想在界面上展示友好性”之类的,仅限于便利性。

注意

  • 文档中的规范相当多。
  • 运行composer build将创建npm目录,但基本上不必在意。
    • 计划使用Babel或uglifyJs,因此正在使用npm,但目前只用于locutus的下载。
      locutus也被js群组包含在存储库中,所以目前没有使用。

许可证

MIT