symfonyアプリのリクエストの検証


こんにちは👋
ある夜、私はsymfonyアプリでarroundを演奏していました、そして、実現しました、私はコントローラ本体自体で要求本体を確認する行為が好きではありません.私はSymfonyに比較的新しいです、それで、私はそれが私自身を試みる良いことであるかもしれないと思いました.
ドキュメントごとに、次のようになります.
public function author(ValidatorInterface $validator)
{
    $author = new Author();

    // ... do something to the $author object

    $errors = $validator->validate($author);

    if (count($errors) > 0) {
        /*
         * Uses a __toString method on the $errors variable which is a
         * ConstraintViolationList object. This gives us a nice string
         * for debugging.
         */
        $errorsString = (string) $errors;

        return new Response($errorsString);
    }

    return new Response('The author is valid! Yes!');
}
これも結構ですが、どこか他の場所に移動できたらいいかもしれません.
理想的には、リクエストクラスにヒントを入力し、別のメソッドを呼び出して検証を行うことができます.
こうして私はそれを想像した.まず、作成しましょうExampleRequest 単純なPHPのプロパティとしてフィールドとクラスを定義します.
<?php

namespace App\Requests;

class ExampleRequest
{
    protected $id;

    protected $firstName;
}
ここで、PHP 8属性を使用することができます(または注釈を使用する)フィールドの検証規則を記述できます.
<?php

namespace App\Requests;

use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Type;

class ExampleRequest
{
    #[Type('integer')]
    #[NotBlank()]
    protected $id;

    #[NotBlank([])]
    protected $firstName;
}
パーフェクト、今楽しいパート.次のAPI作業を行いましょう.
#[Route('/app', name: 'app')]
public function index(ExampleRequest $request): Response
{
    $request->validate();

    return $this->json([
        'message' => 'Welcome to your new controller!',
        'path' => 'src/Controller/AppController.php',
    ]);
}
我々は、持っていませんvalidate() メソッドExampleRequest . そこに直接追加する代わりに、私はBaseRequest すべてのリクエストに対して再利用できるクラスであるため、個々のクラスでは検証や解決などの心配はありません.
<?php

namespace App\Requests;

use Symfony\Component\Validator\ConstraintViolationListInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;

abstract class BaseRequest
{
    public function __construct(protected ValidatorInterface $validator)
    {
    }

    public function validate(): ConstraintViolationListInterface
    {
        return $this->validator->validate($this);
    }
}

Don't forget to extend BaseRequest in ExampleRequest class.


これを実行すると、妥当性検査に関連することは起こりません.あなたは定期的なコントローラの応答が表示されます:あなたの新しいコントローラへようこそ!
これは結構です.我々は、アプリケーションの確認を停止するには、要求を破る、または何か他の言っていない.
どのエラーが発生したか確認しましょう.
#[Route('/app', name: 'app')]
public function index(ExampleRequest $request): Response
{
    $errors = $request->validate();

    dd($errors);

    return $this->json([
        'message' => 'Welcome to your new controller!',
        'path' => 'src/Controller/AppController.php',
    ]);
}
体にこれらのフィールドを使用してリクエストを発射しましょう.

何が起こっているの?私たちは送ったid リクエスト本文ではまだ不平を言う.さて、私たちはリクエスト本文をExampleReequestに写像しませんでした.
それをしましょうBaseRequest クラス.
<?php

namespace App\Requests;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Validator\ConstraintViolationListInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;

abstract class BaseRequest
{
    public function __construct(protected ValidatorInterface $validator)
    {
        $this->populate();
    }

    public function validate(): ConstraintViolationListInterface
    {
        return $this->validator->validate($this);
    }

    public function getRequest(): Request
    {
        return Request::createFromGlobals();
    }

    protected function populate(): void
    {
        foreach ($this->getRequest()->toArray() as $property => $value) {
            if (property_exists($this, $property)) {
                $this->{$property} = $value;
            }
        }
    }
}
ここで何をしましたか.まず第一に、我々はpopulate() リクエスト本文をループし、そのプロパティが存在する場合はフィールドをクラスプロパティにマップするメソッドです.
私たちが同じ要求を再び発射するならば、確認が方法について叫ばない方法に注意してくださいid プロパティ.

提供しましょうfirstName また、何が起こるかを見てください.

