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 是“经理”,它管理视图的行为。在控制器外部(或在 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 已转换为更面向对象的形式,以便更容易使用依赖注入容器。如果您需要以类似于 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 提供了一种简单的方式来配置框架的各个方面,以适应您的应用程序需求。有些配置是默认设置的,但您可以根据需要覆盖它们。您还可以设置自己的变量,以便在整个应用程序中使用。

理解

您可以通过 set 方法设置配置值来自定义 Flight 的某些行为。

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

app/config/config.php 文件中,您可以看到所有可用的默认配置变量。

基本用法

Flight 配置选项

以下是所有可用配置设置的列表:

Loader 配置

加载器还有另一个配置设置。这将允许您自动加载类名中包含 _ 的类。

// Enable class loading with underscores
// Defaulted to true
Loader::$v2ClassLoading = false;

变量

Flight 允许您保存变量,以便在应用程序的任何地方使用它们。

// Save your variable
Flight::set('id', 123);

// Elsewhere in your application
$id = Flight::get('id');

要检查变量是否已设置,您可以这样做:

if (Flight::has('id')) {
  // Do something
}

您可以通过以下方式清除变量:

// Clears the id variable
Flight::clear('id');

// Clears all variables
Flight::clear();

注意: 仅仅因为您可以设置变量并不意味着您应该这样做。请谨慎使用此功能。原因是这里存储的任何内容都会成为全局变量。全局变量很糟糕,因为它们可以从应用程序的任何地方更改,这使得跟踪错误变得困难。此外,这还会使诸如 unit testing 之类的事情复杂化。

错误和异常

所有错误和异常都会被 Flight 捕获并传递给 error 方法,如果 flight.handle_errors 设置为 true。

默认行为是发送一个通用的 HTTP 500 Internal Server Error 响应,并附带一些错误信息。

您可以覆盖此行为以满足自己的需求:

Flight::map('error', function (Throwable $error) {
  // Handle error
  echo $error->getTraceAsString();
});

默认情况下,错误不会记录到 Web 服务器。您可以通过更改配置来启用此功能:

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

404 未找到

当找不到 URL 时,Flight 会调用 notFound 方法。默认行为是发送一个 HTTP 404 Not Found 响应,并附带一个简单的消息。

您可以覆盖此行为以满足自己的需求:

Flight::map('notFound', function () {
  // Handle not found
});

另请参阅

故障排除

更新日志

Learn/ai

使用 Flight 的 AI 与开发者体验

概述

Flight 让您轻松为 PHP 项目注入 AI 驱动的工具和现代开发者工作流程。通过内置命令连接 LLM(大型语言模型)提供商并生成项目特定的 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 中)。

示例:

欢迎使用 AI Init!
您想使用哪个 LLM API?[1] openai, [2] grok, [3] claude: 1
输入 LLM API 的基础 URL [https://api.openai.com]:
输入您的 openai API 密钥: sk-...
输入您想使用的模型名称(例如 gpt-4、claude-3-opus 等)[gpt-4o]:
凭据已保存到 .runway-creds.json

生成项目特定的 AI 指令

ai:generate-instructions 命令帮助您创建或更新针对项目的 AI 编码助手的指令。

php runway ai:generate-instructions

您将回答一些关于项目的问题(描述、数据库、模板、安全、团队规模等)。Flight 使用您的 LLM 提供商生成指令,然后将它们写入:

示例:

请描述您的项目用途?我的awesome API
您计划使用什么数据库?MySQL
您计划使用什么 HTML 模板引擎(如果有)?latte
安全是否是此项目的重要元素?(y/n) y
...
AI 指令更新成功。

现在,您的 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 文件中设置了不错的默认值,但我们可以做得更好。首先,它们没有为我们设置日期/时区。从分发角度来看这是合理的,但没有时区,PHP 在调用任何日期/时间相关函数时会抛出 E_WARNING 错误。下面是一些推荐设置:

1.3 扩展

禁用(或至少不启用)您不会使用的扩展(如数据库驱动程序)也是个好主意。要查看哪些已启用,运行 phpinfo() 命令或转到命令行并运行以下命令。

$ php -i

信息相同,但 phpinfo() 添加了 HTML 格式。CLI 版本更容易通过 grep 管道传输以查找特定信息。例如。

$ php -i | grep error_log

不过,此方法有一个警告:网页版本和 CLI 版本的 PHP 设置可能不同。

2 使用 Composer

这可能令人惊讶,但编写现代 PHP 的最佳实践之一是编写更少的代码。虽然事实是练习编程是提高技能的最好方法,但 PHP 领域已经解决了大量问题,如路由、基本输入验证库、单位转换、数据库抽象层等... 只是去 Packagist 浏览一下。您可能会发现您要解决的问题的很大一部分已经编写并经过测试。

虽然自己编写所有代码很诱人(作为学习体验编写自己的框架或库也没问题),但您应该克服“非我发明不使用”的感觉,从而节省大量时间和麻烦。遵循 PIE 原则——自豪地使用他处发明的代码。而且,如果您选择编写自己的东西,除非它与现有产品有显著不同或更好,否则不要发布它。

Composer 是 PHP 的包管理器,类似于 Python 中的 pip、Ruby 中的 gem 和 Node 中的 npm。它允许您定义一个 JSON 文件,列出代码的依赖项,并尝试通过下载和安装必要的代码包来解决这些要求。

2.1 安装 Composer

我们假设这是一个本地项目,因此为当前项目安装 Composer 实例。导航到您的项目目录并运行此命令:

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

请记住,直接将任何下载管道传输到脚本解释器(sh、ruby、php 等)存在安全风险,因此请阅读安装代码并确保您对其感到舒适后再运行此类命令。

出于方便(如果您更喜欢键入 composer install 而不是 php composer.phar install),您可以使用此命令全局安装 Composer 的单个副本:

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

根据您的文件权限,您可能需要使用 sudo 运行这些命令。

2.2 使用 Composer

Composer 有两个主要类别依赖项:“require”和“require-dev”。列为“require”的依赖项 everywhere 都会安装,但“require-dev”依赖项仅在特定请求时安装。这些通常是开发时使用的工具,例如 PHP_CodeSniffer。下面显示了如何安装 Guzzle,一个流行的 HTTP 库的示例。

$ php composer.phar require guzzle/guzzle

要仅为开发目的安装工具,请添加 --dev 标志:

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

这会安装 PHP Copy-Paste Detector 作为开发-only 依赖项,这是另一个代码质量工具。

2.3 安装 vs 更新

首次运行 composer install 时,它会根据 composer.json 文件安装我们需要的库及其依赖项。完成后,Composer 会创建一个锁文件,预料之中名为 composer.lock。此文件包含 Composer 为我们找到的依赖项及其确切版本的列表,包括哈希。然后,未来每次运行 composer install,它会查看锁文件并安装那些确切版本。

composer update 有点不同。它会忽略 composer.lock 文件(如果存在),并尝试找到每个依赖项的最更新版本,同时仍满足 composer.json 中的约束。然后,它会在完成时写入一个新的 composer.lock 文件。

2.4 自动加载

Composer install 和 Composer update 都会为我们生成一个自动加载器,告诉 PHP 在哪里找到我们刚刚安装的库所需的所有文件。要使用它,只需添加此行(通常到每个请求执行的引导文件):

require 'vendor/autoload.php';

3 遵循良好设计原则

3.1 SOLID

SOLID 是一个助记符,用于提醒我们良好面向对象软件设计中的五个关键原则。

3.1.1 S - 单一责任原则

这指出类应该只有一个责任,或者换句话说,它们应该只有一个变化原因。这与 Unix 哲学相符,即许多小型工具,每一个都做得很好。只有一个功能的类更容易测试和调试,而且它们不太可能让您感到惊讶。您不希望调用 Validator 类的函数更新数据库记录。这是基于 ActiveRecord 模式 的应用程序中常见 SRP 违规示例。

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

这是一个相当基本的实体 模型。不过,其中一个东西不属于这里。实体模型的唯一责任应该是与它所代表的实体相关行为,它不应该负责自身持久化。

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

这更好。Person 模型恢复到只做一件事,而保存行为已移动到持久化对象中。请注意,我只在 Model 上进行了类型提示,而不是 Person。我们将在讨论 SOLID 的 L 和 D 部分时返回这一点。

3.1.2 O - 开闭原则

有一个很棒的测试可以很好地总结这个原则:考虑一个要实现的功能,可能是在您最近处理或正在处理的那个。您能在现有代码库中仅通过添加新类而无需更改任何现有类来实现该功能吗?您的配置和 wiring 代码可以稍作例外,但在大多数字系中,这出奇地困难。您必须依赖多态分发,而大多数代码库根本没有为此做好准备。如果您感兴趣,可以在 YouTube 上找到一个关于多态性和编写无 If 代码 的优秀 Google 演讲,进一步深入探讨。作为奖励,演讲由Miško Hevery 进行,许多人可能知道他是 AngularJs 的创建者。

3.1.3 L - 里氏替换原则

这个原则以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;
    }
}

这是我们的第一个形状,正方形。相当直观的形状,对吗?您可以假设有一个构造函数设置尺寸,但从这个实现中可以看到,长度和高度总是相同的。正方形就是这样。

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 的高度,我们不能再假设形状的长度会匹配。我们违反了在使用 Square 形状时与用户达成的约定。

这是 LSP 违规的教科书示例,我们需要这种原则来最好地利用类型系统。即使是鸭子类型 也不会告诉我们底层行为是否不同,而且既然我们无法在不看到它崩溃的情况下知道,因此最好确保它不是不同的。

3.1.3 I - 接口隔离原则

这个原则建议优先使用许多小型、细粒度的接口,而不是一个大型接口。接口应该基于行为,而不是“它是这些类之一”。考虑 PHP 自带的接口。Traversable、Countable、Serializable 之类的东西。它们广告对象所拥有的功能,而不是它从哪里继承。所以保持接口小。您不希望接口上有 30 个函数,3 个是一个更好的目标。

3.1.4 D - 依赖倒置原则

您可能在其他地方听说过这与依赖注入 相关,但依赖倒置和依赖注入并不完全相同。依赖倒置实际上只是说您应该依赖系统的抽象而不是其细节。那么这对您日常工作意味着什么?

不要在代码中到处直接使用 mysqli_query(),而是使用像 DataStore->query() 这样的东西。

这个原则的核心实际上是关于抽象的。它更多的是说“使用数据库适配器”而不是依赖于像 mysqli_query 这样的直接调用。如果您在半数类中直接使用 mysqli_query,那么您就把一切直接绑定到数据库。没有针对 MySQL 的内容,但如果您使用 mysqli_query,这种低级细节应该只隐藏在一个地方,然后通过通用包装器公开功能。

我知道这是一个陈词滥调的示例,因为在产品投入生产后,您实际完全更改数据库引擎的次数非常、非常低。我选择它是因为我认为人们会从他们自己的代码中熟悉这个想法。而且,即使您有一个您知道要坚持的数据库,该抽象包装器对象允许您修复错误、更改行为或实现您希望选择的数据库具有的功能。它还使单元测试成为可能,而低级调用不会。

4 对象体操

这不是对这些原则的完整深入探讨,但前两个易于记住,提供良好价值,并且可以立即应用于几乎任何代码库。

4.1 方法中不超过一层缩进

这是一个帮助将方法分解为更小块的有用方式,从而让代码更清晰和更自文档化。缩进层越多,方法做的就越多,您在处理时必须在脑海中跟踪的状态就越多。

我马上知道人们会反对,但这只是一个指导/启发式规则,不是硬性规定。我不期望任何人强制 PHP_CodeSniffer 规则来执行这个(尽管有人有)。

让我们快速浏览一个样本,看看这可能是什么样子:

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 函数而不是 PHP 来循环。我们必须进行一些技巧将逗号传递给 implode,所以您可以说停止在上一部更易理解。

4.2 尽量不使用 else

这主要涉及两个想法。首先是从方法中多个返回语句。如果您有足够信息来决定方法的结果,请继续做出决定并返回。其次是一个称为Guard Clauses 的想法。这些基本上是与早期返回结合的验证检查,通常在方法顶部附近。让我向您展示我的意思。

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 个整数相加并返回结果,或者如果任何参数不是整数则返回 null。忽略我们可以使用 AND 运算符将所有这些检查组合到一行的事实,我想您可以看到嵌套 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 来验证我们对传递参数的初始断言,并在它们不通过时立即退出方法。我们也不再有中间变量来跟踪总和贯穿整个方法。在这种情况下,我们已经验证了我们已经在快乐路径上,我们可以只做我们来这里要做的事。我们可以用一个 if 来做所有这些检查,但原则应该很清楚。

