Learn/flight_vs_laravel

Flight vs Laravel

Laravel とは?

Laravel は、すべての機能が揃ったフル機能のフレームワークで、素晴らしい開発者向けエコシステムを備えていますが、パフォーマンスと複雑さの代償を伴います。Laravel の目標は、開発者が最高レベルの生産性を発揮し、共通のタスクを容易にすることです。Laravel は、フル機能のエンタープライズ Web アプリケーションを構築したい開発者にとって優れた選択肢です。これにはいくつかのトレードオフが伴い、特にパフォーマンスと複雑さの点でそうです。Laravel の基礎を学ぶのは簡単ですが、フレームワークに習熟するには時間がかかる場合があります。

また、Laravel のモジュールが非常に多く、開発者は問題を解決する唯一の方法がこれらのモジュールを使うことだと感じることがよくあります。しかし、実際には別のライブラリを使ったり、自分のコードを書いたりするだけで十分な場合もあります。

Flight との比較での利点

Flight との比較での欠点

Learn/migrating_to_v3

v3 への移行

後方互換性は主に維持されていますが、v2 から v3 への移行時に注意すべきいくつかの変更点があります。これらの変更は、デザインパターンとあまりにも対立するため、いくつかの調整が必要でした。

出力バッファリングの動作

v3.5.0

出力バッファリング は、PHP スクリプトによって生成された出力がクライアントに送信される前に、PHP 内部のバッファに保存されるプロセスです。これにより、出力がクライアントに送信される前にそれを変更できます。

MVC アプリケーションでは、Controller が「マネージャー」であり、view が何をするかを管理します。Controller の外(または Flight の場合、時には匿名関数)で出力が生成されることは、MVC パターンを破ります。この変更は、MVC パターンに沿うようにし、フレームワークをより予測しやすく使いやすくするためのものです。

v2 では、出力バッファリングは一貫して自身の出力バッファを閉じない方法で処理されており、これが ユニットテスト および ストリーミング をより困難にしました。ほとんどのユーザーにとって、この変更は実際には影響を与えない可能性があります。ただし、コールバックやコントローラーの外(例: フック内)でコンテンツを出力している場合、問題が発生する可能性が高いです。フック内でコンテンツを出力したり、フレームワークが実際に実行される前に出力したりすることは、過去には動作したかもしれませんが、今後は動作しません。

問題が発生する可能性のある箇所

// index.php
require 'vendor/autoload.php';

// 例です
define('START_TIME', microtime(true));

function hello() {
    echo 'Hello World';
}

Flight::map('hello', 'hello');
Flight::after('hello', function(){
    // これは実際に問題ありません
    echo '<p>This Hello World phrase was brought to you by the letter "H"</p>';
});

Flight::before('start', function(){
    // このようなものはエラーを引き起こします
    echo '<html><head><title>My Page</title></head><body>';
});

Flight::route('/', function(){
    // これは実際に問題ありません
    echo 'Hello World';

    // これも問題ないはずです
    Flight::hello();
});

Flight::after('start', function(){
    // これはエラーを引き起こします
    echo '<div>Your page loaded in '.(microtime(true) - START_TIME).' seconds</div></body></html>';
});

v2 レンダリング動作の有効化

古いコードを書き換えずに v3 で動作させることはまだ可能ですか? はい、可能です! flight.v2.output_buffering 構成オプションを true に設定することで、v2 レンダリング動作を有効にできます。これにより、古いレンダリング動作を継続して使用できますが、今後修正することを推奨します。フレームワークの v4 では、これが削除されます。

// index.php
require 'vendor/autoload.php';

Flight::set('flight.v2.output_buffering', true);

Flight::before('start', function(){
    // 今度はこれも問題ありません
    echo '<html><head><title>My Page</title></head><body>';
});

// さらにコード

ディスパッチャーの変更

v3.7.0

Dispatcher の静的メソッド、例えば Dispatcher::invokeMethod()Dispatcher::execute() などを直接呼び出していた場合、これらのメソッドを直接呼び出さないようにコードを更新する必要があります。Dispatcher は、よりオブジェクト指向的に変更され、Dependency Injection コンテナをより簡単に使用できるようにされました。Dispatcher のようにメソッドを呼び出す必要がある場合、手動で $result = $class->$method(...$params);call_user_func_array() を使用できます。

halt() stop() redirect() および error() の変更

v3.10.0

3.10.0 以前のデフォルト動作は、ヘッダーとレスポンスボディの両方をクリアするものでした。これを、レスポンスボディのみをクリアするように変更しました。ヘッダーもクリアする必要がある場合、Flight::response()->clear() を使用できます。

Learn/configuration

設定

概要

Flightは、アプリケーションのニーズに合わせてフレームワークのさまざまな側面を設定する簡単な方法を提供します。一部はデフォルトで設定されていますが、必要に応じて上書きできます。また、アプリケーション全体で使用する独自の変数を設定することもできます。

明確で階層化された設定(ファイルのデフォルト + 環境シークレット)は、AIコーディングツールにも役立ちます。エージェントは、コントローラー内で $_ENV 読み取りを独自に作り出す代わりに、リテラルのための場所とシークレットのための場所を1つずつ学ぶことができます。

理解

Flightの特定の動作は、setメソッドを通じて設定値を設定することでカスタマイズできます。

Flight::set('flight.log_errors', true);

構造化されたアプリ(スケルトンを含む)では、通常 app/config/config.php からプロジェクト設定を読み込み、関連するキーをエンジンに適用します(例: flight.base_urlflight.views.path)。また、グローバルをあちこちで読み取る代わりに、小さな設定オブジェクトをコントローラーに注入することもできます。これにより、テストや AGENTS.md に従うエージェントにとってより親しみやすくなります。

基本的な使い方

Flight設定オプション

以下は、利用可能なすべての設定項目のリストです。

ローダー設定

ローダーにはもう1つの設定項目があります。これにより、クラス名に_を含むクラスをオートロードできます。

// アンダースコアを使用したクラス読み込みを有効にする
// デフォルトはtrue
Loader::$v2ClassLoading = false;

オートローディングは、名前空間と一致するフォルダーの大文字小文字にも依存することを忘れないでください。特にスケルトンの App\ + app/Controller/ レイアウトでは重要です。

プロジェクト設定と.env(スケルトンパターン)

Flightのコアは.envファイルを必要としません。多くのアプリはPHPの設定配列のみを使用します。公式スケルトンは設定を階層化しているため、シークレットをgitの管理外に保ちながら、Runwayがリテラル設定を安全に書き換えることができます。

  1. .env / 実際の環境 — シークレットとデプロイ時の上書き(gitignoreされます)。
  2. app/config/config.php — リテラルなPHP配列のデフォルト(config_sample.phpからコピー)。このファイル内では $_ENV[...] 式を使わないことをお勧めします。runway config:set のようなツールはこれを静的値として書き換え、シークレットをファイルに焼き付ける可能性があります。
  3. ブートストラップ時にマージ — マッピングされたキーでは環境変数が優先されます。アプリコードはコントローラー内の$_ENVではなく、設定オブジェクトまたは$app->get()を読み取ります。

config_sample.php / config.php の例(簡略版):

<?php
// リテラルのみ — シークレットはスケルトンワークフローでは .env に置く
return [
    'app' => [
        'env' => 'development',
        'debug' => true,
        'base_url' => '/',
        'timezone' => 'UTC',
    ],
    'database' => [
        'driver' => 'sqlite', // または mysql、または無効にする場合は ''
        'host' => 'localhost',
        'dbname' => '',
        'user' => '',
        'password' => '',
        'file_path' => __DIR__ . '/../../database.sqlite',
    ],
    // ...
];
# .env.example → .env(スケルトン)
APP_ENV=development
APP_DEBUG=true
FLIGHT_BASE_URL=/
DB_DRIVER=sqlite
# DB_PASSWORD=...

この分割は、AIフレンドリーなプロジェクトのために意図的に行われています。手順書には「デフォルトは config.php、シークレットは .env、Config / Engine を注入し、コントローラーで env アクセスを独自に作らないこと」と記載できます。既存のアプリは .env を完全に無視して、単一の設定ファイルを維持することもできます。

変数

Flightを使用すると、アプリケーションのどこでも使用できる変数を保存できます。

// 変数を保存
Flight::set('id', 123);

// アプリケーション内の別の場所
$id = Flight::get('id');

変数が設定されているかどうかを確認するには、次のようにします。

if (Flight::has('id')) {
  // 何かを行う
}

変数をクリアするには、次のようにします。

// id変数をクリア
Flight::clear('id');

// すべての変数をクリア
Flight::clear();

注: 変数を設定できるからといって、それを使うべきとは限りません。この機能は控えめに使用してください。ここに保存されたものはすべてグローバル変数になるためです。グローバル変数は、アプリケーションのどこからでも変更できるため、バグの追跡が難しくなります。さらに、ユニットテストなどを複雑にする可能性があります。コントローラーが必要とするサービスや設定には、コンストラクター注入(スケルトン + Dice設定のように)を優先してください。

エラーと例外

すべてのエラーと例外はFlightによってキャッチされ、errorメソッドに渡されます(flight.handle_errorsがtrueに設定されている場合)。

デフォルトの動作は、いくつかのエラー情報を含む一般的な HTTP 500 Internal Server Error レスポンスを送信することです。

この動作は、必要に応じて上書きできます。

Flight::map('error', function (Throwable $error) {
  // エラーを処理
  echo $error->getTraceAsString();
});

デフォルトでは、エラーはWebサーバーに記録されません。設定を変更することで有効にできます。

Flight::set('flight.log_errors', true);

404 Not Found

URLが見つからない場合、FlightはnotFoundメソッドを呼び出します。デフォルトの動作は、簡単なメッセージを含むHTTP 404 Not Foundレスポンスを送信することです。

この動作は、必要に応じて上書きできます。

Flight::map('notFound', function () {
  // 見つからない場合の処理
});

関連項目

トラブルシューティング

変更履歴

Learn/ai

Flight と AI / 開発者エクスペリエンス

概要

Flight は AI コーディングツールと 連携する ように設計されており、対抗するものではありません。シンプルで予測可能な API、公式スケルトン による明確なアプリ構成、そしてプロジェクト固有の指示ファイルにより、GitHub Copilot、Cursor、Windsurf、Claude Code、Gemini などのアシスタントは、あなたが手書きするのと同じパターンに従うことができます。

組み込みの Runway コマンドを使用して LLM プロバイダーに接続し、プロジェクトの指示を生成できるため、Flight はあなたとあなたのチームが、毎回同じコンテキストをチャットに貼り付けることなく、一貫性のある関連性の高い支援を受けられるようにします。

はじめに

AI コーディングアシスタントは、プロジェクトのコンテキスト、規約、目標を理解しているときに最も役立ちます。Flight の AI ヘルパーを使用すると、次のことができます。

これらの機能は Flight コア CLI(Runway 経由)に同梱されており、公式の flightphp/skeleton スターターに事前に組み込まれています。

スケルトンが AI 向けに提供するもの

公式スターターは、AI ツールにとって AGENTS.md を信頼できる情報源 として扱います。

ファイル 役割
AGENTS.md(プロジェクトルート) グローバルなルール、起動フロー、名前空間、DI、「やってはいけないこと」
スコープ付き AGENTS.mdapp/migrations/tests/ など) そのツリー内で作業する際の、軽量でフォルダ固有のヒント
SECURITY.md シークレット、ヘッダー、XSS/SQL、報告——セキュリティは意図的に独立して管理されます

スケルトンには、Copilot / Cursor / Gemini / Windsurf 用の独立したハウススタイルファイルは ありません。アシスタントはルートの AGENTS.md を参照させ(スコープ付きファイルへのリンクを辿らせます)、人間はこれらのファイルを完全に無視して README を使用できます。レイアウトはどちらの場合も同じです。

ドキュメントは API を教え、スケルトンはレイアウトを教えます。 これらのドキュメントにある短い Flight:: の例は学習に適しています。スケルトンアプリでは、コントローラー内の静的ファサードよりも App\… クラス、コンストラクターインジェクション、$this->app を優先してください。インストールオートローディング を参照してください。

基本的な使い方

LLM 認証情報の設定

ai:init コマンドは、プロジェクトを LLM プロバイダーに接続する手順をガイドします。

php runway ai:init

次のプロンプトが表示されます。

これにより、後続の LLM リクエスト(指示の生成など)で使用される認証情報が作成されます。

例:

Welcome to AI Init!
Which LLM API do you want to use? [1] openai, [2] grok, [3] claude: 1
Enter the base URL for the LLM API [https://api.openai.com]:
Enter your API key for openai: sk-...
Enter the model name you want to use (e.g. gpt-4, claude-3-opus, etc) [gpt-4o]:
Credentials saved to .runway-creds.json

プロジェクト固有の AI 指示の生成

ai:generate-instructions コマンドは、あなたのプロジェクトに合わせた AI コーディングアシスタント向けの指示を作成または更新します。

php runway ai:generate-instructions

いくつかの質問(説明、データベース、テンプレートエンジン、セキュリティ、チーム規模など)に答えます。Flight は LLM プロバイダーを使用して指示を生成し、主に次の場所に書き込みます。

CLI のバージョンとオプションによっては、このコマンドは古いワークフロー向けのツール固有のコピー(Copilot、Cursor、Windsurf、Gemini のルールファイルなど)も書き込む場合があります。スケルトンからの新規プロジェクトでは、AGENTS.md(および app/ の下に保持するスコープ付き AGENTS.md ファイル)を単一の情報源として扱い、5 つの異なる指示ファイルを手作業で保守しないでください。

例:

Please describe what your project is for? My awesome API
What database are you planning on using? MySQL
What HTML templating engine will you plan on using (if any)? twig
Is security an important element of this project? (y/n) y
...
AI instructions updated successfully.

これで、AI ツールは一般的な PHP チュートリアルではなく、実際のスタックとレイアウトに一致するコードを提案できるようになります。

高度な使い方

関連情報

トラブルシューティング

変更履歴

Learn/unit_testing_and_solid_principles

この記事は元々 2015 年に Airpair で公開されました。全クレジットは元々この記事を書いた Airpair と Brian Fenton に帰属しますが、ウェブサイトはもはや利用できず、記事は Wayback Machine でのみ存在します。この記事は PHP コミュニティ全体の学習と教育目的でサイトに追加されました。

1 セットアップと構成

1.1 現在のものを保持

最初からこれを強調しましょう - 野生で使われている PHP のインストールの驚くほど少ない数が、現在のもの、または更新されたものです。これは共有ホスティングの制限、デフォルトが誰も変更しないこと、またはアップグレードテストのための時間/予算がないためです。PHP のバイナリは後回しにされがちです。ですから、常に現在の PHP のバージョンを使用するという明確なベストプラクティスを強調する必要があります(この記事時点では 5.6.x)。さらに、PHP 自体と使用している拡張やベンダーライブラリを定期的にアップグレードするスケジュールを組むことも重要です。アップグレードにより、新しい言語機能、改善された速度、少ないメモリ使用量、そしてセキュリティ更新が得られます。アップグレードを頻繁に行うほど、プロセスが苦痛にならなくなります。

1.2 適切なデフォルトを設定

PHP は php.ini.developmentphp.ini.production ファイルでデフォルトの良い設定をしますが、さらに改善できます。例えば、それらは日付/タイムゾーンを設定してくれません。これは配布の観点から理にかなっていますが、設定がないと、日付/時間関連の関数を呼び出すたびに E_WARNING エラーが発生します。以下は推奨設定です:

1.3 拡張

使用しない拡張(例: データベースドライバなど)は無効にする(または少なくとも有効にしない)のが良い考えです。有効になっているものを確認するには、phpinfo() コマンドを実行するか、コマンドラインでこれを実行します。

$ php -i

情報は同じですが、phpinfo() には HTML フォーマットが追加されています。CLI バージョンは、特定の情報を検索するために grep にパイプしやすくなります。例えば。

$ php -i | grep error_log

ただし、この方法の注意点: ウェブ向けのバージョンと CLI バージョンの PHP 設定が異なる可能性があります。

2 Composer を使用

これは驚きかもしれませんが、現代の PHP を書くためのベストプラクティスの一つは、少ないコードを書くことです。プログラミングを上達させる最良の方法は実際にやることでありますが、ルーティング、基本的な入力検証ライブラリ、単位変換、データベース抽象レイヤーなどの多くの問題は、PHP 領域で既に解決されています。ただ Packagist に行って調べてみてください。おそらく、解決しようとしている問題の重要な部分が既に書かれていてテストされているでしょう。

すべてを自分で書きたくなる temptation はありますが(学習体験として自分のフレームワークやライブラリを書くこと自体は問題ありません)、Not Invented Here の感情に戦って、時間と頭痛を節約してください。代わりに PIE の教義に従ってください - Proudly Invented Elsewhere。また、自分で書くものを選んだ場合、既存のものと大きく異なったり優れているものでない限り、公開しないでください。

Composer は PHP のパッケージマネージャーで、Python の pip、Ruby の gem、Node の npm に似ています。JSON ファイルでコードの依存を定義し、それらの要件を解決して必要なコードバンドルをダウンロードしてインストールします。

2.1 Composer のインストール

これはローカルプロジェクトだと仮定しますので、現在のプロジェクト用の Composer のインスタンスをインストールしましょう。プロジェクトディレクトリに移動して、これを実行します:

$ curl -sS https://getcomposer.org/installer | php

任意のダウンロードをスクリプトインタープリタ (sh, ruby, php など) に直接パイプするのはセキュリティリスクです。ですから、インストールコードを読み、実行する前に快適に感じてください。

利便性のために ( php composer.phar install より composer install とタイプするのが好みなら)、composer の単一コピーをグローバルにインストールするコマンドを使えます:

$ mv composer.phar /usr/local/bin/composer
$ chmod +x composer

ファイル権限によって、sudo で実行する必要があるかもしれません。

2.2 Composer の使用

Composer は管理できる依存の主なカテゴリを 2 つ持っています: "require" と "require-dev"。 "require" としてリストされた依存はどこでもインストールされますが、"require-dev" の依存は特にリクエストされた場合にのみインストールされます。通常、これらはアクティブな開発中のツールで、PHP_CodeSniffer のようなものです。以下は Guzzle、人気の HTTP ライブラリをインストールする方法の例です。

$ php composer.phar require guzzle/guzzle

開発目的のみのツールをインストールするには、--dev フラグを追加します:

$ php composer.phar require --dev 'sebastian/phpcpd'

これは PHP Copy-Paste Detector を、開発専用依存としてインストールします。他のコード品質ツールです。

2.3 Install 対 update

最初に composer install を実行すると、composer.json ファイルに基づいて必要なライブラリとその依存をインストールします。それが完了すると、composer は composer.lock というロックファイルを作成します。このファイルには、composer が見つけた依存とその正確なバージョン、 hashes が含まれています。その後、将来 composer install を実行するたびに、ロックファイルを見てその正確なバージョンをインストールします。

composer update は少し違います。composer.lock ファイル (存在する場合) を無視して、composer.json の制約を満たす各依存の最新バージョンを探します。完了したら、新しい composer.lock ファイルを書き込みます。

2.4 オートロード

composer install と composer update の両方が、インストールしたライブラリを使うために必要なファイルを PHP に教える autoloader を生成します。使用するには、この行を追加します (通常、毎リクエストで実行されるブートストラップファイルに):

require 'vendor/autoload.php';

3 良い設計原則に従う

3.1 SOLID

SOLID は、良いオブジェクト指向ソフトウェア設計の 5 つの主要な原則を思い出すためのニーモニックです。

3.1.1 S - 単一責任原則

これは、クラスは 1 つの責任だけを持つべきだと言っています。つまり、変更する理由は 1 つだけです。これは、Unix の哲学である、1 つのことをうまくやる小さなツールのたくさんと一致します。1 つのことだけをするクラスは、テストしやすく、デバッグしやすく、驚かされにくくなります。Validator クラスのメソッド呼び出しが DB レコードを更新しないようにしたいです。以下は ActiveRecord pattern に基づくアプリケーションでよく見られる、SRP 違反の例です。

class Person extends Model
{
    public $name;
    public $birthDate;
    protected $preferences;
    public function getPreferences() {}
    public function save() {}
}

これは基本的な entity モデルです。ただし、これらのうち 1 つはここに属していません。エンティティモデルの唯一の責任は、それが表すエンティティに関連する行動であって、自分自身を永続化する責任を持つべきではありません。

class Person extends Model
{
    public $name;
    public $birthDate;
    protected $preferences;
    public function getPreferences() {}
}
class DataStore
{
    public function save(Model $model) {}
}

これは改善です。Person モデルは 1 つのことだけに戻り、save 行動は永続化オブジェクトに移動しました。また、Model のみを型ヒントに使用したことに注意してください。Person ではありません。SOLID の L と D の部分でこれに戻ります。

3.1.2 O - 開放閉鎖原則

これをまとめた素晴らしいテストがあります: 実装する機能について考えてみてください。おそらく最近作業したもの、または作業中のもの。既存のコードベースで、新しいクラスを追加するだけで、既存のクラスの変更なしにその機能を実装できますか? 構成と配線コードは少し例外ですが、ほとんどのシステムでこれは驚くほど難しいです。ポリモーフックディスパッチに頼らなければなりませんし、ほとんどのコードベースはそれに設定されていません。これに興味があるなら、polymorphism and writing code without Ifs についての良い Google トークが YouTube にあります。ボーナスとして、トークは Miško Hevery によって行われ、多くの人が AngularJs の作成者として知っています。

3.1.3 L - Liskov 置換原則

この原則は Barbara Liskov の名前にちなんで名付けられ、以下のように述べられています:

"プログラム内のオブジェクトは、そのサブタイプのインスタンスに置き換えても、プログラムの正しさを変えないべきです。"

これはすべて良さそうですが、例でより明確に示されます。

abstract class Shape
{
    public function getHeight();
    public function setHeight($height);
    public function getLength();
    public function setLength($length);
}

これは基本的な四角形を表します。何も特別なものはありません。

class Square extends Shape
{
    protected $size;
    public function getHeight() {
        return $this->size;
    }
    public function setHeight($height) {
        $this->size = $height;
    }
    public function getLength() {
        return $this->size;
    }
    public function setLength($length) {
        $this->size = $length;
    }
}

私たちの最初の形状、Square です。まっすぐな形状ですね? 寸法を設定するコンストラクタがあると仮定できますが、この実装から、length と height は常に同じになることがわかります。Square はそういうものです。

class Rectangle extends Shape
{
    protected $height;
    protected $length;
    public function getHeight() {
        return $this->height;
    }
    public function setHeight($height) {
        $this->height = $height;
    }
    public function getLength() {
        return $this->length;
    }
    public function setLength($length) {
        $this->length = $length;
    }
}

だからここに別の形状があります。同じメソッドシグネチャを持ちますが、四角形ですが、お互いに置き換えて使い始めるとどうなるでしょうか? Shape の height を変更すると、shape の length が一致しなくなります。私たちの Square 形状に与えた契約に違反しています。

これは LSP の違反の教科書的な例で、型システムを最大限に活用するためにこのような原則が必要です。duck typing でさえ、基礎的な行動が違うことを教えてくれませんし、それが壊れるまで知ることはできないので、最初から違うものにしないのが最善です。

3.1.3 I - インターフェース分離原則

この原則は、多くの小さな、細かいインターフェースを好むと言っています。一つ大きなものではなく。インターフェースは行動に基づくべきで、"これらのクラスの一つ" ではありません。PHP に付属するインターフェースを考えてみてください。Traversable、Countable、Serializable などです。それらはオブジェクトが持つ能力を宣伝し、継承するものではありません。だから、インターフェースを小さく保ってください。30 メソッドを持つものは望ましくなく、3 が良い目標です。

3.1.4 D - 依存逆転原則

これは Dependency Injection について話した他の場所で聞いたことがあるかもしれませんが、依存逆転と依存注入は全く同じものではありません。依存逆転は、システムの詳細ではなく抽象に依存すべきだと言う方法です。日常的にこれは何を意味するでしょうか?

コード全体で mysqli_query() を直接使用しないで、DataStore->query() のようなものを使ってください。

この原則の核心は抽象についてです。つまり、"データベースアダプタを使用" と言うことなので、mysqli_query のような直接呼び出しに依存しないということです。mysqli_query を半分のクラスで直接使用している場合、すべてをデータベースに直接結びつけています。ここで MySQL に反対しているわけではありませんが、mysqli_query を使用している場合、そのような低レベルの詳細は 1 つの場所に隠され、汎用ラッパー経由で公開されるべきです。

今、私はこれが hackneyed な例だと知っていますが、製品が本番環境にある後でデータベースエンジンを完全に変更する回数は非常に少ないです。私は人々が自分のコードからアイデアに慣れていると思ったので選んだものです。また、特定のデータベースに固執している場合でも、その抽象ラッパーオブジェクトはバグを修正、行動を変更、または選択したデータベースに欲しい機能を実装することを可能にします。また、ユニットテストを可能にします。

4 オブジェクトキャリスティクス

これはこれらの原則への完全な潜入ではありませんが、最初の 2 つは簡単に覚えやすく、良い価値を提供し、ほぼすべてのコードベースにすぐに適用できます。

4.1 メソッドごとのインデントを 1 レベル以内に

これは、メソッドを小さなチャンクに分解して考えるのに役立ち、より明確で自己文書化されたコードを残します。インデントのレベルが多いほど、メソッドがより多くのことをし、作業中に頭の中で追跡する状態が増えます。

すぐに人々がこれに反対するでしょうが、これはガイドライン/ヒューリスティックで、厳格なルールではありません。私は PHP_CodeSniffer のルールをこれで施行するのを期待していません (しかし people have)。

これがどうなるかを素早くサンプルで実行しましょう:

public function transformToCsv($data)
{
    $csvLines = array();
    $csvLines[] = implode(',', array_keys($data[0]));
    foreach ($data as $row) {
        if (!$row) {
            continue;
        }
        $csvLines[] = implode(',', $row);
    }
    return $csvLines;
}

これはひどいコードではありません (技術的に正しく、テスト可能など) が、これを明確にするために多くを改善できます。ここでネストのレベルを減らすには?

まず、foreach ループを簡略化する必要があります (または完全に削除) ので、そこから始めましょう。

if (!$row) {
    continue;
}

これは簡単です。これは空の行を無視するだけです。ループに到達する前に、PHP の組み込み関数でこれをショートカットできます。

$data = array_filter($data);
foreach ($data as $row) {
    $csvLines[] = implode(',', $row);
}

今、単一のネストレベルです。しかし、これを見ると、配列の各項目に関数を適用しているだけです。これで foreach ループは必要ありません。

$data = array_filter($data);
$csvLines = array_map(function($row) {
    return implode(',', $row);
}, $data);

今、ネストが全くありません、そしてコードは速くなるでしょう。なぜなら、ループをネイティブ C 関数で行っているからです。ただし、implode にコンマを渡すための少しの trickery が必要なので、前のステップで止めるのがより理解しやすいと主張できます。

4.2 else を使用しない

これは 2 つの主要なアイデアを扱っています。1 つ目は、メソッドからの複数の return 文です。メソッドの結果についての決定を下すのに十分な情報がある場合、その決定を下して return してください。2 つ目は Guard Clauses として知られるアイデアです。これらは基本的に、メソッドの先頭近くで検証チェックと早期 return を組み合わせたものです。意味を説明しましょう。

public function addThreeInts($first, $second, $third) {
    if (is_int($first)) {
        if (is_int($second)) {
            if (is_int($third)) {
                $sum = $first + $second + $third;
            } else {
                return null;
            }
        } else {
            return null;
        }
    } else {
        return null;
    }
    return $sum;
}

これは再びストレートフォワードで、3 つの int を加えて結果を返します、またはパラメータのいずれかが整数でない場合 null を返します。AND 演算子でこれらのチェックを 1 行に組み合わせられることを無視して、入れ子になった if/else 構造がコードを追いづらくしていると思います。代わりにこの例を見てください。

public function addThreeInts($first, $second, $third) {
    if (!is_int($first)) {
        return null;
    }
    if (!is_int($second)) {
        return null;
    }
    if (!is_int($third)) {
        return null;
    }
    return $first + $second + $third;
}

私にとってこの例はより追いやすいです。ここで guard clauses を使用して、パラメータについての初期の主張を検証し、それらが通過しない場合すぐにメソッドを終了します。また、sum をメソッド全体で追跡する中間変数もありません。私たちは既に happy path にあり、来たことをするだけです。再び、すべてのチェックを 1 つの if でできるのですが、原則は明確です。

5 ユニットテスト

ユニットテストは、コードの行動を検証する小さなテストを書く練習です。ほとんどいつもコードと同じ言語 (この場合 PHP) で書かれ、いつでも実行できるほど速いです。これらはコードを改善するための非常に価値あるツールです。コードが何をしているかを確保するという明らかな利点以外に、ユニットテストは設計フィードバックも提供します。テストしにくいコードは、設計問題をしばしば示します。また、回帰に対する安全網を与え、より頻繁にリファクタリングし、コードをよりクリーンな設計に進化させることを可能にします。

5.1 ツール

PHP にはいくつかのユニットテストツールがありますが、断然最も一般的なのは PHPUnit です。PHAR ファイルを directly ダウンロードするか、composer でインストールできます。composer を他のすべてに使用しているので、その方法を示します。また、PHPUnit は本番環境に展開されない可能性が高いので、dev 依存として以下コマンドでインストールできます:

composer require --dev phpunit/phpunit

5.2 テストは仕様

コード内のユニットテストの最も重要な役割は、コードが何をするはずかを提供する実行可能な仕様です。テストコードが間違っている、またはコードにバグがあるとしても、システムが supposed に何をするかを知ることは非常に価値があります。

5.3 最初にテストを書く

コードの前にテストを書いたものと、コードが完成した後に書いたものを比較すると、驚くほど違います。"後" のテストはクラスの実装詳細に焦点を当て、良い行カバレッジを確保しますが、"前" のテストは望ましい外部行動を検証します。それがユニットテストで気にするものです。つまり、クラスが正しい行動を示すことです。実装に焦点を当てたテストは、クラスの内部が変更すると壊れるので、リファクタリングを難しくします、そして OOP の情報隠蔽の利点を失います。

5.4 良いユニットテストの条件

良いユニットテストは以下の特性を共有します:

これらのいくつかに反する理由がありますが、一般的なガイドラインとして役立ちます。

5.5 テストが苦痛なとき

Unit testing forces you to feel the pain of bad design up front - Michael Feathers

ユニットテストを書いているとき、クラスを実際に使用してものを成し遂げています。テストを最後に書くか、または最悪の場合、QA や誰かにコードを投げてテストを書かせるなら、クラスが実際にどう行動するかのフィードバックが得られません。テストを書いていて、クラスが本当に苦痛なら、それを書きながら知ることになり、これはほぼ最安の修正時間です。

クラスがテストしにくい場合、それは設計の欠陥です。異なる欠陥は異なる方法で現れます。多くの mocking をしなければならない場合、クラスに依存が多すぎるか、メソッドが多すぎる可能性があります。各テストのためのセットアップが多い場合、メソッドが多すぎる可能性が高いです。行動を練習するために複雑なテストシナリオを書かなければならない場合、クラスのメソッドが多すぎる可能性があります。プライベートメソッドと状態の内部を掘ってテストしなければならない場合、もしかすると別のクラスが外に出ようとしているのかもしれません。ユニットテストは "iceberg classes" を公開するのが非常に上手く、クラスの 80% が保護またはプライベートコードで隠されているものです。私は以前、可能な限り多くを保護にするのが大ファンでしたが、今は個々のクラスが多すぎる責任を持っていたことに気づき、真の解決策はクラスを小さな部分に分解するでした。

Brian Fenton 執筆 - Brian Fenton はミッドウェストとベイエリアで 8 年間 PHP 開発者で、現在 Thismoment で働いています。彼はコード職人技と設計原則に焦点を当てています。ブログは www.brianfenton.us、Twitter は @brianfenton。お父さんをしている以外に、食べ物、ビール、ゲーム、そして学習を楽しんでいます。

Learn/security

セキュリティ

概要

Webアプリケーションにおいて、セキュリティは非常に重要です。アプリケーションを安全に保ち、ユーザーのデータを保護する必要があります。Flightは、Webアプリケーションを保護するための多くの機能を提供します。

公式のskeletonには、専用のSECURITY.mdとセキュリティヘッダーミドルウェアも同梱されており、AIコーディングツール(および人間)が、AGENTS.mdの一般的なコーディングスタイルとは別に、シークレット、ヘッダー、XSS/SQLルールを意図的に配置できる場所を提供します。

理解

Webアプリケーションを構築する際に注意すべき一般的なセキュリティ脅威がいくつかあります。最も一般的な脅威には次のようなものがあります。

テンプレートは、出力をデフォルトでエスケープすることでXSSを防ぐのに役立ちます(TwigとLatteはこれを自動で行います。この利点を活かしてください)。セッションは、以下に説明するようにCSRFトークンをユーザーのセッションに保存することでCSRFを防ぐことができます。PDOでのプリペアドステートメント、またはSimplePdoのヘルパーを使用すると、SQLインジェクションを防ぐことができます。CORSは、Flight::start()が呼び出される前のシンプルなフックで処理できます。

これらの方法はすべて連携して、Webアプリケーションのセキュリティを維持します。セキュリティのベストプラクティスを学び、理解することを常に最優先にしてください。トレードオフを理解せずにページを読み込むためだけに、AIアシスタントに「CSPを無効にする」またはヘッダーを弱めるように依頼してはいけません。

基本的な使い方

ヘッダー

HTTPヘッダーは、Webアプリケーションを保護する最も簡単な方法のひとつです。クリックジャッキング、XSS、その他の攻撃を防ぐためにヘッダーを使用できます。アプリケーションにこれらのヘッダーを追加する方法はいくつかあります。

ヘッダーのセキュリティを確認するのに役立つ優れたWebサイトは、securityheaders.comobservatory.mozilla.orgです。以下のコードを設定したら、これらのWebサイトでヘッダーが機能していることを簡単に確認できます。

skeletonには、App\Middleware\SecurityHeadersMiddleware(リクエストごとのnonce、フレームオプション、HSTSなどを備えたCSP)が含まれています。ヘッダーを無効にするよりも、意図的に拡張することを推奨します。

手動で追加する

Flight\Responseオブジェクトのheaderメソッドを使用して、これらのヘッダーを手動で追加できます。

// クリックジャッキングを防ぐためにX-Frame-Optionsヘッダーを設定
Flight::response()->header('X-Frame-Options', 'SAMEORIGIN');

// XSSを防ぐためにContent-Security-Policyヘッダーを設定
// 注:このヘッダーは非常に複雑になるため、
//  アプリケーションに合わせてインターネット上の例を参照してください
Flight::response()->header("Content-Security-Policy", "default-src 'self'");

// XSSを防ぐためにX-XSS-Protectionヘッダーを設定
Flight::response()->header('X-XSS-Protection', '1; mode=block');

// MIMEスニッフィングを防ぐためにX-Content-Type-Optionsヘッダーを設定
Flight::response()->header('X-Content-Type-Options', 'nosniff');

// 送信されるリファラー情報の量を制御するためにReferrer-Policyヘッダーを設定
Flight::response()->header('Referrer-Policy', 'no-referrer-when-downgrade');

// HTTPSを強制するためにStrict-Transport-Securityヘッダーを設定
Flight::response()->header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');

// 使用できる機能とAPIを制御するためにPermissions-Policyヘッダーを設定
Flight::response()->header('Permissions-Policy', 'geolocation=()');

これらは、routes.phpまたはindex.phpファイルの先頭に追加できます。

フィルターとして追加する

次のように、フィルター/フックで追加することもできます。

// フィルターでヘッダーを追加
Flight::before('start', function() {
    Flight::response()->header('X-Frame-Options', 'SAMEORIGIN');
    Flight::response()->header("Content-Security-Policy", "default-src 'self'");
    Flight::response()->header('X-XSS-Protection', '1; mode=block');
    Flight::response()->header('X-Content-Type-Options', 'nosniff');
    Flight::response()->header('Referrer-Policy', 'no-referrer-when-downgrade');
    Flight::response()->header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
    Flight::response()->header('Permissions-Policy', 'geolocation=()');
});

ミドルウェアとして追加する

適用するルートを柔軟に指定できるミドルウェアクラスとして追加することもできます。一般に、これらのヘッダーはすべてのHTMLおよびAPIレスポンスに適用する必要があります。

skeletonスタイルのパスと名前空間(フォルダーの大文字小文字はApp\Middlewareと一致):

// app/Middleware/SecurityHeadersMiddleware.php

namespace App\Middleware;

use flight\Engine;

class SecurityHeadersMiddleware
{
    protected Engine $app;

    public function __construct(Engine $app)
    {
        $this->app = $app;
    }

    public function before(array $params): void
    {
        $response = $this->app->response();
        // インラインスクリプトがある場合は、ブートストラップのCSP nonceを優先します(skeletonはcsp_nonceを設定します)
        $nonce = $this->app->get('csp_nonce');
        $csp = $nonce
            ? "default-src 'self'; script-src 'self' 'nonce-{$nonce}'; style-src 'self' 'nonce-{$nonce}'"
            : "default-src 'self'";

        $response->header('X-Frame-Options', 'SAMEORIGIN');
        $response->header('Content-Security-Policy', $csp);
        $response->header('X-XSS-Protection', '1; mode=block');
        $response->header('X-Content-Type-Options', 'nosniff');
        $response->header('Referrer-Policy', 'no-referrer-when-downgrade');
        $response->header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
        $response->header('Permissions-Policy', 'geolocation=()');
    }
}

// app/config/routes.php — 空の文字列グループ = すべてのルートに適用されるグローバルミドルウェア
use App\Middleware\SecurityHeadersMiddleware;
use flight\net\Router;

$router->group('', function (Router $router) {
    $router->get('/users', [ \App\Controller\UserController::class, 'getUsers' ]);
    // その他のルート
}, [SecurityHeadersMiddleware::class]);

古いプロジェクトではapp/middlewaresapp\middlewaresがまだ使用されている場合があります。フォルダーが一致していれば機能します。新しいskeletonアプリではapp/Middleware/App\Middlewareを使用します。オートローディングを参照してください。

クロスサイトリクエストフォージェリ(CSRF)

クロスサイトリクエストフォージェリ(CSRF)は、悪意のあるWebサイトがユーザーのブラウザーにあなたのWebサイトへのリクエストを送信させる攻撃の一種です。これは、ユーザーの知らないうちにあなたのWebサイトでアクションを実行するために使用される可能性があります。Flightには組み込みのCSRF保護メカニズムはありませんが、ミドルウェアを使用して簡単に独自のものを実装できます。

セットアップ

まず、CSRFトークンを生成してユーザーのセッションに保存する必要があります。次に、このトークンをフォームで使用し、フォームが送信されたときにチェックできます。セッションを管理するには、flightphp/sessionプラグインを使用します。

// CSRFトークンを生成し、ユーザーのセッションに保存します
// (セッションオブジェクトを作成してFlightにアタッチしていると仮定します)
// 詳細はセッションドキュメントを参照してください
Flight::register('session', flight\Session::class);

// セッションごとにトークンを1つ生成するだけで済みます(同じユーザーの
// 複数のタブやリクエストにわたって機能します)
if(Flight::session()->get('csrf_token') === null) {
    Flight::session()->set('csrf_token', bin2hex(random_bytes(32)) );
}
デフォルトのPHP Flightテンプレートを使用する場合
<!-- フォームでCSRFトークンを使用 -->
<form method="post">
    <input type="hidden" name="csrf_token" value="<?= Flight::session()->get('csrf_token') ?>">
    <!-- その他のフォームフィールド -->
</form>
Twigを使用する場合(skeletonのデフォルト)

Twig関数を登録するか、トークンをすべてのフォームビューに渡します。グローバルとフォームフィールドを使用した最小限の例:

// Twigを設定するとき(例:services.php)
$twig->addGlobal('csrf_token', $app->session()->get('csrf_token'));
{# app/views/form.twig #}
<form method="post">
    <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
    {# その他のフィールド #}
</form>
Latteを使用する場合

LatteテンプレートでCSRFトークンを出力するカスタム関数を設定することもできます。


Flight::map('render', function(string $template, array $data, ?string $block): void {
    $latte = new Latte\Engine;

    // その他の設定...

    // CSRFトークンを出力するカスタム関数を設定
    $latte->addFunction('csrf', function() {
        $csrfToken = Flight::session()->get('csrf_token');
        return new \Latte\Runtime\Html('<input type="hidden" name="csrf_token" value="' . $csrfToken . '">');
    });

    $latte->render($finalPath, $data, $block);
});

そして、Latteテンプレートでcsrf()関数を使用してCSRFトークンを出力できます。

<form method="post">
    {csrf()}
    <!-- その他のフォームフィールド -->
</form>

CSRFトークンのチェック

CSRFトークンは、いくつかの方法でチェックできます。

ミドルウェア
// app/Middleware/CsrfMiddleware.php

namespace App\Middleware;

use flight\Engine;

class CsrfMiddleware
{
    protected Engine $app;

    public function __construct(Engine $app)
    {
        $this->app = $app;
    }

    public function before(array $params): void
    {
        if($this->app->request()->method == 'POST') {
            $token = $this->app->request()->data->csrf_token;
            if($token !== $this->app->session()->get('csrf_token')) {
                $this->app->halt(403, 'Invalid CSRF token');
            }
        }
    }
}

// routes.php
use App\Middleware\CsrfMiddleware;

$router->group('', function ($router) {
    $router->get('/users', [ \App\Controller\UserController::class, 'getUsers' ]);
    // その他のルート
}, [CsrfMiddleware::class]);
イベントフィルター
// このミドルウェアは、リクエストがPOSTかどうかを確認し、POSTの場合はCSRFトークンが有効かどうかをチェックします
Flight::before('start', function() {
    if(Flight::request()->method == 'POST') {

        // フォームの値からCSRFトークンを取得
        $token = Flight::request()->data->csrf_token;
        if($token !== Flight::session()->get('csrf_token')) {
            Flight::halt(403, 'Invalid CSRF token');
            // またはJSONレスポンスの場合
            Flight::jsonHalt(['error' => 'Invalid CSRF token'], 403);
        }
    }
});

クロスサイトスクリプティング(XSS)

クロスサイトスクリプティング(XSS)は、悪意のあるフォーム入力をあなたのWebサイトにコードを注入できる攻撃の一種です。これらの機会のほとんどは、エンドユーザーが記入するフォームの値から発生します。ユーザーからの出力を決して信頼してはいけません!すべてのユーザーが世界最高のハッカーであると常に想定してください。彼らは悪意のあるJavaScriptやHTMLをあなたのページに注入する可能性があります。このコードは、ユーザーから情報を盗んだり、あなたのWebサイトでアクションを実行したりするために使用される可能性があります。FlightのビュークラスまたはTwigLatteのようなテンプレートエンジンを使用すると、XSS攻撃を防ぐために出力を簡単にエスケープできます。

// ユーザーが賢くて、これを自分の名前として使用しようとしていると仮定します
$name = '<script>alert("XSS")</script>';

// これにより出力がエスケープされます
Flight::view()->set('name', $name);
// これにより出力されます: &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;

// Twig(skeletonのデフォルト)とLatteはデフォルトで自動エスケープします — 生のPHP echoよりもこれらを優先してください
Flight::render('template', ['name' => $name]);
// Twig: {{ name }}  → エスケープされます
// コンテンツが完全に信頼できる場合を除き、|raw やエスケープされていない出力は避けてください

SQLインジェクション

SQLインジェクションは、悪意のあるユーザーがSQLコードをデータベースに注入できる攻撃の一種です。これは、データベースから情報を盗んだり、データベースでアクションを実行したりするために使用される可能性があります。繰り返しますが、ユーザーからの入力を決して信頼してはいけません!常に彼らは血を求めていると想定してください。プリペアドステートメントを使用してください。SimplePdoヘルパーはこれをデフォルトのパスにします。

// Flight::db() が SimplePdo として登録されていると仮定します(またはコントローラーに SimplePdo を注入します)
$statement = Flight::db()->prepare('SELECT * FROM users WHERE username = :username');
$statement->execute([':username' => $username]);
$users = $statement->fetchAll();

// SimplePdo(推奨)— バインドされたパラメーターを使用したワンライナー
$users = Flight::db()->fetchAll('SELECT * FROM users WHERE username = :username', [ 'username' => $username ]);

// ?プレースホルダーでも同様
$users = Flight::db()->fetchAll('SELECT * FROM users WHERE username = ?', [ $username ]);

skeletonスタイルのコントローラーでは、テストとAI生成コードの一貫性を保つために、Flight::db()よりもSimplePdoのコンストラクター注入を推奨します(DIC)。

安全でない例

以下は、SQLプリペアドステートメントを使用して、以下のような無害な例から保護する理由です。

// エンドユーザーがWebフォームに記入します。
// フォームの値として、ハッカーは次のようなものを入力します:
$username = "' OR 1=1; -- ";

$sql = "SELECT * FROM users WHERE username = '$username' LIMIT 5";
$users = Flight::db()->fetchAll($sql);
// クエリが構築された後、次のようになります
// SELECT * FROM users WHERE username = '' OR 1=1; -- LIMIT 5

// 奇妙に見えますが、これは機能する有効なクエリです。実際、
// これはすべてのユーザーを返す非常に一般的なSQLインジェクション攻撃です。

var_dump($users); // これにより、データベース内のすべてのユーザーがダンプされます(単一のユーザー名だけでなく)

シークレットと設定

JSONPコールバックの検証

FlightのFlight::jsonp()メソッドを使用する場合、FlightはJSONPコールバックパラメーター名を厳格な許可リストregex(/^[A-Za-z_$][\w$.]{0,127}$/)に対して検証することに注意してください。このパターンに一致しないコールバック名があると、Flightは例外をスローし、悪意のあるコールバック値を介した任意のJavaScriptの注入を防ぎます。

この検証は組み込まれており、追加の設定は不要ですが、JSONPエンドポイントから予期しないエラーをデバッグするときに知っておくと便利です。

CORS

クロスオリジンリソースシェアリング(CORS)は、Webページ上の多くのリソース(フォント、JavaScriptなど)を、そのリソースが発信されたドメインの外部の別のドメインからリクエストできるようにするメカニズムです。Flightには組み込みの機能はありませんが、Flight::start()メソッドが呼び出される前に実行されるフックで簡単に処理できます。

// app/Utils/CorsUtil.php  (skeleton: PascalCaseのUtilsフォルダー → App\Utils)

namespace App\Utils;

use flight\Engine;

class CorsUtil
{
    protected Engine $app;

    public function __construct(Engine $app)
    {
        $this->app = $app;
    }

    public function set(array $params = []): void
    {
        $request = $this->app->request();
        $response = $this->app->response();
        if ($request->getVar('HTTP_ORIGIN') !== '') {
            $this->allowOrigins();
            $response->header('Access-Control-Allow-Credentials', 'true');
            $response->header('Access-Control-Max-Age', '86400');
        }

        if ($request->method === 'OPTIONS') {
            if ($request->getVar('HTTP_ACCESS_CONTROL_REQUEST_METHOD') !== '') {
                $response->header(
                    'Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD'
                );
            }
            if ($request->getVar('HTTP_ACCESS_CONTROL_REQUEST_HEADERS') !== '') {
                $response->header(
                    "Access-Control-Allow-Headers",
                    $request->getVar('HTTP_ACCESS_CONTROL_REQUEST_HEADERS')
                );
            }

            $response->status(200);
            $response->send();
            exit;
        }
    }

    private function allowOrigins(): void
    {
        // 許可するホストをここでカスタマイズします。
        $allowed = [
            'capacitor://localhost',
            'ionic://localhost',
            'http://localhost',
            'http://localhost:4200',
            'http://localhost:8080',
            'http://localhost:8100',
        ];

        $request = $this->app->request();

        if (in_array($request->getVar('HTTP_ORIGIN'), $allowed, true) === true) {
            $response = $this->app->response();
            $response->header("Access-Control-Allow-Origin", $request->getVar('HTTP_ORIGIN'));
        }
    }
}

// ブートストラップ / ルート — startの前に実行
$app = Flight::app();
$cors = new \App\Utils\CorsUtil($app);
$app->before('start', [ $cors, 'set' ]);

Flight設定の堅牢化

Flightには、セキュリティに直接影響するいくつかのエンジン設定があります。これらを正しく設定することは、アプリケーションを堅牢化する最も簡単な方法のひとつです。

flight.allow_method_override

デフォルトでは、FlightはクライアントがX-HTTP-Method-OverrideヘッダーまたはPOST本文の_methodフィールドを使用してリクエストのHTTPメソッドを上書きできるようにします。これはGET/POSTのみを送信できるHTMLフォームには便利ですが、予期していない場合は危険です。攻撃者が通常のフォームを介してDELETEまたはPUTリクエストを偽造する可能性があります。

アプリケーションがこの動作に依存していない場合(たとえば、任意のHTTP動詞を送信できる最新のクライアントやJavaScriptフロントエンドが使用するAPIを構築している場合)、これを無効にする必要があります。

// index.phpまたはブートストラップファイル内、Flight::start()の前
Flight::set('flight.allow_method_override', false);

デフォルト値は後方互換性のためtrueですが、オーバーライド機能を明示的に必要としないアプリケーションではfalseに設定することを強くお勧めします

flight.debug

Flightには、未処理の例外が発生したときに、詳細なエラー情報(例外メッセージ、コード、完全なスタックトレース)をブラウザーに表示するかどうかを制御するflight.debug設定があります。デフォルトはfalseで、クライアントには内部詳細が漏洩せず、一般的な500 Internal Server Errorメッセージのみが表示されます。

本番サーバーでこれを有効にしないでください。ローカルまたはステージング環境でのみ使用してください。

// ローカル開発のみで安全 — 本番では絶対に使用しないでください
Flight::set('flight.debug', true);

flight.debugfalse(デフォルト)の場合でも、flight.log_errorsを有効にすることでエラーをキャプチャできます。

// クライアントに公開せずにサーバー側でエラーを記録
Flight::set('flight.debug', false);
Flight::set('flight.log_errors', true);

推奨される本番設定

// index.phpまたはアプリ設定/ブートストラップから適用
Flight::set('flight.allow_method_override', false);
Flight::set('flight.debug', false);
Flight::set('flight.log_errors', true);

エラーハンドリング

本番環境では、攻撃者に情報を漏洩しないように、機密性の高いエラー詳細を非表示にします。本番環境では、display_errors0に設定してエラーを表示するのではなく、ログに記録します。

// bootstrap.php または index.php 内

// これを app/config/config.php に追加します
$environment = ENVIRONMENT;
if ($environment === 'production') {
    ini_set('display_errors', 0); // エラー表示を無効化
    ini_set('log_errors', 1);     // 代わりにエラーをログに記録
    ini_set('error_log', '/path/to/error.log');
}

// ルートまたはコントローラー内
// 制御されたエラーレスポンスには Flight::halt() を使用
Flight::halt(403, 'Access denied');

入力のサニタイズ

ユーザー入力を信頼しないでください。悪意のあるデータが混入するのを防ぐために、処理前にfilter_varを使用してサニタイズします。アプリコードでは、生の$_GET / $_POSTではなく、$app->request()(またはFlight::request())を介して入力を読み取ることを推奨します。


// $_POST['input'] と $_POST['email'] を持つ $_POST リクエストを想定します

// 文字列入力をサニタイズ
$clean_input = filter_var(Flight::request()->data->input, FILTER_SANITIZE_STRING);
// メールアドレスをサニタイズ
$clean_email = filter_var(Flight::request()->data->email, FILTER_SANITIZE_EMAIL);

パスワードのハッシュ化

PHPの組み込み関数であるpassword_hashpassword_verifyを使用して、パスワードを安全に保存および検証します。パスワードは平文で保存してはいけません。また、可逆的な方法で暗号化してもいけません。ハッシュ化により、データベースが侵害された場合でも、実際のパスワードは保護されたままになります。

$password = Flight::request()->data->password;
// 保存時にパスワードをハッシュ化(例:登録時)
$hashed_password = password_hash($password, PASSWORD_DEFAULT);

// パスワードを検証(例:ログイン時)
if (password_verify($password, $stored_hash)) {
    // パスワードが一致
}

レート制限

キャッシュを使用してリクエストレートを制限し、ブルートフォース攻撃やサービス拒否攻撃から保護します。

// flightphp/cache がインストールおよび登録されていると仮定します
// フィルターで flightphp/cache を使用
Flight::before('start', function() {
    $cache = Flight::cache();
    $ip = Flight::request()->ip;
    $key = "rate_limit_{$ip}";
    $attempts = (int) $cache->retrieve($key);

    if ($attempts >= 10) {
        Flight::halt(429, 'Too many requests');
    }

    $cache->set($key, $attempts + 1, 60); // 60秒後にリセット
});

関連情報

トラブルシューティング

変更履歴

Learn/routing

ルーティング

概要

Flight PHP のルーティングは、URL パターンをコールバック関数やクラスのメソッドにマッピングし、高速かつシンプルなリクエスト処理を可能にします。最小限のオーバーヘッド、初心者にも使いやすく、外部依存なしで拡張できるように設計されています。

理解

ルーティングは、HTTP リクエストをアプリケーションのロジックに接続する中核的な仕組みです。ルートを定義することで、関数、クラスメソッド、コントローラアクションのいずれを通しても、異なる URL が特定のコードをトリガーする方法を指定できます。Flight のルーティングシステムは柔軟で、基本的なパターン、名前付きパラメータ、正規表現、依存性注入やリソースフルルーティングなどの高度な機能をサポートしています。このアプローチにより、コードは整理されて保守しやすく保たれ、初心者には高速でシンプル、上級者には拡張可能です。

注: ルーティングについてもっと理解したいですか? 詳しい説明は 「なぜフレームワークを使うのか?」 のページを参照してください。

基本的な使い方

シンプルなルートの定義

Flight の基本的なルーティングは、URL パターンをコールバック関数またはクラスとメソッドの配列とマッチングさせることで行います。

Flight::route('/', function(){
    echo 'hello world!';
});

ルートは定義された順序でマッチングされます。リクエストに最初にマッチしたルートが呼び出されます。

コールバックとして関数を使う

コールバックは、呼び出し可能なオブジェクトであれば何でもかまいません。通常の関数も使用できます:

function hello() {
    echo 'hello world!';
}

Flight::route('/', 'hello');

コントローラーとしてクラスとメソッドを使う

クラスのメソッド(静的または非静的)も使用できます:

class GreetingController {
    public function hello() {
        echo 'hello world!';
    }
}

Flight::route('/', [ 'GreetingController','hello' ]);
// または
Flight::route('/', [ GreetingController::class, 'hello' ]); // 推奨される方法
// または
Flight::route('/', [ 'GreetingController::hello' ]);
// または 
Flight::route('/', [ 'GreetingController->hello' ]);

または、先にオブジェクトを作成してからメソッドを呼び出します:

use flight\Engine;

// GreetingController.php
class GreetingController
{
    protected Engine $app
    public function __construct(Engine $app) {
        $this->app = $app;
        $this->name = 'John Doe';
    }

    public function hello() {
        echo "Hello, {$this->name}!";
    }
}

// index.php
$app = Flight::app();
$greeting = new GreetingController($app);

Flight::route('/', [ $greeting, 'hello' ]);

注: デフォルトでは、フレームワーク内でコントローラーが呼び出されるとき、依存性注入コンテナ で指定しない限り、flight\Engine クラスが常に注入されます。

メソッドごとのルーティング

デフォルトでは、ルートパターンはすべてのリクエストメソッドに対してマッチングされます。URL の前に識別子を置くことで、特定のメソッドに応答できます。

Flight::route('GET /', function () {
  echo 'I received a GET request.';
});

Flight::route('POST /', function () {
  echo 'I received a POST request.';
});

// ルートには Flight::get() を使用できません。これは変数を取得するメソッドであり、
// ルートを作成するものではないためです。
Flight::post('/', function() { /* code */ });
Flight::patch('/', function() { /* code */ });
Flight::put('/', function() { /* code */ });
Flight::delete('/', function() { /* code */ });

| 区切り文字を使用して、複数のメソッドを単一のコールバックにマッピングすることもできます:

Flight::route('GET|POST /', function () {
  echo 'I received either a GET or a POST request.';
});

HEAD および OPTIONS リクエストの特別処理

Flight は、HEAD および OPTIONS HTTP リクエストに対する組み込みの処理を提供します:

HEAD リクエスト

Flight::route('GET /info', function() {
    echo 'This is some info!';
});
// /info への HEAD リクエストは同じヘッダーを返しますが、ボディは返しません。

OPTIONS リクエスト

OPTIONS リクエストは、定義された任意のルートに対して Flight によって自動的に処理されます。

// 次のように定義されたルートの場合:
Flight::route('GET|POST /users', function() { /* ... */ });

// /users への OPTIONS リクエストは次のように応答します:
//
// Status: 204 No Content
// Allow: GET, POST, HEAD, OPTIONS

Router オブジェクトを使う

さらに、使用できるヘルパーメソッドを持つ Router オブジェクトを取得できます:


$router = Flight::router();

// Flight::route() と同様にすべてのメソッドをマッピングする
$router->map('/', function() {
    echo 'hello world!';
});

// GET リクエスト
$router->get('/users', function() {
    echo 'users';
});
$router->post('/users',             function() { /* code */});
$router->put('/users/update/@id',   function() { /* code */});
$router->delete('/users/@id',       function() { /* code */});
$router->patch('/users/@id',        function() { /* code */});

正規表現(Regex)

ルートで正規表現を使用できます:

Flight::route('/user/[0-9]+', function () {
  // これは /user/1234 にマッチします
});

この方法も利用可能ですが、名前付きパラメータ、または正規表現を使用した名前付きパラメータを使用することをお勧めします。読みやすく、保守が容易だからです。

名前付きパラメータ

ルート内に名前付きパラメータを指定でき、コールバック関数に渡されます。これは何よりもルートの読みやすさのためのものです。重要な注意点については以下のセクションを参照してください。

Flight::route('/@name/@id', function (string $name, string $id) {
  echo "hello, $name ($id)!";
});

名前付きパラメータに正規表現を含める場合は、: 区切り文字を使用します:

Flight::route('/@name/@id:[0-9]{3}', function (string $name, string $id) {
  // これは /bob/123 にマッチします
  // ただし /bob/12345 にはマッチしません
});

注: 位置パラメータを持つ正規表現グループ () のマッチングはサポートされていません。例: :'\(

重要な注意点

上記の例では、@name が変数 $name に直接結びついているように見えますが、実際はそうではありません。コールバック関数のパラメータの順序が、渡される内容を決定します。コールバック関数のパラメータの順序を入れ替えると、変数も入れ替わります。次に例を示します:

Flight::route('/@name/@id', function (string $id, string $name) {
  echo "hello, $name ($id)!";
});

そして、/bob/123 という URL にアクセスすると、出力は hello, 123 (bob)! になります。ルートとコールバック関数を設定するときは注意してください!

オプションパラメータ

セグメントを括弧で囲むことで、マッチングにオプションの名前付きパラメータを指定できます。

Flight::route(
  '/blog(/@year(/@month(/@day)))',
  function(?string $year, ?string $month, ?string $day) {
    // これは次の URL にマッチします:
    // /blog/2012/12/10
    // /blog/2012/12
    // /blog/2012
    // /blog
  }
);

マッチしなかったオプションパラメータは NULL として渡されます。

ワイルドカードルーティング

マッチングは個々の URL セグメントに対してのみ行われます。複数のセグメントにマッチさせたい場合は、* ワイルドカードを使用できます。

Flight::route('/blog/*', function () {
  // これは /blog/2000/02/01 にマッチします
});

すべてのリクエストを単一のコールバックにルーティングするには、次のようにします:

Flight::route('*', function () {
  // 何か処理をする
});

404 Not Found ハンドラ

デフォルトでは、URL が見つからない場合、Flight は非常にシンプルで素朴な HTTP 404 Not Found レスポンスを送信します。よりカスタマイズされた 404 レスポンスが必要な場合は、独自の notFound メソッドを マップ できます:

Flight::map('notFound', function() {
    $url = Flight::request()->url;

    // カスタムテンプレートで Flight::render() を使用することもできます。
    $output = <<<HTML
        <h1>My Custom 404 Not Found</h1>
        <h3>The page you have requested {$url} could not be found.</h3>
        HTML;

    $this->response()
        ->clearBody()
        ->status(404)
        ->write($output)
        ->send();
});

Method Not Found ハンドラ

デフォルトでは、URL は見つかったがメソッドが許可されていない場合、Flight は非常にシンプルで素朴な HTTP 405 Method Not Allowed レスポンスを送信します(例: Method Not Allowed. Allowed Methods are: GET, POST)。また、その URL で許可されているメソッドを含む Allow ヘッダーも含まれます。

よりカスタマイズされた 405 レスポンスが必要な場合は、独自の methodNotFound メソッドを マップ できます:

use flight\net\Route;

Flight::map('methodNotFound', function(Route $route) {
    $url = Flight::request()->url;
    $methods = implode(', ', $route->methods);

    // カスタムテンプレートで Flight::render() を使用することもできます。
    $output = <<<HTML
        <h1>My Custom 405 Method Not Allowed</h1>
        <h3>The method you have requested for {$url} is not allowed.</h3>
        <p>Allowed Methods are: {$methods}</p>
        HTML;

    $this->response()
        ->clearBody()
        ->status(405)
        ->setHeader('Allow', $methods)
        ->write($output)
        ->send();
});

上級の使い方

ルートでの依存性注入

コンテナ(PSR-11、PHP-DI、Dice など)を介した依存性注入を使用したい場合、それが利用できるルート定義は、オブジェクトを自分で直接作成してコンテナでオブジェクトを作成する方法か、呼び出すクラスとメソッドを文字列で定義する方法のいずれかです。詳細については、依存性注入 のページを参照してください。

簡単な例を示します:


use flight\database\SimplePdo;

// Greeting.php
class Greeting
{
    protected SimplePdo $db;
    public function __construct(SimplePdo $db) {
        $this->db = $db;
    }

    public function hello(int $id) {
        // $this->db を使って何か処理をする
        $name = $this->db->fetchField("SELECT name FROM users WHERE id = ?", [ $id ]);
        echo "Hello, world! My name is {$name}!";
    }
}

// index.php

// 必要なパラメータを指定してコンテナをセットアップします
// PSR-11 の詳細については依存性注入のページを参照してください
$dice = new \Dice\Dice();

// 変数を '$dice = ' で再代入することを忘れないでください!!!!!
$dice = $dice->addRule(SimplePdo::class, [
    'shared' => true,
    'constructParams' => [ 
        'mysql:host=localhost;dbname=test', 
        'root',
        'password'
    ]
]);

// コンテナハンドラを登録する
Flight::registerContainerHandler(function($class, $params) use ($dice) {
    return $dice->create($class, $params);
});

// 通常どおりルートを定義する
Flight::route('/hello/@id', [ 'Greeting', 'hello' ]);
// または
Flight::route('/hello/@id', 'Greeting->hello');
// または
Flight::route('/hello/@id', 'Greeting::hello');

Flight::start();

次のルートへの実行の受け渡し

非推奨 コールバック関数から true を返すことで、次にマッチするルートに実行を渡すことができます。

Flight::route('/user/@name', function (string $name) {
  // 何らかの条件をチェックする
  if ($name !== "Bob") {
    // 次のルートに進む
    return true;
  }
});

Flight::route('/user/*', function () {
  // これは呼び出されます
});

このような複雑なユースケースを処理するには、ミドルウェア を使用することをお勧めします。

ルートエイリアス

ルートにエイリアスを割り当てることで、後でアプリ内でそのエイリアスを動的に呼び出して URL を生成できます(例: HTML テンプレート内のリンク、リダイレクト URL の生成など)。

Flight::route('/users/@id', function($id) { echo 'user:'.$id; }, false, 'user_view');
// または 
Flight::route('/users/@id', function($id) { echo 'user:'.$id; })->setAlias('user_view');

// 後でコード内のどこかで
class UserController {
    public function update() {

        // ユーザーを保存するコード...
        $id = $user['id']; // 例: 5

        $redirectUrl = Flight::getUrl('user_view', [ 'id' => $id ]); // '/users/5' を返します
        Flight::redirect($redirectUrl);
    }
}

これは、URL が変更される場合に特に役立ちます。上記の例で、users が代わりに /admin/users/@id に移動されたとします。ルートにエイリアスを設定しておけば、エイリアスが上記の例のように /admin/users/5 を返すため、コード内の古い URL をすべて見つけて変更する必要はもうありません。

ルートエイリアスはグループ内でも機能します:

Flight::group('/users', function() {
    Flight::route('/@id', function($id) { echo 'user:'.$id; }, false, 'user_view');
    // または
    Flight::route('/@id', function($id) { echo 'user:'.$id; })->setAlias('user_view');
});

ルート情報の確認

マッチしたルート情報を確認したい場合、2 つの方法があります:

  1. Flight::router() オブジェクトの executedRoute プロパティを使用する。
  2. ルートメソッドの第 3 パラメータに true を渡すことで、ルートオブジェクトをコールバックに渡すようリクエストする。ルートオブジェクトは常にコールバック関数の最後のパラメータとして渡されます。

executedRoute

Flight::route('/', function() {
  $route = Flight::router()->executedRoute;
  // $route を使って何か処理をする
  // マッチした HTTP メソッドの配列
  $route->methods;

  // 名前付きパラメータの配列
  $route->params;

  // マッチした正規表現
  $route->regex;

  // URL パターンで使用された '*' の内容を含む
  $route->splat;

  // URL パスを表示する...本当に必要な場合
  $route->pattern;

  // このルートに割り当てられたミドルウェアを表示する
  $route->middleware;

  // このルートに割り当てられたエイリアスを表示する
  $route->alias;
});

注: executedRoute プロパティは、ルートが実行された後にのみ設定されます。ルートが実行される前にアクセスしようとすると、NULL になります。また、executedRoute は ミドルウェア 内でも使用できます!

ルート定義に true を渡す

Flight::route('/', function(\flight\net\Route $route) {
  // マッチした HTTP メソッドの配列
  $route->methods;

  // 名前付きパラメータの配列
  $route->params;

  // マッチした正規表現
  $route->regex;

  // URL パターンで使用された '*' の内容を含む
  $route->splat;

  // URL パスを表示する...本当に必要な場合
  $route->pattern;

  // このルートに割り当てられたミドルウェアを表示する
  $route->middleware;

  // このルートに割り当てられたエイリアスを表示する
  $route->alias;
}, true);// <-- この true パラメータによってそれが実現されます

ルートのグループ化とミドルウェア

関連するルートをグループ化したい場合があります(例: /api/v1)。これを行うには、group メソッドを使用します:

Flight::group('/api/v1', function () {
  Flight::route('/users', function () {
    // /api/v1/users にマッチする
  });

  Flight::route('/posts', function () {
    // /api/v1/posts にマッチする
  });
});

グループをネストすることもできます:

Flight::group('/api', function () {
  Flight::group('/v1', function () {
    // Flight::get() は変数を取得するもので、ルートを設定するものではありません。以下のオブジェクトコンテキストを参照してください
    Flight::route('GET /users', function () {
      // GET /api/v1/users にマッチする
    });

    Flight::post('/posts', function () {
      // POST /api/v1/posts にマッチする
    });

    Flight::put('/posts/1', function () {
      // PUT /api/v1/posts にマッチする
    });
  });
  Flight::group('/v2', function () {

    // Flight::get() は変数を取得するもので、ルートを設定するものではありません。以下のオブジェクトコンテキストを参照してください
    Flight::route('GET /users', function () {
      // GET /api/v2/users にマッチする
    });
  });
});

オブジェクトコンテキストでのグループ化

Engine オブジェクトを使用してルートグループを使うこともできます:

$app = Flight::app();

$app->group('/api/v1', function (Router $router) {

  // $router 変数を使用する
  $router->get('/users', function () {
    // GET /api/v1/users にマッチする
  });

  $router->post('/posts', function () {
    // POST /api/v1/posts にマッチする
  });
});

注: これはルートとグループを $router オブジェクトで定義する推奨方法です。

ミドルウェアでのグループ化

ルートのグループにミドルウェアを割り当てることもできます:

Flight::group('/api/v1', function () {
  Flight::route('/users', function () {
    // /api/v1/users にマッチする
  });
}, [ MyAuthMiddleware::class ]); // インスタンスを使用する場合は [ new MyAuthMiddleware() ] も可

詳細については、グループミドルウェア のページを参照してください。

リソースルーティング

resource メソッドを使用して、リソース用のルートセットを作成できます。これにより、RESTful 規約に従ったリソース用のルートセットが作成されます。

リソースを作成するには、次のようにします:

Flight::resource('/users', UsersController::class);

バックグラウンドでは、次のルートが作成されます:

[
      'index' => 'GET /users',
      'create' => 'GET /users/create',
      'store' => 'POST /users',
      'show' => 'GET /users/@id',
      'edit' => 'GET /users/@id/edit',
      'update' => 'PUT /users/@id',
      'destroy' => 'DELETE /users/@id'
]

コントローラーでは、次のメソッドを使用します:

class UsersController
{
    public function index(): void
    {
    }

    public function show(string $id): void
    {
    }

    public function create(): void
    {
    }

    public function store(): void
    {
    }

    public function edit(string $id): void
    {
    }

    public function update(string $id): void
    {
    }

    public function destroy(string $id): void
    {
    }
}

: 新しく追加されたルートは、php runway routes を実行することで runway で確認できます。

リソースルートのカスタマイズ

リソースルートを設定するためのオプションがいくつかあります。

エイリアスベース

aliasBase を設定できます。デフォルトでは、エイリアスは指定された URL の最後の部分です。たとえば、/users/ の場合、aliasBaseusers になります。これらのルートが作成されるとき、エイリアスは users.indexusers.create などになります。エイリアスを変更したい場合は、aliasBase を希望する値に設定します。

Flight::resource('/users', UsersController::class, [ 'aliasBase' => 'user' ]);
Only と Except

only および except オプションを使用して、作成するルートを指定することもできます。

// これらのメソッドのみをホワイトリスト化し、残りをブラックリスト化する
Flight::resource('/users', UsersController::class, [ 'only' => [ 'index', 'show' ] ]);
// これらのメソッドのみをブラックリスト化し、残りをホワイトリスト化する
Flight::resource('/users', UsersController::class, [ 'except' => [ 'create', 'store', 'edit', 'update', 'destroy' ] ]);

これらは基本的にホワイトリストおよびブラックリストのオプションであり、作成するルートを指定できます。

ミドルウェア

resource メソッドによって作成された各ルートで実行されるミドルウェアを指定することもできます。

Flight::resource('/users', UsersController::class, [ 'middleware' => [ MyAuthMiddleware::class ] ]);

ストリーミングレスポンス

stream() または streamWithHeaders() を使用して、クライアントにレスポンスをストリーミングできるようになりました。これは、大きなファイルの送信、長時間実行プロセス、または大きなレスポンスの生成に役立ちます。ストリーミングルートは、通常のルートとは少し異なる方法で処理されます。

注: ストリーミングレスポンスは、flight.v2.output_bufferingfalse に設定されている場合にのみ利用できます。

手動ヘッダーによるストリーミング

ルートの stream() メソッドを使用して、クライアントにレスポンスをストリーミングできます。これを行う場合、クライアントに何かを出力する前に、すべてのヘッダーを手動で設定する必要があります。これは、PHP の header() 関数または Flight::response()->setRealHeader() メソッドを使用して行います。

Flight::route('/@filename', function($filename) {

    $response = Flight::response();

    // もちろん、パスをサニタイズするなどしてください。
    $fileNameSafe = basename($filename);

    // ルート実行後にここで追加のヘッダーを設定する場合、
    // 何かが出力される前に定義する必要があります。
    // それらはすべて header() 関数への生の呼び出し、または
    // Flight::response()->setRealHeader() への呼び出しである必要があります。
    header('Content-Disposition: attachment; filename="'.$fileNameSafe.'"');
    // または
    $response->setRealHeader('Content-Disposition: attachment; filename="'.$fileNameSafe.'"');

    $filePath = '/some/path/to/files/'.$fileNameSafe;

    if (!is_readable($filePath)) {
        Flight::halt(404, 'File not found');
    }

    // 必要に応じてコンテンツの長さを手動で設定する
    header('Content-Length: '.filesize($filePath));
    // または
    $response->setRealHeader('Content-Length: '.filesize($filePath));

    // ファイルを読み取りながらクライアントにストリーミングする
    readfile($filePath);

// ここが魔法の行です
})->stream();

ヘッダー付きストリーミング

streamWithHeaders() メソッドを使用して、ストリーミングを開始する前にヘッダーを設定することもできます。

Flight::route('/stream-users', function() {

    // ここに追加のヘッダーを自由に追加できます
    // header() または Flight::response()->setRealHeader() を使用する必要があります

    // データの取得方法は何でも構いませんが、例として...
    $users_stmt = Flight::db()->query("SELECT id, first_name, last_name FROM users");

    echo '{';
    $user_count = count($users);
    while($user = $users_stmt->fetch(PDO::FETCH_ASSOC)) {
        echo json_encode($user);
        if(--$user_count > 0) {
            echo ',';
        }

        // これはデータをクライアントに送信するために必要です
        ob_flush();
    }
    echo '}';

// ストリーミングを開始する前にヘッダーを設定する方法は次のとおりです。
})->streamWithHeaders([
    'Content-Type' => 'application/json',
    'Content-Disposition' => 'attachment; filename="users.json"',
    // オプションのステータスコード、デフォルトは 200
    'status' => 200
]);

関連項目

トラブルシューティング

404 Not Found または予期しないルート動作

404 Not Found エラーが表示されている場合(しかし、それが本当に存在し、タイプミスではないと確信している場合)、これは実際にはルートエンドポイントで値をエコーせずに返していることが問題である可能性があります。この理由は意図的なものですが、一部の開発者には気づかれないかもしれません。

Flight::route('/hello', function(){
    // これは 404 Not Found エラーを引き起こす可能性があります
    return 'Hello World';
});

// おそらく必要なのはこちら
Flight::route('/hello', function(){
    echo 'Hello World';
});

この理由は、ルーターに組み込まれた特別なメカニズムによるもので、返された出力を「次のルートに進む」ための合図として処理します。この動作は、ルーティング セクションで文書化されています。

変更履歴

Learn/learn

Flightについて学ぶ

Flightは、PHP向けの高速でシンプル、そして拡張可能なフレームワークです。非常に多用途で、あらゆる種類のWebアプリケーションの構築に使用できます。 シンプルさを念頭に置いて構築されており、人間にもAIコーディングアシスタントにも理解しやすく使いやすい方法で書かれています。

注記: Flight::を静的変数として使う例と、$app->エンジンオブジェクトを使う例があります。どちらも互換性があります。コントローラー/ミドルウェアでの$app$this->appは、Flightチームが推奨する方法です(そして公式スケルトン+ AGENTS.mdが新規プロジェクトで標準としている方法です)。

コアコンポーネント

ルーティング

Webアプリケーションのルートを管理する方法を学びます。ルートのグループ化、ルートパラメータ、ミドルウェアも含まれます。

ミドルウェア

アプリケーションでリクエストとレスポンスをフィルタリングするためのミドルウェアの使用方法を学びます。

オートローディング

独自のクラスをオートロードする方法を学びます。フォルダの大文字小文字は名前空間と一致している必要があります。スケルトンではApp\と、app/Controller/のようなパスカルケースのフォルダを使用します。

リクエスト

アプリケーションでリクエストとレスポンスを処理する方法を学びます。

レスポンス

ユーザーにレスポンスを送信する方法を学びます。

HTMLテンプレート

組み込みのPHPビューだけでなく、Twig(スケルトンのデフォルト)、Latte、または他のエンジンを使ってHTMLをレンダリングする方法を学びます。

セキュリティ

一般的なセキュリティ脅威からアプリケーションを保護する方法を学びます。

設定

アプリケーション向けにフレームワークを設定する方法を学びます。

イベントマネージャー

イベントシステムを使用してアプリケーションにカスタムイベントを追加する方法を学びます。

Flightの拡張

独自のメソッドとクラスを追加してフレームワークを拡張する方法を学びます。

メソッドフックとフィルタリング

自分のメソッドやフレームワーク内部のメソッドにイベントフックを追加する方法を学びます。

依存性注入コンテナ (DIC)

依存性注入コンテナ(DIC)を使用してアプリケーションの依存関係を管理する方法を学びます。

ユーティリティクラス

コレクション

コレクションはデータを保持し、使いやすさのために配列またはオブジェクトとしてアクセスできるようにするために使用されます。

JSONラッパー

JSONのエンコードとデコードを一貫して行うためのシンプルな関数がいくつかあります。

SimplePdo

PDOは時として必要以上に頭痛の種になることがあります。SimplePdoは、insert()update()delete()transaction()のような便利なメソッドを備えた最新のPDOヘルパークラスで、データベース操作をはるかに簡単にします。

PdoWrapper(非推奨)

元のPDOラッパーはv3.18.0で非推奨となりました。代わりにSimplePdoを使用してください。

アップロードファイルハンドラー

アップロードされたファイルを管理し、恒久的な場所に移動するのに役立つシンプルなクラスです。

重要なコンセプト

なぜフレームワークなのか?

フレームワークを使うべき理由についての短い記事です。フレームワークを使い始める前に、その利点を理解しておくことは良い考えです。

さらに、@lubianaによって素晴らしいチュートリアルが作成されています。Flightについて特に詳しく説明しているわけではありませんが、このガイドはフレームワークを取り巻く主要な概念と、それらがなぜ有益であるかを理解するのに役立ちます。チュートリアルはこちらにあります。

Flightと他のフレームワークの比較

Laravel、Slim、Fat-Free、Symfonyなどの他のフレームワークからFlightに移行する場合、このページは両者の違いを理解するのに役立ちます。

その他のトピック

ユニットテスト

このガイドに従って、Flightコードを堅牢にするためのユニットテストの方法を学びましょう。

AIと開発者体験

FlightはコーディングLLMと連携するように構築されています:AGENTS.md、Runwayのai:*コマンド、そしてエージェントがパターンを維持できるようにする明確なスケルトンレイアウトです。

v2 -> v3への移行

後方互換性はほとんど維持されていますが、v2からv3に移行する際に注意すべき変更点がいくつかあります。

Learn/unit_testing

ユニットテスト

概要

Flightでのユニットテストは、アプリケーションが期待どおりに動作することを確認し、バグを早期に発見し、コードベースを保守しやすくするのに役立ちます。Flightは、最も人気のあるPHPテストフレームワークであるPHPUnitとスムーズに連携するように設計されています。

理解

ユニットテストは、アプリケーションの小さな部分(コントローラやサービスなど)の動作を単体でチェックします。Flightでは、ルート、コントローラ、ロジックがさまざまな入力にどのように応答するかを、グローバル状態や実際の外部サービスに依存せずにテストすることを意味します。

主要な原則:

基本的な使い方

PHPUnitのセットアップ

  1. ComposerでPHPUnitをインストールします。
    composer require --dev phpunit/phpunit
  2. プロジェクトルートに tests ディレクトリを作成します。
  3. composer.json にテストスクリプトを追加します。
    "scripts": {
        "test": "phpunit --configuration phpunit.xml"
    }
  4. phpunit.xml ファイルを作成します。
    <?xml version="1.0" encoding="UTF-8"?>
    <phpunit bootstrap="vendor/autoload.php">
        <testsuites>
            <testsuite name="Flight Tests">
                <directory>tests</directory>
            </testsuite>
        </testsuites>
    </phpunit>

これで composer test を実行してテストを実行できます。

シンプルなルートハンドラのテスト

メールアドレスを検証するルートがあるとします。

// index.php
$app->route('POST /register', [ UserController::class, 'register' ]);

// UserController.php
class UserController {
    protected $app;
    public function __construct(flight\Engine $app) {
        $this->app = $app;
    }
    public function register() {
        $email = $this->app->request()->data->email;
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            return $this->app->json(['status' => 'error', 'message' => 'Invalid email']);
        }
        return $this->app->json(['status' => 'success', 'message' => 'Valid email']);
    }
}

このコントローラのシンプルなテスト:

use PHPUnit\Framework\TestCase;
use flight\Engine;

class UserControllerTest extends TestCase {
    public function testValidEmailReturnsSuccess() {
        $app = new Engine();
        $app->request()->data->email = 'test@example.com';
        $controller = new UserController($app);
        $controller->register();
        $response = $app->response()->getBody();
        $output = json_decode($response, true);
        $this->assertEquals('success', $output['status']);
        $this->assertEquals('Valid email', $output['message']);
    }

    public function testInvalidEmailReturnsError() {
        $app = new Engine();
        $app->request()->data->email = 'invalid-email';
        $controller = new UserController($app);
        $controller->register();
        $response = $app->response()->getBody();
        $output = json_decode($response, true);
        $this->assertEquals('error', $output['status']);
        $this->assertEquals('Invalid email', $output['message']);
    }
}

ヒント:

テスト可能なコントローラのための依存性注入の使用

依存関係(データベースやメーラーなど)をコントローラに注入することで、テストでモックしやすくなります。

use flight\database\SimplePdo;

class UserController {
    protected $app;
    protected $db;
    protected $mailer;
    public function __construct($app, $db, $mailer) {
        $this->app = $app;
        $this->db = $db;
        $this->mailer = $mailer;
    }
    public function register() {
        $email = $this->app->request()->data->email;
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            return $this->app->json(['status' => 'error', 'message' => 'Invalid email']);
        }
        $this->db->runQuery('INSERT INTO users (email) VALUES (?)', [$email]);
        $this->mailer->sendWelcome($email);
        return $this->app->json(['status' => 'success', 'message' => 'User registered']);
    }
}

モックを使ったテスト:

use PHPUnit\Framework\TestCase;

class UserControllerDICTest extends TestCase {
    public function testValidEmailSavesAndSendsEmail() {
        $mockDb = $this->createMock(flight\database\SimplePdo::class);
        $mockDb->method('runQuery')->willReturn(true);
        $mockMailer = new class {
            public $sentEmail = null;
            public function sendWelcome($email) { $this->sentEmail = $email; return true; }
        };
        $app = new flight\Engine();
        $app->request()->data->email = 'test@example.com';
        $controller = new UserController($app, $mockDb, $mockMailer);
        $controller->register();
        $response = $app->response()->getBody();
        $result = json_decode($response, true);
        $this->assertEquals('success', $result['status']);
        $this->assertEquals('User registered', $result['message']);
        $this->assertEquals('test@example.com', $mockMailer->sentEmail);
    }
}

高度な使い方

関連情報

トラブルシューティング

変更履歴

Learn/flight_vs_symfony

フライト対シンフォニー

シンフォニーとは?

Symfony は、再利用可能な PHP コンポーネントと、Web プロジェクト用の PHP フレームワークです。

最高の PHP アプリケーションが構築される標準基盤。独自のアプリケーションに使用できる 50 個のスタンドアロンコンポーネントのいずれかを選択してください。

PHP Web アプリケーションの作成とメンテナンスをスピードアップします。反復的なコーディングタスクを終了し、コードを制御する力を享受します。

フライトとの比較での長所

フライトとの比較での短所

Learn/flight_vs_another_framework

他のフレームワークとのFlightの比較

もし、Laravel、Slim、Fat-Free、あるいはSymfonyのような他のフレームワークからFlightに移行している場合、このページは両者の違いを理解するのに役立ちます。

Laravel

Laravelはベルと笛がすべて揃った充実したフレームワークで、驚くべき開発者中心のエコシステムを持っていますが、パフォーマンスと複雑さと引き換えになります。

LaravelとFlightの比較を見る.

Slim

SlimはFlightに似たマイクロフレームワークです。軽量で使いやすく設計されていますが、Flightよりも少し複雑になることがあります。

SlimとFlightの比較を見る.

Fat-Free

Fat-Freeはより小さなパッケージで提供されるフルスタックフレームワークです。ツールはすべてそろっていますが、いくつかのプロジェクトをより複雑にするデータアーキテクチャを持っています。

Fat-FreeとFlightの比較を見る.

Symfony

Symfonyはモジュール式のエンタープライズレベルのフレームワークであり、柔軟性と拡張性を備えています。より小さなプロジェクトや新人開発者にとって、Symfonyは少し抵抗があるかもしれません。

SymfonyとFlightの比較を見る.

Learn/pdo_wrapper

PdoWrapper PDO ヘルパークラス

警告

非推奨: PdoWrapper は Flight v3.18.0 以降非推奨です。将来的なバージョンで削除されることはありませんが、後方互換性のためメンテナンスされます。代わりに SimplePdo を使用してください。これは同じ機能を提供し、共通のデータベース操作のための追加のヘルパーメソッドも提供します。

概要

Flight の PdoWrapper クラスは、PDO を使用してデータベースを操作するための親しみやすいヘルパーです。共通のデータベースタスクを簡素化し、結果を取得するための便利なメソッドを追加し、結果を Collections として返して簡単にアクセスできるようにします。また、クエリログとアプリケーションのパフォーマンス監視 (APM) を高度なユースケースでサポートします。

理解

PHP でデータベースを操作する場合、特に PDO を直接使用すると冗長になることがあります。PdoWrapper は PDO を拡張し、クエリ、取得、結果の処理をはるかに簡単にします。準備されたステートメントやフェッチモードを扱う代わりに、共通のタスクのためのシンプルなメソッドが得られ、すべての行が Collection として返されるため、配列またはオブジェクト表記を使用できます。

PdoWrapper を Flight で共有サービスとして登録し、Flight::db() を介してアプリケーションのどこでも使用できます。

基本的な使用方法

PDO ヘルパーの登録

まず、PdoWrapper クラスを Flight に登録します:

Flight::register('db', \flight\database\PdoWrapper::class, [
    'mysql:host=localhost;dbname=cool_db_name', 'user', 'pass', [
        PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'utf8mb4\'',
        PDO::ATTR_EMULATE_PREPARES => false,
        PDO::ATTR_STRINGIFY_FETCHES => false,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
    ]
]);

これで、Flight::db() をどこでも使用してデータベース接続を取得できます。

クエリの実行

runQuery()

function runQuery(string $sql, array $params = []): PDOStatement

INSERT や UPDATE、または結果を手動で取得したい場合に使用します:

$db = Flight::db();
$statement = $db->runQuery("SELECT * FROM users WHERE status = ?", ['active']);
while ($row = $statement->fetch()) {
    // $row は配列です
}

書き込みにも使用できます:

$db->runQuery("INSERT INTO users (name) VALUES (?)", ['Alice']);
$db->runQuery("UPDATE users SET name = ? WHERE id = ?", ['Bob', 1]);

fetchField()

function fetchField(string $sql, array $params = []): mixed

データベースから単一の値を入手します:

$count = Flight::db()->fetchField("SELECT COUNT(*) FROM users WHERE status = ?", ['active']);

fetchRow()

function fetchRow(string $sql, array $params = []): Collection

単一の行を Collection (配列/オブジェクトアクセス) として入手します:

$user = Flight::db()->fetchRow("SELECT * FROM users WHERE id = ?", [123]);
echo $user['name'];
// または
echo $user->name;

fetchAll()

function fetchAll(string $sql, array $params = []): array<Collection>

すべての行を Collection の配列として入手します:

$users = Flight::db()->fetchAll("SELECT * FROM users WHERE status = ?", ['active']);
foreach ($users as $user) {
    echo $user['name'];
    // または
    echo $user->name;
}

IN() プレースホルダーの使用

IN() 句で単一の ? を使用し、配列またはカンマ区切りの文字列を渡せます:

$ids = [1, 2, 3];
$users = Flight::db()->fetchAll("SELECT * FROM users WHERE id IN (?)", [$ids]);
// または
$users = Flight::db()->fetchAll("SELECT * FROM users WHERE id IN (?)", ['1,2,3']);

高度な使用方法

クエリログと APM

クエリのパフォーマンスを追跡したい場合、登録時に APM 追跡を有効にします:

Flight::register('db', \flight\database\PdoWrapper::class, [
    'mysql:host=localhost;dbname=cool_db_name', 'user', 'pass', [/* オプション */], true // 最後のパラメータで APM を有効にします
]);

クエリを実行した後、手動でログを記録できますが、APM が有効な場合は自動的にログを記録します:

Flight::db()->logQueries();

これはイベント (flight.db.queries) をトリガーし、接続とクエリメトリクスを含み、Flight のイベントシステムを使用してリッスンできます。

完全な例

Flight::route('/users', function () {
    // すべてのユーザーを取得
    $users = Flight::db()->fetchAll('SELECT * FROM users');

    // すべてのユーザーをストリーミング
    $statement = Flight::db()->runQuery('SELECT * FROM users');
    while ($user = $statement->fetch()) {
        echo $user['name'];
    }

    // 単一のユーザーを取得
    $user = Flight::db()->fetchRow('SELECT * FROM users WHERE id = ?', [123]);

    // 単一の値を入手
    $count = Flight::db()->fetchField('SELECT COUNT(*) FROM users');

    // 特別な IN() 構文
    $users = Flight::db()->fetchAll('SELECT * FROM users WHERE id IN (?)', [[1,2,3,4,5]]);
    $users = Flight::db()->fetchAll('SELECT * FROM users WHERE id IN (?)', ['1,2,3,4,5']);

    // 新しいユーザーを挿入
    Flight::db()->runQuery("INSERT INTO users (name, email) VALUES (?, ?)", ['Bob', 'bob@example.com']);
    $insert_id = Flight::db()->lastInsertId();

    // ユーザーを更新
    Flight::db()->runQuery("UPDATE users SET name = ? WHERE id = ?", ['Bob', 123]);

    // ユーザーを削除
    Flight::db()->runQuery("DELETE FROM users WHERE id = ?", [123]);

    // 影響を受けた行数を入手
    $statement = Flight::db()->runQuery("UPDATE users SET name = ? WHERE name = ?", ['Bob', 'Sally']);
    $affected_rows = $statement->rowCount();
});

関連項目

トラブルシューティング

変更履歴

Learn/dependency_injection_container

依存性注入コンテナ

概要

依存性注入コンテナ(DIC)は、アプリケーションの依存関係を管理できる強力な拡張機能です。また、Flightが AIコーディングツール やユニットテストとうまく連携できる最大の理由の1つでもあります。コントローラーは、グローバルにアクセスする代わりに、コンストラクターで必要なものを受け取ります。

理解

依存性注入(DI)は、現代のPHPフレームワークにおける重要な概念であり、オブジェクトのインスタンス化と構成を管理するために使用されます。DICライブラリの例としては、flightphp/containerDicePimplePHP-DIleague/container などがあります。

DICは、クラスを一元管理された場所で作成・管理するための凝った方法です。同じオブジェクトを複数のクラス(コントローラー、ミドルウェア、コマンドなど)に渡す必要がある場合に便利です。

公式の flightphp/skeleton は、app/config/services.phpDice を配線し、共有の flight\Engine インスタンスを置き換え、[App\Controller\HomeController::class, 'index'] のようなルートターゲットを解決します。新しいプロジェクトでは、人間とエージェントが同じ場所を編集できるように、このパターンを採用してください。

基本的な使い方

従来のやり方は次のようになるでしょう。


require 'vendor/autoload.php';

// データベースからユーザーを管理するクラス
class UserController {

    protected PDO $pdo;

    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
    }

    public function view(int $id) {
        $stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = :id');
        $stmt->execute(['id' => $id]);

        print_r($stmt->fetch());
    }
}

// routes.php ファイル内

$db = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');

$UserController = new UserController($db);
Flight::route('/user/@id', [ $UserController, 'view' ]);
// その他のUserControllerルート...
Flight::start();

上記のコードから、新しい PDO オブジェクトを作成して UserController クラスに渡していることがわかります。これは小規模なアプリケーションには問題ありませんが、アプリケーションが大きくなるにつれて、同じ PDO オブジェクトを複数の場所で作成または受け渡ししていることに気づくでしょう。ここでDICが役立ちます。

次に、DIC(Diceを使用)を使った同じ例を示します。


require 'vendor/autoload.php';

// 上記と同じクラスです。何も変更されていません
class UserController {

    protected PDO $pdo;

    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
    }

    public function view(int $id) {
        $stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = :id');
        $stmt->execute(['id' => $id]);

        print_r($stmt->fetch());
    }
}

// 新しいコンテナを作成
$container = new \Dice\Dice;

// PDOオブジェクトの作成方法をコンテナに指示するルールを追加
// 下記のように必ず自分自身に再代入することを忘れないでください!
$container = $container->addRule('PDO', [
    // sharedは、毎回同じオブジェクトが返されることを意味します
    'shared' => true,
    'constructParams' => ['mysql:host=localhost;dbname=test', 'user', 'pass' ]
]);

// これによりコンテナハンドラーが登録され、Flightがそれを使用することを認識します。
Flight::registerContainerHandler(function($class, $params) use ($container) {
    return $container->create($class, $params);
});

// これでコンテナを使ってUserControllerを作成できます
Flight::route('/user/@id', [ UserController::class, 'view' ]);

Flight::start();

きっと、この例には余分なコードがたくさん追加されたと思っているかもしれません。魔法が発揮されるのは、PDO オブジェクトを必要とする別のコントローラーがあるときです。


// すべてのコントローラーのコンストラクターがPDOオブジェクトを必要とする場合
// 以下の各ルートには自動的にPDOが注入されます!!!
Flight::route('/company/@id', [ CompanyController::class, 'view' ]);
Flight::route('/organization/@id', [ OrganizationController::class, 'view' ]);
Flight::route('/category/@id', [ CategoryController::class, 'view' ]);
Flight::route('/settings', [ SettingsController::class, 'view' ]);

DICを利用する追加の利点は、ユニットテストがはるかに簡単になることです。モックオブジェクトを作成してクラスに渡すことができます。これは、アプリケーションのテストを書く際に大きなメリットです。また、AIアシスタントがコントローラーを生成する場合、コンストラクターインジェクションにより、従うべき明確で一貫性のあるパターンが提供されます(ユニットテストガイド)。

集中管理型DICハンドラーの作成

アプリを拡張することで、サービスファイルに集中管理型のDICハンドラーを作成できます。次に例を示します。

// services.php

// 新しいコンテナを作成
$container = new \Dice\Dice;
// 下記のように必ず自分自身に再代入することを忘れないでください!
$container = $container->addRule('PDO', [
    // sharedは、毎回同じオブジェクトが返されることを意味します
    'shared' => true,
    'constructParams' => ['mysql:host=localhost;dbname=test', 'user', 'pass' ]
]);

// これで、任意のオブジェクトを作成するためのマップ可能なメソッドを作成できます。
Flight::map('make', function($class, $params = []) use ($container) {
    return $container->create($class, $params);
});

// これによりコンテナハンドラーが登録され、Flightがコントローラー/ミドルウェアにそれを使用することを認識します。
Flight::registerContainerHandler(function($class, $params) {
    return Flight::make($class, $params);
});


// コンストラクターでPDOオブジェクトを受け取る次のサンプルクラスがあるとします
class EmailCron {
    protected PDO $pdo;

    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
    }

    public function send() {
        // メールを送信するコード
    }
}

// そして最後に、依存性注入を使用してオブジェクトを作成できます
$emailCron = Flight::make(EmailCron::class);
$emailCron->send();

flightphp/container

Flightには、依存性注入を処理するために使用できるシンプルなPSR-11準拠コンテナを提供するプラグインがあります。その使用例を簡単に示します。


// 例: index.php
require 'vendor/autoload.php';

use flight\Container;

$container = new Container;

$container->set(PDO::class, fn(): PDO => new PDO('sqlite::memory:'));

Flight::registerContainerHandler([$container, 'get']);

class TestController {
  private PDO $pdo;

  function __construct(PDO $pdo) {
    $this->pdo = $pdo;
  }

  function index() {
    var_dump($this->pdo);
    // これは正しく出力されます!
  }
}

Flight::route('GET /', [TestController::class, 'index']);

Flight::start();

flightphp/container の高度な使い方

依存関係を再帰的に解決することもできます。次に例を示します。

<?php

require 'vendor/autoload.php';

use flight\Container;

class User {}

interface UserRepository {
  function find(int $id): ?User;
}

class PdoUserRepository implements UserRepository {
  private PDO $pdo;

  function __construct(PDO $pdo) {
    $this->pdo = $pdo;
  }

  function find(int $id): ?User {
    // 実装 ...
    return null;
  }
}

$container = new Container;

$container->set(PDO::class, static fn(): PDO => new PDO('sqlite::memory:'));
$container->set(UserRepository::class, PdoUserRepository::class);

$userRepository = $container->get(UserRepository::class);
var_dump($userRepository);

/*
object(PdoUserRepository)#4 (1) {
  ["pdo":"PdoUserRepository":private]=>
  object(PDO)#3 (0) {
  }
}
 */

DICE

独自のDICハンドラーを作成することもできます。これは、PSR-11ではない独自のコンテナ(Diceなど)を使用したい場合に便利です。これを行う方法については、基本的な使い方 のセクションを参照してください。

さらに、Flightを使用する際に作業を容易にする便利なデフォルトがいくつかあります。

Engineインスタンス($app インジェクションに必要)

コントローラーやミドルウェアで flight\Engine を型宣言する場合、Dice は新しい Engine を構築してはなりません。ブートストラップから同じインスタンスを置き換えてください。これが公式スケルトンのやり方であり、AI生成コントローラーで AGENTS.md が期待するパターンです。

// ブートストラップ / services.php のどこか
use flight\Engine;
use flight\database\SimplePdo;

$app = Flight::app(); // または $engine = Flight::app();

$container = new \Dice\Dice;
$container = $container->addRule('*', [
    'substitutions' => [
        // 重要: ブートストラップ済みのEngineを再利用してください。Diceに `new Engine()` をさせないでください
        Engine::class => $app,
        // 新しいコードではSimplePdoを推奨します
        // SimplePdo::class => $db,
        // Config::class => $config,
        // \Twig\Environment::class => $twig,
    ]
]);

$app->registerContainerHandler(function ($class, $params) use ($container) {
    return $container->create($class, $params);
});

// ルート以外のコード用のオプションヘルパー
$app->map('make', function ($class, $params = []) use ($container) {
    return $container->create($class, $params);
});
// app/Controller/MyController.php(スケルトンレイアウト — フォルダー名の大文字小文字は名前空間と一致します)
namespace App\Controller;

use flight\Engine;

class MyController
{
    protected Engine $app;

    public function __construct(Engine $app)
    {
        $this->app = $app;
    }

    public function index(): void
    {
        // アプリ層では Flight:: ファサードを使用しません — テストが容易で、AIツールにとっても明確です
        $this->app->render('welcome', ['message' => 'Hello']);
    }
}
// app/config/routes.php
use App\Controller\MyController;

$router->get('/', [MyController::class, 'index']);

Engine の置き換えをスキップすると、Dice が2つ目の Engine を構築し、コントローラーがブートストラップからのルート、設定、マップされた Twig render を共有しなくなる可能性があります。

他の共有サービスを追加する(SimplePdo、Config、Twig)

use flight\database\SimplePdo;
use flight\Engine;

// services.php で $db、$config、$twig を作成した後:
$substitutions = [
    Engine::class => $app,
    SimplePdo::class => $db,
    // App\Utils\Config::class => $config,
    // \Twig\Environment::class => $twig,
];

$container = $container->addRule('*', [
    'substitutions' => $substitutions,
]);

これで、コントローラーはコンストラクターで SimplePdo $db(または設定型)を受け取り、Flight::db() を呼び出す必要がなくなります。これは、ユニットテスト のガイダンスとスケルトンのハウススタイルに一致します。

他のクラスを追加する

コンテナに追加したい他のクラスがある場合、Dice ではコンテナによって自動的に解決されるため簡単です。次に例を示します。


$container = new \Dice\Dice;
// クラスに依存関係を注入する必要がない場合
// 何も定義する必要はありません!
Flight::registerContainerHandler(function($class, $params) use ($container) {
    return $container->create($class, $params);
});

class MyCustomClass {
    public function parseThing() {
        return 'thing';
    }
}

class UserController {

    protected MyCustomClass $MyCustomClass;

    public function __construct(MyCustomClass $MyCustomClass) {
        $this->MyCustomClass = $MyCustomClass;
    }

    public function index() {
        echo $this->MyCustomClass->parseThing();
    }
}

Flight::route('/user', 'UserController->index');

PSR-11

Flightは、PSR-11準拠の任意のコンテナも使用できます。つまり、PSR-11インターフェースを実装した任意のコンテナを使用できます。次に、LeagueのPSR-11コンテナを使用した例を示します。


require 'vendor/autoload.php';

use flight\database\SimplePdo;

// 上記と同じUserControllerの考え方ですが、素のPDOの代わりにSimplePdoを型宣言します

$container = new \League\Container\Container();
$container->add(UserController::class)->addArgument(SimplePdo::class);
$container->add(SimplePdo::class)
    ->addArgument('mysql:host=localhost;dbname=test')
    ->addArgument('user')
    ->addArgument('pass');
Flight::registerContainerHandler($container);

Flight::route('/user', [ 'UserController', 'view' ]);

Flight::start();

これは以前のDiceの例よりも少し冗長かもしれませんが、同じ利点で目的を達成できます!

関連情報

トラブルシューティング

変更履歴

Learn/middleware

ミドルウェア

概要

Flight はルートおよびグループルートのミドルウェアをサポートします。ミドルウェアは、アプリケーションの一部で、ルートコールバックの前(または後)にコードが実行される場所です。これは、コードに API 認証チェックを追加する優れた方法です。また、ユーザーがルートにアクセスする権限があるかを検証することもできます。

理解

ミドルウェアはアプリを大幅に簡素化できます。複雑な抽象クラス継承やメソッドオーバーライドの代わりに、ミドルウェアを使用することで、カスタムのアプリロジックをルートに割り当ててルートを制御できます。ミドルウェアはサンドイッチのようなものだと考えられます。外側にパンがあり、その中にレタス、トマト、肉、チーズなどの層があります。そして、各リクエストがサンドイッチを一口かじるようなもので、外側の層から食べてコアに向かっていくイメージです。

ミドルウェアの動作の視覚的な例を以下に示します。その後、この機能の実践的な例を示します。

ユーザー リクエストが URL /api に到達 ----> 
    Middleware->before() が実行 ----->
        /api にアタッチされたコールバック/メソッドが実行され、レスポンスが生成 ------>
    Middleware->after() が実行 ----->
ユーザーがサーバーからレスポンスを受信

そして、実践的な例はこちらです:

ユーザーが URL /dashboard に移動
    LoggedInMiddleware->before() が実行
        before() が有効なログインモッションをチェック
            有効な場合、何もしないで実行を続行
            無効な場合、ユーザーを /login にリダイレクト
                /api にアタッチされたコールバック/メソッドが実行され、レスポンスが生成
    LoggedInMiddleware->after() に何も定義されていないため、実行を続行
ユーザーがサーバーからダッシュボードの HTML を受信

実行順序

ミドルウェア関数は、ルートに追加された順序で実行されます。この実行は、Slim Framework がこれを扱う方法 に似ています。

before() メソッドは追加された順序で実行され、after() メソッドは逆順で実行されます。

例: Middleware1->before()、Middleware2->before()、Middleware2->after()、Middleware1->after()。

基本的な使用方法

ミドルウェアは、匿名関数やクラス(推奨)を含む任意のコールバックメソッドとして使用できます。

匿名関数

簡単な例を以下に示します:

Flight::route('/path', function() { echo ' Here I am!'; })->addMiddleware(function() {
    echo 'Middleware first!';
});

Flight::start();

// これは "Middleware first! Here I am!" を出力します

注意: 匿名関数を使用する場合、解釈されるのは before() メソッドのみです。匿名クラスで after() 動作を定義できません

クラスの使用

ミドルウェアはクラスとして登録できます(推奨されます)。「after」機能が必要な場合は、クラスを使用する必要があります

class MyMiddleware {
    public function before($params) {
        echo 'Middleware first!';
    }

    public function after($params) {
        echo 'Middleware last!';
    }
}

$MyMiddleware = new MyMiddleware();
Flight::route('/path', function() { echo ' Here I am! '; })->addMiddleware($MyMiddleware); 
// また ->addMiddleware([ $MyMiddleware, $MyMiddleware2 ]) も可能です

Flight::start();

// これは "Middleware first! Here I am! Middleware last!" を表示します

ミドルウェアのクラス名のみを定義し、クラスをインスタンス化することもできます。

Flight::route('/path', function() { echo ' Here I am! '; })->addMiddleware(MyMiddleware::class); 

注意: ミドルウェアの名前のみを渡す場合、依存性注入コンテナ によって自動的に実行され、ミドルウェアは必要なパラメータで実行されます。依存性注入コンテナが登録されていない場合、デフォルトで __construct(Engine $app)flight\Engine インスタンスが渡されます。

パラメータ付きルートの使用

ルートからパラメータが必要な場合、それらはミドルウェア関数に単一の配列として渡されます。(function($params) { ... } または public function before($params) { ... })。その理由は、パラメータをグループ化し、一部のグループでパラメータの順序が異なり、誤ったパラメータを参照してミドルウェア関数を壊す可能性があるためです。この方法では、位置ではなく名前でアクセスできます。

use flight\Engine;

class RouteSecurityMiddleware {

    protected Engine $app;

    public function __construct(Engine $app) {
        $this->app = $app;
    }

    public function before(array $params) {
        $clientId = $params['clientId'];

        // jobId は渡される場合とされない場合があります
        $jobId = $params['jobId'] ?? 0;

        // job ID がない場合、何も検索する必要がないかもしれません
        if($jobId === 0) {
            return;
        }

        // データベースで何らかの検索を実行
        $isValid = !!$this->app->db()->fetchField("SELECT 1 FROM client_jobs WHERE client_id = ? AND job_id = ?", [ $clientId, $jobId ]);

        if($isValid !== true) {
            $this->app->halt(400, 'You are blocked, muahahaha!');
        }
    }
}

// routes.php
$router->group('/client/@clientId/job/@jobId', function(Router $router) {

    // 下記のグループも親のミドルウェアを受け取ります
    // ただし、パラメータはミドルウェアに単一の配列として渡されます
    $router->group('/job/@jobId', function(Router $router) {
        $router->get('', [ JobController::class, 'view' ]);
        $router->put('', [ JobController::class, 'update' ]);
        $router->delete('', [ JobController::class, 'delete' ]);
        // さらにルート...
    });
}, [ RouteSecurityMiddleware::class ]);

ミドルウェア付きのルートグループ化

ルートグループを追加し、そのグループ内のすべてのルートに同じミドルウェアを適用できます。これは、ヘッダーの API キーをチェックする Auth ミドルウェアなどでルートをグループ化する必要がある場合に便利です。


// グループメソッドの最後に追加
Flight::group('/api', function() {

    // この「空」のルートは実際には /api に一致します
    Flight::route('', function() { echo 'api'; }, false, 'api');
    // これは /api/users に一致します
    Flight::route('/users', function() { echo 'users'; }, false, 'users');
    // これは /api/users/1234 に一致します
    Flight::route('/users/@id', function($id) { echo 'user:'.$id; }, false, 'user_view');
}, [ new ApiAuthMiddleware() ]);

すべてのルートにグローバルなミドルウェアを適用したい場合、「空」のグループを追加できます:


// グループメソッドの最後に追加
Flight::group('', function() {

    // これは依然として /users です
    Flight::route('/users', function() { echo 'users'; }, false, 'users');
    // これは依然として /users/1234 です
    Flight::route('/users/@id', function($id) { echo 'user:'.$id; }, false, 'user_view');
}, [ ApiAuthMiddleware::class ]); // または [ new ApiAuthMiddleware() ]、同じです

一般的な使用例

API キー検証

/api ルートを保護するために API キーが正しいかを検証したい場合、ミドルウェアで簡単に処理できます。

use flight\Engine;

class ApiMiddleware {

    protected Engine $app;

    public function __construct(Engine $app) {
        $this->app = $app;
    }

    public function before(array $params) {
        $authorizationHeader = $this->app->request()->getHeader('Authorization');
        $apiKey = str_replace('Bearer ', '', $authorizationHeader);

        // データベースで API キーを検索
        $apiKeyHash = hash('sha256', $apiKey);
        $hasValidApiKey = !!$this->db()->fetchField("SELECT 1 FROM api_keys WHERE hash = ? AND valid_date >= NOW()", [ $apiKeyHash ]);

        if($hasValidApiKey !== true) {
            $this->app->jsonHalt(['error' => 'Invalid API Key']);
        }
    }
}

// routes.php
$router->group('/api', function(Router $router) {
    $router->get('/users', [ ApiController::class, 'getUsers' ]);
    $router->get('/companies', [ ApiController::class, 'getCompanies' ]);
    // さらにルート...
}, [ ApiMiddleware::class ]);

これで、設定した API キー検証ミドルウェアによってすべての API ルートが保護されます!ルータグループにさらにルートを追加すると、即座に同じ保護が適用されます!

ログインバリデーション

ログインユーザーのみが利用可能なルートを保護したいですか?ミドルウェアで簡単に実現できます!

use flight\Engine;

class LoggedInMiddleware {

    protected Engine $app;

    public function __construct(Engine $app) {
        $this->app = $app;
    }

    public function before(array $params) {
        $session = $this->app->session();
        if($session->get('logged_in') !== true) {
            $this->app->redirect('/login');
            exit;
        }
    }
}

// routes.php
$router->group('/admin', function(Router $router) {
    $router->get('/dashboard', [ DashboardController::class, 'index' ]);
    $router->get('/clients', [ ClientController::class, 'index' ]);
    // さらにルート...
}, [ LoggedInMiddleware::class ]);

ルートパラメータ検証

ユーザーが URL の値を変更してアクセスすべきでないデータにアクセスするのを防ぎたいですか?ミドルウェアで解決できます!

use flight\Engine;

class RouteSecurityMiddleware {

    protected Engine $app;

    public function __construct(Engine $app) {
        $this->app = $app;
    }

    public function before(array $params) {
        $clientId = $params['clientId'];
        $jobId = $params['jobId'];

        // データベースで何らかの検索を実行
        $isValid = !!$this->app->db()->fetchField("SELECT 1 FROM client_jobs WHERE client_id = ? AND job_id = ?", [ $clientId, $jobId ]);

        if($isValid !== true) {
            $this->app->halt(400, 'You are blocked, muahahaha!');
        }
    }
}

// routes.php
$router->group('/client/@clientId/job/@jobId', function(Router $router) {
    $router->get('', [ JobController::class, 'view' ]);
    $router->put('', [ JobController::class, 'update' ]);
    $router->delete('', [ JobController::class, 'delete' ]);
    // さらにルート...
}, [ RouteSecurityMiddleware::class ]);

ミドルウェア実行の処理

認証ミドルウェアがあり、認証されていない場合にユーザーをログインページにリダイレクトしたいとします。いくつかのオプションがあります:

  1. ミドルウェア関数から false を返し、Flight が自動的に 403 Forbidden エラーを返しますが、カスタマイズはできません。
  2. Flight::redirect() を使用してユーザーをログインページにリダイレクトできます。
  3. ミドルウェア内でカスタムエラーを作成し、ルートの実行を停止できます。

シンプルでストレート

簡単な return false; の例を以下に示します:

class MyMiddleware {
    public function before($params) {
        $hasUserKey = Flight::session()->exists('user');
        if ($hasUserKey === false) {
            return false;
        }

        // true の場合、すべて続行されます
    }
}

リダイレクトの例

ユーザーをログインページにリダイレクトする例を以下に示します:

class MyMiddleware {
    public function before($params) {
        $hasUserKey = Flight::session()->exists('user');
        if ($hasUserKey === false) {
            Flight::redirect('/login');
            exit;
        }
    }
}

カスタムエラーの例

API を構築していて JSON エラーをスローする必要があるとします。以下のようにできます:

class MyMiddleware {
    public function before($params) {
        $authorization = Flight::request()->getHeader('Authorization');
        if(empty($authorization)) {
            Flight::jsonHalt(['error' => 'You must be logged in to access this page.'], 403);
            // または
            Flight::json(['error' => 'You must be logged in to access this page.'], 403);
            exit;
            // または
            Flight::halt(403, json_encode(['error' => 'You must be logged in to access this page.']));
        }
    }
}

関連項目

トラブルシューティング

変更履歴

Learn/filtering

フィルタリング

概要

Flight は、マップされたメソッド が呼び出される前と後にフィルタリングを許可します。

理解

覚える必要のある事前定義されたフックはありません。デフォルトのフレームワーク メソッドのいずれか、またはマップしたカスタム メソッドのいずれかをフィルタリングできます。

フィルター関数は以下のようになります:

/**
 * @param array $params The parameters passed to the method being filtered.
 * @param string $output (v2 output buffering only) The output of the method being filtered.
 * @return bool Return true/void or don't return to continue the chain, false to break the chain.
 */
function (array &$params, string &$output): bool {
  // Filter code
}

渡された変数を使用して、入力パラメータと/または出力を操作できます。

メソッドの前にフィルターを実行するには:

Flight::before('start', function (array &$params, string &$output): bool {
  // Do something
});

メソッドの後にフィルターを実行するには:

Flight::after('start', function (array &$params, string &$output): bool {
  // Do something
});

任意のメソッドに必要な数のフィルターを追加できます。それらは宣言された順序で呼び出されます。

フィルタリング プロセスの例を以下に示します:

// Map a custom method
Flight::map('hello', function (string $name) {
  return "Hello, $name!";
});

// Add a before filter
Flight::before('hello', function (array &$params, string &$output): bool {
  // Manipulate the parameter
  $params[0] = 'Fred';
  return true;
});

// Add an after filter
Flight::after('hello', function (array &$params, string &$output): bool {
  // Manipulate the output
  $output .= " Have a nice day!";
  return true;
});

// Invoke the custom method
echo Flight::hello('Bob');

これは以下を表示するはずです:

Hello Fred! Have a nice day!

複数のフィルターを定義している場合、フィルター関数のいずれかで false を返すことでチェーンを中断できます:

Flight::before('start', function (array &$params, string &$output): bool {
  echo 'one';
  return true;
});

Flight::before('start', function (array &$params, string &$output): bool {
  echo 'two';

  // This will end the chain
  return false;
});

// This will not get called
Flight::before('start', function (array &$params, string &$output): bool {
  echo 'three';
  return true;
});

Note: Core methods such as map and register cannot be filtered because they are called directly and not invoked dynamically. See Extending Flight for more information.

関連項目

トラブルシューティング

変更履歴

Learn/requests

リクエスト

概要

Flight は HTTP リクエストを単一のオブジェクトにカプセル化し、以下の方法でアクセスできます:

$request = Flight::request();

理解

HTTP リクエストは、HTTP ライフサイクルの理解に不可欠なコア要素の一つです。ユーザーがウェブブラウザや HTTP クライアントでアクションを実行すると、ヘッダー、本文、URL などをあなたのプロジェクトに送信します。これらのヘッダー(ブラウザの言語、扱える圧縮の種類、ユーザーエージェントなど)をキャプチャし、Flight アプリケーションに送信された本文と URL をキャプチャできます。これらのリクエストは、アプリが次に何をするかを理解するために不可欠です。

基本的な使用方法

PHP には $_GET$_POST$_REQUEST$_SERVER$_FILES$_COOKIE などのスーパーグローバルがあります。Flight はこれらを便利な Collections に抽象化します。querydatacookiesfiles プロパティを配列またはオブジェクトとしてアクセスできます。

注意: プロジェクトでこれらのスーパーグローバルを使用することは強く推奨されません。request() オブジェクト経由で参照してください。

注意: $_ENV の抽象化は利用できません。

$_GET

$_GET 配列は query プロパティ経由でアクセスできます:

// GET /search?keyword=something
Flight::route('/search', function(){
    $keyword = Flight::request()->query['keyword'];
    // または
    $keyword = Flight::request()->query->keyword;
    echo "You are searching for: $keyword";
    // $keyword でデータベースをクエリしたり、その他のことを行います
});

$_POST

$_POST 配列は data プロパティ経由でアクセスできます:

Flight::route('POST /submit', function(){
    $name = Flight::request()->data['name'];
    $email = Flight::request()->data['email'];
    // または
    $name = Flight::request()->data->name;
    $email = Flight::request()->data->email;
    echo "You submitted: $name, $email";
    // $name と $email でデータベースに保存したり、その他のことを行います
});

$_COOKIE

$_COOKIE 配列は cookies プロパティ経由でアクセスできます:

Flight::route('GET /login', function(){
    $savedLogin = Flight::request()->cookies['myLoginCookie'];
    // または
    $savedLogin = Flight::request()->cookies->myLoginCookie;
    // 本当に保存されているかチェックし、保存されている場合は自動的にログインします
    if($savedLogin) {
        Flight::redirect('/dashboard');
        return;
    }
});

新しいクッキー値の設定に関するヘルプについては、overclokk/cookie を参照してください。

$_SERVER

$_SERVER 配列は getVar() メソッド経由でアクセスするためのショートカットがあります:


$host = Flight::request()->getVar('HTTP_HOST');

$_FILES

アップロードされたファイルは files プロパティ経由でアクセスできます:

// $_FILES プロパティへの生アクセス。推奨アプローチは以下を参照
$uploadedFile = Flight::request()->files['myFile']; 
// または
$uploadedFile = Flight::request()->files->myFile;

詳細については Uploaded File Handler を参照してください。

ファイルアップロードの処理

v3.12.0

フレームワークを使用してヘルパーメソッドでファイルアップロードを処理できます。基本的に、リクエストからファイルデータを引き出し、新しい場所に移動するだけです。

Flight::route('POST /upload', function(){
    // 入力フィールドが <input type="file" name="myFile"> のような場合
    $uploadedFileData = Flight::request()->getUploadedFiles();
    $uploadedFile = $uploadedFileData['myFile'];
    $uploadedFile->moveTo('/path/to/uploads/' . $uploadedFile->getClientFilename());
});

複数のファイルがアップロードされた場合、それらをループで処理できます:

Flight::route('POST /upload', function(){
    // 入力フィールドが <input type="file" name="myFiles[]"> のような場合
    $uploadedFiles = Flight::request()->getUploadedFiles()['myFiles'];
    foreach ($uploadedFiles as $uploadedFile) {
        $uploadedFile->moveTo('/path/to/uploads/' . $uploadedFile->getClientFilename());
    }
});

セキュリティ注意: ユーザー入力の検証とサニタイズを常に実行してください。特にファイルアップロードの場合、許可する拡張子の種類を検証し、ファイルの「マジックバイト」を検証して、ユーザーが主張するファイルの種類であることを確認してください。このヘルプには 記事 ライブラリ が利用可能です。

リクエスト本文

POST/PUT リクエストなどの生の HTTP リクエスト本文を取得するには、以下のようにします:

Flight::route('POST /users/xml', function(){
    $xmlBody = Flight::request()->getBody();
    // 送信された XML で何かを行います。
});

JSON 本文

コンテンツタイプ application/json のリクエストを受け取り、例として {"id": 123} のデータの場合、data プロパティから利用可能です:

$id = Flight::request()->data->id;

リクエストヘッダー

getHeader() または getHeaders() メソッドを使用してリクエストヘッダーにアクセスできます:


// Authorization ヘッダーが必要な場合
$host = Flight::request()->getHeader('Authorization');
// または
$host = Flight::request()->header('Authorization');

// すべてのヘッダーを取得する場合
$headers = Flight::request()->getHeaders();
// または
$headers = Flight::request()->headers();

リクエストメソッド

method プロパティまたは getMethod() メソッドを使用してリクエストメソッドにアクセスできます:

$method = Flight::request()->method; // 実際には getMethod() で設定されます
$method = Flight::request()->getMethod();

注意: getMethod() メソッドは最初に $_SERVER['REQUEST_METHOD'] からメソッドを引き出し、存在する場合に $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] または $_REQUEST['_method'] で上書きできます。

リクエストオブジェクトのプロパティ

リクエストオブジェクトは以下のプロパティを提供します:

ヘルパーメソッド

URL の一部を組み合わせたり、特定のヘッダーを扱うためのいくつかのヘルパーメソッドがあります。

フル URL

getFullUrl() メソッドを使用してフルリクエスト URL にアクセスできます:

$url = Flight::request()->getFullUrl();
// https://example.com/some/path?foo=bar

ベース URL

getBaseUrl() メソッドを使用してベース URL にアクセスできます:

// http://example.com/path/to/something/cool?query=yes+thanks
$url = Flight::request()->getBaseUrl();
// https://example.com
// 注意: 末尾のスラッシュはありません。

クエリ解析

parseQuery() メソッドに URL を渡すと、クエリ文字列を連想配列に解析できます:

$query = Flight::request()->parseQuery('https://example.com/some/path?foo=bar');
// ['foo' => 'bar']

コンテンツ Accept タイプのネゴシエーション

v3.17.2

negotiateContentType() メソッドを使用して、クライアントが送信した Accept ヘッダーに基づいて、最適なコンテンツタイプを決定できます。


// 例: Accept ヘッダー: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
// 以下でサポートするものを定義します。
$availableTypes = ['application/json', 'application/xml'];
$typeToServe = Flight::request()->negotiateContentType($availableTypes);
if ($typeToServe === 'application/json') {
    // JSON レスポンスを送信
} elseif ($typeToServe === 'application/xml') {
    // XML レスポンスを送信
} else {
    // デフォルトで何か他のものを設定するか、エラーをスロー
}

注意: Accept ヘッダーに利用可能なタイプが見つからない場合、メソッドは null を返します。Accept ヘッダーが定義されていない場合、メソッドは $availableTypes 配列の最初のタイプを返します。

関連項目

トラブルシューティング

変更履歴

Learn/why_frameworks

フレームワークを使う理由

一部のプログラマーは、フレームワークの使用に熱烈に反対しています。フレームワークは膨大で、遅く、学習が困難だと主張しています。 彼らは、フレームワークは不要であり、それらなしでより良いコードを書くことができると述べています。 フレームワークを使用することのデメリットについては、いくつかの妥当なポイントがあります。 ただし、フレームワークを使用する利点もたくさんあります。

フレームワークを使用する理由

フレームワークを使用したいと思う理由のいくつかを以下に示します:

Flightはマイクロフレームワークです。 つまり、小さく軽量です。 LaravelやSymfonyのような大規模なフレームワークほどの機能は提供しません。 ただし、Webアプリケーションを構築するために必要な機能の多くを提供します。 また、学びやすく使用も容易です。 これにより、簡単かつ迅速にWebアプリケーションを構築するのに適しています。 フレームワークに新しい場合は、Flightは初心者に最適なフレームワークです。 フレームワークを使用する利点を学び、過度な複雑さで圧倒されることなく学習するのに役立ちます。 Flightの経験を積んだ後は、LaravelやSymfonyなどのより複雑なフレームワークに移ることがより簡単になります。 ただし、Flightでも成功した堅牢なアプリケーションを作成できます。

ルーティングとは?

ルーティングはFlightフレームワークの中核ですが、それは一体何でしょうか? ルーティングとは、URLを取得してコード内の特定の関数に一致させるプロセスです。 これにより、WebサイトをリクエストされたURLに基づいて異なる動作をさせることができます。 たとえば、ユーザーが/user/1234を訪れたときにユーザープロフィールを表示したいが、/usersを訪れたときに全ユーザーのリストを表示したいとします。 これはすべてルーティングを通じて行われます。

以下のようになります:

そして、なぜ重要なの?

適切な中央集権的なルーターを持つことで、あなたの生活が劇的に簡単になる可能性があります! 最初はそれが見えにくいかもしれません。 以下は、そのいくつか理由です:

多分、Webサイトを作成するためのスクリプトごとの方法に慣れているかもしれません。 index.phpというファイルがあり、URLを確認し、URLに基づいて特定の関数を実行します。 これもルーティングの一形態ですが、整理されていない上にすぐに手に負えなくなります。 Flightのルーティングシステムは、ルーティングを処理するより整理された強力な方法です。

これ?


// /user/view_profile.php?id=1234
if ($_GET['id']) {
    $id = $_GET['id'];
    viewUserProfile($id);
}

// /user/edit_profile.php?id=1234
if ($_GET['id']) {
    $id = $_GET['id'];
    editUserProfile($id);
}

// etc...

それともこちら?


// index.php
Flight::route('/user/@id', [ 'UserController', 'viewUserProfile' ]);
Flight::route('/user/@id/edit', [ 'UserController', 'editUserProfile' ]);

// In maybe your app/controllers/UserController.php
class UserController {
    public function viewUserProfile($id) {
        // do something
    }

    public function editUserProfile($id) {
        // do something
    }
}

中央集権的なルーティングシステムを使用する利点が見え始めたことを願っています。 長い目で見れば、管理や理解が容易になります!

リクエストとレスポンス

Flightはリクエストとレスポンスを処理するための簡単で簡単な方法を提供します。 これがWebフレームワークの中核です。 ユーザーのブラウザからのリクエストを受け取り、処理してからレスポンスを返すことで、Webアプリケーションを構築できます。 これにより、ユーザープロフィールを表示したり、ユーザーのログインを許可したり、新しいブログ投稿を許可したりするWebアプリケーションを構築できます。

リクエスト

リクエストは、ユーザーのブラウザがウェブサイトを訪れるときにサーバーに送信するものです。 このリクエストには、ユーザーが何をしたいかに関する情報が含まれます。 たとえば、ユーザーが訪れたいURLの情報、ユーザーがサーバーに送りたいデータ、サーバーから受け取りたいデータの種類などが含まれるかもしれません。 リクエストは読み取り専用です。 リクエストを変更することはできませんが、読み取ることはできます。

Flightはリクエストに関する情報にアクセスするための簡単な方法を提供します。 Flight::request()メソッドを使用してリクエストに関する情報にアクセスできます。 このメソッドはリクエストに関する情報を含むRequestオブジェクトを返します。 このオブジェクトを使用して、URL、メソッド、ユーザーがサーバーに送信したデータなどの情報にアクセスできます。

レスポンス

レスポンスとは、ユーザーのブラウザがウェブサイトを訪れるときにサーバーがユーザーのブラウザに送り返すものです。 このレスポンスには、サーバーが行いたいことに関する情報が含まれます。 たとえば、サーバーがユーザーに送信したいデータの種類、ユーザーから受け取りたいデータの種類、サーバーがユーザーのコンピュータに保存したいデータの種類などが含まれるかもしれません。

Flightはユーザーのブラウザにレスポンスを送信する簡単な方法を提供します。 Flight::response()メソッドを使用してレスポンスを送信できます。 このメソッドは、Responseオブジェクトを引数として受け取り、そのレスポンスをユーザーのブラウザに送信します。 このオブジェクトを使用して、HTML、JSON、ファイルなど、ユーザーのブラウザにレスポンスを送信できます。 Flightはレスポンスの一部を自動生成して簡単にするお手伝いをしますが、最終的にはユーザーに送り返す内容を制御できます。

Learn/responses

レスポンス

概要

Flight はレスポンスヘッダーの一部を生成するのを手伝いますが、ユーザーに返す内容のほとんどの制御はあなたが持ちます。通常は response() オブジェクトに直接アクセスしますが、Flight にはレスポンスヘッダーの一部を設定するためのヘルパーメソッドもあります。

理解

ユーザーが request リクエストをアプリケーションに送信した後、彼らに適切なレスポンスを生成する必要があります。彼らは好みの言語、特定の種類の圧縮を扱えるかどうか、ユーザーエージェントなどを含む情報を送信してきました。すべてを処理した後、適切なレスポンスを彼らに返します。これはヘッダーの設定、HTML や JSON のボディを出力、またはページへのリダイレクトです。

基本的な使用方法

レスポンスボディの送信

Flight は出力のバッファリングに ob_start() を使用します。これにより、echoprint を使用してユーザーにレスポンスを送信でき、Flight がそれをキャプチャして適切なヘッダーと共にユーザーに返します。

// This will send "Hello, World!" to the user's browser
Flight::route('/', function() {
    echo "Hello, World!";
});

// HTTP/1.1 200 OK
// Content-Type: text/html
//
// Hello, World!

代替として、write() メソッドを呼び出してボディに追加することもできます。

// This will send "Hello, World!" to the user's browser
Flight::route('/', function() {
    // verbose, but gets the job sometimes when you need it
    Flight::response()->write("Hello, World!");

    // if you want to retrieve the body that you've set at this point
    // you can do so like this
    $body = Flight::response()->getBody();
});

JSON

Flight は JSON および JSONP レスポンスの送信をサポートします。JSON レスポンスを送信するには、JSON エンコードされるデータを渡します:

Flight::route('/@companyId/users', function(int $companyId) {
    // somehow pull out your users from a database for example
    $users = Flight::db()->fetchAll("SELECT id, first_name, last_name FROM users WHERE company_id = ?", [ $companyId ]);

    Flight::json($users);
});
// [{"id":1,"first_name":"Bob","last_name":"Jones"}, /* more users */ ]

Note: By default, Flight will send a Content-Type: application/json header with the response. It will also use the flags JSON_THROW_ON_ERROR and JSON_UNESCAPED_SLASHES when encoding the JSON.

ステータスコード付き JSON

2 番目の引数としてステータスコードを渡すこともできます:

Flight::json(['id' => 123], 201);

プリティプリント付き JSON

最後の位置に引数を渡してプリティプリントを有効にすることもできます:

Flight::json(['id' => 123], 200, true, 'utf-8', JSON_PRETTY_PRINT);

JSON 引数の順序変更

Flight::json() は非常に古いメソッドですが、Flight の目標はプロジェクトの後方互換性を維持することです。引数の順序を変更してよりシンプルな構文を使用したい場合、JSON メソッドを他の Flight メソッドと同様に remap するだけです:

Flight::map('json', function($data, $code = 200, $options = 0) {

    // now you don't have to `true, 'utf-8'` when using the json() method!
    Flight::_json($data, $code, true, 'utf-8', $options);
}

// And now it can be used like this
Flight::json(['id' => 123], 200, JSON_PRETTY_PRINT);

JSON と実行の停止

v3.10.0

JSON レスポンスを送信して実行を停止したい場合、jsonHalt() メソッドを使用できます。これは、承認などのチェックを行い、ユーザーが承認されていない場合に JSON レスポンスを即座に送信し、既存のボディコンテンツをクリアして実行を停止するのに便利です。

Flight::route('/users', function() {
    $authorized = someAuthorizationCheck();
    // Check if the user is authorized
    if($authorized === false) {
        Flight::jsonHalt(['error' => 'Unauthorized'], 401);
        // no exit; needed here.
    }

    // Continue with the rest of the route
});

v3.10.0 以前では、以下のようにする必要がありました:

Flight::route('/users', function() {
    $authorized = someAuthorizationCheck();
    // Check if the user is authorized
    if($authorized === false) {
        Flight::halt(401, json_encode(['error' => 'Unauthorized']));
    }

    // Continue with the rest of the route
});

レスポンスボディのクリア

レスポンスボディをクリアしたい場合、clearBody メソッドを使用できます:

Flight::route('/', function() {
    if($someCondition) {
        Flight::response()->write("Hello, World!");
    } else {
        Flight::response()->clearBody();
    }
});

上記の使用例は一般的ではないかもしれませんが、middleware で使用される場合に一般的になる可能性があります。

レスポンスボディに対するコールバックの実行

addResponseBodyCallback メソッドを使用して、レスポンスボディにコールバックを実行できます:

Flight::route('/users', function() {
    $db = Flight::db();
    $users = $db->fetchAll("SELECT * FROM users");
    Flight::render('users_table', ['users' => $users]);
});

// This will gzip all the responses for any route
Flight::response()->addResponseBodyCallback(function($body) {
    return gzencode($body, 9);
});

複数のコールバックを追加でき、追加された順序で実行されます。これには任意の callable を受け入れるため、クラス配列 [ $class, 'method' ]、クロージャ $strReplace = function($body) { str_replace('hi', 'there', $body); };、または HTML コードを最小化するための関数名 'minify' などを渡せます。

Note: Route callbacks will not work if you are using the flight.v2.output_buffering configuration option.

特定のルートコールバック

これを特定のルートにのみ適用したい場合、ルート内でコールバックを追加できます:

Flight::route('/users', function() {
    $db = Flight::db();
    $users = $db->fetchAll("SELECT * FROM users");
    Flight::render('users_table', ['users' => $users]);

    // This will gzip only the response for this route
    Flight::response()->addResponseBodyCallback(function($body) {
        return gzencode($body, 9);
    });
});

ミドルウェアオプション

middleware を使用して、すべてのルートにコールバックをミドルウェア経由で適用することもできます:

// MinifyMiddleware.php
class MinifyMiddleware {
    public function before() {
        // Apply the callback here on the response() object.
        Flight::response()->addResponseBodyCallback(function($body) {
            return $this->minify($body);
        });
    }

    protected function minify(string $body): string {
        // minify the body somehow
        return $body;
    }
}

// index.php
Flight::group('/users', function() {
    Flight::route('', function() { /* ... */ });
    Flight::route('/@id', function($id) { /* ... */ });
}, [ new MinifyMiddleware() ]);

ステータスコード

status メソッドを使用して、レスポンスのステータスコードを設定できます:

Flight::route('/@id', function($id) {
    if($id == 123) {
        Flight::response()->status(200);
        echo "Hello, World!";
    } else {
        Flight::response()->status(403);
        echo "Forbidden";
    }
});

現在のステータスコードを取得したい場合、引数なしで status メソッドを使用できます:

Flight::response()->status(); // 200

レスポンスヘッダーの設定

header メソッドを使用して、レスポンスのコンテンツタイプなどのヘッダーを設定できます:

// This will send "Hello, World!" to the user's browser in plain text
Flight::route('/', function() {
    Flight::response()->header('Content-Type', 'text/plain');
    // or
    Flight::response()->setHeader('Content-Type', 'text/plain');
    echo "Hello, World!";
});

リダイレクト

redirect() メソッドを使用して現在のリクエストをリダイレクトでき、新しい URL を渡します:

Flight::route('/login', function() {
    $username = Flight::request()->data->username;
    $password = Flight::request()->data->password;
    $passwordConfirm = Flight::request()->data->password_confirm;

    if($password !== $passwordConfirm) {
        Flight::redirect('/new/location');
        return; // this is necessary so functionality below doesn't execute
    }

    // add the new user...
    Flight::db()->runQuery("INSERT INTO users ....");
    Flight::redirect('/admin/dashboard');
});

Note: By default Flight sends a HTTP 303 ("See Other") status code. You can optionally set a custom code:

Flight::redirect('/new/location', 301); // permanent

ルート実行の停止

halt メソッドを呼び出して、任意の時点でフレームワークを停止して即座に終了できます:

Flight::halt();

オプションの HTTP ステータスコードとメッセージを指定することもできます:

Flight::halt(200, 'Be right back...');

halt を呼び出すと、その時点までのレスポンスコンテンツを破棄し、すべての実行を停止します。フレームワークを停止して現在のレスポンスを出力したい場合、stop メソッドを使用します:

Flight::stop($httpStatusCode = null);

Note: Flight::stop() has some odd behavior such as it will output the response but continue executing your script which might not be what you are after. You can use exit or return after calling Flight::stop() to prevent further execution, but it is generally recommended to use Flight::halt().

This will save the header key and value to the response object. At the end of the request lifecycle it will build the headers and send a response.

高度な使用方法

ヘッダーの即時送信

ヘッダーでカスタムなことをする必要があり、作業中のそのコード行でヘッダーを送信する必要がある場合があります。streamed route を設定する場合、これが必要です。これは response()->setRealHeader() で達成できます。

Flight::route('/', function() {
    Flight::response()->setRealHeader('Content-Type: text/plain');
    echo 'Streaming response...';
    sleep(5);
    echo 'Done!';
})->stream();

JSONP

JSONP リクエストの場合、コールバック関数を定義するためのクエリパラメータ名をオプションで渡せます:

Flight::jsonp(['id' => 123], 'q');

したがって、?q=my_func を使用した GET リクエストの場合、出力は以下のようになります:

my_func({"id":123});

クエリパラメータ名を渡さない場合、デフォルトで jsonp になります。

Note: If you are still using JSONP requests in 2025 and beyond, hop in the chat and tell us why! We love hearing some good battle/horror stories!

レスポンスデータのクリア

clear() メソッドを使用して、レスポンスボディとヘッダーをクリアできます。これはレスポンスに割り当てられたすべてのヘッダーをクリアし、レスポンスボディをクリアし、ステータスコードを 200 に設定します。

Flight::response()->clear();

レスポンスボディのみのクリア

レスポンスボディのみをクリアしたい場合、clearBody() メソッドを使用できます:

// This will still keep any headers set on the response() object.
Flight::response()->clearBody();

HTTP キャッシング

Flight は HTTP レベルのキャッシングのビルトインサポートを提供します。キャッシング条件が満たされた場合、Flight は HTTP 304 Not Modified レスポンスを返します。次にクライアントが同じリソースをリクエストすると、ローカルにキャッシュされたバージョンの使用が促されます。

ルートレベルのキャッシング

レスポンス全体をキャッシュしたい場合、cache() メソッドを使用してキャッシュ時間を渡せます。


// This will cache the response for 5 minutes
Flight::route('/news', function () {
  Flight::response()->cache(time() + 300);
  echo 'This content will be cached.';
});

// Alternatively, you can use a string that you would pass
// to the strtotime() method
Flight::route('/news', function () {
  Flight::response()->cache('+5 minutes');
  echo 'This content will be cached.';
});

Last-Modified

lastModified メソッドを使用して、ページが最後に変更された日時を UNIX タイムスタンプで設定できます。クライアントは最後に変更された値が変更されるまでキャッシュを使用し続けます。

Flight::route('/news', function () {
  Flight::lastModified(1234567890);
  echo 'This content will be cached.';
});

ETag

ETag キャッシングは Last-Modified に似ていますが、リソースに任意の ID を指定できます:

Flight::route('/news', function () {
  Flight::etag('my-unique-id');
  echo 'This content will be cached.';
});

lastModified または etag のいずれかを呼び出すと、両方ともキャッシュ値を設定してチェックします。リクエスト間でキャッシュ値が同じ場合、Flight は即座に HTTP 304 レスポンスを送信して処理を停止します。

ファイルのダウンロード

v3.12.0

エンドユーザーにファイルをストリーミングするためのヘルパーメソッドがあります。download メソッドを使用してパスを渡せます。

Flight::route('/download', function () {
  Flight::download('/path/to/file.txt');
  // As of v3.17.1 you can specify a custom filename for the download
  Flight::download('/path/to/file.txt', 'custom_name.txt');
});

関連項目

トラブルシューティング

Changelog

Learn/events

イベント マネージャー

v3.15.0 時点

概要

イベントにより、アプリケーション内でカスタム動作を登録およびトリガーできます。Flight::onEvent()Flight::triggerEvent() の追加により、アプリのライフサイクルにおける重要な時点にフックしたり、独自のイベント(通知やメールなど)を定義したりして、コードをよりモジュール化し、拡張しやすくできます。これらのメソッドは Flight の mappable methods の一部であり、必要に応じて動作をオーバーライドできます。

理解

イベントにより、アプリケーションの異なる部分を分離し、互いに過度に依存しないようにできます。この分離—しばしば デカップリング と呼ばれる—は、コードの更新、拡張、デバッグを容易にします。一つの大きな塊で全てを書く代わりに、論理を特定のアクション(イベント)に応答する小さな独立したピースに分割できます。

ブログアプリを構築していると想像してください:

イベントなしでは、これらを一つの関数に詰め込むことになります。イベントを使うと、分離できます:一部がコメントを保存し、もう一部が 'comment.posted' のようなイベントをトリガーし、別々のリスナーがメールとログを処理します。これによりコードがクリーンになり、通知のような機能を追加または削除する際にコアロジックに触れずに済みます。

一般的なユースケース

主に、イベントはオプションのものに適しており、システムの絶対的なコア部分ではありません。例えば、以下のものは便利ですが、何らかの理由で失敗してもアプリケーションは動作するはずです:

ただし、パスワードを忘れた機能があるとします。これはコア機能の一部であり、イベントではありません。なぜなら、そのメールが送信されなければ、ユーザーはパスワードをリセットできず、アプリケーションを使用できないからです。

基本的な使用方法

Flight のイベントシステムは、2 つの主なメソッドを中心に構築されています:イベントリスナーを登録するための Flight::onEvent() と、イベントを発火するための Flight::triggerEvent()。これらを使用する方法は以下の通りです:

イベントリスナーの登録

イベントをリッスンするには、Flight::onEvent() を使用します。このメソッドにより、イベントが発生したときに何が起こるかを定義できます。

Flight::onEvent(string $event, callable $callback): void

イベントが発生したときに Flight に何をするかを伝えることで、イベントに「購読」します。コールバックはイベントトリガーから渡された引数を受け取ることができます。

Flight のイベントシステムは同期型です。つまり、各イベントリスナーは順番に実行されます。イベントをトリガーすると、そのイベントのすべての登録されたリスナーが完了するまでコードが続行されません。これは、非同期イベントシステム(リスナーが並行して実行されたり、後で実行されたりする)とは異なるため、理解が重要です。

シンプルな例

Flight::onEvent('user.login', function ($username) {
    echo "Welcome back, $username!";

    // you can send an email if the login is from a new location
    // 新しい場所からのログインの場合、メールを送信できます
});

ここで、'user.login' イベントがトリガーされると、ユーザーを名前で挨拶し、必要に応じてメール送信のロジックを含めることができます。

注意: コールバックは関数、匿名関数、またはクラスのメソッドです。

イベントのトリガー

イベントを発生させるには、Flight::triggerEvent() を使用します。これにより、Flight にそのイベントのすべてのリスナーを実行し、提供したデータを渡します。

Flight::triggerEvent(string $event, ...$args): void

シンプルな例

$username = 'alice';
Flight::triggerEvent('user.login', $username);

これにより、'user.login' イベントがトリガーされ、'alice' が以前定義したリスナーに送信され、出力は Welcome back, alice! になります。

イベントの停止

リスナーが false を返すと、そのイベントの追加のリスナーは実行されません。これにより、特定の条件に基づいてイベントチェーンを停止できます。リスナーの順序が重要であることを覚えておいてください。最初に false を返すものが残りを停止します。

:

Flight::onEvent('user.login', function ($username) {
    if (isBanned($username)) {
        logoutUser($username);
        return false; // Stops subsequent listeners
        // 後続のリスナーを停止
    }
});
Flight::onEvent('user.login', function ($username) {
    sendWelcomeEmail($username); // this is never sent
    // これは決して送信されません
});

イベントメソッドのオーバーライド

Flight::onEvent()Flight::triggerEvent()拡張可能 です。つまり、それらの動作を再定義できます。これは、イベントシステムをカスタマイズしたい上級ユーザー(ログ追加やイベントディスパッチの変更など)にとって優れています。

例: onEvent のカスタマイズ

Flight::map('onEvent', function (string $event, callable $callback) {
    // Log every event registration
    // すべてのイベント登録をログに記録
    error_log("New event listener added for: $event");
    // Call the default behavior (assuming an internal event system)
    // デフォルト動作を呼び出し(内部イベントシステムを想定)
    Flight::_onEvent($event, $callback);
});

これで、イベントを登録するたびにログが記録された後、処理が続行されます。

オーバーライドの理由

イベントの配置場所

プロジェクトでイベントの概念に慣れていない場合、アプリ内でこれらのイベントをどこで登録するのか? と疑問に思うかもしれません。Flight のシンプルさにより、厳格なルールはありません—プロジェクトに適した場所に配置できます。ただし、アプリが成長するにつれてコードを維持しやすくするために、整理しておくことが役立ちます。Flight の軽量な性質に合わせた実用的なオプションとベストプラクティスを以下に示します:

オプション 1: メインの index.php

小さなアプリやクイックプロトタイプの場合、index.php ファイル内でルートと共にイベントを登録できます。これにより全てを一箇所にまとめ、シンプルさを優先する場合に適しています。

require 'vendor/autoload.php';

// Register events
// イベントを登録
Flight::onEvent('user.login', function ($username) {
    error_log("$username logged in at " . date('Y-m-d H:i:s'));
    // $username が " . date('Y-m-d H:i:s') . " にログイン
});

// Define routes
// ルートを定義
Flight::route('/login', function () {
    $username = 'bob';
    Flight::triggerEvent('user.login', $username);
    echo "Logged in!";
    // ログインしました!
});

Flight::start();

オプション 2: 専用の events.php ファイル

少し大きなアプリの場合、イベント登録を app/config/events.php のような専用ファイルに移動することを検討してください。index.php でルートの前にこのファイルをインクルードします。これは、Flight プロジェクトでよく見られる app/config/routes.php のルート整理を模倣しています。

// app/config/events.php
Flight::onEvent('user.login', function ($username) {
    error_log("$username logged in at " . date('Y-m-d H:i:s'));
    // $username が " . date('Y-m-d H:i:s') . " にログイン
});

Flight::onEvent('user.registered', function ($email, $name) {
    echo "Email sent to $email: Welcome, $name!";
    // $email にメール送信: ようこそ、$name!
});
// index.php
require 'vendor/autoload.php';
require 'app/config/events.php';

Flight::route('/login', function () {
    $username = 'bob';
    Flight::triggerEvent('user.login', $username);
    echo "Logged in!";
    // ログインしました!
});

Flight::start();

オプション 3: トリガーされる場所の近く

もう一つのアプローチは、イベントをトリガーされる場所の近くで登録すること、例えばコントローラーやルート定義内です。イベントがアプリの一部の特定のものに適している場合に有効です。

Flight::route('/signup', function () {
    // Register event here
    // ここでイベントを登録
    Flight::onEvent('user.registered', function ($email) {
        echo "Welcome email sent to $email!";
        // $email にようこそメールを送信!
    });

    $email = 'jane@example.com';
    Flight::triggerEvent('user.registered', $email);
    echo "Signed up!";
    // 登録ありがとう!
});

Flight のベストプラクティス

ヒント: 目的別にグループ化

events.php 内で、関連イベント(例: すべてのユーザー関連イベント)をコメント付きでグループ化:

// app/config/events.php
// User Events
// ユーザーイベント
Flight::onEvent('user.login', function ($username) {
    error_log("$username logged in");
    // $username がログイン
});
Flight::onEvent('user.registered', function ($email) {
    echo "Welcome to $email!";
    // $email へようこそ!
});

// Page Events
// ページイベント
Flight::onEvent('page.updated', function ($pageId) {
    Flight::cache()->delete("page_$pageId");
});

この構造は拡張しやすく、初心者向けです。

実世界の例

イベントの動作と有用性を示すために、いくつかの実世界のシナリオを歩いてみましょう。

例 1: ユーザー login のログ記録

// Step 1: Register a listener
// ステップ 1: リスナーを登録
Flight::onEvent('user.login', function ($username) {
    $time = date('Y-m-d H:i:s');
    error_log("$username logged in at $time");
    // $username が $time にログイン
});

// Step 2: Trigger it in your app
// ステップ 2: アプリ内でトリガー
Flight::route('/login', function () {
    $username = 'bob'; // Pretend this comes from a form
    // フォームから来たと仮定
    Flight::triggerEvent('user.login', $username);
    echo "Hi, $username!";
    // こんにちは、$username!
});

有用な理由: ログインコードはログについて知る必要がなく、イベントをトリガーするだけです。後でリスナーを追加(例: ようこそメール送信)でき、ルートを変更せずに済みます。

例 2: 新規ユーザーの通知

// Listener for new registrations
// 新規登録のリスナー
Flight::onEvent('user.registered', function ($email, $name) {
    // Simulate sending an email
    // メール送信をシミュレート
    echo "Email sent to $email: Welcome, $name!";
    // $email にメール送信: ようこそ、$name!
});

// Trigger it when someone signs up
// 誰かがサインアップしたときにトリガー
Flight::route('/signup', function () {
    $email = 'jane@example.com';
    $name = 'Jane';
    Flight::triggerEvent('user.registered', $email, $name);
    echo "Thanks for signing up!";
    // サインアップありがとう!
});

有用な理由: サインアップロジックはユーザー作成に集中し、イベントが通知を処理します。後でリスナーを追加(例: サインアップのログ)できます。

例 3: キャッシュのクリア

// Listener to clear a cache
// キャッシュクリアのリスナー
Flight::onEvent('page.updated', function ($pageId) {
    // if using the flightphp/cache plugin
    // flightphp/cache プラグインを使用する場合
    Flight::cache()->delete("page_$pageId");
    echo "Cache cleared for page $pageId.";
    // ページ $pageId のキャッシュをクリア。
});

// Trigger when a page is edited
// ページが編集されたときにトリガー
Flight::route('/edit-page/(@id)', function ($pageId) {
    // Pretend we updated the page
    // ページを更新したと仮定
    Flight::triggerEvent('page.updated', $pageId);
    echo "Page $pageId updated.";
    // ページ $pageId を更新。
});

有用な理由: 編集コードはキャッシングを気にせず、更新をシグナルするだけです。アプリの他の部分が必要に応じて反応できます。

ベストプラクティス

Flight PHP のイベントシステムは、Flight::onEvent()Flight::triggerEvent() により、シンプルでありながら強力な方法で柔軟なアプリケーションを構築できます。アプリの異なる部分がイベントを通じて互いに通信することで、コードを整理、再利用しやすく、拡張しやすく保てます。アクションのログ、通知の送信、更新の管理など、イベントによりロジックを絡めずに実行できます。さらに、これらのメソッドをオーバーライドできるため、システムをニーズに合わせて調整できます。一つのイベントから小さく始め、アプリの構造がどのように変化するかを観察してください!

組み込みイベント

Flight PHP には、フレームワークのライフサイクルにフックするためのいくつかの組み込みイベントがあります。これらのイベントは、リクエスト/レスポンスサイクルの特定の時点でトリガーされ、特定のアクションが発生したときにカスタムロジックを実行できます。

組み込みイベントリスト

関連項目

トラブルシューティング

変更履歴

Learn/templates

HTMLビューとテンプレート

概要

Flightはデフォルトでいくつかの基本的なHTMLテンプレート機能を提供しています。テンプレートは、アプリケーションロジックをプレゼンテーション層から切り離すための非常に効果的な方法です。専用エンジン(Twig、Latteなど)は、AIコーディングツールに馴染みのある制約された構文を提供するため、ビジネスロジックをHTMLに混ぜ込みにくくなります。

理解

アプリケーションを構築するとき、エンドユーザーに返したいHTMLがあるでしょう。PHP自体はテンプレート言語ですが、データベース呼び出しやAPI呼び出しなどのビジネスロジックをHTMLファイルに簡単に埋め込めてしまい、テストや疎結合化が非常に困難になります。データをテンプレートに渡し、テンプレート自体にレンダリングさせることで、コードの疎結合化とユニットテストがはるかに容易になります。テンプレートを使えば、きっと感謝されますよ!

基本的な使い方

Flightでは、renderをマップする(またはビュークラスを登録する)だけで、デフォルトのビューエンジンを別のものに置き換えることができます。Twig、Latte、Smarty、Bladeなどの詳細は下にスクロールしてください。

スケルトンのデフォルト: 公式のflightphp/skeletonは、app/views/*.twig)配下でTwigのみを使用します。コントローラーは$this->app->render('welcome', $data)を呼び出します(拡張子は省略可能)。これは新規プロジェクトのためのアプリケーション側の選択であり、Flightコアの要件ではありません。Latteやその他のエンジンも引き続き完全にサポートされています。

Twig

スケルトンのデフォルト

Twigは、Symfonyや多くのPHPプロジェクトで使用されている、柔軟で高速かつ安全なテンプレートエンジンです。AIコーディングツールは特にTwigをよく知っており、デフォルトで出力を自動エスケープするためXSS対策にも役立ちます。

インストール

composer require twig/twig

composer create-project flightphp/skeletonでインストールした場合は、すでに含まれています。)

基本設定

renderメソッドを上書きして、デフォルトのPHPレンダラーの代わりにTwigを使用します。

// デフォルトのPHPレンダラーの代わりにTwigを使うようにrenderメソッドを上書きします
Flight::map('render', function(string $template, array $data): void {
    $loader = new \Twig\Loader\FilesystemLoader(Flight::get('flight.views.path'));
    $twig = new \Twig\Environment($loader, [
        // Twigがコンパイル済みテンプレートを保存する場所
        'cache' => __DIR__ . '/../cache/twig',
        'auto_reload' => true,
    ]);

    // "welcome" または "welcome.twig" を許可します
    if (substr($template, -5) !== '.twig') {
        $template .= '.twig';
    }

    echo $twig->render($template, $data);
});

スケルトンでは、この配線(設定)はapp/config/services.phpにあります(共有Twig環境、キャッシュパス、base_url / CSP nonceなどのグローバル変数)。コードをAIフレンドリーかつテストフレンドリーに保つには、Engineを注入し、コントローラーから$app->render()を呼び出すことをお勧めします。

FlightでのTwigの使用

Twigでレンダリングできるようになったので、次のようにできます。

{# app/views/home.twig #}
<html>
  <head>
    <title>{% if title %}{{ title }} - {% endif %}My App</title>
    <link rel="stylesheet" href="style.css">
  </head>
  <body>
    <h1>Hello, {{ name }}!</h1>
  </body>
</html>
// routes.php
Flight::route('/@name', function ($name) {
    Flight::render('home.twig', [
        'title' => 'Home Page',
        'name' => $name
    ]);
});

ブラウザで/Bobにアクセスすると、出力は次のようになります。

<html>
  <head>
    <title>Home Page - My App</title>
    <link rel="stylesheet" href="style.css">
  </head>
  <body>
    <h1>Hello, Bob!</h1>
  </body>
</html>

詳細情報

Twigをレイアウトと一緒に使うより完全な例は、このドキュメントのawesome pluginsセクションにあります。Tracyバーでレンダリング時間のメトリクスを確認するには、Tracy ExtensionsのTwigパネルを参照してください。

Twigの全機能については、公式ドキュメントをご覧ください。

Latte

優れた代替案

Latteは、PHPに似た構文を持つフル機能のエンジンです。Flightアプリケーションにとっても優れた選択肢です。スケルトンは、共通のデフォルトとしてTwigを採用しているだけです(特にAIツールがテンプレートを生成する場合に便利です)。

インストール

composer require latte/latte

基本設定

主なアイデアは、renderメソッドを上書きして、デフォルトのPHPレンダラーの代わりにLatteを使用することです。

// デフォルトのPHPレンダラーの代わりにLatteを使うようにrenderメソッドを上書きします
Flight::map('render', function(string $template, array $data, ?string $block): void {
    $latte = new Latte\Engine;

    // Latteがキャッシュを保存する場所
    $latte->setTempDirectory(__DIR__ . '/../cache/');

    $finalPath = Flight::get('flight.views.path') . $template;

    $latte->render($finalPath, $data, $block);
});

FlightでのLatteの使用

Latteでレンダリングできるようになったので、次のようにできます。

<!-- app/views/home.latte -->
<html>
  <head>
    <title>{$title ? $title . ' - '}My App</title>
    <link rel="stylesheet" href="style.css">
  </head>
  <body>
    <h1>Hello, {$name}!</h1>
  </body>
</html>
// routes.php
Flight::route('/@name', function ($name) {
    Flight::render('home.latte', [
        'title' => 'Home Page',
        'name' => $name
    ]);
});

ブラウザで/Bobにアクセスすると、出力は次のようになります。

<html>
  <head>
    <title>Home Page - My App</title>
    <link rel="stylesheet" href="style.css">
  </head>
  <body>
    <h1>Hello, Bob!</h1>
  </body>
</html>

詳細情報

Latteをレイアウトと一緒に使うより複雑な例は、このドキュメントのawesome pluginsセクションにあります。

翻訳や言語機能を含むLatteの全機能については、公式ドキュメントをご覧ください。

組み込みビューエンジン

非推奨

注: これはまだデフォルトの機能であり、技術的にはまだ動作します。

ビューテンプレートを表示するには、テンプレートファイル名とオプションのテンプレートデータを指定してrenderメソッドを呼び出します。

Flight::render('hello.php', ['name' => 'Bob']);

渡したテンプレートデータは自動的にテンプレートに注入され、ローカル変数のように参照できます。テンプレートファイルは単なるPHPファイルです。hello.phpテンプレートファイルの内容が次の場合:

Hello, <?= $name ?>!

出力は次のようになります。

Hello, Bob!

また、setメソッドを使用してビュー変数を手動で設定することもできます。

Flight::view()->set('name', 'Bob');

これで、name変数はすべてのビューで使用できるようになります。したがって、単純に次のようにできます。

Flight::render('hello');

renderメソッドでテンプレートの名前を指定するとき、.php拡張子は省略できることに注意してください。

デフォルトでは、Flightはテンプレートファイル用にviewsディレクトリを探します。次の設定を行うことで、テンプレートの代替パスを設定できます。

Flight::set('flight.views.path', '/path/to/views');

レイアウト

Webサイトでは、コンテンツを差し替えられる単一のレイアウトテンプレートファイルを持つことが一般的です。レイアウトで使用するコンテンツをレンダリングするには、renderメソッドにオプションのパラメータを渡します。

Flight::render('header', ['heading' => 'Hello'], 'headerContent');
Flight::render('body', ['body' => 'World'], 'bodyContent');

これで、ビューにはheaderContentおよびbodyContentという変数が保存されます。次に、次のようにしてレイアウトをレンダリングできます。

Flight::render('layout', ['title' => 'Home Page']);

テンプレートファイルが次のようになっている場合:

header.php:

<h1><?= $heading ?></h1>

body.php:

<div><?= $body ?></div>

layout.php:

<html>
  <head>
    <title><?= $title ?></title>
  </head>
  <body>
    <?= $headerContent ?>
    <?= $bodyContent ?>
  </body>
</html>

出力は次のようになります。

<html>
  <head>
    <title>Home Page</title>
  </head>
  <body>
    <h1>Hello</h1>
    <div>World</div>
  </body>
</html>

Smarty

ビューにSmartyテンプレートエンジンを使用する方法は次のとおりです。

// Smartyライブラリを読み込みます
require './Smarty/libs/Smarty.class.php';

// Smartyをビュークラスとして登録します
// また、読み込み時にSmartyを設定するコールバック関数を渡します
Flight::register('view', Smarty::class, [], function (Smarty $smarty) {
  $smarty->setTemplateDir('./templates/');
  $smarty->setCompileDir('./templates_c/');
  $smarty->setConfigDir('./config/');
  $smarty->setCacheDir('./cache/');
});

// テンプレートデータを割り当てます
Flight::view()->assign('name', 'Bob');

// テンプレートを表示します
Flight::view()->display('hello.tpl');

完全を期すために、Flightのデフォルトのrenderメソッドも上書きする必要があります。

Flight::map('render', function(string $template, array $data): void {
  Flight::view()->assign($data);
  Flight::view()->display($template);
});

Blade

ビューにBladeテンプレートエンジンを使用する方法は次のとおりです。

まず、Composerを使用してBladeOneライブラリをインストールする必要があります。

composer require eftec/bladeone

次に、FlightでBladeOneをビュークラスとして設定できます。

<?php
// BladeOneライブラリを読み込みます
use eftec\bladeone\BladeOne;

// BladeOneをビュークラスとして登録します
// また、読み込み時にBladeOneを設定するコールバック関数を渡します
Flight::register('view', BladeOne::class, [], function (BladeOne $blade) {
  $views = __DIR__ . '/../views';
  $cache = __DIR__ . '/../cache';

  $blade->setPath($views);
  $blade->setCompiledPath($cache);
});

// テンプレートデータを共有します
Flight::view()->share('name', 'Bob');

// テンプレートを表示します
echo Flight::view()->run('hello', []);

完全を期すために、Flightのデフォルトのrenderメソッドも上書きする必要があります。

<?php
Flight::map('render', function(string $template, array $data): void {
  echo Flight::view()->run($template, $data);
});

この例では、hello.blade.phpテンプレートファイルは次のようになります。

<?php
Hello, {{ $name }}!

出力は次のようになります。

Hello, Bob!

関連項目

トラブルシューティング

変更履歴

Learn/simple_pdo

SimplePdo PDO ヘルパークラス

概要

Flight の SimplePdo クラスは、PDO を使用したデータベース操作のためのモダンで機能豊富なヘルパーです。PdoWrapper を拡張し、insert()update()delete()、およびトランザクションなどの一般的なデータベース操作のための便利なヘルパーメソッドを追加します。データベースタスクを簡素化し、結果を Collections として返して簡単なアクセスを可能にし、高度なユースケースのためのクエリログとアプリケーションのパフォーマンス監視 (APM) をサポートします。

理解

SimplePdo クラスは、PHP でのデータベース操作をはるかに簡単に設計されています。プリペアドステートメント、フェッチモード、冗長な SQL 操作を扱う代わりに、一般的なタスクのためのクリーンでシンプルなメソッドが得られます。各行は Collection として返されるため、配列表記 ($row['name']) とオブジェクト表記 ($row->name) の両方を使用できます。

このクラスは PdoWrapper のスーパーセットであり、PdoWrapper のすべての機能に加えて、コードをよりクリーンで保守しやすくする追加のヘルパーメソッドを含みます。現在 PdoWrapper を使用している場合、SimplePdo へのアップグレードは PdoWrapper を拡張しているため簡単です。

Flight で SimplePdo を共有サービスとして登録し、Flight::db() を使用してアプリのどこからでも使用できます。

基本的な使用方法

SimplePdo の登録

まず、Flight に SimplePdo クラスを登録します:

Flight::register('db', \flight\database\SimplePdo::class, [
    'mysql:host=localhost;dbname=cool_db_name', 'user', 'pass', [
        PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'utf8mb4\'',
        PDO::ATTR_EMULATE_PREPARES => false,
        PDO::ATTR_STRINGIFY_FETCHES => false,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
    ]
]);

注意

PDO::ATTR_DEFAULT_FETCH_MODE を指定しない場合、SimplePdo は自動的に PDO::FETCH_ASSOC に設定します。

これで、どこからでも Flight::db() を使用してデータベース接続を取得できます。

クエリの実行

runQuery()

function runQuery(string $sql, array $params = []): PDOStatement

INSERT、UPDATE、または結果を手動でフェッチする場合に使用します:

$db = Flight::db();
$statement = $db->runQuery("SELECT * FROM users WHERE status = ?", ['active']);
while ($row = $statement->fetch()) {
    // $row は配列です
}

書き込みにも使用できます:

$db->runQuery("INSERT INTO users (name) VALUES (?)", ['Alice']);
$db->runQuery("UPDATE users SET name = ? WHERE id = ?", ['Bob', 1]);

fetchField()

function fetchField(string $sql, array $params = []): mixed

データベースから単一の値を入手します:

$count = Flight::db()->fetchField("SELECT COUNT(*) FROM users WHERE status = ?", ['active']);

fetchRow()

function fetchRow(string $sql, array $params = []): ?Collection

単一の行を Collection (配列/オブジェクトアクセス) として入手します:

$user = Flight::db()->fetchRow("SELECT * FROM users WHERE id = ?", [123]);
echo $user['name'];
// または
echo $user->name;

ヒント

SimplePdo は、fetchRow() クエリにすでに存在しない場合に自動的に LIMIT 1 を追加し、クエリをより効率的にします。

fetchAll()

function fetchAll(string $sql, array $params = []): array<Collection>

すべての行を Collections の配列として入手します:

$users = Flight::db()->fetchAll("SELECT * FROM users WHERE status = ?", ['active']);
foreach ($users as $user) {
    echo $user['name'];
    // または
    echo $user->name;
}

fetchColumn()

function fetchColumn(string $sql, array $params = []): array

単一の列を配列としてフェッチします:

$ids = Flight::db()->fetchColumn("SELECT id FROM users WHERE active = ?", [1]);
// 戻り値: [1, 2, 3, 4, 5]

fetchPairs()

function fetchPairs(string $sql, array $params = []): array

結果をキー-バリューペア (最初の列をキー、2番目の列を値) としてフェッチします:

$userNames = Flight::db()->fetchPairs("SELECT id, name FROM users");
// 戻り値: [1 => 'John', 2 => 'Jane', 3 => 'Bob']

IN() プレースホルダーの使用

IN() 句で単一の ? を使用し、配列を渡すことができます:

$ids = [1, 2, 3];
$users = Flight::db()->fetchAll("SELECT * FROM users WHERE id IN (?)", [$ids]);

ヘルパーメソッド

SimplePdoPdoWrapper に対する主な利点の1つは、一般的なデータベース操作のための便利なヘルパーメソッドの追加です。

insert()

function insert(string $table, array $data): string

1つまたは複数の行を挿入し、最後の挿入 ID を返します。

単一の挿入:

$id = Flight::db()->insert('users', [
    'name' => 'John',
    'email' => 'john@example.com'
]);

一括挿入:

$id = Flight::db()->insert('users', [
    ['name' => 'John', 'email' => 'john@example.com'],
    ['name' => 'Jane', 'email' => 'jane@example.com'],
]);

update()

function update(string $table, array $data, string $where, array $whereParams = []): int

行を更新し、影響を受けた行の数を返します:

$affected = Flight::db()->update(
    'users',
    ['name' => 'Jane', 'email' => 'jane@example.com'],
    'id = ?',
    [1]
);

注意

SQLite の rowCount() は、データが実際に変更された行の数を返します。行を既存の値と同じ値で更新した場合、rowCount() は 0 を返します。これは、PDO::MYSQL_ATTR_FOUND_ROWS を使用した MySQL の動作とは異なります。

delete()

function delete(string $table, string $where, array $whereParams = []): int

行を削除し、削除された行の数を返します:

$deleted = Flight::db()->delete('users', 'id = ?', [1]);

transaction()

function transaction(callable $callback): mixed

トランザクション内でコールバックを実行します。トランザクションは成功時に自動的にコミットされ、エラー時にロールバックされます:

$result = Flight::db()->transaction(function($db) {
    $db->insert('users', ['name' => 'John']);
    $db->insert('logs', ['action' => 'user_created']);
    return $db->lastInsertId();
});

コールバック内で例外が発生した場合、トランザクションは自動的にロールバックされ、例外が再スローされます。

高度な使用方法

クエリログと APM

クエリのパフォーマンスを追跡したい場合、登録時に APM 追跡を有効にします:

Flight::register('db', \flight\database\SimplePdo::class, [
    'mysql:host=localhost;dbname=cool_db_name',
    'user',
    'pass',
    [/* PDO options */],
    [
        'trackApmQueries' => true,
        'maxQueryMetrics' => 1000
    ]
]);

クエリを実行した後、手動でログを記録できますが、有効にされている場合 APM は自動的にログを記録します:

Flight::db()->logQueries();

これにより、接続とクエリメトリクスを含むイベント (flight.db.queries) がトリガーされ、Flight のイベントシステムを使用してリッスンできます。

完全な例

Flight::route('/users', function () {
    // すべてのユーザーを取得
    $users = Flight::db()->fetchAll('SELECT * FROM users');

    // すべてのユーザーをストリーム
    $statement = Flight::db()->runQuery('SELECT * FROM users');
    while ($user = $statement->fetch()) {
        echo $user['name'];
    }

    // 単一のユーザーを取得
    $user = Flight::db()->fetchRow('SELECT * FROM users WHERE id = ?', [123]);

    // 単一の値を入手
    $count = Flight::db()->fetchField('SELECT COUNT(*) FROM users');

    // 単一の列を取得
    $ids = Flight::db()->fetchColumn('SELECT id FROM users');

    // キー-バリューペアを取得
    $userNames = Flight::db()->fetchPairs('SELECT id, name FROM users');

    // 特別な IN() 構文
    $users = Flight::db()->fetchAll('SELECT * FROM users WHERE id IN (?)', [[1,2,3,4,5]]);

    // 新しいユーザーを挿入
    $id = Flight::db()->insert('users', [
        'name' => 'Bob',
        'email' => 'bob@example.com'
    ]);

    // ユーザーを一括挿入
    Flight::db()->insert('users', [
        ['name' => 'Bob', 'email' => 'bob@example.com'],
        ['name' => 'Jane', 'email' => 'jane@example.com']
    ]);

    // ユーザーを更新
    $affected = Flight::db()->update('users', ['name' => 'Bob'], 'id = ?', [123]);

    // ユーザーを削除
    $deleted = Flight::db()->delete('users', 'id = ?', [123]);

    // トランザクションを使用
    $result = Flight::db()->transaction(function($db) {
        $db->insert('users', ['name' => 'John', 'email' => 'john@example.com']);
        $db->insert('audit_log', ['action' => 'user_created']);
        return $db->lastInsertId();
    });
});

PdoWrapper からの移行

現在 PdoWrapper を使用している場合、SimplePdo への移行は簡単です:

  1. 登録を更新:

    // 旧
    Flight::register('db', \flight\database\PdoWrapper::class, [ /* ... */ ]);
    
    // 新
    Flight::register('db', \flight\database\SimplePdo::class, [ /* ... */ ]);
  2. すべての既存の PdoWrapper メソッドが SimplePdo で動作 - 破壊的な変更はありません。既存のコードは引き続き動作します。

  3. 新しいヘルパーメソッドをオプションで使用 - insert()update()delete()transaction() を使用してコードを簡素化します。

関連項目

トラブルシューティング

変更履歴

Learn/collections

コレクション

概要

FlightのCollectionクラスは、データセットを管理するための便利なユーティリティです。配列表記とオブジェクト表記の両方でデータにアクセス・操作できるため、コードがよりクリーンで柔軟になります。

理解

Collectionは基本的に配列のラッパーですが、いくつかの追加機能があります。配列のように使用したり、ループしたり、アイテム数を数えたり、アイテムをオブジェクトプロパティのようにアクセスしたりできます。これは、アプリ内で構造化データを渡したい場合や、コードを少し読みやすくしたい場合に特に便利です。

コレクションはいくつかのPHPインターフェースを実装しています:

基本的な使い方

コレクションの作成

コンストラクタに配列を渡すだけで、コレクションを作成できます:

use flight\util\Collection;

$data = [
  'name' => 'Flight',
  'version' => 3,
  'features' => ['routing', 'views', 'extending']
];

$collection = new Collection($data);

アイテムへのアクセス

配列表記またはオブジェクト表記のどちらでもアイテムにアクセスできます:

// 配列表記
echo $collection['name']; // 出力: FlightPHP

// オブジェクト表記
echo $collection->version; // 出力: 3

存在しないキーにアクセスしようとすると、エラーではなくnullが返ります。

アイテムの設定

アイテムの設定も、どちらの表記でも行えます:

// 配列表記
$collection['author'] = 'Mike Cao';

// オブジェクト表記
$collection->license = 'MIT';

アイテムの確認と削除

アイテムが存在するか確認する:

if (isset($collection['name'])) {
  // 何かを行う
}

if (isset($collection->version)) {
  // 何かを行う
}

アイテムを削除する:

unset($collection['author']);
unset($collection->license);

コレクションの反復処理

コレクションは反復可能なので、foreachループで使用できます:

foreach ($collection as $key => $value) {
  echo "$key: $value\n";
}

アイテム数のカウント

コレクション内のアイテム数を数えることができます:

echo count($collection); // 出力: 4

すべてのキーまたはデータを取得

すべてのキーを取得:

$keys = $collection->keys(); // ['name', 'version', 'features', 'license']

すべてのデータを配列として取得:

$data = $collection->getData();

コレクションのクリア

すべてのアイテムを削除:

$collection->clear();

JSONシリアライズ

コレクションは簡単にJSONに変換できます:

echo json_encode($collection);
// 出力: {"name":"FlightPHP","version":3,"features":["routing","views","extending"],"license":"MIT"}

高度な使い方

必要に応じて、内部のデータ配列を完全に置き換えることができます:

$collection->setData(['foo' => 'bar']);

コレクションは、コンポーネント間で構造化データを渡したい場合や、配列データに対してよりオブジェクト指向のインターフェースを提供したい場合に特に便利です。

関連項目

トラブルシューティング

変更履歴

Learn/flight_vs_fat_free

FlightとFat-Freeの比較

Fat-Freeとは?

Fat-Free(通称 F3)は、動的で堅牢なWebアプリケーションを迅速に構築するために設計された、強力でありながら使いやすいPHPマイクロフレームワークです。

Flightは、多くの点でFat-Freeと比較でき、おそらく機能とシンプルさの面で最も近い親戚です。Fat-FreeにはFlightにはない機能が多くありますが、Flightにある機能も多く持っています。Fat-Freeは時代を感じさせ始めており、かつてほど人気はありません。

アップデートの頻度は減り、コミュニティも以前ほど活発ではありません。コード自体はシンプルですが、構文の規律に欠けるため、読み解くのが難しいこともあります。PHP 8.3では動作しますが、コード自体はまだPHP 5.3時代のものに見えます。

Flightと比較した場合の利点

Flightと比較した場合の欠点

Learn/extending

拡張

概要

Flight は拡張可能なフレームワークとして設計されています。フレームワークにはデフォルトのメソッドとコンポーネントのセットが付属していますが、ご自身のメソッドをマップしたり、ご自身のクラスを登録したり、既存のクラスやメソッドをオーバーライドしたりすることが可能です。

理解

Flight の機能を拡張する方法は 2 つあります:

  1. メソッドのマッピング - アプリケーション内のどこからでも呼び出せるシンプルなカスタムメソッドを作成するために使用されます。これらは、コード内のどこからでも呼び出したいユーティリティ関数に通常使用されます。
  2. クラスの登録 - Flight にご自身のクラスを登録するために使用されます。これは、依存関係があるクラスや設定を必要とするクラスに通常使用されます。

プロジェクトのニーズに合わせてデフォルトの動作を変更するために、既存のフレームワークメソッドをオーバーライドすることも可能です。

DIC(Dependency Injection Container)をお探しの場合、Dependency Injection Container ページに移動してください。

基本的な使用方法

フレームワークメソッドのオーバーライド

Flight は、コードを変更せずにご自身のニーズに合わせてデフォルトの機能をオーバーライドすることを許可します。オーバーライド可能なすべてのメソッドは 以下 をご覧ください。

たとえば、Flight が URL をルートにマッチングできない場合、notFound メソッドを呼び出して一般的な HTTP 404 レスポンスを送信します。この動作を map メソッドを使用してオーバーライドできます:

Flight::map('notFound', function() {
  // カスタム 404 ページを表示
  include 'errors/404.html';
});

Flight はフレームワークのコアコンポーネントを置き換えることも許可します。 たとえば、デフォルトの Router クラスを独自のカスタムクラスに置き換えることができます:

// カスタム Router クラスを作成
class MyRouter extends \flight\net\Router {
    // ここでメソッドをオーバーライド
    // たとえば、GET リクエストのショートカットで
    // pass route 機能を削除
    public function get($pattern, $callback, $alias = '') {
        return parent::get($pattern, $callback, false, $alias);
    }
}

// カスタムクラスを登録
Flight::register('router', MyRouter::class);

// Flight が Router インスタンスをロードするとき、ご自身のクラスがロードされます
$myRouter = Flight::router();
$myRouter->get('/hello', function() {
  echo "Hello World!";
}, 'hello_alias');

ただし、mapregister などのフレームワークメソッドはオーバーライドできません。これを試みるとエラーが発生します(リストについては 以下 をご覧ください)。

マッピング可能なフレームワークメソッド

以下はフレームワークの完全なメソッドセットです。コアメソッド(通常の静的メソッド)と拡張可能メソッド(フィルタリングやオーバーライドが可能なマップされたメソッド)で構成されています。

コアメソッド

これらのメソッドはフレームワークのコアであり、オーバーライドできません。

Flight::map(string $name, callable $callback, bool $pass_route = false) // カスタムフレームワークメソッドを作成。
Flight::register(string $name, string $class, array $params = [], ?callable $callback = null) // フレームワークメソッドにクラスを登録。
Flight::unregister(string $name) // フレームワークメソッドからクラスを登録解除。
Flight::before(string $name, callable $callback) // フレームワークメソッドの前にフィルタを追加。
Flight::after(string $name, callable $callback) // フレームワークメソッドの後にフィルタを追加。
Flight::path(string $path) // クラスの自動ロードのためのパスを追加。
Flight::get(string $key) // Flight::set() で設定された変数を取得。
Flight::set(string $key, mixed $value) // Flight エンジン内で変数を設定。
Flight::has(string $key) // 変数が設定されているかをチェック。
Flight::clear(array|string $key = []) // 変数をクリア。
Flight::init() // フレームワークをデフォルト設定に初期化。
Flight::app() // アプリケーションオブジェクトインスタンスを取得
Flight::request() // リクエストオブジェクトインスタンスを取得
Flight::response() // レスポンスオブジェクトインスタンスを取得
Flight::router() // ルーターオブジェクトインスタンスを取得
Flight::view() // ビューオブジェクトインスタンスを取得

拡張可能メソッド

Flight::start() // フレームワークを開始。
Flight::stop() // フレームワークを停止し、レスポンスを送信。
Flight::halt(int $code = 200, string $message = '') // オプションのステータスコードとメッセージでフレームワークを停止。
Flight::route(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') // URL パターンをコールバックにマップ。
Flight::post(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') // POST リクエスト URL パターンをコールバックにマップ。
Flight::put(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') // PUT リクエスト URL パターンをコールバックにマップ。
Flight::patch(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') // PATCH リクエスト URL パターンをコールバックにマップ。
Flight::delete(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') // DELETE リクエスト URL パターンをコールバックにマップ。
Flight::group(string $pattern, callable $callback) // URL のグループを作成、パターンは文字列である必要があります。
Flight::getUrl(string $name, array $params = []) // ルートエイリアスに基づいて URL を生成。
Flight::redirect(string $url, int $code) // 別の URL にリダイレクト。
Flight::download(string $filePath) // ファイルをダウンロード。
Flight::render(string $file, array $data, ?string $key = null) // テンプレートファイルをレンダリング。
Flight::error(Throwable $error) // HTTP 500 レスポンスを送信。
Flight::notFound() // HTTP 404 レスポンスを送信。
Flight::etag(string $id, string $type = 'string') // ETag HTTP キャッシングを実行。
Flight::lastModified(int $time) // 最終更新日時 HTTP キャッシングを実行。
Flight::json(mixed $data, int $code = 200, bool $encode = true, string $charset = 'utf8', int $option) // JSON レスポンスを送信。
Flight::jsonp(mixed $data, string $param = 'jsonp', int $code = 200, bool $encode = true, string $charset = 'utf8', int $option) // JSONP レスポンスを送信。
Flight::jsonHalt(mixed $data, int $code = 200, bool $encode = true, string $charset = 'utf8', int $option) // JSON レスポンスを送信し、フレームワークを停止。
Flight::onEvent(string $event, callable $callback) // イベントリスナーを登録。
Flight::triggerEvent(string $event, ...$args) // イベントをトリガー。

mapregister で追加したカスタムメソッドもフィルタリング可能です。これらのメソッドをフィルタリングする方法の例については、Filtering Methods ガイドを参照してください。

拡張可能なフレームワーククラス

拡張してご自身のクラスを登録することで、機能のオーバーライドが可能なクラスがいくつかあります。これらのクラスは:

Flight::app() // アプリケーションクラス - flight\Engine クラスを拡張
Flight::request() // リクエストクラス - flight\net\Request クラスを拡張
Flight::response() // レスポンスクラス - flight\net\Response クラスを拡張
Flight::router() // ルータークラス - flight\net\Router クラスを拡張
Flight::view() // ビュークラス - flight\template\View クラスを拡張
Flight::eventDispatcher() // イベントディスパッチャークラス - flight\core\Dispatcher クラスを拡張

カスタムメソッドのマッピング

シンプルなカスタムメソッドをマップするには、map 関数を使用します:

// メソッドをマップ
Flight::map('hello', function (string $name) {
  echo "hello $name!";
});

// カスタムメソッドを呼び出し
Flight::hello('Bob');

シンプルなカスタムメソッドを作成することは可能ですが、PHP で標準関数を作成することを推奨します。これにより IDE でオートコンプリートが可能で、読みやすくなります。上記のコードの同等例は:

function hello(string $name) {
  echo "hello $name!";
}

hello('Bob');

これは、メソッドに変数を渡して期待される値を取得する必要がある場合に使用されます。以下のように register() メソッドを使用するのは、設定を渡して事前設定されたクラスを呼び出す場合に適しています。

カスタムクラスの登録

ご自身のクラスを登録して設定するには、register 関数を使用します。map() よりも利点は、この関数を呼び出すたびに同じクラスを再利用できることです(Flight::db() で同じインスタンスを共有するのに役立ちます)。

// クラスを登録
Flight::register('user', User::class);

// クラスのインスタンスを取得
$user = Flight::user();

register メソッドは、クラスコンストラクタにパラメータを渡すことも許可します。カスタムクラスをロードすると、事前初期化された状態になります。コンストラクタパラメータは追加の配列を渡すことで定義できます。データベース接続をロードする例:

// コンストラクタパラメータでクラスを登録
Flight::register('db', PDO::class, ['mysql:host=localhost;dbname=test', 'user', 'pass']);

// クラスのインスタンスを取得
// これは定義されたパラメータでオブジェクトを作成します
//
// new PDO('mysql:host=localhost;dbname=test','user','pass');
//
$db = Flight::db();

// コードの後で必要になった場合、同じメソッドを再度呼び出すだけです
class SomeController {
  public function __construct() {
    $this->db = Flight::db();
  }
}

追加のコールバックパラメータを渡すと、クラス構築直後に実行されます。これにより、新しいオブジェクトのセットアップ手順を実行できます。コールバック関数は、新しいオブジェクトのインスタンスを 1 つのパラメータとして受け取ります。

// コールバックは構築されたオブジェクトが渡されます
Flight::register(
  'db',
  PDO::class,
  ['mysql:host=localhost;dbname=test', 'user', 'pass'],
  function (PDO $db) {
    $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
  }
);

デフォルトでは、クラスをロードするたびに共有インスタンスが取得されます。クラスの新しいインスタンスを取得するには、単に false をパラメータとして渡します:

// クラスの共有インスタンス
$shared = Flight::db();

// クラスの新しいインスタンス
$new = Flight::db(false);

注意: マップされたメソッドは登録されたクラスよりも優先されます。同じ名前で両方を宣言した場合、マップされたメソッドのみが呼び出されます。

コアに組み込まれていない機能で Flight を拡張する方法の例をいくつか示します。

ログ

Flight には組み込みのログシステムはありませんが、Flight でログライブラリを使用するのは非常に簡単です。Monolog ライブラリを使用した例:

// services.php

// Flight にロガーを登録
Flight::register('log', Monolog\Logger::class, [ 'name' ], function(Monolog\Logger $log) {
    $log->pushHandler(new Monolog\Handler\StreamHandler('path/to/your.log', Monolog\Logger::WARNING));
});

登録したら、アプリケーションで使用できます:

// コントローラーやルート内で
Flight::log()->warning('This is a warning message');

これにより、指定したログファイルにメッセージがログされます。エラーが発生したときに何かをログしたい場合、error メソッドを使用できます:

// コントローラーやルート内で
Flight::map('error', function(Throwable $ex) {
    Flight::log()->error($ex->getMessage());
    // カスタムエラーページを表示
    include 'errors/500.html';
});

beforeafter メソッドを使用して基本的な APM(Application Performance Monitoring)システムを作成することもできます:

// services.php ファイル内で

Flight::before('start', function() {
    Flight::set('start_time', microtime(true));
});

Flight::after('start', function() {
    $end = microtime(true);
    $start = Flight::get('start_time');
    Flight::log()->info('Request '.Flight::request()->url.' took ' . round($end - $start, 4) . ' seconds');

    // リクエストやレスポンスヘッダーを追加してログすることも可能
    // (リクエストが多い場合、データ量が多いので注意)
    Flight::log()->info('Request Headers: ' . json_encode(Flight::request()->headers));
    Flight::log()->info('Response Headers: ' . json_encode(Flight::response()->headers));
});

キャッシング

Flight には組み込みのキャッシングシステムはありませんが、Flight でキャッシングライブラリを使用するのは非常に簡単です。PHP File Cache ライブラリを使用した例:

// services.php

// Flight にキャッシュを登録
Flight::register('cache', \flight\Cache::class, [ __DIR__ . '/../cache/' ], function(\flight\Cache $cache) {
    $cache->setDevMode(ENVIRONMENT === 'development');
});

登録したら、アプリケーションで使用できます:

// コントローラーやルート内で
$data = Flight::cache()->get('my_cache_key');
if (empty($data)) {
    // データ取得のための処理を実行
    $data = [ 'some' => 'data' ];
    Flight::cache()->set('my_cache_key', $data, 3600); // 1 時間キャッシュ
}

簡単な DIC オブジェクトインスタンス化

アプリケーションで DIC(Dependency Injection Container)を使用している場合、Flight を使用してオブジェクトをインスタンス化できます。Dice ライブラリを使用した例:

// services.php

// 新しいコンテナを作成
$container = new \Dice\Dice;
// 以下のように自身に再割り当てすることを忘れずに!
$container = $container->addRule('PDO', [
    // shared は同じオブジェクトが毎回返されることを意味
    'shared' => true,
    'constructParams' => ['mysql:host=localhost;dbname=test', 'user', 'pass' ]
]);

// 任意のオブジェクトを作成するためのマッピング可能メソッドを作成
Flight::map('make', function($class, $params = []) use ($container) {
    return $container->create($class, $params);
});

// これはコントローラー/ミドルウェアで使用するためのコンテナハンドラを登録
Flight::registerContainerHandler(function($class, $params) {
    Flight::make($class, $params);
});


// コンストラクタで PDO オブジェクトを受け取るサンプルクラスがあると仮定
class EmailCron {
    protected PDO $pdo;

    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
    }

    public function send() {
        // メール送信コード
    }
}

// 最後に、依存注入を使用してオブジェクトを作成
$emailCron = Flight::make(EmailCron::class);
$emailCron->send();

かっこいいでしょう?

関連項目

トラブルシューティング

変更履歴

Learn/json

JSON Wrapper

Overview

FlightのJsonクラスは、アプリケーションでJSONデータをエンコードおよびデコードするためのシンプルで一貫した方法を提供します。PHPのネイティブJSON関数をより良いエラー処理と便利なデフォルト値でラップしており、JSONの使用をより簡単で安全にします。

Understanding

JSONの使用は、現代のPHPアプリケーションで非常に一般的です。特にAPIの構築やAJAXリクエストの処理時にそうです。Jsonクラスは、すべてのJSONエンコードとデコードを一元化するため、PHPの組み込み関数からの奇妙なエッジケースや暗号めいたエラーについて心配する必要がありません。

主な機能:

Basic Usage

データのJSONエンコード

PHPデータをJSON文字列に変換するには、Json::encode()を使用します:

use flight\util\Json;

$data = [
  'framework' => 'Flight',
  'version' => 3,
  'features' => ['routing', 'views', 'extending']
];

$json = Json::encode($data);
echo $json;
// Output: {"framework":"Flight","version":3,"features":["routing","views","extending"]}

エンコードが失敗した場合、役立つエラーメッセージ付きの例外が発生します。

プリティプリント

JSONを人間が読みやすい形式にしたいですか? prettyPrint()を使用します:

echo Json::prettyPrint($data);
/*
{
  "framework": "Flight",
  "version": 3,
  "features": [
    "routing",
    "views",
    "extending"
  ]
}
*/

JSON文字列のデコード

JSON文字列をPHPデータに戻すには、Json::decode()を使用します:

$json = '{"framework":"Flight","version":3}';
$data = Json::decode($json);
echo $data->framework; // Output: Flight

オブジェクトではなく連想配列が欲しい場合、2番目の引数にtrueを渡します:

$data = Json::decode($json, true);
echo $data['framework']; // Output: Flight

デコードが失敗した場合、明確なエラーメッセージ付きの例外が発生します。

JSONの検証

文字列が有効なJSONかどうかをチェックします:

if (Json::isValid($json)) {
  // 有効です!
} else {
  // 有効なJSONではありません
}

最後のエラーの取得

ネイティブPHP関数からの最後のJSONエラーメッセージを確認したい場合:

$error = Json::getLastError();
if ($error !== '') {
  echo "Last JSON error: $error";
}

Advanced Usage

より多くの制御が必要な場合、エンコードとデコードのオプションをカスタマイズできます(PHPのjson_encodeオプションを参照):

// HEX_TAGオプションでエンコード
$json = Json::encode($data, JSON_HEX_TAG);

// カスタム深さでデコード
$data = Json::decode($json, false, 1024);

See Also

Troubleshooting

Changelog

Learn/flight_vs_slim

Flight vs Slim

Slimとは?

Slim は、シンプルでありながら強力なWebアプリケーションやAPIをすばやく作成するのに役立つPHPマイクロフレームワークです。

Flight の v3 機能の多くは、実際には Slim から着想を得ています。ルートのグループ化と、特定の順序でミドルウェアを実行することは、Slim に触発された2つの機能です。Slim v3 はシンプルさを重視して登場しましたが、v4 については賛否両論があります。

Flight と比べた長所

Flight と比べた短所

Learn/autoloading

オートローディング

概要

オートローディングとは、PHPにおいてクラスを読み込むディレクトリを指定する概念です。requireinclude を使ってクラスを読み込むよりもはるかに有益です。また、Composerパッケージを使用するための要件でもあります。

オートローディングを正しく設定することは、AI支援開発 にとっても重要です。エージェントは名前空間が示す場所にファイルを配置するからです。フォルダの大文字小文字と名前空間の大文字小文字が一致しない場合、大文字小文字を区別しないMacのディスク上では「動いていた」としても、Linuxではクラスが見つからないエラーが発生します。

理解

デフォルトでは、あらゆる Flight クラスはComposerのおかげで自動的にオートロードされます。あなた自身のアプリケーションクラスについては、一般的な2つの方法があります。

  1. Composer PSR-4公式スケルトン が使用): composer.json で名前空間のプレフィックスをディレクトリにマッピングし、composer dump-autoload を実行します。
  2. Flight::path(): Flightのローダーにディレクトリを指定します(シンプルなアプリや、アプリコードにComposerを使わない場合に便利です)。

オートローダーを使うとコードが大幅に簡素化されます。毎回ファイルの先頭に大量の include / require を並べる代わりに、クラスを最初に使用したときに読み込まれます。

大文字小文字の区別(2回読んでください)

名前空間はディレクトリ構造と、そのディレクトリの大文字小文字の両方に一致している必要があります。

動作する Linuxで壊れる
App\Controller\HomeControllerapp/Controller/HomeController.php App\Controller\… でフォルダが app/controllers/ の場合
app\controllers\MyControllerapp/controllers/MyController.php App\ と小文字の controllers を混在させる

PHPの名前空間は一部の文脈では大文字小文字を区別しませんが、Composerとファイルシステムは区別します。公式スケルトンは次のように統一しています。

古いドキュメントやコミュニティの例では、小文字の app\controllers が使われることがありました。フォルダが小文字であればそれでも機能します。ただし新しいスケルトンプロジェクトでは App\ + パスカルケースのフォルダを使用します。プロジェクトごとに1つの規約を選んでそれを守り、人間とAIツールが別のレイアウトを作り出さないようにしてください。

スケルトン(新規プロジェクト向け推奨)

composer create-project flightphp/skeleton の後、アプリコードはComposer経由でオートロードされます。App\ クラスに対して Flight::path() は不要です。

{
  "autoload": {
    "psr-4": {
      "App\\": "app/"
    }
  }
}
// app/Controller/HomeController.php
namespace App\Controller;

use flight\Engine;

class HomeController
{
    protected Engine $app;

    public function __construct(Engine $app)
    {
        $this->app = $app;
    }

    public function index(): void
    {
        $this->app->render('welcome', ['message' => 'Hello!']);
    }
}
// app/config/routes.php — Diceがコンテナ経由でApp\Controller\… を解決します
$router->get('/', [HomeController::class, 'index']);

完全なツリーは インストール を、コーディング支援ツール向けにこのレイアウトを AGENTS.md がどう文書化するかは AIと開発者体験 を参照してください。

基本的な使用方法(Flight::path()

次のようなディレクトリツリーがあると仮定します。

# パスの例
/home/user/project/my-flight-project/
├── app
│   ├── cache
│   ├── config
│   ├── controllers - このプロジェクトのコントローラを格納
│   ├── translations
│   ├── UTILS - このアプリケーション専用のクラスを格納(後で例を示すため意図的にすべて大文字)
│   └── views
└── public
    └── css
    └── js
    └── index.php

これは典型的なアプリツリーに似ていることに気付いたかもしれません(ドキュメントサイト自体も構造化レイアウトを使用しています)。ここで小文字の controllers は有効な選択肢です。ただ、スケルトンの現在のデフォルトではありません。

各ディレクトリを次のように指定して読み込むことができます。


/**
 * public/index.php
 */

// オートローダーにパスを追加
Flight::path(__DIR__.'/../app/controllers/');
Flight::path(__DIR__.'/../app/utils/');


/**
 * app/controllers/MyController.php
 */

// 名前空間は不要

// オートロードされるすべてのクラスはパスカルケース(各単語の先頭を大文字にし、スペースを入れない)を推奨
class MyController {

    public function index() {
        // 何かを行う
    }
}

Flight::path() で名前空間を使う

名前空間を使う場合、実装は非常に簡単になります。Flight::path() メソッドで、アプリケーションのルートディレクトリ(ドキュメントルートや public/ フォルダではなく)を指定する必要があります。


/**
 * public/index.php
 */

// オートローダーにパスを追加
Flight::path(__DIR__.'/../');

これで、コントローラは次のようになります。下の例を見てください。ただし、重要な情報はコメントに注目してください。

/**
 * app/controllers/MyController.php
 */

// 名前空間は必須
// 名前空間はディレクトリ構造と同じ
// 名前空間はディレクトリ構造と同じ大文字小文字に従う必要がある
// 名前空間とディレクトリにはアンダースコアを含めることはできない(Loader::setV2ClassLoading(false) が設定されている場合を除く)
namespace app\controllers;

// オートロードされるすべてのクラスはパスカルケース(各単語の先頭を大文字にし、スペースを入れない)を推奨
// 3.7.2以降、Loader::setV2ClassLoading(false); を実行することでクラス名にPascal_Snake_Caseを使用できます
class MyController {

    public function index() {
        // 何かを行う
    }
}

そして、utilsディレクトリ内のクラスをオートロードしたい場合は、基本的に同じことを行います。


/**
 * app/UTILS/ArrayHelperUtil.php
 */

// 名前空間はディレクトリ構造と大文字小文字に一致させる必要がある(上記のファイルツリーのようにUTILSディレクトリがすべて大文字であることに注意)
namespace app\UTILS;

class ArrayHelperUtil {

    public function changeArrayCase(array $array) {
        // 何かを行う
    }
}

スケルトンスタイルの名前空間(同じルール、異なる大文字小文字)

/**
 * app/Controller/MyController.php
 */
namespace App\Controller;

class MyController {
    // ...
}

ルールは変わりません。スケルトンが選んだフォルダ/名前空間の大文字小文字が変わるだけです。フォルダがどの大文字小文字でも、namespace 行はそれに一致させる必要があります。

クラス名のアンダースコア

3.7.2以降、Loader::setV2ClassLoading(false); を実行することでクラス名にPascal_Snake_Caseを使用できます。これにより、クラス名にアンダースコアを使用できるようになります。推奨はされませんが、必要とする人のために利用可能です。

use flight\core\Loader;

/**
 * public/index.php
 */

// オートローダーにパスを追加
Flight::path(__DIR__.'/../app/controllers/');
Flight::path(__DIR__.'/../app/utils/');
Loader::setV2ClassLoading(false);

/**
 * app/controllers/My_Controller.php
 */

// 名前空間は不要

class My_Controller {

    public function index() {
        // 何かを行う
    }
}

関連情報

トラブルシューティング

クラスが見つからない(オートローディングが機能していない)

これが発生する理由はいくつか考えられます。以下に例を示します。

ファイル名の誤り

最も一般的なのは、クラス名がファイル名と一致しないことです。

MyClass というクラスがある場合、ファイル名は MyClass.php でなければなりません。MyClass というクラスなのにファイル名が myclass.php の場合、オートローダーはそれを見つけることができません。

名前空間またはフォルダの大文字小文字の誤り

名前空間を使用している場合、名前空間はディレクトリ構造に大文字小文字も含めて一致する必要があります。

// ...コード...

// MyControllerが app/Controller(スケルトン)にあり、名前空間が App\Controller の場合
// これは機能しません:
Flight::route('/hello', 'MyController->hello');

// スケルトンスタイル:
use App\Controller\MyController;
Flight::route('/hello', [ MyController::class, 'hello' ]);

// 古い小文字レイアウト(フォルダが実際に app/controllers の場合のみ):
use app\controllers\MyController;
Flight::route('/hello', [ MyController::class, 'hello' ]);
// または完全修飾名:
Flight::route('/hello', [ 'App\Controller\MyController', 'hello' ]);

path() が定義されていない(Composer以外のアプリコード)

アプリケーションクラスにComposerではなく Flight::path() を頼る場合、それらのクラスを参照するルートの前にパスを定義してください(多くの場合、ブートストラップの早い段階または public/index.php 内)。

// オートローダーにパスを追加(名前空間付きアプリの場合はプロジェクトルート)
Flight::path(__DIR__.'/../');

公式スケルトンは主に App\ に対してComposer PSR-4を使用するため、通常コントローラやモデルに Flight::path() は必要ありません。

変更履歴

Learn/uploaded_file

アップロードされたファイルハンドラー

概要

Flight の UploadedFile クラスは、アプリケーションでファイルのアップロードを簡単かつ安全に扱うことを可能にします。PHP のファイルアップロードプロセスの詳細をラップし、ファイル情報をアクセスし、アップロードされたファイルを移動するためのシンプルでオブジェクト指向の方法を提供します。

理解

ユーザーがフォーム経由でファイルをアップロードすると、PHP は $_FILES スーパーグローバルにファイルに関する情報を格納します。Flight では、$_FILES に直接アクセスすることはほとんどありません。代わりに、Flight の Request オブジェクト(Flight::request() 経由でアクセス可能)が getUploadedFiles() メソッドを提供し、UploadedFile オブジェクトの配列を返します。これにより、ファイルの扱いがはるかに便利で堅牢になります。

UploadedFile クラスは以下のメソッドを提供します:

このクラスは、ファイルアップロードの一般的な落とし穴(エラーの扱いやファイルの安全な移動など)を避けるのに役立ちます。

基本的な使用方法

リクエストからアップロードされたファイルにアクセスする

アップロードされたファイルにアクセスする推奨される方法は、リクエストオブジェクト経由です:

Flight::route('POST /upload', function() {
    // <input type="file" name="myFile"> という名前のフォームフィールドの場合
    $uploadedFiles = Flight::request()->getUploadedFiles();
    $file = $uploadedFiles['myFile'];

    // これで UploadedFile メソッドを使用できます
    if ($file->getError() === UPLOAD_ERR_OK) {
        $file->moveTo('/path/to/uploads/' . $file->getClientFilename());
        echo "File uploaded successfully!";
    } else {
        echo "Upload failed: " . $file->getError();
    }
});

複数のファイルアップロードの扱い

フォームが name="myFiles[]" を使用して複数のアップロードを行う場合、UploadedFile オブジェクトの配列が得られます:

Flight::route('POST /upload', function() {
    // <input type="file" name="myFiles[]"> という名前のフォームフィールドの場合
    $uploadedFiles = Flight::request()->getUploadedFiles();
    foreach ($uploadedFiles['myFiles'] as $file) {
        if ($file->getError() === UPLOAD_ERR_OK) {
            $file->moveTo('/path/to/uploads/' . $file->getClientFilename());
            echo "Uploaded: " . $file->getClientFilename() . "<br>";
        } else {
            echo "Failed to upload: " . $file->getClientFilename() . "<br>";
        }
    }
});

UploadedFile インスタンスを手動で作成する

通常、UploadedFile を手動で作成することはありませんが、必要に応じて作成できます:

use flight\net\UploadedFile;

$file = new UploadedFile(
  $_FILES['myfile']['name'],
  $_FILES['myfile']['type'],
  $_FILES['myfile']['size'],
  $_FILES['myfile']['tmp_name'],
  $_FILES['myfile']['error']
);

ファイル情報のアクセス

アップロードされたファイルの詳細を簡単に取得できます:

echo $file->getClientFilename();   // ユーザーのコンピューターからのオリジナルのファイル名
echo $file->getClientMediaType();  // MIME タイプ(例: image/png)
echo $file->getSize();             // バイト単位のファイルサイズ
echo $file->getTempName();         // サーバー上のテンポラリファイルパス
echo $file->getError();            // アップロードエラーコード(0 はエラーなし)

アップロードされたファイルの移動

ファイルを検証した後、永続的な場所に移動します:

try {
  $file->moveTo('/path/to/uploads/' . $file->getClientFilename());
  echo "File uploaded successfully!";
} catch (Exception $e) {
  echo "Upload failed: " . $e->getMessage();
}

moveTo() メソッドは、何かがうまくいかない場合(アップロードエラーや権限の問題など)に例外をスローします。

アップロードエラーの扱い

アップロード中に問題が発生した場合、人間が読めるエラーメッセージを取得できます:

if ($file->getError() !== UPLOAD_ERR_OK) {
  // エラーコードを使用するか、moveTo() からの例外をキャッチできます
  echo "There was an error uploading the file.";
}

関連項目

トラブルシューティング

変更履歴

Guides/unit_testing

Flight PHP と PHPUnit によるユニットテスト

このガイドでは、PHPUnit を使った Flight PHP のユニットテストを紹介します。なぜユニットテストが重要なのか、そしてそれを実際にどう適用するのかを理解したい初心者を対象としています。単純な計算ではなく、メールの送信やレコードの保存など、アプリケーションが期待どおりに動作するかという動作のテストに焦点を当てます。まずは単純なルートハンドラから始めて、依存性注入(DI)とサードパーティサービスのモックを組み込んだ、より複雑なコントローラへと進みます。

なぜユニットテストを行うのか?

ユニットテストは、コードが期待どおりに動作することを保証し、本番環境に到達する前にバグを発見します。特に Flight では、軽量なルーティングと柔軟性により複雑な相互作用が生じる可能性があるため、これは非常に価値があります。個人開発者でもチームでも、ユニットテストはセーフティネットとして機能し、期待される動作を文書化し、後でコードを再訪したときにリグレッションを防ぎます。また、設計の改善にも役立ちます。テストしにくいコードは、クラスが過度に複雑であるか、結合度が高すぎることを示していることがよくあります。

単純な例(例:x * y = z のテスト)とは異なり、入力の検証、データの保存、メール送信などのアクションのトリガーといった、実際の世界の動作に焦点を当てます。目標は、テストを身近で有意義なものにすることです。

一般的な指針

  1. 動作をテストし、実装をテストしない: 内部の詳細ではなく、結果(例:「メール送信」「レコード保存」)に焦点を当てます。これにより、リファクタリングに対してテストが堅牢になります。
  2. Flight:: の使用をやめる: Flight の静的メソッドは非常に便利ですが、テストを困難にします。$app = Flight::app(); から取得できる $app 変数を使うことに慣れてください。$app には Flight:: と同じメソッドがすべてあります。コントローラ内などで $app->route()$this->app->json() を引き続き使用できます。また、実際の Flight ルーターを $router = $app->router() で使い、$router->get()$router->post()$router->group() などを使用することもできます。ルーティング を参照してください。
  3. テストを高速に保つ: テストが高速だと頻繁に実行できます。ユニットテストではデータベース呼び出しなどの低速な操作を避けてください。テストが遅い場合は、ユニットテストではなく統合テストを書いている可能性があります。統合テストとは、実際のデータベース、実際の HTTP 呼び出し、実際のメール送信などを行うテストです。それらには役割がありますが、遅く、不明な理由で失敗することがあるため不安定になる可能性があります。
  4. 説明的な名前を使う: テスト名は、テスト対象の動作を明確に説明する必要があります。これにより、可読性と保守性が向上します。
  5. グローバル変数を避ける: $app->set()$app->get() の使用を最小限にしてください。これらはグローバル状態のように機能し、すべてのテストでモックが必要になります。DI または DI コンテナを優先してください(依存性注入コンテナ を参照)。$app->map() メソッドの使用も技術的には「グローバル」であり、DI を優先して避けるべきです。テストでセッションオブジェクトをモックできるように、flightphp/session などのセッションライブラリを使用してください。コード内で直接 $_SESSION を呼び出さないでください。グローバル変数をコードに注入することになり、テストが困難になります。
  6. 依存性注入を使用する: 依存関係(例:PDO、メーラー)をコントローラに注入して、ロジックを分離し、モックを簡単にします。依存関係が多すぎるクラスがある場合は、SOLID 原則に従って、それぞれが単一の責任を持つ小さなクラスにリファクタリングすることを検討してください。
  7. サードパーティサービスをモックする: データベース、HTTP クライアント(cURL)、メールサービスなどをモックして、外部呼び出しを避けます。1〜2層の深さをテストしますが、コアロジックは実行させてください。たとえば、アプリがテキストメッセージを送信する場合、テストを実行するたびに実際にテキストメッセージを送信したくはないはずです(コストがかさみ、遅くなるため)。代わりに、テキストメッセージサービスをモックし、コードが正しいパラメータでテキストメッセージサービスを呼び出したことを検証するだけにします。
  8. 高いカバレッジを目指すが、完璧を求めない: 100% の行カバレッジは良いことですが、コード内のすべてが期待どおりにテストされているとは限りません(PHPUnit での分岐・パスカバレッジ を調べてみてください)。重要な動作(例:ユーザー登録、API レスポンス、失敗したレスポンスの取得)を優先してください。
  9. ルートにはコントローラを使用する: ルート定義では、クロージャではなくコントローラを使用してください。flight\Engine $app はデフォルトでコンストラクタを介してすべてのコントローラに注入されます。テストでは、$app = new Flight\Engine() を使用してテスト内で Flight をインスタンス化し、コントローラに注入して、メソッドを直接呼び出します(例:$controller->register())。Flight の拡張ルーティング を参照してください。
  10. モックのスタイルを選んで一貫させる: PHPUnit はいくつかのモックスタイル(例:prophecy、組み込みモック)をサポートしています。または、コード補完やメソッド定義を変更した場合に壊れるなどの利点がある匿名クラスを使用することもできます。テスト全体で一貫させてください。PHPUnit モックオブジェクト を参照してください。
  11. サブクラスでテストしたいメソッドやプロパティには protected 可視性を使用する: これにより、パブリックにせずにテスト用サブクラスでオーバーライドできます。これは特に匿名クラスモックに役立ちます。

PHPUnit のセットアップ

まず、Composer を使用して Flight PHP プロジェクトに PHPUnit をセットアップします。詳細は PHPUnit 入門ガイド を参照してください。

  1. プロジェクトディレクトリで次のコマンドを実行します:

    composer require --dev phpunit/phpunit

    これにより、最新の PHPUnit が開発依存関係としてインストールされます。

  2. プロジェクトのルートにテストファイル用の tests ディレクトリを作成します。

  3. 利便性のために composer.json にテストスクリプトを追加します:

    // composer.json の他の内容
    "scripts": {
        "test": "phpunit --configuration phpunit.xml"
    }
  4. ルートに phpunit.xml ファイルを作成します:

    <?xml version="1.0" encoding="UTF-8"?>
    <phpunit bootstrap="vendor/autoload.php">
        <testsuites>
            <testsuite name="Flight Tests">
                <directory>tests</directory>
            </testsuite>
        </testsuites>
    </phpunit>

これで、テストが構築されたら、composer test を実行してテストを実行できます。

単純なルートハンドラのテスト

まず、ユーザーのメール入力を受け取る基本的なルートから始めましょう。その動作、つまり有効なメールには成功メッセージを返し、無効なメールにはエラーを返すことをテストします。メール検証には、filter_var を使用します。

// index.php
$app->route('POST /register', [ UserController::class, 'register' ]);

// UserController.php
class UserController {
    protected $app;

    public function __construct(flight\Engine $app) {
        $this->app = $app;
    }

    public function register() {
        $email = $this->app->request()->data->email;
        $responseArray = [];
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            $responseArray = ['status' => 'error', 'message' => 'Invalid email'];
        } else {
            $responseArray = ['status' => 'success', 'message' => 'Valid email'];
        }

        $this->app->json($responseArray);
    }
}

これをテストするには、テストファイルを作成します。テストの構造については、ユニットテストと SOLID 原則 を参照してください:

// tests/UserControllerTest.php
use PHPUnit\Framework\TestCase;
use Flight;
use flight\Engine;

class UserControllerTest extends TestCase {

    public function testValidEmailReturnsSuccess() {
        $app = new Engine();
        $request = $app->request();
        $request->data->email = 'test@example.com'; // POST データをシミュレート
        $UserController = new UserController($app);
        $UserController->register($request->data->email);
        $response = $app->response()->getBody();
        $output = json_decode($response, true);
        $this->assertEquals('success', $output['status']);
        $this->assertEquals('Valid email', $output['message']);
    }

    public function testInvalidEmailReturnsError() {
        $app = new Engine();
        $request = $app->request();
        $request->data->email = 'invalid-email'; // POST データをシミュレート
        $UserController = new UserController($app);
        $UserController->register($request->data->email);
        $response = $app->response()->getBody();
        $output = json_decode($response, true);
        $this->assertEquals('error', $output['status']);
        $this->assertEquals('Invalid email', $output['message']);
    }
}

重要なポイント:

composer test を実行して、ルートが期待どおりに動作することを確認します。Flight のリクエストレスポンスの詳細については、関連ドキュメントを参照してください。

依存性注入を使用したテスト可能なコントローラ

より複雑なシナリオでは、依存性注入(DI)を使用してコントローラをテスト可能にします。Flight のグローバル(例:Flight::set()Flight::map()Flight::register())は、グローバル状態のように機能し、すべてのテストでモックが必要になるため避けてください。代わりに、Flight の DI コンテナ、DICEPHP-DI、または手動 DI を使用してください。

生の PDO の代わりに flight\database\SimplePdo を使用しましょう。このヘルパーはモックやユニットテストがはるかに簡単です(そして非推奨の PdoWrapper よりも推奨されます)。

ユーザーをデータベースに保存し、ウェルカムメールを送信するコントローラは次のとおりです:

use flight\database\SimplePdo;

class UserController {
    protected $app;
    protected $db;
    protected $mailer;

    public function __construct(Engine $app, SimplePdo $db, MailerInterface $mailer) {
        $this->app = $app;
        $this->db = $db;
        $this->mailer = $mailer;
    }

    public function register() {
        $email = $this->app->request()->data->email;
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            // return を置くことで、ユニットテストの実行を停止するのに役立ちます
            return $this->app->jsonHalt(['status' => 'error', 'message' => 'Invalid email']);
        }

        $this->db->runQuery('INSERT INTO users (email) VALUES (?)', [$email]);
        $this->mailer->sendWelcome($email);

        return $this->app->json(['status' => 'success', 'message' => 'User registered']);
    }
}

重要なポイント:

モックを使ったコントローラのテスト

次に、UserController の動作をテストしましょう。メールの検証、データベースへの保存、メールの送信です。コントローラを分離するために、データベースとメーラーをモックします。

// tests/UserControllerDICTest.php
use flight\database\SimplePdo;
use PHPUnit\Framework\TestCase;

class UserControllerDICTest extends TestCase {
    public function testValidEmailSavesAndSendsEmail() {

        // モックスタイルを混在させなければならない場合があります
        // ここでは PHPUnit の組み込みモックを PDOStatement に使用します
        $statementMock = $this->createMock(PDOStatement::class);
        $statementMock->method('execute')->willReturn(true);
        // SimplePdo をモックするための匿名クラスを使用
        $mockDb = new class($statementMock) extends SimplePdo {
            protected $statementMock;
            public function __construct($statementMock) {
                $this->statementMock = $statementMock;
            }

            // この方法でモックすると、実際のデータベース呼び出しは行われません。
            // さらに、これを設定して PDOStatement モックを変更し、障害などをシミュレートすることもできます。
            public function runQuery(string $sql, array $params = []): PDOStatement {
                return $this->statementMock;
            }
        };
        $mockMailer = new class implements MailerInterface {
            public $sentEmail = null;
            public function sendWelcome($email): bool {
                $this->sentEmail = $email;
                return true;    
            }
        };
        $app = new Engine();
        $app->request()->data->email = 'test@example.com';
        $controller = new UserControllerDIC($app, $mockDb, $mockMailer);
        $controller->register();
        $response = $app->response()->getBody();
        $result = json_decode($response, true);
        $this->assertEquals('success', $result['status']);
        $this->assertEquals('User registered', $result['message']);
        $this->assertEquals('test@example.com', $mockMailer->sentEmail);
    }

    public function testInvalidEmailSkipsSaveAndEmail() {
         $mockDb = new class() extends SimplePdo {
            // 空のコンストラクタは親コンストラクタをバイパスします
            public function __construct() {}
            public function runQuery(string $sql, array $params = []): PDOStatement {
                throw new Exception('Should not be called');
            }
        };
        $mockMailer = new class implements MailerInterface {
            public $sentEmail = null;
            public function sendWelcome($email): bool {
                throw new Exception('Should not be called');
            }
        };
        $app = new Engine();
        $app->request()->data->email = 'invalid-email';

        // 終了を避けるために jsonHalt をマップする必要があります
        $app->map('jsonHalt', function($data) use ($app) {
            $app->json($data, 400);
        });
        $controller = new UserControllerDIC($app, $mockDb, $mockMailer);
        $controller->register();
        $response = $app->response()->getBody();
        $result = json_decode($response, true);
        $this->assertEquals('error', $result['status']);
        $this->assertEquals('Invalid email', $result['message']);
    }
}

重要なポイント:

モックしすぎる

コードをモックしすぎないように注意してください。私たちの UserController を使って、これがなぜ良くないのかの例を以下に示します。そのチェックを isEmailValidfilter_var を使用)というメソッドに変更し、他の新しい追加を registerUser という別のメソッドに変更します。

use flight\database\SimplePdo;
use flight\Engine;

// UserControllerDICV2.php
class UserControllerDICV2 {
    protected $app;
    protected $db;
    protected $mailer;

    public function __construct(Engine $app, SimplePdo $db, MailerInterface $mailer) {
        $this->app = $app;
        $this->db = $db;
        $this->mailer = $mailer;
    }

    public function register() {
        $email = $this->app->request()->data->email;
        if (!$this->isEmailValid($email)) {
            // return を置くことで、ユニットテストの実行を停止するのに役立ちます
            return $this->app->jsonHalt(['status' => 'error', 'message' => 'Invalid email']);
        }

        $this->registerUser($email);

        $this->app->json(['status' => 'success', 'message' => 'User registered']);
    }

    protected function isEmailValid($email) {
        return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
    }

    protected function registerUser($email) {
        $this->db->runQuery('INSERT INTO users (email) VALUES (?)', [$email]);
        $this->mailer->sendWelcome($email);
    }
}

そして今度は、実際には何もテストしていない過剰にモックされたユニットテストです:

use PHPUnit\Framework\TestCase;

class UserControllerTest extends TestCase {
    public function testValidEmailSavesAndSendsEmail() {
        $app = new Engine();
        $app->request()->data->email = 'test@example.com';
        // ここでは「簡単」だからという理由で追加の依存性注入を省略しています
        $controller = new class($app) extends UserControllerDICV2 {
            protected $app;
            // コンストラクタの依存関係をバイパス
            public function __construct($app) {
                $this->app = $app;
            }

            // これを強制的に有効にします。
            protected function isEmailValid($email) {
                return true; // 常に true を返し、実際の検証をバイパス
            }

            // 実際の DB とメーラーの呼び出しをバイパス
            protected function registerUser($email) {
                return false;
            }
        };
        $controller->register();
        $response = $app->response()->getBody();
        $result = json_decode($response, true);
        $this->assertEquals('success', $result['status']);
        $this->assertEquals('User registered', $result['message']);
    }
}

やった、ユニットテストがあって、それらはパスしています!でも待ってください。isEmailValidregisterUser の内部動作を実際に変更したらどうなるでしょうか?すべての機能をモックしてしまったので、テストはまだパスします。どういうことかお見せしましょう。

// UserControllerDICV2.php
class UserControllerDICV2 {

    // ... 他のメソッド ...

    protected function isEmailValid($email) {
        // 変更されたロジック
        $validEmail = filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
        // これで特定のドメインのみ許可されるようになりました
        $validDomain = strpos($email, '@example.com') !== false; 
        return $validEmail && $validDomain;
    }
}

上記のユニットテストを実行すると、それでもパスします!しかし、動作をテストしていなかった(コードの一部を実際に実行させていなかった)ため、本番環境で発生するバグをコードに埋め込んだ可能性があります。テストは新しい動作を考慮して修正する必要があり、また動作が期待どおりでない場合の逆のケースも考慮する必要があります。

完全な例

ユニットテストを含む Flight PHP プロジェクトの完全な例は、GitHub にあります: n0nag0n/flight-unit-tests-guide。より深く理解するには、ユニットテストと SOLID 原則 を参照してください。

よくある落とし穴

ユニットテストのスケーリング

ユニットテストは、大規模なプロジェクトや数か月後にコードを再訪する際に力を発揮します。動作を文書化し、リグレッションを検出して、アプリを再学習する手間を省きます。個人開発者の場合は、重要なパス(例:ユーザー登録、支払い処理)をテストしてください。チームの場合は、テストによって貢献全体で一貫した動作が保証されます。フレームワークとテストを使用する利点の詳細については、フレームワークを使う理由は? を参照してください。

Flight PHP ドキュメントリポジトリにあなた自身のテストのヒントを貢献してください!

執筆: n0nag0n 2025

Guides/blog

Flight PHPでシンプルなブログを構築する

このガイドでは、Flight PHPフレームワークを使って基本的なブログを作成する手順を説明します。プロジェクトのセットアップ、ルートの定義、JSONでの投稿管理、Latteテンプレートエンジンでのレンダリングを行い、Flightのシンプルさと柔軟性を示します。最後には、ホームページ、個別投稿ページ、作成フォームを備えた機能的なブログが完成します。

前提条件

ステップ 1: プロジェクトのセットアップ

まず、新しいプロジェクトディレクトリを作成し、Composerを使ってFlightをインストールします。

  1. ディレクトリを作成:

    mkdir flight-blog
    cd flight-blog
  2. Flightをインストール:

    composer require flightphp/core
  3. publicディレクトリを作成: Flightは単一のエントリポイント(index.php)を使用します。そのためのpublic/フォルダを作成します:

    mkdir public
  4. 基本的なindex.php: public/index.phpにシンプルな「Hello World」ルートを作成します:

    <?php
    require '../vendor/autoload.php';
    
    Flight::route('/', function () {
        echo 'Hello, Flight!';
    });
    
    Flight::start();
  5. ビルトインサーバーを実行: PHPの開発サーバーでセットアップをテストします:

    php -S localhost:8000 -t public/

    http://localhost:8000にアクセスして「Hello, Flight!」が表示されることを確認します。

ステップ 2: プロジェクト構造を整理する

クリーンなセットアップのために、プロジェクトを次のように構成します:

flight-blog/
├── app/
│   ├── config/
│   └── views/
├── data/
├── public/
│   └── index.php
├── vendor/
└── composer.json

ステップ 3: Latteのインストールと設定

Latteは、Flightとよく統合する軽量なテンプレートエンジンです。

  1. Latteをインストール:

    composer require latte/latte
  2. FlightでLatteを設定: public/index.phpを更新して、Latteをビューエンジンとして登録します:

    <?php
    require '../vendor/autoload.php';
    
    use Latte\Engine;
    
    Flight::register('view', Engine::class, [], function ($latte) {
        $latte->setTempDirectory(__DIR__ . '/../cache/');
        $latte->setLoader(new \Latte\Loaders\FileLoader(__DIR__ . '/../app/views/'));
    });
    
    Flight::route('/', function () {
        Flight::view()->render('home.latte', ['title' => 'My Blog']);
    });
    
    Flight::start();
  3. レイアウトテンプレートを作成: app/views/layout.latte:

    <!DOCTYPE html>
    <html>
    <head>
     <title>{$title}</title>
    </head>
    <body>
     <header>
         <h1>My Blog</h1>
         <nav>
             <a href="/">Home</a> | 
             <a href="/create">Create a Post</a>
         </nav>
     </header>
     <main>
         {block content}{/block}
     </main>
     <footer>
         <p>&copy; {date('Y')} Flight Blog</p>
     </footer>
    </body>
    </html>
  4. ホームテンプレートを作成: app/views/home.latte:

    {extends 'layout.latte'}
    
     {block content}
         <h2>{$title}</h2>
         <ul>
         {foreach $posts as $post}
             <li><a href="/post/{$post['slug']}">{$post['title']}</a></li>
         {/foreach}
         </ul>
     {/block}

    サーバーを終了した場合は再起動し、http://localhost:8000にアクセスしてレンダリングされたページを確認します。

  5. データファイルを作成:

    簡単にするために、JSONファイルでデータベースをシミュレートします。

    data/posts.json:

    [
        {
            "slug": "first-post",
            "title": "My First Post",
            "content": "This is my very first blog post with Flight PHP!"
        }
    ]

ステップ 4: ルートを定義する

整理しやすくするために、ルートを設定ファイルに分離します。

  1. routes.phpを作成: app/config/routes.php:

    <?php
    Flight::route('/', function () {
        Flight::view()->render('home.latte', ['title' => 'My Blog']);
    });
    
    Flight::route('/post/@slug', function ($slug) {
        Flight::view()->render('post.latte', ['title' => 'Post: ' . $slug, 'slug' => $slug]);
    });
    
    Flight::route('GET /create', function () {
        Flight::view()->render('create.latte', ['title' => 'Create a Post']);
    });
  2. index.phpを更新: ルートファイルを読み込みます:

    <?php
    require '../vendor/autoload.php';
    
    use Latte\Engine;
    
    Flight::register('view', Engine::class, [], function ($latte) {
        $latte->setTempDirectory(__DIR__ . '/../cache/');
        $latte->setLoader(new \Latte\Loaders\FileLoader(__DIR__ . '/../app/views/'));
    });
    
    require '../app/config/routes.php';
    
    Flight::start();

ステップ 5: ブログ投稿の保存と取得

投稿を読み込んで保存するメソッドを追加します。

  1. 投稿メソッドを追加: index.phpで、投稿を読み込むメソッドを追加します:

    Flight::map('posts', function () {
        $file = __DIR__ . '/../data/posts.json';
        return json_decode(file_get_contents($file), true);
    });
  2. ルートを更新: app/config/routes.phpを変更して投稿を使用します:

    <?php
    Flight::route('/', function () {
        $posts = Flight::posts();
        Flight::view()->render('home.latte', [
            'title' => 'My Blog',
            'posts' => $posts
        ]);
    });
    
    Flight::route('/post/@slug', function ($slug) {
        $posts = Flight::posts();
        $post = array_filter($posts, fn($p) => $p['slug'] === $slug);
        $post = reset($post) ?: null;
        if (!$post) {
            Flight::notFound();
            return;
        }
        Flight::view()->render('post.latte', [
            'title' => $post['title'],
            'post' => $post
        ]);
    });
    
    Flight::route('GET /create', function () {
        Flight::view()->render('create.latte', ['title' => 'Create a Post']);
    });

ステップ 6: テンプレートを作成する

投稿を表示するようにテンプレートを更新します。

  1. 投稿ページ(app/views/post.latte:

    {extends 'layout.latte'}
    
     {block content}
         <h2>{$post['title']}</h2>
         <div class="post-content">
             <p>{$post['content']}</p>
         </div>
     {/block}

ステップ 7: 投稿作成機能を追加する

フォーム送信を処理して新しい投稿を追加します。

  1. フォームを作成(app/views/create.latte:

    {extends 'layout.latte'}
    
     {block content}
         <h2>{$title}</h2>
         <form method="POST" action="/create">
             <div class="form-group">
                 <label for="title">Title:</label>
                 <input type="text" name="title" id="title" required>
             </div>
             <div class="form-group">
                 <label for="content">Content:</label>
                 <textarea name="content" id="content" required></textarea>
             </div>
             <button type="submit">Save Post</button>
         </form>
     {/block}
  2. POSTルートを追加: app/config/routes.php:

    Flight::route('POST /create', function () {
        $request = Flight::request();
        $title = $request->data['title'];
        $content = $request->data['content'];
        $slug = strtolower(str_replace(' ', '-', $title));
    
        $posts = Flight::posts();
        $posts[] = ['slug' => $slug, 'title' => $title, 'content' => $content];
        file_put_contents(__DIR__ . '/../../data/posts.json', json_encode($posts, JSON_PRETTY_PRINT));
    
        Flight::redirect('/');
    });
  3. テストする:

    • http://localhost:8000/createにアクセスします。
    • 新しい投稿を送信します(例: 「Second Post」とコンテンツ)。
    • ホームページにそれが一覧表示されることを確認します。

ステップ 8: エラーハンドリングを強化する

notFoundメソッドをオーバーライドして、より良い404エラーページを提供します。

index.php:

Flight::map('notFound', function () {
    Flight::view()->render('404.latte', ['title' => 'Page Not Found']);
});

app/views/404.latteを作成:

{extends 'layout.latte'}

{block content}
    <h2>404 - {$title}</h2>
    <p>Sorry, that page doesn't exist!</p>
{/block}

次のステップ

結論

Flight PHPでシンプルなブログを構築できました!このガイドでは、ルーティング、Latteを使ったテンプレート、フォーム送信の処理など、Flightのコア機能を軽量なまま保ちながら紹介しました。Flightのドキュメントを参照して、さらに高度な機能を使い、ブログをさらに発展させましょう!

License

The MIT License (MIT)

Copyright © 2024 @mikecao, @n0nag0n

個人が複製の許可を得ることができるように、このソフトウェアおよび関連ドキュメントファイル(以下「ソフトウェア」という)のコピーを入手することができます。 ソフトウェアを使用、コピー、変更、マージ、公開、配布、サブライセンス、販売する権利などを含む、制限なしでソフトウェアを扱う権利が、以下の条件に従って人々にそれを許可します:

上記の著作権表示およびこの許諾表示は、ソフトウェアのすべての複製または実質的な部分に含まれている必要があります。

ソフトウェアは、「現状有姿」で提供され、商品性、特定目的への適合性、および権利侵害を含むがこれに限定されない、いかなる種類の保証もなしに提供されます。 著作者または著作権保持者は、ソフトウェアまたは使用または他の取引に起因する契約上の行為、不法行為、その他の行為から生じるクレーム、損害、その他の責任について一切責任を負いません。

About

Flight PHP Framework

Flightは、PHP向けの高速でシンプル、かつ拡張可能なフレームワークです。迅速に作業を完了したい開発者向けに構築されています。クラシックなWebアプリ、高速なAPI、またはAIコーディングアシスタントとの組み合わせなど、Flightの軽量なフットプリントとシンプルな設計は完璧に適合します。Flightは軽量であることを意図していますが、エンタープライズアーキテクチャ要件にも対応できます。

なぜFlightを選ぶのか?

ビデオ概要

シンプルですよね?
詳細については、ドキュメントでFlightについて学んでください!

クイックスタート

高速なベアボーンインストールを行うには、Composerでインストールします:

composer require flightphp/core

または、リポジトリのzipをこちらからダウンロードすることもできます。次に、以下のような基本的なindex.phpファイルを作成します:

<?php

// composerでインストールした場合
require 'vendor/autoload.php';
// またはzipファイルで手動インストールした場合
// require 'flight/Flight.php';

Flight::route('/', function() {
  echo 'hello world!';
});

Flight::route('/json', function() {
  Flight::json([
    'hello' => 'world'
  ]);
});

Flight::start();

これだけです!基本的なFlightアプリケーションが完成しました。これで、php -S localhost:8000でこのファイルを実行し、ブラウザでhttp://localhost:8000にアクセスして出力を確認できます。

このような短いFlight::の例は、学習やマイクロアプリに最適です。人間とAIツールが共有する完全なプロジェクトレイアウトについては、以下のスケルトンを使用してください。

スケルトン/ボイラープレートアプリ

新しいFlightプロジェクトを開始するための公式スターターがあります。構造、設定、Composerスクリプト、AIフレンドリーな指示を最初から設定します。

すぐに使えるプロジェクトについてはflightphp/skeletonを確認するか、インスピレーションを得るためにexamplesページを訪問してください。AIワークフローの詳細が必要ですか?AIと開発者体験を探る

(高レベルで)得られるもの:

スケルトンアプリのインストール

簡単です!

# 新しいプロジェクトを作成
composer create-project flightphp/skeleton my-project/
# 新しいプロジェクトディレクトリに入る
cd my-project/
# すぐに開始するためにローカル開発サーバーを起動!
composer start

プロジェクト構造を作成し、config_sample.phpconfig.php(および存在する場合は.env.example.env)をコピーします。これで準備完了です。オプションのサンプルデータ:

php runway migrate
# その後 /posts と /api/posts にアクセス

高パフォーマンス

Flightは、現在存在する最も高速なPHPフレームワークの1つです。その軽量なコアは、オーバーヘッドを減らし、速度を向上させます。伝統的なアプリと現代のAI支援ワークフローの両方に最適です。TechEmpowerで、すべてのベンチマークを確認できます。

他の人気のPHPフレームワークとのベンチマークを以下に示します。

Framework Plaintext Reqs/sec JSON Reqs/sec
Flight 190,421 182,491
Yii 145,749 131,434
Fat-Free 139,238 133,952
Slim 89,588 87,348
Phalcon 95,911 87,675
Symfony 65,053 63,237
Lumen 40,572 39,700
Laravel 26,657 26,901
CodeIgniter 20,628 19,901

FlightとAI

コーディングLLMとのFlightの組み合わせに興味がありますか?AGENTS.md、Runway ai:*コマンド、スケルトンレイアウトがアシスタントを正しい軌道に保つ方法を発見してください。

安定性と後方互換性

私たちはあなたの時間を大切にしています。私たちは皆、数年ごとに完全に自己再発明し、開発者に破損したコードと高価な移行を残すフレームワークを見てきました。Flightは違います。Flight v3はv2の拡張として設計されており、皆さんが知り愛用しているAPIが剥奪されていません。実際、ほとんどのv2プロジェクトはv3で変更なしに動作します。

Flightの安定性を保ち、フレームワークの修正ではなく、アプリの構築に集中できるようにすることにコミットしています。スケルトンは新しいプロジェクトに対しては意見を持っている場合がありますが、コアAPIは他のすべてのユーザーにとって馴染み深いままです。

コミュニティ

Matrix Chatで活動しています

Matrix

そしてDiscordでも

貢献

Flightに貢献する方法は2つあります:

  1. コアリポジトリを訪問して、コアフレームワークに貢献する。
  2. ドキュメントの改善を手伝う!このドキュメントウェブサイトはGithubでホストされています。エラーを見つけた場合や改善したいことがある場合は、プルリクエストを送信してください。更新や新しいアイデアを歓迎します。特にAIと新技術に関するものを歓迎します!

要件

FlightにはPHP 7.4以上が必要です。

注意: PHP 7.4がサポートされている理由は、執筆時点(2024年)で、PHP 7.4が一部のLTS Linuxディストリビューションのデフォルトバージョンであるためです。PHP >8への移行を強制すると、それらのユーザーに多くの問題を引き起こすことになります。フレームワークはPHP >8もサポートしています。

ライセンス

FlightはMITライセンスの下でリリースされています。

Awesome-plugins/php_cookie

クッキー

overclokk/cookie はアプリ内でクッキーを管理するためのシンプルなライブラリです。

インストール

composerを使用して簡単にインストールできます。

composer require overclokk/cookie

使用法

使用法は、Flightクラスに新しいメソッドを登録するだけです。


use Overclokk\Cookie\Cookie;

/*
 * ブートストラップまたはpublic/index.phpファイルに設定
 */

Flight::register('cookie', Cookie::class);

/**
 * ExampleController.php
 */

class ExampleController {
    public function login() {
        // クッキーを設定します

        // インスタンスを取得するためfalseである必要があります
        // オートコンプリートを有効にしたい場合は以下のコメントを使用してください
        /** @var \Overclokk\Cookie\Cookie $cookie */
        $cookie = Flight::cookie(false);
        $cookie->set(
            'stay_logged_in', // クッキーの名前
            '1', // 設定したい値
            86400, // クッキーの有効期間(秒)
            '/', // クッキーが利用可能なパス
            'example.com', // クッキーが利用可能なドメイン
            true, // セキュアな HTTPS 接続でのみクッキーが送信されます
            true // クッキーはHTTPプロトコルを介してのみ利用可能です
        );

        // オプションで、デフォルト値を維持したい場合や、
        // 長期間にわたってクッキーを簡単に設定したい場合
        $cookie->forever('stay_logged_in', '1');
    }

    public function home() {
        // クッキーがあるかどうかをチェック
        if (Flight::cookie()->has('stay_logged_in')) {
            // 例えば、ダッシュボードエリアにリダイレクトします。
            Flight::redirect('/dashboard');
        }
    }
}

Awesome-plugins/php_encryption

PHP 暗号化

defuse/php-encryption はデータの暗号化と復号を行うために使用できるライブラリです。すぐにデータの暗号化と復号を始めることはかなり簡単です。ライブラリの使用方法や暗号化に関連する重要なセキュリティの問題を説明する素晴らしいtutorialがあります。

インストール

composerを使用して簡単にインストールします。

composer require defuse/php-encryption

セットアップ

その後、暗号化キーを生成する必要があります。

vendor/bin/generate-defuse-key

これにより、安全に保持する必要があるキーが生成されます。キーは、ファイルの末尾にある配列内のapp/config/config.phpファイルに保存できます。完璧な場所ではありませんが、少なくとも何かです。

使用方法

ライブラリと暗号化キーがあるので、データの暗号化と復号を開始できます。


use Defuse\Crypto\Crypto;
use Defuse\Crypto\Key;

/*
 * ブートストラップまたはpublic/index.phpファイルに設定します
 */

// 暗号化メソッド
Flight::map('encrypt', function($raw_data) {
    $encryption_key = /* $config['encryption_key']またはキーを配置した場所のfile_get_contents */;
    return Crypto::encrypt($raw_data, Key::loadFromAsciiSafeString($encryption_key));
});

// 復号メソッド
Flight::map('decrypt', function($encrypted_data) {
    $encryption_key = /* $config['encryption_key']またはキーを配置した場所のfile_get_contents */;
    try {
        $raw_data = Crypto::decrypt($encrypted_data, Key::loadFromAsciiSafeString($encryption_key));
    } catch (Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException $ex) {
        // 攻撃! 間違ったキーが読み込まれたか、暗号文が作成されてから変更された可能性があります -- データベースで破損されたか、攻撃を実行しようとするEveによって意図的に変更された可能性があります。

        // ... アプリケーションに適した方法でこのケースを処理します ...
    }
    return $raw_data;
});

Flight::route('/encrypt', function() {
    $encrypted_data = Flight::encrypt('これは秘密です');
    echo $encrypted_data;
});

Flight::route('/decrypt', function() {
    $encrypted_data = '...'; // どこかから暗号化されたデータを取得します
    $decrypted_data = Flight::decrypt($encrypted_data);
    echo $decrypted_data;
});

Awesome-plugins/php_file_cache

flightphp/cache

軽量でシンプルなスタンドアロン PHP ファイル内キャッシュクラス Wruczek/PHP-File-Cache からフォーク

利点

このドキュメントサイトはこのライブラリを使用して各ページをキャッシュしています!

コードを表示するにはこちらをクリックしてください。

インストール

composer 経由でインストール:

composer require flightphp/cache

使用方法

使用方法は非常に簡単です。これによりキャッシュディレクトリにキャッシュファイルが保存されます。

use flight\Cache;

$app = Flight::app();

// コンストラクタにキャッシュを保存するディレクトリを渡します
$app->register('cache', Cache::class, [ __DIR__ . '/../cache/' ], function(Cache $cache) {

    // これにより、キャッシュが本番モードの場合にのみ使用されることが保証されます
    // ENVIRONMENT はブートストラップファイルまたはアプリ内の他の場所で設定される定数です
    $cache->setDevMode(ENVIRONMENT === 'development');
});

キャッシュ値の取得

get() メソッドを使用してキャッシュされた値を取得します。期限切れの場合にキャッシュを更新する便利なメソッドが必要な場合は、refreshIfExpired() を使用できます。


// キャッシュインスタンスを取得
$cache = Flight::cache();
$data = $cache->refreshIfExpired('simple-cache-test', function () {
    return date("H:i:s"); // キャッシュするデータを返す
}, 10); // 10秒

// または
$data = $cache->get('simple-cache-test');
if(empty($data)) {
    $data = date("H:i:s");
    $cache->set('simple-cache-test', $data, 10); // 10秒
}

キャッシュ値の保存

set() メソッドを使用してキャッシュに値を保存します。

Flight::cache()->set('simple-cache-test', 'my cached data', 10); // 10秒

キャッシュ値の削除

delete() メソッドを使用してキャッシュ内の値を削除します。

Flight::cache()->delete('simple-cache-test');

キャッシュ値の存在確認

exists() メソッドを使用してキャッシュに値が存在するかどうかを確認します。

if(Flight::cache()->exists('simple-cache-test')) {
    // 何かを実行
}

キャッシュのクリア

flush() メソッドを使用してキャッシュ全体をクリアします。

Flight::cache()->flush();

キャッシュからメタデータを取得

キャッシュエントリに関するタイムスタンプやその他のメタデータを取得したい場合は、正しいパラメータとして true を渡すようにしてください。

$data = $cache->refreshIfExpired("simple-cache-meta-test", function () {
    echo "Refreshing data!" . PHP_EOL;
    return date("H:i:s"); // キャッシュするデータを返す
}, 10, true); // true = メタデータ付きで返す
// または
$data = $cache->get("simple-cache-meta-test", true); // true = メタデータ付きで返す

/*
メタデータ付きで取得したキャッシュアイテムの例:
{
    "time":1511667506, <-- 保存された unix タイムスタンプ
    "expire":10,       <-- 秒単位の有効期限
    "data":"04:38:26", <-- 非シリアライズデータ
    "permanent":false
}

メタデータを使用することで、例えばアイテムが保存された時刻や有効期限を計算することができます
"data" キーでデータ自体にアクセスすることもできます
*/

$expiresin = ($data["time"] + $data["expire"]) - time(); // データが期限切れになる unix タイムスタンプを取得し、そこから現在のタイムスタンプを減算
$cacheddate = $data["data"]; // "data" キーでデータ自体にアクセス

echo "最新のキャッシュ保存: $cacheddate, $expiresin 秒後に期限切れ";

ソースコード

コードを表示するには https://github.com/flightphp/cache をご覧ください。

Awesome-plugins/permissions

FlightPHP/Permissions

これは、アプリに複数のロールがあり、各ロールに少し異なる機能がある場合にプロジェクトで使用できる権限モジュールです。このモジュールを使用すると、各ロールの権限を定義し、現在のユーザーが特定のページにアクセスしたり特定のアクションを実行したりする権限を持っているかどうかを確認できます。

GitHubのリポジトリはこちらをご覧ください。

インストール

composer require flightphp/permissions を実行すれば準備完了です!

使い方

まず権限を設定し、次にアプリに権限の意味を伝えます。最終的には $Permissions->has()->can()、または is() で権限を確認します。has()can() は同じ機能を持ちますが、コードの可読性を高めるために異なる名前が付けられています。

基本的な例

アプリケーションにユーザーがログインしているかどうかを確認する機能があると仮定します。以下のように権限オブジェクトを作成できます:

// index.php
require 'vendor/autoload.php';

// 何らかのコード

// 現在の役割が誰であるかを示すものがあるでしょう
// セッション変数から現在の役割を取得するものがあるでしょう
// ログイン後、そうでなければ「guest」または「public」の役割になります。
$current_role = 'admin';

// 権限の設定
$permission = new \flight\Permission($current_role);
$permission->defineRule('loggedIn', function($current_role) {
    return $current_role !== 'guest';
});

// このオブジェクトをFlightのどこかに保存したいでしょう
Flight::set('permission', $permission);

次に、コントローラー内のどこかで、以下のようなコードがあるでしょう。

<?php

// 何らかのコントローラー
class SomeController {
    public function someAction() {
        $permission = Flight::get('permission');
        if ($permission->has('loggedIn')) {
            // 何かを実行
        } else {
            // 他の何かを実行
        }
    }
}

これを使用して、アプリケーション内で何かを実行する権限があるかどうかを追跡することもできます。 たとえば、ソフトウェア上でユーザーが投稿を操作できる方法がある場合、特定のアクションを実行する権限があるかどうかを確認できます。

$current_role = 'admin';

// 権限の設定
$permission = new \flight\Permission($current_role);
$permission->defineRule('post', function($current_role) {
    if($current_role === 'admin') {
        $permissions = ['create', 'read', 'update', 'delete'];
    } else if($current_role === 'editor') {
        $permissions = ['create', 'read', 'update'];
    } else if($current_role === 'author') {
        $permissions = ['create', 'read'];
    } else if($current_role === 'contributor') {
        $permissions = ['create'];
    } else {
        $permissions = [];
    }
    return $permissions;
});
Flight::set('permission', $permission);

次に、コントローラー内のどこかで...

class PostController {
    public function create() {
        $permission = Flight::get('permission');
        if ($permission->can('post.create')) {
            // 何かを実行
        } else {
            // 他の何かを実行
        }
    }
}

依存性の注入

権限を定義するクロージャに依存性を注入できます。これは、トグル、ID、またはチェックしたい他のデータポイントがある場合に便利です。Class->Method タイプの呼び出しでも同じことが機能しますが、引数はメソッド内で定義します。

クロージャ

$Permission->defineRule('order', function(string $current_role, MyDependency $MyDependency = null) {
    // ... コード
});

// コントローラーファイル内
public function createOrder() {
    $MyDependency = Flight::myDependency();
    $permission = Flight::get('permission');
    if ($permission->can('order.create', $MyDependency)) {
        // 何かを実行
    } else {
        // 他の何かを実行
    }
}

クラス

namespace MyApp;

class Permissions {

    public function order(string $current_role, MyDependency $MyDependency = null) {
        // ... コード
    }
}

クラスを使用した権限設定のショートカット

クラスを使用して権限を定義することもできます。これは、多数の権限があり、コードをクリーンに保ちたい場合に便利です。以下のようにできます:

<?php

// ブートストラップコード
$Permissions = new \flight\Permission($current_role);
$Permissions->defineRule('order', 'MyApp\Permissions->order');

// myapp/Permissions.php
namespace MyApp;

class Permissions {

    public function order(string $current_role, int $user_id) {
        // 事前に設定したと仮定
        /** @var \flight\database\SimplePdo $db */
        $db = Flight::db();
        $allowed_permissions = [ 'read' ]; // 誰でも注文を表示できます
        if($current_role === 'manager') {
            $allowed_permissions[] = 'create'; // マネージャーは注文を作成できます
        }
        $some_special_toggle_from_db = $db->fetchField('SELECT some_special_toggle FROM settings WHERE id = ?', [ $user_id ]);
        if($some_special_toggle_from_db) {
            $allowed_permissions[] = 'update'; // ユーザーが特別なトグルを持っている場合、注文を更新できます
        }
        if($current_role === 'admin') {
            $allowed_permissions[] = 'delete'; // 管理者は注文を削除できます
        }
        return $allowed_permissions;
    }
}

素晴らしい点は、ショートカット(キャッシュも可能!)を使用できることです。権限クラスにクラス内のすべてのメソッドを権限にマッピングするよう指示するだけです。したがって、order() という名前のメソッドと company() という名前のメソッドがある場合、これらは自動的にマッピングされるため、$Permissions->has('order.read') または $Permissions->has('company.read') を実行するだけで機能します。これを定義するのは非常に難しいので、注意してください。以下のようにするだけです:

一緒にグループ化したい権限のクラスを作成します。

class MyPermissions {
    public function order(string $current_role, int $order_id = 0): array {
        // 権限を決定するコード
        return $permissions_array;
    }

    public function company(string $current_role, int $company_id): array {
        // 権限を決定するコード
        return $permissions_array;
    }
}

次に、このライブラリを使用して権限を発見可能にします。

$Permissions = new \flight\Permission($current_role);
$Permissions->defineRulesFromClassMethods(MyApp\Permissions::class);
Flight::set('permissions', $Permissions);

最後に、コードベースで権限を呼び出して、ユーザーが特定の権限を実行できるかどうかを確認します。

class SomeController {
    public function createOrder() {
        if(Flight::get('permissions')->can('order.create') === false) {
            die('注文を作成できません。申し訳ありません!');
        }
    }
}

キャッシュ

キャッシュを有効にするには、シンプルなwruczak/phpfilecache ライブラリを参照してください。有効にする例を以下に示します。


// この $app はコードの一部である場合もあれば、
// null を渡すだけでコンストラクタで Flight::app() から取得できます
$app = Flight::app();

// 現在はファイルキャッシュとしてこれを受け入れます。他のものは簡単に
// 将来追加できます。
$Cache = new Wruczek\PhpFileCache\PhpFileCache;

$Permissions = new \flight\Permission($current_role, $app, $Cache);
$Permissions->defineRulesFromClassMethods(MyApp\Permissions::class, 3600); // 3600 はこれをキャッシュする秒数です。キャッシュを使用しない場合はこれを省略してください

それでは始めましょう!

Awesome-plugins/simple_job_queue

シンプルジョブキュー

シンプルジョブキューは、非同期でジョブを処理するために使用できるライブラリです。beanstalkd、MySQL/MariaDB、SQLite、およびPostgreSQLで使用できます。

インストール

composer require n0nag0n/simple-job-queue

使用法

これを機能させるには、キューにジョブを追加する方法と、ジョブを処理する方法(ワーカー)が必要です。以下は、ジョブをキューに追加する方法と、そのジョブを処理する方法の例です。

Flightへの追加

これをFlightに追加するのは簡単で、register()メソッドを使用して行います。以下は、これをFlightに追加する方法の例です。

<?php
require 'vendor/autoload.php';

// beanstalkdを使用する場合は、['mysql']を['beanstalkd']に変更してください
Flight::register('queue', n0nag0n\Job_Queue::class, ['mysql'], function($Job_Queue) {
    // Flight::db()で既にPDO接続がある場合
    $Job_Queue->addQueueConnection(Flight::db());

    // または、beanstalkd/Pheanstalkを使用している場合
    $pheanstalk = Pheanstalk\Pheanstalk::create('127.0.0.1');
    $Job_Queue->addQueueConnection($pheanstalk);
});

新しいジョブの追加

ジョブを追加する場合、パイプライン(キュー)を指定する必要があります。これは、RabbitMQのチャネルやbeanstalkdのチューブに相当します。

<?php
Flight::queue()->selectPipeline('send_important_emails');
Flight::queue()->addJob(json_encode([ 'something' => 'that', 'ends' => 'up', 'a' => 'string' ]));

ワーカーの実行

ここにワーカーを実行する方法のサンプルファイルがあります。

<?php

require 'vendor/autoload.php';

$Job_Queue = new n0nag0n\Job_Queue('mysql');
// PDO接続
$PDO = new PDO('mysql:dbname=testdb;host=127.0.0.1', 'user', 'pass');
$Job_Queue->addQueueConnection($PDO);

// または、beanstalkd/Pheanstalkを使用している場合
$pheanstalk = Pheanstalk\Pheanstalk::create('127.0.0.1');
$Job_Queue->addQueueConnection($pheanstalk);

$Job_Queue->watchPipeline('send_important_emails');
while(true) {
    $job = $Job_Queue->getNextJobAndReserve();

    // あなたが夜に良く眠れるように調整してください(データベースキューのみ、beanstalkdではこのif文は必要ありません)
    if(empty($job)) {
        usleep(500000);
        continue;
    }

    echo "処理中 {$job['id']}\n";
    $payload = json_decode($job['payload'], true);

    try {
        $result = doSomethingThatDoesSomething($payload);

        if($result === true) {
            $Job_Queue->deleteJob($job);
        } else {
            // これはレディキューから取り出し、後で拾って「キック」できる別のキューに入れます。
            $Job_Queue->buryJob($job);
        }
    } catch(Exception $e) {
        $Job_Queue->buryJob($job);
    }
}

Supervisordを使用した長いプロセスの処理

Supervisordは、ワーカープロセスが継続的に実行されることを保証するプロセス制御システムです。シンプルジョブキューワーカーの設定に関するより完全なガイドは次のとおりです。

Supervisordのインストール

# Ubuntu/Debian上
sudo apt-get install supervisor

# CentOS/RHEL上
sudo yum install supervisor

# Homebrewを使用したmacOS上
brew install supervisor

ワーカースクリプトの作成

最初に、ワーカーコードを専用のPHPファイルに保存します。

<?php

require 'vendor/autoload.php';

$Job_Queue = new n0nag0n\Job_Queue('mysql');
// PDO接続
$PDO = new PDO('mysql:dbname=your_database;host=127.0.0.1', 'username', 'password');
$Job_Queue->addQueueConnection($PDO);

// 監視するパイプラインを設定
$Job_Queue->watchPipeline('send_important_emails');

// ワーカーの開始をログに記録
echo date('Y-m-d H:i:s') . " - ワーカーが開始されました\n";

while(true) {
    $job = $Job_Queue->getNextJobAndReserve();

    if(empty($job)) {
        usleep(500000); // 0.5秒間スリープ
        continue;
    }

    echo date('Y-m-d H:i:s') . " - ジョブ {$job['id']}を処理中\n";
    $payload = json_decode($job['payload'], true);

    try {
        $result = doSomethingThatDoesSomething($payload);

        if($result === true) {
            $Job_Queue->deleteJob($job);
            echo date('Y-m-d H:i:s') . " - ジョブ {$job['id']}は正常に完了しました\n";
        } else {
            $Job_Queue->buryJob($job);
            echo date('Y-m-d H:i:s') . " - ジョブ {$job['id']}が失敗し、埋められました\n";
        }
    } catch(Exception $e) {
        $Job_Queue->buryJob($job);
        echo date('Y-m-d H:i:s') . " - ジョブ {$job['id']}の処理中に例外が発生しました: {$e->getMessage()}\n";
    }
}

Supervisordの設定

ワーカーのための設定ファイルを作成します。

[program:email_worker]
command=php /path/to/worker.php
directory=/path/to/project
autostart=true
autorestart=true
startretries=3
stderr_logfile=/var/log/simple_job_queue_err.log
stdout_logfile=/var/log/simple_job_queue.log
user=www-data
numprocs=2
process_name=%(program_name)s_%(process_num)02d

主要な設定オプション:

Supervisorctlによるワーカーの管理

設定を作成または変更した後:

# supervisor設定を再読み込み
sudo supervisorctl reread
sudo supervisorctl update

# 特定のワーカープロセスを制御
sudo supervisorctl start email_worker:*
sudo supervisorctl stop email_worker:*
sudo supervisorctl restart email_worker:*
sudo supervisorctl status email_worker:*

複数のパイプラインの実行

複数のパイプラインの場合、別々のワーカーファイルと設定を作成します。

[program:email_worker]
command=php /path/to/email_worker.php
# ... その他の設定 ...

[program:notification_worker]
command=php /path/to/notification_worker.php
# ... その他の設定 ...

監視とログ

ワーカーのアクティビティを監視するために、ログを確認します。

# ログを表示
sudo tail -f /var/log/simple_job_queue.log

# ステータスを確認
sudo supervisorctl status

この設定により、ジョブワーカーはクラッシュ、サーバーの再起動、またはその他の問題が発生しても継続的に実行されることが保証され、プロダクション環境におけるキューシステムの信頼性が高まります。

Awesome-plugins/jwt

Firebase JWT - JSON Web Token 認証

JWT (JSON Web Tokens) は、アプリケーションとクライアント間で請求を表すためのコンパクトで URL セーフな方法です。サーバー側のセッションストレージが不要なステートレス API 認証に最適です!このガイドでは、Firebase JWT を Flight と統合して、セキュアなトークンベースの認証を行う方法を示します。

完全なドキュメントと詳細については、Github リポジトリ をご覧ください。

JWT とは?

JSON Web Token は、3つの部分を含む文字列です:

  1. ヘッダー: トークンに関するメタデータ(アルゴリズム、タイプ)
  2. ペイロード: あなたのデータ(ユーザー ID、ロール、期限切れなど)
  3. 署名: 真正性を検証するための暗号署名

例の JWT: eyJ0eXAiOiJKV1QiLCJhbGc...(意味不明に見えますが、構造化されたデータです!)

JWT を使用する理由は?

インストール

Composer を使用してインストール:

composer require firebase/php-jwt

基本的な使用方法

JWT を作成して検証する簡単な例:

use Firebase\JWT\JWT;
use Firebase\JWT\Key;

// あなたのシークレットキー(これを安全に保ってください!)
$secretKey = 'your-256-bit-secret-key-here-keep-it-safe';

// トークンを作成
$payload = [
    'user_id' => 123,
    'username' => 'johndoe',
    'role' => 'admin',
    'iat' => time(),              // 発行日時
    'exp' => time() + 3600        // 1時間後に期限切れ
];

$jwt = JWT::encode($payload, $secretKey, 'HS256');
echo "Token: " . $jwt;

// トークンを検証してデコード
try {
    $decoded = JWT::decode($jwt, new Key($secretKey, 'HS256'));
    echo "User ID: " . $decoded->user_id;
} catch (Exception $e) {
    echo "Invalid token: " . $e->getMessage();
}

Flight 用の JWT ミドルウェア(推奨アプローチ)

Flight で JWT を使用する最も一般的で有用な方法は、API ルートを保護するための ミドルウェア としてです。以下は、完全で本番環境対応の例です:

ステップ 1: JWT ミドルウェアクラスを作成

// app/middleware/JwtMiddleware.php
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Firebase\JWT\ExpiredException;
use Firebase\JWT\SignatureInvalidException;
use flight\Engine;

class JwtMiddleware {

    protected Engine $app;
    protected string $secretKey;

    public function __construct(Engine $app) {
        $this->app = $app;
        // シークレットキーを app/config/config.php に保存し、ハードコードしないでください!
        $this->secretKey = $app->get('config')['jwt_secret'];
    }

    public function before(array $params) {
        $authHeader = $this->app->request()->getHeader('Authorization');

        // Authorization ヘッダーの存在を確認
        if (empty($authHeader)) {
            $this->app->jsonHalt(['error' => '認証トークンが提供されていません'], 401);
        }

        // "Bearer <token>" 形式からトークンを抽出
        if (!preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) {
            $this->app->jsonHalt(['error' => '無効な認証形式です。Bearer <token> を使用してください'], 401);
        }

        $jwt = $matches[1];

        try {
            // トークンをデコードして検証
            $decoded = JWT::decode($jwt, new Key($this->secretKey, 'HS256'));

            // ルートハンドラーで使用するためにユーザー情報をリクエストに保存
            $this->app->request()->data->user = $decoded;

        } catch (ExpiredException $e) {
            $this->app->jsonHalt(['error' => 'トークンの有効期限が切れています'], 401);
        } catch (SignatureInvalidException $e) {
            $this->app->jsonHalt(['error' => '無効なトークン署名'], 401);
        } catch (Exception $e) {
            $this->app->jsonHalt(['error' => '無効なトークン: ' . $e->getMessage()], 401);
        }
    }
}

ステップ 2: 設定に JWT シークレットを登録

// app/config/config.php
return [
    'jwt_secret' => getenv('JWT_SECRET') ?: 'your-fallback-secret-for-development'
];

// app/config/bootstrap.php または index.php
// 設定をアプリに公開したい場合は、この行を追加してください
$app->set('config', $config);

セキュリティノート: シークレットキーをハードコードしないでください!本番環境では環境変数を使用してください。

ステップ 3: ミドルウェアでルートを保護

// 単一のルートを保護
Flight::route('GET /api/user/profile', function() {
    $user = Flight::request()->data->user; // ミドルウェアで設定
    Flight::json([
        'user_id' => $user->user_id,
        'username' => $user->username,
        'role' => $user->role
    ]);
})->addMiddleware(JwtMiddleware::class);

// ルートのグループ全体を保護(より一般的!)
Flight::group('/api', function() {
    Flight::route('GET /users', function() { /* ... */ });
    Flight::route('GET /posts', function() { /* ... */ });
    Flight::route('POST /posts', function() { /* ... */ });
    Flight::route('DELETE /posts/@id', function($id) { /* ... */ });
}, [ JwtMiddleware::class ]); // このグループ内のすべてのルートが保護されます!

ミドルウェアの詳細については、ミドルウェアドキュメント を参照してください。

一般的な使用事例

1. ログインエンドポイント(トークン生成)

認証成功後に JWT を生成するルートを作成:

Flight::route('POST /api/login', function() {
    $data = Flight::request()->data;
    $username = $data->username ?? '';
    $password = $data->password ?? '';

    // 認証情報を検証(例 — 独自のロジックを使用!)
    $user = validateUserCredentials($username, $password);

    if (!$user) {
        Flight::jsonHalt(['error' => '無効な認証情報'], 401);
    }

    // JWT を生成
    $secretKey = Flight::get('config')['jwt_secret'];
    $payload = [
        'user_id' => $user->id,
        'username' => $user->username,
        'role' => $user->role,
        'iat' => time(),
        'exp' => time() + (60 * 60) // 1時間の有効期限
    ];

    $jwt = JWT::encode($payload, $secretKey, 'HS256');

    Flight::json([
        'success' => true,
        'token' => $jwt,
        'expires_in' => 3600
    ]);
});

function validateUserCredentials($username, $password) {
    // ここにデータベース検索とパスワード検証
    // 例:
    $db = Flight::db();
    $user = $db->fetchRow("SELECT * FROM users WHERE username = ?", [$username]);

    if ($user && password_verify($password, $user['password_hash'])) {
        return (object) [
            'id' => $user['id'],
            'username' => $user['username'],
            'role' => $user['role']
        ];
    }
    return null;
}

2. トークン更新フロー

長期間のセッションのためのリフレッシュトークンシステムを実装:

Flight::route('POST /api/login', function() {
    // ... 認証情報を検証 ...

    $secretKey = Flight::get('config')['jwt_secret'];
    $refreshSecret = Flight::get('config')['jwt_refresh_secret'];

    // 短期間のアクセストークン (15 分)
    $accessToken = JWT::encode([
        'user_id' => $user->id,
        'type' => 'access',
        'iat' => time(),
        'exp' => time() + (15 * 60)
    ], $secretKey, 'HS256');

    // 長期間のリフレッシュトークン (7 日)
    $refreshToken = JWT::encode([
        'user_id' => $user->id,
        'type' => 'refresh',
        'iat' => time(),
        'exp' => time() + (7 * 24 * 60 * 60)
    ], $refreshSecret, 'HS256');

    Flight::json([
        'access_token' => $accessToken,
        'refresh_token' => $refreshToken,
        'expires_in' => 900
    ]);
});

Flight::route('POST /api/refresh', function() {
    $refreshToken = Flight::request()->data->refresh_token ?? '';
    $refreshSecret = Flight::get('config')['jwt_refresh_secret'];

    try {
        $decoded = JWT::decode($refreshToken, new Key($refreshSecret, 'HS256'));

        // これがリフレッシュトークンであることを検証
        if ($decoded->type !== 'refresh') {
            Flight::jsonHalt(['error' => '無効なトークンタイプ'], 401);
        }

        // 新しいアクセストークンを生成
        $secretKey = Flight::get('config')['jwt_secret'];
        $accessToken = JWT::encode([
            'user_id' => $decoded->user_id,
            'type' => 'access',
            'iat' => time(),
            'exp' => time() + (15 * 60)
        ], $secretKey, 'HS256');

        Flight::json([
            'access_token' => $accessToken,
            'expires_in' => 900
        ]);

    } catch (Exception $e) {
        Flight::jsonHalt(['error' => '無効なリフレッシュトークン'], 401);
    }
});

3. ロールベースのアクセス制御

ミドルウェアを拡張してユーザー ロールをチェック:

class JwtRoleMiddleware {

    protected Engine $app;
    protected array $allowedRoles;

    public function __construct(Engine $app, array $allowedRoles = []) {
        $this->app = $app;
        $this->allowedRoles = $allowedRoles;
    }

    public function before(array $params) {
        // JwtMiddleware がすでに実行され、ユーザー データが設定されていると仮定
        $user = $this->app->request()->data->user ?? null;

        if (!$user) {
            $this->app->jsonHalt(['error' => '認証が必要です'], 401);
        }

        // ユーザーが必要なロールを持っているかをチェック
        if (!empty($this->allowedRoles) && !in_array($user->role, $this->allowedRoles)) {
            $this->app->jsonHalt(['error' => '権限が不足しています'], 403);
        }
    }
}

// 使用例: 管理者専用ルート
Flight::route('DELETE /api/users/@id', function($id) {
    // ユーザー削除ロジック
})->addMiddleware([
    JwtMiddleware::class,
    new JwtRoleMiddleware(Flight::app(), ['admin'])
]);

4. ユーザーごとのレート制限付きパブリック API

セッションなしで JWT を使用してユーザー を追跡し、レート制限:

class RateLimitMiddleware {

    public function before(array $params) {
        $user = Flight::request()->data->user ?? null;
        $userId = $user ? $user->user_id : Flight::request()->ip;

        $cacheKey = "rate_limit:$userId";
        // app/config/services.php でキャッシュサービスを設定してください
        $requests = Flight::cache()->get($cacheKey, 0);

        if ($requests >= 100) { // 1時間あたり 100 リクエスト
            Flight::jsonHalt(['error' => 'レート制限を超えました'], 429);
        }

        Flight::cache()->set($cacheKey, $requests + 1, 3600);
    }
}

セキュリティのベストプラクティス

1. 強力なシークレットキー を使用

// セキュアなシークレットキーを生成(一度実行し、.env ファイルに保存)
$secretKey = base64_encode(random_bytes(32));
echo $secretKey; // これを .env ファイルに保存!

2. シークレットを環境変数に保存

// シークレットをバージョン管理にコミットしないでください!
// .env ファイルと vlucas/phpdotenv などのライブラリを使用

// .env ファイル:
// JWT_SECRET=your-base64-encoded-secret-here
// JWT_REFRESH_SECRET=another-base64-encoded-secret-here

// app/config/config.php ファイルを使用してシークレットを保存することも可能
// 設定ファイルがバージョン管理にコミットされないことを確認
// return [
//     'jwt_secret' => 'your-base64-encoded-secret-here',
//     'jwt_refresh_secret' => 'another-base64-encoded-secret-here',
// ];

// アプリ内:
// $secretKey = getenv('JWT_SECRET');

3. 適切な有効期限を設定

// 良い習慣: 短期間のアクセストークン
'exp' => time() + (15 * 60)  // 15 分

// リフレッシュトークンの場合: 長い有効期限
'exp' => time() + (7 * 24 * 60 * 60)  // 7 日

4. 本番環境で HTTPS を使用

JWT は 常に HTTPS で送信されるべきです。本番環境ではプレーン HTTP でトークンを送信しないでください!

5. トークンクレームを検証

重要なクレームを常に検証:

$decoded = JWT::decode($jwt, new Key($secretKey, 'HS256'));

// 期限切れのチェックはライブラリで自動的に処理されます
// ただし、カスタム検証を追加可能:
if ($decoded->iat > time()) {
    throw new Exception('トークンが発行される前に使用されました');
}

if (isset($decoded->nbf) && $decoded->nbf > time()) {
    throw new Exception('トークンがまだ有効ではありません');
}

6. ログアウトのためのトークンブラックリストを検討

追加のセキュリティのため、無効化されたトークンのブラックリストを維持:

Flight::route('POST /api/logout', function() {
    $authHeader = Flight::request()->getHeader('Authorization');
    preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches);
    $jwt = $matches[1];

    // トークンの有効期限を抽出
    $decoded = Flight::request()->data->user;
    $ttl = $decoded->exp - time();

    // 有効期限までキャッシュ/Redis に保存
    Flight::cache()->set("blacklist:$jwt", true, $ttl);

    Flight::json(['message' => '正常にログアウトしました']);
});

// JwtMiddleware に追加:
public function before(array $params) {
    // ... JWT を抽出 ...

    // ブラックリストをチェック
    if (Flight::cache()->get("blacklist:$jwt")) {
        $this->app->jsonHalt(['error' => 'トークンが取り消されました'], 401);
    }

    // ... トークンを検証 ...
}

アルゴリズムとキー タイプ

Firebase JWT は複数のアルゴリズムをサポート:

対称アルゴリズム (HMAC)

$jwt = JWT::encode($payload, $secretKey, 'HS256');
$decoded = JWT::decode($jwt, new Key($secretKey, 'HS256'));

非対称アルゴリズム (RSA/ECDSA)

// 鍵を生成: openssl genrsa -out private.key 2048
// openssl rsa -in private.key -pubout -out public.key

$privateKey = file_get_contents('/path/to/private.key');
$publicKey = file_get_contents('/path/to/public.key');

// 秘密鍵でエンコード
$jwt = JWT::encode($payload, $privateKey, 'RS256');

// 公開鍵でデコード
$decoded = JWT::decode($jwt, new Key($publicKey, 'RS256'));

RSA を使用するタイミング: 検証のための公開鍵を配布する必要がある場合に RSA を使用(例: マイクロサービス、サードパーティ統合)。単一のアプリケーションの場合、HS256 がよりシンプルで十分です。

トラブルシューティング

"Expired token" エラー

トークンの exp クレームが過去です。新しいトークンを発行するか、トークン更新を実装してください。

"Signature verification failed"

use Firebase\JWT\JWT;

JWT::$leeway = 60; // 60 秒のクロックスキューを許可
$decoded = JWT::decode($jwt, new Key($secretKey, 'HS256'));

リクエストでトークンが送信されない

クライアントが Authorization ヘッダーを送信していることを確認:

// JavaScript の例
fetch('/api/users', {
    headers: {
        'Authorization': 'Bearer ' + token
    }
});

メソッド

Firebase JWT ライブラリはこれらのコアメソッドを提供:

このライブラリを使用する理由は?

関連資料

ライセンス

Firebase JWT ライブラリは BSD 3-Clause ライセンスの下でライセンスされています。詳細は Github リポジトリ を参照してください。

Awesome-plugins/n0nag0n_wordpress

WordPress 統合: n0nag0n/wordpress-integration-for-flight-framework

WordPress サイト内で Flight PHP を使用したいですか? このプラグインはそれを簡単に行えます! n0nag0n/wordpress-integration-for-flight-framework を使用すると、WordPress のインストールと並行して完全な Flight アプリを実行できます—カスタム API、マイクロサービス、またはフル機能のアプリを WordPress の快適な環境から離れずに構築するのに最適です。


これは何をするのですか?

インストール

  1. flight-integration フォルダを /wp-content/plugins/ ディレクトリにアップロードします。
  2. WordPress の管理画面 (Plugins メニュー) でプラグインを有効化します。
  3. Settings > Flight Framework に移動してプラグインを設定します。
  4. Flight のインストールのパスをベンダーパスに設定します (または Composer を使用して Flight をインストール)。
  5. アプリフォルダのパスを設定し、フォルダ構造を作成します (プラグインがこれを支援します!)。
  6. Flight アプリケーションの構築を開始します!

使用例

基本的なルート例

アプリの app/config/routes.php ファイルで:

Flight::route('GET /api/hello', function() {
    Flight::json(['message' => 'Hello World!']);
});

コントローラ例

app/controllers/ApiController.php にコントローラを作成します:

namespace app\controllers;

use Flight;

class ApiController {
    public function getUsers() {
        // Flight 内で WordPress 関数を使用できます!
        $users = get_users();
        $result = [];
        foreach($users as $user) {
            $result[] = [
                'id' => $user->ID,
                'name' => $user->display_name,
                'email' => $user->user_email
            ];
        }
        Flight::json($result);
    }
}

次に、routes.php で:

Flight::route('GET /api/users', [app\controllers\ApiController::class, 'getUsers']);

FAQ

Q: このプラグインを使用するために Flight を知っておく必要がありますか?
A: はい、これは WordPress 内で Flight を使用したい開発者向けです。Flight のルーティングとリクエスト処理の基本的な知識をおすすめします。

Q: これは私の WordPress サイトを遅くしますか?
A: いいえ! プラグインは Flight のルートに一致するリクエストのみ処理します。他のリクエストは通常通り WordPress に渡されます。

Q: Flight アプリで WordPress の関数を使用できますか?
A: もちろんです! Flight のルートとコントローラから WordPress のすべての関数、フック、グローバル変数にアクセスできます。

Q: カスタムルートを作成するにはどうしたらいいですか?
A: アプリフォルダ内の config/routes.php ファイルでルートを定義します。フォルダ構造ジェネレータで作成されたサンプルファイルを参考にしてください。

変更履歴

1.0.0
初回リリース。


詳細については、GitHub repo を確認してください。

Awesome-plugins/ghost_session

Ghostff/Session

PHP セッションマネージャー(非ブロッキング、フラッシュ、セグメント、セッション暗号化)。PHP open_ssl を使用してセッション データのオプションの暗号化/復号化をサポートします。File, MySQL, Redis, and Memcached をサポートします。

こちらをクリックしてコードを表示します。

インストール

Composer でインストールします。

composer require ghostff/session

基本的な構成

デフォルトの設定を使用するには何も渡す必要はありません。詳細な設定については、Github Readmeを参照してください。

use Ghostff\Session\Session;

require 'vendor/autoload.php';

$app = Flight::app();

$app->register('session', Session::class);

// 各ページの読み込みでセッションをコミットする必要がありますことを覚えておいてください
// または、構成で auto_commit を実行する必要があります。

簡単な例

これがこの使用方法の簡単な例です。

Flight::route('POST /login', function() {
    $session = Flight::session();

    // ここでログインのロジックを実行します
    // パスワードを検証するなど。

    // ログインに成功した場合
    $session->set('is_logged_in', true);
    $session->set('user', $user);

    // セッションに書き込んだら、意図的にコミットする必要があります。
    $session->commit();
});

// このチェックは制限されたページのロジックで実行するか、ミドルウェアでラップできます。
Flight::route('/some-restricted-page', function() {
    $session = Flight::session();

    if(!$session->get('is_logged_in')) {
        Flight::redirect('/login');
    }

    // ここで制限されたページのロジックを実行します
});

// ミドルウェア版
Flight::route('/some-restricted-page', function() {
    // 通常のページロジック
})->addMiddleware(function() {
    $session = Flight::session();

    if(!$session->get('is_logged_in')) {
        Flight::redirect('/login');
    }
});

より複雑な例

これがこの使用方法のより複雑な例です。

use Ghostff\Session\Session;

require 'vendor/autoload.php';

$app = Flight::app();

// 最初の引数としてセッション構成ファイルのカスタムパスを設定します
// または、カスタム配列を与えます
$app->register('session', Session::class, [ 
    [
        // セッション データをデータベースに保存したい場合(例: 「すべてのデバイスからログアウト」機能)
        Session::CONFIG_DRIVER        => Ghostff\Session\Drivers\MySql::class,
        Session::CONFIG_ENCRYPT_DATA  => true,
        Session::CONFIG_SALT_KEY      => hash('sha256', 'my-super-S3CR3T-salt'), // これは別のものに変更してください
        Session::CONFIG_AUTO_COMMIT   => true, // これは必要で、commit() が難しい場合のみ実行してください。
                                                // さらに、Flight::after('start', function() { Flight::session()->commit(); }); を実行できます。
        Session::CONFIG_MYSQL_DS         => [
            'driver'    => 'mysql',             # Database driver for PDO dns eg(mysql:host=...;dbname=...)
            'host'      => '127.0.0.1',         # Database host
            'db_name'   => 'my_app_database',   # Database name
            'db_table'  => 'sessions',          # Database table
            'db_user'   => 'root',              # Database username
            'db_pass'   => '',                  # Database password
            'persistent_conn'=> false,          # スクリプトがデータベースにアクセスするたびに新しい接続を確立するオーバーヘッドを避ける。詳細は自分で確認してください
        ]
    ] 
]);

助けて! 私のセッションデータが持続しません!

セッションデータを設定してもリクエスト間で持続しない場合、セッションデータをコミットすることを忘れている可能性があります。$session->commit() を呼び出して設定した後でこれを実行してください。

Flight::route('POST /login', function() {
    $session = Flight::session();

    // ここでログインのロジックを実行します
    // パスワードを検証するなど。

    // ログインに成功した場合
    $session->set('is_logged_in', true);
    $session->set('user', $user);

    // セッションに書き込んだら、意図的にコミットする必要があります。
    $session->commit();
});

これを回避する方法として、セッション サービスを設定するときに構成で auto_committrue に設定します。これにより、各リクエスト後にセッションデータが自動的にコミットされます。

$app->register('session', Session::class, [ 'path/to/session_config.php', bin2hex(random_bytes(32)) ], function(Session $session) {
        $session->updateConfiguration([
            Session::CONFIG_AUTO_COMMIT   => true,
        ]);
    }
);

さらに、Flight::after('start', function() { Flight::session()->commit(); }); を実行して各リクエスト後にセッションデータをコミットすることもできます。

ドキュメント

完全なドキュメントについては、Github Readmeを訪問してください。構成オプションはdefault_config.php ファイル自体でよく文書化されています。コードは自分で確認したい場合に簡単に理解できます。

Awesome-plugins/mcp

FlightPHP MCP Server

FlightPHP MCP Server は、MCP 互換の AI コーディングアシスタントに、FlightPHP のドキュメント全体(ルーティング、ミドルウェア、プラグイン、ガイドなど)への即時で構造化されたアクセスを提供します。AI が API の詳細を幻覚したり、メソッドシグネチャを推測したりする代わりに、必要に応じて実際のドキュメントを取得します。API キー不要、ホスト版ではインストールも不要です。

完全なソースコードと詳細については、Github repository をご覧ください。

Quick Start

サーバーは公開ホストされており、すぐに使用可能です:

https://mcp.flightphp.com/mcp

この URL を AI コーディング拡張機能に追加するだけです。サインアップ不要、認証情報不要。人気のツール向けのコピー&ペースト設定については、以下の IDE Configuration セクションを参照してください。

What It Does

接続後、AI アシスタントは以下の操作が可能です:

Key Points

IDE / AI Extension Configuration

サーバーは Streamable HTTP トランスポートを使用します。以下の拡張機能を選択して設定を貼り付けてください。

Claude Code (CLI)

プロジェクトに追加するには、以下のコマンドを実行します:

claude mcp add --transport http flightphp-docs https://mcp.flightphp.com/mcp

または、プロジェクトの .mcp.json に手動で追加します:

{
  "mcpServers": {
    "flightphp-docs": {
      "type": "http",
      "url": "https://mcp.flightphp.com/mcp"
    }
  }
}

GitHub Copilot (VS Code)

ワークスペースの .vscode/mcp.json に追加します:

{
  "servers": {
    "flightphp-docs": {
      "type": "http",
      "url": "https://mcp.flightphp.com/mcp"
    }
  }
}

Kilo Code (VS Code)

VS Code の settings.json に追加します:

{
  "kilocode.mcpServers": {
    "flightphp-docs": {
      "url": "https://mcp.flightphp.com/mcp",
      "transport": "streamable-http"
    }
  }
}

Continue.dev (VS Code / JetBrains)

~/.continue/config.json に追加します:

{
  "mcpServers": [
    {
      "name": "flightphp-docs",
      "transport": {
        "type": "http",
        "url": "https://mcp.flightphp.com/mcp"
      }
    }
  ]
}

Available Tools

MCP サーバーは、AI アシスタントに以下のツールを公開します:

Tool Description
list_docs_pages すべての利用可能なコアドキュメントトピックをスラッグと説明付きでリストアップ
get_docs_page トピックスラッグ(例: routingmiddlewaresecurity)でコアドキュメントページを取得
list_guide_pages すべての利用可能なステップバイステップガイドをリストアップ
get_guide_page スラッグ(例: blogunit-testing)で完全なガイドを取得
list_plugin_pages すべての利用可能なプラグインと拡張ページをリストアップ
get_plugin_docs スラッグ(例: active-recordsessionjwt)で完全なプラグインドキュメントを取得
search_docs すべてのドキュメント、ガイド、プラグイン全体でキーワードやトピックを検索
fetch_url 完全な docs.flightphp.com URL で任意のページを直接取得

Self-Hosting

独自のインスタンスを実行したいですか? PHP >= 8.1 と Composer が必要です。

git clone https://github.com/flightphp/mcp.git
cd mcp
composer install
php server.php

サーバーはデフォルトで http://0.0.0.0:8890/mcp で開始します。IDE 設定をローカルアドレスに更新します:

{
  "mcpServers": {
    "flightphp-docs": {
      "type": "http",
      "url": "http://localhost:8890/mcp"
    }
  }
}

Awesome-plugins/async

Async

Async は、Flight フレームワーク用の小さなパッケージで、Swoole、AdapterMan、ReactPHP、Amp、RoadRunner、Workerman などの非同期サーバーおよびランタイム内で Flight アプリを実行できるようにします。デフォルトで Swoole と AdapterMan のアダプターが含まれています。

目標:PHP-FPM(または組み込みサーバー)で開発およびデバッグを行い、本番環境では最小限の変更で Swoole(または他の非同期ドライバー)に切り替えることです。

要件

インストール

Composer を使用してインストールします:

composer require flightphp/async

Swoole で実行する予定の場合、拡張をインストールします:

# pecl を使用
pecl install swoole
# または openswoole
pecl install openswoole

# またはパッケージマネージャー(Debian/Ubuntu の例)
sudo apt-get install php-swoole

Swoole の簡単な例

以下は、PHP-FPM(または組み込みサーバー)と Swoole の両方を同じコードベースでサポートする方法を示す最小限のセットアップです。

プロジェクトで必要なファイル:

index.php

このファイルは、開発時にアプリを PHP モードで実行するように強制するシンプルなスイッチです。

// index.php
<?php

define('NOT_SWOOLE', true);

include 'swoole_server.php';

swoole_server.php

このファイルは Flight アプリをブートストラップし、NOT_SWOOLE が定義されていない場合に Swoole ドライバーを開始します。

// swoole_server.php
<?php

require_once __DIR__ . '/vendor/autoload.php';

$app = Flight::app();

$app->route('/', function() use ($app) {
    $app->json(['hello' => 'world']);
});

if (!defined('NOT_SWOOLE')) {
    // Swoole モードで実行する場合に SwooleServerDriver クラスを require します。
    require_once __DIR__ . '/SwooleServerDriver.php';

    Swoole\Runtime::enableCoroutine();
    $Swoole_Server = new SwooleServerDriver('127.0.0.1', 9501, $app);
    $Swoole_Server->start();
} else {
    $app->start();
}

SwooleServerDriver.php

AsyncBridge と Swoole アダプターを使用して Swoole リクエストを Flight にブリッジする方法を示す簡潔なドライバーです。

// SwooleServerDriver.php
<?php

use flight\adapter\SwooleAsyncRequest;
use flight\adapter\SwooleAsyncResponse;
use flight\AsyncBridge;
use flight\Engine;
use Swoole\HTTP\Server as SwooleServer;
use Swoole\HTTP\Request as SwooleRequest;
use Swoole\HTTP\Response as SwooleResponse;

class SwooleServerDriver {
    protected $Swoole;
    protected $app;

    public function __construct(string $host, int $port, Engine $app) {
        $this->Swoole = new SwooleServer($host, $port);
        $this->app = $app;

        $this->setDefault();
        $this->bindWorkerEvents();
        $this->bindHttpEvent();
    }

    protected function setDefault() {
        $this->Swoole->set([
            'daemonize'             => false,
            'dispatch_mode'         => 1,
            'max_request'           => 8000,
            'open_tcp_nodelay'      => true,
            'reload_async'          => true,
            'max_wait_time'         => 60,
            'enable_reuse_port'     => true,
            'enable_coroutine'      => true,
            'http_compression'      => false,
            'enable_static_handler' => true,
            'document_root'         => __DIR__,
            'static_handler_locations' => ['/css', '/js', '/images', '/.well-known'],
            'buffer_output_size'    => 4 * 1024 * 1024,
            'worker_num'            => 4,
        ]);

        $app = $this->app;
        $app->map('stop', function (?int $code = null) use ($app) {
            if ($code !== null) {
                $app->response()->status($code);
            }
        });
    }

    protected function bindHttpEvent() {
        $app = $this->app;
        $AsyncBridge = new AsyncBridge($app);

        $this->Swoole->on('Start', function(SwooleServer $server) {
            echo "Swoole http server is started at http://127.0.0.1:9501\n";
        });

        $this->Swoole->on('Request', function (SwooleRequest $request, SwooleResponse $response) use ($AsyncBridge) {
            $SwooleAsyncRequest = new SwooleAsyncRequest($request);
            $SwooleAsyncResponse = new SwooleAsyncResponse($response);

            $AsyncBridge->processRequest($SwooleAsyncRequest, $SwooleAsyncResponse);

            $response->end();
            gc_collect_cycles();
        });
    }

    protected function bindWorkerEvents() {
        $createPools = function() {
            // ここでワーカー固有の接続プールを作成します
        };
        $closePools = function() {
            // ここでプールを閉じてクリーンアップします
        };
        $this->Swoole->on('WorkerStart', $createPools);
        $this->Swoole->on('WorkerStop', $closePools);
        $this->Swoole->on('WorkerError', $closePools);
    }

    public function start() {
        $this->Swoole->start();
    }
}

サーバーの実行

ヒント:本番環境では、TLS、静的ファイル、負荷分散を処理するために Swoole の前にリバースプロキシ(Nginx)を使用してください。

設定の注意点

Swoole ドライバーはいくつかの設定オプションを公開しています:

これらをホストのリソースとトラフィックパターンに合わせて調整してください。

エラーハンドリング

AsyncBridge は Flight のエラーを適切な HTTP レスポンスに変換します。ルートレベルのエラーハンドリングも追加できます:

$app->route('/*', function() use ($app) {
    try {
        // ルートロジック
    } catch (Exception $e) {
        $app->response()->status(500);
        $app->json(['error' => $e->getMessage()]);
    }
});

AdapterMan および他のランタイム

AdapterMan は代替ランタイムアダプターとしてサポートされています。このパッケージは適応性が高く設計されており、他のアダプターを追加または使用する場合も、通常同じパターンを踏襲します:サーバーリクエスト/レスポンスを AsyncBridge とランタイム固有のアダプター経由で Flight のリクエスト/レスポンスに変換します。

Awesome-plugins/migrations

マイグレーション

プロジェクトのマイグレーションは、プロジェクトに関与するすべてのデータベース変更を追跡します。 byjg/php-migration は、あなたが始めるのに非常に役立つコアライブラリです。

インストール

PHP ライブラリ

プロジェクトで PHP ライブラリのみを使用したい場合:

composer require "byjg/migration"

コマンドラインインターフェース

コマンドラインインターフェースはスタンドアロンであり、プロジェクトにインストールする必要はありません。

グローバルにインストールし、シンボリックリンクを作成できます。

composer require "byjg/migration-cli"

マイグレーション CLI に関する詳細情報は、byjg/migration-cliをご覧ください。

サポートされているデータベース

データベース ドライバー 接続文字列
Sqlite pdo_sqlite sqlite:///path/to/file
MySql/MariaDb pdo_mysql mysql://username:password@hostname:port/database
Postgres pdo_pgsql pgsql://username:password@hostname:port/database
Sql Server pdo_dblib, pdo_sysbase Linux dblib://username:password@hostname:port/database
Sql Server pdo_sqlsrv Windows sqlsrv://username:password@hostname:port/database

どのように機能しますか?

データベースマイグレーションは、データベースのバージョン管理に純粋な SQL を使用します。 機能させるためには、次のことを行う必要があります。

SQL スクリプト

スクリプトは、3 つのセットのスクリプトに分かれています。

スクリプトディレクトリは次のとおりです:

 <root dir>
     |
     +-- base.sql
     |
     +-- /migrations
              |
              +-- /up
                   |
                   +-- 00001.sql
                   +-- 00002.sql
              +-- /down
                   |
                   +-- 00000.sql
                   +-- 00001.sql

マルチ開発環境

複数の開発者や複数のブランチで作業する場合、次の番号を特定するのは難しいです。

その場合、バージョン番号の後にサフィックス "-dev" を付けます。

シナリオを見てみましょう:

どちらの場合も、開発者は 43-dev.sql というファイルを作成します。 両方の開発者は UP と DOWN を問題なく移行し、あなたのローカルバージョンは 43 になります。

しかし、開発者 1 が変更をマージし、最終バージョン 43.sql を作成しました(git mv 43-dev.sql 43.sql)。 開発者 2 がローカルブランチを更新すると、彼はファイル 43.sql(dev 1 から)とファイル 43-dev.sql を持ちます。 彼が UP または DOWN に移行しようとすると、マイグレーションスクリプトはダウンし、バージョンが 43 の 2 つが存在することを警告します。その場合、開発者 2 はファイルを 44-dev.sql に更新し、変更をマージして最終バージョンを生成するまで作業を続行しなければなりません。

PHP API を使用してプロジェクトに統合する

基本的な使用法は

例えばを見る:

<?php
// 接続 URI を作成
// 詳細: https://github.com/byjg/anydataset#connection-based-on-uri
$connectionUri = new \ByJG\Util\Uri('mysql://migrateuser:migratepwd@localhost/migratedatabase');

// データベースまたはデータベースをその URI で処理するように登録します:
\ByJG\DbMigration\Migration::registerDatabase(\ByJG\DbMigration\Database\MySqlDatabase::class);

// マイグレーションインスタンスを作成する
$migration = new \ByJG\DbMigration\Migration($connectionUri, '.');

// 実行からの情報を受け取るためにコールバック進捗関数を追加します
$migration->addCallbackProgress(function ($action, $currentVersion, $fileInfo) {
    echo "$action, $currentVersion, ${fileInfo['description']}\n";
});

// "base.sql" スクリプトを使用してデータベースを復元し
// データベースのバージョンを最新バージョンまでアップグレードするためのすべてのスクリプトを実行します
$migration->reset();

// 現在のバージョンから $version 番号までのデータベースのバージョンのためのすべてのスクリプトを実行します;
// バージョン番号が指定されていない場合、最後のデータベースバージョンまで移行します
$migration->update($version = null);

マイグレーションオブジェクトは、データベースのバージョンを制御します。

プロジェクト内のバージョン管理作成

<?php
// データベースまたはデータベースをその URI で処理するように登録します:
\ByJG\DbMigration\Migration::registerDatabase(\ByJG\DbMigration\Database\MySqlDatabase::class);

// マイグレーションインスタンスを作成する
$migration = new \ByJG\DbMigration\Migration($connectionUri, '.');

// このコマンドは、データベース内にバージョンテーブルを作成します
$migration->createVersion();

現在のバージョンの取得

<?php
$migration->getCurrentVersion();

コールバックを追加して進捗を制御

<?php
$migration->addCallbackProgress(function ($command, $version, $fileInfo) {
    echo "コマンドを実行中: $command バージョン $version - ${fileInfo['description']}, ${fileInfo['exists']}, ${fileInfo['file']}, ${fileInfo['checksum']}\n";
});

Db ドライバーインスタンスの取得

<?php
$migration->getDbDriver();

使用するには、次を訪れてください: https://github.com/byjg/anydataset-db

部分的なマイグレーションを避ける(MySQL では利用できません)

部分的なマイグレーションは、エラーや手動の中断によりマイグレーションスクリプトがプロセスの途中で中断される場合です。

マイグレーションテーブルは partial up または partial down の状態になり、再度移行できるようになる前に手動で修正する必要があります。

この状況を避けるために、マイグレーションがトランザクショナルコンテキストで実行されることを指定できます。 マイグレーションスクリプトが失敗すると、トランザクションはロールバックされ、マイグレーションテーブルは complete とマークされ、 バージョンはエラーを引き起こしたスクリプトの直前のバージョンになります。

この機能を有効にするには、withTransactionEnabled メソッドを呼び出して、true をパラメータとして渡す必要があります:

<?php
$migration->withTransactionEnabled(true);

注: この機能は、MySQL では利用できません。DDL コマンドをトランザクション内でサポートしていないためです。 このメソッドを MySQL で使用した場合、マイグレーションは静かに無視します。 詳細情報: https://dev.mysql.com/doc/refman/8.0/en/cannot-roll-back.html

Postgres 用の SQL マイグレーションを書く際のヒント

トリガーと SQL 関数の作成時

-- DO
CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$
    BEGIN
        -- empname と salary が指定されていることを確認します
        IF NEW.empname IS NULL THEN
            RAISE EXCEPTION 'empname cannot be null'; -- これらのコメントが空でも関係ありません
        END IF; --
        IF NEW.salary IS NULL THEN
            RAISE EXCEPTION '% cannot have null salary', NEW.empname; --
        END IF; --

        -- 誰が私たちのために働いているのか、彼らはそれのために支払わなければなりませんか?
        IF NEW.salary < 0 THEN
            RAISE EXCEPTION '% cannot have a negative salary', NEW.empname; --
        END IF; --

        -- 誰が給与を変更したのかを記憶して
        NEW.last_date := current_timestamp; --
        NEW.last_user := current_user; --
        RETURN NEW; --
    END; --
$emp_stamp$ LANGUAGE plpgsql;


-- DON'T
CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$
    BEGIN
        -- empname と salary が指定されていることを確認します
        IF NEW.empname IS NULL THEN
            RAISE EXCEPTION 'empname cannot be null';
        END IF;
        IF NEW.salary IS NULL THEN
            RAISE EXCEPTION '% cannot have null salary', NEW.empname;
        END IF;

        -- 誰が私たちのために働いているのか、彼らはそれのために支払わなければなりませんか?
        IF NEW.salary < 0 THEN
            RAISE EXCEPTION '% cannot have a negative salary', NEW.empname;
        END IF;

        -- 誰が給与を変更したのかを記憶して
        NEW.last_date := current_timestamp;
        NEW.last_user := current_user;
        RETURN NEW;
    END;
$emp_stamp$ LANGUAGE plpgsql;

PDO データベース抽象層は SQL ステートメントのバッチを実行できないため、byjg/migration がマイグレーションファイルを読み込むと、ファイルの内容全体をセミコロンで分割し、ステートメントを個別に実行する必要があります。ただし、1 つの種類のステートメントはその本体の間に複数のセミコロンを持つことがあります: 関数です。

関数を正しく解析できるようにするために、byjg/migration 2.1.0 からマイグレーションファイルを セミコロン + EOL シーケンスで分割するようになりました。これにより、関数定義の各内部セミコロンの後に空のコメントを追加すると、byjg/migration がそれを解析できるようになります。

不幸にも、これらのコメントのいずれかを追加するのを忘れると、ライブラリは CREATE FUNCTION ステートメントを複数の部分に分割し、マイグレーションは失敗します。

コロン文字(:)を避ける

-- DO
CREATE TABLE bookings (
  booking_id UUID PRIMARY KEY,
  booked_at  TIMESTAMPTZ NOT NULL CHECK (CAST(booked_at AS DATE) <= check_in),
  check_in   DATE NOT NULL
);


-- DON'T
CREATE TABLE bookings (
  booking_id UUID PRIMARY KEY,
  booked_at  TIMESTAMPTZ NOT NULL CHECK (booked_at::DATE <= check_in),
  check_in   DATE NOT NULL
);

PDO は名前付きパラメータのプレフィックスとしてコロン文字を使用するため、他のコンテキストでの使用は問題を引き起こします。

例えば、PostgreSQL ステートメントは :: を使用して型の間で値をキャストできます。 一方で、PDO はこれを無効な名前付きパラメータとして解釈し、無効なコンテキストで失敗します。

この不一致を修正する唯一の方法は、コロンを完全に避けることです(この場合、PostgreSQL にも代替構文があります: CAST(value AS type))。

SQL エディタを使用する

最後に、手動で SQL マイグレーションを書くことは面倒ですが、SQL 構文を理解し、オートコンプリートを提供し、現在のデータベーススキーマを調査しているエディタを使用すれば、格段に簡単になります。

1 つのスキーマ内での異なるマイグレーションの処理

同じスキーマ内で異なるマイグレーションスクリプトやバージョンを作成する必要がある場合、可能ですがリスクが高く、私は 全く推奨しません

これを行うには、コンストラクタにパラメータを渡して異なる "マイグレーションテーブル" を作成する必要があります。

<?php
$migration = new \ByJG\DbMigration\Migration("db:/uri", "/path", true, "NEW_MIGRATION_TABLE_NAME");

セキュリティ上の理由から、この機能はコマンドラインでは利用できませんが、環境変数 MIGRATION_VERSION を使用して名前を保存できます。

この機能を使用しないことを強く推奨します。推奨は、1 つのスキーマにつき 1 つのマイグレーションです。

ユニットテストの実行

基本的なユニットテストは次のように実行できます:

vendor/bin/phpunit

データベーステストの実行

統合テストを実行するには、データベースを立ち上げておく必要があります。基本的な docker-compose.yml を提供しており、テストのためにデータベースを起動する際に使用できます。

データベースの実行

docker-compose up -d postgres mysql mssql

テストを実行

vendor/bin/phpunit
vendor/bin/phpunit tests/SqliteDatabase*
vendor/bin/phpunit tests/MysqlDatabase*
vendor/bin/phpunit tests/PostgresDatabase*
vendor/bin/phpunit tests/SqlServerDblibDatabase*
vendor/bin/phpunit tests/SqlServerSqlsrvDatabase*

オプションとして、ユニットテストで使用されるホストとパスワードを設定できます。

export MYSQL_TEST_HOST=localhost     # デフォルトは localhost
export MYSQL_PASSWORD=newpassword    # null パスワードが必要な場合は '.' を使用
export PSQL_TEST_HOST=localhost      # デフォルトは localhost
export PSQL_PASSWORD=newpassword     # null パスワードが必要な場合は '.' を使用
export MSSQL_TEST_HOST=localhost     # デフォルトは localhost
export MSSQL_PASSWORD=Pa55word
export SQLITE_TEST_HOST=/tmp/test.db      # デフォルトは /tmp/test.db

Awesome-plugins/comment_template

CommentTemplate

CommentTemplate は、強力な PHP テンプレートエンジンで、アセットのコンパイル、テンプレートの継承、変数の処理を備えています。ビルトインの CSS/JS 最小化とキャッシュにより、シンプルで柔軟なテンプレート管理を提供します。

機能

インストール

Composer を使用してインストールします。

composer require knifelemon/comment-template

基本設定

開始するための基本的な設定オプションがあります。これらについての詳細は CommentTemplate Repo を参照してください。

方法 1: コールバック関数を使用

<?php
require_once 'vendor/autoload.php';

use KnifeLemon\CommentTemplate\Engine;

$app = Flight::app();

$app->register('view', Engine::class, [], function (Engine $engine) use ($app) {
    // ルートディレクトリ(index.php が存在する場所) - Web アプリケーションのドキュメントルート
    $engine->setPublicPath(__DIR__);

    // テンプレートファイルのディレクトリ - 相対パスと絶対パスの両方をサポート
    $engine->setSkinPath('views');             // パブリックパスに対する相対パス

    // コンパイルされたアセットの保存場所 - 相対パスと絶対パスの両方をサポート
    $engine->setAssetPath('assets');           // パブリックパスに対する相対パス

    // テンプレートファイルの拡張子
    $engine->setFileExtension('.php');
});

$app->map('render', function(string $template, array $data) use ($app): void {
    echo $app->view()->render($template, $data);
});

方法 2: コンストラクタパラメータを使用

<?php
require_once 'vendor/autoload.php';

use KnifeLemon\CommentTemplate\Engine;

$app = Flight::app();

// __construct(string $publicPath = "", string $skinPath = "", string $assetPath = "", string $fileExtension = "")
$app->register('view', Engine::class, [
    __DIR__,                // publicPath - ルートディレクトリ(index.php が存在する場所)
    'views',                // skinPath - テンプレートパス(相対/絶対をサポート)
    'assets',               // assetPath - コンパイルされたアセットパス(相対/絶対をサポート)
    '.php'                  // fileExtension - テンプレートファイルの拡張子
]);

$app->map('render', function(string $template, array $data) use ($app): void {
    echo $app->view()->render($template, $data);
});

Tracy デバッガー統合

CommentTemplateは開発ログとデバッグのためにTracy Debuggerとの統合を含んでいます。

Comment Template Tracy

インストール

composer require tracy/tracy

使用方法

<?php
use KnifeLemon\CommentTemplate\Engine;
use Tracy\Debugger;

// Tracyを有効にする(出力前に呼び出す必要があります)
Debugger::enable(Debugger::DEVELOPMENT);
Flight::set('flight.content_length', false);

// テンプレートのオーバーライド
$app->register('view', Engine::class, [], function (Engine $builder) use ($app) {
    $builder->setPublicPath($app->get('flight.views.topPath'));
    $builder->setAssetPath($app->get('flight.views.assetPath'));
    $builder->setSkinPath($app->get('flight.views.path'));
    $builder->setFileExtension($app->get('flight.views.extension'));
});
$app->map('render', function(string $template, array $data) use ($app): void {
    echo $app->view()->render($template, $data);
});

$app->start();

デバッグパネル機能

CommentTemplateは4つのタブを持つカスタムパネルをTracyのデバッグバーに追加します:

ログに記録される内容

注意: Tracyがインストールされていないか無効になっている場合、パフォーマンスへの影響はゼロです。

Flight PHPでの完全な動作例を参照してください。

テンプレートディレクティブ

レイアウト継承

共通の構造を作成するためにレイアウトを使用します:

layout/global_layout.php:

<!DOCTYPE html>
<html>
<head>
    <title>{$title}</title>
</head>
<body>
    <!--@contents-->
</body>
</html>

view/page.php:

<!--@layout(layout/global_layout)-->
<h1>{$title}</h1>
<p>{$content}</p>

アセット管理

CSS ファイル

<!--@css(/css/styles.css)-->          <!-- 最小化されキャッシュされる -->
<!--@cssSingle(/css/critical.css)-->  <!-- 単一ファイル、最小化されない -->

JavaScript ファイル

CommentTemplate は異なる JavaScript 読み込み戦略をサポートします:

<!--@js(/js/script.js)-->             <!-- 最小化され、ボトムで読み込まれる -->
<!--@jsAsync(/js/analytics.js)-->     <!-- 最小化され、ボトムで async で読み込まれる -->
<!--@jsDefer(/js/utils.js)-->         <!-- 最小化され、ボトムで defer で読み込まれる -->
<!--@jsTop(/js/critical.js)-->        <!-- 最小化され、head で読み込まれる -->
<!--@jsTopAsync(/js/tracking.js)-->   <!-- 最小化され、head で async で読み込まれる -->
<!--@jsTopDefer(/js/polyfill.js)-->   <!-- 最小化され、head で defer で読み込まれる -->
<!--@jsSingle(/js/widget.js)-->       <!-- 単一ファイル、最小化されない -->
<!--@jsSingleAsync(/js/ads.js)-->     <!-- 単一ファイル、最小化されない、async -->
<!--@jsSingleDefer(/js/social.js)-->  <!-- 単一ファイル、最小化されない、defer -->

CSS/JS ファイル内のアセットディレクティブ

CommentTemplate は、コンパイル中に CSS と JavaScript ファイル内のアセットディレクティブも処理します:

CSS の例:

/* あなたの CSS ファイル内で */
@font-face {
    font-family: 'CustomFont';
    src: url('<!--@asset(fonts/custom.woff2)-->') format('woff2');
}

.background-image {
    background: url('<!--@asset(images/bg.jpg)-->');
}

.inline-icon {
    background: url('<!--@base64(icons/star.svg)-->');
}

JavaScript の例:

/* あなたの JS ファイル内で */
const fontUrl = '<!--@asset(fonts/custom.woff2)-->';
const imageData = '<!--@base64(images/icon.png)-->';

Base64 エンコーディング

<!--@base64(images/logo.png)-->       <!-- データ URI としてインライン -->

例:

<!-- 小さな画像をデータ URI としてインライン化して高速読み込み -->
<img src="<!--@base64(images/logo.png)-->" alt="Logo">
<div style="background-image: url('<!--@base64(icons/star.svg)-->');">
    背景としての小さなアイコン
</div>

アセットのコピー

<!--@asset(images/photo.jpg)-->       <!-- 単一アセットをパブリックディレクトリにコピー -->
<!--@assetDir(assets)-->              <!-- 全体のディレクトリをパブリックディレクトリにコピー -->

例:

<!-- 静的アセットをコピーして参照 -->
<img src="<!--@asset(images/hero-banner.jpg)-->" alt="Hero Banner">
<a href="<!--@asset(documents/brochure.pdf)-->" download>ブロシュアをダウンロード</a>

<!-- 全体のディレクトリ(フォント、アイコンなど)をコピー -->
<!--@assetDir(assets/fonts)-->
<!--@assetDir(assets/icons)-->

テンプレートのインクルード

<!--@import(components/header)-->     <!-- 他のテンプレートを含める -->

例:

<!-- 再利用可能なコンポーネントを含める -->
<!--@import(components/header)-->

<main>
    <h1>当社のウェブサイトへようこそ</h1>
    <!--@import(components/sidebar)-->

    <div class="content">
        <p>ここにメインコンテンツ...</p>
    </div>
</main>

<!--@import(components/footer)-->

変数の処理

基本変数

<h1>{$title}</h1>
<p>{$description}</p>

変数フィルター

{$title|upper}                       <!-- 大文字に変換 -->
{$content|lower}                     <!-- 小文字に変換 -->
{$html|striptag}                     <!-- HTML タグを除去 -->
{$text|escape}                       <!-- HTML をエスケープ -->
{$multiline|nl2br}                   <!-- 改行を <br> に変換 -->
{$html|br2nl}                        <!-- <br> タグを改行に変換 -->
{$description|trim}                  <!-- 空白をトリム -->
{$subject|title}                     <!-- タイトルケースに変換 -->

変数コマンド

{$title|default=Default Title}       <!-- デフォルト値を設定 -->
{$name|concat= (Admin)}              <!-- テキストを連結 -->

変数コマンド

{$content|striptag|trim|escape}      <!-- 複数のフィルターをチェーン -->

コメント

テンプレートコメントは出力から完全に削除され、最終的な HTML に表示されません:

{* これは1行のテンプレートコメントです *}

{* 
   これは複数行の 
   テンプレートコメントです 
   複数行にわたります
*}

<h1>{$title}</h1>
{* デバッグコメント: title 変数が動作するかを確認 *}
<p>{$content}</p>

注意: テンプレートコメント {* ... *} は HTML コメント <!-- ... --> と異なります。テンプレートコメントは処理中に削除され、ブラウザに到達しません。

例のプロジェクト構造

project/
├── source/
│   ├── layouts/
│   │   └── default.php
│   ├── components/
│   │   ├── header.php
│   │   └── footer.php
│   ├── css/
│   │   ├── bootstrap.min.css
│   │   └── custom.css
│   ├── js/
│   │   ├── app.js
│   │   └── bootstrap.min.js
│   └── homepage.php
├── public/
│   └── assets/           # 生成されたアセット
│       ├── css/
│       └── js/
└── vendor/

Awesome-plugins/easy_query

EasyQuery

knifelemon/easy-query は、軽量で流暢な SQL クエリビルダーであり、プリペアドステートメント用の SQL とパラメータを生成します。SimplePdo と連携します。

機能

インストール

composer require knifelemon/easy-query

クイックスタート

use KnifeLemon\EasyQuery\Builder;

$q = Builder::table('users')
    ->select(['id', 'name', 'email'])
    ->where(['status' => 'active'])
    ->orderBy('created_at DESC')
    ->limit(10)
    ->build();

// FlightのSimplePdoと使用
$users = Flight::db()->fetchAll($q['sql'], $q['params']);

build() の理解

build() メソッドは、sqlparams を持つ配列を返します。この分離により、プリペアドステートメントを使用してデータベースを安全に保ちます。

$q = Builder::table('users')
    ->where(['email' => 'user@example.com'])
    ->build();

// 返される値:
// [
//     'sql' => 'SELECT * FROM users WHERE email = ?',
//     'params' => ['user@example.com']
// ]

クエリタイプ

SELECT

// すべての列を選択
$q = Builder::table('users')->build();
// SELECT * FROM users

// 特定の列を選択
$q = Builder::table('users')
    ->select(['id', 'name', 'email'])
    ->build();
// SELECT id, name, email FROM users

// テーブル別名付き
$q = Builder::table('users')
    ->alias('u')
    ->select(['u.id', 'u.name'])
    ->build();
// SELECT u.id, u.name FROM users AS u

INSERT

$q = Builder::table('users')
    ->insert([
        'name' => 'John Doe',
        'email' => 'john@example.com',
        'status' => 'active'
    ])
    ->build();
// INSERT INTO users SET name = ?, email = ?, status = ?

Flight::db()->runQuery($q['sql'], $q['params']);
$userId = Flight::db()->lastInsertId();

UPDATE

$q = Builder::table('users')
    ->update(['status' => 'inactive', 'updated_at' => date('Y-m-d H:i:s')])
    ->where(['id' => 123])
    ->build();
// UPDATE users SET status = ?, updated_at = ? WHERE id = ?

Flight::db()->runQuery($q['sql'], $q['params']);

DELETE

$q = Builder::table('users')
    ->delete()
    ->where(['id' => 123])
    ->build();
// DELETE FROM users WHERE id = ?

Flight::db()->runQuery($q['sql'], $q['params']);

COUNT

$q = Builder::table('users')
    ->count()
    ->where(['status' => 'active'])
    ->build();
// SELECT COUNT(*) AS cnt FROM users WHERE status = ?

$count = Flight::db()->fetchField($q['sql'], $q['params']);

WHERE 条件

単純な等価性

$q = Builder::table('users')
    ->where(['id' => 123, 'status' => 'active'])
    ->build();
// WHERE id = ? AND status = ?

比較演算子

$q = Builder::table('users')
    ->where([
        'age' => ['>=', 18],
        'score' => ['<', 100],
        'name' => ['!=', 'admin']
    ])
    ->build();
// WHERE age >= ? AND score < ? AND name != ?

LIKE

$q = Builder::table('users')
    ->where(['name' => ['LIKE', '%john%']])
    ->build();
// WHERE name LIKE ?

IN / NOT IN

// IN
$q = Builder::table('users')
    ->where(['id' => ['IN', [1, 2, 3, 4, 5]]])
    ->build();
// WHERE id IN (?, ?, ?, ?, ?)

// NOT IN
$q = Builder::table('users')
    ->where(['status' => ['NOT IN', ['banned', 'deleted']]])
    ->build();
// WHERE status NOT IN (?, ?)

BETWEEN

$q = Builder::table('products')
    ->where(['price' => ['BETWEEN', [100, 500]]])
    ->build();
// WHERE price BETWEEN ? AND ?

OR 条件

orWhere() を使用して OR グループ化された条件を追加します:

$q = Builder::table('users')
    ->where(['status' => 'active'])
    ->orWhere([
        'role' => 'admin',
        'permissions' => ['LIKE', '%manage%']
    ])
    ->build();
// WHERE status = ? AND (role = ? OR permissions LIKE ?)

JOIN

INNER JOIN

$q = Builder::table('users')
    ->alias('u')
    ->select(['u.id', 'u.name', 'p.title'])
    ->innerJoin('posts', 'u.id = p.user_id', 'p')
    ->build();
// SELECT u.id, u.name, p.title FROM users AS u INNER JOIN posts AS p ON u.id = p.user_id

LEFT JOIN

$q = Builder::table('users')
    ->alias('u')
    ->select(['u.name', 'o.total'])
    ->leftJoin('orders', 'u.id = o.user_id', 'o')
    ->build();
// ... LEFT JOIN orders AS o ON u.id = o.user_id

複数の JOIN

$q = Builder::table('orders')
    ->alias('o')
    ->select(['o.id', 'u.name AS customer', 'p.title AS product'])
    ->innerJoin('users', 'o.user_id = u.id', 'u')
    ->leftJoin('order_items', 'o.id = oi.order_id', 'oi')
    ->leftJoin('products', 'oi.product_id = p.id', 'p')
    ->where(['o.status' => 'completed'])
    ->build();

並べ替え、グループ化、制限

ORDER BY

$q = Builder::table('users')
    ->orderBy('created_at DESC')
    ->build();
// ORDER BY created_at DESC

GROUP BY

$q = Builder::table('orders')
    ->select(['user_id', 'COUNT(*) as order_count'])
    ->groupBy('user_id')
    ->build();
// SELECT user_id, COUNT(*) as order_count FROM orders GROUP BY user_id

LIMIT と OFFSET

$q = Builder::table('users')
    ->limit(10)
    ->build();
// LIMIT 10

$q = Builder::table('users')
    ->limit(10, 20)  // limit, offset
    ->build();
// LIMIT 10 OFFSET 20

Raw SQL 式

SQL 関数やバインドパラメータとして扱われない式が必要な場合に raw() を使用します。

基本的な Raw

$q = Builder::table('users')
    ->update([
        'login_count' => Builder::raw('login_count + 1'),
        'updated_at' => Builder::raw('NOW()')
    ])
    ->where(['id' => 123])
    ->build();
// SET login_count = login_count + 1, updated_at = NOW()

バインドパラメータ付き Raw

$q = Builder::table('orders')
    ->update([
        'total' => Builder::raw('COALESCE(subtotal, ?) + ?', [0, 10])
    ])
    ->where(['id' => 1])
    ->build();
// SET total = COALESCE(subtotal, ?) + ?
// params: [0, 10, 1]

WHERE 内の Raw (サブクエリ)

$q = Builder::table('products')
    ->where([
        'price' => ['>', Builder::raw('(SELECT AVG(price) FROM products)')]
    ])
    ->build();
// WHERE price > (SELECT AVG(price) FROM products)

ユーザー入力のための安全な識別子

列名がユーザー入力から来る場合、safeIdentifier() を使用して SQL インジェクションを防ぎます:

$sortColumn = $_GET['sort'];  // 例: 'created_at'
$safeColumn = Builder::safeIdentifier($sortColumn);

$q = Builder::table('users')
    ->orderBy($safeColumn . ' DESC')
    ->build();

// ユーザーが試した場合: "name; DROP TABLE users--"
// InvalidArgumentException をスロー

ユーザー提供の列名のための rawSafe

$userColumn = $_GET['aggregate_column'];

$q = Builder::table('orders')
    ->select([
        Builder::rawSafe('SUM({col})', ['col' => $userColumn])->value . ' AS total'
    ])
    ->build();
// 列名を検証し、無効な場合は例外をスロー

警告: ユーザー入力を raw() に直接連結しないでください。常にバインドパラメータまたは safeIdentifier() を使用してください。


クエリビルダーの再利用

クリアメソッド

ビルダーを再利用するために特定の部分をクリアします:

$query = Builder::table('users')
    ->select(['id', 'name'])
    ->where(['status' => 'active'])
    ->orderBy('created_at DESC');

// 最初のクエリ
$q1 = $query->limit(10)->build();

// クリアして再利用
$query->clearWhere()->clearLimit();

// 異なる条件の2番目のクエリ
$q2 = $query
    ->where(['status' => 'pending'])
    ->limit(5)
    ->build();

利用可能なクリアメソッド

メソッド 説明
clearWhere() WHERE 条件とパラメータをクリア
clearSelect() SELECT 列をデフォルトの '*' にリセット
clearJoin() すべての JOIN 句をクリア
clearGroupBy() GROUP BY 句をクリア
clearOrderBy() ORDER BY 句をクリア
clearLimit() LIMIT と OFFSET をクリア
clearAll() ビルダーを初期状態にリセット

ページネーションの例

$baseQuery = Builder::table('users')
    ->select(['id', 'name', 'email'])
    ->where(['status' => 'active'])
    ->orderBy('created_at DESC');

// 合計カウントを取得
$countQuery = clone $baseQuery;
$countResult = $countQuery->clearSelect()->count()->build();
$total = Flight::db()->fetchField($countResult['sql'], $countResult['params']);

// ページネーションされた結果を取得
$page = 1;
$perPage = 20;
$listResult = $baseQuery->limit($perPage, ($page - 1) * $perPage)->build();
$users = Flight::db()->fetchAll($listResult['sql'], $listResult['params']);

動的クエリ構築

$query = Builder::table('products')->alias('p');

if (!empty($categoryId)) {
    $query->where(['p.category_id' => $categoryId]);
}

if (!empty($minPrice)) {
    $query->where(['p.price' => ['>=', $minPrice]]);
}

if (!empty($maxPrice)) {
    $query->where(['p.price' => ['<=', $maxPrice]]);
}

if (!empty($searchTerm)) {
    $query->where(['p.name' => ['LIKE', "%{$searchTerm}%"]]);
}

$result = $query->orderBy('p.created_at DESC')->limit(20)->build();
$products = Flight::db()->fetchAll($result['sql'], $result['params']);

完全な FlightPHP の例

use KnifeLemon\EasyQuery\Builder;

// ページネーション付きのユーザー一覧
Flight::route('GET /users', function() {
    $page = (int) (Flight::request()->query['page'] ?? 1);
    $perPage = 20;

    $q = Builder::table('users')
        ->select(['id', 'name', 'email', 'created_at'])
        ->where(['status' => 'active'])
        ->orderBy('created_at DESC')
        ->limit($perPage, ($page - 1) * $perPage)
        ->build();

    $users = Flight::db()->fetchAll($q['sql'], $q['params']);
    Flight::json(['users' => $users, 'page' => $page]);
});

// ユーザー作成
Flight::route('POST /users', function() {
    $data = Flight::request()->data;

    $q = Builder::table('users')
        ->insert([
            'name' => $data->name,
            'email' => $data->email,
            'created_at' => Builder::raw('NOW()')
        ])
        ->build();

    Flight::db()->runQuery($q['sql'], $q['params']);
    Flight::json(['id' => Flight::db()->lastInsertId()]);
});

// ユーザー更新
Flight::route('PUT /users/@id', function($id) {
    $data = Flight::request()->data;

    $q = Builder::table('users')
        ->update([
            'name' => $data->name,
            'email' => $data->email,
            'updated_at' => Builder::raw('NOW()')
        ])
        ->where(['id' => $id])
        ->build();

    Flight::db()->runQuery($q['sql'], $q['params']);
    Flight::json(['success' => true]);
});

// ユーザー削除
Flight::route('DELETE /users/@id', function($id) {
    $q = Builder::table('users')
        ->delete()
        ->where(['id' => $id])
        ->build();

    Flight::db()->runQuery($q['sql'], $q['params']);
    Flight::json(['success' => true]);
});

API リファレンス

静的メソッド

メソッド 説明
Builder::table(string $table) テーブルの新しいビルダーインスタンスを作成
Builder::raw(string $sql, array $bindings = []) 生の SQL 式を作成
Builder::rawSafe(string $expr, array $identifiers, array $bindings = []) 安全な識別子置換付きの生の式
Builder::safeIdentifier(string $identifier) 列名/テーブル名を検証して安全なものを返す

インスタンスメソッド

メソッド 説明
alias(string $alias) テーブル別名を設定
select(string\|array $columns) 選択する列を設定 (デフォルト: '*')
where(array $conditions) WHERE 条件 (AND) を追加
orWhere(array $conditions) OR WHERE 条件を追加
join(string $table, string $condition, string $alias, string $type) JOIN 句を追加
innerJoin(string $table, string $condition, string $alias) INNER JOIN を追加
leftJoin(string $table, string $condition, string $alias) LEFT JOIN を追加
groupBy(string $groupBy) GROUP BY 句を追加
orderBy(string $orderBy) ORDER BY 句を追加
limit(int $limit, int $offset = 0) LIMIT と OFFSET を追加
count(string $column = '*') クエリを COUNT に設定
insert(array $data) クエリを INSERT に設定
update(array $data) クエリを UPDATE に設定
delete() クエリを DELETE に設定
build() ['sql' => ..., 'params' => ...] を構築して返す
get() build() のエイリアス

Tracy デバッガーの統合

EasyQuery は、インストールされている場合に Tracy デバッガーと自動的に統合します。設定は不要です!

composer require tracy/tracy
use Tracy\Debugger;

Debugger::enable();

// すべてのクエリが Tracy パネルに自動的にログ出力されます
$q = Builder::table('users')->where(['status' => 'active'])->build();

Tracy パネルには以下が表示されます:

完全なドキュメントについては、GitHub リポジトリ をご覧ください。

Awesome-plugins/twig

Twig

Twig は、PHP向けの柔軟で高速かつ安全なテンプレートエンジンです。Symfonyやその他の多くのプロジェクトで使用されているテンプレート言語であり、AIコーディングツールや大多数のPHP開発者がその構文をよく理解しています。Twigはテンプレートを最適化されたPHPにコンパイルし、デフォルトで出力を自動エスケープ(XSS対策に有効)し、フィルタ、関数、拡張機能で簡単に拡張できます。

インストール

composerでインストールします。

composer require twig/twig

基本設定

開始するための基本的な設定オプションがいくつかあります。詳細はTwigドキュメントでご覧いただけます。

require 'vendor/autoload.php';

$app = Flight::app();

$app->map('render', function(string $template, array $data): void {
    $loader = new \Twig\Loader\FilesystemLoader(Flight::get('flight.views.path'));
    $twig = new \Twig\Environment($loader, [
        // Twigがコンパイル済みテンプレートを保存する場所
        'cache' => __DIR__ . '/../cache/twig',
        // ソースが変更されたときにテンプレートを再コンパイル(開発時に便利)
        'auto_reload' => true,
    ]);

    echo $twig->render($template, $data);
});

Twigをビュークラスとして登録する

単一のTwig環境を再利用したい場合(本番環境で推奨)、それを登録してrenderをそれに向けることができます:

require 'vendor/autoload.php';

$app = Flight::app();

$app->register('view', \Twig\Environment::class, [
    new \Twig\Loader\FilesystemLoader($app->get('flight.views.path')),
    [
        'cache' => __DIR__ . '/../cache/twig',
        'auto_reload' => true,
    ],
]);

$app->map('render', function(string $template, array $data): void {
    echo Flight::view()->render($template, $data);
});

シンプルなレイアウト例

これはレイアウトファイルの簡単な例です。このファイルは他のすべてのビューをラップするために使用されます。

{# app/views/layout.twig #}
<!doctype html>
<html lang="en">
    <head>
        <title>{% if title %}{{ title }} - {% endif %}My App</title>
        <link rel="stylesheet" href="style.css">
    </head>
    <body>
        <header>
            <nav>
                {# ここにナビゲーション要素を配置 #}
            </nav>
        </header>
        <div id="content">
            {# ここが魔法です #}
            {% block content %}{% endblock %}
        </div>
        <div id="footer">
            &copy; Copyright
        </div>
    </body>
</html>

そして、コンテンツブロック内にレンダリングされるファイルを作成します:

{# app/views/home.twig #}
{# これはTwigにこのファイルが「layout.twigの中にある」ことを伝えます #}
{% extends 'layout.twig' %}

{# これはコンテンツブロック内のレイアウト内にレンダリングされるコンテンツです #}
{% block content %}
    <h1>Home Page</h1>
    <p>Welcome to my app!</p>
{% endblock %}

その後、関数またはコントローラー内でこれをレンダリングする場合、以下のようにします:

// シンプルなルート
Flight::route('/', function () {
    Flight::render('home.twig', [
        'title' => 'Home Page'
    ]);
});

// またはコントローラーを使用する場合
Flight::route('/', [HomeController::class, 'index']);

// HomeController.php
class HomeController
{
    public function index()
    {
        Flight::render('home.twig', [
            'title' => 'Home Page'
        ]);
    }
}

Twigを最大限に活用する方法の詳細については、Twigドキュメントをご覧ください!

デバッグ

Twigには、テンプレート内で使用できるdump()関数を追加するデバッグ拡張機能が付属しています。開発時のみ有効にします:

$app->register('view', \Twig\Environment::class, [
    new \Twig\Loader\FilesystemLoader($app->get('flight.views.path')),
    [
        'cache' => __DIR__ . '/../cache/twig',
        'debug' => true, // dump()関数に必要
        'auto_reload' => true,
    ],
], function (\Twig\Environment $twig): void {
    $twig->addExtension(new \Twig\Extension\DebugExtension());
});

テンプレート内では:

{{ dump(user) }}

また、PHPレベルのデバッグ用にTracyとTwigを組み合わせることもできます。テンプレートレベルのメトリクス(レンダリング時間、メモリ、実行されたテンプレート/ブロック)については、flightphp/tracy-extensionsのオプションのTwigパネルを使用します:Twig\Profiler\Profiletwig_profileとしてTracyExtensionLoaderに渡します。オプションのTwigTracyExtensionは、Tracyがオンのときにテンプレート内で{{ dump() }} / {{ bdump() }} / {{ dumpe() }}を公開します。

セキュリティに関する注意

Twigはデフォルトで出力を自動エスケープするため、XSS攻撃から保護するのに役立ちます。テキストには{{ variable }}を優先してください。HTMLコンテンツを意図的に信頼する場合(例:サーバーサイドで既に処理済みのサニタイズされたMarkdown)のみ、|rawフィルタを使用してください。

Awesome-plugins/session

FlightPHP セッション - 軽量なファイルベースのセッション ハンドラ

これは、Flight PHP Framework 向けの軽量でファイルベースのセッション ハンドラ プラグインです。ノンブロッキングのセッション読み込み、オプションの暗号化、オートコミット機能、開発用のテスト モードなどの機能を提供し、セッション管理を簡単かつ強力にします。セッション データはファイルに保存されるため、データベースを必要としないアプリケーションに理想的です。

データベースを使用したい場合は、同じ機能の多くを持つがデータベース バックエンドを備えた ghostff/session プラグインを参照してください。

完全なソース コードと詳細については、Github リポジトリを訪問してください。

インストール

Composer を介してプラグインをインストールします:

composer require flightphp/session

基本的な使用方法

Flight アプリケーションで flightphp/session プラグインを使用する簡単な例です:

require 'vendor/autoload.php';

use flight\Session;

$app = Flight::app();

// セッション サービスを登録
$app->register('session', Session::class);

// セッションを使用した例のルート
Flight::route('/login', function() {
    $session = Flight::session();
    $session->set('user_id', 123);
    $session->set('username', 'johndoe');
    $session->set('is_admin', false);

    echo $session->get('username'); // 出力: johndoe
    echo $session->get('preferences', 'default_theme'); // 出力: default_theme

    if ($session->get('user_id')) {
        Flight::json(['message' => 'User is logged in!', 'user_id' => $session->get('user_id')]);
    }
});

Flight::route('/logout', function() {
    $session = Flight::session();
    $session->clear(); // すべてのセッション データをクリア
    Flight::json(['message' => 'Logged out successfully']);
});

Flight::start();

重要なポイント

構成

登録時にオプションの配列を渡すことで、セッション ハンドラをカスタマイズできます:

// はい、二重配列です :)
$app->register('session', Session::class, [ [
    'save_path' => '/custom/path/to/sessions',         // セッション ファイルのディレクトリ
    'prefix' => 'myapp_',                              // セッション ファイルのプレフィックス
    'encryption_key' => 'a-secure-32-byte-key-here',   // 暗号化を有効にする (AES-256-CBC のために 32 バイト推奨)
    'auto_commit' => false,                            // オートコミットを無効にして手動制御
    'start_session' => true,                           // 自動的にセッションを開始 (デフォルト: true)
    'test_mode' => false,                              // 開発用のテスト モードを有効
    'serialization' => 'json',                         // シリアル化方法: 'json' (デフォルト) または 'php' (レガシー)
] ]);

構成オプション

Option Description Default Value
save_path セッション ファイルが保存されるディレクトリ sys_get_temp_dir() . '/flight_sessions'
prefix 保存されたセッション ファイルのプレフィックス sess_
encryption_key AES-256-CBC 暗号化のためのキー (オプション) null (暗号化なし)
auto_commit シャットダウン時にセッション データを自動保存 true
start_session 自動的にセッションを開始 true
test_mode PHP セッションに影響を与えないテスト モードで実行 false
test_session_id テスト モード用のカスタム セッション ID (オプション) 設定されていない場合ランダム生成
serialization シリアル化方法: 'json' (デフォルト、安全) または 'php' (レガシー、オブジェクトを許可) 'json'

シリアル化モード

このライブラリはデフォルトで JSON シリアル化 を使用し、セッション データの安全性が高く、PHP オブジェクト注入の脆弱性を防ぎます。セッションに PHP オブジェクトを保存する必要がある場合 (ほとんどのアプリでは推奨されません) は、レガシーの PHP シリアル化を選択できます:

注: JSON シリアル化を使用している場合、オブジェクトを保存しようとすると例外が発生します。

高度な使用方法

手動コミット

オートコミットを無効にした場合、変更を手動でコミットする必要があります:

$app->register('session', Session::class, ['auto_commit' => false]);

Flight::route('/update', function() {
    $session = Flight::session();
    $session->set('key', 'value');
    $session->commit(); // 変更を明示的に保存
});

暗号化によるセッション セキュリティ

機密データを保護するために暗号化を有効にします:

$app->register('session', Session::class, [
    'encryption_key' => 'your-32-byte-secret-key-here'
]);

Flight::route('/secure', function() {
    $session = Flight::session();
    $session->set('credit_card', '4111-1111-1111-1111'); // 自動的に暗号化
    echo $session->get('credit_card'); // 取得時に復号化
});

セッション ID の再生成

セキュリティのために (例: ログイン後) セッション ID を再生成します:

Flight::route('/post-login', function() {
    $session = Flight::session();
    $session->regenerate(); // 新しい ID、データを保持
    // または
    $session->regenerate(true); // 新しい ID、古いデータを削除
});

ミドルウェアの例

セッション ベースの認証でルートを保護します:

Flight::route('/admin', function() {
    Flight::json(['message' => 'Welcome to the admin panel']);
})->addMiddleware(function() {
    $session = Flight::session();
    if (!$session->get('is_admin')) {
        Flight::halt(403, 'Access denied');
    }
});

これはミドルウェアでの簡単な例です。より詳細な例については、middleware ドキュメントを参照してください。

メソッド

Session クラスは以下のメソッドを提供します:

get()id() を除くすべてのメソッドは、チェイニングのために Session インスタンスを返します。

このプラグインを使う理由

技術詳細

貢献

貢献を歓迎します! リポジトリ をフォークし、変更を加えてプル リクエストを送信してください。バグの報告や機能の提案は Github のイシュー トラッカーで行ってください。

ライセンス

このプラグインは MIT ライセンスの下でライセンスされています。詳細は Github リポジトリ を参照してください。

Awesome-plugins/runway

ランナウェイ

ランナウェイは、Flightアプリケーションを管理するためのCLIアプリケーションです。コントローラーの生成、すべてのルートの表示、AIセットアップヘルパー、マイグレーション(スケルトン内)などを実行できます。優れたadocore/php-cliライブラリを基にしています。

コードを表示するにはこちらをクリックしてください。

スキャフォールディングコマンドはofficial skeletonと意図的に連携されており、AI coding toolsと人間が毎回同じパス、名前空間、コンストラクタインジェクションスタイルを取得できるようにしています。

インストール

composerでインストールします。

composer require flightphp/runway

スケルトンはすでにランナウェイに依存しているため、プロジェクトルートからphp runwayを使用します。

基本設定

ランナウェイを初めて実行すると、app/config/config.php'runway'キーを介してrunway設定を見つけようとします。

<?php
// app/config/config.php
return [
    'runway' => [
        'app_root' => 'app/',
        'public_root' => 'public/',
        // オプション: スケルトンはパブリックエントリのindex_rootも使用します
        'index_root' => 'public/index.php',
    ],
];

注意 - v1.2.0以降、.runway-config.jsonapp/config/config.phpに置き換えられ、非推奨となりました。古いプロジェクトをアップグレードする場合はphp runway config:migrateで移行してください。スケルトンは互換性のため、create-project時に小さな.runway-config.jsonを書き込む場合がありますが、今後はconfig.phprunwayキーを優先してください。

プロジェクトルート検出

ランナウェイはプロジェクトのルートを検出する機能が十分にあり、サブディレクトリから実行しても検出できます。composer.json.gitapp/config/config.phpなどのインジケータを探して、プロジェクトルートを判断します。つまり、プロジェクト内のどこからでもランナウェイコマンドを実行できるということです!

使用方法

ランナウェイには、Flightアプリケーションを管理するために使用できるいくつかのコマンドがあります。ランナウェイを使用するには、2つの簡単な方法があります。

  1. スケルトンプロジェクトを使用している場合は、プロジェクトのルートからphp runway [command]を実行できます。
  2. composer経由でインストールされたパッケージとしてランナウェイを使用している場合は、プロジェクトのルートからvendor/bin/runway [command]を実行できます。

コマンドリスト

php runwayコマンドを実行すると、利用可能なすべてのコマンドのリストを表示できます。

php runway

インストールに実際に表示されるコマンドのみに依存してください(コアランナウェイコマンドと、スケルトンのmigrateのようなプロジェクト固有のコマンド)。

コマンドヘルプ

任意のコマンドで--helpフラグを渡すと、コマンドの使用方法に関する詳細情報を取得できます。

php runway routes --help
php runway make:controller --help

いくつかの例を以下に示します:

コントローラーの生成

make:controllerは公式スケルトンレイアウトに一致するコントローラーをスキャフォールドします:

パス app/Controller/{Name}.php
名前空間 App\Controller
スタイル flight\Engineのコンストラクタインジェクション(クラス本体にFlight::なし)
php runway make:controller MyController
# → app/Controller/MyController.php
#   namespace App\Controller;

期待される形状の例(簡略化):

<?php

declare(strict_types=1);

namespace App\Controller;

use flight\Engine;

class MyController
{
    protected Engine $app;

    public function __construct(Engine $app)
    {
        $this->app = $app;
    }

    public function index(): void
    {
        // 例: $this->app->render('…', […]);
    }
}

Diceがコントローラーを構築できるようにクラス呼び出し可能で登録します:

// app/config/routes.php
use App\Controller\MyController;

$router->get('/mine', [MyController::class, 'index']);

このレイアウトの理由は? フォルダーの大文字小文字は、LinuxでのComposer PSR-4のために名前空間と一致する必要があります(controllersではなくController) - Autoloadingを参照してください。ルートとスコープ付きAGENTS.mdファイルがAIツールに使用するよう指示するパスも同じであり、生成されたコントローラーと手書きのコントローラーが同一に保たれます。

古いドキュメントやコミュニティプロジェクトでは、app/controllers/app\controllersが使用されることがありました。あなたのツリーがまだ小文字のフォルダーを使用している場合は、そのまま有効です。新しいスケルトンプロジェクトと現在のmake:controller出力は、app/Controller/ + App\Controllerを使用します。

アクティブレコードモデルの生成

まず、Active Recordプラグインをインストールしていることを確認してください。

php runway make:record users

公式スケルトンでは、モデルは名前空間App\Modelapp/Model/の下に配置され、DB接続はSimplePdo(ActiveRecordコンストラクタに注入または渡す)です。生成されるファイル名と名前空間はランナウェイの現在のデフォルトとrunway設定に従います。新しいモデルをApp\Modelに合わせることで、autoloadingAGENTS.mdに一致するようにしてください。

スケルトンの投稿デモと一致するモデルの例:

<?php

declare(strict_types=1);

namespace App\Model;

use flight\ActiveRecord;

/**
 * @property int $id
 * @property string $title
 * // …
 */
class Post extends ActiveRecord
{
    protected array $relations = [];

    public function __construct($databaseConnection)
    {
        parent::__construct($databaseConnection, 'posts');
    }
}

古いジェネレータがまだapp/records / app\recordsを出力する場合は、レガシーアプリでその規約を維持するか、ファイルをapp/Model/に移動して名前空間をフォルダーケースに合わせて更新できます。

マイグレーション(スケルトン)

公式スケルトンには、app/commands/から検出されたプロジェクトコマンド(以下のようなもの)が付属しています:

php runway migrate

マイグレーションはmigrations/の下にあるSQLファイル(SQLiteの場合はYYYYMMDDHHMMSS_description.sql、MySQLの場合は…_description.mysql.sqlなど)で、データベースドライバ設定/環境から選択されます。正確なフラグと動作はそのプロジェクトコマンドによって定義されています - アプリでphp runway migrate --helpを実行してください。

AIヘルパー

ランナウェイはAI & developer experienceで使用されるAI指向のコマンドを公開しています:

php runway ai:init
php runway ai:generate-instructions

これらはLLM認証情報を保存し、プロジェクトの指示(主にAGENTS.md)を生成します。スケルトンでは、AGENTS.md(およびapp/の下のスコープ付きコピー)とSECURITY.mdをエージェントの真実のソースとして扱ってください。

すべてのルートの表示

現在Flightに登録されているすべてのルートを表示します。

php runway routes

特定のルートのみを表示したい場合は、フラグを渡してルートをフィルタリングできます。

# GETルートのみを表示
php runway routes --get

# POSTルートのみを表示
php runway routes --post

# など

ランナウェイへのカスタムコマンドの追加

Flight用のパッケージを作成する場合、またはプロジェクトに独自のカスタムコマンドを追加したい場合は、プロジェクト/パッケージのsrc/commands/flight/commands/app/commands/、またはcommands/ディレクトリを作成することで実行できます。さらなるカスタマイズが必要な場合は、以下の設定セクションを参照してください。

スケルトンでは、プロジェクトコマンドは名前空間App\Commandapp/commands/に配置されます。ランナウェイはパスでそれらを検出します。そのフォルダーは、プロジェクトがすでにComposer classmap/PSR-4と同期しているようにしてください。

コマンドを作成するには、AbstractBaseCommandクラスを拡張し、最低限__constructメソッドとexecuteメソッドを実装します。

<?php

declare(strict_types=1);

namespace App\Command;

use flight\commands\AbstractBaseCommand;

class ExampleCommand extends AbstractBaseCommand
{
    /**
     * コンストラクタ
     *
     * @param array<string,mixed> $config app/config/config.phpからの設定
     */
    public function __construct(array $config)
    {
        parent::__construct('make:example', 'ドキュメントの例を作成します', $config);
        $this->argument('<funny-gif>', '面白いgifの名前');
    }

    /**
     * 関数を実行します
     *
     * @return void
     */
    public function execute()
    {
        $io = $this->app()->io();

        $io->info('例を作成中...');

        // ここで何かを実行

        $io->ok('例が作成されました!');
    }
}

Flightアプリケーションに独自のカスタムコマンドを構築する方法の詳細については、adhocore/php-cli Documentationを参照してください!

設定管理

設定はv1.2.0以降、app/config/config.phpに移動したため、設定を管理するためのヘルパーコマンドがいくつかあります。

スケルトンのヒント: config.phpリテラルなPHP値として保持してください。シークレットは.envに属します。config.php内に$_ENV[...]式を使用しないでください - config:setはファイルを静的データとして書き換えるため、シークレットがファイルに焼き付けられる可能性があります。Configurationを参照してください。

古い設定の移行

古い.runway-config.jsonファイルがある場合は、次のコマンドでapp/config/config.phpに簡単に移行できます:

php runway config:migrate

設定値の設定

config:setコマンドを使用して設定値を設定できます。これはファイルを開かずに設定値を更新したい場合に便利です。

php runway config:set app_root "app/"

設定値の取得

config:getコマンドを使用して設定値を取得できます。

php runway config:get app_root

すべてのランナウェイ設定

ランナウェイの設定をカスタマイズする必要がある場合は、app/config/config.phpにこれらの値を設定できます。以下に設定できる追加の設定をいくつか示します:

<?php
// app/config/config.php
return [
    // ... 他の設定値 ...

    'runway' => [
        // アプリケーションのディレクトリが配置されている場所
        'app_root' => 'app/',

        // ルートインデックスファイルが配置されているディレクトリ
        'index_root' => 'public/',

        // 他のプロジェクトのルートへのパス
        'root_paths' => [
            '/home/user/different-project',
            '/var/www/another-project'
        ],

        // ベースパスはほとんどの場合設定する必要はありませんが、必要であればここにあります
        'base_paths' => [
            '/includes/libs/vendor', // ベンダーディレクトリや何かに対して本当にユニークなパスがある場合
        ],

        // ファイナルパスはコマンドファイルを検索するためのプロジェクト内の場所です
        'final_paths' => [
            'src/diff-path/commands',
            'app/module/admin/commands',
        ],

        // フルパスを追加したい場合は、すぐに追加してください(プロジェクトルートからの絶対パスまたは相対パス)
        'paths' => [
            '/home/user/different-project/src/diff-path/commands',
            '/var/www/another-project/app/module/admin/commands',
            'app/my-unique-commands'
        ]
    ]
];

設定へのアクセス

設定値に効果的にアクセスする必要がある場合は、__constructメソッドまたはapp()メソッドを通じてアクセスできます。app/config/services.phpファイルがある場合、それらのサービスもコマンドで利用できることも重要です。

public function execute()
{
    $io = $this->app()->io();

    // 設定へのアクセス
    $app_root = $this->config['runway']['app_root'];

    // データベース接続などのサービスへのアクセス
    $database = $this->config['database']

    // ...
}

AIヘルパーラッパー

ランナウェイには、AIがコマンドを生成しやすくするためのヘルパーラッパーがいくつかあります。Symfony Consoleに似た方法でaddOptionaddArgumentを使用できます。これはAIツールを使用してコマンドを生成する場合に役立ちます。

public function __construct(array $config)
{
    parent::__construct('make:example', 'ドキュメントの例を作成します', $config);

    // モード引数はnull可能で、完全にオプションがデフォルトです
    $this->addOption('name', '例の名前', null);
}

関連項目

Awesome-plugins/tracy_extensions

Tracy Flight パネル拡張

これはFlightの作業をより豊かにするための拡張機能セットです。

これは特にofficial skeletonで便利で、デフォルトでTwigを使用しています:同じレイアウトAIツールに従うと、Tracyバーにも明確に表示されます。

これがパネルです

Flight Bar

各パネルにはアプリケーションに関する非常に役立つ情報が表示されます!

Flight Data Flight Database Flight Request

コードを表示するにはこちらをクリックしてください。

インストール

composer require flightphp/tracy-extensions --devを実行すれば準備完了です!

Twigはパッケージのハード依存関係ではありません。Twigパネルが必要な場合のみtwig/twigをインストールしてください(スケルトンでは既にビュー用にインストールされています)。

設定

これを開始するために必要な設定はほとんどありません。Tracyデバッガーをこのhttps://tracy.nette.org/en/guideを使用する前に初期化する必要があります:

<?php

use Tracy\Debugger;
use flight\debug\tracy\TracyExtensionLoader;

// bootstrapコード
require __DIR__ . '/vendor/autoload.php';

Debugger::enable();
// 環境を指定する必要があるかもしれません:Debugger::enable(Debugger::DEVELOPMENT)

// アプリでデータベース接続を使用する場合、
// 開発専用(本番環境では使用しないでください!)に使用する
// 必要なPDOラッパーがあります
// 通常のPDO接続と同じパラメータを持っています
$pdo = new PdoQueryCapture('sqlite:test.db', 'user', 'pass');
// またはFlightフレームワークにこれをアタッチする場合
Flight::register('db', PdoQueryCapture::class, ['sqlite:test.db', 'user', 'pass']);
// これでクエリを実行するたびに時間、クエリ、パラメータがキャプチャされます

// ドットを接続します
if(Debugger::$showBar === true) {
    // Tracyが実際にレンダリングできないため、これをfalseにする必要があります :(
    Flight::set('flight.content_length', false);
    new TracyExtensionLoader(Flight::app());
}

// より多くのコード

Flight::start();

追加設定

セッションデータ

ghostff/sessionのようなカスタムセッションハンドラを使用している場合、任意のセッションデータ配列をTracyに渡すことができ、自動的に出力されます。TracyExtensionLoaderコンストラクタの2番目のパラメータのsession_dataキーで渡します。


use Ghostff\Session\Session;
// またはflight\Sessionを使用;

require 'vendor/autoload.php';

$app = Flight::app();

$app->register('session', Session::class);

if(Debugger::$showBar === true) {
    // Tracyが実際にレンダリングできないため、これをfalseにする必要があります :(
    Flight::set('flight.content_length', false);
    new TracyExtensionLoader(Flight::app(), [ 'session_data' => Flight::session()->getAll() ]);
}

// ルートやその他のもの...

Flight::start();

Twigパネル(オプション)

アプリがTwigを使用している場合(公式スケルトンを含む)、Tracyバーにテンプレートメトリクスを表示できます。TwigのProfileを作成し、ProfilerExtensionを環境にアタッチし、そのプロファイルをtwig_profileキーの下のローダーに渡します。開発時のみプロファイリングをアタッチしてください。

<?php

use flight\debug\tracy\TracyExtensionLoader;
use flight\debug\tracy\TwigTracyExtension;
use Tracy\Debugger;
use Twig\Environment;
use Twig\Extension\ProfilerExtension;
use Twig\Loader\FilesystemLoader;
use Twig\Profiler\Profile;

$loader = new FilesystemLoader(__DIR__ . '/views');
$twig = new Environment($loader, [
    'debug' => true,
    'cache' => false,
]);

// オプション:テンプレートでTracyダンプヘルパーを公開
// {{ dump(var) }}, {{ bdump(var) }}, {{ dumpe(var) }}
$twig->addExtension(new TwigTracyExtension());

$tracyConfig = [];
if (Debugger::$showBar === true) {
    $profile = new Profile();
    $twig->addExtension(new ProfilerExtension($profile));
    $tracyConfig['twig_profile'] = $profile;
}

if (Debugger::$showBar === true) {
    Flight::set('flight.content_length', false);
    new TracyExtensionLoader(Flight::app(), $tracyConfig);
}

// Flight::render()をTwigにマッピング(例)
Flight::map('render', function (string $template, array $data = []) use ($twig) {
    if (substr($template, -5) !== '.twig') {
        $template .= '.twig';
    }
    echo $twig->render($template, $data);
});

パネルに表示される内容

Twigタブは、リクエストでテンプレートがレンダリングされなかった場合、またはtwig_profileを省略した場合(またはTwigがインストールされていない場合)に非表示になります - 他のFlightパネルは引き続き動作します。

skeletonスタイルのservices.phpでは、デバッグがオンの時に同じ$profile/ProfilerExtensionを構築し、twig_profileTracyExtensionLoaderに渡し、$app->render()に共有Twig環境を使用し続けます。

Latte

このセクションではPHP 8.1+が必要です。

プロジェクトにLatteがインストールされている場合、Tracyにはテンプレートを分析するためのLatteとのネイティブ統合があります。Latteインスタンスに拡張機能を登録するだけです(これは上記のTwigパネルではなく、Latte独自のTracyブリッジです)。


require 'vendor/autoload.php';

$app = Flight::app();

$app->map('render', function($template, $data, $block = null) {
    $latte = new Latte\Engine;

    // その他の設定...

    // Tracyデバッグバーが有効な場合のみ拡張機能を追加
    if(Debugger::$showBar === true) {
        // ここでLatteパネルをTracyに追加
        $latte->addExtension(new Latte\Bridges\Tracy\TracyExtension);
    }

    $latte->render($template, $data, $block);
});

関連項目

Awesome-plugins/apm

FlightPHP APM ドキュメント

FlightPHP APMへようこそ—あなたのアプリ専用のパフォーマンスコーチです!このガイドは、FlightPHPを使ったアプリケーションパフォーマンスモニタリング(APM)のセットアップ、使用、習得への道筋です。遅いリクエストの調査やレイテンシチャートの分析など、私たちがサポートします。アプリをより速く、ユーザーをより満足させ、デバッグセッションをスムーズにしましょう!

Flight Docsサイトのdemoを表示します。

FlightPHP APM

APMが重要な理由

あなたのアプリが忙しいレストランだと想像してみてください。注文にかかる時間やキッチンの混雑状況を追跡する方法がなければ、なぜ顧客が不機嫌に帰ってしまうのかを推測することになります。APMはあなたのスーシェフのようなもので、着信リクエストからデータベースクエリまで、すべてのステップを監視し、遅延の原因を特定します。ページの表示が遅いとユーザーを失います(調査によると、サイトの読み込みに3秒以上かかると53%が離脱します!)が、APMは問題が深刻化するに発見するのに役立ちます。これは積極的な安心感—「なぜこれが壊れているの?」という瞬間を減らし、「これがどれだけスムーズに動いているか!」という成功体験を増やします。

インストール

Composerで始めましょう:

composer require flightphp/apm

必要なもの:

サポートされるデータベース

FlightPHP APMは現在、メトリクスを保存するために以下のデータベースをサポートしています:

設定ステップ(以下参照)でデータベースの種類を選択できます。PHP環境に必要な拡張機能がインストールされていることを確認してください(例:pdo_sqlite または pdo_mysql)。

はじめに

APMの素晴らしさへのステップバイステップ:

1. APMの登録

トラッキングを開始するために、index.php または services.php ファイルに以下を追加します:

use flight\apm\logger\LoggerFactory;
use flight\database\SimplePdo;
use flight\Apm;

$ApmLogger = LoggerFactory::create(__DIR__ . '/../../.runway-config.json');
$Apm = new Apm($ApmLogger);
$Apm->bindEventsToFlightInstance($app);

// データベース接続を追加する場合
// SimplePdo(または開発環境ではTracy ExtensionsのPdoQueryCapture)を推奨。
// オプション配列(5番目の引数)でAPMクエリトラッキングを有効化。
$pdo = new SimplePdo('mysql:host=localhost;dbname=example', 'user', 'pass', null, [
    'trackApmQueries' => true, // APM用のクエリキャプチャに必要
]);
$Apm->addPdoConnection($pdo);

ここで何が起こっているか?

プロのヒント: サンプリング アプリが忙しい場合、すべてのリクエストをログに記録するとシステムに負荷がかかる可能性があります。サンプルレート(0.0から1.0)を使用してください:

$Apm = new Apm($ApmLogger, 0.1); // 10%のリクエストをログに記録

これによりパフォーマンスを維持しながら、信頼性の高いデータを提供します。

2. 設定

.runway-config.json を作成するために以下を実行します:

php vendor/bin/runway apm:init

これは何をするか?

このプロセスでは、このセットアップのためのマイグレーションを実行するかどうかも尋ねられます。初めて設定する場合は、はいと答えてください。

なぜ2つの場所が必要か? 生のメトリクスは急速に蓄積されます(フィルタリングされていないログのように)。ワーカーはそれらをダッシュボード用の構造化された宛先に処理します。整理された状態を維持します!

3. ワーカーでメトリクスを処理

ワーカーは生のメトリクスをダッシュボード対応データに変換します。一度実行します:

php vendor/bin/runway apm:worker

何をしているか?

継続的な実行 ライブアプリの場合、継続的な処理が必要です。オプションは以下の通りです:

なぜ重要か? ワーカーがなければ、ダッシュボードは空です。生のログと実用的な洞察の間の橋渡しです。

4. ダッシュボードの起動

アプリの状態を確認:

php vendor/bin/runway apm:dashboard

これは何をするか?

カスタマイズ:

php vendor/bin/runway apm:dashboard --host 0.0.0.0 --port 8080 --php-path=/usr/local/bin/php

ブラウザでURLを開いて探索してください!

本番モード

本番環境では、ファイアウォールやその他のセキュリティ対策があるため、ダッシュボードを実行するためにいくつかのテクニックを試す必要があるかもしれません。いくつかのオプション:

異なるダッシュボードが必要ですか?

独自のダッシュボードを構築できます!独自のダッシュボード用のデータの表示方法については、vendor/flightphp/apm/src/apm/presenterディレクトリを参照してください!

ダッシュボードの機能

ダッシュボードはAPMの本部です—ここで確認できる内容:

その他の機能:

: /users へのリクエストは以下を表示する可能性があります:

カスタムイベントの追加

API呼び出しや支払い処理など、任意のものを追跡:

use flight\apm\CustomEvent;

$app->eventDispatcher()->trigger('apm.custom', new CustomEvent('api_call', [
    'endpoint' => 'https://api.example.com/users',
    'response_time' => 0.25,
    'status' => 200
]));

どこに表示されるか? ダッシュボードのリクエスト詳細の「カスタムイベント」セクション—見やすいJSONフォーマットで展開可能。

ユースケース:

$start = microtime(true);
$apiResponse = file_get_contents('https://api.example.com/data');
$app->eventDispatcher()->trigger('apm.custom', new CustomEvent('external_api', [
    'url' => 'https://api.example.com/data',
    'time' => microtime(true) - $start,
    'success' => $apiResponse !== false
]));

これでそのAPIがアプリを遅くしているかどうかがわかります!

データベースモニタリング

以下のようにPDOクエリを追跡:

use flight\database\SimplePdo;

$pdo = new SimplePdo('sqlite:/path/to/db.sqlite', null, null, null, [
    'trackApmQueries' => true, // APM用のクエリキャプチャに必要
]);
$Apm->addPdoConnection($pdo);

取得できる内容:

注意:

出力例:

ワーカーオプション

好みに合わせてワーカーを調整:

:

php vendor/bin/runway apm:worker --daemon --batch_size 100 --timeout 3600

1時間実行し、一度に100のメトリクスを処理します。

アプリ内のリクエストID

各リクエストには追跡用のユニークなリクエストIDがあります。アプリ内でこのIDを使用してログとメトリクスを関連付けることができます。例えば、エラーページにリクエストIDを追加できます:

Flight::map('error', function($message) {
    // レスポンスヘッダーX-Flight-Request-IdからリクエストIDを取得
    $requestId = Flight::response()->getHeader('X-Flight-Request-Id');

    // さらにFlight変数から取得することも可能
    // この方法はswooleやその他の非同期プラットフォームではうまく動作しません。
    // $requestId = Flight::get('apm.request_id');

    echo "Error: $message (Request ID: $requestId)";
});

アップグレード

APMの新しいバージョンにアップグレードする場合、実行する必要があるデータベースマイグレーションがある可能性があります。以下のコマンドを実行することで実行できます:

php vendor/bin/runway apm:migrate

これにより、データベーススキーマを最新バージョンに更新するために必要なマイグレーションが実行されます。

注意: APMデータベースのサイズが大きい場合、これらのマイグレーションには時間がかかる可能性があります。オフピーク時間にこのコマンドを実行することをお勧めします。

0.4.3から0.5.0へのアップグレード

0.4.3から0.5.0にアップグレードする場合、以下のコマンドを実行する必要があります:

php vendor/bin/runway apm:config-migrate

これにより、古い形式の.runway-config.jsonファイルを使用した設定を、新しい形式のconfig.phpファイルにキーと値を保存する形式に移行します。

古いデータの削除

データベースを整理するために、古いデータを削除できます。忙しいアプリを実行していてデータベースサイズを管理したい場合に特に便利です。以下のコマンドを実行することで実行できます:

php vendor/bin/runway apm:purge

これにより、30日以上前のすべてのデータがデータベースから削除されます。--daysオプションに異なる値を渡すことで、日数を調整できます:

php vendor/bin/runway apm:purge --days 7

これにより、7日以上前のすべてのデータがデータベースから削除されます。

トラブルシューティング

困った場合は以下を試してください:

Awesome-plugins/tracy

Tracy

TracyはFlightで使用できる素晴らしいエラーハンドラです。アプリケーションのデバッグに役立つ複数のパネルを備えています。また、非常に簡単に拡張でき、独自のパネルを追加することも可能です。Flightチームは、flightphp/tracy-extensionsプラグインを使用してFlightプロジェクト向けにいくつかのパネルを作成しました(Flight変数、DBクエリ、リクエスト、セッション、およびプロファイラープロファイルを渡す場合のオプションのTwigパネル—Tracy Extensionsを参照)。

インストール

Composerでインストールします。また、Tracyには本番環境用のエラーハンドリングコンポーネントが付属しているため、dev版ではなく通常版をインストールすることをお勧めします。

composer require tracy/tracy

基本設定

開始するための基本的な設定オプションがあります。詳細についてはTracy Documentationをご覧ください。


require 'vendor/autoload.php';

use Tracy\Debugger;

// Tracyを有効化
Debugger::enable();
// Debugger::enable(Debugger::DEVELOPMENT) // 明示的に指定する必要がある場合もあります(Debugger::PRODUCTIONも同様)
// Debugger::enable('23.75.345.200'); // IPアドレスの配列を指定することもできます

// エラーと例外が記録される場所です。このディレクトリが存在し、書き込み可能であることを確認してください。
Debugger::$logDirectory = __DIR__ . '/../log/';
Debugger::$strictMode = true; // すべてのエラーを表示
// Debugger::$strictMode = E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED; // 非推奨通知を除くすべてのエラー
if (Debugger::$showBar) {
    $app->set('flight.content_length', false); // デバッガーバーが表示されている場合、Flightはcontent-lengthを設定できません

    // 含めている場合はFlight用のTracy Extensionに固有です
    // それ以外の場合はコメントアウトしてください。
    new TracyExtensionLoader($app);
}

役立つヒント

コードをデバッグする際、データを表示するための非常に便利な関数があります。

Awesome-plugins/active_record

Flight Active Record

アクティブ レコードは、データベース エンティティを PHP オブジェクトにマッピングするものです。簡単に言うと、データベースに users テーブルがある場合、そのテーブルの行をコードベース内の User クラスと $user オブジェクトに「変換」できます。基本例 を参照してください。

GitHub のリポジトリは こちら をクリックしてください。

Basic Example

以下のテーブルがあると仮定しましょう:

CREATE TABLE users (
    id INTEGER PRIMARY KEY, 
    name TEXT, 
    password TEXT 
);

このテーブルを表す新しいクラスを設定できます:

/**
 * ActiveRecord クラスは通常単数形です
 * 
 * テーブルのプロパティをコメントとしてここに追加することを強く推奨します
 * 
 * @property int    $id
 * @property string $name
 * @property string $password
 */ 
class User extends flight\ActiveRecord {
    public function __construct($database_connection)
    {
        // この方法で設定できます
        parent::__construct($database_connection, 'users');
        // またはこの方法で
        parent::__construct($database_connection, null, [ 'table' => 'users']);
    }
}

今、マジックが起こります!

// SQLite の場合
$database_connection = new PDO('sqlite:test.db'); // これは単なる例です。本物のデータベース接続を使用するはずです

// MySQL の場合
$database_connection = new PDO('mysql:host=localhost;dbname=test_db&charset=utf8bm4', 'username', 'password');

// または mysqli
$database_connection = new mysqli('localhost', 'username', 'password', 'test_db');
// またはオブジェクトベースでない mysqli の作成
$database_connection = mysqli_connect('localhost', 'username', 'password', 'test_db');

$user = new User($database_connection);
$user->name = 'Bobby Tables';
$user->password = password_hash('some cool password');
$user->insert();
// または $user->save();

echo $user->id; // 1

$user->name = 'Joseph Mamma';
$user->password = password_hash('some cool password again!!!');
$user->insert();
// ここで $user->save() を使用できません。更新と判断されるためです!

echo $user->id; // 2

新しいユーザーを追加するのに、これほど簡単だったとは!データベースにユーザーの行が存在する今、それを引き出すにはどうしますか?

$user->find(1); // データベースで id = 1 を検索して返します。
echo $user->name; // 'Bobby Tables'

すべてのユーザーを検索したい場合はどうでしょうか?

$users = $user->findAll();

特定の条件付きで?

$users = $user->like('name', '%mamma%')->findAll();

これがどれほど楽しいかわかりますか?インストールして始めましょう!

Installation

Composer で簡単にインストールします

composer require flightphp/active-record 

Usage

これはスタンドアロン ライブラリとして使用するか、Flight PHP Framework と共に使用できます。完全にあなた次第です。

Standalone

コンストラクタに PDO 接続を渡すことを確認してください。

$pdo_connection = new PDO('sqlite:test.db'); // これは単なる例です。本物のデータベース接続を使用するはずです

$User = new User($pdo_connection);

コンストラクタでデータベース接続を毎回設定したくない場合、データベース接続管理 を参照して他のアイデアを見てください!

Flight でメソッドとして登録

Flight PHP Framework を使用している場合、ActiveRecord クラスをサービスとして登録できますが、必須ではありません。

Flight::register('user', 'User', [ $pdo_connection ]);

// コントローラー、関数などで以下のように使用できます。

Flight::user()->find(1);

runway Methods

runway は Flight の CLI ツールで、このライブラリ用のカスタムコマンドがあります。

# Usage
php runway make:record database_table_name [class_name]

# Example
php runway make:record users

これにより、app/records/ ディレクトリに UserRecord.php という新しいクラスが作成され、以下の内容が含まれます:

<?php

declare(strict_types=1);

namespace app\records;

/**
 * users テーブルの ActiveRecord クラス。
 * @link https://docs.flightphp.com/awesome-plugins/active-record
 *
 * @property int $id
 * @property string $username
 * @property string $email
 * @property string $password_hash
 * @property string $created_dt
 */
class UserRecord extends \flight\ActiveRecord
{
    /**
     * @var array $relations モデルのリレーションシップを設定します
     *   https://docs.flightphp.com/awesome-plugins/active-record#relationships
     */
    protected array $relations = [
        // 'relation_name' => [ self::HAS_MANY, 'RelatedClass', 'foreign_key' ],
    ];

    /**
     * コンストラクタ
     * @param mixed $databaseConnection データベースへの接続
     */
    public function __construct($databaseConnection)
    {
        parent::__construct($databaseConnection, 'users');
    }
}

CRUD functions

find($id = null) : boolean|ActiveRecord

1 つのレコードを検索し、現在のオブジェクトに割り当てます。$id を渡すと、主キーに対してその値で検索を実行します。何も渡さない場合、テーブル内の最初のレコードを検索します。

さらに、他のヘルパー メソッドを渡してテーブルをクエリできます。

// 事前に条件を指定してレコードを検索
$user->notNull('password')->orderBy('id DESC')->find();

// 特定の id でレコードを検索
$id = 123;
$user->find($id);

findAll(): array<int,ActiveRecord>

指定したテーブルのすべてのレコードを検索します。

$user->findAll();

isHydrated(): boolean (v0.4.0)

現在のレコードがデータベースから取得(ハイドレート)されている場合に true を返します。

$user->find(1);
// データ付きのレコードが見つかった場合...
$user->isHydrated(); // true

insert(): boolean|ActiveRecord

現在のレコードをデータベースに挿入します。

$user = new User($pdo_connection);
$user->name = 'demo';
$user->password = md5('demo');
$user->insert();
テキストベースの主キー

テキストベースの主キー(例: UUID)がある場合、挿入前に主キー値を 2 つの方法のいずれかで設定できます。

$user = new User($pdo_connection, [ 'primaryKey' => 'uuid' ]);
$user->uuid = 'some-uuid';
$user->name = 'demo';
$user->password = md5('demo');
$user->insert(); // または $user->save();

または、イベントを通じて主キーを自動生成できます。

class User extends flight\ActiveRecord {
    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users', [ 'primaryKey' => 'uuid' ]);
        // 上記の配列の代わりにこの方法で primaryKey を設定することもできます。
        $this->primaryKey = 'uuid';
    }

    protected function beforeInsert(self $self) {
        $self->uuid = uniqid(); // またはユニーク ID を生成する方法
    }
}

挿入前に主キーを設定しない場合、rowid に設定され、データベースが生成しますが、テーブルにそのフィールドが存在しない場合、永続化されません。これがイベントを使用して自動的に処理することを推奨する理由です。

update(): boolean|ActiveRecord

現在のレコードをデータベースに更新します。

$user->greaterThan('id', 0)->orderBy('id desc')->find();
$user->email = 'test@example.com';
$user->update();

save(): boolean|ActiveRecord

現在のレコードをデータベースに挿入または更新します。レコードに id がある場合更新し、そうでない場合は挿入します。

$user = new User($pdo_connection);
$user->name = 'demo';
$user->password = md5('demo');
$user->save();

注意: クラスにリレーションシップが定義されている場合、定義、インスタンス化、更新が必要なダーティ データがある場合、それらのリレーションを再帰的に保存します。(v0.4.0 以降)

delete(): boolean

現在のレコードをデータベースから削除します。

$user->gt('id', 0)->orderBy('id desc')->find();
$user->delete();

事前の検索を実行して複数のレコードを削除することもできます。

$user->like('name', 'Bob%')->delete();

dirty(array $dirty = []): ActiveRecord

ダーティ データとは、レコード内で変更されたデータを指します。

$user->greaterThan('id', 0)->orderBy('id desc')->find();

// この時点では何も「ダーティ」ではありません。

$user->email = 'test@example.com'; // 変更されたので email は「ダーティ」と見なされます。
$user->update();
// 更新されてデータベースに永続化されたので、ダーティ データはなくなります

$user->password = password_hash()'newpassword'); // これがダーティになります
$user->dirty(); // 何も渡さないとすべてのダーティ エントリがクリアされます。
$user->update(); // ダーティとしてキャプチャされたものが何もないので何も更新されません。

$user->dirty([ 'name' => 'something', 'password' => password_hash('a different password') ]);
$user->update(); // name と password の両方が更新されます。

copyFrom(array $data): ActiveRecord (v0.4.0)

これは dirty() メソッドのエイリアスです。何をしているのかが少し明確です。

$user->copyFrom([ 'name' => 'something', 'password' => password_hash('a different password') ]);
$user->update(); // name と password の両方が更新されます。

isDirty(): boolean (v0.4.0)

現在のレコードが変更されている場合に true を返します。

$user->greaterThan('id', 0)->orderBy('id desc')->find();
$user->email = 'test@email.com';
$user->isDirty(); // true

reset(bool $include_query_data = true): ActiveRecord

現在のレコードを初期状態にリセットします。ループ型の動作で使用するのに非常に便利です。true を渡すと、現在のオブジェクトを検索するために使用されたクエリ データもリセットされます(デフォルト動作)。

$users = $user->greaterThan('id', 0)->orderBy('id desc')->find();
$user_company = new UserCompany($pdo_connection);

foreach($users as $user) {
    $user_company->reset(); // クリーンな状態から開始
    $user_company->user_id = $user->id;
    $user_company->company_id = $some_company_id;
    $user_company->insert();
}

getBuiltSql(): string (v0.4.1)

find()findAll()insert()update()、または save() メソッドを実行した後、構築された SQL を取得してデバッグに使用できます。

SQL Query Methods

select(string $field1 [, string $field2 ... ])

テーブル内の特定の列のみを選択できます(多くの列を持つ広いテーブルでパフォーマンスが向上します)

$user->select('id', 'name')->find();

from(string $table)

技術的には別のテーブルを選択することもできます!なぜそうしないのですか?!

$user->select('id', 'name')->from('user')->find();

join(string $table_name, string $join_condition)

データベース内の別のテーブルにジョインすることもできます。

$user->join('contacts', 'contacts.user_id = users.id')->find();

where(string $where_conditions)

カスタム where 引数を設定できます(この where 文ではパラメータを設定できません)

$user->where('id=1 AND name="demo"')->find();

セキュリティ 注意 - $user->where("id = '{$id}' AND name = '{$name}'")->find(); のようなことをしたくなるかもしれません。絶対にこれをしないでください!!!これは SQL インジェクション攻撃の脆弱性があります。オンラインにたくさんの記事があります。「sql injection attacks php」を Google 検索してください。このトピックに関する多くの記事が見つかります。このライブラリでこれを適切に扱う方法は、この where() メソッドの代わりに、$user->eq('id', $id)->eq('name', $name)->find(); のようなことを行うことです。絶対にこれをしなければならない場合、PDO ライブラリには $pdo->quote($var) があり、それをエスケープします。quote() を使用した後でのみ、where() 文で使用できます。

group(string $group_by_statement)/groupBy(string $group_by_statement)

特定の条件で結果をグループ化します。

$user->select('COUNT(*) as count')->groupBy('name')->findAll();

order(string $order_by_statement)/orderBy(string $order_by_statement)

返されるクエリを特定の方法でソートします。

$user->orderBy('name DESC')->find();

limit(string $limit)/limit(int $offset, int $limit)

返されるレコードの数を制限します。2 番目の int が与えられた場合、SQL のようにオフセット、リミットになります。

$user->orderby('name DESC')->limit(0, 10)->findAll();

WHERE conditions

equal(string $field, mixed $value) / eq(string $field, mixed $value)

Where field = $value

$user->eq('id', 1)->find();

notEqual(string $field, mixed $value) / ne(string $field, mixed $value)

Where field <> $value

$user->ne('id', 1)->find();

isNull(string $field)

Where field IS NULL

$user->isNull('id')->find();

isNotNull(string $field) / notNull(string $field)

Where field IS NOT NULL

$user->isNotNull('id')->find();

greaterThan(string $field, mixed $value) / gt(string $field, mixed $value)

Where field > $value

$user->gt('id', 1)->find();

lessThan(string $field, mixed $value) / lt(string $field, mixed $value)

Where field < $value

$user->lt('id', 1)->find();

greaterThanOrEqual(string $field, mixed $value) / ge(string $field, mixed $value) / gte(string $field, mixed $value)

Where field >= $value

$user->ge('id', 1)->find();

lessThanOrEqual(string $field, mixed $value) / le(string $field, mixed $value) / lte(string $field, mixed $value)

Where field <= $value

$user->le('id', 1)->find();

like(string $field, mixed $value) / notLike(string $field, mixed $value)

Where field LIKE $value または field NOT LIKE $value

$user->like('name', 'de')->find();

in(string $field, array $values) / notIn(string $field, array $values)

Where field IN($value) または field NOT IN($value)

$user->in('id', [1, 2])->find();

between(string $field, array $values)

Where field BETWEEN $value AND $value1

$user->between('id', [1, 2])->find();

OR Conditions

条件を OR 文でラップすることが可能です。これは startWrap()endWrap() メソッドを使用するか、フィールドと値の後の条件の 3 番目のパラメータを埋めることで行います。

// Method 1
$user->eq('id', 1)->startWrap()->eq('name', 'demo')->or()->eq('name', 'test')->endWrap('OR')->find();
// これは `id = 1 AND (name = 'demo' OR name = 'test')` に評価されます

// Method 2
$user->eq('id', 1)->eq('name', 'demo', 'OR')->find();
// これは `id = 1 OR name = 'demo'` に評価されます

Relationships

このライブラリを使用して、テーブル間の one->many および one->one リレーションシップを設定できます。これにはクラス内で少し追加の設定が必要です。

$relations 配列を設定するのは簡単ですが、正しい構文を推測するのは混乱するかもしれません。

protected array $relations = [
    // キーの名前は任意に付けられます。ActiveRecord の名前が良いでしょう。例: user, contact, client
    'user' => [
        // 必須
        // self::HAS_MANY, self::HAS_ONE, self::BELONGS_TO
        self::HAS_ONE, // これはリレーションシップのタイプです

        // 必須
        'Some_Class', // これは参照する「他の」ActiveRecord クラスです

        // 必須
        // リレーションシップのタイプによって異なります
        // self::HAS_ONE = ジョインを参照する外部キー
        // self::HAS_MANY = ジョインを参照する外部キー
        // self::BELONGS_TO = ジョインを参照するローカルキー
        'local_or_foreign_key',
        // FYI、これも「他の」モデルの主キーにのみジョインします

        // オプション
        [ 'eq' => [ 'client_id', 5 ], 'select' => 'COUNT(*) as count', 'limit' 5 ], // ジョイン時の追加条件
        // $record->eq('client_id', 5)->select('COUNT(*) as count')->limit(5))

        // オプション
        'back_reference_name' // これを自身にバック参照したい場合、例: $user->contact->user;
    ];
]
class User extends ActiveRecord{
    protected array $relations = [
        'contacts' => [ self::HAS_MANY, Contact::class, 'user_id' ],
        'contact' => [ self::HAS_ONE, Contact::class, 'user_id' ],
    ];

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }
}

class Contact extends ActiveRecord{
    protected array $relations = [
        'user' => [ self::BELONGS_TO, User::class, 'user_id' ],
        'user_with_backref' => [ self::BELONGS_TO, User::class, 'user_id', [], 'contact' ],
    ];
    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'contacts');
    }
}

これで参照が設定されたので、簡単に使用できます!

$user = new User($pdo_connection);

// 最新のユーザーを検索。
$user->notNull('id')->orderBy('id desc')->find();

// リレーションを使用して連絡先を取得:
foreach($user->contacts as $contact) {
    echo $contact->id;
}

// または逆方向に。
$contact = new Contact();

// 1 つの連絡先を検索
$contact->find();

// リレーションを使用してユーザー取得:
echo $contact->user->name; // これはユーザー名です

かなりクールですね?

Eager Loading

Overview

Eager loading は、N+1 クエリ問題を解決し、リレーションシップを事前にロードします。各レコードのリレーションシップごとに別々のクエリを実行する代わりに、リレーションシップごとに 1 つの追加クエリですべての関連データを取得します。

注意: Eager loading は v0.7.0 以降でのみ利用可能です。

Basic Usage

with() メソッドを使用して、eager load するリレーションシップを指定します:

// N+1 の代わりに 2 つのクエリでユーザーとその連絡先をロード
$users = $user->with('contacts')->findAll();
foreach ($users as $u) {
    foreach ($u->contacts as $contact) {
        echo $contact->email; // 追加のクエリなし!
    }
}

Multiple Relations

複数のリレーションシップを一度にロード:

$users = $user->with(['contacts', 'profile', 'settings'])->findAll();

Relationship Types

HAS_MANY
// 各ユーザーのすべての連絡先を eager load
$users = $user->with('contacts')->findAll();
foreach ($users as $u) {
    // $u->contacts はすでに配列としてロードされています
    foreach ($u->contacts as $contact) {
        echo $contact->email;
    }
}
HAS_ONE
// 各ユーザーの 1 つの連絡先を eager load
$users = $user->with('contact')->findAll();
foreach ($users as $u) {
    // $u->contact はすでにオブジェクトとしてロードされています
    echo $u->contact->email;
}
BELONGS_TO
// すべての連絡先の親ユーザーを eager load
$contacts = $contact->with('user')->findAll();
foreach ($contacts as $c) {
    // $c->user はすでにロードされています
    echo $c->user->name;
}
With find()

Eager loading は findAll() と find() の両方で動作します:

$user = $user->with('contacts')->find(1);
// ユーザーとすべての連絡先が 2 つのクエリでロードされます

Performance Benefits

Eager loading なし(N+1 問題):

$users = $user->findAll(); // 1 クエリ
foreach ($users as $u) {
    $contacts = $u->contacts; // N クエリ(ユーザーごとに 1 つ!)
}
// 合計: 1 + N クエリ

Eager loading あり:

$users = $user->with('contacts')->findAll(); // 合計 2 クエリ
foreach ($users as $u) {
    $contacts = $u->contacts; // 追加のクエリ 0!
}
// 合計: 2 クエリ(ユーザー用 1 + すべての連絡先用 1)

10 人のユーザーの場合、クエリが 11 から 2 に減少し、82% の削減です!

Important Notes

Limitations

Setting Custom Data

時には ActiveRecord にカスタム計算などのユニークなものをアタッチする必要がある場合があります。これをテンプレートに渡されるオブジェクトにアタッチする方が簡単かもしれません。

setCustomData(string $field, mixed $value)

setCustomData() メソッドを使用してカスタム データをアタッチします。

$user->setCustomData('page_view_count', $page_view_count);

次に、通常のオブジェクト プロパティのように参照します。

echo $user->page_view_count;

Events

このライブラリのもう一つの超すごい機能はイベントについてです。イベントは特定のメソッドを呼び出す特定のタイミングでトリガーされます。データを自動的に設定するのに非常に役立ちます。

onConstruct(ActiveRecord $ActiveRecord, array &config)

デフォルトの接続を設定する必要がある場合に非常に役立ちます。

// index.php または bootstrap.php
Flight::register('db', 'PDO', [ 'sqlite:test.db' ]);

//
//
//

// User.php
class User extends flight\ActiveRecord {

    protected function onConstruct(self $self, array &$config) { // & 参照を忘れずに
        // 接続を自動的に設定するためにこれを実行できます
        $config['connection'] = Flight::db();
        // またはこれ
        $self->transformAndPersistConnection(Flight::db());

        // この方法でテーブル名も設定できます。
        $config['table'] = 'users';
    } 
}

beforeFind(ActiveRecord $ActiveRecord)

毎回クエリ操作が必要な場合にのみ有用です。

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function beforeFind(self $self) {
        // それがお好みなら id >= 0 を常に実行
        $self->gte('id', 0); 
    } 
}

afterFind(ActiveRecord $ActiveRecord)

このレコードが取得されるたびに常にロジックを実行する必要がある場合に、より有用です。何かを復号化する必要がありますか?毎回カスタム カウント クエリを実行する必要がありますか(パフォーマンスは悪いですが、まあ)?

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function afterFind(self $self) {
        // 何かを復号化
        $self->secret = yourDecryptFunction($self->secret, $some_key);

        // クエリのようなカスタムなものを保存?
        $self->setCustomData('view_count', $self->select('COUNT(*) count')->from('user_views')->eq('user_id', $self->id)['count']; 
    } 
}

beforeFindAll(ActiveRecord $ActiveRecord)

毎回クエリ操作が必要な場合にのみ有用です。

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function beforeFindAll(self $self) {
        // それがお好みなら id >= 0 を常に実行
        $self->gte('id', 0); 
    } 
}

afterFindAll(array<int,ActiveRecord> $results)

afterFind() に似ていますが、すべてのレコードに対して実行できます!

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function afterFindAll(array $results) {

        foreach($results as $self) {
            // afterFind() のように何かクールなことをする
        }
    } 
}

beforeInsert(ActiveRecord $ActiveRecord)

毎回デフォルト値を設定する必要がある場合に非常に役立ちます。

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function beforeInsert(self $self) {
        // いくつかの健全なデフォルトを設定
        if(!$self->created_date) {
            $self->created_date = gmdate('Y-m-d');
        }

        if(!$self->password) {
            $self->password = password_hash((string) microtime(true));
        }
    } 
}

afterInsert(ActiveRecord $ActiveRecord)

挿入後にデータを変更するユース ケースがあるかもしれません?

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function afterInsert(self $self) {
        // あなた次第
        Flight::cache()->set('most_recent_insert_id', $self->id);
        // または何でも....
    } 
}

beforeUpdate(ActiveRecord $ActiveRecord)

更新時に毎回デフォルト値を設定する必要がある場合に非常に役立ちます。

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function beforeInsert(self $self) {
        // いくつかの健全なデフォルトを設定
        if(!$self->updated_date) {
            $self->updated_date = gmdate('Y-m-d');
        }
    } 
}

afterUpdate(ActiveRecord $ActiveRecord)

更新後にデータを変更するユース ケースがあるかもしれません?

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function afterInsert(self $self) {
        // あなた次第
        Flight::cache()->set('most_recently_updated_user_id', $self->id);
        // または何でも....
    } 
}

beforeSave(ActiveRecord $ActiveRecord)/afterSave(ActiveRecord $ActiveRecord)

挿入または更新の両方でイベントが発生するようにしたい場合に有用です。長い説明は省きますが、何かわかるはずです。

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function beforeSave(self $self) {
        $self->last_updated = gmdate('Y-m-d H:i:s');
    } 
}

beforeDelete(ActiveRecord $ActiveRecord)/afterDelete(ActiveRecord $ActiveRecord)

ここで何をしたいかわかりませんが、判断はしません!やってみてください!

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function beforeDelete(self $self) {
        echo 'He was a brave soldier... :cry-face:';
    } 
}

Database Connection Management

このライブラリを使用する場合、データベース接続をいくつかの方法で設定できます。コンストラクタで設定するか、$config['connection'] で設定するか、setDatabaseConnection() で設定できます (v0.4.1)。

$pdo_connection = new PDO('sqlite:test.db'); // 例
$user = new User($pdo_connection);
// または
$user = new User(null, [ 'connection' => $pdo_connection ]);
// または
$user = new User();
$user->setDatabaseConnection($pdo_connection);

アクティブ レコードを呼び出すたびに $database_connection を常に設定したくない場合、それには方法があります!

// index.php または bootstrap.php
// Flight で登録されたクラスとして設定
Flight::register('db', 'PDO', [ 'sqlite:test.db' ]);

// User.php
class User extends flight\ActiveRecord {

    public function __construct(array $config = [])
    {
        $database_connection = $config['connection'] ?? Flight::db();
        parent::__construct($database_connection, 'users', $config);
    }
}

// そして今、引数は不要!
$user = new User();

注意: ユニット テストを計画している場合、この方法でいくつかの課題が生じる可能性がありますが、setDatabaseConnection() または $config['connection'] で接続をインジェクトできるため、全体としてそれほど悪くありません。

データベース接続を更新する必要がある場合、例えば長時間実行される CLI スクリプトを実行していて、定期的に接続を更新する必要がある場合、$your_record->setDatabaseConnection($pdo_connection) で接続を再設定できます。

Contributing

ぜひ貢献してください。 :D

Setup

貢献する場合、composer test-coverage を実行して 100% のテスト カバレッジを維持してください(これは真のユニット テスト カバレッジではなく、統合テストに近いです)。

また、composer beautifycomposer phpcs を実行して、リンティング エラーを修正してください。

License

MIT

Awesome-plugins/latte

Latte

Latte は、非常に使いやすく、Twig や Smarty よりも PHP 構文に近いフル機能のテンプレートエンジンです。また、拡張して独自のフィルターや関数を追加することも非常に簡単です。

インストール

Composer でインストールします。

composer require latte/latte

基本設定

開始するための基本的な設定オプションがあります。これらについての詳細は、Latte ドキュメント を参照してください。


require 'vendor/autoload.php';

$app = Flight::app();

$app->map('render', function(string $template, array $data, ?string $block): void {
    $latte = new Latte\Engine;

    // Latte がキャッシュを格納する場所
    $latte->setTempDirectory(__DIR__ . '/../cache/');

    $finalPath = Flight::get('flight.views.path') . $template;

    $latte->render($finalPath, $data, $block);
});

シンプルなレイアウト例

ここにレイアウトファイルのシンプルな例を示します。これは、他のすべてのビューをラップするために使用されるファイルです。

<!-- app/views/layout.latte -->
<!doctype html>
<html lang="en">
    <head>
        <title>{$title ? $title . ' - '}My App</title>
        <link rel="stylesheet" href="style.css">
    </head>
    <body>
        <header>
            <nav>
                <!-- ここにナビゲーション要素を配置 -->
            </nav>
        </header>
        <div id="content">
            <!-- ここが魔法の部分です -->
            {block content}{/block}
        </div>
        <div id="footer">
            &copy; Copyright
        </div>
    </body>
</html>

そして、content ブロック内にレンダリングされるファイルです:

<!-- app/views/home.latte -->
<!-- これにより、Latte にこのファイルが layout.latte ファイルの「内部」であることを伝えます -->
{extends layout.latte}

<!-- レイアウト内の content ブロック内にレンダリングされるコンテンツです -->
{block content}
    <h1>ホームページ</h1>
    <p>私のアプリへようこそ!</p>
{/block}

次に、関数やコントローラー内でこれをレンダリングする場合、以下のようにします:

// シンプルなルート
Flight::route('/', function () {
    Flight::render('home.latte', [
        'title' => 'Home Page'
    ]);
});

// またはコントローラーを使用する場合
Flight::route('/', [HomeController::class, 'index']);

// HomeController.php
class HomeController
{
    public function index()
    {
        Flight::render('home.latte', [
            'title' => 'Home Page'
        ]);
    }
}

Latte を最大限に活用する方法の詳細については、Latte ドキュメント を参照してください!

Tracy を使用したデバッグ

このセクションには PHP 8.1+ が必要です。

Tracy を使用して、Latte テンプレートファイルをすぐにデバッグすることもできます! すでに Tracy をインストールしている場合、Tracy に Latte 拡張を追加する必要があります。

// services.php
use Tracy\Debugger;

$app->map('render', function(string $template, array $data, ?string $block): void {
    $latte = new Latte\Engine;

    // Latte がキャッシュを格納する場所
    $latte->setTempDirectory(__DIR__ . '/../cache/');

    $finalPath = Flight::get('flight.views.path') . $template;

    // Tracy デバッグバーが有効な場合のみ拡張を追加します
    if (Debugger::$showBar === true) {
        // ここで Tracy に Latte パネルを追加します
        $latte->addExtension(new Latte\Bridges\Tracy\TracyExtension);
    }
    $latte->render($finalPath, $data, $block);
});

Awesome-plugins/awesome_plugins

素晴らしいプラグイン

Flightは非常に拡張可能です。Flightアプリケーションに機能を追加するために使用できるプラグインがいくつかあります。一部はFlightチームによって公式にサポートされており、他のものは開始に役立つマイクロ/ライトライブラリです。

AIツール

FlightはAIを活用したプラグインでさらにクールにすることができます。

APIドキュメント

APIドキュメントはどのAPIにとっても重要です。開発者がAPIとの対話方法や期待される結果を理解するのに役立ちます。FlightプロジェクトのAPIドキュメントを生成するためのツールがいくつか利用可能です。

アプリケーションパフォーマンス監視 (APM)

アプリケーションパフォーマンス監視 (APM) はどのアプリケーションにとっても重要です。アプリケーションのパフォーマンスやボトルネックを理解するのに役立ちます。Flightで使用できるAPMツールがいくつかあります。

非同期処理

Flightはすでに高速なフレームワークですが、ターボエンジンを搭載することで、さらに楽しく(そして挑戦的になります)!

認可/権限

認可と権限は、誰が何にアクセスできるかを制御する必要があるアプリケーションにとって重要です。

認証

認証は、ユーザーIDの検証やAPIエンドポイントのセキュリティ保護が必要なアプリケーションに不可欠です。

キャッシュ

キャッシュはアプリケーションを高速化する素晴らしい方法です。Flightで使用できるキャッシュライブラリがいくつかあります。

CLI

CLIアプリケーションはアプリケーションと対話する素晴らしい方法です。コントローラーの生成、すべてのルートの表示などに使用できます。

クッキー

クッキーはクライアント側に少量のデータを保存する素晴らしい方法です。ユーザー設定、アプリケーション設定などの保存に使用できます。

デバッグ

ローカル環境での開発時には、デバッグが重要です。デバッグ体験を向上させるプラグインがいくつかあります。

データベース

データベースはほとんどのアプリケーションの中核です。データの保存と取得に使用されます。一部のデータベースライブラリはクエリを記述するための単なるラッパーであり、一部は本格的なORMです。

暗号化

暗号化は機密データを保存するアプリケーションにとって重要です。データの暗号化と復号化はそれほど難しくありませんが、暗号化キーの適切な保存は難しい場合があります難しい場合があります難しい場合があります。最も重要なことは、暗号化キーを公開ディレクトリに保存したり、コードリポジトリにコミットしたりしないことです。

ジョブキュー

ジョブキューはタスクを非同期で処理するのに非常に便利です。メールの送信、画像の処理、またはリアルタイムで行う必要のないタスクなどに使用できます。

セッション

セッションはAPIにはあまり役に立ちませんが、Webアプリケーションを構築する場合は、状態とログイン情報を維持するためにセッションが重要になります。

テンプレート

テンプレートはUIを持つWebアプリケーションの中核です。Flightで使用できるテンプレートエンジンがいくつかあります。

WordPress統合

WordPressプロジェクトでFlightを使用したいですか?それのための便利なプラグインがあります!

貢献

共有したいプラグインがありますか?プルリクエストを送信してリストに追加してください!

Media

メディア

Flight に関するインターネット上のさまざまな種類のメディアを可能な限り追跡しようとしました。Flight についてさらに学ぶために使用できるさまざまなリソースを以下に示します。

記事と解説

動画とチュートリアル

何か欠けているものはありますか?

あなたが書いたり録音したりしたものが欠けていませんか?イシューやプルリクエストでお知らせください!

Examples

クイックスタートが必要ですか?

新しい Flight プロジェクトを始めるための 2 つのオプションがあります:

コミュニティ提供の例:

インスピレーションが必要ですか?

これらは Flight チームの公式スポンサーではありませんが、Flight で構築した独自のプロジェクトの構造化方法についてのアイデアを提供する可能性があります!

自分の例を共有したいですか?

共有したいプロジェクトがある場合、このリストに追加するためのプルリクエストを送信してください!

Install/install

インストール手順

Flightをインストールする前に、いくつかの前提条件があります。具体的には以下が必要です:

  1. システムにPHPをインストール
  2. 最良の開発体験のためにComposerをインストール

基本的なインストール

Composerを使用している場合は、次のコマンドを実行できます:

composer require flightphp/core

これにより、Flightのコアファイルだけがシステムにインストールされます。プロジェクト構造、レイアウト依存関係設定オートローディングなどは自分で定義する必要があります。この方法では、Flight以外の依存関係はインストールされません。

ファイルをダウンロードして、Webディレクトリに直接展開することもできます。

基本的なインストールは、学習、マイクロAPI、コピー&ペーストの実験に最適です。人間とAIコーディングツールが同じ方法で従える完全なアプリレイアウトが必要な場合は、以下の推奨スケルトンを使用してください。

推奨インストール

新しいプロジェクトには、flightphp/skeletonアプリから始めることを強くお勧めします。インストールは簡単です。

composer create-project flightphp/skeleton my-project/
cd my-project/
composer start
# オプションのサンプルDB + 投稿デモ
php runway migrate

このステップにより、プロジェクト構造、Composer PSR-4オートローディング、設定、およびTracyTracy ExtensionsRunwayなどのツールがセットアップされます。また、ルートのAGENTS.md(およびapp/配下のスコープ付きコピー)が同梱されているため、AIアシスタントはあなたと同じレイアウトを共有できます。AIと開発者体験を参照してください。

スケルトンが提供するもの

project-root/
├── AGENTS.md              # AI / エージェントの情報源
├── SECURITY.md            # セキュリティの期待値
├── .env.example           # シークレット / デプロイオーバーレイ(.envにコピー)
├── public/index.php       # Webエントリのみ
├── app/
│   ├── config/            # bootstrap、routes、services、config_sample.php
│   ├── Controller/        # App\Controller\*(パスカルケースのフォルダ!)
│   ├── Middleware/        # App\Middleware\*
│   ├── Model/             # App\Model\*(ActiveRecord)
│   ├── Utils/             # Config、Env、DatabaseFactory
│   ├── commands/          # Runway CLIコマンド
│   ├── views/             # Twigテンプレート(*.twig)
│   ├── cache/
│   └── log/
├── migrations/            # SQLマイグレーション(.sql / .mysql.sql)
└── tests/                 # PHPUnit

名前空間はフォルダーの大文字小文字に従います。 Composerは"App\\": "app/"をマッピングするため、次のようになります:

ディスク上のパス 名前空間
app/Controller/HomeController.php App\Controller\HomeController
app/Middleware/… App\Middleware\…
app/Model/… App\Model\…
app/Utils/… App\Utils\…

Linuxでは、app/controller/app/Controller/と同じではありません。オートローディングは大文字小文字を区別します。スケルトンのパスカルケースフォルダーに合わせてください。詳細:オートローディング

スタックのデフォルト(新規プロジェクト): Twigビュー、SimplePdo + ActiveRecord、Engine注入を使用したDice(アプリクラス内でのFlight::の使用は避ける)、php runway migrate後のオプションのSQLite。

create-projectは通常、app/config/config_sample.phpconfig.phpに、.env.example.envにコピーします(存在する場合)。ルートはapp/config/routes.phpに、サービスとDIはapp/config/services.phpにあります。

ドキュメント ↔ スケルトン: これらのドキュメントはFlightのAPIを教えます(多くの場合、短いFlight::サンプルを使用)。スケルトンはアプリケーションの形を固定します。app/配下にコードを追加する場合は、スケルトンのツリーに従ってください。メソッド名、オプション、プラグインについてはドキュメントを使用してください。

Webサーバーの設定

PHPビルトイン開発サーバー

これは断然最も簡単な起動方法です。ビルトインサーバーを使用してアプリケーションを実行でき、データベースにSQLiteを使用することもできます(システムにsqlite3がインストールされていれば)。PHPがインストールされていれば、次のコマンドを実行するだけです:

php -S localhost:8000
# またはスケルトンアプリの場合
composer start

その後、ブラウザを開いてhttp://localhost:8000にアクセスします。

プロジェクトのドキュメントルートを別のディレクトリにしたい場合(例:プロジェクトが~/myprojectで、ドキュメントルートが~/myproject/public/の場合)、~/myprojectディレクトリにいる状態で次のコマンドを実行できます:

php -S localhost:8000 -t public/
# スケルトンアプリでは、これは既に設定されています
composer start

その後、ブラウザを開いてhttp://localhost:8000にアクセスします。

Apache

Apacheがシステムにインストールされていることを確認してください。インストールされていない場合は、お使いのシステムへのApacheのインストール方法をGoogleで検索してください。

Apacheの場合、.htaccessファイルを次のように編集します:

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L]

注: flightをサブディレクトリで使用する必要がある場合は、RewriteEngine Onの直後にRewriteBase /subdir/の行を追加してください。

注: DBやenvファイルなど、すべてのサーバーファイルを保護したい場合は、.htaccessファイルに次のように記述します:

RewriteEngine On
RewriteRule ^(.*)$ index.php

Nginx

Nginxがシステムにインストールされていることを確認してください。インストールされていない場合は、お使いのシステムへのNginxのインストール方法をGoogleで検索してください。

Nginxの場合、サーバー宣言に次を追加します:

server {
  location / {
    try_files $uri $uri/ /index.php;
  }
}

index.phpファイルを作成する

基本的なインストールを行う場合は、開始するためのコードが必要です。

<?php

// Composerを使用している場合は、オートローダーを読み込みます。
require 'vendor/autoload.php';
// Composerを使用しない場合は、フレームワークを直接読み込みます
// require 'flight/Flight.php';

// 次にルートを定義し、リクエストを処理する関数を割り当てます。
Flight::route('/', function () {
  echo 'hello world!';
});

// 最後に、フレームワークを起動します。
Flight::start();

スケルトンアプリでは、パブリックエントリはアプリを起動するだけです。ルートはapp/config/routes.phpで登録されます(通常は[App\Controller\…::class, 'method']の形式で、Diceが依存関係を注入できるようにします)。サービス、Twig、SimplePdo、コンテナはapp/config/services.phpで配線されます。この構造は、AIツールと人間が毎回同じ場所を編集するように意図されています。

PHPのインストール

お使いのシステムにphpが既にインストールされている場合は、これらの手順をスキップしてダウンロードセクションに進んでください。

macOS

Homebrewを使用したPHPのインストール

  1. Homebrewをインストール(まだインストールされていない場合):

    • ターミナルを開いて実行:
      /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
  2. PHPをインストール

    • 最新バージョンをインストール:
      brew install php
    • 特定のバージョン(例:PHP 8.1)をインストールする場合:
      brew tap shivammathur/php
      brew install shivammathur/php/php@8.1
  3. PHPバージョンの切り替え

    • 現在のバージョンのリンクを解除し、希望するバージョンにリンク:
      brew unlink php
      brew link --overwrite --force php@8.1
    • インストールされたバージョンを確認:
      php -v

Windows 10/11

PHPの手動インストール

  1. PHPをダウンロード

    • PHP for Windowsにアクセスし、最新版または特定のバージョン(例:7.4、8.0)を非スレッドセーフ版のzipファイルとしてダウンロードします。
  2. PHPを展開

    • ダウンロードしたzipファイルをC:\phpに展開します。
  3. PHPをシステムのPATHに追加

    • システムのプロパティ > 環境変数に移動します。
    • システム変数の下でPathを見つけ、編集をクリックします。
    • パスC:\php(またはPHPを展開した場所)を追加します。
    • OKをクリックしてすべてのウィンドウを閉じます。
  4. PHPを設定

    • php.ini-developmentphp.iniにコピーします。
    • php.iniを編集して、必要に応じてPHPを設定します(例:extension_dirの設定、拡張機能の有効化)。
  5. PHPのインストールを確認

    • コマンドプロンプトを開いて実行:
      php -v

複数バージョンのPHPをインストール

  1. 各バージョンについて上記の手順を繰り返し、それぞれを別のディレクトリ(例:C:\php7C:\php8)に配置します。

  2. システムのPATH変数を目的のバージョンのディレクトリに調整して、バージョンを切り替えます

Ubuntu(20.04、22.04など)

aptを使用したPHPのインストール

  1. パッケージリストを更新

    • ターミナルを開いて実行:
      sudo apt update
  2. PHPをインストール

    • 最新のPHPバージョンをインストール:
      sudo apt install php
    • 特定のバージョン(例:PHP 8.1)をインストールする場合:
      sudo apt install php8.1
  3. 追加モジュールをインストール(オプション):

    • 例えば、MySQLサポートをインストールする場合:
      sudo apt install php8.1-mysql
  4. PHPバージョンの切り替え

    • update-alternativesを使用:
      sudo update-alternatives --set php /usr/bin/php8.1
  5. インストールされたバージョンを確認

    • 実行:
      php -v

Rocky Linux

yum/dnfを使用したPHPのインストール

  1. EPELリポジトリを有効化

    • ターミナルを開いて実行:
      sudo dnf install epel-release
  2. Remiリポジトリをインストール

    • 実行:
      sudo dnf install https://rpms.remirepo.net/enterprise/remi-release-8.rpm
      sudo dnf module reset php
  3. PHPをインストール

    • デフォルトバージョンをインストール:
      sudo dnf install php
    • 特定のバージョン(例:PHP 7.4)をインストールする場合:
      sudo dnf module install php:remi-7.4
  4. PHPバージョンの切り替え

    • dnfモジュールコマンドを使用:
      sudo dnf module reset php
      sudo dnf module enable php:remi-8.0
      sudo dnf install php
  5. インストールされたバージョンを確認

    • 実行:
      php -v

一般的な注意事項

Guides

ガイド

Flight PHP はシンプルでありながら強力に設計されており、私たちのガイドは実際のアプリケーションをステップバイステップで構築するのに役立ちます。これらの実践的なチュートリアルは、Flight を効果的に使用する方法を示すために、完全なプロジェクトを説明します。

公式ガイド

Building a Blog

Flight PHP を使用して機能的なブログアプリケーションを作成する方法を学びます。このガイドでは以下を説明します:

このチュートリアルは、実際のアプリケーションですべての部品がどのように組み合わさるかを理解したい初心者向けです。

Unit Testing and SOLID Principles

このガイドは、Flight PHP アプリケーションでのユニットテストの基本をカバーします。以下を含みます:

非公式ガイド

これらのガイドは Flight チームによって公式にメンテナンスされているわけではありませんが、コミュニティによって作成された貴重なリソースです。さまざまなトピックとユースケースをカバーし、Flight PHP の使用に関する追加の洞察を提供します。

Creating a RESTful API with Flight Framework

このガイドでは、Flight PHP フレームワークを使用して RESTful API を作成する方法を説明します。API の基本的な設定、ルートの定義、JSON 応答の返却をカバーします。

Building a Simple Blog

このガイドでは、Flight PHP フレームワークを使用して基本的なブログを作成する方法を説明します。実際には 2 つのパートがあり、1 つは基本をカバーし、もう 1 つは本番環境向けの高度なトピックと改良をカバーします。

Building a Pokémon API in PHP: A Beginner's Guide

この楽しいガイドでは、Flight PHP を使用してシンプルな Pokémon API を作成する方法を説明します。API の基本的な設定、ルートの定義、JSON 応答の返却をカバーします。

貢献

ガイドのアイデアがありますか? 間違いを見つけましたか? 貢献を歓迎します! 私たちのガイドは FlightPHP ドキュメント リポジトリ でメンテナンスされています。

Flight で興味深いものを構築し、それをガイドとして共有したい場合、プルリクエストを送信してください。知識を共有することで Flight コミュニティを成長させることができます。

API ドキュメントをお探しですか?

Flight のコア機能とメソッドに関する特定の情報を探している場合、私たちのドキュメントの Learn セクションを確認してください。