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

理解

Flight の特定の動作をカスタマイズするには、set メソッドを使用して構成値を設定できます。

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

app/config/config.php ファイルでは、利用可能なすべてのデフォルト構成変数を確認できます。

基本的な使用方法

Flight 構成オプション

以下のリストは、利用可能なすべての構成設定です:

ローダー構成

ローダーには追加の構成設定があります。これにより、クラス名に _ を含むクラスを自動ロードできます。

// アンダースコア付きのクラスロードを有効にする
// デフォルトは true
Loader::$v2ClassLoading = false;

変数

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

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

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

変数が設定されているかどうかを確認するには:

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

変数をクリアするには:

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

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

注意: 変数を設定できるからといって、設定すべきとは限りません。この機能は控えめに使用してください。理由は、ここに保存されたものはグローバル変数になるためです。グローバル変数は、アプリケーションのどこからでも変更可能であるため、バグの追跡が難しくなるため悪いのです。また、これにより ユニットテスト などのことが複雑になります。

エラーと例外

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

デフォルトの動作は、一般的な HTTP 500 Internal Server Error 応答をいくつかのエラー情報とともに送信することです。

この動作を独自のニーズに合わせて 上書き できます:

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

デフォルトでは、エラーはウェブサーバーにログされません。これを有効にするには、構成を変更します:

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 駆動のツールと現代的な開発者ワークフローを使用して PHP プロジェクトを強化することを容易にします。LLM (Large Language Model) プロバイダへの接続やプロジェクト固有の AI コーディング指示の生成のための組み込みコマンドにより、Flight は GitHub Copilot、Cursor、Windsurf などの AI アシスタントから最大限の効果を得るのをあなたとあなたのチームを支援します。

理解

AI コーディングアシスタントは、プロジェクトのコンテキスト、慣習、目標を理解しているときに最も役立ちます。Flight の AI ヘルパーは以下のことを可能にします:

これらの機能は、Flight コア CLI と公式の flightphp/skeleton スタータープロジェクトに組み込まれています。

基本的な使用方法

LLM 認証情報の設定

ai:init コマンドは、プロジェクトを LLM プロバイダに接続するためのガイドを提供します。

php runway ai:init

以下のプロンプトが表示されます:

これにより、プロジェクトルートに .runway-creds.json ファイルが作成され (.gitignore に含まれることを確認)、。

例:

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 プロバイダを使用して指示を生成し、次のファイルに書き込みます:

例:

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)? latte
Is security an important element of this project? (y/n) y
...
AI instructions updated successfully.

これで、AI ツールはプロジェクトの実ニーズに基づいたより賢く関連性の高い提案を提供します。

高度な使用方法

関連項目

トラブルシューティング

変更履歴

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

セキュリティ

概要

ウェブアプリケーションにおいてセキュリティは重要な問題です。アプリケーションが安全であることを確認し、ユーザーのデータが安全であることを保証する必要があります。Flight は、ウェブアプリケーションを保護するためのいくつかの機能を提供します。

理解

ウェブアプリケーションを構築する際に注意すべき一般的なセキュリティ脅威がいくつかあります。最も一般的な脅威には以下が含まれます:

Templates は、デフォルトで出力をエスケープすることで XSS を防ぎます。これを覚えておく必要はありません。Sessions は、以下の説明のようにユーザーのセッションに CSRF トークンを保存することで CSRF を防ぐのに役立ちます。PDO でプリペアドステートメントを使用すると SQL インジェクション攻撃を防げます(または PdoWrapper クラスの便利なメソッドを使用)。CORS は、Flight::start() が呼び出される前のシンプルなフックで処理できます。

これらの方法はすべて連携してウェブアプリケーションを安全に保つのに役立ちます。常にセキュリティのベストプラクティスを学び、理解することが重要です。

基本的な使用方法

ヘッダー

HTTP ヘッダーは、ウェブアプリケーションを保護する最も簡単な方法の一つです。ヘッダーを使用してクリックジャッキング、XSS、その他の攻撃を防げます。これらのヘッダーをアプリケーションに追加する方法がいくつかあります。

ヘッダーのセキュリティを確認するための優れたウェブサイトは securityheaders.comobservatory.mozilla.org です。以下のコードを設定した後、これらのウェブサイトでヘッダーが動作しているかを簡単に確認できます。

手動で追加

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 レスポンスに適用されるべきです。

// app/middlewares/SecurityHeadersMiddleware.php

namespace app\middlewares;

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();
        $response->header('X-Frame-Options', 'SAMEORIGIN');
        $response->header("Content-Security-Policy", "default-src 'self'");
        $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=()');
    }
}

// index.php またはルートがある場所
// FYI、この空の文字列グループはすべてのルートのグローバルミドルウェアとして機能します。
// もちろん、特定のルートにのみ追加することもできます。
Flight::group('', function(Router $router) {
    $router->get('/users', [ 'UserController', 'getUsers' ]);
    // 他のルート
}, [ SecurityHeadersMiddleware::class ]);

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

クロスサイトリクエストフォージェリ (CSRF) は、悪意のあるウェブサイトがユーザーのブラウザからあなたのウェブサイトへのリクエストを送信できる攻撃の一種です。これにより、ユーザーの知識なしにウェブサイト上でアクションを実行できます。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>
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/middlewares/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');
            }
        }
    }
}

// index.php またはルートがある場所
use app\middlewares\CsrfMiddleware;

Flight::group('', function(Router $router) {
    $router->get('/users', [ 'UserController', 'getUsers' ]);
    // 他のルート
}, [ CsrfMiddleware::class ]);
イベントフィルター
// このミドルウェアは、リクエストが 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) は、悪意のあるフォーム入力がウェブサイトにコードを注入できる攻撃の一種です。これらの機会のほとんどは、エンドユーザーが記入するフォーム値から来ます。ユーザーの出力は 決して 信頼しないでください! すべてが世界最高のハッカーと仮定してください。彼らはページに悪意のある JavaScript や HTML を注入できます。このコードは、ユーザーの情報を盗んだり、ウェブサイト上でアクションを実行したりするために使用できます。Flight のビュークラスや Latte のような別のテンプレートエンジンを使用すると、出力をエスケープして XSS 攻撃を簡単に防げます。

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

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

// Latte をビュークラスとして登録した場合、これも自動的にエスケープされます
Flight::view()->render('template', ['name' => $name]);

SQL インジェクション

SQL インジェクションは、悪意のあるユーザーがデータベースに SQL コードを注入できる攻撃の一種です。これにより、データベースから情報を盗んだり、データベース上でアクションを実行したりできます。再び、ユーザーの入力は 決して 信頼しないでください! 常に彼らが悪意を持っていると仮定してください。PDO オブジェクトでプリペアドステートメントを使用すると SQL インジェクションを防げます。

// Flight::db() を PDO オブジェクトとして登録したと仮定
$statement = Flight::db()->prepare('SELECT * FROM users WHERE username = :username');
$statement->execute([':username' => $username]);
$users = $statement->fetchAll();

// PdoWrapper クラスを使用する場合、1行で簡単に実行できます
$users = Flight::db()->fetchAll('SELECT * FROM users WHERE username = :username', [ 'username' => $username ]);

// ? プレースホルダー付きの PDO オブジェクトでも同じことができます
$statement = Flight::db()->fetchAll('SELECT * FROM users WHERE username = ?', [ $username ]);

非セキュアな例

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

// エンドユーザーがウェブフォームを記入。
// フォームの値に、ハッカーが以下のようなものを入力:
$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); // これにより、データベース内のすべてのユーザーがダンプされ、単一のユーザー名だけではありません

CORS