5 单元测试

单元测试是编写小测试来验证代码行为的实践。这些测试几乎总是用与代码相同的语言编写(在本例中为 PHP),并且旨在足够快以随时运行。它们作为改进代码的工具非常有价值。除了确保代码按预期工作的明显好处外,单元测试还可以提供非常有用的设计反馈。如果一段代码难以测试,通常会突出设计问题。它们还为您提供了一个防止回归的安全网,这让您可以更频繁地重构并将代码演变为更清洁的设计。

5.1 工具

PHP 中有几个单元测试工具,但最常见的是 PHPUnit。您可以通过下载 PHAR 文件直接 安装它,或者用 Composer 安装。由于我们使用 Composer 进行其他所有操作,我们将展示该方法。而且,由于 PHPUnit 不太可能部署到生产环境,我们可以将其作为开发依赖项使用以下命令安装:

composer require --dev phpunit/phpunit

5.2 测试是一个规范

单元测试在代码中最重要的作用是提供一个可执行规范,说明代码应该做什么。即使测试代码错误,或者代码有错误,知道系统应该做什么的知识是无价的。

5.3 先写测试

如果您有机会看到一组测试在代码之前编写和一个在代码完成后编写,它们会截然不同。“后”测试更关注类的实现细节和确保良好的行覆盖率,而“前”测试更关注验证所需的外部分行为。这确实是我们用单元测试关心的,即确保类表现出正确行为。以实现为中心的测试实际上会使重构更困难,因为如果类的内部发生变化,它们会中断,而您刚刚失去了 OOP 的信息隐藏好处。

5.4 什么是好的单元测试

好的单元测试共享以下许多特征:

有理由违反其中一些,但作为一般指南,它们会为您服务。

5.5 测试痛苦时

单元测试会让您提前感受到糟糕设计带来的痛苦 - Michael Feathers

当您编写单元测试时,您是在强迫自己实际使用类来完成事情。如果您在最后编写测试,或者更糟的是,只是将代码扔给 QA 或其他人编写测试,您不会得到关于类实际行为的任何反馈。如果我们在编写时编写测试,并且类使用起来很痛苦,我们会在编写时发现,这几乎是最便宜的修复时间。

如果一个类难以测试,那就是设计缺陷。不同的缺陷以不同的方式表现出来。如果您必须进行大量模拟,您的类可能有太多依赖项,或者方法做了太多事情。每个测试必须做的设置越多,方法做的就越多。如果您必须编写非常复杂的测试场景来行使行为,类的方法可能做了太多事情。如果您必须挖掘一堆私有方法和状态来测试东西,也许还有另一个类试图出来。单元测试非常擅长暴露“冰山类”,其中 80% 的工作隐藏在受保护或私有的代码中。我曾经是让尽可能多东西受保护的忠实粉丝,但现在我意识到我只是让我的单个类负责太多,真正的解决方案是将类分解成更小的部分。

由 Brian Fenton 撰写 - Brian Fenton 在中西部和湾区担任 PHP 开发人员已有 8 年,目前在 Thismoment。他专注于代码工艺和设计原则。博客在 www.brianfenton.us,Twitter 在 @brianfenton。当他不忙着当爸爸时,他喜欢食物、啤酒、游戏和学习。

Learn/security

安全

概述

安全对于 Web 应用程序来说是一个重要问题。您需要确保您的应用程序是安全的,并且您的用户数据是安全的。Flight 提供了一系列功能来帮助您保护 Web 应用程序的安全。

理解

在构建 Web 应用程序时,您应该了解一些常见的威胁。其中最常见的威胁包括:

Templates 通过默认转义输出来帮助防范 XSS,这样您就不必记住要这样做。Sessions 可以通过在用户会话中存储 CSRF 令牌来帮助防范 CSRF,如下面所述。使用 PDO 的预准备语句可以帮助防止 SQL 注入攻击(或者使用 PdoWrapper 类中的便捷方法)。CORS 可以通过在调用 Flight::start() 之前使用简单的钩子来处理。

所有这些方法共同协作以帮助保持您的 Web 应用程序安全。您应该始终将学习和理解安全最佳实践放在首位。

基本用法

标头

HTTP 标头是保护 Web 应用程序的最简单方法之一。您可以使用标头来防止点击劫持、XSS 和其他攻击。您可以通过几种方式将这些标头添加到您的应用程序中。

检查标头安全的两个优秀网站是 securityheaders.comobservatory.mozilla.org。在设置下面的代码后,您可以轻松使用这两个网站验证您的标头是否有效。

手动添加

您可以使用 Flight\Response 对象上的 header 方法手动添加这些标头。

// Set the X-Frame-Options header to prevent clickjacking
Flight::response()->header('X-Frame-Options', 'SAMEORIGIN');

// Set the Content-Security-Policy header to prevent XSS
// Note: this header can get very complex, so you'll want
//  to consult examples on the internet for your application
Flight::response()->header("Content-Security-Policy", "default-src 'self'");

// Set the X-XSS-Protection header to prevent XSS
Flight::response()->header('X-XSS-Protection', '1; mode=block');

// Set the X-Content-Type-Options header to prevent MIME sniffing
Flight::response()->header('X-Content-Type-Options', 'nosniff');

// Set the Referrer-Policy header to control how much referrer information is sent
Flight::response()->header('Referrer-Policy', 'no-referrer-when-downgrade');

// Set the Strict-Transport-Security header to force HTTPS
Flight::response()->header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');

// Set the Permissions-Policy header to control what features and APIs can be used
Flight::response()->header('Permissions-Policy', 'geolocation=()');

这些可以在您的 routes.phpindex.php 文件顶部添加。

作为过滤器添加

您也可以像以下一样在过滤器/钩子中添加它们:

// Add the headers in a filter
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 or wherever you have your routes
// FYI, this empty string group acts as a global middleware for
// all routes. Of course you could do the same thing and just add
// this only to specific routes.
Flight::group('', function(Router $router) {
    $router->get('/users', [ 'UserController', 'getUsers' ]);
    // more routes
}, [ SecurityHeadersMiddleware::class ]);

跨站请求伪造 (CSRF)

跨站请求伪造 (CSRF) 是一种攻击类型,其中恶意网站可以让用户的浏览器向您的网站发送请求。这可以用于在用户不知情的情况下在您的网站上执行操作。Flight 不提供内置的 CSRF 保护机制,但您可以使用中间件轻松实现自己的机制。

设置

首先,您需要生成一个 CSRF 令牌并将其存储在用户会话中。然后,您可以在表单中使用此令牌,并在表单提交时检查它。我们将使用 flightphp/session 插件来管理会话。

// Generate a CSRF token and store it in the user's session
// (assuming you've created a session object at attached it to Flight)
// see the session documentation for more information
Flight::register('session', flight\Session::class);

// You only need to generate a single token per session (so it works 
// across multiple tabs and requests for the same user)
if(Flight::session()->get('csrf_token') === null) {
    Flight::session()->set('csrf_token', bin2hex(random_bytes(32)) );
}
使用默认 PHP Flight 模板
<!-- Use the CSRF token in your form -->
<form method="post">
    <input type="hidden" name="csrf_token" value="<?= Flight::session()->get('csrf_token') ?>">
    <!-- other form fields -->
</form>
使用 Latte

您也可以在 Latte 模板中设置一个自定义函数来输出 CSRF 令牌。


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

    // other configurations...

    // Set a custom function to output the CSRF token
    $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()}
    <!-- other form fields -->
</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 or wherever you have your routes
use app\middlewares\CsrfMiddleware;

Flight::group('', function(Router $router) {
    $router->get('/users', [ 'UserController', 'getUsers' ]);
    // more routes
}, [ CsrfMiddleware::class ]);
事件过滤器
// This middleware checks if the request is a POST request and if it is, it checks if the CSRF token is valid
Flight::before('start', function() {
    if(Flight::request()->method == 'POST') {

        // capture the csrf token from the form values
        $token = Flight::request()->data->csrf_token;
        if($token !== Flight::session()->get('csrf_token')) {
            Flight::halt(403, 'Invalid CSRF token');
            // or for a JSON response
            Flight::jsonHalt(['error' => 'Invalid CSRF token'], 403);
        }
    }
});

跨站脚本攻击 (XSS)

跨站脚本攻击 (XSS) 是一种攻击类型,其中恶意表单输入可以将代码注入到您的网站中。这些机会大多来自最终用户将填写的表单值。您绝不应该信任来自用户的输出!始终假设他们是世界上最好的黑客。他们可以将恶意的 JavaScript 或 HTML 注入到您的页面中。此代码可用于从您的用户窃取信息或在您的网站上执行操作。使用 Flight 的视图类或其他模板引擎如 Latte,您可以轻松转义输出以防止 XSS 攻击。

// Let's assume the user is clever as tries to use this as their name
$name = '<script>alert("XSS")</script>';

// This will escape the output
Flight::view()->set('name', $name);
// This will output: &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;

// If you use something like Latte registered as your view class, it will also auto escape this.
Flight::view()->render('template', ['name' => $name]);

SQL 注入

SQL 注入是一种攻击类型,其中恶意用户可以将 SQL 代码注入到您的数据库中。这可以用于从您的数据库窃取信息或在您的数据库上执行操作。您再次绝不应该信任来自用户的输入!始终假设他们是来者不善的。您可以使用 PDO 对象中的预准备语句来防止 SQL 注入。

// Assuming you have Flight::db() registered as your PDO object
$statement = Flight::db()->prepare('SELECT * FROM users WHERE username = :username');
$statement->execute([':username' => $username]);
$users = $statement->fetchAll();

// If you use the PdoWrapper class, this can easily be done in one line
$users = Flight::db()->fetchAll('SELECT * FROM users WHERE username = :username', [ 'username' => $username ]);

// You can do the same thing with a PDO object with ? placeholders
$statement = Flight::db()->fetchAll('SELECT * FROM users WHERE username = ?', [ $username ]);

不安全示例

下面是为什么我们使用 SQL 预准备语句来保护免受像下面这样的无害示例的影响:

// end user fills out a web form.
// for the value of the form, the hacker puts in something like this:
$username = "' OR 1=1; -- ";

$sql = "SELECT * FROM users WHERE username = '$username' LIMIT 5";
$users = Flight::db()->fetchAll($sql);
// After the query is build it looks like this
// SELECT * FROM users WHERE username = '' OR 1=1; -- LIMIT 5

// It looks strange, but it's a valid query that will work. In fact,
// it's a very common SQL injection attack that will return all users.

var_dump($users); // this will dump all users in the database, not just the one single username

CORS

跨源资源共享 (CORS) 是一种机制,允许 Web 页面上的许多资源(例如字体、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
    {
        // customize your allowed hosts here.
        $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 or wherever you have your routes
$CorsUtil = new CorsUtil();

// This needs to be run before start runs.
Flight::before('start', [ $CorsUtil, 'setupCors' ]);

错误处理

在生产环境中隐藏敏感的错误细节,以避免向攻击者泄露信息。在生产环境中,将 display_errors 设置为 0,并记录错误而不是显示它们。

// In your bootstrap.php or index.php

// add this to your app/config/config.php
$environment = ENVIRONMENT;
if ($environment === 'production') {
    ini_set('display_errors', 0); // Disable error display
    ini_set('log_errors', 1);     // Log errors instead
    ini_set('error_log', '/path/to/error.log');
}

// In your routes or controllers
// Use Flight::halt() for controlled error responses
Flight::halt(403, 'Access denied');

输入净化

绝不信任用户输入。在处理之前使用 filter_var 净化它,以防止恶意数据潜入。


// Lets assume a $_POST request with $_POST['input'] and $_POST['email']

// Sanitize a string input
$clean_input = filter_var(Flight::request()->data->input, FILTER_SANITIZE_STRING);
// Sanitize an email
$clean_email = filter_var(Flight::request()->data->email, FILTER_SANITIZE_EMAIL);

密码哈希

使用 PHP 的内置函数如 password_hashpassword_verify 安全地存储密码并验证它们。密码绝不应该以明文形式存储,也不应该使用可逆方法加密它们。哈希确保即使您的数据库被入侵,实际密码仍然受到保护。

$password = Flight::request()->data->password;
// Hash a password when storing (e.g., during registration)
$hashed_password = password_hash($password, PASSWORD_DEFAULT);

// Verify a password (e.g., during login)
if (password_verify($password, $stored_hash)) {
    // Password matches
}

速率限制

通过使用缓存限制请求速率来保护免受暴力攻击或拒绝服务攻击。

// Assuming you have flightphp/cache installed and registered
// Using flightphp/cache in a filter
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); // Reset after 60 seconds
});

