Web宣言型プログラミング

画像







宣言型プログラミングとは何ですか? ウィキペディアが教えてくれます:







宣言型プログラミングは、問題の解決策の仕様を指定するプログラミングパラダイムです。つまり、問題の内容と期待される結果を記述します。

記事の残りの部分では、現代のWebプログラミングでこのパラダイムを使用する方法について説明します。 特に、Webサービスの入力データの検証/検証の問題に触れたいと思います。 この言語は専門的に私に最も近いため、例はphpになります。







シンプルな形



それでは、簡単なものから始めましょう-Webフォームデータを処理します。 トピックは長い間ハックされてきましたが、それでも私は知っています。 サイトにユーザー認証フォームがあるとします:







画像







この場合、サーバー上には、このフォームからのリクエストを処理する特定のエンドポイントがあります。 ちょっとした注意点-RESTfulサービス、つまり この場合のフォームは、JSアプリケーションによって処理されます。 それを説明してみましょう:









Swagger(オープンAPI標準)



このような記述は、チームのテスターに​​とっては素晴らしいものですが、自動化するのは簡単ではありません。 これにより、APIを開発およびテストするためのツールであるSwaggerが役立ちます。 Swaggerは、JSONオブジェクトを記述するためのオープンスタンダードであるJSONスキーマ(後で説明します)に基づいています。







宣言



上記で提案したリストを基本として、それをSwagger形式に「変換」すると、同様の結果が得られます。







 { "swagger": "2.0", "host": "example.com/login.php", "basePath": "/v1", "tags": [{"name": "login", "description": "User login form"}], "schemes": ["http", "https"], "paths": { "/user/login": { "post": { "tags": ["login"], "summary": "Authenticate the user", "consumes": ["application/x-www-form-urlencoded"], "produces": ["application/json; charset=utf-8"], "parameters": [ { "in": "formData", "name": "email", "description": "User email", "required": true, "schema": {"type": "string","maxLength": 50,"format": "email"} }, { "in": "formData", "name": "password", "description": "User password", "required": true, "schema": {"type": "string","maxLength": 16,"minLength": 8} } ], "responses": { "200": { "description": "successful login", "schema": { "type": "object", "properties": [ { "name": "status", "schema": {"type": "string"} } ], "example": {"status": "ok"} } }, "422": { "description": "Invalid login data", "schema": { "type": "object", "properties": [ {"name": "status","schema": {"type": "string"}}, {"name": "code","schema": {"type": "integer"}} ], "example": {"status": "fail","code": 12345} } } } } } } }
      
      





したがって、エンドポイントの仕様を変更することで、既成のfront-end



back-end



なしでback-end



テストを開始でき、多くの場合、チーム内での対話が大幅に高速化および簡素化されます。 Swagger UIを使用して、ブラウザーで直接バックエンド要求を生成できます。







画像







注:これを行うには、 Swagger UI



ファイルをバックエンドと同じドメインに配置するか、上記でクロスドメインリクエストを有効にする必要があります。 CORSチートシート

おそらくSwagger宣言で最も楽しい部分は、 definitions



を通じて同じオブジェクトを再利用する機能です。 この例では、それらに触れませんでしたが、公式Webサイトの例にあります。 SwaggerはJSON schema



基づいているため、 JSON



データを検証するときは、以下のdefnitions



例を見てdefnitions



ます。







複雑な入力の場合、特定のオブジェクトの例を指定する非常に便利な機会があります。 Swagger UIを使用する場合、テストのためにフォームに自動的に挿入されるため、すべてを手動で入力しなくてもエラーの時間と可能性が減少します。







画像

http://petstore.swagger.io/#/user/createUsersWithArrayInput







IDEサポート



swaggerファイルでの作業をさらに楽しくするために、お気に入りのIDEのプラグインをインストールできます。









NetBeansのプラグインは見つかりませんでしたが、確かにそうです。 あなたがそれをどこで入手できるかを知っていれば-私はリンクに感謝します。


世代



Swagger



ファイルのサポートを単調で単調で退屈なタスクにしないようにするには、ソースコードに基づいてSwagger JSON



