PHP
laravel5.5

Laravelのレスポンス拡張 (正常/異常/メンテ)

概要

Laravelでレスポンスを統一したかったので、Responseクラスの拡張方法を調べた結果をまとめます。

レスポンスマクロ

調べてみたらマニュアルに記述があったのでそのままやってみる。

ServiceProviderの作成

下記のようにServiceProviderのクラスを作成してみる。

app/Providers/ResponseApiServiceProvider.php
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Response;

/**
 * レスポンス (API)
 * @package App\Providers
 */
class ResponseApiServiceProvider extends ServiceProvider
{
    /**
     * アプリケーションのレスポンスマクロ登録
     *
     * @return void
     */
    public function boot()
    {
        // Success (200 OK.)
        Response::macro('success', function ($data) {
            return response()->json([
                'response' => $data
            ]);
        });

        // Error (4xx, 5xx)
        Response::macro('error', function ($message, array $errors = [], $status = ResponseStatus::HTTP_INTERNAL_SERVER_ERROR) {
            return response()->json([
                'message' => $message,
                'errors'  => (object) $errors
            ], $status);
        });

        // Under Maintenance (503 Service Unavailable.)
        Response::macro('maintenance', function () {
            return response()->json([
                'message' => 'Site is under maintenance.',
                'errors'  => (object) []
            ], ResponseStatus::HTTP_SERVICE_UNAVAILABLE);
        });
    }
}

作っただけだとだめなので、ServiceProviderを登録する。

app/config/app.php
<?php

return [

    ...

    'providers' => [
        /*
         * Application Service Providers...
         */

        ...

        App\Providers\RouteServiceProvider::class,
        App\Providers\ResponseApiServiceProvider::class,   <- 追加

    ],

    ...
];

使ってみる

まず、正常系を実装してみる。

app/Http/Controllers/HogeController.php
<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use Request;

/**
 * Hoge Class
 * @package App\Http\Controllers
 */
class HogeController extends Controller
{
    /**
     * 正常系
     *
     * @access  public
     * @param   Request $request
     * @return  \Illuminate\Http\JsonResponse
     */
    public function success(Request $request)
    {
        $id = $request->input('id', '');
        if (empty($id)) {
            throw ValidationException::withMessages([
                'id' => 'id is required.',
            ]);
        }

        if ($id == '999') {
            throw new NotFoundHttpException('id is not exist.');
        }

        return response()->success([
            'email' => 'hoge@example.com'
        ]);
    }
}

次に異常系を実装してみる。

app/Exceptions/Handler.php
<?php

namespace App\Exceptions;

use Exception;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\Response;
use Illuminate\Validation\ValidationException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Http\Exceptions\HttpResponseException;
use App\Providers\ResponseApiServiceProvider;

/**
 * 例外ハンドラー
 *
 * @package App\Exceptions
 */
class Handler extends ExceptionHandler
{
    /**
     * Report or log an exception.
     *
     * @param  \Exception $exception
     * @return void
     */
    public function report(Exception $exception)
    {
        parent::report($exception);
    }

    /**
     * Render an exception into an HTTP response.
     *
     * @param  \Illuminate\Http\Request $request
     * @param  \Exception $e
     * @return \Symfony\Component\HttpFoundation\Response
     */
    public function render($request, Exception $e)
    {
        // [お知らせ] レスポンス形式を統一するため、コメントアウトしました
        #if (method_exists($e, 'render') && $response = $e->render($request)) {
        #    return Router::toResponse($request, $response);
        #} elseif ($e instanceof Responsable) {
        #    return $e->toResponse($request);
        #}

        $e = $this->prepareException($e);
        if ($e instanceof HttpResponseException) {
            return $e->getResponse();
        } elseif ($e instanceof AuthenticationException) {
            $status  = Response::HTTP_UNAUTHORIZED;
            $message = Response::$statusTexts[$status];
            $errors  = [];
        } elseif ($e instanceof ValidationException) {
            // [お知らせ] レスポンス形式を統一するため、コメントアウトしました
            #if ($e->response) {
            #    return $e->response;
            #}

            $status  = $e->status;
            $message = Response::$statusTexts[$status];
            $errors  = $e->errors();
        } elseif ($this->isHttpException($e)) {
            $status  = $e->getStatusCode();
            $message = (isset(Response::$statusTexts[$status])) ? Response::$statusTexts[$status] : '';
            $errors  = [];
        } else {
            $status  = Response::HTTP_INTERNAL_SERVER_ERROR;
            $message = 'Server Error';
            $errors  = [];
        }

        // ResponseApiServiceProviderが実行される前にエラーが発生した場合の対応
        if (! method_exists(response(), 'error')) {
            $app = app();
            $provide = new ResponseApiServiceProvider($app);
            $provide->boot();
        }

        return response()->error($message, $errors, $status);
    }
}