另请参阅

故障排除

更新日志

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!';
});
// HEAD 请求到 /info 将返回相同的标头,但无主体。

OPTIONS 请求

OPTIONS 请求由 Flight 为任何定义的路由自动处理。

// 对于定义为:
Flight::route('GET|POST /users', function() { /* ... */ });

// OPTIONS 请求到 /users 将响应:
//
// 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
});

注意: 不支持使用位置参数匹配正则组 ()。例如::'\(

重要注意事项

在上面的示例中,@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 未找到处理程序

默认情况下,如果找不到 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 响应(例如:方法不允许。允许的方法是:GET, POST)。它还将包含一个 Allow 标头,带有该 URL 的允许方法。

如果您想要更自定义的 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');
});

检查路由信息

如果您想检查匹配的路由信息,有两种方式可以做到:

  1. 您可以使用 Flight::router() 对象上的 executedRoute 属性。
  2. 您可以通过在路由方法中将第三个参数传递为 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_buffering 设置为 false 时可用。

手动标头的流式传输

您可以通过在路由上使用 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 未找到或意外路由行为

如果您看到 404 未找到错误(但您发誓它确实存在,并且不是拼写错误),这实际上可能是因为您在路由端点中返回了一个值而不是简单地回显它。这是有意的,但可能会让一些开发者措手不及。

Flight::route('/hello', function(){
    // 这可能会导致 404 未找到错误
    return 'Hello World';
});

// 您可能想要的
Flight::route('/hello', function(){
    echo 'Hello World';
});

这样做的原因是路由器中内置了一个特殊机制,将返回输出处理为“转到下一个路由”的信号。 您可以在 路由 部分查看文档化的行为。

更新日志

Learn/learn

了解 Flight

Flight 是一个快速、简单、可扩展的 PHP 框架。它非常通用,可用于构建任何类型的 Web 应用程序。 它以简单性为设计理念,并以易于理解和使用的方式编写。

注意: 您将看到一些示例使用 Flight:: 作为静态变量,而另一些使用 $app-> 引擎对象。两者可以互换使用。在控制器/中间件中,$app$this->app 是 Flight 团队推荐的方法。

核心组件

路由

了解如何管理 Web 应用程序的路由。这还包括路由分组、路由参数和中间件。

中间件

了解如何使用中间件来过滤应用程序中的请求和响应。

自动加载

了解如何在应用程序中自动加载您自己的类。

请求

了解如何在应用程序中处理请求和响应。

响应

了解如何向用户发送响应。

HTML 模板

了解如何使用内置视图引擎渲染 HTML 模板。

安全

了解如何保护应用程序免受常见安全威胁。

配置

了解如何为您的应用程序配置框架。

事件管理器

了解如何使用事件系统向应用程序添加自定义事件。

扩展 Flight

了解如何通过添加您自己的方法和类来扩展框架。

方法钩子和过滤

了解如何向方法和内部框架方法添加事件钩子。

依赖注入容器 (DIC)

了解如何使用依赖注入容器 (DIC) 来管理应用程序的依赖项。

实用类

集合

集合用于存储数据,并可以作为数组或对象访问,以方便使用。

JSON 包装器

这提供了几个简单函数,使 JSON 的编码和解码保持一致。

PDO 包装器

PDO 有时会带来不必要的麻烦。这个简单的包装类可以显著简化与数据库的交互。

上传文件处理程序

一个简单的类,帮助管理上传的文件并将其移动到永久位置。

重要概念

为什么使用框架?

这是一篇简短的文章,解释为什么您应该使用框架。在开始使用框架之前,了解其好处是个好主意。

此外,@lubiana 创建了一个优秀的教程。虽然它没有详细介绍 Flight 的具体内容, 但这个指南将帮助您理解围绕框架的一些主要概念,以及为什么使用它们有益。 您可以在 这里 找到该教程。

Flight 与其他框架的比较

如果您从其他框架(如 Laravel、Slim、Fat-Free 或 Symfony)迁移到 Flight,此页面将帮助您了解两者之间的差异。

其他主题

单元测试

按照此指南学习如何对 Flight 代码进行单元测试,使其坚如磐石。

AI 与开发者体验

了解 Flight 如何与 AI 工具和现代开发者工作流程配合,帮助您更快、更智能地编码。

从 v2 迁移到 v3

向后兼容性在大多数情况下已保持,但从 v2 迁移到 v3 时,您应该注意一些更改。

Learn/unit_testing

单元测试

概述

Flight 中的单元测试帮助您确保应用程序按预期运行,早发现错误,并使您的代码库更容易维护。Flight 设计为与 PHPUnit 无缝协作,这是最受欢迎的 PHP 测试框架。

理解

单元测试检查应用程序的小部分行为(如控制器或服务)在隔离状态下。在 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

Flight 与 Symfony

什么是Symfony?

Symfony 是一组可重复使用的 PHP 组件和用于 Web 项目的 PHP 框架。

构建最佳 PHP 应用程序的标准基础。选择任何您自己应用程序所需的 50 个独立组件。

加速创建和维护您的 PHP Web 应用程序。结束重复的编码任务,享受控制代码的力量。

与 Flight 相比的优势

与 Flight 相比的缺点

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 辅助工具

首先,将 PdoWrapper 类与 Flight 注册:

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

现在,您可以在任何地方使用 Flight::db() 来获取数据库连接。

运行查询

runQuery()

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

用于 INSERT、UPDATE,或者当您想要手动获取结果时:

$db = Flight::db();
$statement = $db->runQuery("SELECT * FROM users WHERE status = ?", ['active']);
while ($row = $statement->fetch()) {
    // $row 是数组
}

您也可以用于写入:

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

fetchField()

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

从数据库获取单个值:

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

fetchRow()

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

获取单行作为 Collection(数组/对象访问):

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

fetchAll()

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

获取所有行作为 Collection 数组:

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

使用 IN() 占位符

您可以在 IN() 子句中使用单个 ?,并传递数组或逗号分隔的字符串:

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

高级用法

查询日志记录 & APM

如果您想要跟踪查询性能,请在注册时启用 APM 跟踪:

Flight::register('db', \flight\database\PdoWrapper::class, [
    'mysql:host=localhost;dbname=cool_db_name', 'user', 'pass', [/* options */], 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),这很有用。请参阅basic usage部分了解如何做。

此外,还有一些有用的默认设置,当使用 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); 

注意: 如果您仅传入中间件名称,它将自动由 依赖注入容器 执行,并且中间件将使用它需要的参数执行。如果您没有注册依赖注入容器,它将默认将 flight\Engine 实例传入 __construct(Engine $app)

使用带参数的路由

如果您需要路由中的参数,它们将以单个数组形式传递给您的中间件函数。(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;

        // 也许如果没有作业 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 ]);

使用中间件分组路由

您可以添加一个路由组,然后该组中的每个路由都将具有相同的中间件。如果您需要按 Auth 中间件分组一堆路由来检查标头中的 API 密钥,这将非常有用。


// 添加到 group 方法的末尾
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() ]);

如果您想将全局中间件应用到所有路由,可以添加一个“空”组:


// 添加到 group 方法的末尾
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 禁止错误,但没有自定义。
  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;
        }
    }
}

自定义错误示例

假设您需要抛出 JSON 错误,因为您正在构建 API。您可以这样实现:

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 允许您在调用 mapped methods 前后过滤它们。

理解

没有预定义的钩子需要您记忆。您可以过滤任何默认框架方法以及您已映射的任何自定义方法。

过滤函数看起来像这样:

/**
 * @param array $params 被过滤方法的传递参数。
 * @param string $output (仅 v2 输出缓冲)被过滤方法的输出。
 * @return bool 返回 true/void 或不返回以继续链条,返回 false 以中断链条。
 */
function (array &$params, string &$output): bool {
  // 过滤代码
}

使用传递的变量,您可以操纵输入参数和/或输出。

您可以通过以下方式让过滤器在方法之前运行:

Flight::before('start', function (array &$params, string &$output): bool {
  // 执行某些操作
});

您可以通过以下方式让过滤器在方法之后运行:

Flight::after('start', function (array &$params, string &$output): bool {
  // 执行某些操作
});

您可以为任何方法添加任意数量的过滤器。它们将按照声明顺序被调用。

以下是过滤过程的示例:

// 映射一个自定义方法
Flight::map('hello', function (string $name) {
  return "Hello, $name!";
});

// 添加一个 before 过滤器
Flight::before('hello', function (array &$params, string &$output): bool {
  // 操纵参数
  $params[0] = 'Fred';
  return true;
});

// 添加一个 after 过滤器
Flight::after('hello', function (array &$params, string &$output): bool {
  // 操纵输出
  $output .= " Have a nice day!";
  return true;
});

// 调用自定义方法
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';

  // 这将结束链条
  return false;
});

// 这个不会被调用
Flight::before('start', function (array &$params, string &$output): bool {
  echo 'three';
  return true;
});

注意: 核心方法如 mapregister 无法被过滤,因为它们被直接调用而非动态调用。请参阅 Extending Flight 以获取更多信息。

另请参阅

故障排除

更新日志

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

你可以通过 query 属性访问 $_GET 数组:

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

你可以通过 data 属性访问 $_POST 数组:

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

你可以通过 cookies 属性访问 $_COOKIE 数组:

Flight::route('GET /login', function(){
    $savedLogin = Flight::request()->cookies['myLoginCookie'];
    // 或
    $savedLogin = Flight::request()->cookies->myLoginCookie;
    // 检查它是否真的保存了,如果是则自动登录
    if($savedLogin) {
        Flight::redirect('/dashboard');
        return;
    }
});

有关设置新 cookie 值的帮助,请参阅 overclokk/cookie

$_SERVER

可以通过 getVar() 方法快捷访问 $_SERVER 数组:


$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());
    }
});

安全注意: 始终验证和清理用户输入,特别是处理文件上传时。始终验证你允许上传的扩展类型,但你还应该验证文件的“魔术字节”以确保它是用户声称的文件类型。有 文章 可帮助处理此问题。

请求主体

要获取原始 HTTP 请求主体,例如处理 POST/PUT 请求时,你可以这样做:

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
// 注意,没有尾随斜杠。

查询解析

你可以将 URL 传递给 parseQuery() 方法来解析查询字符串为关联数组:

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

协商内容接受类型

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 与代码中的特定功能匹配的过程。 这是您可以根据被请求的 URL 使您的网站执行不同操作的方法。例如,当用户访问 /user/1234 时,您可能希望显示用户的个人资料, 但当他们访问 /users 时显示所有用户的列表。所有这些都通过路由完成。

可能像这样运作:

  1. 用户转到您的浏览器并键入 http://example.com/user/1234
  2. 服务器收到请求,检查 URL 并将其传递到您的 Flight 应用程序代码。
  3. 假设在您的 Flight 代码中有类似 Flight::route('/user/@id', [ 'UserController', 'viewUserProfile' ]); 这样的东西。 您的 Flight 应用程序代码检查 URL 并看到它匹配您定义的路由,然后运行为该路由定义的代码。
  4. 然后 Flight 路由将运行并调用 UserController 类中的 viewUserProfile($id) 方法,将 1234 作为 $id 参数传入该方法。
  5. 您的 viewUserProfile() 方法中的代码将运行并执行您告诉它要执行的操作。 您可能会输出一些用户资料页的 HTML,或者如果这是一个 RESTful API,则可能打印出包含用户信息的 JSON 响应。
  6. Flight 将其整理起来,生成响应头并将其发送回用户的浏览器。
  7. 用户充满喜悦,自我给自己一个温暖的拥抱!

为什么重要?

拥有一个合适的中心化路由器实际上会大大简化您的生活!起初可能有点难以看到。以下是一些原因:

可能您熟悉逐个脚本的方式创建网站。您可能有一个名为 index.php 的文件,其中包含一堆 if 语句,以检查 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);
}

// 等等...

还是这样?


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

// 可能在您的 app/controllers/UserController.php 中
class UserController {
    public function viewUserProfile($id) {
        // 做一些事情
    }

    public function editUserProfile($id) {
        // 做一些事情
    }
}

希望您开始看到使用中心化路由系统的好处。长期而言,这将更易于管理和理解!

请求与响应