いいね我々はバリデータを渡した!
この点で、我々は我々自身でバリデータを呼ぶ必要がないので、これはすでに改善です.でも、さらに一歩進めましょう.何かが間違っているなら、確認メッセージでJSONを返すようにしましょう.
リファクタリングしたいvalidate() メソッドBaseRequest .
public function validate()
{
    $errors = $this->validator->validate($this);

    $messages = ['message' => 'validation_failed', 'errors' => []];

    /** @var \Symfony\Component\Validator\ConstraintViolation  */
    foreach ($errors as $message) {
        $messages['errors'][] = [
            'property' => $message->getPropertyPath(),
            'value' => $message->getInvalidValue(),
            'message' => $message->getMessage(),
        ];
    }

    if (count($messages['errors']) > 0) {
        $response = new JsonResponse($messages);
        $response->send();

        exit;
    }
}
それは巨大な変化だ.それはかなり簡単です.まず、検証メッセージを通してループし、最終応答となる1つの大きな配列にスタックします.
エラーが発生した場合は、現在のリクエストを停止し、JSONレスポンスをすべてのメッセージで返します.
削除しましょうdd() コントローラからもう一度テストしてください.
#[Route('/app', name: 'app')]
public function index(ExampleRequest $request): Response
{
    $request->validate();

    return $this->json([
        'message' => 'Welcome to your new controller!',
        'path' => 'src/Controller/AppController.php',
    ]);
}
さあリクエストをしましょう.

いいねそれはクールです、我々は現在妥当性検査メッセージを自動的に返しています.それだ!今、属性/注釈を持つプレーンなPHPクラスを使用することができますし、検証するたびに自分自身で呼び出す必要はありません.
ボーナス!
私はそれを削除したかった$request->validate() 行も.それはかなり簡単です!
我々は自動的にvalidate() メソッドをリクエストクラスで指定した場合、自動的に検証します.
このようにしましょう.
インBaseRequest 以下のメソッドを追加します.
protected function autoValidateRequest(): bool
{
    return true;
}
そして今、同じコンストラクタでBaseRequest 次のことができます.
abstract class BaseRequest
{
    public function __construct(protected ValidatorInterface $validator)
    {
        $this->populate();

        if ($this->autoValidateRequest()) {
            $this->validate();
        }
    }

   // Rest of BaseRequest
デフォルトでは、要求を検証し、エラーを表示します.このリクエストクラスを無効にする場合は、このメソッドを上書きできます.
<?php

namespace App\Requests;

use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Type;

class ExampleRequest extends BaseRequest
{
    #[Type('integer')]
    #[NotBlank()]
    protected $id;

    #[NotBlank([])]
    protected $firstName;

    protected function autoValidateRequest(): bool
    {
        return false;
    }
}
もちろん、それを調整することができますfalse デフォルトでは、あなたのピック.
今すぐコールする必要はありません$request->validate() 全く.
これは良い探しです!
    #[Route('/app', name: 'app')]
    public function index(ExampleRequest $request): Response
    {
        return $this->json([
            'message' => 'Welcome to your new controller!',
            'path' => 'src/Controller/AppController.php',
        ]);
    }
これはすべての変更後、baserequestです.
<?php

namespace App\Requests;

use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Validator\Validator\ValidatorInterface;

abstract class BaseRequest
{
    public function __construct(protected ValidatorInterface $validator)
    {
        $this->populate();

        if ($this->autoValidateRequest()) {
            $this->validate();
        }
    }

    public function validate()
    {
        $errors = $this->validator->validate($this);

        $messages = ['message' => 'validation_failed', 'errors' => []];

        /** @var \Symfony\Component\Validator\ConstraintViolation  */
        foreach ($errors as $message) {
            $messages['errors'][] = [
                'property' => $message->getPropertyPath(),
                'value' => $message->getInvalidValue(),
                'message' => $message->getMessage(),
            ];
        }

        if (count($messages['errors']) > 0) {
            $response = new JsonResponse($messages, 201);
            $response->send();

            exit;
        }
    }

    public function getRequest(): Request
    {
        return Request::createFromGlobals();
    }

    protected function populate(): void
    {
        foreach ($this->getRequest()->toArray() as $property => $value) {
            if (property_exists($this, $property)) {
                $this->{$property} = $value;
            }
        }
    }

    protected function autoValidateRequest(): bool
    {
        return true;
    }
}
これを使う方法です.
ステップ1 :プロパティを定義します.検証規則を使用して注釈を付けます.
<?php

namespace App\Requests;

use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Type;

class ExampleRequest
{
    #[Type('integer')]
    #[NotBlank()]
    protected $id;

    #[NotBlank([])]
    protected $firstName;
}
ステップ2 :要求クラスを拡張するBaseRequest
class ExampleRequest extends BaseRequest
それだ!ハッピーコーディング!