S3みたいなストレージサーバーっぽいものを自前で用意する②【ミドルウェア実装】

2022/03/26 2022/03/26 #S3 #ストレージサーバー

S3みたいなストレージサーバーっぽいものを自前で用意する②【ミドルウェア実装】

こんにちは。のんです。

前回に引き続き自前でストレージサーバーを開発していこうと思います。

GitHubプロジェクトはこちら

ミドルウェアを実装する

league/route の MiddlewareInterface

league/routePSR-15 準拠のミドルウェア実装に対応しているので、

にあるように、

The following interface MUST be implemented by compatible middleware components.

namespace Psr\Http\Server;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

/**
 * Participant in processing a server request and response.
 *
 * An HTTP middleware component participates in processing an HTTP message:
 * by acting on the request, generating the response, or forwarding the
 * request to a subsequent middleware and possibly acting on its response.
 */
interface MiddlewareInterface
{
    /**
     * Process an incoming server request.
     *
     * Processes an incoming server request in order to produce a response.
     * If unable to produce the response itself, it may delegate to the provided
     * request handler to do so.
     */
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface;
}

を実装するようにします。

process の引数である ServerRequestInterfacePSR-7RequestHandlerInterfacePSR-15 準拠です。

ServerRequestInterface

(※ 長いので省略)

RequestHandlerInterface

The following interface MUST be implemented by request handlers.

namespace Psr\Http\Server;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

/**
 * Handles a server request and produces a response.
 *
 * An HTTP request handler process an HTTP request in order to produce an
 * HTTP response.
 */
interface RequestHandlerInterface
{
    /**
     * Handles a request and produces a response.
     *
     * May call other collaborating code to generate the response.
     */
    public function handle(ServerRequestInterface $request): ResponseInterface;
}

ミドルウェアはよくドーナツ型の図で表現されます

スクリーンショット 2022-03-26 22.04.32.png

前処理や後処理では、実際にControllersやActionに行く前にしておきたい共通処理を書くことが多いですね。

  • ログの書き込み
  • 認証チェック
  • 共通データの取得

などなど。もちろん、このドーナツは二重三重にすることができます。

スクリーンショット 2022-03-26 22.33.54.png

実際に作成した AuthMiddleware がこちら。

<?php
declare(strict_types=1);

namespace Nonz250\Storage\App\Http\Auth;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

class AuthMiddleware implements MiddlewareInterface
{
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        // ミドルウェアでしたい処理。ここではDigest認証を行います。

        return $handler->handle($request);
    }
}

と言っても、まだDigest認証の実装はしていません。この実装については次回の記事で書こうと思います。

前回作成したルートにこの AuthMiddleware を設定します。

$router = new League\Route\Router();
$router
    ->group('/', static function (League\Route\RouteGroup $router) {
        // 認証が必要なアクションをここに登録します。
    })
    ->middleware(new Nonz250\Storage\App\Http\Auth\AuthMiddleware);

これで $router->group() で登録したアクションを実行するときに必ず AuthMiddleware が発火します。

認証エラーの場合は例外をスローする実装にしておけばアクションまでは実行されないという寸法です。

ついでにRequestBodyをParseするMiddlewareを作成する

フルスタックなフレームワークを利用していないので、 Content-Typeapplication/json の状態でアクションからRequestBodyを取得するとStringで取得してします。

<?php
declare(strict_types=1);

namespace Nonz250\Storage\App\Http\Test;

use Laminas\Diactoros\Response;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

class TestAction
{
    /**
     * @param ServerRequestInterface $request
     * @return ResponseInterface
     */
    public function __invoke(ServerRequestInterface $request): ResponseInterface
    {
        $response = new Response();
        // ここで取得できる$contentsがjson。つまりStringのまま。
        $contents = $request->getBody()->getContents();
        $response->getBody()->write($contents);
        return $response;
    }
}

Laravelなどのフレームワークなら自動的に配列に変換されていたり、プロパティとして取得できるのですが、それができていません。これだと非常に不便なので、同じようにMiddlewareを用意してその中でRequestBodyをjsonから配列に変換する処理を行います。

ServerRequestInterface には withParsedBodygetParsedBody という関数が実装されていますので、これを利用します。

実際に作成した ParseRequestMiddleware はこちら

<?php
declare(strict_types=1);

namespace Nonz250\Storage\App\Http\ParseRequest;

use JsonException;
use Nonz250\Storage\App\Foundation\Exceptions\HttpBadRequestException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

class ParseRequestMiddleware implements MiddlewareInterface
{
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        try {
            try {
                $contentTypes = $request->getHeader('Content-Type');
                if (count($contentTypes) === 0) {
                    throw new EmptyContentTypeException();
                }

                $contentType = $contentTypes[0];
                if ($contentType !== 'application/json') {
                    throw new InvalidContentTypeException();
                }

                $contents = $request->getBody()->getContents();
                $parsedBody = json_decode($contents, true, 512, JSON_THROW_ON_ERROR);
            } catch (EmptyContentTypeException|InvalidContentTypeException $e) {
                // TODO: ログ記録
                throw new HttpBadRequestException($e->getMessage());
            } catch (JsonException $e) {
                // TODO: ログ記録
                throw new HttpBadRequestException('Json syntax error.');
            }
        } catch (HttpBadRequestException $e) {
            return $e->getApiProblemResponse();
        }

        $request = $request->withParsedBody($parsedBody);

        return $handler->handle($request);
    }
}

一つずつ説明していきます。

$contentTypes = $request->getHeader('Content-Type');
if (count($contentTypes) === 0) {
    throw new EmptyContentTypeException();
}