Flight 提供了一种简单易用的方式处理请求和响应。这是 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 会捕获它并使用适当的头部发送回用户。

// 这将向用户的浏览器发送 "Hello, World!"
Flight::route('/', function() {
    echo "Hello, World!";
});

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

作为替代,您也可以调用 write() 方法来添加主体内容。

// 这将向用户的浏览器发送 "Hello, World!"
Flight::route('/', function() {
    // 冗长,但有时在需要时能完成任务
    Flight::response()->write("Hello, World!");

    // 如果您想在此时检索已设置的主体内容
    // 您可以这样操作
    $body = Flight::response()->getBody();
});

JSON

Flight 提供对发送 JSON 和 JSONP 响应的支持。要发送 JSON 响应,您需要传递一些要进行 JSON 编码的数据:

Flight::route('/@companyId/users', function(int $companyId) {
    // 例如,从数据库中提取用户
    $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"}, /* 更多用户 */ ]

注意: 默认情况下,Flight 会发送 Content-Type: application/json 头部与响应一起。它还会使用标志 JSON_THROW_ON_ERRORJSON_UNESCAPED_SLASHES 来编码 JSON。

带状态码的 JSON

您也可以将状态码作为第二个参数传递:

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

美化打印的 JSON

您也可以在最后一个位置传递一个参数来启用美化打印:

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

更改 JSON 参数顺序

Flight::json() 是一个非常旧的方法,但 Flight 的目标是维护项目的向后兼容性。 实际上,如果您想重新排列参数顺序以使用更简单的语法,您可以像 任何其他 Flight 方法 一样重新映射 JSON 方法:

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

    // 现在在使用 json() 方法时,您不必再使用 `true, 'utf-8'`!
    Flight::_json($data, $code, true, 'utf-8', $options);
}

// 现在可以这样使用
Flight::json(['id' => 123], 200, JSON_PRETTY_PRINT);

JSON 和停止执行

v3.10.0

如果您想发送 JSON 响应并停止执行,您可以使用 jsonHalt() 方法。 这对于检查某种授权类型的情况很有用,如果用户未授权,您可以立即发送 JSON 响应、清空现有主体内容并停止执行。

Flight::route('/users', function() {
    $authorized = someAuthorizationCheck();
    // 检查用户是否已授权
    if($authorized === false) {
        Flight::jsonHalt(['error' => 'Unauthorized'], 401);
        // 这里不需要 exit。
    }

    // 继续执行路由的其余部分
});

在 v3.10.0 之前,您必须这样做:

Flight::route('/users', function() {
    $authorized = someAuthorizationCheck();
    // 检查用户是否已授权
    if($authorized === false) {
        Flight::halt(401, json_encode(['error' => 'Unauthorized']));
    }

    // 继续执行路由的其余部分
});

清空响应主体

如果您想清空响应主体,您可以使用 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]);
});

// 这将对任何路由的所有响应进行 gzip 压缩
Flight::response()->addResponseBodyCallback(function($body) {
    return gzencode($body, 9);
});

您可以添加多个回调,它们将按添加顺序运行。因为这可以接受任何 callable,它可以接受类数组 [ $class, 'method' ]、闭包 $strReplace = function($body) { str_replace('hi', 'there', $body); };,或函数名 'minify',例如如果您有一个函数来压缩 HTML 代码。

注意: 如果您使用 flight.v2.output_buffering 配置选项,路由回调将不起作用。

特定路由回调

如果您希望这仅适用于特定路由,您可以在路由本身中添加回调:

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

    // 这将仅对该路由的响应进行 gzip 压缩
    Flight::response()->addResponseBodyCallback(function($body) {
        return gzencode($body, 9);
    });
});

中间件选项

您也可以使用 middleware 通过中间件将回调应用于所有路由:

// MinifyMiddleware.php
class MinifyMiddleware {
    public function before() {
        // 在 response() 对象上应用回调。
        Flight::response()->addResponseBodyCallback(function($body) {
            return $this->minify($body);
        });
    }

    protected function minify(string $body): string {
        // 以某种方式压缩主体
        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 方法设置诸如响应内容类型的头部:

// 这将以纯文本形式向用户的浏览器发送 "Hello, World!"
Flight::route('/', function() {
    Flight::response()->header('Content-Type', 'text/plain');
    // 或
    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; // 这是必要的,以防止下面的功能执行
    }

    // 添加新用户...
    Flight::db()->runQuery("INSERT INTO users ....");
    Flight::redirect('/admin/dashboard');
});

注意: 默认情况下,Flight 发送 HTTP 303 (“See Other”) 状态码。您可以选择设置自定义代码:

Flight::redirect('/new/location', 301); // 永久

停止路由执行

您可以通过调用 halt 方法在任何点停止框架并立即退出:

Flight::halt();

您也可以指定可选的 HTTP 状态码和消息:

Flight::halt(200, 'Be right back...');

调用 halt 将丢弃到该点的任何响应内容并停止所有执行。 如果您想停止框架并输出当前响应,请使用 stop 方法:

Flight::stop($httpStatusCode = null);

注意: Flight::stop() 有一些奇怪的行为,例如它会输出响应但继续执行您的脚本,这可能不是您想要的。您可以在调用 Flight::stop() 后使用 exitreturn 来防止进一步执行,但一般推荐使用 Flight::halt()

这将保存头部键和值到响应对象。在请求生命周期结束时,它将构建头部并发送响应。

高级用法

立即发送头部

有时您需要对头部进行自定义操作,并在您正在处理的代码行上发送头部。 如果您正在设置 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

注意: 如果您在 2025 年及以后仍然使用 JSONP 请求,请加入聊天并告诉我们原因!我们喜欢听一些好的战斗/恐怖故事!

清空响应数据

您可以使用 clear() 方法清空响应主体和头部。这将清除分配给响应的任何头部、清空响应主体,并将状态码设置为 200

Flight::response()->clear();

仅清空响应主体

如果您只想清空响应主体,您可以使用 clearBody() 方法:

// 这仍然会保留在 response() 对象上设置的任何头部。
Flight::response()->clearBody();

HTTP 缓存

Flight 提供内置支持 HTTP 级别的缓存。如果满足缓存条件,Flight 将返回 HTTP 304 Not Modified 响应。下次客户端请求相同资源时,他们将被提示使用本地缓存版本。

路由级别缓存

如果您想缓存整个响应,您可以使用 cache() 方法并传递缓存时间。

// 这将缓存响应 5 分钟
Flight::route('/news', function () {
  Flight::response()->cache(time() + 300);
  echo 'This content will be cached.';
});

// 或者,您可以使用传递给 strtotime() 方法的字符串
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.';
});

请记住,调用 lastModifiedetag 都会设置和检查缓存值。如果请求之间的缓存值相同,Flight 将立即发送 HTTP 304 响应并停止处理。

下载文件

v3.12.0

有一个辅助方法可以将文件流式传输到最终用户。您可以使用 download 方法并传递路径。

Flight::route('/download', function () {
  Flight::download('/path/to/file.txt');
  // 从 v3.17.1 开始,您可以为下载指定自定义文件名
  Flight::download('/path/to/file.txt', 'custom_name.txt');
});

另请参阅

故障排除

更新日志

Learn/events

事件管理器

自 v3.15.0 起

概述

事件允许您在应用程序中注册和触发自定义行为。通过添加 Flight::onEvent()Flight::triggerEvent(),您现在可以钩入应用程序生命周期的关键时刻,或者定义自己的事件(例如通知和电子邮件),使您的代码更模块化和可扩展。这些方法是 Flight 的 mappable methods 的一部分,这意味着您可以根据需要覆盖它们的行为。

理解

事件允许您将应用程序的不同部分分离,从而避免它们过于依赖彼此。这种分离——通常称为解耦——使您的代码更容易更新、扩展或调试。与将所有内容写成一个大块不同,您可以将逻辑拆分为更小、更独立的片段,这些片段响应特定操作(事件)。

想象您正在构建一个博客应用程序:

没有事件,您会将所有这些塞到一个函数中。有了事件,您可以将其拆分:一部分保存评论,另一部分触发像 'comment.posted' 这样的事件,单独的监听器处理电子邮件和日志记录。这使您的代码更干净,并允许您添加或移除功能(例如通知),而无需触及核心逻辑。

常见用例

大多数情况下,事件适用于可选但不是系统绝对核心的部分。例如,以下是好的但如果因某种原因失败,您的应用程序仍应正常工作:

然而,假设您有一个忘记密码功能。那应该是您核心功能的一部分,而不是事件,因为如果那封电子邮件没有发送出去,用户就无法重置密码并使用您的应用程序。

基本用法

Flight 的事件系统围绕两个主要方法构建: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'));
});

// 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'));
});

Flight::onEvent('user.registered', function ($email, $name) {
    echo "Email sent to $email: Welcome, $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 = '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");
});
Flight::onEvent('user.registered', function ($email) {
    echo "Welcome to $email!";
});

// Page Events
Flight::onEvent('page.updated', function ($pageId) {
    Flight::cache()->delete("page_$pageId");
});

这种结构扩展良好且保持对初学者的友好。

真实世界示例

让我们通过一些真实世界场景来展示事件如何工作以及为什么它们有用。

示例 1:记录用户登录

// Step 1: Register a listener
Flight::onEvent('user.login', function ($username) {
    $time = date('Y-m-d H:i:s');
    error_log("$username logged in at $time");
});

// Step 2: Trigger it in your app
Flight::route('/login', function () {
    $username = 'bob'; // Pretend this comes from a form
    Flight::triggerEvent('user.login', $username);
    echo "Hi, $username!";
});

为什么有用:登录代码不需要知道日志记录——它只需触发事件。您可以稍后添加更多监听器(例如,发送欢迎电子邮件),而无需更改路由。

示例 2:通知新用户

// Listener for new registrations
Flight::onEvent('user.registered', function ($email, $name) {
    // Simulate sending an email
    echo "Email sent to $email: Welcome, $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
    Flight::cache()->delete("page_$pageId");
    echo "Cache cleared for page $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.";
});

为什么有用:编辑代码不关心缓存——它只需信号更新。应用程序的其他部分可以根据需要反应。

最佳实践

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 方法,使用 Latte 而不是默认的 PHP 渲染器。

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

集合

概述

Flight 中的 Collection 类是一个方便的实用工具,用于管理数据集合。它允许您使用数组和对象表示法来访问和操作数据,使您的代码更简洁和灵活。

理解

Collection 基本上是一个数组的包装器,但具有一些额外的功能。您可以像数组一样使用它,遍历它,计算其项数,甚至将项访问为对象属性。这在您希望在应用程序中传递结构化数据,或使您的代码更易读时特别有用。

Collection 实现了几个 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

Collection 是可迭代的,因此您可以在 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 序列化

Collection 可以轻松转换为 JSON:

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

高级用法

如果需要,您可以完全替换内部数据数组:

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

Collection 在您希望在组件之间传递结构化数据,或为数组数据提供更面向对象的接口时特别有用。

另请参阅

故障排除

更新日志

Learn/flight_vs_fat_free

Flight 与 Fat-Free

什么是 Fat-Free?

Fat-Free(亲切地称为 F3)是一个强大且易于使用的 PHP 微框架,旨在帮助您快速构建动态且健壮的 Web 应用程序!

Flight 在许多方面与 Fat-Free 相似,并且在功能和简单性方面可能是最接近的亲戚。Fat-Free 拥有 许多 Flight 没有的功能,但它也拥有许多 Flight 拥有的功能。Fat-Free 开始显示出它的年龄 ,并且不像曾经那样流行。

更新变得不那么频繁,社区也不像曾经那样活跃。代码足够简单,但有时缺乏 语法规范会使其难以阅读和理解。它确实支持 PHP 8.3,但代码本身仍然看起来像是生活在 PHP 5.3 中。

与 Flight 相比的优点

与 Flight 相比的缺点

Learn/extending

扩展

概述

Flight 被设计为一个可扩展的框架。该框架附带一组默认方法和组件,但它允许您映射自己的方法、注册自己的类,甚至覆盖现有的类和方法。

理解

您可以通过 2 种方式扩展 Flight 的功能:

  1. 映射方法 - 这用于创建简单的自定义方法,您可以在应用程序的任何地方调用它们。这些通常用于实用函数,您希望能够在代码的任何地方调用。
  2. 注册类 - 这用于将自己的类注册到 Flight 中。这通常用于具有依赖项或需要配置的类。

您也可以覆盖现有的框架方法,以更改其默认行为,以更好地满足您的项目需求。