クロスオリジンリソースシェアリング (CORS) は、ウェブページ上の多くのリソース(例: フォント、JavaScript など)が、リソースが起源となったドメイン外の別のドメインからリクエストできるメカニズムです。Flight にはビルトインの機能はありませんが、Flight::start() メソッドが呼び出される前のフックで簡単に処理できます。

// app/utils/CorsUtil.php

namespace app\utils;

class CorsUtil
{
    public function set(array $params): void
    {
        $request = Flight::request();
        $response = Flight::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 = Flight::request();

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

// index.php またはルートがある場所
$CorsUtil = new CorsUtil();

// start が実行される前にこれを実行する必要があります。
Flight::before('start', [ $CorsUtil, 'setupCors' ]);

エラーハンドリング

プロダクションでは、攻撃者に情報を漏らさないよう、機密のエラー詳細を非表示にします。プロダクションでは、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 を使用してサニタイズします。


// $_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 パターンをコールバック関数やクラスメソッドにマッピングし、高速でシンプルなリクエスト処理を可能にします。最小限のオーバーヘッド、初心者向けの使いやすさ、外部依存なしの拡張性を目的として設計されています。

理解

ルーティングは、Flight で 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 は HEADOPTIONS 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 = 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 はマッチしません
});

注意: 位置パラメータとのマッチング用 regex グループ () はサポートされていません。例: :'\(

重要な注意事項

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

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

以下の URL にアクセスした場合:/bob/123、出力は 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();
});

メソッドが見つからないハンドラー

デフォルトでは、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\PdoWrapper;

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

    public function hello(int $id) {
        // $this->pdoWrapper で何かを実行
        $name = $this->pdoWrapper->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('flight\database\PdoWrapper', [
    '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 () {
  // これが呼び出されます
});

複雑なユースケースを扱うには、ミドルウェア を使用することを推奨します。

ルートエイリアス

ルートにエイリアスを割り当てることで、後でアプリ内で動的にそのエイリアスを呼び出してコード内で生成できます(例: 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 が変更された場合に特に役立ちます。上記の例で、ユーザーが /admin/users/@id に移動したとします。ルートにエイリアスを設定しているため、コード内のすべての古い URL を探して変更する必要はありません。エイリアスは上記の例のように /admin/users/5 を返します。

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

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

onlyexcept オプションを使用して、作成するルートを指定できます。

// これらのメソッドのみホワイトリストし、残りをブラックリスト
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() メソッドを使用してクライアントにレスポンスをストリーミングできます。 これを行う場合、クライアントに出力する前にすべてのヘッダーを手動で設定する必要があります。 これは header() PHP 関数または 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 向けの高速でシンプルで拡張可能なフレームワークです。非常に汎用性が高く、あらゆる種類のウェブアプリケーションを構築するために使用できます。 シンプルさを念頭に置いて構築されており、理解しやすく使用しやすい方法で記述されています。

注意: Flight:: を静的変数として使用する例と、$app-> Engine オブジェクトを使用する例の両方を見ることになります。これらは互換性があり、どちらも使用可能です。コントローラー/ミドルウェア内の $app および $this->app が Flight チームのおすすめのアプローチです。

コアコンポーネント

Routing

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

Middleware

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

Autoloading

アプリケーション内で独自のクラスをオートロードする方法を学びます。

Requests

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

Responses

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

HTML Templates

ビルトインのビューエンジンを使用して HTML テンプレートをレンダリングする方法を学びます。

Security

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

Configuration

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

Event Manager

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

Extending Flight

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

Method Hooks and Filtering

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

Dependency Injection Container (DIC)

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

ユーティリティクラス

Collections

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

JSON Wrapper

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

PDO Wrapper

PDO は時に必要以上に頭痛の種になることがあります。このシンプルなラッパークラスにより、データベースとのやり取りが大幅に簡単になります。

Uploaded File Handler

アップロードされたファイルを管理し、パーマネントな場所に移動するのを支援するシンプルなクラスです。

重要な概念

なぜフレームワークか?

フレームワークを使用する理由についての短い記事です。フレームワークを使用する前に、その利点を理解しておくのが良い考えです。

さらに、@lubiana によって作成された優れたチュートリアルがあります。Flight について特に詳細に触れていませんが、 このガイドはフレームワークを取り巻く主要な概念とその利点について理解するのに役立ちます。 チュートリアルは ここ から見つかります。

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

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

その他のトピック

ユニットテスト

Flight のコードを堅牢にするためのユニットテストの方法を学ぶためのガイドに従ってください。

AI & Developer Experience

Flight が 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\PdoWrapper;

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\PdoWrapper::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 ヘルパー クラス

概要

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

理解

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

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

基本的な使用方法

PDO ヘルパーの登録

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

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>

すべての行を Collections の配列として取得します:

$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) は、アプリケーションの依存関係を管理するための強力な拡張機能です。

理解

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

DIC は、クラスを作成し管理するための中央集権的な方法を提供する洗練された方法です。これは、同じオブジェクトを複数のクラス(例: コントローラーやミドルウェア)に渡す必要がある場合に便利です。

基本的な使用方法

従来の方法は次のようになります:


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 オブジェクトを必要とするコンストラクタを持っている場合
// 以下の各ルートに自動的に注入されます!!!
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 を利用する追加の利点は、ユニットテストがはるかに簡単になることです。モックオブジェクトを作成し、クラスに渡すことができます。これは、アプリケーションのテストを書く際に大きな利点です!

中央集権的な DIC ハンドラの作成

拡張 により、アプリケーションを拡張することで、services ファイルに中央集権的な 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) {
    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 インスタンス

コントローラー/ミドルウェアで Engine インスタンスを使用している場合の設定方法は以下の通りです:


// ブートストラップファイルのどこかで
$engine = Flight::app();

$container = new \Dice\Dice;
$container = $container->addRule('*', [
    'substitutions' => [
        // ここでインスタンスを渡します
        Engine::class => $engine
    ]
]);

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

// これでコントローラー/ミドルウェアで Engine インスタンスを使用できます

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

    public function index() {
        $this->app->render('index');
    }
}

他のクラスの追加

コンテナに追加したい他のクラスがある場合、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';

// 上記と同じ UserController クラス

