グローバルナビゲーションへ

本文へ

フッターへ

お役立ち情報Blog



PHPのマイクロフレームワーク「Slim4」の「slim-skeleton」を使用してハマったこと

Slim は PHP のマイクロフレームワークです。
アプリケーション開発までのセットアップを短時間でできるように、公式で slim-skeleton を提供しています。

今回は PHP のバージョン 7.2 以降で使用可能な Slim4 を使用してハマったことの記録です。


記事で使用した slim-skeleton のバージョンは 4.1.0 になります。


slim-skeleton のインストール

composer 公式イメージを使ってslim-skeletonをインストールしてきます。

$ docker run --rm -u $(id -u):$(id -g) -v $(pwd):/app composer create-project slim/slim-skeleton

開発環境(Docker)の準備

slim-skeleton の公式リポジトリから作成したプロジェクトには docker-compose.yml が付属しています。

docker-compose.yml

version: '3.7'

volumes:
    logs:
        driver: local

services:
    slim:
        image: php:7-alpine
        working_dir: /var/www
        command: php -S 0.0.0.0:8080 -t public
        environment:
            docker: "true"
        ports:
            - 8080:8080
        volumes:
            - .:/var/www
            - logs:/var/www/logs

この記事ではデフォルト提供の docker-compose.yml で定義されているPHPのビルトインサーバを使用します。

以下のコマンドで Docker を起動します。

$ docker-compose up -d

curl コマンドでPHPのビルトインサーバが起動されていることを確認します。

$ curl http://localhost:8080
Hello world!

$ curl http:/localhost:8080/users
{
    "statusCode": 200,
    "data": [
        {
            "id": 1,
            "username": "bill.gates",
            "firstName": "Bill",
            "lastName": "Gates"
        },
        {
            "id": 2,
            "username": "steve.jobs",
            "firstName": "Steve",
            "lastName": "Jobs"
        },
        {
            "id": 3,
            "username": "mark.zuckerberg",
            "firstName": "Mark",
            "lastName": "Zuckerberg"
        },
        {
            "id": 4,
            "username": "evan.spiegel",
            "firstName": "Evan",
            "lastName": "Spiegel"
        },
        {
            "id": 5,
            "username": "jack.dorsey",
            "firstName": "Jack",
            "lastName": "Dorsey"
        }
    ]
}

開発環境の準備は以上です。

サブディレクトリ運用でハマったこと

Slim3 まではサブディレクトリでシステムを扱う場合でも特別な設定は必要なかったのですが、 Slim4 をサブディレクトリで運用する場合には別途設定が必要になったようです。

public/index.php

+ $app->setBasePath('/myapp');

ハードコードが嫌な場合は以下の関数で設定すればいいようです。

$app->setBasePath((function () {
    $scriptDir = str_replace('\\', '/', dirname($_SERVER['SCRIPT_NAME']));
    $uri = (string) parse_url('http://a' . $_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH);
    if (stripos($uri, $_SERVER['SCRIPT_NAME']) === 0) {
        return $_SERVER['SCRIPT_NAME'];
    }
    if ($scriptDir !== '/' && stripos($uri, $scriptDir) === 0) {
        return $scriptDir;
    }
    return '';
})());

Loggerの設定でハマったこと

デフォルトでは Slim のLoggerを使用しており、PHPの error_log() 関数が使用されるようになっています。

Logger を Monolog に変更しようとしたときの試行錯誤です。
Monolog は slim-skeleton でプロジェクトを立ち上げると vendor に既に追加されているので、Monolog を使うように変更します。

diff --git a/public/index.php b/public/index.php
index 57722ec..a90b930 100644
--- a/public/index.php
+++ b/public/index.php
@@ -5,6 +5,7 @@ use App\Application\Handlers\HttpErrorHandler;
 use App\Application\Handlers\ShutdownHandler;
 use App\Application\ResponseEmitter\ResponseEmitter;
 use DI\ContainerBuilder;