如果您正在寻找 DIC(依赖注入容器),请跳转到 依赖注入容器 页面。

基本用法

覆盖框架方法

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 添加的任何自定义方法也可以被过滤。有关如何过滤这些方法的示例,请参阅 过滤方法 指南。

可扩展的框架类

您可以通过扩展它们并注册自己的类来覆盖几个类的功能。这些类是:

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();
  }
}

如果您传入额外的回调参数,它将在类构造后立即执行。这允许您为新对象执行任何设置过程。回调函数接受一个参数,即新对象的实例。

// 回调将传递已构造的对象
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(应用程序性能监控)系统:

// 在您的 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(依赖注入容器),您可以使用 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 知道用于控制器/中间件的它
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 包装器

概述

Flight 中的 Json 类提供了一种简单、一致的方式来编码和解码应用程序中的 JSON 数据。它封装了 PHP 的原生 JSON 函数,具有更好的错误处理和一些有用的默认设置,使处理 JSON 变得更容易和更安全。

理解

在现代 PHP 应用程序中处理 JSON 非常常见,尤其是在构建 API 或处理 AJAX 请求时。Json 类集中处理所有 JSON 编码和解码,因此您无需担心 PHP 内置函数的奇怪边缘情况或神秘错误。

关键特性:

基本用法

将数据编码为 JSON

要将 PHP 数据转换为 JSON 字符串,请使用 Json::encode()

use flight\util\Json;

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

$json = Json::encode($data);
echo $json;
// 输出: {"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; // 输出: Flight

如果您想要关联数组而不是对象,请将 true 作为第二个参数传递:

$data = Json::decode($json, true);
echo $data['framework']; // 输出: Flight

如果解码失败,您将收到一个带有清晰错误消息的异常。

验证 JSON

检查字符串是否为有效的 JSON:

if (Json::isValid($json)) {
  // 它是有效的!
} else {
  // 不是有效的 JSON
}

获取最后错误

如果您想要检查最后 JSON 错误消息(来自原生 PHP 函数):

$error = Json::getLastError();
if ($error !== '') {
  echo "最后 JSON 错误: $error";
}

高级用法

如果您需要更多控制,可以自定义编码和解码选项(参见 PHP 的 json_encode 选项):

// 使用 HEX_TAG 选项编码
$json = Json::encode($data, JSON_HEX_TAG);

// 使用自定义深度解码
$data = Json::decode($json, false, 1024);

另请参阅

故障排除

更新日志

Learn/flight_vs_slim

Flight 与 Slim

什么是 Slim?

Slim 是一个 PHP 微框架,它帮助您快速编写简单却强大的 Web 应用程序和 API。

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() {
        // 做些什么
    }
}

命名空间

如果您确实有命名空间,实现起来其实非常简单。您应该使用 Flight::path() 方法指定应用程序的根目录(不是文档根目录或 public/ 文件夹)。


/**
 * 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, 那么自动加载器将无法找到它。

错误的命名空间

如果您使用命名空间,那么命名空间应该匹配目录结构。

// ...代码...

// 如果您的 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

使用 PHPUnit 在 Flight PHP 中进行单元测试

本指南介绍了使用 PHPUnit 在 Flight PHP 中进行单元测试,针对希望了解为什么单元测试重要以及如何实际应用它的初学者。我们将重点关注测试行为——确保您的应用程序按预期执行,例如发送电子邮件或保存记录——而不是琐碎的计算。我们将从一个简单的 route handler 开始,逐步推进到一个更复杂的 controller,并结合 dependency injection (DI) 和模拟第三方服务。

为什么进行单元测试?

单元测试确保您的代码按预期行为,在问题到达生产环境之前捕获 bug。在 Flight 中,这尤其有价值,因为轻量级路由和灵活性可能导致复杂的交互。对于独行开发者或团队,单元测试充当安全网,记录预期行为,并在您稍后重访代码时防止回归。它们还能改善设计:难以测试的代码通常表明类过于复杂或耦合紧密。

与简单示例(例如,测试 x * y = z)不同,我们将重点关注现实世界的行为,例如验证输入、保存数据或触发电子邮件等操作。我们的目标是使测试易于接近且有意义。

一般指导原则

  1. 测试行为,而不是实现:关注结果(例如,“电子邮件已发送”或“记录已保存”),而不是内部细节。这使测试在重构时更健壮。
  2. 停止使用 Flight:::Flight 的静态方法非常方便,但会使测试变得困难。您应该习惯使用 $app = Flight::app(); 中的 $app 变量。$app 具有与 Flight:: 相同的全部方法。您仍然可以在控制器中使用 $app->route()$this->app->json() 等。您还应该使用真实的 Flight 路由器 $router = $app->router(),然后可以使用 $router->get()$router->post()$router->group() 等。请参阅 Routing
  3. 保持测试快速:快速测试鼓励频繁执行。避免在单元测试中使用慢速操作,如数据库调用。如果您有一个慢速测试,这表明您正在编写集成测试,而不是单元测试。集成测试涉及实际的数据库、实际的 HTTP 调用、实际的电子邮件发送等。它们有其位置,但它们缓慢且可能不稳定,意味着它们有时会因未知原因失败。
  4. 使用描述性名称:测试名称应清楚描述被测试的行为。这提高了可读性和可维护性。
  5. 像瘟疫一样避免全局变量:最小化 $app->set()$app->get() 的使用,因为它们像全局状态一样,需要在每个测试中模拟。优先使用 DI 或 DI 容器(请参阅 Dependency Injection Container)。即使使用 $app->map() 方法在技术上也是“全局”的,应避免使用 DI 替代。使用会话库如 flightphp/session,以便在测试中模拟会话对象。不要在您的代码中直接调用 $_SESSION,因为这会将全局变量注入您的代码,使其难以测试。
  6. 使用依赖注入:将依赖项(例如,PDO、邮件程序)注入控制器,以隔离逻辑并简化模拟。如果您的类有太多依赖项,请考虑将其重构为更小的类,每个类都有单一职责,遵循 SOLID 原则
  7. 模拟第三方服务:模拟数据库、HTTP 客户端(cURL)或电子邮件服务,以避免外部调用。测试一到两层深度,但让您的核心逻辑运行。例如,如果您的应用发送短信,您希望每次运行测试时都真正发送短信,因为那些费用会累积(而且会更慢)。相反,模拟短信服务,并仅验证您的代码以正确的参数调用了短信服务。
  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 可见性:这允许您在测试子类中覆盖它们,而无需将它们设为 public,这对于匿名类模拟特别有用。

设置 PHPUnit

首先,在您的 Flight PHP 项目中使用 Composer 设置 PHPUnit 以进行轻松测试。请参阅 PHPUnit 入门指南 以获取更多细节。

  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 来执行测试。

测试简单的路由处理器

让我们从一个基本的 route 开始,它验证用户的电子邮件输入。我们将测试其行为:对于有效电子邮件返回成功消息,对于无效电子邮件返回错误。对于电子邮件验证,我们使用 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,请参阅相关文档。

使用依赖注入创建可测试的控制器

对于更复杂的场景,使用 dependency injection (DI) 来使控制器可测试。避免 Flight 的全局变量(例如,Flight::set()Flight::map()Flight::register()),因为它们像全局状态一样,需要为每个测试模拟。相反,使用 Flight 的 DI 容器、DICEPHP-DI 或手动 DI。

让我们使用 flight\database\PdoWrapper 而不是原始 PDO。这个包装器更容易模拟和单元测试!

这是一个将用户保存到数据库并发送欢迎电子邮件的控制器:

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;
    }
}

如果我运行上面的单元测试,它们仍然会通过!但因为我没有针对行为进行测试(实际让一些代码运行),我可能在生产环境中编码了一个等待发生的 bug。测试应该修改以考虑新行为,以及当行为不是我们预期的相反情况。

完整示例

您可以在 GitHub 上找到一个带有单元测试的完整 Flight PHP 项目示例:n0nag0n/flight-unit-tests-guide。 对于更深入的理解,请参阅 Unit Testing and SOLID Principles

常见陷阱

使用单元测试扩展

单元测试在大项目中或在数月后重访代码时大放异彩。它们记录行为并捕获回归,从而节省您重新学习应用程序的时间。对于独行开发者,测试关键路径(例如,用户注册、支付处理)。对于团队,测试确保贡献行为一致。请参阅 Why Frameworks? 以获取更多关于使用框架和测试的好处的信息。

向 Flight PHP 文档仓库贡献您自己的测试提示!

n0nag0n 撰写 2025

Guides/blog

使用 Flight PHP 构建简单博客

本指南带您通过使用 Flight PHP 框架创建基本博客的过程。您将设置项目,定义路由,使用 JSON 管理帖子,并使用 Latte 模板引擎进行呈现——所有这些都展示了 Flight 的简单性和灵活性。到最后,您将拥有一个功能性博客,包含主页、单独的帖子页面和创建表单。

先决条件

第一步:设置您的项目

首先创建一个新的项目目录并通过 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: 创建 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!”。

第二步:组织您的项目结构

为了保持设置整洁,请将项目构建为如下结构:

flight-blog/
├── app/
│   ├── config/
│   └── views/
├── data/
├── public/
│   └── index.php
├── vendor/
└── composer.json

第三步:安装和配置 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 撰写的第一篇博客帖子!"
       }
    ]

第四步:定义路由

将路由分开到配置文件中,以便更好地组织。

  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();

第五步:存储和检索博客帖子

添加加载和保存帖子的功能。

  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' => '创建帖子']);
    });

第六步:创建模板

更新您的模板以显示帖子。

  1. 帖子页面 (app/views/post.latte)

    {extends 'layout.latte'}
    
    {block content}
        <h2>{$post['title']}</h2>
        <div class="post-content">
            <p>{$post['content']}</p>
        </div>
    {/block}

第七步:添加帖子创建功能

处理表单提交以添加新帖子。

  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
    • 提交新的帖子(例如,“第二篇帖子”,以及一些内容)。
    • 检查主页以查看其是否已列出。

第八步:增强错误处理

覆盖 notFound 方法以提供更好的 404 体验。

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

MIT 许可证

版权所有 © 2024 @mikecao, @n0nag0n

特此免费授予任何获得本软件副本及相关文档文件(以下简称“软件”)的人,无偿使用本软件的权限,包括但不限于使用、复制、修改、合并、发布、分发、再许可以及销售本软件的副本,并允许被授予本软件的人员这样做,但须符合以下条件:

上述版权通知和本许可通知应包含在所有副本或重要部分的软件中。

本软件按原样提供,不附带任何形式的担保,包括但不限于适销性、特定用途的适用性和非侵权性的保证。在任何情况下,作者或版权所有者均不承担任何索赔、损害赔偿或其他责任,无论是合同诉讼、侵权行为或其他方面,来源于、无论是源于还是与本软件或本软件的使用或其他交易有关。

About

Flight PHP 框架

Flight 是一个快速、简单、可扩展的 PHP 框架——专为那些希望快速完成任务且不希望大费周章的开发人员而构建。不管您是在构建经典的网络应用、极速的 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 框架比较。

框架 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 聊天

Matrix

以及 Discord

贡献

您可以通过两种方式为 Flight 贡献:

  1. 贡献于核心框架,通过访问 core repository
  2. 帮助改进文档!这个文档网站托管在 Github 上。如果您发现错误或想改进某些内容,请随时提交拉取请求。我们喜欢更新和新想法——尤其是围绕 AI 和新技术!

要求

Flight 需要 PHP 7.4 或更高版本。

注意: PHP 7.4 得到支持,因为在撰写本文时(2024 年),PHP 7.4 是一些 LTS Linux 发行版中的默认版本。强制迁移到 PHP >8 会给那些用户带来很多麻烦。该框架也支持 PHP >8。

许可

Flight 发布 under the MIT 许可。

Awesome-plugins/php_cookie

Cookies

overclokk/cookie 是一个简单的库,用于在应用程序中管理 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() {
        // 设置一个 cookie

        // 想要将其设置为 false,以便获得一个新的实例
        // 如果您想要自动完成,请使用下面的注释
        /** @var \Overclokk\Cookie\Cookie $cookie */
        $cookie = Flight::cookie(false);
        $cookie->set(
            'stay_logged_in', // cookie 的名称
            '1', // 您想设置的值
            86400, // cookie 应持续的秒数
            '/', // 可以访问到 cookie 的路径
            'example.com', // 可以访问到 cookie 的域
            true, // cookie 只会通过安全的 HTTPS 连接传输
            true // cookie 只能通过 HTTP 协议访问
        );

        // 可选地,如果您希望保留默认值,并且希望以更长时间设置 cookie
        $cookie->forever('stay_logged_in', '1');
    }

    public function home() {
        // 检查您是否拥有该 cookie
        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($原始数据) {
    $加密密钥 = /* $config['encryption_key'] 或者是存放密钥位置的 file_get_contents */;
    return Crypto::encrypt($原始数据, Key::loadFromAsciiSafeString($加密密钥));
});

