😇

TempestはPHP界に「嵐」を巻き起こすか? 新星フレームワークで遊んでみた!

に公開

はじめに

PHPフレームワーク戦国時代に、また一つ新たな風が吹きました。その名もTempest(嵐)。名前からして何やらすごそうですが、果たしてその実力は?
この記事では、この新進気鋭のPHPフレームワークTempestを使い、簡単なアプリ作成からデプロイまで、その魅力と実力を探っていきます!

Tempestって何者?

公式ドキュメントから抜粋

Tempestは、PHP開発のためのフレームワークで、開発者の邪魔にならないように設計されています。その核となる理念は、フレームワークに煩わされることなく、アプリケーションコードに集中できるようにすることです。

https://tempestphp.com/1.x/getting-started/introduction

なるほど...?つまり、LaravelやSymfonyのような「至れり尽くせり」なフルスタックフレームワークが時に課してくる"お作法"や"独自ルール"から解放され、もっと自由に、もっとピュアなPHPでアプリが作れる...ということでしょうか?

もし本当にそうなら、試さない手はありません。

技術スタック

  • フレームワーク: Tempest 1.6
  • PHPバージョン: 8.4
  • データベース: SQLite
  • コンテナイメージ: FrankenPHP (Classicモード)
  • IaC: Pulumi

準備

Tempestは最新のPHPの機能をふんだんに使っているため、PHP 8.4以上が必須です。

devboxでの環境構築

私は普段macOSで開発しているのですが、PHPのバージョン管理が面倒なので、devboxを利用して環境構築しました。

1. devboxのインストール

devboxをインストールしてない場合は、以下のコマンドでインストールします。

curl -fsSL https://get.jetify.com/devbox | bash

https://www.jetify.com/docs/devbox/installing_devbox/?install-method=macos

2. devboxの初期化

プロジェクト用のディレクトリを作り、devboxを初期化します

devbox init

3. PHPの追加

PHP 8.4とcomposerなどの環境を追加します。

devbox add php@8.4.11 php84Packages.composer@latest php84Packages.phpstan@latest

4. devboxシェルの起動

devboxのシェルを起動することで、シェルの中でPHP 8.4が利用可能になります。

devbox shell

Tempestのプロジェクト作成

準備は整いました。いよいよTempestプロジェクトを作成します。
私はインフラ管理にPulumiを使う予定なので、srcディレクトリの中にプロジェクトを作ることにします。

composer create-project tempest/app src

https://tempestphp.com/1.x/getting-started/installation

作成に成功したら、srcディレクトリに移動して、以下コマンドでビルトインサーバーを起動します。

cd src
php tempest serve

ブラウザでhttp://localhost:8000にアクセスして、Tempestのウェルカムページが表示されれば成功です!

色々試してみる

0. ディレクトリ構成変更

デフォルトのディレクトリ構成はこんな感じ。

.
├── .tempest/
├── app/
│   ├── HelloCommand.php
│   ├── home.view.php
│   ├── HomeController.php
│   └── x-base.view.php
├── public/
├── tests/
├── vendor/
├── .env
├── composer.json
├── composer.lock
└── tempest

appディレクトリにControllerもViewもごちゃ混ぜなのが少し気になります。
そこで、私好みに整理整頓してみました。

.
├── .tempest/
├── app/
│   ├── Command/
│   └── Controller/
├── config/
├── public/
├── tests/
├── vendor/
├── view/
├── .env
├── composer.json
├── composer.lock
└── tempest

viewconfigを外に出し、appには純粋なアプリケーションロジックだけを置くスタイルです。
「え、そんな勝手なことして大丈夫?」と思いますよね。大丈夫なんです(たぶん)この変更は公式ドキュメントには載っていないので自己責任ですが、composer.jsonを少し書き換えるだけで対応できます。

    "autoload": {
        "psr-4": {
            "App\\": "app/",
            "View\\": "view/",
            "Config\\": "config/"
        }
    },

後に説明するのですが、この設定を行うことで、TempestのDiscovery機能により自動的にviewconfigディレクトリを認識してくれます。

1. コントローラー作成&ルーティング設定

まずは、tempestコマンドでapp/Controller以下にコントローラーを作成します。

php tempest make:controller Controller/JsonController

生成されたapp/Controller/JsonController.phpがこちら。

<?php

namespace App\Controller;

use Tempest\Router\Get;
use Tempest\View\View;

use function Tempest\view;

final class JsonController
{
    #[Get(uri: '/dummy-path')]
    public function __invoke(): View
    {
        return view('dummy-view');
    }
}

このコントローラーは、/dummy-pathにGETリクエストが来たときに、dummy-viewテンプレートをレンダリングして返すだけのシンプルなものです。
Tempestでは、メソッドに対してGet, Post, Put, Deleteアトリビュートを設定することでルーティングすることができます(今風ですね!)