+use Psr\Log\LoggerInterface;
 use Slim\Factory\AppFactory;
 use Slim\Factory\ServerRequestCreatorFactory;

@@ -54,7 +55,8 @@ $request = $serverRequestCreator->createServerRequestFromGlobals();

 // Create Error Handler
 $responseFactory = $app->getResponseFactory();
-$errorHandler = new HttpErrorHandler($callableResolver, $responseFactory);
+$logger = $container->get(LoggerInterface::class);
+$errorHandler = new HttpErrorHandler($callableResolver, $responseFactory, $logger);

 // Create Shutdown Handler
 $shutdownHandler = new ShutdownHandler($request, $errorHandler, $displayErrorDetails);
@@ -64,7 +66,10 @@ register_shutdown_function($shutdownHandler);
 $app->addRoutingMiddleware();

 // Add Error Middleware
-$errorMiddleware = $app->addErrorMiddleware($displayErrorDetails, false, false);
+// 第二引数と第三引数を true にする
+// 第二引数 $logErrors はエラーを記録するか
+// 第三引数は $logErrorDetails エラー詳細を表示するか
+$errorMiddleware = $app->addErrorMiddleware($displayErrorDetails, true, true);
 $errorMiddleware->setDefaultErrorHandler($errorHandler);

 // Run App & Emit Response

意図的に例外を発生させてログに出力されるか動作を確認してみます。

ListUserAction.php

diff --git a/src/Application/Actions/User/ListUsersAction.php b/src/Application/Actions/User/ListUsersAction.php
index 21b4750..01a1094 100644
--- a/src/Application/Actions/User/ListUsersAction.php
+++ b/src/Application/Actions/User/ListUsersAction.php
@@ -15,6 +15,8 @@ class ListUsersAction extends UserAction
         $users = $this->userRepository->findAll();

         $this->logger->info("Users list was viewed.");
+        echo EXPECT_NOTICE_ERROR;
+        throw new \RuntimeException('An error was occured.');

         return $this->respondWithData($users);
     }
 http://localhost:8080/users にリクエストを飛ばしてエラーログが出るか確認してみます。

$ curl http://localhost:8080/users

<br />
<b>Warning</b>:  Use of undefined constant EXPECT_NOTICE_ERROR - assumed 'EXPECT_NOTICE_ERROR' (this will throw an Error in a future version of PHP) in <b>/var/www/src/Application/Actions/User/ListUsersAction.php</b> on line <b>18</b><br />
{
    "statusCode": 500,
    "error": {
        "type": "SERVER_ERROR",
        "description": "An error was occured."
    }
}{
    "statusCode": 500,
    "error": {
        "type": "SERVER_ERROR",
        "description": "ERROR: Use of undefined constant EXPECT_NOTICE_ERROR - assumed 'EXPECT_NOTICE_ERROR' (this will throw an Error in a future version of PHP) on line 18 in file \/var\/www\/src\/Application\/Actions\/User\/ListUsersAction.php."
    }
}

slim-skeleton のログ出力は特に設定していないと Docker 経由だと標準出力に出力されます。 以下のコマンドでログを確認します。

$ docker-compose logs slim