// 解密方法
Flight::map('decrypt', function($加密数据) {
    $加密密钥 = /* $config['encryption_key'] 或者是存放密钥位置的 file_get_contents */;
    try {
        $原始数据 = Crypto::decrypt($加密数据, Key::loadFromAsciiSafeString($加密密钥));
    } catch (Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException $ex) {
        // 一种攻击!加载了错误的密钥,或者自创建以来,密文已更改--在数据库中已损坏或Eve试图执行攻击时故意修改。

        // ...以适合您的应用程序的方式处理这种情况...
    }
    return $原始数据;
});

Flight::route('/encrypt', function() {
    $加密数据 = Flight::encrypt('这是一个机密');
    echo $加密数据;
});

Flight::route('/decrypt', function() {
    $加密数据 = '...'; // 从某处获取加密数据
    $解密数据 = Flight::decrypt($加密数据);
    echo $解密数据;
});

Awesome-plugins/php_file_cache

flightphp/cache

Light, simple and standalone PHP in-file caching class forked from Wruczek/PHP-File-Cache

Advantages

This docs site is using this library to cache each of the pages!

Click here to view the code.

Installation

Install via composer:

composer require flightphp/cache

Usage

Usage is fairly straightforward. This saves a cache file in the cache directory.

use flight\Cache;

$app = Flight::app();

// You pass the directory the cache will be stored in into the constructor
$app->register('cache', Cache::class, [ __DIR__ . '/../cache/' ], function(Cache $cache) {

    // This ensures that the cache is only used when in production mode
    // ENVIRONMENT is a constant that is set in your bootstrap file or elsewhere in your app
    $cache->setDevMode(ENVIRONMENT === 'development');
});

Get a Cache Value

You use the get() method to get a cached value. If you want a convenience method that will refresh the cache if it is expired, you can use refreshIfExpired().


// Get cache instance
$cache = Flight::cache();
$data = $cache->refreshIfExpired('simple-cache-test', function () {
    return date("H:i:s"); // return data to be cached
}, 10); // 10 seconds

// or
$data = $cache->get('simple-cache-test');
if(empty($data)) {
    $data = date("H:i:s");
    $cache->set('simple-cache-test', $data, 10); // 10 seconds
}

Store a Cache Value

You use the set() method to store a value in the cache.

Flight::cache()->set('simple-cache-test', 'my cached data', 10); // 10 seconds

Erase a Cache Value

You use the delete() method to erase a value in the cache.

Flight::cache()->delete('simple-cache-test');

Check if a Cache Value Exists

You use the exists() method to check if a value exists in the cache.

if(Flight::cache()->exists('simple-cache-test')) {
    // do something
}

Clear the Cache

You use the flush() method to clear the entire cache.

Flight::cache()->flush();

Pull out meta data with cache

If you want to pull out timestamps and other meta data about a cache entry, make sure you pass true as the correct parameter.

$data = $cache->refreshIfExpired("simple-cache-meta-test", function () {
    echo "Refreshing data!" . PHP_EOL;
    return date("H:i:s"); // return data to be cached
}, 10, true); // true = return with metadata
// or
$data = $cache->get("simple-cache-meta-test", true); // true = return with metadata

/*
Example cached item retrieved with metadata:
{
    "time":1511667506, <-- save unix timestamp
    "expire":10,       <-- expire time in seconds
    "data":"04:38:26", <-- unserialized data
    "permanent":false
}

Using metadata, we can, for example, calculate when item was saved or when it expires
We can also access the data itself with the "data" key
*/

$expiresin = ($data["time"] + $data["expire"]) - time(); // get unix timestamp when data expires and subtract current timestamp from it
$cacheddate = $data["data"]; // we access the data itself with the "data" key

echo "Latest cache save: $cacheddate, expires in $expiresin seconds";

Documentation

Visit https://github.com/flightphp/cache to view the code. Make sure you see the examples folder for additional ways to use the cache.

Awesome-plugins/permissions

FlightPHP/Permissions

这是一个权限模块,可以在您的项目中使用,如果您的应用程序中有多个角色,并且每个角色有稍微不同的功能。此模块允许您为每个角色定义权限,然后检查当前用户是否具有权限访问某个页面或执行某个操作。

单击此处查看 GitHub 上的存储库。

安装

运行 composer require flightphp/permissions,您就可以开始了!

用法

首先,您需要设置权限,然后告诉您的应用程序这些权限的含义。最终,您将使用 $Permissions->has()->can()is() 检查权限。has()can() 具有相同的功能,但命名不同,以使您的代码更易读。

基本示例

假设您的应用程序中有一个功能,用于检查用户是否已登录。您可以像这样创建一个权限对象:

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

// 一些代码

// 然后您可能有一些内容告诉您当前角色是谁
// 可能您有一些内容从会话变量中提取当前角色
// 以定义此内容
// 在某人登录后,否则他们将拥有 'guest' 或 'public' 角色。
$current_role = 'admin';

// 设置权限
$permission = new \flight\Permission($current_role);
$permission->defineRule('loggedIn', function($current_role) {
    return $current_role !== 'guest';
});

// 您可能希望在 Flight 中某处持久化此对象
Flight::set('permission', $permission);

然后在某个控制器中,您可能会有如下代码。

<?php

// 某个控制器
class SomeController {
    public function someAction() {
        $permission = Flight::get('permission');
        if ($permission->has('loggedIn')) {
            // 做某事
        } else {
            // 做其他事
        }
    }
}

您还可以使用此功能跟踪用户在应用程序中是否具有执行某些操作的权限。 例如,如果您的应用程序允许用户与软件上的帖子进行交互,您可以 检查他们是否有权限执行某些操作。

$current_role = 'admin';

// 设置权限
$permission = new \flight\Permission($current_role);
$permission->defineRule('post', function($current_role) {
    if($current_role === 'admin') {
        $permissions = ['create', 'read', 'update', 'delete'];
    } else if($current_role === 'editor') {
        $permissions = ['create', 'read', 'update'];
    } else if($current_role === 'author') {
        $permissions = ['create', 'read'];
    } else if($current_role === 'contributor') {
        $permissions = ['create'];
    } else {
        $permissions = [];
    }
    return $permissions;
});
Flight::set('permission', $permission);

然后在某个控制器中...

class PostController {
    public function create() {
        $permission = Flight::get('permission');
        if ($permission->can('post.create')) {
            // 做某事
        } else {
            // 做其他事
        }
    }
}

注入依赖项

您可以将依赖项注入定义权限的闭包中。如果您有某种切换、ID 或任何其他要检查的数据点,这很有用。对于 Class->Method 类型的调用同样适用,只是您需要在方法中定义参数。

闭包

$Permission->defineRule('order', function(string $current_role, MyDependency $MyDependency = null) {
    // ... 代码
});

// 在您的控制器文件中
public function createOrder() {
    $MyDependency = Flight::myDependency();
    $permission = Flight::get('permission');
    if ($permission->can('order.create', $MyDependency)) {
        // 做某事
    } else {
        // 做其他事
    }
}

namespace MyApp;

class Permissions {

    public function order(string $current_role, MyDependency $MyDependency = null) {
        // ... 代码
    }
}

使用类设置权限的快捷方式

您还可以使用类来定义您的权限。如果您有很多权限要保持代码整洁,这很有用。您可以这样做:

<?php

// 启动代码
$Permissions = new \flight\Permission($current_role);
$Permissions->defineRule('order', 'MyApp\Permissions->order');

// myapp/Permissions.php
namespace MyApp;

class Permissions {

