PHP で Slim を使ったルーティング

開発環境

項目バージョン
macOS14.2
php8.3.2
composer2.6.6

Slimのインストールと動作確認

下記コマンドでSlimをインストールします。

zsh
composer create-project slim/slim-skeleton test_slim 
項目バージョン
slim/slim4.13.0

Slim Framework - Slim Framework

test_slimディレクトリへ移動して、ビルトインサーバーを起動します。
zsh
cd test_slim
php -S localhost:8885 -t public
curl http://localhost:8885/ を実行すると Hello world! が返却されるはずです。

もっと簡単に、こちらのコマンドでもサーバーを起動できます。

zsh
composer start
> php -S localhost:8080 -t public

ポート番号を変えるには composer.json ファイルで以下の部分を修正します。

composer.json
....
"scripts": {
    "start": "php -S localhost:8885 -t public",
    "test": "phpunit"
}

Slimを使ってみる

ルーターの設定

プロジェクトの app/routes.php をのぞくと、ルーターの設定が記述されていました。

php
...
return function (App $app) {
    $app->options('/{routes:.*}', function (Request $request, Response $response) {
        // CORS Pre-Flight OPTIONS Request Handler
        return $response;
    });

    $app->get('/', function (Request $request, Response $response) {
        $response->getBody()->write('Hello world!');
        return $response;
    });

    $app->group('/users', function (Group $group) {
        $group->get('', ListUsersAction::class);
        $group->get('/{id}', ViewUserAction::class);
    });
};

HTTPリクエストして動作確認

サンプルとして users APIが実装されていることが分かります。ここに記述されている通り、APIへリクエストしてみます。

$ curl http://localhost:8885/users

次のようなJSONデータが返ってきました。

json
{
    "statusCode": 200,
    "data": [
        {
            "id": 1,
            "username": "bill.gates",
            "firstName": "Bill",
            "lastName": "Gates"
        },
...
        {
            "id": 5,
            "username": "jack.dorsey",
            "firstName": "Jack",
            "lastName": "Dorsey"
        }
    ]
}

ユーザーIDでリクエスト

さらにこの中から、id=5 のユーザー情報をリクエストしてみます。

$ curl http://localhost:8885/users/5
json
{
    "statusCode": 200,
    "data": {
        "id": 5,
        "username": "jack.dorsey",
        "firstName": "Jack",
        "lastName": "Dorsey"
    }
}
id=5のユーザーだけフィルタリングされて返ってきました。見事にAPIとして機能してますね!

Slimのビジネスロジック(モデル)

ところでビジネスロジックは、どこでどのように実装されているのでしょうか?ルーターの記述をもう一度確認してみます。

php
$app->group('/users', function (Group $group) {
    $group->get('', ListUsersAction::class);
    $group->get('/{id}', ViewUserAction::class);
});
ListUsersAction::classViewUserAction::classにロジックが実装されている予感。PhpStormでプロジェクトを開いて、ViewUserActionクラスへ飛んでみます。
ViewUserAction.php
...
class ViewUserAction extends UserAction
{
    /**
     * {@inheritdoc}
     */
    protected function action(): Response
    {
        $userId = (int) $this->resolveArg('id');
        $user = $this->userRepository->findUserOfId($userId);

        $this->logger->info("User of id `${userId}` was viewed.");

        return $this->respondWithData($user);
    }
}

ビジネスロジックを探す

$this->userRepository->findUserOfId($userId); この部分で、ユーザーIDによるフィルタリングを行なっていることが分かります。

ちなみに ViewUserAction クラスは、抽象クラス Action を継承した UserAction を継承して実装されていました。Action クラスでは、HTTPリクエストやJSON処理などが実装されています。また UserAction クラスでは、DIによる UserRepository が実装されます。本来は UserRepository クラスでデータベースを扱いますが、サンプルなのでハードコーディングによるモックデータになります。 今回表示されたユーザーデータの本体は InMemoryUserRepository クラスに実装されていました。InMemoryUserRepositoryUserRepository を継承しており、public function findUserOfId(int $id): User 関数が実装されています。ロジックの正体はここにありました。

Slimの依存ライブラリ一覧

Slimをインストールすると一緒にインストールされるライブラリの一覧をまとめてみました。これらの情報は composer show を実行すると得られます。説明はChatGPT翻訳によるものなので、おかしな文章もありますがご容赦ください。