$container = new \League\Container\Container();
$container->add(UserController::class)->addArgument(PdoWrapper::class);
$container->add(PdoWrapper::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 テンプレート機能を備えています。テンプレートは、アプリケーションのロジックをプレゼンテーション層から分離する非常に効果的な方法です。

理解

アプリケーションを構築する際、エンドユーザーに返す HTML を準備する必要があるでしょう。PHP 自体がテンプレート言語ですが、データベース呼び出し、API 呼び出しなどのビジネスロジックを HTML ファイルに混ぜ込んでしまうと、テストや分離が非常に困難になります。データをテンプレートに押し込み、テンプレート自身にレンダリングさせることで、コードの分離と単体テストがはるかに容易になります。テンプレートを使用すれば、私たちに感謝するはずです!

基本的な使用方法

Flight では、デフォルトのビューエンジンを置き換えるために、独自のビュー クラスを登録するだけで簡単に切り替えられます。Smarty、Latte、Blade などの使用例を見るには、下にスクロールしてください!

Latte

推奨

Latte テンプレート エンジンをビューで使用する方法を以下に示します。

インストール

composer require latte/latte

基本的な設定

主なアイデアは、render メソッドをオーバーライドして、デフォルトの PHP レンダラーではなく Latte を使用することです。

// overwrite the render method to use latte instead of the default PHP renderer
Flight::map('render', function(string $template, array $data, ?string $block): void {
    $latte = new Latte\Engine;

    // Where latte specifically stores its cache
    $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');

レイアウト

ウェブサイトでは、交換可能なコンテンツを持つ単一のレイアウト テンプレート ファイルが一般的です。レイアウトで使用するコンテンツをレンダリングするには、render メソッドにオプションのパラメータを渡せます。

Flight::render('header', ['heading' => 'Hello'], 'headerContent');
Flight::render('body', ['body' => 'World'], 'bodyContent');

ビューには、headerContentbodyContent という名前の変数が保存されます。次に、レイアウトを以下のようにレンダリングできます:

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 テンプレート エンジンをビューで使用する方法を以下に示します:

// Load Smarty library
require './Smarty/libs/Smarty.class.php';

// Register Smarty as the view class
// Also pass a callback function to configure Smarty on load
Flight::register('view', Smarty::class, [], function (Smarty $smarty) {
  $smarty->setTemplateDir('./templates/');
  $smarty->setCompileDir('./templates_c/');
  $smarty->setConfigDir('./config/');
  $smarty->setCacheDir('./cache/');
});

// Assign template data
Flight::view()->assign('name', 'Bob');

// Display the template
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
// Load BladeOne library
use eftec\bladeone\BladeOne;

// Register BladeOne as the view class
// Also pass a callback function to configure BladeOne on load
Flight::register('view', BladeOne::class, [], function (BladeOne $blade) {
  $views = __DIR__ . '/../views';
  $cache = __DIR__ . '/../cache';

  $blade->setPath($views);
  $blade->setCompiledPath($cache);
});

// Assign template data
Flight::view()->share('name', 'Bob');

// Display the template
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/collections

Collections

概要

Flight の Collection クラスは、データセットを管理するための便利なユーティリティです。配列表記とオブジェクト表記を使用してデータにアクセスおよび操作できるため、コードをよりクリーンで柔軟にします。

理解

Collection は基本的に配列のラッパーですが、いくつかの追加機能があります。配列のように使用でき、ループ処理、項目のカウント、さらにはオブジェクトのプロパティのように項目にアクセスできます。これは、アプリ内で構造化されたデータを渡す場合や、コードを少し読みやすくする場合に特に便利です。

Collections はいくつかの PHP インターフェースを実装しています:

基本的な使用方法

Collection の作成

コンストラクタに配列を渡すことでコレクションを作成できます:

use flight\util\Collection;

$data = [
  'name' => 'Flight',
  'version' => 3,
  'features' => ['routing', 'views', 'extending']
];

$collection = new Collection($data);

項目へのアクセス

配列表記またはオブジェクト表記を使用して項目にアクセスできます:

// 配列表記
echo $collection['name']; // 出力: Flight

// オブジェクト表記
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);

Collection のイテレーション

Collections はイテラブルなので、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 のクリア

すべての項目を削除:

$collection->clear();

JSON シリアライズ

Collections を簡単に JSON に変換できます:

echo json_encode($collection);
// 出力: {"name":"Flight","version":3,"features":["routing","views","extending"],"license":"MIT"}

高度な使用方法

必要に応じて、内部データ配列を完全に置き換えることができます:

$collection->setData(['foo' => 'bar']);

Collections は、コンポーネント間で構造化されたデータを渡す場合や、配列データに対してよりオブジェクト指向のインターフェースを提供する場合に特に便利です。

関連項目

トラブルシューティング

変更履歴

Learn/flight_vs_fat_free

Flight vs Fat-Free

Fat-Free とは?

Fat-Free(愛情を込めて F3 と呼ばれる)は、動的で堅牢なウェブアプリケーションを迅速に構築するための強力で使いやすい PHP マイクロフレームワークです。

Flight は Fat-Free と多くの点で比較でき、機能とシンプルさの点で最も近い親戚です。Fat-Free には Flight にない機能がたくさんありますが、Flight にもある機能もたくさんあります。Fat-Free は古さを見せ始め、かつてほど人気はありません。

更新は徐々に少なくなり、コミュニティもかつてほど活発ではありません。コードは十分にシンプルですが、文法の規律の欠如が読みにくく理解しにくくすることがあります。PHP 8.3 で動作しますが、コード自体はまだ PHP 5.3 に住んでいるように見えます。

Flight との Pros

Flight との Cons

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 は、シンプルでありながら強力なウェブアプリケーションと API を迅速に記述するための PHP マイクロフレームワークです。

Flight の v3 のいくつかの機能のインスピレーションは、実際には Slim から来ています。ルートのグループ化と、特定の順序でミドルウェアを実行する機能は、Slim から着想を得たものです。Slim v3 はシンプルさを重視してリリースされましたが、v4 については混合したレビューがあります。

Flight との比較での利点

Flight との比較での欠点

Learn/autoloading

オートローディング

概要

オートローディングは、PHPの概念で、クラスをロードするためのディレクトリまたはディレクトリを指定します。これは、requireinclude を使用してクラスをロードするよりもはるかに有益です。また、Composerパッケージを使用するための要件でもあります。

理解

デフォルトでは、Flight のクラスは Composer のおかげで自動的にオートロードされます。ただし、自分のクラスをオートロードしたい場合は、Flight::path() メソッドを使用してクラスをロードするためのディレクトリを指定できます。

オートローダーを使用することで、コードを大幅に簡素化できます。ファイルの先頭に多数の includerequire 文を記述して、そのファイルで使用されるすべてのクラスをキャプチャする代わりに、クラスを動的に呼び出すだけで自動的にインクルードされます。

基本的な使用方法

以下のディレクトリツリーがあると仮定しましょう:

# 例のパス
/home/user/project/my-flight-project/
├── app
│   ├── cache
│   ├── config
│   ├── controllers - このプロジェクトのコントローラーを含む
│   ├── translations
│   ├── UTILS - このアプリケーション専用のクラスを含む(後で例を示すために意図的に大文字)
│   └── views
└── public
    └── css
    └── js
    └── index.php

このドキュメントサイトと同じファイル構造であることに気づいたかもしれません。

各ディレクトリをロードするための指定は以下のようになります:


/**
 * public/index.php
 */

// オートローダーにパスを追加
Flight::path(__DIR__.'/../app/controllers/');
Flight::path(__DIR__.'/../app/utils/');

/**
 * app/controllers/MyController.php
 */

// 名前空間は不要

// オートロードされるすべてのクラスは Pascal Case を推奨(各単語の先頭を大文字、スペースなし)
class MyController {

    public function index() {
        // 何かを行う
    }
}

名前空間

名前空間を使用する場合、これは非常に簡単に実装できます。アプリケーションのルートディレクトリ(ドキュメントルートや public/ フォルダではない)を指定するために Flight::path() メソッドを使用するべきです。


/**
 * public/index.php
 */

// オートローダーにパスを追加
Flight::path(__DIR__.'/../');

これがコントローラーの例です。以下の例を見て、重要な情報のためにコメントに注意してください。

/**
 * app/controllers/MyController.php
 */

// 名前空間は必須
// 名前空間はディレクトリ構造と同じ
// 名前空間はディレクトリ構造と同じケースに従う
// 名前空間とディレクトリにはアンダースコアを含められない(Loader::setV2ClassLoading(false) を設定しない限り)
namespace app\controllers;

// オートロードされるすべてのクラスは Pascal Case を推奨(各単語の先頭を大文字、スペースなし)
// 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) {
        // 何かを行う
    }
}

クラス名のアンダースコア

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 の場合、オートローダーはそれを見つけられません。

名前空間が不正

名前空間を使用している場合、名前空間はディレクトリ構造に一致するべきです。

// ...code...

// MyController が app/controllers ディレクトリにあり、名前空間付きの場合
// これは動作しません。
Flight::route('/hello', 'MyController->hello');

// これらのオプションのいずれかを選択する必要があります
Flight::route('/hello', 'app\controllers\MyController->hello');
// または上部に use 文がある場合

use app\controllers\MyController;

Flight::route('/hello', [ MyController::class, 'hello' ]);
// また、次のように書けます
Flight::route('/hello', MyController::class.'->hello');
// また...
Flight::route('/hello', [ 'app\controllers\MyController', 'hello' ]);