このコードでRequestHeaderから Content-Type の値を取得します。

取得できない場合は Content-Type が無い旨を示す例外をスローします。

独自に作成した HttpExceptionHttpExceptionInterface を継承した例外です。

return $e->getApiProblemResponse();

にある RFC 7807 に従ったエラーレスポンスを返す関数を用意しています。

実は league/route にも HttpException は実装されていますが、上記のような独自処理を実装したかったし、 league に依存しすぎるのもどうかと思ったので自前で用意したという流れです。

この件についてもいずれ記事にしたいと思います。

次のコードです。

$contentType = $contentTypes[0];
if ($contentType !== 'application/json') {
    throw new InvalidContentTypeException();
}

このコードは Content-Type の内容が取得できたらその値が application/json かどうかを確認します。

違ったら不適切な Content-Type であることを示す例外をスローします。

$contents = $request->getBody()->getContents();
$parsedBody = json_decode($contents, true, 512, JSON_THROW_ON_ERROR);

バリデーションが終了したら実際にRequestBodyを取得します。
json_encode を利用して連想配列へ変換します。このとき、Parseでエラーが発生したら JsonException をスローするように設定しているので、それを catch したらその旨を HttpBadRequestException として投げ直しています。

無事、 $parsedBody を取得できたら、 withParsedBody でRequestクラスに保存しておきます。

$request = $request->withParsedBody($parsedBody);

こうすることで、

$contents = $request->getParsedBody();

で連想配列形式で取得できるようになります。

このMiddlewareをRouterに登録しておきます。

$router = new League\Route\Router();
// AuthMiddlewareと違い、全てのルートでONにしておく
$router->middleware(new Nonz250\Storage\App\Http\ParseRequest\ParseRequestMiddleware);
$router
    ->group('/', static function (League\Route\RouteGroup $router) {
        // 認証が必要なアクションをここに登録します。
    })
    ->middleware(new Nonz250\Storage\App\Http\Auth\AuthMiddleware);

最後に

今回はMiddleware、特に前処理を利用して、認証やRequestBodyの加工の実装を行いました。

連想配列を返すっていうのもまた微妙に使いづらいですが、ひとまずここで良しとしておきましょう。
必要がある場合はまた自前で拡張してプロパティから取得できるように実装すればいいでしょう。...が、その場合は素直にフレームワークを利用したほうが安心できるかもしれませんね。

次回はDigest認証の実装について記事を書こうと思います。

その後に RFC 7807 について記事にする予定です。

そのときはよしなに。

.

のん

所属 : 株式会社スマレジ 開発部

YouTube : のんラボ

Twitter : @nonz250

Github : nonz250

Qiita : @nonz250

My Qiita posts My Qiita contributions My Qiita followers

主にPHPを使用し、サーバーサイドを担当。最近はフロントにも興味津々。

なにかを作ったりいじったりするのが好きで、個人開発なども行っている。

趣味はバイクアイコン画像は大抵愛車の「Z250」である。友達にアイコン描いてもらえて嬉しい。

PHP / Laravel / CakePHP2 / CakePHP3 / Vue / Nuxt / C# / etc...

Tags

#のんラボ #Laravel #Vue #個人開発 #ブログ #プログラミング #javascript #Html5 #WEBサービス #Twitter #今年の抱負メーカー #勉強方法 #PWA #モバイルアプリ #Android #ツーリング #バイクに乗るエンジニア #Z250 #秋吉台 #能登半島 #バイク #冒険 #東尋坊 #Squid #リバースプロキシ #hosts #axios #cropper #AdSense #Bootstrap #MySQL #高速化 #トドTask #Telescope #デバッグ #composer #テスト #セキュリティ #POSレジ #スマレジ #本部機能 #バリデーション #入力チェック #Mac #Chrome #テスト駆動開発 #開発手法 #UI #デザイン #WEBサイト #機能美 #PHP #Laravel 6 #コメント #バージョンアップ #vue-cli #localhost #BIツール #売上分析 #TANAX #MFK250 #ツアーシェルケース2 #RESTful #API #REST API #実務的 #PHP Tech Tutor #Smaregi Tech Talk #勉強会 # ブログ #CakePHP3 #CSRF #VSCode #開発環境 #CakePHP3.0 #さくらのレンタルサーバー #モジュールモード #シェル #メール #Gmail #relay #OGP #エラーページ #抱負 #家庭教師 #ドメイン駆動設計 #DDD #読書会 #那智の滝 #伊勢志摩 #伊勢志摩スカイライン #フロント #三方五湖 #レインボーライン #ボーイスカウト・ルール #プログラマが知るべき97のこと #リファクタリング #ユビキタス言語 #車輪の再発明 #マイクロサービス #デプロイ #QA #laravel-mix #Tips #storybook #@storybook/addon-actions #昇降デスク #コードレス #書斎 #オフィス #リモートワーク #働き方 #エラーハンドリング #スマレジ4 #pixel 5 #レビュー #スマレコ #TDD #RSS #404 #高山ダム #ラーツー #React #Nuxt #node_modules #エラー #インポート #設定方法 #環境構築 #Docker #フォレストパーク神野山 #学生向け #PR #採用 #Node.js #npm #しまなみ海道 #youtube #CSS #IE #SLA #Rust #千里浜なぎさドライブウェイ #千里浜 #インサイドセールス #曽爾高原 #無線LAN #ポートフォリオ #バルカンS #納車 #Next.js #チームビルディング #リーダー #悩み #Github Actions #Marp #ネタ #サーバー移行 #S3 #ストレージサーバー #RFC 7807 #Digest #認証 #Xdebug #CLI #西日本 #例外