Symfony2およびKnockoutJS-フォームの検証

数ヶ月前、私は人気のあるSymfony2 PHPフレームワークを学び始めました。 最近、KnockoutJSライブラリを使用して、クライアント側でフォームに記入する正確性をチェックするタスクがありました。 この場合、検証のルールは、コードの複製を行わないように、エンティティクラスからSymfonyを取得することをお勧めします。

問題のいずれかの側面をカバーする10,000を超えるプラグイン、ライブラリ、およびバンドルがあります。 包括的なソリューションが見つかりませんでした。 タスクの最初と2番目の部分で最も人気のある2つのソリューション(Knockout-ValidationとAPYJsFormValidationBundle)を組み合わせる労力を見積もって、すべてをゼロから作成することにしました。 カットの下の詳細。



Symfony2での検証

私の場合、検証ルールは注釈に設定されています。 メモリを更新するために、リストを提供します:



/** * Acme\UsersBundle\Entity\User */ class User implements JsonSerializable { /** * @var string $name . * * @ORM\Column(name="name", type="string", length=255, unique = true, nullable=false) * * @Assert\NotBlank(message=" ") * @Assert\MinLength(limit=3, message="  ") * @Assert\MaxLength(limit=15, message="  ") * @Assert\Regex(pattern="/^[A-z0-9_-]+$/ui", match=true, message="   ") */ private $name; // .... }
      
      





最初に行うことは、これらのコメントを解析することです。 もちろん、フレームワーク自体はすでにこれを行っています。 解析結果は、環境に応じて、アドレス「app / cache / dev / annotations /」または「app / cache / prod / annotations /」のキャッシュに保存されます。 少し考えてから、小さなメソッドを作成しました。



 /** *    . * * @param string $bundle    "Bundle". * @param string $entity         . * @param string $env  ("dev"  "prod"). * @param string $namespace   (  "Acme"). * @return array . */ private function readEntityAnnotations($bundle, $entity, $env = 'prod', $namespace = 'Acme') { $result = array(); $files = glob($_SERVER['DOCUMENT_ROOT'] . '/../app/cache/' . $env . '/annotations/' . $namespace . '-' . $bundle .'Bundle-Entity-' . $bundle . $entity .'$*.php'); foreach ($files as $path) { //        preg_match('/\\$(.*?)\\./', $path, $matches); //   foreach (include $path as $annotation) { //       if (get_parent_class($annotation) === 'Symfony\\Component\\Validator\\Constraint') { $type = preg_replace('/^.*\\\/', '', get_class($annotation)); $annotation = (array)$annotation; unset($annotation['charset']); $result[$matches[1]][$type] = (array)$annotation; } } } return $result; }
      
      





おそらくそのようなコードは従うべき悪い例ですが、そのタスクに対処します。 後で書き直します。



その結果、クライアント上で同様のものを取得できます。

注釈



検証とKnockoutJS

検証ルールがわかったら、クライアントコードの記述を開始できます。 実装のアイデアはKnockout Validationから借用しました。 このプラグインで検証ルールを設定する例を示します。



 var myComplexValue = ko.observable() myComplexValue.extend({ required: true }) .extend({ minLength: 42 }) .extend({ pattern: { message: 'Hey this doesnt match my pattern', params: '^[A-Z0-9].$' }});
      
      





つまり、本質は、2番目のKnockoutブランチから登場したエクステンダーの使用にあります。 エクステンダーを使用すると、あらゆる種類のオブザーバブルの動作を変更または補完できます。 例を考えてみましょう:



var name = ko.observable('habrahabr').extend({MinLength: 42});







観測された名前プロパティを更新するとき、ノックアウトはMinLengthという名前のエクステンダーを見つけようとし、成功するとそれを呼び出します。 エクステンダーへのパラメーターとして、監視されているプロパティ自体と数値42が渡されます。



次に、エクステンダー自体を実装します。



 ko.extenders.MinLength = function(observavle, params) { // .... };
      
      





アイデアは明確です。実装に移りましょう。 たとえば、次のモデルを見てください。



 var AppViewModel = new (function () { var self = this; //     this.name = ko.observable(''); //   this.mail = ko.observable(''); // E-mail //   ko.validation.init(self, _ANNOTATIONS_); //    this.submit = function () { if (self.isValid()) { alert(' '); } else { alert('  '); } }; })();
      
      





ko.validation.initとself.isValidを除き、すべてがここで明確になります。 ko.validation.initは、Symfonyから引数として取得した注釈情報を含むモデルとオブジェクトを受け取るバリデータ初期化関数です。 isValidメソッドは、バリデーターが初期化されるときにモデルに追加されます。



 <form action="#"> <p> <label for=""></label> <input type="text" data-bind="value: name, valueUpdate: 'keyup'"> <span data-bind="visible: name.isError, text: name.message"></span> </p> <p> <label for="">E-mail</label> <input type="text" data-bind="value: mail, valueUpdate: 'keyup'"> <span data-bind="visible: mail.isError, text: mail.message"></span> </p> <button data-bind="click: submit"></button> </form>
      
      





isErrorプロパティとmessageプロパティは、それぞれエラーフラグとエラーメッセージです。 これらのプロパティは両方とも監視可能であり、初期化時にメインプロパティに追加されます。



 AppViewModel.name.isError = ko.observable(); //    AppViewModel.name.message = ko.observable(); //    AppViewModel.name.typeError = ''; //   
      
      





これは、投稿の対象読者にとっては問題になりませんが、念のために説明します。JavaScriptではすべてがオブジェクトであるか、タイプごとにオブジェクトラッパーがあります。 変換は必要に応じて自動的に行われます。 同じことが関数にも当てはまります。 したがって、AppViewModel.nameプロパティ(実際は関数)にいくつかのプロパティを追加することを妨げるものは何もありません。



フォーム検証アルゴリズムは次のようになります。

-ユーザーがフォームを送信するまで何もしません

-有効ではないため、送信に失敗した最初の失敗の後、各更新(キーアップと変更)でフィールドをチェックします。



次に、コード全体を提供し、詳細に分析します。



 ko.validation = new (function () { /** *   . * @return {Boolean} */ var isValid = function () { this.validate(true); //      for (var opt in this) if (ko.isObservable(this[opt])) { //     if (this[opt].isError !== undefined && this[opt].isError() === true) { return false; } } return true; }; return { /** *  . * @param {object} AppViewModel  . * @param {object} annotations   . */ init: function (AppViewModel, annotations) { var asserts, options; AppViewModel.validate = ko.observable(false); //        for (var field in annotations) if (annotations.hasOwnProperty(field)) { asserts = annotations[field]; //   (AppViewModel)        if (AppViewModel[field] !== undefined && ko.isObservable(AppViewModel[field])) { AppViewModel[field].isError = ko.observable(); //    AppViewModel[field].message = ko.observable(); //    //      for (var i in asserts) if (asserts.hasOwnProperty(i)) { options = {}; options[i] = asserts[i]; //   options[i]['asserts'] = asserts; //    options[i]['AppViewModel'] = AppViewModel; //    //      AppViewModel[field].extend(options); } } } //      AppViewModel.isValid = isValid; }, /** *    . * @param name  . * @param validate  . * @param checkAsserts */ addAssert: function (name, validate, checkAsserts) { //  extender' ko.extenders[name] = function(target, option) { //     "AppViewModel.validate" ko.computed(function () { //          if (validate(target, option) === false && option.AppViewModel.validate()) { checkAsserts = checkAsserts || new Function('t,o', 'return false'); //     if (checkAsserts(target, option) === false) { target.isError(true); //    target.message(option.message); //    target.typeError = name; //   } return; } //          if (target.isError.peek() === true && target.typeError === name) { target.isError(false); } }); return target; }; } } })();
      
      





いくつかの検証メソッドをすぐに追加します。



 // NotBlank ko.validation.addAssert('NotBlank', function (target, option) { return (target().length > 0); }); // MaxLength ko.validation.addAssert('MaxLength', function (target, option) { return (target().length <= option.limit); });
      
      







一般的なデバイス

コードは、Stefan Stoyanov氏の著書「Javascriptパターン」の「モジュール」と呼ばれる設計パターンに従って構成されています。 つまり すぐに呼び出される匿名関数は、initとaddAssertの2つのメソッドを持つオブジェクトを返します。 クロージャ内では、isValidメソッドが定義されています。



IsValidメソッド モデル検証
モデルの有効性を確認します。 メソッドは、モデルのコンテキストで呼び出されます。 isValidメソッド内のこれはAppViewModelです。 まず、検証モデルの監視プロパティをtrueに設定します。 これは、フォームを送信する試みを示します。 検証プロパティ自体は、initメソッドを使用した初期化中にモデルに追加されます。

次に、メソッドはモデルのすべての監視されたプロパティを実行し、エラーフラグをチェックします。



初期化メソッド 検証の初期化
まず、このメソッドは、前述の検証プロパティとisValidメソッドをモデルに追加します。 そのサイクルの後、制限が示され、同じ名前の監視可能なプロパティがあるフィールドを通過し、モデルの最後にisErrorとmessageを追加します。 2番目のネストされたループは制限をバイパスし、適切なエクステンダーでフィールドを拡張しようとします。 エクステンダーパラメーターとして、オブジェクトはSymfonyキャッシュから取得した制限パラメーター、モデルへのリンク(AppViewModel)、およびこのフィールドのすべての制限のリストとともに渡されます。



AddAssertメソッド。 新しい検証方法の登録
このメソッドは3つのパラメーターを取ります。name-新しい検証メソッドの名前、validate-検証関数、checkAsserts-エラーを確認する関数。 最後のパラメーターについては少し後で検討します。

エクステンダーメソッドの本体は、AppViewModel.validateの更新時に検証が確実に再開されるように、計算されたプロパティでラップされます。



CheckAssertsメソッド
これは、addAssertメソッドのオプションのパラメーターです。 他のバリデーターがエラーをスローするかどうかを確認する必要があります。 たとえば、フィールドに入力された文字列の長さをチェックする場合。 フィールドが空の場合、「フィールドに入力」と言い、3文字未満の場合-「名前には少なくとも3文字が含まれている必要があります」など。 ただし、MinLengthチェックがNotBlankより後に発生するという保証はありません。 MinLengt検証メソッド(エクステンダー)の例を次に示します。



 // MinLength ko.validation.addAssert( 'MinLength', function (target, option) { return (target().length >= option.limit); }, function (target, option) { //       "NotBlank" return (target().length === 0 && option.asserts.NotBlank !== undefined); } );
      
      





もちろん、バリデーターのリストを使用してオブジェクトを作成し、特定の順序で並べ替えることができます。 オブジェクトのプロパティの列挙は、割り当ての順序で発生します。 これは規格には記載されていませんが、実際にはこれはかなり信頼できるルールです。 ただし、ノックアウトが内部にどのように配置されるか、および次のバージョンで同じように配置されるかどうかの問題が発生します。 したがって、これまでのところ、松葉杖のような機能を備えたオプションは最適なように思えます。



使用する

検証ルールは、Symfonyエンティティクラスに同じフィールドがあり、説明されている制限があるすべての監視可能なプロパティに適用されます。 したがって、クライアント側でフィールドをチェックする必要がない場合、解決策は明らかです-別の名前を付けるか、注釈の読み取り時に取得したjsオブジェクトからプロパティを削除します。



codepenには小さなデモがあります: codepen.io/alexismaster/pen/LAaqc



最後に、readEntityAnnotationsメソッドの約束された改善。 検証サービスを通じて注釈を取得できます。



 //      "name"   "User" $validator = $this->get('validator'); $metadata = $validator->getMetadataFactory()->getClassMetadata("Acme\\UsersBundle\\Entity\\User"); var_dump($metadata->properties['name']->constraints);
      
      







参照:

github.com/Abhoryo/APYJsFormValidationBundle-検証用のJSコードを生成するSymfonyバンドル

github.com/Knockout-Contrib/Knockout-Validation

habrahabr.ru/post/136782-KnockoutJSとエクステンダーに関する興味深い投稿

phalcon-docs-ru.readthedocs.org/ru/latest/reference/annotations.html-注釈パーサー

habrahabr.ru/post/133270-Symfony 2のカスタムアノテーション



All Articles