path() が定義されていない

スケルトンアプリでは、これは config.php ファイル内で定義されていますが、クラスが見つかるためには、path() メソッドが定義されていることを確認する必要があります(おそらくディレクトリのルートに)。使用しようとする前に。

// オートローダーにパスを追加
Flight::path(__DIR__.'/../');

変更履歴

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 変数を使用する習慣を付けましょう。$appFlight:: と同じメソッドを持ちます。コントローラーなどで $app->route()$this->app->json() を使用できます。また、実際の Flight ルーターを $router = $app->router(); で使用し、$router->get()$router->post()$router->group() などを利用してください。Routing を参照。
  3. テストを高速に保つ:高速なテストは頻繁な実行を促します。ユニットテストではデータベース呼び出しのような遅い操作を避けます。テストが遅い場合、それは統合テストを書いているサインで、ユニットテストではありません。統合テストは実際のデータベース、HTTP 呼び出し、メール送信などを含みます。それらは有用ですが、遅く不安定で、未知の理由で失敗することがあります。
  4. 記述的な名前を使用する:テスト名はテストされる動作を明確に記述すべきです。これにより読みやすさと保守性が向上します。
  5. グローバル変数を避ける$app->set()$app->get() の使用を最小限にし、それらはグローバル状態として機能し、すべてのテストでモックを必要とします。DI または DI コンテナ(Dependency Injection Container を参照)を優先してください。$app->map() メソッドの使用も技術的には「グローバル」なので、DI を優先して避けます。flightphp/session のようなセッションワイブラリを使用し、テストでセッションオブジェクトをモックできるようにします。コードで $_SESSION を直接呼び出さないでください。それはグローバル変数を注入し、テストを難しくします。
  6. 依存性注入を使用する:コントローラーに依存(例:PDO、メーラー)を注入し、ロジックを分離してモッキングを簡素化します。依存が多すぎるクラスがある場合、SOLID principles に従った単一責任の小さなクラスにリファクタリングを検討してください。
  7. サードパーティサービスをモックする:データベース、HTTP クライアント(cURL)、メールサービスをモックし、外部呼び出しを避けます。1 つか 2 層深くテストし、コアロジックを実行します。例えば、アプリがテキストメッセージを送信する場合、テストごとに実際のメッセージを送信したくありません(料金がかかり、遅くなります)。代わりにテキストメッセージサービスをモックし、コードが正しいパラメータでサービスを呼び出したかを検証します。
  8. 完全ではなく高いカバレッジを目指す:100% 行カバレッジは良いですが、コードが正しくテストされていることを意味しません(PHPUnit でのブランチ/パス カバレッジ を調べてください)。重要な動作(例:ユーザー登録、API レスポンス、失敗レスポンスのキャプチャ)を優先します。
  9. ルートにコントローラーを使用する:ルート定義ではクロージャではなくコントローラーを使用します。flight\Engine $app はデフォルトでコンストラクタ経由ですべてのコントローラーに注入されます。テストでは $app = new Flight\Engine(); で Flight をインスタンス化し、コントローラーに注入し、メソッドを直接呼び出します(例:$controller->register())。Extending FlightRouting を参照。
  10. モッキングスタイルを選んで一貫させる:PHPUnit は複数のモッキングスタイル(例:prophecy、内蔵モック)をサポートします。匿名クラスもコード補完やメソッド定義変更時の破損などの利点があります。テスト全体で一貫してください。PHPUnit Mock Objects を参照。
  11. サブクラスでテストしたいメソッド/プロパティに protected 可視性を付ける:これにより、公開せずにテストサブクラスでオーバーライドできます。これは匿名クラスモックで特に有用です。

PHPUnit のセットアップ

まず、Composer を使用して Flight PHP プロジェクトに PHPUnit をセットアップします。簡単なテストのために。PHPUnit Getting Started guide で詳細を確認してください。

  1. プロジェクトディレクトリで実行:

    composer require --dev phpunit/phpunit

    これで最新の PHPUnit が開発依存としてインストールされます。

  2. プロジェクトルートに tests ディレクトリを作成し、テストファイルを置きます。

  3. composer.json にテストスクリプトを追加して便利に:

    // other composer.json content
    "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);
    }
}

これをテストするために、テストファイルを作成します。テストの構造については Unit Testing and SOLID Principles を参照:

// 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'; // Simulate POST data
        $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'; // Simulate POST data
        $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 の requestsresponses については関連ドキュメントを参照。

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

より複雑なシナリオでは、依存性注入 (DI) を使用してコントローラーをテスト可能にします。Flight のグローバル(例:Flight::set()Flight::map()Flight::register())を避け、それらはグローバル状態として機能し、すべてのテストでモックを必要とします。代わりに、Flight の DI コンテナ、DICEPHP-DI、または手動 DI を使用します。

生の PDO の代わりに flight\database\PdoWrapper を使用しましょう。このラッパーはモックとユニットテストがはるかに簡単です!

データベースにユーザーを保存し、ウェルカムメールを送信するコントローラー:

use flight\database\PdoWrapper;

class UserController {
    protected $app;
    protected $db;
    protected $mailer;

    public function __construct(Engine $app, PdoWrapper $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)) {
            // adding the return here helps unit testing to stop execution
            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 PHPUnit\Framework\TestCase;

class UserControllerDICTest extends TestCase {
    public function testValidEmailSavesAndSendsEmail() {

        // Sometimes mixing mocking styles is necessary
        // Here we use PHPUnit's built-in mock for PDOStatement
        $statementMock = $this->createMock(PDOStatement::class);
        $statementMock->method('execute')->willReturn(true);
        // Using an anonymous class to mock PdoWrapper
        $mockDb = new class($statementMock) extends PdoWrapper {
            protected $statementMock;
            public function __construct($statementMock) {
                $this->statementMock = $statementMock;
            }

            // When we mock it this way, we are not really making a database call.
            // We can further setup this to alter the PDOStatement mock to simulate failures, etc.
            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 PdoWrapper {
            // An empty constructor bypasses the parent constructor
            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';

        // Need to map jsonHalt to avoid exiting
        $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 を使用した例を示します。これが悪い理由です。チェックを isEmailValid メソッド(filter_var を使用)に変更し、他の新しい追加を registerUser という別メソッドにします。

use flight\database\PdoWrapper;
use flight\Engine;

// UserControllerDICV2.php
class UserControllerDICV2 {
    protected $app;
    protected $db;
    protected $mailer;

    public function __construct(Engine $app, PdoWrapper $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)) {
            // adding the return here helps unit testing to stop execution
            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';
        // we are skipping the extra dependency injection here cause it's "easy"
        $controller = new class($app) extends UserControllerDICV2 {
            protected $app;
            // Bypass the deps in the construct
            public function __construct($app) {
                $this->app = $app;
            }

            // We'll just force this to be valid.
            protected function isEmailValid($email) {
                return true; // Always return true, bypassing real validation
            }

            // Bypass the actual DB and mailer calls
            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 {

    // ... other methods ...

    protected function isEmailValid($email) {
        // Changed logic
        $validEmail = filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
        // Now it should only have a specific domain
        $validDomain = strpos($email, '@example.com') !== false; 
        return $validEmail && $validDomain;
    }
}

上記のユニットテストを実行しても、まだパスします!しかし、動作をテストしていなかった(コードの一部を実行させなかった)ため、本番でバグが発生する可能性があります。テストは新しい動作を考慮して修正し、期待しない動作の反対も含めるべきです。

完全な例

Flight PHP プロジェクトのユニットテストの完全な例は GitHub で見つかります:n0nag0n/flight-unit-tests-guide。 より深い理解のために、Unit Testing and SOLID Principles を参照。

一般的な落とし穴

ユニットテストによるスケーリング

ユニットテストは大規模プロジェクトや数ヶ月後のコード再訪で輝きます。動作を文書化し、回帰を検出してアプリの再学習を防ぎます。ソロ開発者には重要なパス(例:ユーザー登録、支払い処理)をテスト。チームには貢献間の動作の一貫性を確保。フレームワークとテストの利点については Why Frameworks? を参照。

Flight PHP ドキュメントリポジトリに自分のテストのヒントを貢献してください!

Written by 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. パブリックディレクトリの作成: Flightは単一のエントリーポイント(index.php)を使用します。それ用にpublic/フォルダを作成します:

    mkdir public
  4. 基本的なindex.php: シンプルな「Hello World」ルートを持つpublic/index.phpを作成します:

    <?php
    require '../vendor/autoload.php';
    
    Flight::route('/', function () {
       echo 'こんにちは、Flight!';
    });
    
    Flight::start();
  5. 組み込みサーバーの起動: PHPの開発サーバーを使用してセットアップをテストします:

    php -S localhost:8000 -t public/

    http://localhost:8000にアクセスして「こんにちは、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' => '私のブログ']);
    });
    
    Flight::start();
  3. レイアウトテンプレートを作成する: app/views/layout.latte:

    <!DOCTYPE html>
    <html>
    <head>
    <title>{$title}</title>
    </head>
    <body>
    <header>
        <h1>私のブログ</h1>
        <nav>
            <a href="/">ホーム</a> | 
            <a href="/create">投稿を作成</a>
        </nav>
    </header>
    <main>
        {block content}{/block}
    </main>
    <footer>
        <p>&copy; {date('Y')} Flightブログ</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": "私の最初の投稿",
           "content": "これはFlight PHPを使用した私の初めてのブログ投稿です!"
       }
    ]

ステップ4: ルートの定義

ルートを構成ファイルに分けることで、整理を良くしましょう。

  1. routes.phpを作成: app/config/routes.phpで:

    <?php
    Flight::route('/', function () {
       Flight::view()->render('home.latte', ['title' => '私のブログ']);
    });
    
    Flight::route('/post/@slug', function ($slug) {
       Flight::view()->render('post.latte', ['title' => '投稿: ' . $slug, 'slug' => $slug]);
    });
    
    Flight::route('GET /create', function () {
       Flight::view()->render('create.latte', ['title' => '投稿を作成']);
    });
  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' => '私のブログ',
           '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' => '投稿を作成']);
    });