これをJsonを返すように書き換えてみましょう。

<?php

declare(strict_types=1);

namespace App\Controller;

use Tempest\Http\ContentType;
use Tempest\Http\Response;
use Tempest\Http\Responses\Ok;
use Tempest\Router\Get;

final class JsonController
{
    #[Get(uri: '/json')]
    public function __invoke(): Response
    {
        return new Ok([
            'message' => 'Hello, World!',
            'status' => 'success',
            'data' => [
                'example' => 'This is a JSON response from the JsonController.',
            ],
        ])->setContentType(ContentType::JSON);
    }
}

試しにcurlでアクセスしてみると、以下のようにJsonが返ってきます。

curl http://localhost:8000/json | jq
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   116    0   116    0     0   1336      0 --:--:-- --:--:-- --:--:--  1348
{
  "message": "Hello, World!",
  "status": "success",
  "data": {
    "example": "This is a JSON response from the JsonController."
  }
}

ちなみに、追加したルートの一覧を確認するには、以下のコマンドを実行します。

php tempest routes

// REGISTERED ROUTES
These routes are registered in your application.

     GET / ............................................................ App\Controller\HomeController::__invoke()
     GET /json ........................................................ App\Controller\JsonController::__invoke()

2. コマンド作成

次に、tempestコマンドでapp/Command以下にコマンドを作成します。

php tempest make:command Command/GreetCommand 

コマンドを実行することで、app/Command/GreetCommand.phpが作成されます。

<?php

namespace App\Command;

use Tempest\Console\Console;
use Tempest\Console\ConsoleCommand;

final class GreetCommand
{
    public function __construct(
        private Console $console,
    ) {
    }

    #[ConsoleCommand(name: 'command-greet-command')]
    public function __invoke(): void
    {
        $this->console->success('Done!');
    }
}

このコマンドは、command-greet-commandという名前で実行でき、実行するとDone!と表示されるだけのシンプルなものです。
生成されたファイルに#[ConsoleCommand]アトリビュートを使ってコマンドを定義します。これも直感的でいいですね。

とりあえず、挨拶を返すように修正してみます。

<?php

namespace App\Command;

use Tempest\Console\Console;
use Tempest\Console\ConsoleCommand;

final class GreetCommand
{
    public function __construct(
        private Console $console,
    ) {}

    #[ConsoleCommand(
        name: 'greet',
        description: 'Outputs a greeting message.',
    )]
    public function greet(string $name = 'stranger'): void
    {
        $this->console->success("Hello, {$name}!");
    }
}

実行してみましょう。

php tempest greet --name=Tempest 
✓ // Hello, Tempest!

ちなみに、追加しコマンドの詳細を確認するには、以下のコマンドを実行します。

php tempest greet --help

// GREET
Outputs a greeting message.

// USAGE
greet [name=stranger]

お気付きでしょうか?

ここで、勘の良い方は気付いたかもしれません。
私は独断でディレクトリ構成を変更しました。appディレクトリにあったControllerをapp/Controllerに移動し、viewconfigも完全に別の場所に配置しました。

なのに、なぜフレームワークはそれらを正しく認識しているのでしょうか?

これがTempestのDiscovery(検出)機能です!

Tempestは、composer.jsonautoload設定などをヒントに、プロジェクト内のController、Command、設定ファイルなどを自動的に探し出して登録してくれるのです。
これにより、私たちは面倒な設定ファイルから解放され、より自由に、直感的に開発を進めることができます。
この開発体験は、まさに「嵐」級のインパクトです!

(本番環境ではパフォーマンスのためにキャッシュが推奨されています)

https://tempestphp.com/1.x/internals/discovery

アプリ作成

Tempestのすごさがわかってきたところで、簡単なユーザー管理アプリをサクッと作ってみます。

1. データベース接続設定とマイグレーション

今回はお手軽なSQLiteを使います。まず、config/database.config.phpに設定を記述。
https://github.com/takemo101/tempestphp-example/blob/b3228b6ec64e9dd7550f748b6c2c05dc2afd1166/src/config/database.config.php#L1-L8

次に、マイグレーションファイルを作成します。

php tempest make:migration Migration/CreateUserTable

生成されたMigration/CreateUserTable.phpのテーブル定義を修正...
https://github.com/takemo101/tempestphp-example/blob/b3228b6ec64e9dd7550f748b6c2c05dc2afd1166/src/app/Migration/CreateUserTable.php#L1-L34
マイグレーション実行!

php tempest migrate:up

マイグレーションが成功すると、database.sqliteファイルが作成され、usersテーブルが作成されます。

2. ユーザーモデルとリポジトリ作成