ファイルジェネレーターを使用できます。 したがって、私たちは一度に複数の「一石の鳥」を殺します。









SwaggerのPHPアノテーションの例



 /** * @SWG\Post( * path="/product", * summary="Create/add a product", * tags={"product"}, * operationId="addProduct", * produces={"application/json"}, * consumes={"application/json"}, * @SWG\Parameter( * name="body", * in="body", * description="Create/alter product request", * required=true, * type="object", * @SWG\Schema(ref="#/definitions/Alteration") * ), * @SWG\Response( * response=201, * description="Product created", * @SWG\Schema(ref="#/definitions/Product") * ), * @SWG\Response( * response=400, * description="Empty data - nothing to insert", * @SWG\Schema(ref="#/definitions/Error") * ), * @SWG\Response( * response=422, * description="Product with the specified title already exists", * @SWG\Schema(ref="#/definitions/Error") * ) * ) */
      
      





JSONファイルを生成するコマンドの例:



 ./vendor/bin/swagger --output wwwroot/swagger.json //   public  --exclude vendor/ //   
      
      





要約すると、 Swagger



を使用して、外の世界でenpoint



がどのようにenpoint



するかを宣言しました。







このような中間UIを使用すると、enpointのすべての種類の入力データを生成し、意図したとおりに機能することを確認できます。 この段階で、UIは「エミュレート」され、サーバー側に移動します。







宣言的な方法でデータを検証および消去するには、ネイティブのfilter_var_array



関数がfilter_var_array



です:







 $data = filter_var_array($_REQUEST, [ 'email' => FILTER_SANITIZE_ENCODED, 'password' => FILTER_SANITIZE_ENCODED ]); $result = (false === $data) ? ['status' => 'fail', 'code' => 12345] : ['status' => 'ok']; die(json_encode($result));
      
      





この例が非常に原始的であることは明らかです。 では、さらに複雑な例に移りましょう。







ジョンソン



JSONデータを検証するには、同じJSONスキーマを使用します。 さらに運用するために、学校の生徒からデータを収集する必要があるとします。 フォームには、学生、保護者、連絡先に関する情報が含まれます。 justinrainbow/json-schema



ライブラリを使用してデータを検証します。







そして、これが私たちの図です:







 { "$schema": "http://json-schema.org/draft-04/schema#", "title": "EntryPoll", "type": "object", "definitions": { "contacts": { "type": "object", "properties": { "email": {"type": "string", "format": "email"}, "phone": {"type": "string", "pattern": "^\\+7\\(845\\)[0-9]{3}-[0-9]{2}-[0-9]{2}$"} } }, "name": { "type": "object", "properties": { "firstName": {"type": "string"}, "lastName": {"type": "string"}, "gender": {"type": "string", "enum": ["m", "f", "n/a"]} }, "required": ["firstName", "lastName"] } }, "properties": { "student": { "type": "object", "description": "The person who will be attending classes", "properties": { "name": {"$ref": "#definitions/name"}, "contacts": {"$ref": "#definitions/contacts"}, "dob": {"type": "string","format": "date"} }, "required": ["name", "dob"] }, "parents": { "type": "array", "minItems": 1, "maxItems": 3, "items": { "type": "object", "properties": { "name": {"$ref": "#definitions/name"}, "contacts": {"$ref": "#definitions/contacts"}, "relation": { "type": "string", "enum": ["father", "mother", "grandfather", "grandmother", "sibling", "other"] } }, "required": ["name", "contacts"] } }, "address": { "type": "object", "description": "The address where the family lives (not the legal address)", "properties": { "street": {"type": "string"}, "number": {"type": "number"}, "flat": {"type": "number"} } }, "legal": { "type": "boolean", "description": "The allowance to use submitted personal data" } }, "required": ["student", "address", "legal"] }
      
      





JSONスキーマ形式は、単純な文字列とintで始まり、複雑で広範囲のデータ型で終わる多くのデータ型をサポートします: date-time



email



hostname



ipv4



ipv6



uri



json-pointer



。 その結果、単純な「レンガ」から非常に複雑な形状を構築できます。