    public function order(string $current_role, int $user_id) {
        // 假设您先前设置好了这一点
        /** @var \flight\database\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('您无法创建订单。抱歉!');
        }
    }
}

缓存

要启用缓存,请参阅简单的 wruczak/phpfilecache 库。以下是启用此功能的示例。


// 此 $app 可以是您代码的一部分,
// 或者您可以只传递 null,并在构造函数中
// 从 Flight::app() 获取
$app = Flight::app();

// 现在它接受此文件缓存。将来可以轻松添加其他缓存。
$Cache = new Wruczek\PhpFileCache\PhpFileCache;

$Permissions = new \flight\Permission($current_role, $app, $Cache);
$Permissions->defineRulesFromClassMethods(MyApp\Permissions::class, 3600); // 3600 是要将其缓存多少秒。如果不使用缓存,请勿包含此选项

然后开启吧!

Awesome-plugins/simple_job_queue

简单作业队列

简单作业队列是一个可以用于异步处理作业的库。它可以与 beanstalkd、MySQL/MariaDB、SQLite 和 PostgreSQL 一起使用。

安装

composer require n0nag0n/simple-job-queue

用法

为了使这一切正常工作,您需要一种将作业添加到队列的方法以及一种处理作业的方法(工作者)。下面是如何将作业添加到队列以及如何处理作业的示例。

添加到 Flight

将其添加到 Flight 是简单的,并且使用 register() 方法完成。以下是如何将其添加到 Flight 的示例。

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

// 如果您想使用 beanstalkd,请将 ['mysql'] 更改为 ['beanstalkd']
Flight::register('queue', n0nag0n\Job_Queue::class, ['mysql'], function($Job_Queue) {
    // 如果您已经在 Flight::db(); 上有一个 PDO 连接
    $Job_Queue->addQueueConnection(Flight::db());

    // 或者如果您正在使用 beanstalkd/Pheanstalk
    $pheanstalk = Pheanstalk\Pheanstalk::create('127.0.0.1');
    $Job_Queue->addQueueConnection($pheanstalk);
});

添加新作业

当您添加作业时,您需要指定一个管道(队列)。这可以与 RabbitMQ 中的通道或 beanstalkd 中的管道相媲美。

<?php
Flight::queue()->selectPipeline('send_important_emails');
Flight::queue()->addJob(json_encode([ 'something' => 'that', 'ends' => 'up', 'a' => 'string' ]));

运行工作者

以下是如何运行工作者的示例文件。

<?php

require 'vendor/autoload.php';

$Job_Queue = new n0nag0n\Job_Queue('mysql');
// PDO 连接
$PDO = new PDO('mysql:dbname=testdb;host=127.0.0.1', 'user', 'pass');
$Job_Queue->addQueueConnection($PDO);

// 或者如果您正在使用 beanstalkd/Pheanstalk
$pheanstalk = Pheanstalk\Pheanstalk::create('127.0.0.1');
$Job_Queue->addQueueConnection($pheanstalk);

$Job_Queue->watchPipeline('send_important_emails');
while(true) {
    $job = $Job_Queue->getNextJobAndReserve();

    // 调整为任何让您晚上睡得更好的方法(仅适用于数据库队列,如果没有必要,这个 if 语句不会影响 beanstalkd)
    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

# 在 macOS 上使用 Homebrew
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 管理工作者

创建或修改配置后:

# 重新加载 supervisord 配置
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,您可以将完整的 Flight 应用程序与 WordPress 安装一起运行——非常适合构建自定义 API、微服务,甚至是功能齐全的应用程序,而无需离开 WordPress 的舒适环境。


它能做什么?

安装

  1. flight-integration 文件夹上传到您的 /wp-content/plugins/ 目录。
  2. 在 WordPress 管理后台(插件菜单)激活插件。
  3. 转到 设置 > 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']);

常见问题

问:我需要了解 Flight 才能使用这个插件吗?
答:是的,这适用于希望在 WordPress 中使用 Flight 的开发人员。建议掌握 Flight 的路由和请求处理的基本知识。

问:这会让我的 WordPress 站点变慢吗?
答:不会!插件只处理匹配 Flight 路由的请求。其他请求会像往常一样转到 WordPress。

问:我可以在我的 Flight 应用程序中使用 WordPress 函数吗?
答:当然!您可以从 Flight 路由和控制器中完全访问所有 WordPress 函数、钩子和全局变量。

问:如何创建自定义路由?
答:将您的路由定义在应用程序文件夹中的 config/routes.php 文件中。查看文件夹结构生成器创建的示例文件以获取示例。

更新日志

1.0.0
初始发布。


有关更多信息,请查看 GitHub repo

Awesome-plugins/ghost_session

Ghostff/Session

PHP 会话管理器(非阻塞、闪存、分段、会话加密)。使用 PHP open_ssl 进行可选的会话数据加密/解密。支持 File、MySQL、Redis 和 Memcached。

点击here查看代码。

安装

使用 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);

    // 每次写入会话时,您必须 deliberate 提交它。
    $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, // 仅在需要时或难以提交()您的会话时才这样做。
                                                // 另外,您可以执行 Flight::after('start', function() { Flight::session()->commit(); });
        Session::CONFIG_MYSQL_DS         => [
            'driver'    => 'mysql',             # 数据库驱动程序,用于 PDO dns,例如(mysql:host=...;dbname=...)
            'host'      => '127.0.0.1',         # 数据库主机
            'db_name'   => 'my_app_database',   # 数据库名称
            'db_table'  => 'sessions',          # 数据库表
            'db_user'   => 'root',              # 数据库用户名
            'db_pass'   => '',                  # 数据库密码
            'persistent_conn'=> false,          # 避免每次脚本需要与数据库通信时建立新连接,从而使 Web 应用程序更快。自己找到缺点
        ]
    ] 
]);

帮助!我的会话数据没有持久化!

您设置了会话数据,但它在请求之间没有持久化?您可能忘记了提交会话数据。您可以通过在设置会话数据后调用 $session->commit() 来实现。

Flight::route('POST /login', function() {
    $session = Flight::session();

    // 在这里执行您的登录逻辑
    // 验证密码等。

    // 如果登录成功
    $session->set('is_logged_in', true);
    $session->set('user', $user);

    // 每次写入会话时,您必须 deliberate 提交它。
    $session->commit();
});

另一种方法是,当您设置会话服务时,在您的配置中将 auto_commit 设置为 true。这将在每个请求后自动提交您的会话数据。

$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 是 Flight 框架的一个小型包,它允许您在异步服务器和运行时环境中运行 Flight 应用,例如 Swoole、AdapterMan、ReactPHP、Amp、RoadRunner、Workerman 等。开箱即用,它包含 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_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();
    }
}

运行服务器

提示:对于生产环境,请在 Swoole 前使用反向代理(Nginx)来处理 TLS、静态文件和负载均衡。

配置说明

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"

请访问 byjg/migration-cli 以获取有关迁移 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 脚本

脚本分为三组脚本:

脚本目录如下:

 <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 文件(来自开发人员 1)和您的 43-dev.sql 文件。 如果他试图进行 UP 或 DOWN 迁移,迁移脚本将出现问题并警告他存在两个版本 43。在这种情况下,开发人员 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 实例
$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 实例
$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 uppartial 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 和薪水是否已给出
        IF NEW.empname IS NULL THEN
            RAISE EXCEPTION 'empname 不能为空'; -- 这些注释是否为空并不重要
        END IF; --
        IF NEW.salary IS NULL THEN
            RAISE EXCEPTION '% 不能有空薪水', NEW.empname; --
        END IF; --

        -- 谁为我们工作时必须为此支付?
        IF NEW.salary < 0 THEN
            RAISE EXCEPTION '% 不能有负薪水', 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 和薪水是否已给出
        IF NEW.empname IS NULL THEN
            RAISE EXCEPTION 'empname 不能为空';
        END IF;
        IF NEW.salary IS NULL THEN
            RAISE EXCEPTION '% 不能有空薪水', NEW.empname;
        END IF;

        -- 谁为我们工作时必须为此支付?
        IF NEW.salary < 0 THEN
            RAISE EXCEPTION '% 不能有负薪水', 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 读取迁移文件时,必须在分号处分割整个 SQL 文件的内容,并逐个运行语句。然而,有一种语句可以在其主体中包含多个分号:函数。

为了正确解析函数,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 语法的编辑器,提供自动完成、检查当前数据库架构和/或自动格式化代码,这将简单得多。

处理同一架构中的不同迁移

如果您需要在同一架构内创建不同的迁移脚本和版本,这是可能的,但风险非常大,我 推荐这样做。

要做到这一点,您需要通过将参数传递给构造函数来创建不同的“迁移表”。

<?php
$migration = new \ByJG\DbMigration\Migration("db:/uri", "/path", true, "NEW_MIGRATION_TABLE_NAME");

出于安全原因,此功能在命令行不可用,但您可以使用环境变量 MIGRATION_VERSION 来存储名称。

我们强烈建议不要使用此功能。推荐是一个架构一个迁移。

运行单元测试

基本单元测试可以通过以下方式运行:

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    # 如果想有一个空密码,请使用 '.'
export PSQL_TEST_HOST=localhost      # 默认为 localhost
export PSQL_PASSWORD=newpassword     # 如果想有一个空密码,请使用 '.'
export MSSQL_TEST_HOST=localhost     # 默认为 localhost
export MSSQL_PASSWORD=Pa55word
export SQLITE_TEST_HOST=/tmp/test.db      # 默认为 /tmp/test.db

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_id' => $session->get('user_id')]);
    }
});

Flight::route('/logout', function() {
    $session = Flight::session();
    $session->clear(); // 清除所有会话数据
    Flight::json(['message' => '成功登出']);
});

Flight::start();

关键点

配置

在注册时,通过传递一个选项数组来自定义会话处理程序:

// 是的,这是一个双数组 :)
$app->register('session', Session::class, [ [
    'save_path' => '/custom/path/to/sessions',         // 会话文件的目录
    'prefix' => 'myapp_',                              // 会话文件的前缀
    'encryption_key' => 'a-secure-32-byte-key-here',   // 启用加密(推荐使用 32 字节用于 AES-256-CBC)
    'auto_commit' => false,                            // 禁用自动提交以手动控制
    'start_session' => true,                           // 自动启动会话(默认: true)
    'test_mode' => false,                              // 启用测试模式用于开发
    'serialization' => 'json',                         // 序列化方法: 'json'(默认)或 'php'(旧版)
] ]);

配置选项

选项 描述 默认值
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:

Flight::route('/post-login', function() {
    $session = Flight::session();
    $session->regenerate(); // 新 ID,保留数据
    // 或
    $session->regenerate(true); // 新 ID,删除旧数据
});

中间件示例

使用基于会话的身份验证保护路由:

Flight::route('/admin', function() {
    Flight::json(['message' => '欢迎来到管理面板']);
})->addMiddleware(function() {
    $session = Flight::session();
    if (!$session->get('is_admin')) {
        Flight::halt(403, '访问被拒绝');
    }
});

这是一个在中间件中使用它的简单示例。有关更深入的示例,请参阅 middleware 文档。

方法

Session 类提供以下方法:

get()id() 外的所有方法都返回 Session 实例以支持链式调用。

为什么使用此插件?

技术细节

贡献

欢迎贡献!分叉 仓库,进行更改,然后提交拉取请求。通过 Github 问题跟踪器报告错误或建议功能。

许可证

此插件采用 MIT 许可证。有关详细信息,请参阅 Github 仓库

Awesome-plugins/runway

跑道

跑道是一个CLI应用程序,可帮助您管理您的Flight应用程序。它可以生成控制器,显示所有路由等。它基于优秀的 adhocore/php-cli 库。

点击这里 查看代码。

安装

使用 composer 安装。

composer require flightphp/runway

基本配置

第一次运行跑道时,它将引导您完成设置过程并在项目根目录中创建一个 .runway.json 配置文件。此文件将包含一些Runway正常工作所需的配置。

用法

跑道有许多命令可用于管理您的Flight应用程序。有两种简单的方法可以使用跑道。

  1. 如果您使用的是骨架项目,可以从项目的根目录运行 php runway [command]
  2. 如果您是通过composer安装的包来使用跑道,您可以从项目的根目录运行 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_at,则类似以下内容的文件将在 app/records/UserRecord.php 文件中创建:

<?php

declare(strict_types=1);

namespace app\records;

/**
 * users 表的ActiveRecord类。
 * @link https://docs.flightphp.com/awesome-plugins/active-record
 * 
 * @property int $id
 * @property string $name
 * @property string $email
 * @property string $created_at
 * @property string $updated_at
 * // 一旦在 $relations 数组中定义了关系,您也可以在此处添加关系
 * @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 面板扩展

这是一个扩展集,用于使与 Flight 的协作更加丰富。

这是面板

Flight Bar

每个面板都会显示有关您的应用程序的非常有用的信息!

Flight Data Flight Database Flight Request

点击这里查看代码。

安装

运行 composer require flightphp/tracy-extensions --dev,您就上路了!

配置

要开始使用此扩展,几乎不需要进行任何配置。您需要在使用此扩展之前初始化 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();

附加配置

会话数据

如果您有自定义会话处理程序(例如 ghostff/session),您可以将会话数据的任何数组传递给 Tracy,它将自动为您输出。您可以通过 TracyExtensionLoader 构造函数的第二个参数中的 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 调试栏时添加扩展
    if(Debugger::$showBar === true) {
        // 这就是您将 Latte 面板添加到 Tracy 的地方
        $latte->addExtension(new Latte\Bridges\Tracy\TracyExtension);
    }

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

Awesome-plugins/apm

FlightPHP APM 文档

欢迎来到 FlightPHP APM——您应用的个人性能教练!本指南是您设置、使用和掌握使用 FlightPHP 的应用性能监控 (APM) 的路线图。无论您是在追查慢请求,还是只是想对延迟图表着迷,我们都会为您提供支持。让我们让您的应用更快,让您的用户更快乐,让您的调试会话变得轻松!

查看 Flight Docs 站点的仪表板演示

FlightPHP APM

为什么 APM 重要

想象一下:您的应用就像一家繁忙的餐厅。没有一种方法来跟踪订单需要多长时间或厨房在哪里卡住,您只能猜测为什么客户离开时心情不好。APM 是您的副厨师——它监视每一个步骤,从传入请求到数据库查询,并标记任何减慢您速度的东西。慢页面会丢失用户(研究表明,如果网站加载超过 3 秒,53% 的用户会跳出!),APM 帮助您在问题刺痛之前抓住它们。它是主动的安心——更少的“为什么这个坏了?”时刻,更多的“看这个运行得多顺畅!”胜利。

安装

使用 Composer 开始:

composer require flightphp/apm

您需要:

支持的数据库

FlightPHP APM 当前支持以下数据库来存储指标:

您可以在配置步骤(见下文)中选择您的数据库类型。请确保您的 PHP 环境安装了必要的扩展(例如,pdo_sqlitepdo_mysql)。

快速开始

以下是通往 APM 卓越的逐步指南:

1. 注册 APM

将此代码放入您的 index.phpservices.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); // <-- 需要 true 以在 APM 中启用跟踪。
$Apm->addPdoConnection($pdo);

这里发生了什么?

专业提示:采样 如果您的应用很繁忙,记录每个请求可能会过载。用采样率(0.0 到 1.0):

$Apm = new Apm($ApmLogger, 0.1); // 记录 10% 的请求

这保持性能敏捷,同时仍提供可靠的数据。

2. 配置它

运行此命令来创建您的 .runway-config.json

php vendor/bin/runway apm:init

这是做什么?

此过程还会询问您是否要为此设置运行迁移。如果您是第一次设置,答案是是的。

为什么有两个位置? 原始指标堆积很快(想想未过滤的日志)。工作进程将它们处理成仪表板用的结构化目标。保持一切整洁!

3. 使用 Worker 处理指标

Worker 将原始指标转换为仪表板就绪数据。运行一次:

php vendor/bin/runway apm:worker

它在做什么?

保持它运行 对于实时应用,您需要连续处理。以下是您的选项:

为什么费心? 没有 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
]));

它在哪里显示? 在仪表板的请求详细信息下“Custom Events”——带有漂亮的 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); // <-- 需要 true 以在 APM 中启用跟踪。
$Apm->addPdoConnection($pdo);

您得到什么

注意

示例输出

Worker 选项

根据您的喜好调整 Worker:

示例

php vendor/bin/runway apm:worker --daemon --batch_size 100 --timeout 3600

运行一小时,一次处理 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 团队为 Flight 项目创建了一些特定的面板,使用了 flightphp/tracy-extensions 插件。

安装

使用 composer 进行安装。实际上,您希望在没有开发版本的情况下安装此项,因为 Tracy 自带一个生产错误处理组件。

composer require tracy/tracy

基本配置

有一些基本的配置选项可供开始使用。您可以在 Tracy 文档 中了解更多信息。


require 'vendor/autoload.php';

use Tracy\Debugger;

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

查找一条记录并分配给当前对象。如果你传递一个类型的 $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),你可以通过两种方式在插入之前设置主键值。

$user = new User($pdo_connection, [ 'primaryKey' => 'uuid' ]);
$user->uuid = 'some-uuid';
$user->name = 'demo';
$user->password = md5('demo');
$user->insert(); // 或 $user->save();

或者你可以通过事件自动为你生成主键。

class User extends flight\ActiveRecord {
    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users', [ 'primaryKey' => 'uuid' ]);
        // 你也可以用这种方式来设置 primaryKey,而不是上面的数组。
        $this->primaryKey = 'uuid';
    }

    protected function beforeInsert(self $self) {
        $self->uuid = uniqid(); // 或者你需要用来生成唯一 ID 的任何方式
    }
}

如果在插入之前没有设置主键,它将被设置为 rowid,数据库会为你生成它,但它不会持久化,因为该字段可能在你的表中不存在。这就是为什么推荐使用事件来自动处理它。

update(): boolean|ActiveRecord

将当前记录更新到数据库。

$user->greaterThan('id', 0)->orderBy('id desc')->find();
$user->email = 'test@example.com';
$user->update();

save(): boolean|ActiveRecord

将当前记录插入或更新到数据库。如果记录有 id,它将更新;否则将插入。

$user = new User($pdo_connection);
$user->name = 'demo';
$user->password = md5('demo');
$user->save();

注意: 如果你在类中定义了关系,它将递归地保存那些关系,如果它们已被定义、实例化并且有脏数据以便更新。(v0.4.0 及以上版本)

delete(): boolean

从数据库中删除当前记录。

$user->gt('id', 0)->orderBy('id desc')->find();
$user->delete();

你还可以在执行搜索之前删除多条记录。

$user->like('name', 'Bob%')->delete();

dirty(array $dirty = []): ActiveRecord

脏数据是指记录中已更改的数据。

$user->greaterThan('id', 0)->orderBy('id desc')->find();

// 到目前为止,没有任何数据是“脏”的。

$user->email = 'test@example.com'; // 现在 email 被认为是“脏的”,因为它已更改。
$user->update();
// 现在没有脏数据,因为它已经在数据库中更新和持久化

$user->password = password_hash('newpassword'); // 现在这也是脏的
$user->dirty(); // 不传任何内容将清除所有脏条目。
$user->update(); // 不会更新任何东西,因为没有被捕获为脏数据。

$user->dirty([ 'name' => 'something', 'password' => password_hash('a different password') ]);
$user->update(); // 姓名和密码都已更新。

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”,你会发现很多关于这个主题的文章。使用这个库时,处理此问题的正确方法是, вместо этого вы должны сделать что-то более подобное $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)

限制返回的记录数量。如果给定第二个整数,它将像 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 $valuefield 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() 方法或通过在字段和值之后填充条件的第三个参数来完成的。

// 方法 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();

// 查找一个联系人
$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 beforeInsert(self $self) {
        // 设置一些合理的默认值
        if(!$self->updated_date) {
            $self->updated_date = gmdate('Y-m-d');
        }
    } 
}

afterUpdate(ActiveRecord $ActiveRecord)

也许你有用例,在数据更新后更改数据?

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function afterInsert(self $self) {
        // 你随意
        Flight::cache()->set('most_recently_updated_user_id', $self->id);
        // 或者别的……
    } 
}

beforeSave(ActiveRecord $ActiveRecord)/afterSave(ActiveRecord $ActiveRecord)

如果你想在插入或更新时都发生事件,这很有用。我就不多说了,但你肯定能猜到它的用途。

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function beforeSave(self $self) {
        $self->last_updated = gmdate('Y-m-d H:i:s');
    } 
}

beforeDelete(ActiveRecord $ActiveRecord)/afterDelete(ActiveRecord $ActiveRecord)

不确定你想在这里做什么,但在这里没有评判!去做吧!

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function beforeDelete(self $self) {
        echo '他是一名勇敢的士兵... :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 beautifycomposer 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;

    // Where latte specifically stores its cache
    $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 . ' - '}我的应用</title>
        <link rel="stylesheet" href="style.css">
    </head>
    <body>
        <header>
            <nav>
                <!-- 您的导航元素放在这里 -->
            </nav>
        </header>
        <div id="content">
            <!-- 这就是魔力所在 -->
            {block content}{/block}
        </div>
        <div id="footer">
            &copy; 版权所有
        </div>
    </body>
</html>

现在,我们有一个文件将在那个内容块中渲染:

<!-- app/views/home.latte -->
<!-- 这告诉 Latte 该文件“在” layout.latte 文件内部 -->
{extends layout.latte}

<!-- 这是在布局中内容块内渲染的内容 -->
{block content}
    <h1>首页</h1>
    <p>欢迎来到我的应用!</p>
{/block}

然后,当您在函数或控制器中渲染这个文件时,您会这样做:

// 简单路由
Flight::route('/', function () {
    Flight::render('home.latte', [
        'title' => '首页'
    ]);
});

// 或者如果您使用控制器
Flight::route('/', [HomeController::class, 'index']);

// HomeController.php
class HomeController
{
    public function index()
    {
        Flight::render('home.latte', [
            'title' => '首页'
        ]);
    }
}

请参阅 Latte 文档,了解如何充分利用 Latte 的更多信息!

使用 Tracy 进行调试

本节需要 PHP 8.1+。

您还可以使用 Tracy 来帮助调试您的 Latte 模板文件,开箱即用!如果您已经安装了 Tracy,则需要将 Latte 扩展添加到 Tracy 中。

// services.php
use Tracy\Debugger;

$app->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;

    // This will only add the extension if the Tracy Debug Bar is enabled
    if (Debugger::$showBar === true) {
        // this is where you add the Latte Panel to Tracy
        $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) 对任何应用程序都至关重要。它帮助您了解应用程序的性能以及瓶颈所在。有许多 APM 工具可与 Flight 一起使用。

异步

Flight 已经是一个快速的框架,但为其添加涡轮引擎会让一切变得更有趣(且具挑战性)!

授权/权限

授权和权限对任何需要控制访问权限的应用程序都至关重要。

缓存

缓存是加速应用程序的绝佳方式。有许多缓存库可与 Flight 一起使用。

CLI

CLI 应用程序是与您的应用程序交互的绝佳方式。您可以使用它们生成控制器、显示所有路由等。

Cookies

Cookies 是存储客户端小量数据的绝佳方式。它们可用于存储用户偏好、应用程序设置等。

调试

在本地环境中开发时,调试至关重要。有几个插件可以提升您的调试体验。

数据库

数据库是大多数应用程序的核心。这是存储和检索数据的方式。有些数据库库只是用于编写查询的包装器,有些则是完整的 ORM。

加密

加密对任何存储敏感数据的应用程序都至关重要。加密和解密数据并不太难,但正确存储加密密钥 可能 困难。最重要的是,切勿将加密密钥存储在公共目录中或提交到代码仓库。

作业队列

作业队列对于异步处理任务非常有帮助。这可以包括发送电子邮件、处理图像或任何不需要实时完成的任务。

会话

会话对于 API 并不太有用,但对于构建 Web 应用程序,会话对于维护状态和登录信息至关重要。

模板

模板是任何具有 UI 的 Web 应用程序的核心。有许多模板引擎可与 Flight 一起使用。

WordPress 集成

想在您的 WordPress 项目中使用 Flight 吗?有一个方便的插件可供使用!

贡献

有一个您想分享的插件吗?提交拉取请求将其添加到列表中!

Media

媒体

我们已经尽力追踪互联网上与 Flight 相关的各种媒体类型。请参阅以下不同资源,您可以使用这些资源来了解更多关于 Flight 的信息。

文章和评述

视频和教程

缺少什么吗?

我们是否遗漏了您撰写或录制的任何内容?请通过 issue 或 pull request 告知我们!

Examples

需要快速开始吗?

您有两种选项来开始一个新的 Flight 项目:

社区贡献的示例:

需要一些灵感吗?

虽然这些并非由 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 扩展Runway

配置您的 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。如果没有,请在 Google 上搜索如何在您的系统上安装 Apache。

对于 Apache,请使用以下内容编辑您的 .htaccess 文件:

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L]

注意:如果您需要在子目录中使用 flight,请在 RewriteEngine On 之后添加一行 RewriteBase /subdir/

注意:如果您想保护所有服务器文件,如数据库或环境文件。 将此内容放入您的 .htaccess 文件中:

RewriteEngine On
RewriteRule ^(.*)$ index.php

Nginx

确保您的系统上已安装 Nginx。如果没有,请在 Google 上搜索如何在您的系统上安装 Nginx。

对于 Nginx,请将以下内容添加到您的服务器声明中:

server {
  location / {
    try_files $uri $uri/ /index.php;
  }
}

创建您的 index.php 文件

如果您进行基本安装,您需要一些代码来开始。

<?php

// 如果您使用 Composer,请要求加载自动加载器。
require 'vendor/autoload.php';
// 如果您不使用 Composer,请直接加载框架
// require 'flight/Flight.php';

// 然后定义一个路由并分配一个函数来处理请求。
Flight::route('/', function () {
  echo 'hello world!';
});

// 最后,启动框架。
Flight::start();

使用 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)作为非线程安全 zip 文件。
  2. 解压 PHP

    • 将下载的 zip 文件解压到 C:\php
  3. 将 PHP 添加到系统 PATH

    • 转到 系统属性 > 环境变量
    • 系统变量 下,找到 Path 并点击 编辑
    • 添加路径 C:\php(或您解压 PHP 的位置)。
    • 点击 确定 关闭所有窗口。
  4. 配置 PHP

    • php.ini-development 复制到 php.ini
    • 编辑 php.ini 以按需配置 PHP(例如,设置 extension_dir,启用扩展)。
  5. 验证 PHP 安装

    • 打开命令提示符并运行:
      php -v

安装多个 PHP 版本

  1. 为每个版本重复上述步骤,将每个版本放置在单独的目录中(例如,C:\php7C:\php8)。

  2. 通过调整系统 PATH 变量指向所需版本目录在版本之间切换

Ubuntu (20.04, 22.04 等)

使用 apt 安装 PHP

  1. 更新软件包列表

    • 打开终端并运行:
      sudo apt update
  2. 安装 PHP

    • 安装最新 PHP 版本:
      sudo apt install php
    • 要安装特定版本,例如 PHP 8.1:
      sudo apt install php8.1
  3. 安装附加模块(可选):

    • 例如,要安装 MySQL 支持:
      sudo apt install php8.1-mysql
  4. 在 PHP 版本之间切换

    • 使用 update-alternatives
      sudo update-alternatives --set php /usr/bin/php8.1
  5. 验证安装的版本

    • 运行:
      php -v

Rocky Linux

使用 yum/dnf 安装 PHP

  1. 启用 EPEL 仓库

    • 打开终端并运行:
      sudo dnf install epel-release
  2. 安装 Remi's 仓库

    • 运行:
      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。

官方指南

构建一个博客

了解如何使用 Flight PHP 创建一个功能齐全的博客应用程序。此指南将带您完成:

此教程非常适合初学者,他们希望看到所有组件如何在真实应用程序中组合在一起。

单元测试和 SOLID 原则

此指南涵盖了 Flight PHP 应用程序中单元测试的基础知识。它包括:

非官方指南

虽然这些指南不由 Flight 团队官方维护,但它们是社区创建的宝贵资源。它们涵盖了各种主题和用例,提供有关使用 Flight PHP 的额外见解。

使用 Flight 框架创建 RESTful API

此指南将带您创建使用 Flight PHP 框架的 RESTful API。它涵盖了设置 API 的基础知识、定义路由以及返回 JSON 响应。

构建一个简单博客

此指南将带您使用 Flight PHP 框架创建基本博客。它实际上分为两个部分:一个涵盖基础知识,另一个涵盖更高级主题和生产就绪博客的优化。

在 PHP 中构建 Pokémon API:初学者指南

此有趣的指南将带您使用 Flight PHP 创建一个简单 Pokémon API。它涵盖了设置 API 的基础知识、定义路由以及返回 JSON 响应。

贡献

有指南idea?发现错误?我们欢迎贡献!我们的指南在 FlightPHP 文档仓库 中维护。

如果您使用 Flight 构建了一些有趣的东西并希望作为指南分享,请提交拉取请求。分享您的知识有助于 Flight 社区成长。

寻找 API 文档?

如果您正在寻找有关 Flight 核心功能和方法的特定信息,请查看我们文档的学习部分。