ライブラリ名バージョン説明
doctrine/deprecations1.1.3trigger_error(E_USER_DEPRECATED) または PSR-3 ロギングの上に小さなレイヤーを提供し、すべてを無効にするオプションがあります...
doctrine/instantiator2.0.0コンストラクタを呼び出さずに PHP でオブジェクトをインスタンス化するための小さく軽量なユーティリティ
fig/http-message-util1.1.5PSR-7 (psr/http-message) を使用するためのユーティリティクラスと定数
jangregor/phpstan-prophecy1.0.0phpspec/prophecy のための phpstan/phpstan 拡張機能を提供します
laravel/serializable-closurev1.3.3Laravel Serializable Closure は PHP でクロージャをシリアライズするための簡単で安全な方法を提供します。
monolog/monolog2.9.2ログをファイル、ソケット、受信箱、データベースおよび様々なウェブサービスに送信します
myclabs/deep-copy1.11.1オブジェクトのディープコピー(クローン)を作成します
nikic/fast-routev1.3.0PHPのための高速リクエストルーター
nikic/php-parserv5.0.1PHPで書かれたPHPパーサー
phar-io/manifest2.0.4PHPアーカイブ(PHAR)からphar.ioマニフェスト情報を読み取るためのコンポーネント
phar-io/version3.2.1バージョン情報および制約を扱うためのライブラリ
php-di/invoker2.3.4汎用的かつ拡張可能な呼び出し可能インボーカー
php-di/php-di6.4.0人間のための依存性注入コンテナ
php-di/phpdoc-reader2.2.1PhpDocReaderはPHPドックブロック内の@varと@paramの値を解析します(名前空間付きクラス名をサポートしています)
phpdocumentor/reflection-common2.2.0コード構造を反映するためにphpdocumentorによって使用される共通のリフレクションクラス
phpdocumentor/reflection-docblock5.3.0このコンポーネントを使用すると、ライブラリはDocBlocksを介してアノテーションをサポートしたり、それ以外の方法で...
phpdocumentor/type-resolver1.8.2クラス名、タイプ、および構造要素名のPSR-5ベースのリゾルバー
phpspec/prophecyv1.19.0PHP 5.3+のための非常に意見が分かれるモッキングフレームワーク
phpspec/prophecy-phpunitv2.2.0ProphecyモッキングライブラリをPHPUnitテストケースに統合します
phpstan/extension-installer1.3.1PHPStan拡張機能の自動インストールのためのComposerプラグイン
phpstan/phpdoc-parser1.26.0Nullable、交差およびジェネリックタイプをサポートするPHPDocパーサー
phpstan/phpstan1.10.59PHPStan - PHP静的解析ツール
phpunit/php-code-coverage9.2.31PHPコードカバレッジ情報の収集、処理、およびレンダリング機能を提供するライブラリ
phpunit/php-file-iterator3.0.6リストの接尾辞に基づいてファイルをフィルタリングするFilterIterator実装
phpunit/php-invoker3.1.1タイムアウト付きでcallableを呼び出す
phpunit/php-text-template2.0.4シンプルなテンプレートエンジン
phpunit/php-timer5.0.3タイミング用のユーティリティクラス
phpunit/phpunit9.6.17PHPのユニットテストフレームワーク
psr/container1.1.2共通コンテナインターフェース (PHP FIG PSR-11)
psr/http-factory1.0.2PSR-7 HTTPメッセージファクトリ用の共通インターフェース
psr/http-message1.1HTTPメッセージ用の共通インターフェース
psr/http-server-handler1.0.2HTTPサーバーサイドリクエストハンドラの共通インターフェース
psr/http-server-middleware1.0.2HTTPサーバーサイドミドルウェアの共通インターフェース
psr/log3.0.0ロギングライブラリの共通インターフェース
ralouphie/getallheaders3.0.3getallheadersのポリフィル
sebastian/cli-parser1.0.2CLIオプションを解析するためのライブラリ
sebastian/code-unit1.0.8PHPのコードユニットを表す値オブジェクトのコレクション
sebastian/code-unit-reverse-lookup2.0.3コードの行がどの関数またはメソッドに属するかを調べる
sebastian/comparator4.0.8PHPの値を等価性で比較する機能を提供
sebastian/complexity2.0.3PHPのコードユニットの複雑さを計算するためのライブラリ
sebastian/diff4.0.6Diffの実装
sebastian/environment5.1.5HHVM/PHPの環境を扱う機能を提供
sebastian/exporter4.0.6PHP変数を視覚化するためのエクスポート機能を提供
sebastian/global-state5.0.7グローバルステートのスナップショット
sebastian/lines-of-code1.0.4PHPソースコードのコード行を数えるためのライブラリ
sebastian/object-enumerator4.0.4配列構造とオブジェクトグラフをトラバースして参照されているすべてのオブジェクトを列挙
sebastian/object-reflector2.0.4継承されたものや非公開のものを含む、オブジェクト属性のリフレクションを可能にする
sebastian/recursion-context4.0.5PHP変数を再帰的に処理する機能を提供
sebastian/resource-operations3.0.3リソースを操作するPHPの組み込み関数のリストを提供
sebastian/type3.2.1PHPの型システムの型を表す値オブジェクトのコレクション
sebastian/version3.0.2GitでホストされているPHPプロジェクトのバージョン番号を管理するのに役立つライブラリ
slim/psr71.6.1厳格なPSR-7実装
slim/slim4.13.0Slimは、簡単かつパワフルなWebアプリケーションおよびAPIを迅速に作成するのに役立つPHPマイクロフレームワークです
squizlabs/php_codesniffer3.9.0PHP, JavaScript, CSSファイルをトークン化し、定義されたコーディング規約の違反を検出します
symfony/polyfill-php80v1.29.0いくつかのPHP 8.0+機能をより低いPHPバージョンにバックポートするSymfony polyfill
theseer/tokenizer1.2.3トークン化されたPHPソースコードをXMLやその他のフォーマットに変換するための小さなライブラリ
webmozart/assert1.11.0メソッドの入力/出力を検証するためのアサーションで、わかりやすいエラーメッセージを提供します