検証用のphpコードの例:







 (new \JsonSchema\Validator())->validate( json_decode($request->getBody()->getContents()), //   (object) ['$ref' => 'file://poll-schema.json'], //   //  Exception   \JsonSchema\Constraints\Constraint::CHECK_MODE_EXCEPTIONS );
      
      





XML



ここではすべてがJSONを使用するよりもはるかに単純ですが、何らかの理由で、私が作業した開発者のほとんどはこの可能性を知らないか、単にそれを無視します。 ネイティブのDOMDocumentとlib-xml



拡張が必要になります。これらはほとんどのphp



ビルドでデフォルトで使用可能です。







最初に、検証するリクエストの例を作成します。 複雑な構成の支払いシステムサービスがあり、それにリクエストを送信してユーザーインターフェイスリンクを形成するとします。 リクエストには、承認された支払いシステム、サブスクリプション費用、ユーザー情報などの情報が含まれます。







 <?xml version="1.0" encoding="UTF-8"?> <paymentRequest> <forwardUrl>https://www.example.com</forwardUrl> <language>EN</language> <userId>13339</userId> <affiliateId>my:google:campain:5478669</affiliateId> <userIP>192.168.8.68</userIP> <tosUrl>https://www.example.com/tos</tosUrl> <contracts> <contract name="14-days-test"> <description>14 days test</description> <note>Is automatically converted into a basic package after expiration</note> <termOfContract period="days">14</termOfContract> <contractRenewalTerm period="month">1</contractRenewalTerm> <cancellationPeriod period="days">14</cancellationPeriod> <paytypes> <currency type="EUR"> <creditCard risk="0"/> <directDebit risk="100"/> <paypal risk="85"/> </currency> <currency type="USD"> <creditCard risk="58"/> </currency> </paytypes> <items> <item sequence="0"> <description>14 days test</description> <dueDate>now</dueDate> <amount paytype="creditCard" currency="EUR">1.9</amount> <amount paytype="directDebit" currency="EUR">1.9</amount> <amount paytype="paypal" currency="EUR">1.9</amount> <amount currency="USD">19.9</amount> </item> </items> </contract> </contracts> </paymentRequest>
      
      





次に、受信したリクエストを検証します。







 function validateString($xml = '') { $dom = new \DOMDocument('1.0', 'UTF-8'); // load, validate and try to catch error if (false === $dom->loadXML($xml) || false === $dom->schemaValidate($this->schema)) { $exception = new ValidationException('Invalid XML provided'); $this->cleanUp(); throw $exception; } return true; }
      
      





「すぐに使用できる」次のタイプをサポートしています: xs:string



xs:decimal



xs:integer



xs:boolean



xs:date



xs:time



その他
しかし、良いニュースは、それらに限定されないことです-既存のデータ型を拡大または縮小したり、それらを組み合わせたりするなどして、独自のデータ型を作成できます。 以下は、上記のXML要求のスキーマの例です。







 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE schema SYSTEM "https://www.w3.org/2001/XMLSchema.dtd"> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:include schemaLocation="xsd/paymentRequest.xml" /> <xs:element name="paymentRequest" type="PaymentRequest" /> </xs:schema>
      
      





このドキュメントには、親要素の宣言が含まれています。 子のプラグインドキュメントを個別に検討します。







 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE schema SYSTEM "https://www.w3.org/2001/XMLSchema.dtd"> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:include schemaLocation="type/urls.xsd" /> <xs:include schemaLocation="type/bank.xsd" /> <xs:include schemaLocation="type/address.xsd" /> <xs:include schemaLocation="type/ip.xsd" /> <xs:include schemaLocation="type/contract.xsd" /> <xs:include schemaLocation="type/voucher.xsd" /> <xs:include schemaLocation="type/riskinfo.xsd" /> <xs:include schemaLocation="enum/layouts.xsd" /> <xs:include schemaLocation="enum/schemes.xsd" /> <xs:include schemaLocation="enum/languages.xsd" /> <xs:complexType name="PaymentRequest"> <xs:annotation> <xs:documentation>Initial create enrollment request</xs:documentation> </xs:annotation> <xs:all> <xs:element name="tosUrl" type="TosUrl" /> <xs:element name="serviceHotline" type="xs:string" minOccurs="0" /> <xs:element name="userId"> <xs:simpleType> <xs:union> <xs:simpleType> <xs:restriction base='xs:string'> <xs:minLength value="1" /> </xs:restriction> </xs:simpleType> <xs:simpleType> <xs:restriction base='xs:integer' /> </xs:simpleType> </xs:union> </xs:simpleType> </xs:element> <xs:element name="userIP" type="ipv4" /> <xs:element name="contracts" type="ContractsList" /> <xs:element name="layout" type="AvailableLayouts" minOccurs="0" default="default" /> <xs:element name="colorScheme" type="AvailableSchemes" minOccurs="0" default="default" /> <xs:element name="forwardUrl" type="ForwardUrl" minOccurs="0" /> <xs:element name="language" type="xs:string" minOccurs="0" default="DE" /> <xs:element name="affiliateId" type="xs:string" minOccurs="0" /> <xs:element name="voucher" type="xs:string" minOccurs="0" /> <xs:element name="userBirth" type="xs:date" minOccurs="0" /> <xs:element name="userAddress" type="UserAddress" minOccurs="0" /> <xs:element name="userBankaccount" type="BankAccount" minOccurs="0" /> <xs:element name="userRiskInfo" type="UserRiskInfo" minOccurs="0" /> <xs:element name="vouchers" type="VouchersList" minOccurs="0" /> <xs:element name="voucherCodes" type="VoucherCodesList" minOccurs="0" /> </xs:all> </xs:complexType> </xs:schema>
      
      





もう1つのボーナスとして、XSDドキュメントを相互に接続(含める)できます。 したがって、特定のカスタムデータ型を宣言すると、それを複数のスキームで使用できます。 詳細については、例についてはリポジトリを参照してください。 また、ドキュメントにコメントをドキュメントの本文に直接含めることができます。







最後の例では、たくさんの小さなXSDを接続します。 ご覧のとおり、複雑なオブジェクトの説明には、複雑な複合タイプとより単純で基本的なタイプの両方を含めることができます。 トピックを完全に明らかにするために、単純な複合型の1つの例を考えてみましょう。







 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE schema SYSTEM "https://www.w3.org/2001/XMLSchema.dtd"> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:vc="http://www.w3.org/2007/XMLSchema-versioning" vc:minVersion="1.0"> <xs:simpleType name="ipv4"> <xs:annotation> <xs:documentation>An IP version 4 address.</xs:documentation> </xs:annotation> <xs:restriction base="xs:token"> <xs:pattern value="(25[0-5]|2[0-4][0-9]|[0-1]?[0-9]?[0-9])\.(25[0-5]|2[0-4][0-9]|[0-1]?[0-9]?[0-9])\.(25[0-5]|2[0-4][0-9]|[0-1]?[0-9]?[0-9])\.(25[0-5]|2[0-4][0-9]|[0-1]?[0-9]?[0-9])"/> <xs:pattern value="[0-9A-Fa-f]{8}"/> </xs:restriction> </xs:simpleType> </xs:schema>
      
      





XSDスキームを使用することの楽しい点の1つは、かなり前から存在しており、必要に応じてコンパイルおよび検証されたユーザーデータ型のライブラリ全体を見つけることができることです。 特に、上記の例は2005年12月のメーリングリストから引用したものです。







LibXMLエラー処理



特にツールのデバッグやテストなどの場合、「XMLが無効です」というエラーに満足しないのは私だけではないと思います。 したがって、ドキュメント内のエラーに関する情報を少し拡大してみましょう。 その結果、さらなるアクションとエラーを含む行番号の明確なメッセージを受け取りたいと思います。







 /** * @link http://php.net/manual/en/domdocument.schemavalidate.php */ class Xml { /** * @var string */ protected $schema; /** * @var bool */ protected $errors; /** * Xml constructor. * @param string $schemaPath */ public function __construct($schemaPath = null) { $this->schema = null === $schemaPath ? __DIR__ . '/../config.xsd' : $schemaPath; $this->errors = libxml_use_internal_errors(true); } /** * Restore the values and remove errors */ protected function cleanUp() { libxml_use_internal_errors($this->errors); libxml_clear_errors(); } /** * @param string $xml * @return bool * @throws InvalidArgumentException * @throws ValidationException */ public function validateString($xml = '') { $dom = new \DOMDocument('1.0', 'UTF-8'); // load and try to catch error if (false === @$dom->loadXML($xml) || false === @$dom->schemaValidate($this->schema) ) { $exception = new ValidationException('Invalid XML provided'); $exception->setErrorCollection(new ErrorCollection(libxml_get_errors())); $this->cleanUp(); throw $exception; } return true; } }
      
      





コードは読みやすくするために意図的に短縮されています。 主なアイデアは、 libxml



エラーを収集し、エラー情報を含む特別なクラスからカスタムコレクションに「ラップ」することです。 次に、クラスとコレクションは\JsonSerializable



実装であるため、必要な情報の利用可能度でそれらをクライアントに渡すことができます。 たとえば、エラーが発生したファイルに関する標準\LibXMLError



情報から除外しました。







 /** * Decorator for native LibXmlError to hide file path. */ class LibXMLError implements \JsonSerializable { /** * @var int */ protected $code; /** * @var int */ protected $line; /** * @var string */ protected $message; /** * LibXMLError constructor. * @param \LibXMLError $error */ public function __construct(\LibXMLError $error = null) { if (null !== $error) { $this->line = $error->line; $this->message = $error->message; $this->code = $error->code; } } /** * @return array */ public function jsonSerialize() { return [ 'code' => $this->code, 'message' => $this->message, 'line' => $this->line, ]; } }
      
      





テスト中



既に述べたように、Swaggerを使用する場合、Swagger UIのブラウザーで手動テストを直接実行できます。 検証テストを自動化するために、2つの非常に簡単なテストを作成できます。 単体テストを作成するには、 phpUnitを使用します 。 コードはXML専用ですが、同じアプローチがJSONにうまく移植されています。







有効なXML / JSONエラーの確認



 class SuccessfulTest extends \PHPUnit_Framework_TestCase { public function setUp() { $this->validator = new XmlValidator(XSD_SCHEMA_PATH); } public function tearDown() { $this->validator = null; } /** * @param string $filename * @param string $xml * @dataProvider generateValidDomDocuments */ public function testValidateXmlExample($filename, $xml = '') { try { $this->assertTrue($this->validator->validateString($xml)); } catch (ValidationException $ve) { $this->fail($ve->getMessage().' => '.json_encode($ve, JSON_PRETTY_PRINT)); } } public function generateValidDomDocuments() { $xml = []; $directory = new \DirectoryIterator('valid-xmls-folder'); /** @var \DirectoryIterator $file */ foreach ($directory as $file) { // skip non relevant if ($file->isDot() || !$file->isDir() || 'xml' === $file->getExtension()) { continue; } $xml[] = [$file->getBasename(), file_get_contents($file->getRealPath())]; } return $xml; } }
      
      





無効なXML / JSONの予想されるエラーの存在を確認する



 class SuccessfulTest extends \PHPUnit_Framework_TestCase { public function setUp() { $this->validator = new XmlValidator(XSD_SCHEMA_PATH); } public function tearDown() { $this->validator = null; } /** * @param string $filename * @param string $xml * @expectedException \Validator\Exception\ValidationException * @expectedExceptionCode 422 * @dataProvider generateInvalidDomDocuments */ public function testValidateXmlExample($filename, $xml = '') { $this->assertFalse($this->validator->validateString($xml)); } /** * @return array */ public function generateInvalidDomDocuments() { $xml = []; $directory = new \DirectoryIterator('valid-xmls-folder'); /** @var \DirectoryIterator $file */ foreach ($directory as $file) { // skip non relevant if ($file->isDot() || !$file->isDir() || 'xml' === $file->getExtension()) { continue; } $xml[] = [$file->getBasename(), file_get_contents($file->getRealPath())]; } return $xml; } }
      
      





これで私はすべてを持っています。 宣言をお楽しみください!







PS追加/コメントに感謝します。








All Articles