Attaching to slim-skeleton_slim_1
slim_1  | [Mon Jan  4 09:16:38 2021] PHP 7.4.13 Development Server (http://0.0.0.0:8080) started
slim_1  | [Mon Jan  4 09:16:47 2021] 192.168.80.1:38852 Accepted
slim_1  | [2021-01-04T09:16:47.315350+00:00] slim-app.INFO: Users list was viewed. [] {"uid":"40c8453"}
slim_1  | [2021-01-04T09:16:47.316259+00:00] slim-app.ERROR: Slim Application Error Type: RuntimeException Code: 0 Message: An error was occured. File: /var/www/src/Application/Actions/User/ListUsersAction.php Line: 19 Trace: #0 /var/www/src/Application/Actions/Action.php(58): App\Application\Actions\User\ListUsersAction->action() #1 /var/www/vendor/slim/slim/Slim/Handlers/Strategies/RequestResponse.php(43): App\Application\Actions\Action->__invoke(Object(Slim\Psr7\Request), Object(Slim\Psr7\Response), Array) #2 /var/www/vendor/slim/slim/Slim/Routing/Route.php(384): Slim\Handlers\Strategies\RequestResponse->__invoke(Array, Object(Slim\Psr7\Request), Object(Slim\Psr7\Response), Array) #3 /var/www/vendor/slim/slim/Slim/MiddlewareDispatcher.php(81): Slim\Routing\Route->handle(Object(Slim\Psr7\Request)) #4 /var/www/vendor/slim/slim/Slim/MiddlewareDispatcher.php(81): Slim\MiddlewareDispatcher->handle(Object(Slim\Psr7\Request)) #5 /var/www/vendor/slim/slim/Slim/Routing/Route.php(341): Slim\MiddlewareDispatcher->handle(Object(Slim\Psr7\Request)) #6 /var/www/vendor/slim/slim/Slim/Routing/RouteRunner.php(84): Slim\Routing\Route->run(Object(Slim\Psr7\Request)) #7 /var/www/src/Application/Middleware/SessionMiddleware.php(23): Slim\Routing\RouteRunner->handle(Object(Slim\Psr7\Request)) #8 /var/www/vendor/slim/slim/Slim/MiddlewareDispatcher.php(209): App\Application\Middleware\SessionMiddleware->process(Object(Slim\Psr7\Request), Object(Slim\Routing\RouteRunner)) #9 /var/www/vendor/slim/slim/Slim/Middleware/RoutingMiddleware.php(59): class@anonymous->handle(Object(Slim\Psr7\Request)) #10 /var/www/vendor/slim/slim/Slim/MiddlewareDispatcher.php(147): Slim\Middleware\RoutingMiddleware->process(Object(Slim\Psr7\Request), Object(class@anonymous)) #11 /var/www/vendor/slim/slim/Slim/Middleware/ErrorMiddleware.php(107): class@anonymous->handle(Object(Slim\Psr7\Request)) #12 /var/www/vendor/slim/slim/Slim/MiddlewareDispatcher.php(147): Slim\Middleware\ErrorMiddleware->process(Object(Slim\Psr7\Request), Object(class@anonymous)) #13 /var/www/vendor/slim/slim/Slim/MiddlewareDispatcher.php(81): class@anonymous->handle(Object(Slim\Psr7\Request)) #14 /var/www/vendor/slim/slim/Slim/App.php(215): Slim\MiddlewareDispatcher->handle(Object(Slim\Psr7\Request)) #15 /var/www/public/index.php(76): Slim\App->handle(Object(Slim\Psr7\Request)) #16 {main} [] {"uid":"40c8453"}
slim_1  | [Mon Jan  4 09:16:47 2021] 192.168.80.1:38852 [200]: GET /users
slim_1  | [Mon Jan  4 09:16:47 2021] 192.168.80.1:38852 Closing

例外がログに出力されるようになりましたが、PHP の Notice と Warning がログに出力されていません。
slim-skeleton で提供されている ShutdownHandler.php の以下の引数を変更すると出力されるようになりました。

src/Application/Handlers/ShutdownHandler.php

diff --git a/src/Application/Handlers/ShutdownHandler.php b/src/Application/Handlers/ShutdownHandler.php
index 323f16a..73d5b29 100644
--- a/src/Application/Handlers/ShutdownHandler.php
+++ b/src/Application/Handlers/ShutdownHandler.php
@@ -74,7 +74,7 @@ class ShutdownHandler
             }

             $exception = new HttpInternalServerErrorException($this->request, $message);
-            $response = $this->errorHandler->__invoke($this->request, $exception, $this->displayErrorDetails, false, false);
+            $response = $this->errorHandler->__invoke($this->request, $exception, $this->displayErrorDetails, true, true);

             $responseEmitter = new ResponseEmitter();
             $responseEmitter->emit($response);