RESTfulでCREATE(POST)を実装する

RESTfulなAPIを作成するために、CREATEメソッドをPOSTで実装してみます。

ルーターを調整

routes.php にURIを追加します。
routes.php
$app->group('/users', function (Group $group) {
    $group->get('', ListUsersAction::class);
    $group->get('/{id}', ViewUserAction::class);
    $group->post('', CreateUserAction::class); // ←追記
});

Actionの作成

CreateUserAction.php を次の内容で新規作成します。
CreateUserAction.php
<?php

declare(strict_types=1);

namespace App\Application\Actions\User;

use Psr\Http\Message\ResponseInterface as Response;
use App\Domain\User\User;

class CreateUserAction extends UserAction
{
    /**
     * {@inheritdoc}
     */
    protected function action(): Response
    {
        // リクエストオブジェクトからパースされたボディデータを取得
        $data = $this->request->getParsedBody();

        // ユーザーデータを元にUserオブジェクトを作成
        $user = new User(
            null,
            $data['username'],
            $data['firstname'],
            $data['lastname']
        );

        $newUser = $this->userRepository->create($user);

        $this->logger->info("Created new user.");

        return $this->respondWithData($newUser);
    }
}

リポジトリの調整

UserRepository インターフェースに、メソッドを追加します。
UserRepository.php
...
public function create(User $user): User;
UserRepository クラスを実装した InMemoryUserRepository クラスに、ロジックを実装します。
InMemoryUserRepository.php
public function create(User $user): User
{
    $newId = max(array_keys($this->users)) + 1;
    $user->setId($newId);
    $this->users[$newId] = $user;
    return $user;
}

以上でCREATEの実装は終わりです。curlでPOSTリクエストしてみます。

cURLでのリクエストテスト

POSTリクエストでJSONデータを送信します。

zsh
$ curl -X POST http://localhost:8885/users -H "Content-Type: application/json" -d '{"username":"Toshihiko Arai", "firstname":"Toshihiko", "lastname":"Arai"}'

次の通りレスポンスが返ってくれば成功です!

json
{
    "statusCode": 200,
    "data": {
        "id": 6,
        "username": "toshihiko arai",
        "firstName": "Toshihiko",
        "lastName": "Arai"
    }
}

今回はメモリにデータを追加したので、永続保存されません。次のリクエストでは、追加したデータは消えて元に戻ってしまいます。とりあえず、Slimの使い方を把握するための試験ということで実装してみました。

まとめ

FastRouterよりは高機能で複雑、Laravelよりはシンプルで分かりやすかったです。ビューを必要としないRESTful APIを開発するにはちょうど良さそうな感じがします。Slimプロジェクトを作成すると、デフォルトでアーキテクチャが構成されていて、それに従って実装すれば管理しやすいプロジェクトになりそうです。ADR(アクションドメインレスポンダ)パターンと呼ぶのでしょうか?LaravelのMVC(モデル・ビュー・コントローラー)パターンとは少し違うので注意が必要です。Laravelに使い慣れている方なら、Lumenを選ぶのもありかなと思います。

関連記事

最後までご覧いただきありがとうございます!

▼ 記事に関するご質問やお仕事のご相談は以下よりお願いいたします。
お問い合わせフォーム