メンテナンス中を実装してみる。

App\Http\Middleware\UnderMaintenance
<?php

namespace App\Http\Middleware;

use Closure;

class UnderMaintenance
{
    /**
     * メンテナンスチェック
     *
     * @param  \Illuminate\Http\Request $request
     * @param  \Closure $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        if (env('APP_MAINTENANCE', false) === true) {
            // 503 Service Unavailable.
            return response()->maintenance();
        }

        return $next($request);
    }
}

すべての処理で上記を実行するので、Kernelに登録する。

App\Http\Kernel.php
<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
    /**
     * The application's global HTTP middleware stack.
     *
     * These middleware are run during every request to your application.
     *
     * @var array
     */
    protected $middleware = [

        ...

        \App\Http\Middleware\UnderMaintenance::class, // <- ここに追加
    ];

.envにフラグを追加

.env
APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:*********************************
APP_DEBUG=true
APP_LOG_LEVEL=debug
APP_URL=http://localhost
APP_MAINTENANCE=false                                  <- ここに追加

以上

レスポンスについて

成功時は、下記のようなフォーマットのレスポンスが返ります。

成功(200_OK)
{
  "response": {
    "email": "hoge@example.com"
  }
}

エラー時は、下記のようなフォーマットのレスポンスが返ります。

エラー(403_Forbidden.)
{
    "message":"Forbidden",
    "errors":{}
}
エラー(404_not_found.)
{
    "message" : "Not Found"
    "errors" : { }
}
エラー(422_Unprocessable_Entity.)
{
  "message": "Unprocessable Entity"
  "errors": {
    "id": [
      "id is required."
    ]
  }
}

メンテナンス中の場合、下記フォーマットのレスポンスが返ります。

エラー(503_Site_is_under_maintenance.)
{
    "message": "Site is under maintenance.",
    "errors": {}
}

以上

修正履歴

  • 07/12 DB接続できなかった際の例外処理時にException/Handler.phpでエラー発生するので修正。
2018-07-12 03:40:27     local   ERROR   Database [localhost] not configured.    {"exception":"[object] (InvalidArgumentException(code: 0): Database [localhost] not configured. at /var/www/www.example.com/vendor/laravel/framework/src/Illuminate/Database/DatabaseManager.php:140)"}        {"file":"/var/www/www.example.com/app/Exceptions/Handler.php","line":34,"class":"App\\Exceptions\\Handler","function":"report"}
2018-07-12 03:40:27     local   ERROR   Method error does not exist.    []      {"file":"/var/www/www.example.com/app/Exceptions/Handler.php","line":32,"class":"App\\Exceptions\\Handler","function":"report"}
2018-07-12 03:40:27     local   ERROR   Method error does not exist.    {"exception":"[object] (BadMethodCallException(code: 0): Method error does not exist. at /var/www/www.example.com/vendor/laravel/framework/src/Illuminate/Support/Traits/Macroable.php:96)"}   {"file":"/var/www/www.example.com/app/Exceptions/Handler.php","line":34,"class":"App\\Exceptions\\Handler","function":"report"}
  • 07/13 メンテンス中のレスポンスを追加

参考サイト