Learn/flight_vs_laravel
Flight vs Laravel
什么是 Laravel?
Laravel 是一个功能齐全的框架,具有所有花里胡哨的功能和一个令人惊叹的开发者导向生态系统, 但在性能和复杂性方面付出了代价。Laravel 的目标是让开发者拥有最高水平的生产力,并使常见任务变得容易。Laravel 是那些希望构建功能齐全的企业级 Web 应用程序的开发者的绝佳选择。这会带来一些权衡,特别是性能和复杂性方面的权衡。学习 Laravel 的基础可能很容易,但熟练掌握该框架需要一些时间。
Laravel 还有如此多的模块,以至于开发者常常觉得解决问题的唯一方法是通过这些模块,而实际上你只需使用另一个库或编写自己的代码即可。
与 Flight 相比的优点
- Laravel 拥有一个庞大的生态系统,包括开发者和模块,可用于解决常见问题。
- Laravel 具有功能齐全的 ORM,可用于与数据库交互。
- Laravel 有海量的文档和教程,可用于学习框架。这对于深入探讨细节可能很好,但也可能不好,因为内容太多需要梳理。
- Laravel 具有内置的认证系统,可用于保护您的应用程序。
- Laravel 有播客、会议、聚会、视频和其他资源,可用于学习框架。
- Laravel 针对经验丰富的开发者,他们希望构建功能齐全的企业级 Web 应用程序。
与 Flight 相比的缺点
- Laravel 在底层比 Flight 复杂得多。这在性能方面带来了剧烈的代价。请参阅 TechEmpower 基准测试 以获取更多信息。
- Flight 针对那些希望构建轻量级、快速且易于使用的 Web 应用程序的开发者。
- Flight 注重简单性和易用性。
- Flight 的核心特性之一是它尽力保持向后兼容性。Laravel 在主要版本之间会引起大量挫败感。
- Flight 适合那些首次涉足框架领域的开发者。
- Flight 没有依赖项,而 Laravel 有可怕数量的依赖项。
- Flight 也可以处理企业级应用程序,但它不像 Laravel 那样有大量样板代码。 它还需要开发者在组织和结构化方面保持更多纪律。
- Flight 赋予开发者对应用程序的更多控制权,而 Laravel 在幕后有大量魔法,这可能会令人沮丧。
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 配置选项
以下是所有可用配置设置的列表:
- flight.base_url
?string
- 如果 Flight 在子目录中运行,则覆盖请求的基 URL。(默认:null) - flight.case_sensitive
bool
- URL 的区分大小写匹配。(默认:false) - flight.handle_errors
bool
- 允许 Flight 内部处理所有错误。(默认:true) - flight.log_errors
bool
- 将错误记录到 Web 服务器的错误日志文件。(默认:false)- 如果您安装了 Tracy,Tracy 将根据 Tracy 配置记录错误,而不是此配置。
- flight.views.path
string
- 包含视图模板文件的目录。(默认:./views) - flight.views.extension
string
- 视图模板文件扩展名。(默认:.php) - flight.content_length
bool
- 设置Content-Length
标头。(默认:true)- 如果您使用 Tracy,则需要将此值设置为 false,以便 Tracy 可以正确渲染。
- flight.v2.output_buffering
bool
- 使用旧版输出缓冲。请参阅 migrating to v3。(默认:false)
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
});
另请参阅
- Extending Flight - 如何扩展和自定义 Flight 的核心功能。
- Unit Testing - 如何为您的 Flight 应用程序编写单元测试。
- Tracy - 用于高级错误处理和调试的插件。
- Tracy Extensions - 用于将 Tracy 与 Flight 集成的扩展。
- APM - 用于应用程序性能监控和错误跟踪的插件。
故障排除
- 如果您在找出配置的所有值时遇到问题,您可以执行
var_dump(Flight::get());
更新日志
- v3.5.0 - 添加了
flight.v2.output_buffering
配置以支持旧版输出缓冲行为。 - v2.0 - 添加了核心配置。
Learn/ai
使用 Flight 的 AI 与开发者体验
概述
Flight 让您轻松为 PHP 项目注入 AI 驱动的工具和现代开发者工作流程。通过内置命令连接 LLM(大型语言模型)提供商并生成项目特定的 AI 编码指令,Flight 帮助您和您的团队充分利用 GitHub Copilot、Cursor 和 Windsurf 等 AI 助手。
理解
AI 编码助手在理解您项目的上下文、约定和目标时最为有用。Flight 的 AI 助手让您能够:
- 将您的项目连接到流行的 LLM 提供商(OpenAI、Grok、Claude 等)
- 生成和更新项目特定的 AI 工具指令,确保每个人获得一致、相关的帮助
- 保持团队一致性和生产力,减少解释上下文的时间
这些功能内置于 Flight 核心 CLI 和官方 flightphp/skeleton 启动项目中。
基本用法
设置 LLM 凭据
ai:init
命令将引导您将项目连接到 LLM 提供商。
php runway ai:init
您将被提示:
- 选择您的提供商(OpenAI、Grok、Claude 等)
- 输入您的 API 密钥
- 设置基础 URL 和模型名称
这将在您的项目根目录创建一个 .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 提供商生成指令,然后将它们写入:
.github/copilot-instructions.md
(用于 GitHub Copilot).cursor/rules/project-overview.mdc
(用于 Cursor).windsurfrules
(用于 Windsurf)
示例:
请描述您的项目用途?我的awesome API
您计划使用什么数据库?MySQL
您计划使用什么 HTML 模板引擎(如果有)?latte
安全是否是此项目的重要元素?(y/n) y
...
AI 指令更新成功。
现在,您的 AI 工具将基于项目实际需求提供更智能、更相关的建议。
高级用法
- 您可以使用命令选项自定义凭据或指令文件的位置(查看每个命令的
--help
)。 - AI 助手设计用于与支持 OpenAI 兼容 API 的任何 LLM 提供商配合工作。
- 如果您想随着项目演进而更新指令,只需重新运行
ai:generate-instructions
并再次回答提示。
另请参阅
- Flight Skeleton – 带有 AI 集成的官方启动项目
- Runway CLI – 更多关于驱动这些命令的 CLI 工具的信息
故障排除
- 如果看到“Missing .runway-creds.json”,请先运行
php runway ai:init
。 - 确保您的 API 密钥有效并有权访问选定的模型。
- 如果指令未更新,请检查项目目录中的文件权限。
更新日志
- v3.16.0 – 添加了
ai:init
和ai:generate-instructions
CLI 命令,用于 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.development 和 php.ini.production 文件中设置了不错的默认值,但我们可以做得更好。首先,它们没有为我们设置日期/时区。从分发角度来看这是合理的,但没有时区,PHP 在调用任何日期/时间相关函数时会抛出 E_WARNING 错误。下面是一些推荐设置:
- date.timezone - 从支持的时区列表中选择
- session.savepath - 如果我们使用文件进行会话而不是其他保存处理程序,将其设置为 /tmp 外的某个位置。在共享托管环境中,将其保留为 /tmp 可能有风险,因为 /tmp_ 通常权限宽松。即使设置了 sticky-bit,具有访问权限的人也可以列出该目录的内容,从而获知所有活跃的会话 ID。
- session.cookie_secure - 如果通过 HTTPS 服务 PHP 代码,请毫不犹豫地启用此选项。
- session.cookie_httponly - 设置此选项以防止 PHP 会话 cookie 通过 JavaScript 访问
- 更多... 使用工具如 iniscan 测试配置中的常见漏洞
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.2 以上。
- 主要是假对象 - 单元测试中唯一的“真实”对象应该是我们正在测试的对象和简单值对象。其余应该是某种形式的测试替身
有理由违反其中一些,但作为一般指南,它们会为您服务。
5.5 测试痛苦时
单元测试会让您提前感受到糟糕设计带来的痛苦 - Michael Feathers
当您编写单元测试时,您是在强迫自己实际使用类来完成事情。如果您在最后编写测试,或者更糟的是,只是将代码扔给 QA 或其他人编写测试,您不会得到关于类实际行为的任何反馈。如果我们在编写时编写测试,并且类使用起来很痛苦,我们会在编写时发现,这几乎是最便宜的修复时间。
如果一个类难以测试,那就是设计缺陷。不同的缺陷以不同的方式表现出来。如果您必须进行大量模拟,您的类可能有太多依赖项,或者方法做了太多事情。每个测试必须做的设置越多,方法做的就越多。如果您必须编写非常复杂的测试场景来行使行为,类的方法可能做了太多事情。如果您必须挖掘一堆私有方法和状态来测试东西,也许还有另一个类试图出来。单元测试非常擅长暴露“冰山类”,其中 80% 的工作隐藏在受保护或私有的代码中。我曾经是让尽可能多东西受保护的忠实粉丝,但现在我意识到我只是让我的单个类负责太多,真正的解决方案是将类分解成更小的部分。
由 Brian Fenton 撰写 - Brian Fenton 在中西部和湾区担任 PHP 开发人员已有 8 年,目前在 Thismoment。他专注于代码工艺和设计原则。博客在 www.brianfenton.us,Twitter 在 @brianfenton。当他不忙着当爸爸时,他喜欢食物、啤酒、游戏和学习。
Learn/security
安全
概述
安全对于 Web 应用程序来说是一个重要问题。您需要确保您的应用程序是安全的,并且您的用户数据是安全的。Flight 提供了一系列功能来帮助您保护 Web 应用程序的安全。
理解
在构建 Web 应用程序时,您应该了解一些常见的威胁。其中最常见的威胁包括:
- 跨站请求伪造 (CSRF)
- 跨站脚本攻击 (XSS)
- SQL 注入
- 跨源资源共享 (CORS)
Templates 通过默认转义输出来帮助防范 XSS,这样您就不必记住要这样做。Sessions 可以通过在用户会话中存储 CSRF 令牌来帮助防范 CSRF,如下面所述。使用 PDO 的预准备语句可以帮助防止 SQL 注入攻击(或者使用 PdoWrapper 类中的便捷方法)。CORS 可以通过在调用 Flight::start()
之前使用简单的钩子来处理。
所有这些方法共同协作以帮助保持您的 Web 应用程序安全。您应该始终将学习和理解安全最佳实践放在首位。
基本用法
标头
HTTP 标头是保护 Web 应用程序的最简单方法之一。您可以使用标头来防止点击劫持、XSS 和其他攻击。您可以通过几种方式将这些标头添加到您的应用程序中。
检查标头安全的两个优秀网站是 securityheaders.com 和 observatory.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.php
或 index.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: <script>alert("XSS")</script>
// 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_hash 和 password_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
});
另请参阅
- Sessions - 如何安全管理用户会话。
- Templates - 使用模板自动转义输出并防止 XSS。
- PDO Wrapper - 使用预准备语句简化数据库交互。
- Middleware - 如何使用中间件简化添加安全标头的过程。
- Responses - 如何使用安全标头自定义 HTTP 响应。
- Requests - 如何处理和净化用户输入。
- filter_var - 用于输入净化的 PHP 函数。
- password_hash - 用于安全密码哈希的 PHP 函数。
- password_verify - 用于验证哈希密码的 PHP 函数。
故障排除
- 请参阅上面的“另请参阅”部分,获取与 Flight Framework 组件相关问题的故障排除信息。
更新日志
- v3.1.0 - 添加了关于 CORS、错误处理、输入净化、密码哈希和速率限制的部分。
- v2.0 - 添加了默认视图的转义以防止 XSS。
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 为 HEAD
和 OPTIONS
HTTP 请求提供内置处理:
HEAD 请求
- HEAD 请求 被视为与
GET
请求相同,但 Flight 在发送到客户端之前自动移除响应主体。 - 这意味着您可以为
GET
定义一个路由,HEAD 请求到同一 URL 将仅返回标头(无内容),符合 HTTP 标准。
Flight::route('GET /info', function() {
echo 'This is some info!';
});
// HEAD 请求到 /info 将返回相同的标头,但无主体。
OPTIONS 请求
OPTIONS
请求由 Flight 为任何定义的路由自动处理。
- 当收到 OPTIONS 请求时,Flight 以
204 No Content
状态响应,并包含Allow
标头,列出该路由支持的所有 HTTP 方法。 - 您无需为 OPTIONS 定义单独的路由。
// 对于定义为:
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');
});
检查路由信息
如果您想检查匹配的路由信息,有两种方式可以做到:
- 您可以使用
Flight::router()
对象上的executedRoute
属性。 - 您可以通过在路由方法中将第三个参数传递为
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/
将导致 aliasBase
为 users
。当这些路由创建时,
别名是 users.index
、users.create
等。如果您想更改别名,请将 aliasBase
设置为您想要的值。
Flight::resource('/users', UsersController::class, [ 'aliasBase' => 'user' ]);
Only 和 Except
您也可以使用 only
和 except
选项指定要创建哪些路由。
// 只允许这些方法并阻止其余
Flight::resource('/users', UsersController::class, [ 'only' => [ 'index', 'show' ] ]);
// 只阻止这些方法并允许其余
Flight::resource('/users', UsersController::class, [ 'except' => [ 'create', 'store', 'edit', 'update', 'destroy' ] ]);
这些基本上是白名单和黑名单选项,因此您可以指定要创建哪些路由。
中间件
您也可以指定要在 resource
方法创建的每个路由上运行的中间件。
Flight::resource('/users', UsersController::class, [ 'middleware' => [ MyAuthMiddleware::class ] ]);
流式响应
您现在可以使用 stream()
或 streamWithHeaders()
向客户端流式传输响应。
这对于发送大文件、长时间运行的进程或生成大响应很有用。
流式传输路由的处理方式与常规路由略有不同。
注意: 流式响应仅在您将
flight.v2.output_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
]);
另请参阅
- 中间件 - 使用中间件与路由进行身份验证、日志记录等。
- 依赖注入 - 简化路由中的对象创建和管理。
- 为什么使用框架? - 理解使用像 Flight 这样的框架的好处。
- 扩展 - 如何使用自己的功能扩展 Flight,包括
notFound
方法。 - php.net: preg_match - PHP 用于正则表达式匹配的函数。
故障排除
- 路由参数按顺序匹配,而不是按名称。确保回调参数顺序与路由定义匹配。
- 使用
Flight::get()
不会定义路由;对于路由,请使用Flight::route('GET /...')
或组中的 Router 对象上下文(例如$router->get(...)
)。 - executedRoute 属性仅在路由执行后设置;在执行前为 NULL。
- 流式传输需要禁用遗留 Flight 输出缓冲功能(
flight.v2.output_buffering = false
)。 - 对于依赖注入,只有某些路由定义支持基于容器的实例化。
404 未找到或意外路由行为
如果您看到 404 未找到错误(但您发誓它确实存在,并且不是拼写错误),这实际上可能是因为您在路由端点中返回了一个值而不是简单地回显它。这是有意的,但可能会让一些开发者措手不及。
Flight::route('/hello', function(){
// 这可能会导致 404 未找到错误
return 'Hello World';
});
// 您可能想要的
Flight::route('/hello', function(){
echo 'Hello World';
});
这样做的原因是路由器中内置了一个特殊机制,将返回输出处理为“转到下一个路由”的信号。 您可以在 路由 部分查看文档化的行为。
更新日志
- v3:添加了资源路由、路由别名和流式传输支持、路由组和中间件支持。
- v1:绝大多数基本功能可用。
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 中,这意味着测试您的路由、控制器和逻辑如何响应不同的输入——而不依赖全局状态或真实的外部服务。
关键原则:
- 测试行为,而不是实现: 关注您的代码做什么,而不是如何做。
- 避免全局状态: 使用依赖注入而不是
Flight::set()
或Flight::get()
。 - 模拟外部服务: 用测试替身替换数据库或邮件程序等内容。
- 保持测试快速且专注: 单元测试不应访问真实数据库或 API。
基本用法
设置 PHPUnit
- 使用 Composer 安装 PHPUnit:
composer require --dev phpunit/phpunit
- 在项目根目录中创建
tests
目录。 - 在您的
composer.json
中添加测试脚本:"scripts": { "test": "phpunit --configuration phpunit.xml" }
- 创建
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']);
}
}
提示:
- 使用
$app->request()->data
模拟 POST 数据。 - 在测试中避免使用
Flight::
静态方法——使用$app
实例。
为可测试控制器使用依赖注入
将依赖项(如数据库或邮件程序)注入到您的控制器中,使其在测试中易于模拟:
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);
}
}
高级用法
- 模拟: 使用 PHPUnit 内置的模拟或匿名类来替换依赖项。
- 直接测试控制器: 使用新的
Engine
实例化控制器并模拟依赖项。 - 避免过度模拟: 在可能的情况下让真实逻辑运行;仅模拟外部服务。
另请参阅
- Unit Testing Guide - 单元测试最佳实践的全面指南。
- Dependency Injection Container - 如何使用 DIC 来管理依赖项并提高可测试性。
- Extending - 如何添加自己的助手或覆盖核心类。
- PDO Wrapper - 简化数据库交互,并在测试中更容易模拟。
- Requests - 在 Flight 中处理 HTTP 请求。
- Responses - 向用户发送响应。
- Unit Testing and SOLID Principles - 学习 SOLID 原则如何改善您的单元测试。
故障排除
- 在您的代码和测试中避免使用全局状态(
Flight::set()
、$_SESSION
等)。 - 如果您的测试很慢,您可能在编写集成测试——模拟外部服务以保持单元测试快速。
- 如果测试设置复杂,请考虑重构您的代码以使用依赖注入。
更新日志
- v3.15.0 - 添加了依赖注入和模拟的示例。
Learn/flight_vs_symfony
Flight 与 Symfony
什么是Symfony?
Symfony 是一组可重复使用的 PHP 组件和用于 Web 项目的 PHP 框架。
构建最佳 PHP 应用程序的标准基础。选择任何您自己应用程序所需的 50 个独立组件。
加速创建和维护您的 PHP Web 应用程序。结束重复的编码任务,享受控制代码的力量。
与 Flight 相比的优势
- Symfony 拥有一个庞大的开发者和模块生态系统,可用于解决常见问题。
- Symfony 拥有一个功能齐全的 ORM(Doctrine),可用于与您的数据库交互。
- Symfony 拥有大量文档和教程,可用于学习该框架。
- Symfony 拥有播客、会议、会议、视频和其他资源,可用于学习该框架。
- Symfony 面向有经验的开发人员,他们希望构建功能齐全的企业 Web 应用程序。
与 Flight 相比的缺点
- Symfony 在底层的工作要比 Flight 多得多。从性能的角度来看,这带来了戏剧性的代价。查看TechEmpower benchmarks 获取更多信息。
- Flight 面向那些希望构建轻量、快速和易于使用的 Web 应用程序的开发人员。
- Flight 专注于简单易用。
- Flight 的核心特性之一是尽最大努力保持向后兼容性。
- Flight 没有依赖性,而Symfony 有很多依赖性
- Flight 适用于首次涉足框架领域的开发人员。
- Flight 也可以开发企业级应用程序,但其示例和教程不如Symfony多。这也需要开发人员更多的纪律来保持组织和良好的结构。
- Flight 让开发人员对应用程序有更多的控制,而Symfony 可以在幕后偷偷进行一些魔术操作。
Learn/flight_vs_another_framework
将Flight与另一个框架进行比较
如果您正在从另一个框架(如Laravel、Slim、Fat-Free或Symfony)迁移到Flight,则此页面将帮助您了解两者之间的区别。
Laravel
Laravel是一个功能齐全的框架,拥有所有功能和令人惊叹的开发人员专注生态系统,但需要在性能和复杂性方面付出代价。
Slim
Slim是一个微框架,类似于Flight。它旨在轻量且易于使用,但可能比Flight复杂一些。
Fat-Free
Fat-Free是一个体积更小的全栈框架。虽然它拥有所有工具,但其数据架构可能使一些项目比必要复杂。
Symfony
Symfony是一个模块化的企业级框架,旨在灵活且可扩展。对于较小的项目或新手开发人员,Symfony可能有些令人生畏。
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();
});
另请参阅
- Collections - 了解如何使用 Collection 类进行简单的数据访问。
故障排除
- 如果您收到关于数据库连接的错误,请检查您的 DSN、用户名、密码和选项。
- 所有行都返回为 Collections——如果您需要普通数组,请使用
$collection->getData()
。 - 对于
IN (?)
查询,请确保传递数组或逗号分隔的字符串。
更新日志
- v3.2.0 - PdoWrapper 的初始发布,带有基本查询和获取方法。
Learn/dependency_injection_container
依赖注入容器
概述
依赖注入容器 (DIC) 是一个强大的增强功能,它允许您管理应用程序的依赖关系。
理解
依赖注入 (DI) 是现代 PHP 框架中的一个关键概念,用于管理对象的实例化和配置。一些 DIC 库的示例包括:flightphp/container、Dice、Pimple、 PHP-DI 和 league/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 示例更冗长,但它仍然能完成相同的好处!
另请参阅
- 扩展 Flight - 了解如何通过扩展框架将依赖注入添加到您自己的类中。
- 配置 - 了解如何为您的应用程序配置 Flight。
- 路由 - 了解如何为您的应用程序定义路由,以及依赖注入如何与控制器一起工作。
- 中间件 - 了解如何为您的应用程序创建中间件,以及依赖注入如何与中间件一起工作。
故障排除
- 如果您的容器有问题,请确保您向容器传递正确的类名。
更新日志
- v3.7.0 - 添加了向 Flight 注册 DIC 处理程序的能力。
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 ]);
处理中间件执行
假设您有一个认证中间件,并且想在用户未认证时将他们重定向到登录页面。您有几个选项可用:
- 您可以从中间件函数返回 false,Flight 将自动返回 403 禁止错误,但没有自定义。
- 您可以使用
Flight::redirect()
将用户重定向到登录页面。 - 您可以在中间件中创建自定义错误并停止路由执行。
简单直接
这是一个简单的 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.']);
}
}
}
另请参阅
- Routing - 如何将路由映射到控制器并渲染视图。
- Requests - 理解如何处理传入请求。
- Responses - 如何自定义 HTTP 响应。
- Dependency Injection - 在路由中简化对象创建和管理。
- Why a Framework? - 理解使用像 Flight 这样的框架的好处。
- Middleware Execution Strategy Example
故障排除
- 如果您的中间件中有重定向,但您的应用程序似乎没有重定向,请确保在中间件中添加
exit;
语句。
更新日志
- v3.1: 添加了对中间件的支持。
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;
});
注意: 核心方法如
map
和register
无法被过滤,因为它们被直接调用而非动态调用。请参阅 Extending Flight 以获取更多信息。
另请参阅
故障排除
- 如果您希望链条停止,请确保从过滤函数返回
false
。如果您不返回任何内容,链条将继续。
更新日志
- v2.0 - 初始发布。
Learn/requests
请求
概述
Flight 将 HTTP 请求封装成一个单一的对象,可以通过以下方式访问:
$request = Flight::request();
理解
HTTP 请求是理解 HTTP 生命周期的核心方面之一。用户在网页浏览器或 HTTP 客户端上执行一个操作,他们会向你的项目发送一系列头部、主体、URL 等。你可以捕获这些头部(浏览器的语言、他们能处理的压缩类型、用户代理等),并捕获发送到你的 Flight 应用程序的主体和 URL。这些请求对于你的应用程序了解下一步该做什么至关重要。
基本用法
PHP 有几个超级全局变量,包括 $_GET
、$_POST
、$_REQUEST
、$_SERVER
、$_FILES
和 $_COOKIE
。Flight 将这些抽象成方便的 Collections。你可以将 query
、data
、cookies
和 files
属性访问为数组或对象。
注意: 强烈不鼓励在你的项目中使用这些超级全局变量,应该通过
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']
,它可以被覆盖。
请求对象属性
请求对象提供以下属性:
- body - 原始 HTTP 请求主体
- url - 被请求的 URL
- base - URL 的父子目录
- method - 请求方法 (GET, POST, PUT, DELETE)
- referrer - 引用 URL
- ip - 客户端 IP 地址
- ajax - 请求是否为 AJAX 请求
- scheme - 服务器协议 (http, https)
- user_agent - 浏览器信息
- type - 内容类型
- length - 内容长度
- query - 查询字符串参数
- data - 帖子数据或 JSON 数据
- cookies - Cookie 数据
- files - 上传的文件
- secure - 连接是否安全
- accept - HTTP 接受参数
- proxy_ip - 客户端的代理 IP 地址。按顺序扫描
$_SERVER
数组中的HTTP_CLIENT_IP
、HTTP_X_FORWARDED_FOR
、HTTP_X_FORWARDED
、HTTP_X_CLUSTER_CLIENT_IP
、HTTP_FORWARDED_FOR
、HTTP_FORWARDED
。 - host - 请求主机名
- servername - 来自
$_SERVER
的 SERVER_NAME
辅助方法
有一些辅助方法可以拼凑 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
数组中的第一个类型。
另请参阅
- Routing - 查看如何将路由映射到控制器并渲染视图。
- Responses - 如何自定义 HTTP 响应。
- Why a Framework? - 请求如何融入大局。
- Collections - 处理数据集合。
- Uploaded File Handler - 处理文件上传。
故障排除
- 如果你的 Web 服务器位于代理、负载均衡器等后面,
request()->ip
和request()->proxy_ip
可能不同。
更新日志
- v3.17.2 - 添加了 negotiateContentType()
- v3.12.0 - 添加了通过请求对象处理文件上传的能力。
- v1.0 - 初始发布。
Learn/why_frameworks
为什么使用一个框架?
一些程序员强烈反对使用框架。他们认为框架臃肿、缓慢且难以学习。 他们说框架是不必要的,你可以不用框架编写更好的代码。 关于使用框架的缺点有一些合理的观点。然而,使用框架也有许多优势。
使用框架的原因
下面是您可能考虑使用框架的一些原因:
- 快速开发:框架提供了大量的功能。这意味着您可以更快地构建 Web 应用程序。您不必编写太多代码,因为框架提供了许多您需要的功能。
- 一致性:框架提供一种一致的做事方式。这使您更容易理解代码的工作原理,也使其他开发人员更容易理解您的代码。如果您逐个脚本编写,尤其是在团队开发中,可能会失去脚本之间的一致性.
- 安全性:框架提供安全特性,帮助保护您的 Web 应用程序免受常见的安全威胁。这意味着您不必过于担心安全,因为框架为您处理了很多工作。
- 社区:框架有庞大的开发人员社区。这意味着在遇到问题或疑问时可以从其他开发人员那里获得帮助。这还意味着有很多资源可帮助您学习如何使用框架。
- 最佳实践:框架使用最佳实践构建。这意味着您可以从框架中学习,并在自己的代码中使用相同的最佳实践。这有助于您成为更好的程序员。有时候你不知道你不知道的东西会使你最终后悔。
- 可扩展性:框架被设计为可扩展。这意味着您可以向框架添加自己的功能。这使您能够构建根据您特定需求定制的 Web 应用程序。
Flight 是一个微框架。这意味着它又小又轻量。它的功能不及像 Laravel 或 Symfony 这样的大型框架多。 然而,它确实提供了构建 Web 应用程序所需的许多功能。而且学习和使用它也很容易。 这使其成为迅速轻松构建 Web 应用程序的不错选择。如果您对框架还不熟悉,Flight 是一个很好的开始框架。 它将帮助您了解使用框架的优势,而不会让您在太复杂的内容中迷失方向。 在您有了使用 Flight 的经验后,将更容易转向像 Laravel 或 Symfony 这样更复杂的框架, 但 Flight 仍然可以构建成功的强大应用程序。
什么是路由?
路由是 Flight 框架的核心,但究竟是什么呢?路由是将 URL 与代码中的特定功能匹配的过程。
这是您可以根据被请求的 URL 使您的网站执行不同操作的方法。例如,当用户访问 /user/1234
时,您可能希望显示用户的个人资料,
但当他们访问 /users
时显示所有用户的列表。所有这些都通过路由完成。
可能像这样运作:
- 用户转到您的浏览器并键入
http://example.com/user/1234
。 - 服务器收到请求,检查 URL 并将其传递到您的 Flight 应用程序代码。
- 假设在您的 Flight 代码中有类似
Flight::route('/user/@id', [ 'UserController', 'viewUserProfile' ]);
这样的东西。 您的 Flight 应用程序代码检查 URL 并看到它匹配您定义的路由,然后运行为该路由定义的代码。 - 然后 Flight 路由将运行并调用
UserController
类中的viewUserProfile($id)
方法,将1234
作为$id
参数传入该方法。 - 您的
viewUserProfile()
方法中的代码将运行并执行您告诉它要执行的操作。 您可能会输出一些用户资料页的 HTML,或者如果这是一个 RESTful API,则可能打印出包含用户信息的 JSON 响应。 - Flight 将其整理起来,生成响应头并将其发送回用户的浏览器。
- 用户充满喜悦,自我给自己一个温暖的拥抱!
为什么重要?
拥有一个合适的中心化路由器实际上会大大简化您的生活!起初可能有点难以看到。以下是一些原因:
- 中心化路由:您可以将所有路由集中在一个地方。这使您更容易查看您拥有的路由以及它们的作用。 如果需要,还可以更轻松地对其进行更改。
- 路由参数:您可以使用路由参数将数据传递到路由方法中。这是保持代码整洁和有组织的好方法。
- 路由组:您可以将路由分组。这对保持代码整洁以及对一组路由应用中间件非常有用。
- 路由别名:您可以为路由分配别名,以便稍后在代码中动态生成 URL(比如模板)。例如:您可以将
user_view
作为别名, 而不是在代码中硬编码/user/1234
,以后在决定更改为/admin/user/1234
时,您无需更改所有硬编码的 URL,只需更改与路由关联的 URL。 - 路由中间件:您可以将中间件添加到路由中。中间件非常有用,可以为应用程序添加特定的行为,例如验证某个用户能否访问某个路由或一组路由。
可能您熟悉逐个脚本的方式创建网站。您可能有一个名为 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()
来缓冲输出。这意味着您可以使用 echo
或 print
向用户发送响应,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_ERROR
和JSON_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()
后使用exit
或return
来防止进一步执行,但一般推荐使用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.';
});
请记住,调用 lastModified
或 etag
都会设置和检查缓存值。如果请求之间的缓存值相同,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');
});
另请参阅
- Routing - 如何将路由映射到控制器并渲染视图。
- Requests - 理解如何处理传入请求。
- Middleware - 使用中间件与路由进行身份验证、日志记录等。
- Why a Framework? - 理解使用像 Flight 这样的框架的好处。
- Extending - 如何使用您自己的功能扩展 Flight。
故障排除
- 如果重定向不起作用,请确保在方法中添加
return;
。 stop()
和halt()
不是同一件事。halt()
将立即停止执行,而stop()
将允许执行继续。
更新日志
- v3.17.1 - 在
downloadFile()
方法中添加了$fileName
。 - v3.12.0 - 添加了 downloadFile 辅助方法。
- v3.10.0 - 添加了
jsonHalt
。 - v1.0 - 初始发布。
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
$event
:您事件的一个名称(例如,'user.login'
)。$callback
:事件触发时运行的函数。
您通过告诉 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
$event
:您正在触发的イベント名称(必须匹配注册的事件)。...$args
:发送给监听器的可选参数(可以是任意数量的参数)。
简单示例
$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();
- 优点:保持
index.php
专注于路由,逻辑组织事件,易于查找和编辑。 - 缺点:添加了一点结构,对于非常小的应用程序可能感觉过度。
选项 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 的最佳实践
- 从简单开始:对于小型应用程序,将事件放在
index.php
中。它快速且符合 Flight 的极简主义。 - 智能增长:随着应用程序扩展(例如,超过 5-10 个事件),使用
app/config/events.php
文件。这是自然的升级步骤,就像组织路由一样,并保持您的代码整洁,而无需添加复杂框架。 - 避免过度工程:除非您的应用程序变得巨大,否则不要创建一个完整的“事件管理器”类或目录——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.";
});
为什么有用:编辑代码不关心缓存——它只需信号更新。应用程序的其他部分可以根据需要反应。
最佳实践
- 清晰命名事件:使用像
'user.login'
或'page.updated'
这样的具体名称,以便明显它们做什么。 - 保持监听器简单:不要在监听器中放置缓慢或复杂任务——保持您的应用程序快速。
- 测试您的事件:手动触发它们以确保监听器按预期工作。
- 明智使用事件:它们对于解耦很棒,但太多可能会使您的代码难以跟随——在有意义时使用它们。
Flight PHP 中的事件系统,使用 Flight::onEvent()
和 Flight::triggerEvent()
,为您提供了一种简单却强大的方式来构建灵活的应用程序。通过让应用程序的不同部分通过事件相互通信,您可以保持您的代码组织化、可重用且易于扩展。无论您是在记录操作、发送通知还是管理更新,事件都能帮助您在不纠缠逻辑的情况下完成它。而且,通过覆盖这些方法的能力,您有自由来定制系统以满足您的需求。从单个事件开始小规模,并观察它如何转变您的应用程序结构!
内置事件
Flight PHP 带有几个内置事件,您可以使用它们来钩入框架的生命周期。这些事件在请求/响应周期的特定点触发,允许您在某些操作发生时执行自定义逻辑。
内置事件列表
- flight.request.received:
function(Request $request)
当请求被接收、解析和处理时触发。 - flight.error:
function(Throwable $exception)
当请求生命周期中发生错误时触发。 - flight.redirect:
function(string $url, int $status_code)
当重定向被启动时触发。 - flight.cache.checked:
function(string $cache_key, bool $hit, float $executionTime)
当缓存被检查特定键时以及缓存命中或未命中时触发。 - flight.middleware.before:
function(Route $route)
在 before 中间件执行后触发。 - flight.middleware.after:
function(Route $route)
在 after 中间件执行后触发。 - flight.middleware.executed:
function(Route $route, $middleware, string $method, float $executionTime)
在任何中间件执行后触发 - flight.route.matched:
function(Route $route)
当路由匹配但尚未运行时触发。 - flight.route.executed:
function(Route $route, float $executionTime)
在路由执行和处理后触发。$executionTime
是执行路由(调用控制器等)所需的时间。 - flight.view.rendered:
function(string $template_file_path, float $executionTime)
在视图渲染后触发。$executionTime
是渲染模板所需的时间。注意:如果您覆盖render
方法,您需要重新触发此事件。 - flight.response.sent:
function(Response $response, float $executionTime)
在响应发送给客户端后触发。$executionTime
是构建响应所需的时间。
另请参阅
故障排除
- 如果您没有看到事件监听器被调用,请确保在触发事件之前注册它们。注册顺序很重要。
更新日志
- v3.15.0 - 将事件添加到 Flight。
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');
您的视图将保存名为 headerContent
和 bodyContent
的变量。然后您可以通过这样做来渲染您的布局:
Flight::render('layout', ['title' => 'Home Page']);
如果模板文件看起来像这样:
header.php
:
<h1><?= $heading ?></h1>
body.php
:
<div><?= $body ?></div>
layout.php
:
<html>
<head>
<title><?= $title ?></title>
</head>
<body>
<?= $headerContent ?>
<?= $bodyContent ?>
</body>
</html>
输出将是:
<html>
<head>
<title>Home Page</title>
</head>
<body>
<h1>Hello</h1>
<div>World</div>
</body>
</html>
Smarty
以下是如何使用 Smarty 模板引擎来处理您的视图:
// 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!
另请参阅
故障排除
- 如果您的中间件中有重定向,但您的应用似乎没有重定向,请确保在您的中间件中添加
exit;
语句。
更新日志
- v2.0 - 初始发布。
Learn/collections
集合
概述
Flight 中的 Collection
类是一个方便的实用工具,用于管理数据集合。它允许您使用数组和对象表示法来访问和操作数据,使您的代码更简洁和灵活。
理解
Collection
基本上是一个数组的包装器,但具有一些额外的功能。您可以像数组一样使用它,遍历它,计算其项数,甚至将项访问为对象属性。这在您希望在应用程序中传递结构化数据,或使您的代码更易读时特别有用。
Collection 实现了几个 PHP 接口:
ArrayAccess
(因此您可以使用数组语法)Iterator
(因此您可以使用foreach
循环)Countable
(因此您可以使用count()
)JsonSerializable
(因此您可以轻松转换为 JSON)
基本用法
创建 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 在您希望在组件之间传递结构化数据,或为数组数据提供更面向对象的接口时特别有用。
另请参阅
- Requests - 学习如何处理 HTTP 请求以及如何使用集合来管理请求数据。
- PDO Wrapper - 学习如何在 Flight 中使用 PDO 包装器以及如何使用集合来管理数据库结果。
故障排除
- 如果您尝试访问不存在的键,您将得到
null
而不是错误。 - 请记住,集合不是递归的:嵌套数组不会自动转换为集合。
- 如果您需要重置集合,请使用
$collection->clear()
或$collection->setData([])
。
更新日志
- v3.0 - 改进了类型提示并支持 PHP 8+。
- v1.0 - 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 相比的优点
- Fat-Free 在 GitHub 上比 Flight 多一些星标。
- Fat-Free 拥有一些不错的文档,但某些领域缺乏清晰度。
- Fat-Free 拥有一些稀疏的资源,如 YouTube 教程和在线文章,可用于学习该框架。
- Fat-Free 内置了一些有时有用的有帮助的插件。
- Fat-Free 内置了一个称为 Mapper 的 ORM,可用于与数据库交互。Flight 拥有 active-record。
- Fat-Free 内置了 Sessions、Caching 和本地化。Flight 要求您使用第三方库,但已在 documentation 中覆盖。
- Fat-Free 拥有一个小型的社区创建插件 组,可用于扩展框架。Flight 在 documentation 和 examples 页面中覆盖了一些。
- Fat-Free 与 Flight 一样没有依赖项。
- Fat-Free 与 Flight 一样旨在赋予开发者对应用程序的控制权,并提供简单的开发者体验。
- Fat-Free 与 Flight 一样维护向后兼容性(部分原因是更新变得不那么频繁)。
- Fat-Free 与 Flight 一样适合首次涉足框架领域的开发者。
- Fat-Free 内置了一个比 Flight 的模板引擎更健壮的模板引擎。Flight 推荐使用 Latte 来实现此功能。
- Fat-Free 有一个独特的 CLI 类型“route”命令,您可以在 Fat-Free 内部构建 CLI 应用程序,并将其视为类似于
GET
请求。Flight 使用 runway 来实现此功能。
与 Flight 相比的缺点
- Fat-Free 拥有一些实现测试,甚至有一个自己的非常基本的 test 类。然而, 它不像 Flight 那样 100% 单元测试。
- 您必须使用像 Google 这样的搜索引擎来实际搜索文档站点。
- Flight 的文档站点具有深色模式。(mic drop)
- Fat-Free 拥有一些严重未维护的模块。
- Flight 拥有一个简单的 PdoWrapper,它比 Fat-Free 的内置
DB\SQL
类稍简单一些。 - Flight 拥有一个 permissions plugin,可用于保护您的应用程序。Fat Free 要求您使用 第三方库。
- Flight 拥有一个称为 active-record 的 ORM,它感觉更像是一个 ORM,而不是 Fat-Free 的 Mapper。
active-record
的额外好处是您可以定义记录之间的关系以进行自动连接,而 Fat-Free 的 Mapper 要求您创建 SQL views。 - 令人惊讶的是,Fat-Free 没有根命名空间。Flight 完全命名空间化以避免与您自己的代码冲突。
Cache
类是这里最大的违规者。 - Fat-Free 没有中间件。相反,有
beforeroute
和afterroute
钩子,可用于在控制器中过滤请求和响应。 - Fat-Free 无法分组路由。
- Fat-Free 有一个依赖注入容器处理程序,但文档关于如何使用它的内容极其稀疏。
- 调试可能有点棘手,因为基本上所有内容都存储在所谓的
HIVE
中。
Learn/extending
扩展
概述
Flight 被设计为一个可扩展的框架。该框架附带一组默认方法和组件,但它允许您映射自己的方法、注册自己的类,甚至覆盖现有的类和方法。
理解
您可以通过 2 种方式扩展 Flight 的功能:
- 映射方法 - 这用于创建简单的自定义方法,您可以在应用程序的任何地方调用它们。这些通常用于实用函数,您希望能够在代码的任何地方调用。
- 注册类 - 这用于将自己的类注册到 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');
但是,像 map
和 register
这样的框架方法不能被覆盖。如果您尝试这样做,将得到错误(再次查看 下面 以获取方法列表)。
可映射的框架方法
以下是框架的完整方法集。它包括核心方法,这些是常规的静态方法,以及可扩展方法,这些是可映射的方法,可以被过滤或覆盖。
核心方法
这些方法是框架的核心,不能被覆盖。
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) // 触发事件。
使用 map
和 register
添加的任何自定义方法也可以被过滤。有关如何过滤这些方法的示例,请参阅 过滤方法 指南。
可扩展的框架类
您可以通过扩展它们并注册自己的类来覆盖几个类的功能。这些类是:
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';
});
您还可以使用 before
和 after
方法创建一个基本的 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();
很酷吧?
另请参阅
故障排除
- 记住,映射的方法优先于注册的类。如果您使用相同的名称声明两者,只有映射的方法将被调用。
更新日志
- v2.0 - 初始发布。
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);
另请参阅
- Collections - 用于处理可以轻松转换为 JSON 的结构化数据。
- Configuration - 如何配置您的 Flight 应用程序。
- Extending - 如何添加您自己的实用工具或覆盖核心类。
故障排除
- 如果编码或解码失败,将抛出异常——如果您想优雅地处理错误,请将您的调用包装在 try/catch 中。
- 如果您得到意外结果,请检查您的数据是否包含循环引用或非 UTF-8 字符。
- 在解码之前,使用
Json::isValid()
检查字符串是否为有效的 JSON。
更新日志
- v3.16.0 - 添加了 JSON 包装器实用类。
Learn/flight_vs_slim
Flight 与 Slim
什么是 Slim?
Slim 是一个 PHP 微框架,它帮助您快速编写简单却强大的 Web 应用程序和 API。
Flight 的一些 v3 功能的灵感实际上来自于 Slim。路由分组以及按特定顺序执行中间件是两个受 Slim 启发的功能。Slim v3 推出时以简洁为导向,但 v4 版本的评价褒贬不一。
与 Flight 相比的优势
- Slim 拥有更大的开发者社区,这些开发者会制作实用的模块,帮助您避免重复造轮子。
- Slim 遵循 PHP 社区中常见的许多接口和标准,从而提高了互操作性。
- Slim 拥有不错的文档和教程,可用于学习框架(不过与 Laravel 或 Symfony 相比仍相形见绌)。
- Slim 有各种资源,如 YouTube 教程和在线文章,可用于学习框架。
- Slim 允许您使用任何组件来处理核心路由功能,因为它符合 PSR-7 标准。
与 Flight 相比的劣势
- 令人惊讶的是,作为微框架,Slim 的速度并不像您想象的那么快。请参阅 TechEmpower 基准测试 以获取更多信息。
- Flight 针对那些希望构建轻量级、快速且易用的 Web 应用程序的开发者。
- Flight 无任何依赖项,而 Slim 有一些依赖项,您必须安装它们。
- Flight 以简洁和易用性为导向。
- Flight 的核心功能之一是尽最大努力保持向后兼容性。Slim 从 v3 到 v4 是一个破坏性变更。
- Flight 适合那些首次涉足框架领域的开发者。
- Flight 也可以处理企业级应用程序,但它没有像 Slim 那样多的示例和教程。 它还需要开发者在保持事物组织化和结构良好方面付出更多自律。
- Flight 赋予开发者对应用程序的更多控制权,而 Slim 可能会在幕后偷偷引入一些魔法。
- Flight 有一个简单的 PdoWrapper,可用于与您的数据库交互。Slim 要求您使用第三方库。
- Flight 有一个 permissions plugin,可用于保护您的应用程序。Slim 要求您使用第三方库。
- Flight 有一个名为 active-record 的 ORM,可用于与您的数据库交互。Slim 要求您使用第三方库。
- Flight 有一个名为 runway 的 CLI 应用程序,可用于从命令行运行您的应用程序。Slim 没有。
Learn/autoloading
自动加载
概述
自动加载是 PHP 中的一个概念,您可以指定一个或多个目录来加载类。这比使用 require
或 include
来加载类更有益。它也是使用 Composer 包的要求。
理解
默认情况下,任何 Flight
类都会通过 Composer 自动为您自动加载。但是,如果您想自动加载自己的类,可以使用 Flight::path()
方法指定一个目录来加载类。
使用自动加载器可以显著简化您的代码。文件开头不再需要一堆 include
或 require
语句来捕获该文件中使用的所有类,而是可以动态调用您的类,它们会自动包含。
基本用法
假设我们有一个像下面的目录树:
# 示例路径
/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() {
// 做些什么
}
}
另请参阅
故障排除
- 如果您似乎无法弄清楚为什么您的命名空间类找不到,请记住使用
Flight::path()
到项目中的根目录,而不是您的app/
或src/
目录或等效目录。
类未找到(自动加载不工作)
这可能有几个原因。下面是一些示例,但请确保您也查看自动加载部分。
错误的 文件名
最常见的是类名与文件名不匹配。
如果您有一个名为 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__.'/../');
更新日志
- v3.7.2 - 您可以通过运行
Loader::setV2ClassLoading(false);
使用 Pascal_Snake_Case 作为类名 - v2.0 - 添加了自动加载功能。
Learn/uploaded_file
上传文件处理器
概述
Flight 中的 UploadedFile
类使处理应用程序中的文件上传变得简单且安全。它封装了 PHP 文件上传过程的细节,为您提供一种简单、面向对象的方式来访问文件信息并移动上传的文件。
理解
当用户通过表单上传文件时,PHP 将文件信息存储在 $_FILES
超级全局变量中。在 Flight 中,您很少直接与 $_FILES
交互。相反,Flight 的 Request
对象(通过 Flight::request()
访问)提供了一个 getUploadedFiles()
方法,该方法返回一个 UploadedFile
对象的数组,使文件处理更加方便和健壮。
UploadedFile
类提供了以下方法:
- 获取原始文件名、MIME 类型、大小和临时位置
- 检查上传错误
- 将上传的文件移动到永久位置
此类帮助您避免文件上传的常见陷阱,例如处理错误或安全地移动文件。
基本用法
从请求中访问上传的文件
访问上传文件的最推荐方式是通过请求对象:
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.";
}
另请参阅
- Requests - 了解如何从 HTTP 请求中访问上传的文件,并查看更多文件上传示例。
- Configuration - 如何在 PHP 中配置上传限制和目录。
- Extending - 如何自定义或扩展 Flight 的核心类。
故障排除
- 在移动文件之前始终检查
$file->getError()
。 - 确保您的上传目录可由 Web 服务器写入。
- 如果
moveTo()
失败,请检查异常消息以获取详细信息。 - PHP 的
upload_max_filesize
和post_max_size
设置可能会限制文件上传。 - 对于多个文件上传,始终循环遍历
UploadedFile
对象的数组。
更新日志
- v3.12.0 - 将
UploadedFile
类添加到请求对象中,以简化文件处理。
Guides/unit_testing
使用 PHPUnit 在 Flight PHP 中进行单元测试
本指南介绍了使用 PHPUnit 在 Flight PHP 中进行单元测试,针对希望了解为什么单元测试重要以及如何实际应用它的初学者。我们将重点关注测试行为——确保您的应用程序按预期执行,例如发送电子邮件或保存记录——而不是琐碎的计算。我们将从一个简单的 route handler 开始,逐步推进到一个更复杂的 controller,并结合 dependency injection (DI) 和模拟第三方服务。
为什么进行单元测试?
单元测试确保您的代码按预期行为,在问题到达生产环境之前捕获 bug。在 Flight 中,这尤其有价值,因为轻量级路由和灵活性可能导致复杂的交互。对于独行开发者或团队,单元测试充当安全网,记录预期行为,并在您稍后重访代码时防止回归。它们还能改善设计:难以测试的代码通常表明类过于复杂或耦合紧密。
与简单示例(例如,测试 x * y = z
)不同,我们将重点关注现实世界的行为,例如验证输入、保存数据或触发电子邮件等操作。我们的目标是使测试易于接近且有意义。
一般指导原则
- 测试行为,而不是实现:关注结果(例如,“电子邮件已发送”或“记录已保存”),而不是内部细节。这使测试在重构时更健壮。
- 停止使用
Flight::
:Flight 的静态方法非常方便,但会使测试变得困难。您应该习惯使用$app = Flight::app();
中的$app
变量。$app
具有与Flight::
相同的全部方法。您仍然可以在控制器中使用$app->route()
或$this->app->json()
等。您还应该使用真实的 Flight 路由器$router = $app->router()
,然后可以使用$router->get()
、$router->post()
、$router->group()
等。请参阅 Routing。 - 保持测试快速:快速测试鼓励频繁执行。避免在单元测试中使用慢速操作,如数据库调用。如果您有一个慢速测试,这表明您正在编写集成测试,而不是单元测试。集成测试涉及实际的数据库、实际的 HTTP 调用、实际的电子邮件发送等。它们有其位置,但它们缓慢且可能不稳定,意味着它们有时会因未知原因失败。
- 使用描述性名称:测试名称应清楚描述被测试的行为。这提高了可读性和可维护性。
- 像瘟疫一样避免全局变量:最小化
$app->set()
和$app->get()
的使用,因为它们像全局状态一样,需要在每个测试中模拟。优先使用 DI 或 DI 容器(请参阅 Dependency Injection Container)。即使使用$app->map()
方法在技术上也是“全局”的,应避免使用 DI 替代。使用会话库如 flightphp/session,以便在测试中模拟会话对象。不要在您的代码中直接调用$_SESSION
,因为这会将全局变量注入您的代码,使其难以测试。 - 使用依赖注入:将依赖项(例如,
PDO
、邮件程序)注入控制器,以隔离逻辑并简化模拟。如果您的类有太多依赖项,请考虑将其重构为更小的类,每个类都有单一职责,遵循 SOLID 原则。 - 模拟第三方服务:模拟数据库、HTTP 客户端(cURL)或电子邮件服务,以避免外部调用。测试一到两层深度,但让您的核心逻辑运行。例如,如果您的应用发送短信,您不希望每次运行测试时都真正发送短信,因为那些费用会累积(而且会更慢)。相反,模拟短信服务,并仅验证您的代码以正确的参数调用了短信服务。
- 追求高覆盖率,而不是完美:100% 行覆盖率很好,但它并不意味着您的代码中的一切都按应有的方式进行了测试(请继续研究 PHPUnit 中的分支/路径覆盖率)。优先考虑关键行为(例如,用户注册、API 响应和捕获失败响应)。
- 为路由使用控制器:在您的路由定义中,使用控制器而不是闭包。默认情况下,
flight\Engine $app
通过构造函数注入到每个控制器中。在测试中,使用$app = new Flight\Engine()
在测试中实例化 Flight,将其注入到您的控制器中,并直接调用方法(例如,$controller->register()
)。请参阅 Extending Flight 和 Routing。 - 选择一种模拟风格并坚持使用:PHPUnit 支持几种模拟风格(例如,prophecy、内置模拟),或者您可以使用匿名类,它们有自己的好处,如代码补全、如果您更改方法定义则中断等。只需在您的测试中保持一致。请参阅 PHPUnit Mock Objects。
- 为要在子类中测试的方法/属性使用
protected
可见性:这允许您在测试子类中覆盖它们,而无需将它们设为 public,这对于匿名类模拟特别有用。
设置 PHPUnit
首先,在您的 Flight PHP 项目中使用 Composer 设置 PHPUnit 以进行轻松测试。请参阅 PHPUnit 入门指南 以获取更多细节。
-
在您的项目目录中运行:
composer require --dev phpunit/phpunit
这将安装最新的 PHPUnit 作为开发依赖项。
-
在您的项目根目录中创建一个
tests
目录,用于测试文件。 -
在
composer.json
中添加一个测试脚本以方便使用:// other composer.json content "scripts": { "test": "phpunit --configuration phpunit.xml" }
-
在根目录中创建一个
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']);
}
}
关键点:
- 我们使用请求类模拟 POST 数据。不要使用全局变量如
$_POST
、$_GET
等,因为这会使测试更复杂(您必须始终重置这些值,否则其他测试可能会崩溃)。 - 所有控制器默认都会注入
flight\Engine
实例,即使没有设置 DIC 容器。这使直接测试控制器变得更容易。 - 完全没有使用
Flight::
,使代码更容易测试。 - 测试验证行为:有效/无效电子邮件的正确状态和消息。
运行 composer test
以验证路由按预期行为。对于 Flight 中的 requests 和 responses,请参阅相关文档。
使用依赖注入创建可测试的控制器
对于更复杂的场景,使用 dependency injection (DI) 来使控制器可测试。避免 Flight 的全局变量(例如,Flight::set()
、Flight::map()
、Flight::register()
),因为它们像全局状态一样,需要为每个测试模拟。相反,使用 Flight 的 DI 容器、DICE、PHP-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']);
}
}
关键点:
- 控制器依赖于
PdoWrapper
实例和MailerInterface
(一个假想的第三方电子邮件服务)。 - 依赖项通过构造函数注入,避免全局变量。
使用模拟测试控制器
现在,让我们测试 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']);
}
}
关键点:
- 我们模拟
PdoWrapper
和MailerInterface
以避免真实的数据库或电子邮件调用。 - 测试验证行为:有效电子邮件触发数据库插入和电子邮件发送;无效电子邮件跳过两者。
- 模拟第三方依赖项(例如,
PdoWrapper
、MailerInterface
),让控制器的逻辑运行。
模拟过多
小心不要模拟太多您的代码。下面让我给您一个例子,说明为什么这可能是坏事,使用我们的 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']);
}
}
太好了,我们有单元测试,它们通过了!但是等等,如果我实际更改 isEmailValid
或 registerUser
的内部工作方式呢?我的测试仍然会通过,因为我模拟了所有功能。让我向您展示我的意思。
// 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。
常见陷阱
- 过度模拟:不要模拟每个依赖项;让一些逻辑(例如,控制器验证)运行以测试真实行为。请参阅 Unit Testing and SOLID Principles。
- 全局状态:大量使用全局 PHP 变量(例如,
$_SESSION
、$_COOKIE
)会使测试脆弱。Flight::
也是如此。重构以显式传递依赖项。 - 复杂设置:如果测试设置繁琐,您的类可能有太多依赖项或职责,违反 SOLID 原则。
使用单元测试扩展
单元测试在大项目中或在数月后重访代码时大放异彩。它们记录行为并捕获回归,从而节省您重新学习应用程序的时间。对于独行开发者,测试关键路径(例如,用户注册、支付处理)。对于团队,测试确保贡献行为一致。请参阅 Why Frameworks? 以获取更多关于使用框架和测试的好处的信息。
向 Flight PHP 文档仓库贡献您自己的测试提示!
由 n0nag0n 撰写 2025
Guides/blog
使用 Flight PHP 构建简单博客
本指南带您通过使用 Flight PHP 框架创建基本博客的过程。您将设置项目,定义路由,使用 JSON 管理帖子,并使用 Latte 模板引擎进行呈现——所有这些都展示了 Flight 的简单性和灵活性。到最后,您将拥有一个功能性博客,包含主页、单独的帖子页面和创建表单。
先决条件
- PHP 7.4+:已安装在您的系统中。
- Composer:用于依赖管理。
- 文本编辑器:任何编辑器,如 VS Code 或 PHPStorm。
- PHP 和 Web 开发的基本知识。
第一步:设置您的项目
首先创建一个新的项目目录并通过 Composer 安装 Flight。
-
创建目录:
mkdir flight-blog cd flight-blog
-
安装 Flight:
composer require flightphp/core
-
创建公共目录: Flight 使用单个入口点 (
index.php
)。为其创建public/
文件夹:mkdir public
-
基本的
index.php
: 创建public/index.php
,添加简单的“你好,世界”路由:<?php require '../vendor/autoload.php'; Flight::route('/', function () { echo '你好,Flight!'; }); Flight::start();
-
运行内置服务器: 使用 PHP 的开发服务器测试您的设置:
php -S localhost:8000 -t public/
访问
http://localhost:8000
查看“你好,Flight!”。
第二步:组织您的项目结构
为了保持设置整洁,请将项目构建为如下结构:
flight-blog/
├── app/
│ ├── config/
│ └── views/
├── data/
├── public/
│ └── index.php
├── vendor/
└── composer.json
app/config/
:配置文件(例如,事件,路由)。app/views/
:用于呈现页面的模板。data/
:用于存储博客帖子的 JSON 文件。public/
:包含index.php
的 Web 根目录。
第三步:安装和配置 Latte
Latte 是一个轻量级的模板引擎,与 Flight 很好地集成。
-
安装 Latte:
composer require latte/latte
-
在 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();
-
创建布局模板:在
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>© {date('Y')} Flight 博客</p> </footer> </body> </html>
-
创建首页模板: 在
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
查看渲染页面。 -
创建数据文件:
使用 JSON 文件模拟数据库以简化操作。
在
data/posts.json
:[ { "slug": "first-post", "title": "我的第一篇帖子", "content": "这是我用 Flight PHP 撰写的第一篇博客帖子!" } ]
第四步:定义路由
将路由分开到配置文件中,以便更好地组织。
-
创建
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' => '创建帖子']); });
-
更新
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();
第五步:存储和检索博客帖子
添加加载和保存帖子的功能。
-
添加帖子方法: 在
index.php
中,添加一个加载帖子的的方法:Flight::map('posts', function () { $file = __DIR__ . '/../data/posts.json'; return json_decode(file_get_contents($file), true); });
-
更新路由: 修改
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' => '创建帖子']); });
第六步:创建模板
更新您的模板以显示帖子。
-
帖子页面 (
app/views/post.latte
):{extends 'layout.latte'} {block content} <h2>{$post['title']}</h2> <div class="post-content"> <p>{$post['content']}</p> </div> {/block}
第七步:添加帖子创建功能
处理表单提交以添加新帖子。
-
创建表单 (
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}
-
添加 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('/'); });
-
测试它:
- 访问
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}
下一步
- 添加样式:在您的模板中使用 CSS 以获得更好的外观。
- 数据库:使用
PdoWrapper
替换posts.json
为数据库,例如 SQLite。 - 验证:添加对重复 slug 或空输入的检查。
- 中间件:实施身份验证以进行帖子创建。
结论
您已使用 Flight PHP 构建了一个简单的博客!本指南展示了核心功能,如路由、使用 Latte 进行模板处理和处理表单提交——同时保持轻量化。探索 Flight 的文档以获取更多高级功能以进一步提升您的博客!
License
MIT
许可证
版权所有 © 2024
@mikecao, @n0nag0n
特此免费授予任何获得本软件副本及相关文档文件(以下简称“软件”)的人,无偿使用本软件的权限,包括但不限于使用、复制、修改、合并、发布、分发、再许可以及销售本软件的副本,并允许被授予本软件的人员这样做,但须符合以下条件:
上述版权通知和本许可通知应包含在所有副本或重要部分的软件中。
本软件按原样提供,不附带任何形式的担保,包括但不限于适销性、特定用途的适用性和非侵权性的保证。在任何情况下,作者或版权所有者均不承担任何索赔、损害赔偿或其他责任,无论是合同诉讼、侵权行为或其他方面,来源于、无论是源于还是与本软件或本软件的使用或其他交易有关。
About
Flight PHP 框架
Flight 是一个快速、简单、可扩展的 PHP 框架——专为那些希望快速完成任务且不希望大费周章的开发人员而构建。不管您是在构建经典的网络应用、极速的 API,还是在试验最新的 AI 驱动工具,Flight 的低占用和直观设计使其成为完美选择。Flight 旨在保持精简,但也能满足企业架构需求。
为什么选择 Flight?
- 适合初学者: Flight 是新 PHP 开发人员的一个伟大起点。其清晰的结构和简单语法能帮助您学习网络开发,而不会迷失在样板代码中。
- 专业人士喜爱: 经验丰富的开发人员喜爱 Flight 的灵活性和控制性。您可以从小型原型扩展到功能齐全的应用,而无需切换框架。
- AI 友好: Flight 的最小开销和干净架构使其非常适合集成 AI 工具和 API。不管您是在构建智能聊天机器人、AI 驱动的仪表板,还是只是想试验,Flight 会让您专注于重要事项。该 skeleton app 附带了主要 AI 编码助手的预构建说明文件!了解更多关于使用 AI 与 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 聊天
以及 Discord
贡献
您可以通过两种方式为 Flight 贡献:
- 贡献于核心框架,通过访问 core repository。
- 帮助改进文档!这个文档网站托管在 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
- Light, standalone and simple
- All code in one file - no pointless drivers.
- Secure - every generated cache file have a php header with die, making direct access impossible even if someone knows the path and your server is not configured properly
- Well documented and tested
- Handles concurrency correctly via flock
- Supports PHP 7.4+
- Free under a MIT license
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
主要配置选项:
command
: 运行工作者的命令directory
: 工作者的工作目录autostart
: 当 supervisord 启动时自动启动autorestart
: 如果进程退出,自动重新启动startretries
: 如果启动失败,重试启动的次数stderr_logfile
/stdout_logfile
: 日志文件位置user
: 以哪个系统用户身份运行进程numprocs
: 要运行的工作实例数量process_name
: 多个工作进程的命名格式
使用 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 的舒适环境。
它能做什么?
- 无缝集成 Flight PHP 与 WordPress
- 根据 URL 模式将请求路由到 Flight 或 WordPress
- 使用控制器、模型和视图(MVC)组织您的代码
- 轻松设置推荐的 Flight 文件夹结构
- 使用 WordPress 的数据库连接或您自己的连接
- 微调 Flight 和 WordPress 的交互方式
- 简单的管理界面进行配置
安装
- 将
flight-integration
文件夹上传到您的/wp-content/plugins/
目录。 - 在 WordPress 管理后台(插件菜单)激活插件。
- 转到 设置 > Flight Framework 来配置插件。
- 设置供应商路径指向您的 Flight 安装(或使用 Composer 安装 Flight)。
- 配置您的应用程序文件夹路径并创建文件夹结构(插件可以帮助您完成!)。
- 开始构建您的 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(或其他异步驱动程序),只需最少的更改。
要求
- PHP 7.4 或更高版本
- Flight 框架 3.16.1 或更高版本
- 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
- swoole_server.php
- SwooleServerDriver.php
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();
}
}
运行服务器
- 开发(PHP 内置服务器 / PHP-FPM):
- php -S localhost:8000(如果您的 index 在 public/ 中,请添加 -t public/)
- 生产(Swoole):
- php swoole_server.php
提示:对于生产环境,请在 Swoole 前使用反向代理(Nginx)来处理 TLS、静态文件和负载均衡。
配置说明
Swoole 驱动程序暴露了几个配置选项:
- worker_num:工作进程数量
- max_request:每个工作进程的重启前请求数
- enable_coroutine:使用协程进行并发
- buffer_output_size:输出缓冲区大小
根据您的主机资源和流量模式调整这些设置。
错误处理
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 脚本
- 使用命令行或 API 进行管理。
SQL 脚本
脚本分为三组脚本:
- BASE 脚本包含创建新数据库的所有 SQL 命令;
- UP 脚本包含所有数据库版本 "up" 的 SQL 迁移命令;
- DOWN 脚本包含所有数据库版本 "down" 或还原的 SQL 迁移命令;
脚本目录如下:
<root dir>
|
+-- base.sql
|
+-- /migrations
|
+-- /up
|
+-- 00001.sql
+-- 00002.sql
+-- /down
|
+-- 00000.sql
+-- 00001.sql
- "base.sql" 是基础脚本
- "up" 文件夹包含迁移提升版本的脚本。 例如:00002.sql 是将数据库从版本 '1' 移动到 '2' 的脚本。
- "down" 文件夹包含迁移降低版本的脚本。 例如:00001.sql 是将数据库从版本 '2' 移动到 '1' 的脚本。 "down" 文件夹是可选的。
多开发环境
如果您与多个开发人员和多个分支工作,确定下一个数字将会非常困难。
在这种情况下,您可以在版本号后面加上后缀 "-dev"。
看看这个场景:
- 开发人员 1 创建了一个分支,最新版本为例如 42。
- 开发人员 2 同时创建一个分支,并且有相同的数据库版本号。
在这两种情况下,开发人员将创建一个名为 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 并将其集成到您的项目中
基本用法是
- 创建一个 ConnectionManagement 对象的连接。有关更多信息,请参见 "byjg/anydataset" 组件
- 使用此连接和脚本 SQL 所在文件夹创建 Migration 对象。
- 使用适当的命令进行 "reset"、"up" 或 "down" 迁移脚本。
查看一个示例:
<?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 up
或 partial down
,需要手动修复才能再次迁移。
为了避免这种情况,您可以指定迁移将在事务上下文中运行。
如果迁移脚本失败,事务将被回滚,迁移表将标记为 complete
,版本将是导致错误的脚本之前的立即前一个版本。
要启用此功能,您需要调用方法 withTransactionEnabled
,并传递 true
作为参数:
<?php
$migration->withTransactionEnabled(true);
注意:此功能在 MySQL 中不可用,因为它不支持事务中的 DDL 命令。 如果您在 MySQL 中使用此方法,迁移将悄默忽略它。 更多信息:https://dev.mysql.com/doc/refman/8.0/en/cannot-roll-back.html
编写 Postgres SQL 迁移的提示
创建触发器和 SQL 函数
-- DO
CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$
BEGIN
-- 检查 empname 和薪水是否已给出
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();
关键点
- 非阻塞:默认使用
read_and_close
启动会话,防止会话锁定问题。 - 自动提交:默认启用,因此更改会在关闭时自动保存,除非禁用。
- 文件存储:会话默认存储在系统临时目录下的
/flight_sessions
。
配置
在注册时,通过传递一个选项数组来自定义会话处理程序:
// 是的,这是一个双数组 :)
$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 序列化:
'serialization' => 'json'
(默认):- 只允许会话数据中包含数组和基本类型。
- 更安全:免疫 PHP 对象注入。
- 文件以
J
(纯 JSON)或F
(加密 JSON)开头。
'serialization' => 'php'
:- 允许存储 PHP 对象(请谨慎使用)。
- 文件以
P
(纯 PHP 序列化)或E
(加密 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
类提供以下方法:
set(string $key, $value)
: 在会话中存储一个值。get(string $key, $default = null)
: 检索一个值,如果键不存在,则使用可选默认值。delete(string $key)
: 从会话中删除特定键。clear()
: 删除所有会话数据,但保留相同的会话文件名。commit()
: 将当前会话数据保存到文件系统。id()
: 返回当前会话 ID。regenerate(bool $deleteOldFile = false)
: 再生会话 ID,包括创建新会话文件,保留所有旧数据,旧文件保留在系统中。如果$deleteOldFile
为true
,则删除旧会话文件。destroy(string $id)
: 通过 ID 销毁会话并从系统中删除会话文件。这是SessionHandlerInterface
的一部分,$id
是必需的。典型用法为$session->destroy($session->id())
。getAll()
: 返回当前会话的所有数据。
除 get()
和 id()
外的所有方法都返回 Session
实例以支持链式调用。
为什么使用此插件?
- 轻量级:无需外部依赖——只需文件。
- 非阻塞:默认使用
read_and_close
避免会话锁定。 - 安全:支持 AES-256-CBC 加密用于敏感数据。
- 灵活:提供自动提交、测试模式和手动控制选项。
- Flight 专用:专为 Flight 框架构建。
技术细节
- 存储格式:会话文件以
sess_
为前缀,存储在配置的save_path
中。文件内容前缀:J
:纯 JSON(默认,无加密)F
:加密 JSON(默认使用加密)P
:纯 PHP 序列化(旧版,无加密)E
:加密 PHP 序列化(旧版使用加密)
- 加密:当提供
encryption_key
时,使用 AES-256-CBC 加密,每次会话写入时使用随机 IV。加密适用于 JSON 和 PHP 序列化模式。 - 序列化:JSON 是默认且最安全的方法。PHP 序列化适用于旧版/高级使用,但安全性较低。
- 垃圾回收:实现 PHP 的
SessionHandlerInterface::gc()
以清理过期的会话。
贡献
欢迎贡献!分叉 仓库,进行更改,然后提交拉取请求。通过 Github 问题跟踪器报告错误或建议功能。
许可证
此插件采用 MIT 许可证。有关详细信息,请参阅 Github 仓库。
Awesome-plugins/runway
跑道
跑道是一个CLI应用程序,可帮助您管理您的Flight应用程序。它可以生成控制器,显示所有路由等。它基于优秀的 adhocore/php-cli 库。
点击这里 查看代码。
安装
使用 composer 安装。
composer require flightphp/runway
基本配置
第一次运行跑道时,它将引导您完成设置过程并在项目根目录中创建一个 .runway.json
配置文件。此文件将包含一些Runway正常工作所需的配置。
用法
跑道有许多命令可用于管理您的Flight应用程序。有两种简单的方法可以使用跑道。
- 如果您使用的是骨架项目,可以从项目的根目录运行
php runway [command]
。 - 如果您是通过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
表:id
,name
,email
,created_at
,updated_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 - 分析所有 Flight 变量。
- Database - 分析页面上运行的所有查询(如果您正确初始化数据库连接)。
- Request - 分析所有
$_SERVER
变量并检查所有全局负载($_GET
、$_POST
、$_FILES
)。 - Session - 如果会话处于活动状态,则分析所有
$_SESSION
变量。
这是面板
每个面板都会显示有关您的应用程序的非常有用的信息!
点击这里查看代码。
安装
运行 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 站点的仪表板演示。
为什么 APM 重要
想象一下:您的应用就像一家繁忙的餐厅。没有一种方法来跟踪订单需要多长时间或厨房在哪里卡住,您只能猜测为什么客户离开时心情不好。APM 是您的副厨师——它监视每一个步骤,从传入请求到数据库查询,并标记任何减慢您速度的东西。慢页面会丢失用户(研究表明,如果网站加载超过 3 秒,53% 的用户会跳出!),APM 帮助您在问题刺痛之前抓住它们。它是主动的安心——更少的“为什么这个坏了?”时刻,更多的“看这个运行得多顺畅!”胜利。
安装
使用 Composer 开始:
composer require flightphp/apm
您需要:
- PHP 7.4+:保持我们与 LTS Linux 发行版兼容,同时支持现代 PHP。
- FlightPHP Core v3.15+:我们正在提升的轻量级框架。
支持的数据库
FlightPHP APM 当前支持以下数据库来存储指标:
- SQLite3:简单、基于文件,非常适合本地开发或小型应用。大多数设置中的默认选项。
- MySQL/MariaDB:适合大型项目或生产环境,您需要健壮、可扩展的存储。
您可以在配置步骤(见下文)中选择您的数据库类型。请确保您的 PHP 环境安装了必要的扩展(例如,pdo_sqlite
或 pdo_mysql
)。
快速开始
以下是通往 APM 卓越的逐步指南:
1. 注册 APM
将此代码放入您的 index.php
或 services.php
文件中以开始跟踪:
use flight\apm\logger\LoggerFactory;
use flight\Apm;
$ApmLogger = LoggerFactory::create(__DIR__ . '/../../.runway-config.json');
$Apm = new Apm($ApmLogger);
$Apm->bindEventsToFlightInstance($app);
// 如果您正在添加数据库连接
// 必须是来自 Tracy Extensions 的 PdoWrapper 或 PdoQueryCapture
$pdo = new PdoWrapper('mysql:host=localhost;dbname=example', 'user', 'pass', null, true); // <-- 需要 true 以在 APM 中启用跟踪。
$Apm->addPdoConnection($pdo);
这里发生了什么?
LoggerFactory::create()
获取您的配置(稍后详述)并设置一个日志记录器——默认是 SQLite。Apm
是明星——它监听 Flight 的事件(请求、路由、错误等)并收集指标。bindEventsToFlightInstance($app)
将其全部绑定到您的 Flight 应用。
专业提示:采样 如果您的应用很繁忙,记录每个请求可能会过载。用采样率(0.0 到 1.0):
$Apm = new Apm($ApmLogger, 0.1); // 记录 10% 的请求
这保持性能敏捷,同时仍提供可靠的数据。
2. 配置它
运行此命令来创建您的 .runway-config.json
:
php vendor/bin/runway apm:init
这是做什么?
- 启动一个向导,询问原始指标来自哪里(源)和处理后的数据去哪里(目标)。
- 默认是 SQLite——例如,源是
sqlite:/tmp/apm_metrics.sqlite
,目标是另一个。 - 您将得到类似这样的配置:
{ "apm": { "source_type": "sqlite", "source_db_dsn": "sqlite:/tmp/apm_metrics.sqlite", "storage_type": "sqlite", "dest_db_dsn": "sqlite:/tmp/apm_metrics_processed.sqlite" } }
此过程还会询问您是否要为此设置运行迁移。如果您是第一次设置,答案是是的。
为什么有两个位置? 原始指标堆积很快(想想未过滤的日志)。工作进程将它们处理成仪表板用的结构化目标。保持一切整洁!
3. 使用 Worker 处理指标
Worker 将原始指标转换为仪表板就绪数据。运行一次:
php vendor/bin/runway apm:worker
它在做什么?
- 从您的源读取(例如,
apm_metrics.sqlite
)。 - 处理最多 100 个指标(默认批次大小)到您的目标。
- 完成后停止,或如果没有剩余指标。
保持它运行 对于实时应用,您需要连续处理。以下是您的选项:
-
守护进程模式:
php vendor/bin/runway apm:worker --daemon
永远运行,处理指标如它们到来。适合开发或小型设置。
-
Crontab: 将此添加到您的 crontab(
crontab -e
):* * * * * php /path/to/project/vendor/bin/runway apm:worker
每分钟触发——适合生产。
-
Tmux/Screen: 启动一个可分离会话:
tmux new -s apm-worker php vendor/bin/runway apm:worker --daemon # Ctrl+B,然后 D 分离;`tmux attach -t apm-worker` 重新连接
即使您注销,也保持运行。
-
自定义调整:
php vendor/bin/runway apm:worker --batch_size 50 --max_messages 1000 --timeout 300
--batch_size 50
:一次处理 50 个指标。--max_messages 1000
:处理 1000 个指标后停止。--timeout 300
:5 分钟后退出。
为什么费心? 没有 Worker,您的仪表板是空的。它是原始日志和可操作洞察之间的桥梁。
4. 启动仪表板
查看您应用的生命体征:
php vendor/bin/runway apm:dashboard
这是什么?
- 在
http://localhost:8001/apm/dashboard
启动一个 PHP 服务器。 - 显示请求日志、慢路由、错误率等。
自定义它:
php vendor/bin/runway apm:dashboard --host 0.0.0.0 --port 8080 --php-path=/usr/local/bin/php
--host 0.0.0.0
:从任何 IP 访问(远程查看方便)。--port 8080
:如果 8001 被占用,使用不同端口。--php-path
:如果不在您的 PATH 中,指向 PHP。
在浏览器中访问 URL 并探索!
生产模式
对于生产,您可能需要尝试一些技巧来运行仪表板,因为可能有防火墙和其他安全措施。以下是几个选项:
- 使用反向代理:设置 Nginx 或 Apache 将请求转发到仪表板。
- SSH 隧道:如果您可以 SSH 到服务器,使用
ssh -L 8080:localhost:8001 youruser@yourserver
将仪表板隧道到您的本地机器。 - VPN:如果您的服务器在 VPN 后面,连接到它并直接访问仪表板。
- 配置防火墙:为您的 IP 或服务器网络打开端口 8001(或您设置的任何端口)。
- 配置 Apache/Nginx:如果您的应用前面有 Web 服务器,您可以配置它到域名或子域名。如果您这样做,您将文档根目录设置为
/path/to/your/project/vendor/flightphp/apm/dashboard
想要不同的仪表板?
如果您愿意,您可以构建自己的仪表板!查看 vendor/flightphp/apm/src/apm/presenter 目录以获取如何为自己的仪表板呈现数据的想法!
仪表板功能
仪表板是您的 APM 总部——以下是您将看到的内容:
- 请求日志:每个请求带有时间戳、URL、响应代码和总时间。点击“Details”查看中间件、查询和错误。
- 最慢请求:占用时间最多的前 5 个请求(例如,“/api/heavy” 2.5 秒)。
- 最慢路由:按平均时间的前 5 个路由——非常适合发现模式。
- 错误率:失败请求的百分比(例如,2.3% 500 错误)。
- 延迟百分位:95th (p95) 和 99th (p99) 响应时间——了解您的最坏情况。
- 响应代码图表:随时间可视化 200、404、500 等。
- 长查询/中间件:前 5 个慢数据库调用和中间件层。
- 缓存命中/未命中:您的缓存拯救局面的频率。
额外功能:
- 按“Last Hour”、“Last Day”或“Last Week”过滤。
- 为那些深夜会话切换暗黑模式。
示例:
对 /users
的请求可能显示:
- 总时间:150ms
- 中间件:
AuthMiddleware->handle
(50ms) - 查询:
SELECT * FROM users
(80ms) - 缓存:命中
user_list
(5ms)
添加自定义事件
跟踪任何东西——如 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);
您得到什么:
- 查询文本(例如,
SELECT * FROM users WHERE id = ?
) - 执行时间(例如,0.015s)
- 行数(例如,42)
注意:
- 可选:如果您不需要 DB 跟踪,请跳过。
- 仅 PdoWrapper:核心 PDO 尚未钩住——敬请期待!
- 性能警告:在 DB 密集型站点记录每个查询可能会减慢速度。使用采样(
$Apm = new Apm($ApmLogger, 0.1)
)来减轻负载。
示例输出:
- 查询:
SELECT name FROM products WHERE price > 100
- 时间:0.023s
- 行:15
Worker 选项
根据您的喜好调整 Worker:
--timeout 300
:5 分钟后停止——适合测试。--max_messages 500
:上限 500 个指标——保持有限。--batch_size 200
:一次处理 200 个——平衡速度和内存。--daemon
:不间断运行——适合实时监控。
示例:
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 天的数据。
故障排除
卡住了?试试这些:
-
没有仪表板数据?
- Worker 运行了吗?检查
ps aux | grep apm:worker
。 - 配置路径匹配吗?验证
.runway-config.json
DSN 指向真实文件。 - 手动运行
php vendor/bin/runway apm:worker
来处理待处理指标。
- Worker 运行了吗?检查
-
Worker 错误?
- 窥视您的 SQLite 文件(例如,
sqlite3 /tmp/apm_metrics.sqlite "SELECT * FROM apm_metrics_log LIMIT 5"
)。 - 检查 PHP 日志以获取堆栈跟踪。
- 窥视您的 SQLite 文件(例如,
-
仪表板无法启动?
- 端口 8001 被占用?使用
--port 8080
。 - PHP 未找到?使用
--php-path /usr/bin/php
。 - 防火墙阻塞?打开端口或使用
--host localhost
。
- 端口 8001 被占用?使用
-
太慢?
- 降低采样率:
$Apm = new Apm($ApmLogger, 0.05)
(5%)。 - 减少批次大小:
--batch_size 20
。
- 降低采样率:
-
未跟踪异常/错误?
- 如果您为项目启用了 Tracy,它将覆盖 Flight 的错误处理。您需要禁用 Tracy,然后确保
Flight::set('flight.handle_errors', true);
已设置。
- 如果您为项目启用了 Tracy,它将覆盖 Flight 的错误处理。您需要禁用 Tracy,然后确保
-
未跟踪数据库查询?
- 确保您使用
PdoWrapper
进行数据库连接。 - 确保构造函数中的最后一个参数为
true
。
- 确保您使用
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);
}
有用提示
当您调试代码时,有一些非常有用的函数可以为您输出数据。
bdump($var)
- 这将在单独的面板中将变量转储到 Tracy Bar 中。dumpe($var)
- 这将转储变量,然后立即终止。
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 $value
或 field NOT LIKE $value
$user->like('name', 'de')->find();
in(string $field, array $values) / notIn(string $field, array $values)
其中 field IN($value)
或 field NOT IN($value)
$user->in('id', [1, 2])->find();
between(string $field, array $values)
其中 field BETWEEN $value AND $value1
$user->between('id', [1, 2])->find();
OR 条件
可以将您的条件包装在 OR 语句中。这是通过使用 startWrap()
和 endWrap()
方法或通过在字段和值之后填充条件的第三个参数来完成的。
// 方法 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 beautify
和 composer phpcs
来修复任何语法错误。
许可证
MIT
Awesome-plugins/latte
Latte
Latte 是一个功能齐全的模板引擎,使用起来非常简单,其语法比 Twig 或 Smarty 更接近 PHP。它也非常容易扩展,可以添加自己的过滤器和函数。
安装
使用 Composer 安装。
composer require latte/latte
基本配置
有一些基本的配置选项来开始使用。您可以在 Latte 文档 中阅读更多相关信息。
require 'vendor/autoload.php';
$app = Flight::app();
$app->map('render', function(string $template, array $data, ?string $block): void {
$latte = new Latte\Engine;
// 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">
© 版权所有
</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 文档。
- FlightPHP OpenAPI Generator - 由 Daniel Schreiber 撰写的博客文章,介绍如何使用 OpenAPI 规范与 FlightPHP 结合,以 API 优先方法构建您的 API。
- SwaggerUI - Swagger UI 是一个优秀的工具,可帮助您为 Flight 项目生成 API 文档。它非常易用,并且可以自定义以满足您的需求。这是用于生成 Swagger 文档的 PHP 库。
应用程序性能监控 (APM)
应用程序性能监控 (APM) 对任何应用程序都至关重要。它帮助您了解应用程序的性能以及瓶颈所在。有许多 APM 工具可与 Flight 一起使用。
- official flightphp/apm - Flight APM 是一个简单的 APM 库,可用于监控您的 Flight 应用程序。它可用于监控应用程序性能并帮助您识别瓶颈。
异步
Flight 已经是一个快速的框架,但为其添加涡轮引擎会让一切变得更有趣(且具挑战性)!
- flightphp/async - 官方 Flight Async 库。此库是一种简单的方式,可为您的应用程序添加异步处理。它在底层使用 Swoole/Openswoole,提供简单有效的任务异步运行方式。
授权/权限
授权和权限对任何需要控制访问权限的应用程序都至关重要。
- official flightphp/permissions - 官方 Flight Permissions 库。此库是一种简单的方式,可为您的应用程序添加用户和应用程序级别的权限。
缓存
缓存是加速应用程序的绝佳方式。有许多缓存库可与 Flight 一起使用。
- official flightphp/cache - 轻量、简单且独立的 PHP 文件内缓存类
CLI
CLI 应用程序是与您的应用程序交互的绝佳方式。您可以使用它们生成控制器、显示所有路由等。
- official flightphp/runway - Runway 是一个 CLI 应用程序,帮助您管理 Flight 应用程序。
Cookies
Cookies 是存储客户端小量数据的绝佳方式。它们可用于存储用户偏好、应用程序设置等。
- overclokk/cookie - PHP Cookie 是一个 PHP 库,提供简单有效的 cookies 管理方式。
调试
在本地环境中开发时,调试至关重要。有几个插件可以提升您的调试体验。
- tracy/tracy - 这是一个功能齐全的错误处理程序,可与 Flight 一起使用。它有多个面板可帮助您调试应用程序。它也非常易于扩展并添加您自己的面板。
- official flightphp/tracy-extensions - 与 Tracy 错误处理程序一起使用,此插件添加了一些额外的面板,专门帮助调试 Flight 项目。
数据库
数据库是大多数应用程序的核心。这是存储和检索数据的方式。有些数据库库只是用于编写查询的包装器,有些则是完整的 ORM。
- official flightphp/core PdoWrapper - 官方 Flight PDO Wrapper,是核心的一部分。这是一个简单的包装器,帮助简化编写和执行查询的过程。它不是 ORM。
- official flightphp/active-record - 官方 Flight ActiveRecord ORM/Mapper。优秀的库,用于轻松检索和存储数据库中的数据。
- byjg/php-migration - 用于跟踪项目所有数据库变更的插件。
加密
加密对任何存储敏感数据的应用程序都至关重要。加密和解密数据并不太难,但正确存储加密密钥 可能 会 困难。最重要的是,切勿将加密密钥存储在公共目录中或提交到代码仓库。
- defuse/php-encryption - 这是一个可用于加密和解密数据的库。启动并运行非常简单,即可开始加密和解密数据。
作业队列
作业队列对于异步处理任务非常有帮助。这可以包括发送电子邮件、处理图像或任何不需要实时完成的任务。
- n0nag0n/simple-job-queue - Simple Job Queue 是一个库,可用于异步处理作业。它可与 beanstalkd、MySQL/MariaDB、SQLite 和 PostgreSQL 一起使用。
会话
会话对于 API 并不太有用,但对于构建 Web 应用程序,会话对于维护状态和登录信息至关重要。
- official flightphp/session - 官方 Flight Session 库。这是一个简单的会话库,可用于存储和检索会话数据。它使用 PHP 内置的会话处理。
- Ghostff/Session - PHP Session Manager(非阻塞、闪存、分段、会话加密)。使用 PHP open_ssl 进行可选的会话数据加密/解密。
模板
模板是任何具有 UI 的 Web 应用程序的核心。有许多模板引擎可与 Flight 一起使用。
- deprecated flightphp/core View - 这是一个非常基本的模板引擎,是核心的一部分。如果您的项目有超过几页,不推荐使用。
- latte/latte - Latte 是一个功能齐全的模板引擎,非常易用,其语法比 Twig 或 Smarty 更接近 PHP。它也非常易于扩展并添加您自己的过滤器和函数。
WordPress 集成
想在您的 WordPress 项目中使用 Flight 吗?有一个方便的插件可供使用!
- n0nag0n/wordpress-integration-for-flight-framework - 此 WordPress 插件允许您在 WordPress 旁边运行 Flight。它非常适合使用 Flight 框架为您的 WordPress 站点添加自定义 API、微服务,甚至完整应用程序。如果您想兼得两者的优点,这超级有用!
贡献
有一个您想分享的插件吗?提交拉取请求将其添加到列表中!
Media
媒体
我们已经尽力追踪互联网上与 Flight 相关的各种媒体类型。请参阅以下不同资源,您可以使用这些资源来了解更多关于 Flight 的信息。
文章和评述
- Unit Testing and SOLID Principles 作者:Brian Fenton (2015?)
- PHP Web Framework Flight 作者:ojambo (2025)
- Define, Generate, and Implement: An API-First Approach with OpenAPI Generator and FlightPHP 作者:Daniel Schreiber (2025)
- Best PHP Micro Frameworks for 2024 作者:n0nag0n (2024)
- Creating a RESTful API with Flight Framework 作者:n0nag0n (2024)
- Building a Simple Blog with Flight Part 2 作者:n0nag0n (2024)
- Building a Simple Blog with Flight Part 1 作者:n0nag0n (2024)
- 🚀 Build a Simple CRUD API in PHP with the Flight Framework 作者:soheil-khaledabadi (2024)
- Building a PHP Web Application with the Flight Micro-framework 作者:Arthur C. Codex (2023)
- Best PHP Frameworks for Web Development in 2024 作者:Ravikiran A S (2023)
- Top 12 PHP Frameworks: A Comprehensive Guide for 2023 作者:marketing kbk (2023)
- 5 PHP Frameworks You've (Probably) Never Heard of 作者:n0nag0n (2022)
- 12 top PHP frameworks for web developers to consider in 2023 作者:Anna Monus (2022)
- The Best PHP Microframeworks on a Cloud Server 作者:Shahzeb Ahmed (2021)
- PHP framework: Top 15 powerful ones for your web development 作者:AHT Tech (2020)
- Easy PHP Routing with FlightPHP 作者:Lucas Conceição (2019)
- Trying Out New PHP Framework (Flight) 作者:Leon (2017)
- Setting up FlightPHP to work with Backbonejs 作者:Timothy Tocci (2015)
视频和教程
- Build a Flight PHP App with MVC & MariaDB in 10 Minutes! (Beginner Friendly) 作者:ojamboshop (2025)
- Create a REST API for IoT Devices Using PHP & FlightPHP - ESP32 API 作者:IoT Craft Hub (2024)
- PHP Flight Framework Simple Introductory Video 作者:n0nag0n (2024)
- Set header HTTP code in Flightphp (3 Solutions!!) 作者:Roel Van de Paar (2024)
- PHP Flight Framework Tutorial. Super easy API Project! 作者:n0nag0n (2022)
- Aplicación web CRUD con php y mysql y bootstrap usando flight 作者:Devlopteca - Oscar Uh (2021)
- DevOps & SysAdmins: Lighttpd rewrite rule for Flight PHP microframework 作者:Roel Van de Paar (2021)
- Tutorial REST API Flight PHP #PART2 INSERT TABLE Info #Code (Tagalog) 作者:Info Singkat Official (2020)
- Tutorial REST API Flight PHP #PART1 Info #Code (Tagalog) 作者:Info Singkat Official (2020)
- How To Create JSON REST API IN PHP - Part 2 作者:Codewife (2018)
- How To Create JSON REST API IN PHP - Part 1 作者:Codewife (2018)
- Teste Micro Frameworks PHP - Flight PHP, Lumen, Slim 3 e Laravel 作者:Codemarket (2016)
- Tutorial 1 Flight PHP - Instalación 作者:absagg (2014)
- Tutorial 2 Flight PHP - Route parte 1 作者:absagg (2014)
缺少什么吗?
我们是否遗漏了您撰写或录制的任何内容?请通过 issue 或 pull request 告知我们!
Examples
需要快速开始吗?
您有两种选项来开始一个新的 Flight 项目:
- Full Skeleton Boilerplate:一个更完整的示例,包括控制器和视图。
- Single File Skeleton Boilerplate:一个单一文件,包含运行您的应用程序所需的一切,简单易用。
社区贡献的示例:
- flightravel:FlightPHP 与 Laravel 目录结合,带有 PHP 工具 + GH Actions
- fleact - 一个带有 ReactJS 集成的 FlightPHP 启动套件。
- flastro - 一个带有 Astro 集成的 FlightPHP 启动套件。
- velt - Velt 是一个快速易用的 Svelte 启动模板,带有 FlightPHP 后端。
需要一些灵感吗?
虽然这些并非由 Flight 团队正式赞助,但它们可以为您构建基于 Flight 的项目提供结构思路!
- Ivox Car Rental - Ivox Car Rental 是一个单页、移动友好的汽车租赁 Web 应用程序,使用 PHP (FlightPHP)、JavaScript 和 MySQL 构建。它支持用户注册、浏览和预订汽车,而管理员可以管理汽车、用户和预订。该应用程序具有 REST API、JWT 认证和响应式设计,提供现代租赁体验。
- Decay - Flight v3 与 HTMX 和 SleekDB 结合,全是关于僵尸的!(Demo)
- Flight Example Blog - Flight v3 带有中间件、控制器、Active Record 和 Latte。
- Flight CRUD RESTful API - 使用 Flight 框架的简单 CRUD API 项目,为新用户提供基本结构,快速设置带有 CRUD 操作和数据库连接的 PHP 应用程序。该项目演示了如何使用 Flight 进行 RESTful API 开发,是初学者理想的学习工具,也是经验丰富的开发者的有用启动套件。
- Flight School Management System - Flight v3
- Paste Bin with Comments - Flight v3
- Basic Skeleton App
- Example Wiki
- The IT-Innovator PHP Framework Application
- LittleEducationalCMS (Spanish)
- Italian Yellow Pages API
- Generic Content Management System (with....very little documentation)
- A tiny php framework based on Flight and medoo.
- Example MVC Application
- Production ready Flight Boilerplate - 生产就绪的认证框架,可节省数周开发时间。具有企业级安全功能:2FA/TOTP、LDAP 集成、Azure SSO、智能速率限制、会话指纹识别、暴力破解保护、安全分析仪表板、全面审计日志以及细粒度基于角色的访问控制。
想分享您自己的示例吗?
如果您有一个想分享的项目,请提交拉取请求将其添加到此列表中!
Install/install
安装说明
在安装 Flight 之前,需要满足一些基本先决条件。具体来说,您需要:
- 在您的系统上安装 PHP
- 安装 Composer 以获得最佳的开发者体验。
基本安装
如果您使用 Composer,可以运行以下命令:
composer require flightphp/core
这只会将 Flight 核心文件安装到您的系统上。您需要定义项目结构、布局、依赖项、配置、自动加载 等。此方法确保除了 Flight 之外不会安装其他依赖项。
您也可以直接下载文件 并将它们解压到您的 Web 目录中。
推荐安装
强烈推荐为任何新项目从 flightphp/skeleton 应用开始。安装非常简单。
composer create-project flightphp/skeleton my-project/
这将设置您的项目结构,使用命名空间配置自动加载,设置配置,并提供其他工具,如 Tracy、Tracy 扩展 和 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
-
安装 Homebrew(如果尚未安装):
- 打开终端并运行:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
- 打开终端并运行:
-
安装 PHP:
- 安装最新版本:
brew install php
- 要安装特定版本,例如 PHP 8.1:
brew tap shivammathur/php brew install shivammathur/php/php@8.1
- 安装最新版本:
-
在 PHP 版本之间切换:
- 取消链接当前版本并链接所需版本:
brew unlink php brew link --overwrite --force php@8.1
- 验证安装的版本:
php -v
- 取消链接当前版本并链接所需版本:
Windows 10/11
手动安装 PHP
-
下载 PHP:
- 访问 PHP for Windows 并下载最新版本或特定版本(例如,7.4、8.0)作为非线程安全 zip 文件。
-
解压 PHP:
- 将下载的 zip 文件解压到
C:\php
。
- 将下载的 zip 文件解压到
-
将 PHP 添加到系统 PATH:
- 转到 系统属性 > 环境变量。
- 在 系统变量 下,找到 Path 并点击 编辑。
- 添加路径
C:\php
(或您解压 PHP 的位置)。 - 点击 确定 关闭所有窗口。
-
配置 PHP:
- 将
php.ini-development
复制到php.ini
。 - 编辑
php.ini
以按需配置 PHP(例如,设置extension_dir
,启用扩展)。
- 将
-
验证 PHP 安装:
- 打开命令提示符并运行:
php -v
- 打开命令提示符并运行:
安装多个 PHP 版本
-
为每个版本重复上述步骤,将每个版本放置在单独的目录中(例如,
C:\php7
、C:\php8
)。 -
通过调整系统 PATH 变量指向所需版本目录在版本之间切换。
Ubuntu (20.04, 22.04 等)
使用 apt 安装 PHP
-
更新软件包列表:
- 打开终端并运行:
sudo apt update
- 打开终端并运行:
-
安装 PHP:
- 安装最新 PHP 版本:
sudo apt install php
- 要安装特定版本,例如 PHP 8.1:
sudo apt install php8.1
- 安装最新 PHP 版本:
-
安装附加模块(可选):
- 例如,要安装 MySQL 支持:
sudo apt install php8.1-mysql
- 例如,要安装 MySQL 支持:
-
在 PHP 版本之间切换:
- 使用
update-alternatives
:sudo update-alternatives --set php /usr/bin/php8.1
- 使用
-
验证安装的版本:
- 运行:
php -v
- 运行:
Rocky Linux
使用 yum/dnf 安装 PHP
-
启用 EPEL 仓库:
- 打开终端并运行:
sudo dnf install epel-release
- 打开终端并运行:
-
安装 Remi's 仓库:
- 运行:
sudo dnf install https://rpms.remirepo.net/enterprise/remi-release-8.rpm sudo dnf module reset php
- 运行:
-
安装 PHP:
- 要安装默认版本:
sudo dnf install php
- 要安装特定版本,例如 PHP 7.4:
sudo dnf module install php:remi-7.4
- 要安装默认版本:
-
在 PHP 版本之间切换:
- 使用
dnf
模块命令:sudo dnf module reset php sudo dnf module enable php:remi-8.0 sudo dnf install php
- 使用
-
验证安装的版本:
- 运行:
php -v
- 运行:
一般说明
- 对于开发环境,按照项目要求配置 PHP 设置非常重要。
- 在切换 PHP 版本时,确保为您打算使用的特定版本安装所有相关 PHP 扩展。
- 在切换 PHP 版本或更新配置后,重启您的 Web 服务器(Apache、Nginx 等)以应用更改。
Guides
指南
Flight PHP 旨在简单却强大,我们的指南将帮助您构建真实世界的应用程序。这些实用教程将带您逐步完成完整项目,以演示如何有效使用 Flight。
官方指南
构建一个博客
了解如何使用 Flight PHP 创建一个功能齐全的博客应用程序。此指南将带您完成:
- 设置项目结构
- 使用 Latte 处理模板
- 为帖子实现路由
- 存储和检索数据
- 处理表单提交
- 基本错误处理
此教程非常适合初学者,他们希望看到所有组件如何在真实应用程序中组合在一起。
单元测试和 SOLID 原则
此指南涵盖了 Flight PHP 应用程序中单元测试的基础知识。它包括:
- 设置 PHPUnit
- 使用 SOLID 原则编写可测试代码
- 模拟依赖项
- 避免常见陷阱
- 随着应用程序增长扩展测试 此教程适合希望改进代码质量和可维护性的开发人员。
非官方指南
虽然这些指南不由 Flight 团队官方维护,但它们是社区创建的宝贵资源。它们涵盖了各种主题和用例,提供有关使用 Flight PHP 的额外见解。
使用 Flight 框架创建 RESTful API
此指南将带您创建使用 Flight PHP 框架的 RESTful API。它涵盖了设置 API 的基础知识、定义路由以及返回 JSON 响应。
构建一个简单博客
此指南将带您使用 Flight PHP 框架创建基本博客。它实际上分为两个部分:一个涵盖基础知识,另一个涵盖更高级主题和生产就绪博客的优化。
- 使用 Flight 构建简单博客 - 第 1 部分 - 开始一个简单博客。
- 使用 Flight 构建简单博客 - 第 2 部分 - 为生产优化博客。
在 PHP 中构建 Pokémon API:初学者指南
此有趣的指南将带您使用 Flight PHP 创建一个简单 Pokémon API。它涵盖了设置 API 的基础知识、定义路由以及返回 JSON 响应。
贡献
有指南idea?发现错误?我们欢迎贡献!我们的指南在 FlightPHP 文档仓库 中维护。
如果您使用 Flight 构建了一些有趣的东西并希望作为指南分享,请提交拉取请求。分享您的知识有助于 Flight 社区成长。
寻找 API 文档?
如果您正在寻找有关 Flight 核心功能和方法的特定信息,请查看我们文档的学习部分。