次に、ユーザーモデルとリポジトリを作成します。

php tempest make:model Repository/User 

生成されたapp/Repository/User.phpを修正...
https://github.com/takemo101/tempestphp-example/blob/b3228b6ec64e9dd7550f748b6c2c05dc2afd1166/src/app/Repository/User.php#L1-L63

リポジトリ生成のコマンドはないので、ドキュメントを参考に手動で作成します。
https://github.com/takemo101/tempestphp-example/blob/b3228b6ec64e9dd7550f748b6c2c05dc2afd1166/src/app/Repository/UserRepository.php#L1-L76

3. コントローラーとリクエスト作成

次に、ユーザー管理用のコントローラーとリクエストを作成します。

php tempest make:controller Controller/UserController 

生成されたapp/Controller/UserController.phpを修正...
https://github.com/takemo101/tempestphp-example/blob/b3228b6ec64e9dd7550f748b6c2c05dc2afd1166/src/app/Controller/UserController.php#L1-L51

メソッドの引数にUserRepositoryと書くだけで、自動的にインスタンスが注入(DI)されます。

バリデーション付きのリクエストも作ります。

php tempest make:request Request/CreateUserRequest

必要十分なバリデーションルールが揃っています。
https://github.com/takemo101/tempestphp-example/blob/b3228b6ec64e9dd7550f748b6c2c05dc2afd1166/src/app/Request/CreateUserRequest.php#L1-L16

4. ビュー作成

ユーザー一覧画面のビューファイルview/users.view.phpを作成します。
https://github.com/takemo101/tempestphp-example/blob/b3228b6ec64e9dd7550f748b6c2c05dc2afd1166/src/view/users.view.php#L1-L119

以下のような画面が表示されます。

5. ミドルウェア作成

Laravelなどのフルスタックフレームワークを利用していると、WebフォームでPUTやDELETEメソッドを使いたい時、<input type="hidden" name="_method" value="PUT">というお作法、ありますよね?Tempestにはデフォルトでこの機能がないので、ミドルウェアで自作してみましょう。

php tempest make:middleware Middleware/SetFormMethod

生成されたapp/Middleware/SetFormMethod.phpを修正...
https://github.com/takemo101/tempestphp-example/blob/b3228b6ec64e9dd7550f748b6c2c05dc2afd1166/src/app/Middleware/SetFormMethod.php#L1-L59

ミドルウェアには優先度を設定できるのですが、ルーティングのマッチングの前に実行されるように、ミドルウェアの実行優先度設定であるPriorityアトリビュートを調整します。

6. テスト作成

最後に、ユーザー管理用のテストを作成します。
テスト生成のコマンドはないので、キュメントを参考に手動で作成します。
https://github.com/takemo101/tempestphp-example/blob/b3228b6ec64e9dd7550f748b6c2c05dc2afd1166/src/tests/UserControllerTest.php#L1-L72

テストは、以下のコマンドでPHPUnitを利用して実行します。

composer phpunit

テストは、少し書きにくい感じがしましたが、基本的な部分は抑えられているので、必要十分だと思います。

デプロイ

今回は、AWS Fargateにデプロイしてみます。
今回インフラ部分の詳細は割愛しますが、詳細は今回作成したGitHubリポジトリを参照してください。

https://github.com/takemo101/tempestphp-example

1. Dockerfile作成

今回は、高速で有名なfrankenphpの公式イメージをベースに、Classicモードで動かします。
Dockerfileはこんな感じ。
https://github.com/takemo101/tempestphp-example/blob/b3228b6ec64e9dd7550f748b6c2c05dc2afd1166/src/docker/Dockerfile#L1-L79

2. Pulumiプロジェクト作成

IaCツールPulumiを使って、Fargateの環境をコードで構築します。詳細は割愛しますが、pulumi upコマンド一発でデプロイ完了!
無事、Fargate上でTempestアプリが元気に動いていることを確認できました

まとめ

Tempestを触ってみて感じたのは、「ちょうどいい」 という心地よさでした。

LaravelやSymfonyのようなフルスタックフレームワークが持つ圧倒的な機能群はありません。しかし、その代わりに、開発者を縛らない自由さと、驚くほど直感的な開発体験があります。

特に、あのDiscovery機能は革命的です。ディレクトリ構造を自由にいじれる開放感は、一度味わうと病みつきになるかもしれません。

DIコンテナ、ルーティング、ミドルウェア、バリデーション、テスト...アプリ開発に必要な最小限の武器はすべて揃っています。公式ドキュメントも分かりやすく、学習コストも低い。

Tempestはまだ若いフレームワークですが、PHP開発の「当たり前」を変えるポテンシャルを秘めています。

今後、Tempestがどのように発展していくのか楽しみです。

株式会社ソニックムーブ

Discussion