ステップ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">タイトル:</label>
                <input type="text" name="title" id="title" required>
            </div>
            <div class="form-group">
                <label for="content">コンテンツ:</label>
                <textarea name="content" id="content" required></textarea>
            </div>
            <button type="submit">投稿を保存</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を訪問します。
    • 新しい投稿(例:「第二の投稿」とその内容)を送信します。
    • ホームページでそれがリストされているのを確認します。

ステップ8: エラーハンドリングの強化

より良い404エクスペリエンスのためにnotFoundメソッドをオーバーライドします。

index.phpで:

Flight::map('notFound', function () {
    Flight::view()->render('404.latte', ['title' => 'ページが見つかりません']);
});

app/views/404.latteを作成します:

{extends 'layout.latte'}

{block content}
    <h2>404 - {$title}</h2>
    <p>申し訳ありませんが、そのページは存在しません!</p>
{/block}

次のステップ

結論

Flight PHPを使ってシンプルなブログを構築しました! このガイドでは、ルーティング、Latteによるテンプレーティング、およびフォーム送信の処理などのコア機能を示しました。すべてを軽量に保ちながら実施しています。さらにブログを進化させるためにFlightのドキュメントを探求してください!

License

The MIT License (MIT)

Copyright © 2024 @mikecao, @n0nag0n

個人が複製の許可を得ることができるように、このソフトウェアおよび関連ドキュメントファイル(以下「ソフトウェア」という)のコピーを入手することができます。 ソフトウェアを使用、コピー、変更、マージ、公開、配布、サブライセンス、販売する権利などを含む、制限なしでソフトウェアを扱う権利が、以下の条件に従って人々にそれを許可します:

上記の著作権表示およびこの許諾表示は、ソフトウェアのすべての複製または実質的な部分に含まれている必要があります。

ソフトウェアは、「現状有姿」で提供され、商品性、特定目的への適合性、および権利侵害を含むがこれに限定されない、いかなる種類の保証もなしに提供されます。 著作者または著作権保持者は、ソフトウェアまたは使用または他の取引に起因する契約上の行為、不法行為、その他の行為から生じるクレーム、損害、その他の責任について一切責任を負いません。

About

Flight PHP フレームワーク

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 でプロジェクトを開始するための例のアプリがあります。構造化されたレイアウト、基本的な設定、Composer スクリプトが最初から設定されています! flightphp/skeleton を確認して、すぐに使用可能なプロジェクトを取得するか、examples ページでインスピレーションを得てください。AI の適合方法を知りたいですか? AI 駆動の例を探す

スケルトンアプリのインストール

簡単です!

# 新しいプロジェクトを作成
composer create-project flightphp/skeleton my-project/
# 新しいプロジェクトディレクトリに入る
cd my-project/
# ローカル開発サーバーを起動してすぐに開始!
composer start

これにより、プロジェクト構造が作成され、必要なファイルが設定され、準備完了です!

高パフォーマンス

Flight は、既存の PHP フレームワークの中で最も速いもののひとつです。その軽量なコアは、オーバーヘッドを減らし、速度を向上させ、伝統的なアプリや現代の 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

AI の扱いについて好奇心がありますか? 発見する Flight が、お気に入りのコーディング LLM との作業を簡単にする方法を!

コミュニティ

Matrix Chat で利用可能です

Matrix

そして Discord

コントリビューション

Flight に貢献する方法は 2 つあります:

  1. コアフレームワークに貢献するには、core repository を訪問してください。
  2. ドキュメントを改善する手伝い! このドキュメントウェブサイトは Github でホストされています。エラーを発見したり、何かを改善したい場合、プルリクエストを提出してください。私たちは更新と新しいアイデアを歓迎します。特に AI と新しい技術に関するものを!

要件

Flight には PHP 7.4 以上が必要です。

注記: PHP 7.4 は、執筆時点 (2024 年) でいくつかの 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

Wruczek/PHP-File-Cache からフォークされた、軽量でシンプルなスタンドアロンのPHPインファイルキャッシングクラス

利点

このドキュメントサイトはこのライブラリを使用して各ページをキャッシュしています!

コードを見るには here をクリックしてください。

インストール

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 "Latest cache save: $cacheddate, expires in $expiresin seconds";

ドキュメント

コードを見るには https://github.com/flightphp/cache を訪れてください。キャッシュの使用方法の追加例については examples フォルダを確認してください。

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\PdoWrapper $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('You can\'t create an order. Sorry!');
        }
    }
}

キャッシュ

キャッシュを有効にするには、単純なwruczak/phpfilecacheライブラリを参照してください。これを有効にする例は以下の通りです。


// この $app はあなたのコードの一部である可能性があり、
// コンストラクター内で Flight::app() から取得されるか
// null を渡すと、それがコンストラクター内で取得されます
$app = Flight::app();

// 現時点では、ファイルキャッシュとしてこれを受け入れます。今後他のものも簡単に追加できます。
$Cache = new Wruczek\PhpFileCache\PhpFileCache;

$Permissions = new \flight\Permission($current_role, $app, $Cache);
$Permissions->defineRulesFromClassMethods(MyApp\Permissions::class, 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/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/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);
});

パス設定

CommentTemplate は相対パスと絶対パスの両方に対するインテリジェントなパス処理を提供します:

パブリックパス

パブリックパスは、通常 index.php が配置されている Web アプリケーションのルートディレクトリです。これは Web サーバーがファイルを提供するドキュメントルートです。

// 例: index.phpが /var/www/html/myapp/index.php にある場合
$template->setPublicPath('/var/www/html/myapp');  // ルートディレクトリ

// Windows例: index.phpが C:\xampp\htdocs\myapp\index.php にある場合
$template->setPublicPath('C:\\xampp\\htdocs\\myapp');

テンプレートパス設定

テンプレートパスは相対パスと絶対パスの両方をサポートします:

$template = new Engine();
$template->setPublicPath('/var/www/html/myapp');  // ルートディレクトリ(index.phpがある場所)

// 相対パス - パブリックパスと自動的に結合
$template->setSkinPath('views');           // → /var/www/html/myapp/views/
$template->setSkinPath('templates/pages'); // → /var/www/html/myapp/templates/pages/

// 絶対パス - そのまま使用(Unix/Linux)
$template->setSkinPath('/var/www/templates');      // → /var/www/templates/
$template->setSkinPath('/full/path/to/templates'); // → /full/path/to/templates/

// Windows絶対パス
$template->setSkinPath('C:\\www\\templates');     // → C:\www\templates\
$template->setSkinPath('D:/projects/templates');  // → D:/projects/templates/

// UNCパス(Windowsネットワーク共有)
$template->setSkinPath('\\\\server\\share\\templates'); // → \\server\share\templates\

アセットパス設定

アセットパスも相対パスと絶対パスの両方をサポートします:

// 相対パス - パブリックパスと自動的に結合
$template->setAssetPath('assets');        // → /var/www/html/myapp/assets/
$template->setAssetPath('static/files');  // → /var/www/html/myapp/static/files/

// 絶対パス - そのまま使用(Unix/Linux)
$template->setAssetPath('/var/www/cdn');           // → /var/www/cdn/
$template->setAssetPath('/full/path/to/assets');   // → /full/path/to/assets/

// Windows絶対パス
$template->setAssetPath('C:\\www\\static');       // → C:\www\static\
$template->setAssetPath('D:/projects/assets');    // → D:/projects/assets/

// UNCパス(Windowsネットワーク共有)
$template->setAssetPath('\\\\server\\share\\assets'); // → \\server\share\assets\

スマートパス検出:

動作方法:

<?php
require_once 'vendor/autoload.php';

use KnifeLemon\CommentTemplate\Engine;

$app = Flight::app();

$app->register('view', Engine::class, [], function (Engine $engine) use ($app) {
    // テンプレートファイルが保存される場所
    $engine->setTemplatesPath(__DIR__ . '/views');

    // パブリックアセットが提供される場所
    $engine->setPublicPath(__DIR__ . '/public');

    // コンパイルされたアセットが保存される場所
    $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__ . '/public',    // publicPath - アセットが提供される場所
    __DIR__ . '/views',     // skinPath - テンプレートファイルが保存される場所  
    'assets',               // assetPath - コンパイルされたアセットが保存される場所
    '.php'                  // fileExtension - テンプレートファイルの拡張子
]);

$app->map('render', function(string $template, array $data) use ($app): void {
    echo $app->view()->render($template, $data);
});

テンプレートディレクティブ

レイアウトの継承

共通の構造を作成するためにレイアウトを使用します:

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 には表示されません:

{* これは単一行のテンプレートコメントです *}

{* 
   これは複数行にわたる
   マルチライン 
   テンプレートコメントです
*}

<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/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

ランウェイ

ランウェイはCLIアプリケーションで、Flightアプリケーションの管理を支援します。コントローラを生成したり、すべてのルートを表示したりすることができます。優れたadhocore/php-cliライブラリに基づいています。

こちらをクリックして、コードを表示してください。

インストール

Composerを使用してインストールしてください。

composer require flightphp/runway

基本設定

ランウェイを実行する最初の回は、セットアッププロセスを進め、プロジェクトのルートに.runway.json構成ファイルを作成します。このファイルには、ランウェイが正しく動作するために必要ないくつかの構成が含まれています。

使用法

ランウェイには、Flightアプリケーションを管理するために使用できる複数のコマンドがあります。ランウェイを使用する方法は2つあります。

  1. スケルトンプロジェクトを使用している場合、プロジェクトのルートから php runway [command] を実行できます。
  2. Composerを介してインストールされたパッケージとしてRunwayを使用している場合、プロジェクトのルートから vendor/bin/runway [command] を実行できます。

任意のコマンドに対して、--helpフラグを渡すと、そのコマンドの使用方法に関するより詳細な情報を取得できます。

php runway routes --help

以下はいくつかの例です。

コントローラを生成する

.runway.jsonファイルの構成に基づいて、デフォルトの場所は app/controllers/ ディレクトリにコントローラを生成します。

php runway make:controller MyController

アクティブレコードモデルを生成する

.runway.jsonファイルの構成に基づいて、デフォルトの場所は app/records/ ディレクトリにコントローラを生成します。

php runway make:record users

たとえば、次のスキーマを持つ users テーブルがある場合:idnameemailcreated_atupdated_atapp/records/UserRecord.php ファイルに類似したファイルが作成されます:

<?php

declare(strict_types=1);

namespace app\records;

/**
 * ユーザーテーブルのアクティブレコードクラス。
 * @link https://docs.flightphp.com/awesome-plugins/active-record
 * 
 * @property int $id
 * @property string $name
 * @property string $email
 * @property string $created_at
 * @property string $updated_at
 * // 関係を定義した場合、ここに関係を追加できます
 * @property CompanyRecord $company 関係の例
 */
class UserRecord extends \flight\ActiveRecord
{
    /**
     * @var array $relations モデルの関係を設定します
     *   https://docs.flightphp.com/awesome-plugins/active-record#relationships
     */
    protected array $relations = [];

    /**
     * コンストラクタ
     * @param mixed $databaseConnection データベースへの接続
     */
    public function __construct($databaseConnection)
    {
        parent::__construct($databaseConnection, 'users');
    }
}

すべてのルートを表示する

登録されているすべてのFlightのルートを表示します。

php runway routes

特定のルートのみを表示したい場合、フラグを渡してルートをフィルタリングできます。

# GETルートのみを表示
php runway routes --get

# POSTルートのみを表示
php runway routes --post

# など

ランウェイのカスタマイズ

Flight向けのパッケージを作成しているか、プロジェクトに独自のカスタムコマンドを追加したい場合は、プロジェクト/パッケージ向けに src/commands/flight/commands/app/commands/、または commands/ ディレクトリを作成してください。

コマンドを作成するには、AbstractBaseCommandクラスを拡張し、__constructメソッドとexecuteメソッドを最低限実装します。

<?php

declare(strict_types=1);

namespace flight\commands;

class ExampleCommand extends AbstractBaseCommand
{
    /**
     * コンストラクタ
     *
     * @param array<string,mixed> $config .runway-config.jsonからのJSON構成
     */
    public function __construct(array $config)
    {
        parent::__construct('make:example', 'ドキュメントの例を作成', $config);
        $this->argument('<funny-gif>', '面白いGIFの名前');
    }

    /**
     * 関数を実行
     *
     * @return void
     */
    public function execute(string $controller)
    {
        $io = $this->app()->io();

        $io->info('例を作成します...');

        // ここで何かを実行

        $io->ok('例が作成されました!');
    }
}

独自のカスタムコマンドをFlightアプリケーションに組み込む方法については、adhocore/php-cliドキュメントを参照してください!

Awesome-plugins/tracy_extensions

Tracy Flight Panel Extensions

=====

これは、Flight をより豊かに作業するための拡張機能のセットです。

これは Panel です

Flight Bar