slim_1  | [Mon Jan  4 09:22:06 2021] PHP 7.4.13 Development Server (http://0.0.0.0:8080) started
slim_1  | [Mon Jan  4 09:22:19 2021] 192.168.96.1:34806 Accepted
slim_1  | [2021-01-04T09:22:19.114663+00:00] slim-app.INFO: Users list was viewed. [] {"uid":"c8a86bb"}
slim_1  | [2021-01-04T09:22:19.115963+00:00] slim-app.ERROR: Slim Application Error Type: RuntimeException Code: 0 Message: An error was occured. File: /var/www/src/Application/Actions/User/ListUsersAction.php Line: 19 Trace: #0 /var/www/src/Application/Actions/Action.php(58): App\Application\Actions\User\ListUsersAction->action() #1 /var/www/vendor/slim/slim/Slim/Handlers/Strategies/RequestResponse.php(43): App\Application\Actions\Action->__invoke(Object(Slim\Psr7\Request), Object(Slim\Psr7\Response), Array) #2 /var/www/vendor/slim/slim/Slim/Routing/Route.php(384): Slim\Handlers\Strategies\RequestResponse->__invoke(Array, Object(Slim\Psr7\Request), Object(Slim\Psr7\Response), Array) #3 /var/www/vendor/slim/slim/Slim/MiddlewareDispatcher.php(81): Slim\Routing\Route->handle(Object(Slim\Psr7\Request)) #4 /var/www/vendor/slim/slim/Slim/MiddlewareDispatcher.php(81): Slim\MiddlewareDispatcher->handle(Object(Slim\Psr7\Request)) #5 /var/www/vendor/slim/slim/Slim/Routing/Route.php(341): Slim\MiddlewareDispatcher->handle(Object(Slim\Psr7\Request)) #6 /var/www/vendor/slim/slim/Slim/Routing/RouteRunner.php(84): Slim\Routing\Route->run(Object(Slim\Psr7\Request)) #7 /var/www/src/Application/Middleware/SessionMiddleware.php(23): Slim\Routing\RouteRunner->handle(Object(Slim\Psr7\Request)) #8 /var/www/vendor/slim/slim/Slim/MiddlewareDispatcher.php(209): App\Application\Middleware\SessionMiddleware->process(Object(Slim\Psr7\Request), Object(Slim\Routing\RouteRunner)) #9 /var/www/vendor/slim/slim/Slim/Middleware/RoutingMiddleware.php(59): class@anonymous->handle(Object(Slim\Psr7\Request)) #10 /var/www/vendor/slim/slim/Slim/MiddlewareDispatcher.php(147): Slim\Middleware\RoutingMiddleware->process(Object(Slim\Psr7\Request), Object(class@anonymous)) #11 /var/www/vendor/slim/slim/Slim/Middleware/ErrorMiddleware.php(107): class@anonymous->handle(Object(Slim\Psr7\Request)) #12 /var/www/vendor/slim/slim/Slim/MiddlewareDispatcher.php(147): Slim\Middleware\ErrorMiddleware->process(Object(Slim\Psr7\Request), Object(class@anonymous)) #13 /var/www/vendor/slim/slim/Slim/MiddlewareDispatcher.php(81): class@anonymous->handle(Object(Slim\Psr7\Request)) #14 /var/www/vendor/slim/slim/Slim/App.php(215): Slim\MiddlewareDispatcher->handle(Object(Slim\Psr7\Request)) #15 /var/www/public/index.php(76): Slim\App->handle(Object(Slim\Psr7\Request)) #16 {main} [] {"uid":"c8a86bb"}
slim_1  | [Mon Jan  4 09:22:19 2021] 192.168.96.1:34806 [200]: GET /users
slim_1  | [2021-01-04T09:22:19.117483+00:00] slim-app.ERROR: 500 Internal Server Error Type: Slim\Exception\HttpInternalServerErrorException Code: 500 Message: ERROR: Use of undefined constant EXPECT_NOTICE_ERROR - assumed 'EXPECT_NOTICE_ERROR' (this will throw an Error in a future version of PHP) on line 18 in file /var/www/src/Application/Actions/User/ListUsersAction.php. File: /var/www/src/Application/Handlers/ShutdownHandler.php Line: 76 Trace: #0 [internal function]: App\Application\Handlers\ShutdownHandler->__invoke() #1 {main} [] {"uid":"c8a86bb"}

プロダクション環境では app/settings.php の displayErrorsの値を false にしてエンドユーザーにはエラー詳細を見せない形にすると思いますが、 困ったことに displayErrors の値を false にすると、ログに出力されるエラーの詳細も非表示になってしまいます。

app/settings.php

diff --git a/app/settings.php b/app/settings.php
index 9db7ad1..7b400c9 100644
--- a/app/settings.php
+++ b/app/settings.php
@@ -8,7 +8,7 @@ return function (ContainerBuilder $containerBuilder) {
     // Global Settings Object
     $containerBuilder->addDefinitions([
         'settings' => [
-            'displayErrorDetails' => true, // Should be set to false in production
+            'displayErrorDetails' => false, // Should be set to false in production
             'logger' => [
                 'name' => 'slim-app',
                 'path' => isset($_ENV['docker']) ? 'php://stdout' : __DIR__ . '/../logs/app.log',
slim_1  | [2021-01-05T00:44:43.291938+00:00] slim-app.ERROR: 500 Internal Server Error Type: Slim\Exception\HttpInternalServerErrorException Code: 500 Message: An error while processing your request. Please try again later. File: /var/www/src/Application/Handlers/ShutdownHandler.php Line: 76 Trace: #0 [internal function]: App\Application\Handlers\ShutdownHandler->__invoke() #1 {main} Tips: To display error details in HTTP response set "displayErrorDetails" to true in the ErrorHandler constructor. [] {"uid":"e570d39"}

内部的には Slim\App::addErrorMiddleware メソッド で Slim\Middleware\ErrorMiddleware インスタンス作成時のコンストラクタの第4、第5引数に $logErrors と $logErrorDetails が渡されています。

ErrorMiddleware が使用されるのは  register_shutdown_function()  で登録された ShutdownHandler.php のここで ShutdownHandler クラスのコンストラクタに渡した HttpErrorHandler のインスタンスを実行しています。

HttpErrorHandler クラスでは  __invoke メソッド は実装されておらず、その継承元の Slim\Handlers\ErrorMiddleware のここでログ出力の有無を振り分けているようです。

この辺り slim-skeleton のアプリケーションコードから Slim4 のコードに飛んだり、Slim4 のコードが基底クラスとなって slim-skeleton の子クラスを呼び出していたりと理解が難しい印象です。

ShutdownHandler の $displayErrorDetails  を HttpErrorHandler に渡しているので、ログ出力の時とレスポンスの時とでエラー詳細の表示/非表示が同じ値を参照するようになっていて、レスポンスにはエラー詳細を非表示、ログ出力時にはエラー詳細を表示するといった Noticeや Warning の細かいログ出力設定方法を見つけられませんでした。

slim-skeleton のディレクトリデザイン

ハマったことではないですが、公式の slimphp/slim-skeleton のディレクトリデザインは ADRパターン (Action Domain Responder) に沿って作られているようです。

ShutdownHandler.php と HttpErrorHandler.php と Actions が密接に絡み合っているので、別のディレクトリデザインパターンを採用する時には slim-skeleton 経由でプロジェクトを作成するより Slim4 をインストールして1からプロジェクトを作成した方がいいかもしれません。

まとめ

Slim4 slim-skeleton を使用してハマったことの一部をご紹介しました。

同じような問題を抱えている方の助けになれば幸いです。

この記事を書いた人

美髭公
美髭公事業開発部 web application engineer
2013年にアーティスに入社。システムエンジニアとしてアーティスCMSを使用したWebサイトや受託システムの構築・保守に携わる。環境構築が好き。
この記事のカテゴリ

FOLLOW US

最新の情報をお届けします