そして、各パネルはアプリケーションに関する非常に役立つ情報を表示します!

Flight Data Flight Database Flight Request

コードを表示するには here をクリックしてください。

Installation

composer require flightphp/tracy-extensions --dev を実行するだけで、すぐに始められます!

Configuration

これを始めるために必要な設定はほとんどありません。この機能を使用する前に Tracy デバッガーを開始する必要があります https://tracy.nette.org/en/guide:

<?php

use Tracy\Debugger;
use flight\debug\tracy\TracyExtensionLoader;

// bootstrap code
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) {
    // これが false でないと Tracy がレンダリングできません :(
    Flight::set('flight.content_length', false);
    new TracyExtensionLoader(Flight::app());
}

// more code

Flight::start();

Additional Configuration

Session Data

カスタムセッションハンドラー(例: 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) {
    // これが false でないと Tracy がレンダリングできません :(
    Flight::set('flight.content_length', false);
    new TracyExtensionLoader(Flight::app(), [ 'session_data' => Flight::session()->getAll() ]);
}

// routes and other things...

Flight::start();

Latte

このセクションには PHP 8.1+ が必要です。

プロジェクトに Latte がインストールされている場合、Tracy は Latte とネイティブに統合されており、テンプレートを分析できます。Latte インスタンスに拡張を登録するだけです。


require 'vendor/autoload.php';

$app = Flight::app();

$app->map('render', function($template, $data, $block = null) {
    $latte = new Latte\Engine;

    // other configurations...

    // Tracy Debug Bar が有効な場合のみ拡張を追加
    if(Debugger::$showBar === true) {
        // ここで Latte Panel を Tracy に追加
        $latte->addExtension(new Latte\Bridges\Tracy\TracyExtension);
    }

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

Awesome-plugins/apm

FlightPHP APM ドキュメント

FlightPHP APM へようこそ—アプリのパーソナルパフォーマンスコーチです!このガイドは、FlightPHP を使用したアプリケーション パフォーマンス モニタリング (APM) の設定、使用、マスターするためのロードマップです。遅いリクエストを追跡したり、レイテンシ チャートに没頭したりするかどうかにかかわらず、私たちがカバーします。アプリを速くし、ユーザーを幸せにし、デバッグ セッションを楽にしましょう!

Flight Docs サイトのダッシュボードの デモ をご覧ください。

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\Apm;

$ApmLogger = LoggerFactory::create(__DIR__ . '/../../.runway-config.json');
$Apm = new Apm($ApmLogger);
$Apm->bindEventsToFlightInstance($app);

// データベース接続を追加する場合
// Tracy Extensions からの PdoWrapper または PdoQueryCapture である必要があります
$pdo = new PdoWrapper('mysql:host=localhost;dbname=example', 'user', 'pass', null, true); // <-- APM で追跡を有効にするために True が必要です。
$Apm->addPdoConnection($pdo);

ここで何が起こっていますか?

プロ ティップ: サンプリング アプリが忙しい場合、すべての リクエストをログにするとオーバーロードする可能性があります。サンプル レート(0.0 から 1.0)を使用してください:

$Apm = new Apm($ApmLogger, 0.1); // リクエストの 10% をログにします

これにより、パフォーマンスを維持しつつ、堅実なデータを取得できます。

2. 構成する

.runway-config.json を作成するためにこれを実行してください:

php vendor/bin/runway apm:init

これは何をしますか?

このプロセスは、このセットアップのマイグレーションを実行するかどうかも尋ねます。初めて設定する場合、答えは yes です。

なぜ 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\PdoWrapper;

$pdo = new PdoWrapper('sqlite:/path/to/db.sqlite', null, null, null, true); // <-- APM で追跡を有効にするために True が必要です。
$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 データベースのサイズが大きい場合、これらのマイグレーションは実行に時間がかかる可能性があります。オフピーク時間にこのコマンドを実行することを検討してください。

古いデータの消去

データベースを整理するために、古いデータを消去できます。これは、忙しいアプリを実行していてデータベースのサイズを管理しやすくしたい場合に特に有用です。 以下のコマンドを実行してこれを行います:

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 プラグイン用にいくつかのパネルを作成しました。

インストール

Composer でインストールします。Tracy は本番用のエラーハンドリングコンポーネントが付属しているため、実際には dev バージョンなしでインストールする必要があります。

composer require tracy/tracy

基本設定

開始するための基本的な設定オプションがあります。詳細については、Tracy ドキュメント を参照してください。


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); // Debugger バーが表示されている場合、Flight によって content-length が設定できません。

    // これは Flight 用の Tracy 拡張機能に固有のものです。これを含めた場合は有効にしてください。
    new TracyExtensionLoader($app);
}

便利なヒント

コードのデバッグ中に、データを出力するための非常に役立つ関数がいくつかあります。

Awesome-plugins/active_record

Flight アクティブレコード

アクティブレコードとは、データベースのエンティティをPHPオブジェクトにマッピングすることです。簡単に言えば、データベースにユーザーテーブルがある場合、そのテーブルの行をUserクラスと$userオブジェクトに「翻訳」することができます。 基本例を参照してください。

GitHubのリポジトリについてはこちらをクリックしてください。

基本例

次のテーブルがあると仮定しましょう:

CREATE TABLE users (
    id INTEGER PRIMARY KEY, 
    name TEXT, 
    password TEXT 
);

このテーブルを表す新しいクラスを設定できます:

/**
 * アクティブレコードクラスは通常単数です
 * 
 * ここにテーブルのプロパティをコメントとして追加することを強くお勧めします
 * 
 * @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();

どうですか?楽しいでしょう?インストールして始めましょう!

インストール

Composerで簡単にインストールできます

composer require flightphp/active-record 

使用法

これはスタンドアロンライブラリとして使用することも、Flight PHPフレームワークと一緒に使用することもできます。完全にあなたの好みです。

スタンドアロン

コンストラクタにPDO接続を渡すことを確認してください。

$pdo_connection = new PDO('sqlite:test.db'); // これは単なる例で、実際にはデータベース接続を使用することになります

$User = new User($pdo_connection);

常にコンストラクタでデータベース接続を設定したくないですか?他のアイデアについてはデータベース接続管理を参照してください!

Flightでメソッドとして登録

Flight PHPフレームワークを使用している場合、ActiveRecordクラスをサービスとして登録できますが、本当にそうする必要はありません。

Flight::register('user', 'User', [ $pdo_connection ]);

// 次に、コントローラや関数などでこのように使用できます。

Flight::user()->find(1);

runway メソッド

runway は、Flight用のCLIツールで、このライブラリ用のカスタムコマンドがあります。

# 使用法
php runway make:record database_table_name [class_name]

# 例
php runway make:record users

これにより、app/records/ディレクトリにUserRecord.phpという新しいクラスが作成され、次の内容が含まれます:

<?php

declare(strict_types=1);

namespace app\records;

/**
 * ユーザーテーブル用のアクティブレコードクラスです。
 * @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関数

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' ]);
        // 上記の配列の代わりにこのようにプライマリキーを設定することもできます。
        $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'; // これは変更されているので「ダーティ」と見なされます。
$user->update();
// 更新され、データベースに永続化されたので、ダーティデータはありません。

$user->password = password_hash('newpassword'); // これはダーティです。
$user->dirty(); // 引数を何も渡さないと、すべてのダーティエントリがクリアされます。
$user->update(); // 何も捕捉されていないため、何も更新されません。

$user->dirty([ 'name' => 'something', 'password' => password_hash('a different password') ]);
$user->update(); // 名前とパスワードの両方が更新されます。

copyFrom(array $data): ActiveRecord (v0.4.0)

これはdirty()メソッドの別名です。何をしているのかがより明確です。

$user->copyFrom([ 'name' => 'something', 'password' => password_hash('a different password') ]);
$user->update(); // 名前とパスワードの両方が更新されます。

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クエリメソッド

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)

返されるレコードの数を制限します。二つ目のintが指定された場合、SQLと同様にオフセットを制限します。

$user->orderby('name DESC')->limit(0, 10)->findAll();

WHERE条件

equal(string $field, mixed $value) / eq(string $field, mixed $value)

field = $value の場合

$user->eq('id', 1)->find();

notEqual(string $field, mixed $value) / ne(string $field, mixed $value)

field <> $value の場合

$user->ne('id', 1)->find();

isNull(string $field)

field IS NULL の場合

$user->isNull('id')->find();

isNotNull(string $field) / notNull(string $field)

field IS NOT NULL の場合

$user->isNotNull('id')->find();

greaterThan(string $field, mixed $value) / gt(string $field, mixed $value)

field > $value の場合

$user->gt('id', 1)->find();

lessThan(string $field, mixed $value) / lt(string $field, mixed $value)

field < $value の場合

$user->lt('id', 1)->find();

greaterThanOrEqual(string $field, mixed $value) / ge(string $field, mixed $value) / gte(string $field, mixed $value)

field >= $value の場合

$user->ge('id', 1)->find();

lessThanOrEqual(string $field, mixed $value) / le(string $field, mixed $value) / lte(string $field, mixed $value)

field <= $value の場合

$user->le('id', 1)->find();

like(string $field, mixed $value) / notLike(string $field, mixed $value)

field LIKE $value または field NOT LIKE $value の場合

$user->like('name', 'de')->find();

in(string $field, array $values) / notIn(string $field, array $values)

field IN($value) または field NOT IN($value) の場合

$user->in('id', [1, 2])->find();

between(string $field, array $values)

field BETWEEN $value AND $value1 の場合

$user->between('id', [1, 2])->find();

OR条件

条件をOR文でラップすることも可能です。これは、startWrap()およびendWrap()メソッドを使用するか、フィールドと値の後に条件の3番目のパラメータを指定することで行います。

// メソッド 1
$user->eq('id', 1)->startWrap()->eq('name', 'demo')->or()->eq('name', 'test')->endWrap('OR')->find();
// これは `id = 1 AND (name = 'demo' OR name = 'test')` と評価されます

// メソッド 2
$user->eq('id', 1)->eq('name', 'demo', 'OR')->find();
// これは `id = 1 OR name = 'demo'` と評価されます

リレーションシップ

このライブラリを使用して、いくつかの種類のリレーションシップを設定できます。一対多および一対一のリレーションシップをテーブル間に設定できます。これには、事前にクラス内で追加のセットアップが必要です。

$relations配列の設定は難しくはありませんが、正しい構文を推測することは混乱を招くことがあります。

protected array $relations = [
    // キーの名前は好きなように設定できます。アクティブレコードの名前はおそらく良いでしょう。例:user、contact、client
    'user' => [
        // 必須
        // self::HAS_MANY, self::HAS_ONE, self::BELONGS_TO
        self::HAS_ONE, // これはリレーションのタイプです

        // 必須
        'Some_Class', // これは参照する「他の」アクティブレコードクラスです

        // 必須
        // リレーションシップの種類に応じて
        // self::HAS_ONE = 結合を参照する外部キー
        // self::HAS_MANY = 結合を参照する外部キー
        // self::BELONGS_TO = 結合を参照するローカルキー
        'local_or_foreign_key',
        // 他のモデルのプライマリキーにのみ結合しますので、ご注意ください。

        // オプショナル
        [ '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; // これはユーザー名です

すごいですね!

カスタムデータの設定

場合によっては、アクティブレコードにカスタム計算など、オブジェクトに直接接続したいユニークなものを添付する必要があるかもしれません。

setCustomData(string $field, mixed $value)

カスタムデータは、setCustomData()メソッドを使用して添付します。

$user->setCustomData('page_view_count', $page_view_count);

そして、通常のオブジェクトプロパティのように参照します。

echo $user->page_view_count;

イベント

このライブラリのもう一つの素晴らしい機能は、イベントに関するものです。イベントは、呼び出す特定のメソッドに基づいて特定のタイミングでトリガーされます。自動的にデータをセットアップするのに非常に役立ちます。

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 beforeUpdate(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 afterUpdate(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 '彼は勇敢な兵士でした... :cry-face:';
    } 
}

データベース接続管理

このライブラリを使用する際、データベース接続をいくつかの異なる方法で設定できます。コンストラクタ内で接続を設定することも、$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)で接続を再設定することができます。

貢献

ぜひご協力ください。 :D

セットアップ

貢献する際は、composer test-coverageを実行して100%のテストカバレッジを維持してください(これは真の単体テストカバレッジではなく、むしろ統合テストのカバレッジです)。

また、composer beautifyおよびcomposer phpcsを実行して、すべてのリンティングエラーを修正することを確認してください。

ライセンス

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 チームによって公式にサポートされており、他のものは開始するためのマイクロ/ライトライブラリです。

API ドキュメント

API ドキュメントは、あらゆる API にとって重要です。開発者が API とどのようにやり取りするかを理解し、返されるものを期待することを助けます。Flight プロジェクトの API ドキュメントを生成するのに役立つツールがいくつかあります。

アプリケーション パフォーマンス監視 (APM)

アプリケーション パフォーマンス監視 (APM) は、あらゆるアプリケーションにとって重要です。アプリケーションのパフォーマンスを理解し、ボトルネックがどこにあるかを特定するのに役立ちます。Flight で使用できる APM ツールがいくつかあります。

非同期

Flight はすでに高速なフレームワークですが、ターボエンジンを追加するとすべてがより楽しく(そして挑戦的)になります!

認証/権限

認証と権限は、誰が何にアクセスできるかを制御する必要があるあらゆるアプリケーションにとって重要です。

キャッシュ

キャッシュはアプリケーションを高速化する優れた方法です。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 ディレクトリに抽出することもできます。

推奨インストール

新しいプロジェクトの場合、flightphp/skeleton アプリから始めることを強く推奨します。インストールは簡単です。

composer create-project flightphp/skeleton my-project/

これにより、プロジェクト構造が設定され、名前空間付きの自動読み込みが構成され、設定がセットアップされ、TracyTracy ExtensionsRunway などの他のツールが提供されます。

Web サーバーの設定

ビルトイン PHP 開発サーバー

これは、起動して実行する最も簡単な方法です。ビルトインサーバーを使用してアプリケーションを実行し、データベースとして SQLite を使用することもできます(システムに sqlite3 がインストールされている限り)。ほとんど何も必要ありません! PHP がインストールされたら、以下のコマンドを実行するだけです:

php -S localhost:8000
# または skeleton アプリの場合
composer start

次に、ブラウザを開いて http://localhost:8000 にアクセスします。

プロジェクトのドキュメントルートを別のディレクトリにしたい場合(例: プロジェクトが ~/myproject ですが、ドキュメントルートが ~/myproject/public/ の場合)、~/myproject ディレクトリ内にいる状態で以下のコマンドを実行できます:

php -S localhost:8000 -t public/
# skeleton アプリの場合、これはすでに構成されています
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 します。
require 'vendor/autoload.php';
// Composer を使用していない場合、フレームワークを直接ロードします
// require 'flight/Flight.php';

// 次に、ルートを定義し、リクエストを処理する関数を割り当てます。
Flight::route('/', function () {
  echo 'hello world!';
});

// 最後に、フレームワークを開始します。
Flight::start();

skeleton アプリの場合、これはすでに構成されており、app/config/routes.php ファイルで処理されます。サービスは app/config/services.php で構成されます。

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)を non-thread-safe 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:\php7, C:\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 セクションを確認してください。