Learn
了解 Flight
Flight 是一个快速、简单、可扩展的 PHP 框架。它非常灵活,可用于构建任何类型的 веб 应用程序。它以简洁为设计理念,编写方式易于理解和使用。
重要框架概念
为什么选择框架?
这里有一篇关于为什么你应该使用框架的简短文章。在开始使用框架之前,理解使用框架的好处是一个好主意。
此外,@lubiana 创建了一篇出色的教程。虽然它并没有详细讨论 Flight,但这本指南将帮助你理解围绕框架的一些主要概念以及它们为何具有使用价值。你可以在这里找到该教程。
Flight 与其他框架的比较
如果你正从其他框架(例如 Laravel、Slim、Fat-Free 或 Symfony)迁移到 Flight,本页面将帮助你了解两者之间的区别。
核心主题
自动加载
学习如何在你的应用程序中自动加载自己的类。
路由
学习如何管理你的 веб 应用程序的路由。这还包括路由分组、路由参数和中间件。
中间件
学习如何使用中间件过滤应用程序中的请求和响应。
请求
学习如何处理应用程序中的请求和响应。
响应
学习如何向用户发送响应。
事件
学习如何使用事件系统向你的应用程序添加自定义事件。
HTML 模板
学习如何使用内置视图引擎渲染你的 HTML 模板。
安全
学习如何保护你的应用程序免受常见安全威胁。
配置
学习如何为你的应用程序配置框架。
扩展 Flight
学习如何扩展框架,添加你自己的方法和类。
事件和过滤
学习如何使用事件系统为你的方法和内部框架方法添加钩子。
依赖注入容器
学习如何使用依赖注入容器 (DIC) 来管理你的应用程序的依赖关系。
框架 API
了解框架的核心方法。
迁移到 v3
向后兼容性在大多数情况下得以维护,但在从 v2 迁移到 v3 时,有一些变化是你需要注意的。
故障排除
在使用 Flight 时,可能会遇到一些常见问题。本页面将帮助你解决这些问题。
Learn/stopping
停止
您可以通过调用 halt
方法在任何时候停止框架:
Flight::halt();
您还可以指定一个可选的 HTTP
状态码和消息:
Flight::halt(200, '马上回来...');
调用 halt
将丢弃直到该点的任何响应内容。如果您想要停止框架并输出当前响应,请使用 stop
方法:
Flight::stop();
Learn/errorhandling
错误处理
错误和异常
所有错误和异常都会被 Flight 捕获并传递给error
方法。
默认行为是发送一个通用的HTTP 500 内部服务器错误
响应,带有一些错误信息。
您可以根据自己的需求覆盖此行为:
Flight::map('error', function (Throwable $error) {
// 处理错误
echo $error->getTraceAsString();
});
默认情况下,错误不会记录到 web 服务器。您可以通过更改配置来启用此功能:
Flight::set('flight.log_errors', true);
未找到
当 URL 找不到时,Flight 调用notFound
方法。默认行为是发送一个HTTP 404 未找到
响应,带有一个简单消息。
您可以根据自己的需求覆盖此行为:
Flight::map('notFound', function () {
// 处理未找到
});
Learn/flight_vs_laravel
Flight vs Laravel
什么是 Laravel?
Laravel 是一个全功能的框架,拥有一切必需的功能,以及一个令人惊叹的开发者专注生态系统, 但在性能和复杂性方面有一定代价。 Laravel 的目标是使开发者能够拥有最高水平的生产力,并使常见任务变得简单。 对于那些希望构建一个全功能企业 Web 应用程序的开发者来说,Laravel 是一个很好的选择。 但在性能和复杂性方面存在一些折衷。学习 Laravel 的基础知识可能很容易,但要熟练使用该框架可能需要一些时间。
开发者也经常感觉到,有许多 Laravel 模块,实际上可以通过使用另一个库或编写自己的代码来解决问题。
与 Flight 相比的优点
- Laravel 拥有 庞大的生态系统,其中包括开发者和模块,可用于解决常见问题。
- Laravel 拥有一套全功能的 ORM,可用于与数据库交互。
- Laravel 拥有大量文档和教程,可用于学习该框架。
- Laravel 拥有内置的身份验证系统,可用于保护应用程序。
- Laravel 拥有播客、会议、见面会、视频和其他资源,可用于学习该框架。
- Laravel 面向富有经验的开发者,他们希望构建一个全功能的企业 Web 应用程序。
与 Flight 相比的缺点
- Laravel 的内部复杂度远远超过了 Flight。这导致性能开销巨大。参见 TechEmpower benchmarks 了解更多信息。
- Flight 面向寻求构建轻量级、快速、易于使用的 Web 应用程序的开发者。
- Flight 专注于简单性和易用性。
- Flight 的核心特点之一是它尽最大努力保持向后兼容性。Laravel 导致了大量的困扰 在主要版本之间发生了变化。
- Flight 面向第一次涉足框架领域的开发者。
- Flight 没有依赖性,而 Laravel 有大量依赖。
- Flight 也可以处理企业级应用程序,但比 Laravel 的样板代码要少。 这也将需要开发者更多的纪律来保持组织良好和结构完整。
- Flight 为开发者提供了更多对应用程序的控制权,而 Laravel 在幕后处理了大量的魔术,可能会令人沮丧。
Learn/migrating_to_v3
升级到 v3
在很大程度上保持了向后兼容性,但在从 v2 迁移到 v3 时有一些更改是您应该注意的。
输出缓冲行为 (3.5.0)
输出缓冲 是指 PHP 脚本生成的输出存储在一个缓冲区(PHP 内部)中,然后再发送给客户端。这允许您在发送给客户端之前修改输出。
在 MVC 应用程序中,控制器是“管理者”,它管理视图的操作。在控制器之外生成输出(或在 Flight 的情况下有时是匿名函数)会破坏 MVC 模式。这种更改是为了更符合 MVC 模式,并使框架更加可预测和易于使用。
在 v2 中,输出缓冲处理方式是不一致关闭自己的输出缓冲区,这使得单元测试 和流处理更加困难。对大多数用户来说,这种更改实际上可能不会影响您。但是,如果您在可调用函数和控制器之外输出内容(例如在钩子中),您可能会遇到问题。在钩子中输出内容,并在框架实际执行之前,以前可能有效,但在向前移动时将无效。
您可能会遇到问题的地方
// index.php
require 'vendor/autoload.php';
// just an example
define('START_TIME', microtime(true));
function hello() {
echo 'Hello World';
}
Flight::map('hello', 'hello');
Flight::after('hello', function(){
// this will actually be fine
echo '<p>This Hello World phrase was brought to you by the letter "H"</p>';
});
Flight::before('start', function(){
// things like this will cause an error
echo '<html><head><title>My Page</title></head><body>';
});
Flight::route('/', function(){
// this is actually just fine
echo 'Hello World';
// This should be just fine as well
Flight::hello();
});
Flight::after('start', function(){
// this will cause an error
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(){
// Now this will be just fine
echo '<html><head><title>My Page</title></head><body>';
});
// more code
调度器更改 (3.7.0)
如果您直接调用 Dispatcher
的静态方法,例如 Dispatcher::invokeMethod()
、Dispatcher::execute()
等,您需要更新代码以避免直接调用这些方法。Dispatcher
已经转换为更符合面向对象的方式,因此可以更轻松地使用依赖注入容器。如果需要调用类似于 Dispatcher 的方法,您可以手动使用类似于 $result = $class->$method(...$params);
或 call_user_func_array()
的方式。
halt()
stop()
redirect()
和 error()
更改 (3.10.0)
在 3.10.0 之前的默认行为是清除标头和响应主体。这已更改为仅清除响应主体。如果您需要清除标头,您可以使用 Flight::response()->clear()
。
Learn/configuration
配置
您可以通过使用set
方法设置配置值来自定义Flight的某些行为。
Flight::set('flight.log_errors', true);
可用配置设置
以下是所有可用配置设置的列表:
- flight.base_url
?string
- 覆盖请求的基本URL。 (默认值: null) - flight.case_sensitive
bool
- URL的区分大小写匹配。 (默认值: false) - flight.handle_errors
bool
- 允许Flight在内部处理所有错误。 (默认值: true) - flight.log_errors
bool
- 将错误记录到Web服务器的错误日志文件中。 (默认值: false) - flight.views.path
string
- 包含视图模板文件的目录。 (默认值: ./views) - flight.views.extension
string
- 视图模板文件扩展名。 (默认值: .php) - flight.content_length
bool
- 设置Content-Length
头。 (默认值: true) - flight.v2.output_buffering
bool
- 使用传统的输出缓冲。查看 迁移到v3。 (默认值: false)
加载器配置
此外,加载器还有另一个配置设置。这将允许您自动加载带有类名中的_
的类。
// 启用带下划线的类加载
// 默认为true
Loader::$v2ClassLoading = false;
变量
Flight允许您保存变量,以便可以在应用程序的任何位置使用它们。
// 保存您的变量
Flight::set('id', 123);
// 在应用程序的其他位置
$id = Flight::get('id');
要查看变量是否已设置,您可以执行以下操作:
if (Flight::has('id')) {
// 做某事
}
您可以通过执行以下操作来清除变量:
// 清除id变量
Flight::clear('id');
// 清除所有变量
Flight::clear();
Flight还使用变量进行配置目的。
Flight::set('flight.log_errors', true);
错误处理
错误和异常
Flight会捕获所有错误和异常,并将它们传递给error
方法。默认行为是发送通用的HTTP 500 Internal Server Error
响应以及一些错误信息。
您可以根据自己的需求覆盖此行为:
Flight::map('error', function (Throwable $error) {
// 处理错误
echo $error->getTraceAsString();
});
默认情况下,错误不会记录到Web服务器。您可以通过更改配置来启用此功能:
Flight::set('flight.log_errors', true);
未找到
当无法找到URL时,Flight会调用notFound
方法。默认行为是发送一个带有简单消息的HTTP 404 Not Found
响应。
您可以根据自己的需求覆盖此行为:
Flight::map('notFound', function () {
// 处理未找到
});
Learn/security
安全性
在网络应用程序方面,安全性是一个大问题。您希望确保您的应用程序是安全的,并且用户的数据是安全的。Flight 提供了一系列功能以帮助您保护您的网络应用程序。
头部
HTTP 头部是保护您的网络应用程序的最简单方法之一。您可以使用头部来防止点击劫持、XSS 和其他攻击。有几种方法可以将这些头部添加到您的应用程序中。
两个检查您头部安全性的网站是 securityheaders.com 和 observatory.mozilla.org。
手动添加
您可以通过在 Flight\Response
对象上使用 header
方法手动添加这些头部。
// 设置 X-Frame-Options 头部以防止点击劫持
Flight::response()->header('X-Frame-Options', 'SAMEORIGIN');
// 设置 Content-Security-Policy 头部以防止 XSS
// 注意:这个头部可能会变得非常复杂,您会希望
// 参考互联网上的示例以供您的应用程序使用
Flight::response()->header("Content-Security-Policy", "default-src 'self'");
// 设置 X-XSS-Protection 头部以防止 XSS
Flight::response()->header('X-XSS-Protection', '1; mode=block');
// 设置 X-Content-Type-Options 头部以防止 MIME 嗅探
Flight::response()->header('X-Content-Type-Options', 'nosniff');
// 设置 Referrer-Policy 头部以控制发送多少引用信息
Flight::response()->header('Referrer-Policy', 'no-referrer-when-downgrade');
// 设置 Strict-Transport-Security 头部以强制使用 HTTPS
Flight::response()->header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
// 设置 Permissions-Policy 头部以控制可以使用哪些功能和 API
Flight::response()->header('Permissions-Policy', 'geolocation=()');
可以将这些添加到您的 bootstrap.php
或 index.php
文件的开头。
作为过滤器添加
您还可以像下面这样在过滤器/钩子中添加它们:
// 在过滤器中添加头部
Flight::before('start', function() {
Flight::response()->header('X-Frame-Options', 'SAMEORIGIN');
Flight::response()->header("Content-Security-Policy", "default-src 'self'");
Flight::response()->header('X-XSS-Protection', '1; mode=block');
Flight::response()->header('X-Content-Type-Options', 'nosniff');
Flight::response()->header('Referrer-Policy', 'no-referrer-when-downgrade');
Flight::response()->header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
Flight::response()->header('Permissions-Policy', 'geolocation=()');
});
作为中间件添加
您还可以将它们添加为中间件类。这是一种保持代码简洁和有序的好方法。
// app/middleware/SecurityHeadersMiddleware.php
namespace app\middleware;
class SecurityHeadersMiddleware
{
public function before(array $params): void
{
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=()');
}
}
// index.php 或您拥有路由的地方
// 仅供参考,这个空字符串组作为所有路由的全局中间件。
// 当然您可以做同样的事情,只将其添加到特定路由。
Flight::group('', function(Router $router) {
$router->get('/users', [ 'UserController', 'getUsers' ]);
// 更多路由
}, [ new SecurityHeadersMiddleware() ]);
跨站请求伪造 (CSRF)
跨站请求伪造 (CSRF) 是一种攻击方式,其中恶意网站可以使用户的浏览器向您的网站发送请求。这可以在用户不知情的情况下在您的网站上执行操作。Flight 不提供内置的 CSRF 保护机制,但您可以通过使用中间件轻松实现自己的保护。
设置
首先,您需要生成一个 CSRF 令牌并将其存储在用户的会话中。然后,您可以在您的表单中使用此令牌,并在表单提交时检查它。
// 生成 CSRF 令牌并将其存储在用户的会话中
// (假设您已在 Flight 中创建了一个会话对象并将其附加)
// 有关更多信息,请参见会话文档
Flight::register('session', \Ghostff\Session\Session::class);
// 您只需为每个会话生成一个令牌(以便它在多个选项卡和请求中均能工作)
if(Flight::session()->get('csrf_token') === null) {
Flight::session()->set('csrf_token', bin2hex(random_bytes(32)) );
}
<!-- 在您的表单中使用 CSRF 令牌 -->
<form method="post">
<input type="hidden" name="csrf_token" value="<?= Flight::session()->get('csrf_token') ?>">
<!-- 其他表单字段 -->
</form>
使用 Latte
您还可以设置一个自定义函数,在您的 Latte 模板中输出 CSRF 令牌。
// 设置一个自定义函数以输出 CSRF 令牌
// 注意:视图已配置使用 Latte 作为视图引擎
Flight::view()->addFunction('csrf', function() {
$csrfToken = Flight::session()->get('csrf_token');
return new \Latte\Runtime\Html('<input type="hidden" name="csrf_token" value="' . $csrfToken . '">');
});
现在在您的 Latte 模板中,可以使用 csrf()
函数输出 CSRF 令牌。
<form method="post">
{csrf()}
<!-- 其他表单字段 -->
</form>
简单明了吧?
检查 CSRF 令牌
您可以使用事件过滤器检查 CSRF 令牌:
// 这个中间件检查请求是否为 POST 请求,如果是,则检查 CSRF 令牌是否有效
Flight::before('start', function() {
if(Flight::request()->method == 'POST') {
// 从表单值中获取 csrf 令牌
$token = Flight::request()->data->csrf_token;
if($token !== Flight::session()->get('csrf_token')) {
Flight::halt(403, '无效的 CSRF 令牌');
// 或者用于 JSON 响应
Flight::jsonHalt(['error' => '无效的 CSRF 令牌'], 403);
}
}
});
或者,您可以使用中间件类:
// app/middleware/CsrfMiddleware.php
namespace app\middleware;
class CsrfMiddleware
{
public function before(array $params): void
{
if(Flight::request()->method == 'POST') {
$token = Flight::request()->data->csrf_token;
if($token !== Flight::session()->get('csrf_token')) {
Flight::halt(403, '无效的 CSRF 令牌');
}
}
}
}
// index.php 或您拥有路由的地方
Flight::group('', function(Router $router) {
$router->get('/users', [ 'UserController', 'getUsers' ]);
// 更多路由
}, [ new CsrfMiddleware() ]);
跨站脚本攻击 (XSS)
跨站脚本攻击 (XSS) 是一种攻击,其中恶意网站可以将代码注入到您的网站中。这些机会大多来自于您的终端用户填写的表单值。您绝对不应信任用户的输出!始终假设他们都是世界上最优秀的黑客。他们可以将恶意 JavaScript 或 HTML 注入到您的页面中。此代码可用于窃取用户的信息或在您的网站上执行操作。通过使用 Flight 的视图类,您可以轻松转义输出以防止 XSS 攻击。
// 假设用户聪明到尝试用这个作为他们的名字
$name = '<script>alert("XSS")</script>';
// 这将转义输出
Flight::view()->set('name', $name);
// 这将输出:<script>alert("XSS")</script>
// 如果您使用像 Latte 这样的视图类,它也会自动转义。
Flight::view()->render('template', ['name' => $name]);
SQL 注入
SQL 注入是一种攻击,其中恶意用户可以向您的数据库注入 SQL 代码。这可以用于窃取数据库中的信息或对数据库执行操作。同样,您绝对不应信任用户的输入!始终假设他们是怀着恶意的。您可以在 PDO
对象中使用预处理语句来防止 SQL 注入。
// 假设您已经将 Flight::db() 注册为您的 PDO 对象
$statement = Flight::db()->prepare('SELECT * FROM users WHERE username = :username');
$statement->execute([':username' => $username]);
$users = $statement->fetchAll();
// 如果您使用 PdoWrapper 类,那么可以轻松通过一行代码完成
$users = Flight::db()->fetchAll('SELECT * FROM users WHERE username = :username', [ 'username' => $username ]);
// 使用带有 ? 占位符的 PDO 对象也可以做到同样的事情
$statement = Flight::db()->fetchAll('SELECT * FROM users WHERE username = ?', [ $username ]);
// 只是请答应我永远不要做这样的事情...
$users = Flight::db()->fetchAll("SELECT * FROM users WHERE username = '{$username}' LIMIT 5");
// 因为如果 $username = "' OR 1=1; -- ";
// 查询构建后看起来像这样
// SELECT * FROM users WHERE username = '' OR 1=1; -- LIMIT 5
// 它看起来很奇怪,但这是一个有效的查询,可以工作。实际上,
// 这是一个非常常见的 SQL 注入攻击,将返回所有用户。
跨源资源共享 (CORS)
跨源资源共享 (CORS) 是一种机制,允许在网页上请求来自另一个域的多个资源(如字体、JavaScript 等),这些资源来自原始域之外。Flight 没有内置的功能,但可以通过在调用 Flight::start()
方法之前运行的钩子轻松处理。
// app/utils/CorsUtil.php
namespace app\utils;
class CorsUtil
{
public function set(array $params): void
{
$request = Flight::request();
$response = Flight::response();
if ($request->getVar('HTTP_ORIGIN') !== '') {
$this->allowOrigins();
$response->header('Access-Control-Allow-Credentials', 'true');
$response->header('Access-Control-Max-Age', '86400');
}
if ($request->method === 'OPTIONS') {
if ($request->getVar('HTTP_ACCESS_CONTROL_REQUEST_METHOD') !== '') {
$response->header(
'Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD'
);
}
if ($request->getVar('HTTP_ACCESS_CONTROL_REQUEST_HEADERS') !== '') {
$response->header(
"Access-Control-Allow-Headers",
$request->getVar('HTTP_ACCESS_CONTROL_REQUEST_HEADERS')
);
}
$response->status(200);
$response->send();
exit;
}
}
private function allowOrigins(): void
{
// 在这里自定义您的允许主机。
$allowed = [
'capacitor://localhost',
'ionic://localhost',
'http://localhost',
'http://localhost:4200',
'http://localhost:8080',
'http://localhost:8100',
];
$request = Flight::request();
if (in_array($request->getVar('HTTP_ORIGIN'), $allowed, true) === true) {
$response = Flight::response();
$response->header("Access-Control-Allow-Origin", $request->getVar('HTTP_ORIGIN'));
}
}
}
// index.php 或您拥有路由的地方
$CorsUtil = new CorsUtil();
// 这需要在 start 运行之前执行。
Flight::before('start', [ $CorsUtil, 'setupCors' ]);
错误处理
在生产环境中隐藏敏感的错误详情,以避免向攻击者泄露信息。
// 在您的 bootstrap.php 或 index.php 中
// 在 flightphp/skeleton 中,这位于 app/config/config.php
$environment = ENVIRONMENT;
if ($environment === 'production') {
ini_set('display_errors', 0); // 禁用错误显示
ini_set('log_errors', 1); // 记录错误
ini_set('error_log', '/path/to/error.log');
}
// 在您的路由或控制器中
// 使用 Flight::halt() 进行控制的错误响应
Flight::halt(403, '拒绝访问');
输入清理
绝对不要信任用户输入。在处理之前进行清理,以防止恶意数据潜入。
// 假设一个包含 $_POST['input'] 和 $_POST['email'] 的 $_POST 请求
// 清理字符串输入
$clean_input = filter_var(Flight::request()->data->input, FILTER_SANITIZE_STRING);
// 清理电子邮件
$clean_email = filter_var(Flight::request()->data->email, FILTER_SANITIZE_EMAIL);
密码哈希
安全存储密码并使用 PHP 内置函数安全验证它们。
$password = Flight::request()->data->password;
// 存储时哈希密码(例如,在注册期间)
$hashed_password = password_hash($password, PASSWORD_DEFAULT);
// 验证密码(例如,在登录期间)
if (password_verify($password, $stored_hash)) {
// 密码匹配
}
速率限制
通过使用缓存来限制请求速率,以防止暴力攻击。
// 假设您已安装并注册 flightphp/cache
// 在中间件中使用 flightphp/cache
Flight::before('start', function() {
$cache = Flight::cache();
$ip = Flight::request()->ip;
$key = "rate_limit_{$ip}";
$attempts = (int) $cache->retrieve($key);
if ($attempts >= 10) {
Flight::halt(429, '请求过多');
}
$cache->set($key, $attempts + 1, 60); // 60 秒后重置
});
结论
安全性是一个大问题,确保您的网络应用程序安全非常重要。Flight 提供了一系列功能来帮助您保护网络应用程序,但始终保持警惕,确保您尽力保护用户的数据是非常重要的。始终假设最坏的情况,绝不要信任用户的输入。始终转义输出,使用预处理语句防止 SQL 注入。始终使用中间件保护您的路由免受 CSRF 和 CORS 攻击。如果您做到这些,您将朝着构建安全的网络应用程序的方向迈出坚实的步伐。
Learn/overriding
覆盖
Flight 允许您覆盖其默认功能以满足您自己的需求,而无需修改任何代码。
例如,当 Flight 无法将 URL 与路由匹配时,它会调用 notFound
方法,该方法发送一个通用的 HTTP 404
响应。您可以使用 map
方法覆盖此行为:
Flight::map('notFound', function() {
// 显示自定义 404 页面
include 'errors/404.html';
});
Flight 还允许您替换框架的核心组件。例如,您可以使用自定义类替换默认的 Router 类:
// 注册您的自定义类
Flight::register('router', MyRouter::class);
// 当 Flight 加载 Router 实例时,它将加载您的类
$myrouter = Flight::router();
但是,像 map
和 register
这样的框架方法不能被覆盖。如果您尝试这样做,将会收到错误提示。
Learn/routing
路由
注意: 想了解更多关于路由的内容吗?请查看"为什么选择框架?"页面以获取更深入的解释。
在 Flight 中,基本路由是通过将 URL 模式与回调函数或类和方法的数组匹配来完成的。
Flight::route('/', function(){
echo '你好,世界!';
});
路由按照定义的顺序进行匹配。第一个匹配请求的路由将被调用。
回调/函数
回调可以是任何可调用的对象。因此,您可以使用常规函数:
function hello() {
echo '你好,世界!';
}
Flight::route('/', 'hello');
类
您也可以使用类的静态方法:
class Greeting {
public static function hello() {
echo '你好,世界!';
}
}
Flight::route('/', [ 'Greeting','hello' ]);
或者先创建一个对象,然后调用该方法:
// Greeting.php
class Greeting
{
public function __construct() {
$this->name = '约翰·多';
}
public function hello() {
echo "你好,{$this->name}!";
}
}
// index.php
$greeting = new Greeting();
Flight::route('/', [ $greeting, 'hello' ]);
// 您也可以在未先创建对象的情况下执行此操作
// 注意:没有参数将被注入到构造函数
Flight::route('/', [ 'Greeting', 'hello' ]);
// 此外,您还可以使用更短的语法
Flight::route('/', 'Greeting->hello');
// 或者
Flight::route('/', Greeting::class.'->hello');
通过 DIC(依赖注入容器)进行依赖注入
如果您想通过容器(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 "你好,世界!我的名字是 {$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();
方法路由
默认情况下,路由模式会与所有请求方法进行匹配。您可以通过在 URL 之前添加标识符来响应特定方法。
Flight::route('GET /', function () {
echo '我收到了一个 GET 请求。';
});
Flight::route('POST /', function () {
echo '我收到了一个 POST 请求。';
});
// 您不能使用 Flight::get() 来创建路由,因为那是一个用于获取变量的方法,而不是创建路由。
// Flight::post('/', function() { /* 代码 */ });
// Flight::patch('/', function() { /* 代码 */ });
// Flight::put('/', function() { /* 代码 */ });
// Flight::delete('/', function() { /* 代码 */ });
您也可以通过使用 |
分隔符将多个方法映射到单个回调:
Flight::route('GET|POST /', function () {
echo '我收到了一个 GET 或 POST 请求。';
});
此外,您可以获取具有一些助手方法的 Router 对象:
$router = Flight::router();
// 映射所有方法
$router->map('/', function() {
echo '你好,世界!';
});
// GET 请求
$router->get('/users', function() {
echo '用户';
});
// $router->post();
// $router->put();
// $router->delete();
// $router->patch();
正则表达式
您可以在路由中使用正则表达式:
Flight::route('/user/[0-9]+', function () {
// 这将匹配 /user/1234
});
虽然此方法可用,但建议使用命名参数,或与正则表达式结合的命名参数,因为它们更具可读性且更易于维护。
命名参数
您可以在路由中指定命名参数,这些参数将传递给回调函数。这更是为了路由的可读性,而非其他原因。请参见下面关于重要注意事项的部分。
Flight::route('/@name/@id', function (string $name, string $id) {
echo "你好,$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 "你好,$name ($id)!";
});
如果您访问以下 URL:/bob/123
,输出将是 你好,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 () {
// 做一些事情
});
传递
您可以通过从回调函数返回 true
将执行权传递给下一个匹配路由。
Flight::route('/user/@name', function (string $name) {
// 检查某个条件
if ($name !== "Bob") {
// 继续到下一个路由
return true;
}
});
Flight::route('/user/*', function () {
// 这将被调用
});
路由别名
您可以为路由分配别名,以便稍后在代码中动态生成 URL(例如作为模板)。
Flight::route('/users/@id', function($id) { echo '用户:'.$id; }, false, 'user_view');
// 稍后在代码的某个地方
Flight::getUrl('user_view', [ 'id' => 5 ]); // 将返回 '/users/5'
这在您的 URL 发生变化时特别有用。在上面的示例中,假设用户被移到 /admin/users/@id
。
有了别名后,您不必更改在任何地方引用别名的地方,因为别名将现在返回 /admin/users/5
,就像上面的示例一样。
路由别名仍然可以在组中使用:
Flight::group('/users', function() {
Flight::route('/@id', function($id) { echo '用户:'.$id; }, false, 'user_view');
});
// 稍后在代码的某个地方
Flight::getUrl('user_view', [ 'id' => 5 ]); // 将返回 '/users/5'
路由信息
如果您想检查匹配路由的信息,可以通过在路由方法中传入 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);
路由分组
有时候您可能希望将相关路由分组在一起(例如 /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 = new \flight\Engine();
$app->group('/api/v1', function (Router $router) {
// 使用 $router 变量
$router->get('/users', function () {
// 匹配 GET /api/v1/users
});
$router->post('/posts', function () {
// 匹配 POST /api/v1/posts
});
});
资源路由
您可以使用 resource
方法为资源创建一组路由。这将创建遵循 RESTful 规范的一组路由。
要创建资源,请执行以下操作:
Flight::resource('/users', UsersController::class);
在后台将发生以下内容:
[
'index' => 'GET ',
'create' => 'GET /create',
'store' => 'POST ',
'show' => 'GET /@id',
'edit' => 'GET /@id/edit',
'update' => 'PUT /@id',
'destroy' => 'DELETE /@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
来查看新增的路由。
自定义资源路由
有一些选项可以配置资源路由。
别名基础
您可以配置 aliasBase
。 默认情况下,别名是指定 URL 的最后部分。
例如 /users/
将导致别名为 users
。 创建这些路由时,别名为 users.index
、users.create
等。如果您想更改别名,请将 aliasBase
设置为您想要的值。
Flight::resource('/users', UsersController::class, [ 'aliasBase' => 'user' ]);
仅和排除
您还可以通过使用 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 ] ]);
流式传输
您现在可以使用 streamWithHeaders()
方法将响应流式传输到客户端。
这对于发送大文件、长时间运行的过程或生成大量响应很有用。
流式传输路由的处理方式与常规路由略有不同。
注意: 如果您将
flight.v2.output_buffering
设置为 false,则仅在此情况下可以使用流式响应。
使用手动标头流式传输
您可以通过在路由上使用 stream()
方法将响应流式传输到客户端。如果您这样做,必须在将任何内容输出到客户端之前手动设置所有方法。
这可以通过 header()
php 函数或 Flight::response()->setRealHeader()
方法完成。
Flight::route('/@filename', function($filename) {
// 显然,您需要清理路径等内容。
$fileNameSafe = basename($filename);
// 如果您在路由执行后有其他要设置的标头,
// 您必须在任何内容被输出之前定义它们。
// 它们必须全部是对 header() 函数的原始调用或
// 调用 Flight::response()->setRealHeader()
header('Content-Disposition: attachment; filename="'.$fileNameSafe.'"');
// 或者
Flight::response()->setRealHeader('Content-Disposition', 'attachment; filename="'.$fileNameSafe.'"');
$fileData = file_get_contents('/some/path/to/files/'.$fileNameSafe);
// 错误捕获等
if(empty($fileData)) {
Flight::halt(404, '文件未找到');
}
// 如果您喜欢,可以手动设置内容长度
header('Content-Length: '.filesize($filename));
// 将数据流式传输到客户端
echo $fileData;
// 这是这里的魔法行
})->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
]);
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/variables
变量
Flight允许您保存变量,以便它们可以在应用程序的任何地方使用。
// 保存变量
Flight::set('id', 123);
// 在应用程序的其他地方
$id = Flight::get('id');
要查看变量是否已设置,可以执行以下操作:
if (Flight::has('id')) {
// 做些什么
}
您可以通过以下方式清除变量:
// 清除id变量
Flight::clear('id');
// 清除所有变量
Flight::clear();
Flight还使用变量进行配置目的。
Flight::set('flight.log_errors', true);
Learn/dependency_injection_container
依赖注入容器
介绍
依赖注入容器(DIC)是一个强大的工具,允许您管理应用程序的依赖关系。这是现代 PHP 框架中的一个关键概念,用于管理对象的实例化和配置。一些 DIC 库示例包括: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());
}
}
$User = new UserController(new PDO('mysql:host=localhost;dbname=test', 'user', 'pass'));
Flight::route('/user/@id', [ $UserController, 'view' ]);
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;
// 不要忘记像下面这样重新分配它给自己!
$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', 'view' ]);
// 或者您还可以像这样定义路由
Flight::route('/user/@id', 'UserController->view');
// 或者
Flight::route('/user/@id', 'UserController::view');
Flight::start();
您可能认为在示例中有很多额外的代码。其中的魔法之处在于当您有另一个需要 PDO
对象的控制器时。
// 如果您的所有控制器都有一个需要 PDO 对象的构造函数
// 下面的每个路由将自动注入它!!!
Flight::route('/company/@id', 'CompanyController->view');
Flight::route('/organization/@id', 'OrganizationController->view');
Flight::route('/category/@id', 'CategoryController->view');
Flight::route('/settings', 'SettingsController->view');
利用 DIC 的额外好处是进行单元测试变得更加简单。您可以创建一个模拟对象并将其传递给您的类。当您为应用程序编写测试时,这是一个巨大的好处!
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 示例相比,这可能会更冗长一些,但仍然可以以相同的好处完成工作!
自定义 DIC 处理程序
您还可以创建自己的 DIC 处理程序。如果您有一个不符合 PSR-11(Dice)的自定义容器,这会很有用。参见 基本示例 了解如何处理。
另外,在使用 Flight 时,还有一些有用的默认设置可以让您更轻松。
引擎实例
如果您在控制器/中间件中使用 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');
Learn/middleware
路由中间件
Flight支持路由和路由组中间件。中间件是在路由回调之前(或之后)执行的函数。这是在代码中添加API身份验证检查的绝佳方式,或者验证用户是否有权限访问路由。
基本中间件
这是一个基本示例:
// 如果只提供匿名函数,则将在路由回调之前执行。除类之外,没有“after”中间件函数(见下文)
Flight::route('/path', function() { echo 'Here I am!'; })->addMiddleware(function() {
echo 'Middleware first!';
});
Flight::start();
// 这将输出“Middleware first! Here I am!”
在您使用中间件之前,请务必了解一些非常重要的内容:
- 中间件函数按添加到路由的顺序执行。执行方式类似于Slim Framework处理此问题的方式。
- before按添加顺序执行,after按相反顺序执行。
- 如果您的中间件函数返回false,则所有执行都将停止,并引发403 Forbidden错误。您可能希望通过
Flight::redirect()
或类似方法更加优雅地处理这种情况。 - 如果您需要从路由获取参数,它们将作为单个数组传递给您的中间件函数(
function($params) { ... }
或public function before($params) {}
)。原因在于您可以将参数结构化为组,并在其中的某些组中,您的参数实际上可能以不同顺序显示,这将破坏中间件函数,因为引用错误的参数。通过这种方式,您可以按名称而不是位置访问它们。 - 如果只传入中间件的名称,它将自动由依赖注入容器执行,并且中间件将以其需要的参数执行。如果您尚未注册依赖注入容器,则将
flight\Engine
实例传递给__construct()
。
中间件类
中间件也可以注册为类。如果需要“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!”
处理中间件错误
假设您有一个授权中间件,并希望如果用户没有经过身份验证,则将用户重定向到登录页面。您有几个选择:
- 您可以从中间件函数中返回false,Flight将自动返回403 Forbidden错误,但无法自定义。
- 您可以使用
Flight::redirect()
将用户重定向到登录页面。 - 您可以在中间件中创建自定义错误,并停止路由的执行。
基本示例
这是一个简单的返回false; 示例:
class MyMiddleware {
public function before($params) {
if (isset($_SESSION['user']) === false) {
return false;
}
// 既然是true,一切都将继续进行
}
}
重定向示例
这是将用户重定向到登录页面的示例:
class MyMiddleware {
public function before($params) {
if (isset($_SESSION['user']) === false) {
Flight::redirect('/login');
exit;
}
}
}
自定义错误示例
假设您需要抛出JSON错误,因为您正在构建一个API。您可以这样做:
class MyMiddleware {
public function before($params) {
$authorization = Flight::request()->headers['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.']);
}
}
}
分组中间件
您可以添加一个路由组,然后该组中的每个路由也将具有相同的中间件。如果您需要将一堆路由按照Auth中间件进行分组以检查头部中的API密钥,则这很有用。
// 添加到组方法的末尾
Flight::group('/api', function() {
// 这个“空”外观的路由实际上将匹配/api
Flight::route('', function() { echo 'api'; }, false, 'api');
// 这将匹配/api/users
Flight::route('/users', function() { echo 'users'; }, false, 'users');
// 这将匹配/api/users/1234
Flight::route('/users/@id', function($id) { echo 'user:'.$id; }, false, 'user_view');
}, [ new ApiAuthMiddleware() ]);
如果要对所有路由应用全局中间件,可以添加一个“空”组:
// 添加到组方法的末尾
Flight::group('', function() {
// 这仍然是/users
Flight::route('/users', function() { echo 'users'; }, false, 'users');
// 这仍然是/users/1234
Flight::route('/users/@id', function($id) { echo 'user:'.$id; }, false, 'user_view');
}, [ new ApiAuthMiddleware() ]);
Learn/filtering
过滤
Flight 允许您在调用方法之前和之后对其进行过滤。无需记忆预定义的钩子。您可以过滤任何默认框架方法以及您映射的任何自定义方法。
过滤函数如下所示:
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!";
});
// 添加一个前置过滤器
Flight::before('hello', function (array &$params, string &$output): bool {
// 操作参数
$params[0] = 'Fred';
return true;
});
// 添加一个后置过滤器
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
等核心方法无法进行过滤,因为它们是直接调用而不是动态调用的。
Learn/requests
请求
Flight 将 HTTP 请求封装成一个单独的对象,可以通过以下方式访问:
$request = Flight::request();
典型用例
在 Web 应用程序中处理请求时,您通常会想提取一个头部,或 $_GET
或 $_POST
参数,甚至可能是原始请求体。Flight 提供了一个简单的接口来完成所有这些操作。
以下是获取查询字符串参数的示例:
Flight::route('/search', function(){
$keyword = Flight::request()->query['keyword'];
echo "您正在搜索: $keyword";
// 使用 $keyword 查询数据库或其他内容
});
以下是使用 POST 方法的表单示例:
Flight::route('POST /submit', function(){
$name = Flight::request()->data['name'];
$email = Flight::request()->data['email'];
echo "您提交了: $name, $email";
// 使用 $name 和 $email 保存到数据库或其他内容
});
请求对象属性
请求对象提供以下属性:
- 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 - Post 数据或 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 - 请求主机名
您可以将 query
, data
, cookies
, 和 files
属性作为数组或对象访问。
因此,要获取查询字符串参数,可以执行:
$id = Flight::request()->query['id'];
或可以这样做:
$id = Flight::request()->query->id;
原始请求体
要获取原始 HTTP 请求体,例如在处理 PUT 请求时,可以执行:
$body = Flight::request()->getBody();
JSON 输入
如果您发送带有类型 application/json
和数据 {"id": 123}
的请求,它将可从 data
属性获得:
$id = Flight::request()->data->id;
$_GET
您可以通过 query
属性访问 $_GET
数组:
$id = Flight::request()->query['id'];
$_POST
您可以通过 data
属性访问 $_POST
数组:
$id = Flight::request()->data['id'];
$_COOKIE
您可以通过 cookies
属性访问 $_COOKIE
数组:
$myCookieValue = Flight::request()->cookies['myCookieName'];
$_SERVER
可以通过 getVar()
方法访问 $_SERVER
数组的快捷方式:
$host = Flight::request()->getVar['HTTP_HOST'];
通过 $_FILES
访问上传的文件
您可以通过 files
属性访问上传的文件:
$uploadedFile = Flight::request()->files['myFile'];
处理文件上传
您可以使用框架中的一些助手方法处理文件上传。基本上这归结为从请求中提取文件数据,并将其移动到新位置。
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());
}
});
安全提示: 始终验证和清理用户输入,尤其是在处理文件上传时。始终验证您允许上传的扩展名类型,但您还应该验证文件的“魔术字节”,以确保它实际上是用户声称的文件类型。有可用的 文章 和 库 来帮助处理这个问题。
请求头
您可以使用 getHeader()
或 getHeaders()
方法访问请求头:
// 也许您需要授权头
$host = Flight::request()->getHeader('Authorization');
// 或者
$host = Flight::request()->header('Authorization');
// 如果您需要获取所有头
$headers = Flight::request()->getHeaders();
// 或者
$headers = Flight::request()->headers();
请求体
您可以使用 getBody()
方法访问原始请求体:
$body = Flight::request()->getBody();
请求方法
您可以使用 method
属性或 getMethod()
方法访问请求方法:
$method = Flight::request()->method; // 实际上调用 getMethod()
$method = Flight::request()->getMethod();
注意: getMethod()
方法首先从 $_SERVER['REQUEST_METHOD']
提取方法,然后可以通过 $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']
覆盖它(如果存在),或者通过 $_REQUEST['_method']
(如果存在)覆盖它。
请求 URL
有几个助手方法可以组合 URL 的不同部分,以方便您使用。
完整 URL
您可以使用 getFullUrl()
方法访问完整请求 URL:
$url = Flight::request()->getFullUrl();
// https://example.com/some/path?foo=bar
基础 URL
您可以使用 getBaseUrl()
方法访问基础 URL:
$url = Flight::request()->getBaseUrl();
// 注意,末尾没有斜杠。
// https://example.com
查询解析
您可以将 URL 传递给 parseQuery()
方法,以将查询字符串解析为关联数组:
$query = Flight::request()->parseQuery('https://example.com/some/path?foo=bar');
// ['foo' => 'bar']
Learn/frameworkmethods
# 框架方法
Flight 旨在易于使用和理解。以下是框架的完整方法集。
它包括核心方法,这些是常规静态方法,以及可被筛选或覆盖的可扩展方法,这些是映射方法。
## 核心方法
```php
Flight::map(string $name, callable $callback, bool $pass_route = false) // 创建自定义框架方法。
Flight::register(string $name, string $class, array $params = [], ?callable $callback = null) // 将类注册到框架方法。
Flight::before(string $name, callable $callback) // 在调用框架方法前添加筛选器。
Flight::after(string $name, callable $callback) // 在调用框架方法后添加筛选器。
Flight::path(string $path) // 添加自动加载类的路径。
Flight::get(string $key) // 获取变量。
Flight::set(string $key, mixed $value) // 设置变量。
Flight::has(string $key) // 检查变量是否设置。
Flight::clear(array|string $key = []) // 清除变量。
Flight::init() // 将框架初始化为默认设置。
Flight::app() // 获取应用程序对象实例
可扩展方法
Flight::start() // 启动框架。
Flight::stop() // 停止框架并发送响应。
Flight::halt(int $code = 200, string $message = '') // 停止框架,可选择性地附带状态代码和消息。
Flight::route(string $pattern, callable $callback, bool $pass_route = false) // 将 URL 模式映射到回调。
Flight::group(string $pattern, callable $callback) // 为 URL 创建分组,模式必须为字符串。
Flight::redirect(string $url, int $code) // 重定向到另一个 URL。
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 响应。
map
和 register
添加的任何自定义方法也可进行筛选。
Learn/api
框架 API 方法
Flight 旨在易于使用和理解。以下是框架的完整方法集。它包括核心方法,这些是常规静态方法,以及可扩展方法,这些是可以过滤或重写的方法。
核心方法
这些方法是框架的核心,不能被重写。
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 指南。
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/httpcaching
HTTP 缓存
Flight 提供了内置支持,用于 HTTP 级别的缓存。如果满足缓存条件,Flight 将返回一个 HTTP 304 Not Modified
响应。下一次客户端请求相同资源时,它们将被提示使用本地缓存版本。
上次修改时间
您可以使用 lastModified
方法并传入一个 UNIX 时间戳来设置页面上次修改的日期和时间。客户端将继续使用它们的缓存,直到上次修改的值被更改。
Flight::route('/news', function () {
Flight::lastModified(1234567890);
echo '此内容将被缓存。';
});
ETag
ETag
缓存类似于 Last-Modified
,不同之处在于您可以为资源指定任何想要的 id:
Flight::route('/news', function () {
Flight::etag('my-unique-id');
echo '此内容将被缓存。';
});
请记住,调用 lastModified
或 etag
都将设置并检查缓存值。如果在请求之间的缓存值相同,则 Flight 将立即发送一个 HTTP 304
响应并停止处理。
Learn/responses
响应
Flight 帮助生成部分响应头,但你对返回给用户的内容拥有大部分控制权。有时你可以直接访问 Response
对象,但大多数时候你会使用 Flight
实例来发送响应。
发送基本响应
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();
});
状态码
你可以通过使用 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
设置响应体
你可以通过使用 write
方法来设置响应体,但是,如果你使用 echo 或 print 发送任何内容,
它将被捕获并通过输出缓冲作为响应体发送。
Flight::route('/', function() {
Flight::response()->write("Hello, World!");
});
// 与以下相同
Flight::route('/', function() {
echo "Hello, World!";
});
清除响应体
如果你想清除响应体,可以使用 clearBody
方法:
Flight::route('/', function() {
if($someCondition) {
Flight::response()->write("Hello, World!");
} else {
Flight::response()->clearBody();
}
});
在响应体上运行回调
你可以使用 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);
});
你可以添加多个回调,它们将按照添加的顺序运行。由于这可以接受任何 可调用的,它可以接受类数组 [ $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);
});
});
中间件选项
你还可以使用中间件通过中间件将回调应用于所有路由:
// 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() ]);
设置响应头
你可以通过使用 header
方法设置响应的内容类型等头:
// 这将以纯文本格式向用户的浏览器发送 "Hello, World!"
Flight::route('/', function() {
Flight::response()->header('Content-Type', 'text/plain');
// 或者
Flight::response()->setHeader('Content-Type', 'text/plain');
echo "Hello, World!";
});
JSON
Flight 提供对发送 JSON 和 JSONP 响应的支持。要发送 JSON 响应,您需要将一些数据传递给 JSON 编码:
Flight::json(['id' => 123]);
注意: 默认情况下,Flight 将发送
Content-Type: application/json
头部作为响应。它还将在编码 JSON 时使用常量JSON_THROW_ON_ERROR
和JSON_UNESCAPED_SLASHES
。
带状态码的 JSON
你还可以将状态码作为第二个参数传入:
Flight::json(['id' => 123], 201);
带美化打印的 JSON
你还可以将参数传入最后一个位置以启用美化打印:
Flight::json(['id' => 123], 200, true, 'utf-8', JSON_PRETTY_PRINT);
如果你想更简单的语法可以改变传递给 Flight::json()
的选项,可以直接重新映射 JSON 方法:
Flight::map('json', function($data, $code = 200, $options = 0) {
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);
}
// 继续处理其余的路由
});
在 v3.10.0 之前,你必须做类似这样的事情:
Flight::route('/users', function() {
$authorized = someAuthorizationCheck();
// 检查用户是否获得授权
if($authorized === false) {
Flight::halt(401, json_encode(['error' => 'Unauthorized']));
}
// 继续处理其余的路由
});
JSONP
对于 JSONP 请求,你可以选择性地传递用于定义回调函数的查询参数名:
Flight::jsonp(['id' => 123], 'q');
因此,在使用 ?q=my_func
进行 GET 请求时,你应该收到以下输出:
my_func({"id":123});
如果你没有传递查询参数名称,它将默认为 jsonp
。
重定向到另一个 URL
你可以通过使用 redirect()
方法并传入一个新 URL 来重定向当前请求:
Flight::redirect('/new/location');
默认情况下,Flight 发送 HTTP 303(“见其他”)状态码。你可以选择设置自定义代码:
Flight::redirect('/new/location', 401);
停止
你可以随时通过调用 halt
方法停止框架:
Flight::halt();
你还可以指定可选的 HTTP
状态码和消息:
Flight::halt(200, '稍等...');
调用 halt
将丢弃到目前为止的任何响应内容。如果你想停止框架并输出当前响应,请使用 stop
方法:
Flight::stop();
清除响应数据
你可以使用 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 '此内容将被缓存。';
});
// 或者,你可以使用字符串传递给 strtotime() 方法
Flight::route('/news', function () {
Flight::response()->cache('+5 minutes');
echo '此内容将被缓存。';
});
最后修改时间
你可以使用 lastModified
方法并传入 UNIX 时间戳来设置页面最后修改的日期和时间。客户端将继续使用他们的缓存,直到最后修改值发生变化。
Flight::route('/news', function () {
Flight::lastModified(1234567890);
echo '此内容将被缓存。';
});
ETag
ETag
缓存类似于 Last-Modified
,只不过你可以指定任何你想要的资源 ID:
Flight::route('/news', function () {
Flight::etag('my-unique-id');
echo '此内容将被缓存。';
});
请记住,调用 lastModified
或 etag
都将设置并检查缓存值。如果请求之间的缓存值相同,Flight 将立即发送 HTTP 304
响应并停止处理。
下载文件(v3.12.0)
有一个辅助方法用于下载文件。你可以使用 download
方法并传入路径。
Flight::route('/download', function () {
Flight::download('/path/to/file.txt');
});
Learn/frameworkinstance
框架实例
将 Flight 作为全局静态类运行,您可以选择将其作为对象实例运行。
require 'flight/autoload.php';
$app = Flight::app();
$app->route('/', function () {
echo 'hello world!';
});
$app->start();
因此,您可以通过引擎对象上具有相同名称的实例方法来调用实例方法,而不是调用静态方法。
Learn/redirects
重定向
您可以使用redirect
方法并传入新的URL来重定向当前请求:
Flight::redirect('/new/location');
默认情况下,Flight发送HTTP 303状态码。您还可以选择设置自定义代码:
Flight::redirect('/new/location', 401);
Learn/events
Flight PHP 中的事件系统 (v3.15.0+)
Flight PHP 引入了一个轻量级且直观的事件系统,让你可以在应用中注册和触发自定义事件。通过添加 Flight::onEvent()
和 Flight::triggerEvent()
,你可以在应用的生命周期的关键时刻挂钩,或定义自己的事件,以使代码更加模块化和可扩展。这些方法是 Flight 的可映射方法的一部分,意味着你可以重写它们的行为以满足你的需求。
本指南涵盖了开始使用事件所需的所有知识,包括它们的价值、如何使用它们以及实际示例,帮助初学者理解它们的威力。
为什么使用事件?
事件允许你将应用的不同部分分开,以防彼此依赖过重。这种分离——通常称为解耦——使得你的代码更容易更新、扩展或调试。你可以将逻辑分割成更小的、独立的部分,以响应特定的操作(事件),而不是将所有内容写在一个大块中。
想象一下你正在构建一个博客应用:
- 当用户发布评论时,你可能想要:
- 将评论保存到数据库中。
- 向博客主发送电子邮件。
- 记录该操作以确保安全。
如果没有事件,你会将所有这一切塞进一个函数中。有了事件,你可以将其拆分:一部分保存评论,另一部分触发一个类似 'comment.posted'
的事件,独立的监听器处理电子邮件和日志记录。这保持了代码的整洁,并让你在不触及核心逻辑的情况下添加或移除特性(如通知)。
常见用法
- 日志记录:记录登录或错误等操作,而不混乱主代码。
- 通知:当某些事情发生时发送电子邮件或警报。
- 更新:刷新缓存或通知其他系统有关更改。
注册事件监听器
要监听一个事件,请使用 Flight::onEvent()
。此方法允许你定义在事件发生时应该执行的操作。
语法
Flight::onEvent(string $event, callable $callback): void
$event
:事件的名称 (例如:'user.login'
)。$callback
:当事件被触发时要运行的函数。
工作原理
你通过告诉 Flight 事件发生时该做什么来“订阅”一个事件。回调可以接受事件触发时传递的参数。
Flight 的事件系统是同步的,这意味着每个事件监听器按顺序执行,一个接一个。当你触发一个事件时,该事件所有注册的监听器将在代码继续之前运行完成。这一点很重要,因为与异步事件系统不同,异步事件系统的监听器可能会并行运行或在稍后的时间运行。
简单示例
Flight::onEvent('user.login', function ($username) {
echo "欢迎回来,$username!";
});
在这里,当触发 'user.login'
事件时,它会以用户的名字向其问候。
关键点
- 你可以为同一事件添加多个监听器——它们将按照你注册的顺序运行。
- 回调可以是一个函数、一个匿名函数,或者一个类的方法。
触发事件
要使事件发生,请使用 Flight::triggerEvent()
。这告诉 Flight 运行所有为该事件注册的监听器,并传递你提供的任何数据。
语法
Flight::triggerEvent(string $event, ...$args): void
$event
:你要触发的事件名称(必须与已注册的事件匹配)。...$args
:可选参数,以发送给监听器(可以是任何数量的参数)。
简单示例
$username = 'alice';
Flight::triggerEvent('user.login', $username);
这触发了 'user.login'
事件,并将 'alice'
发送给我们之前定义的监听器,它将输出:欢迎回来,alice!
。
关键点
- 如果没有注册监听器,则什么也不会发生——你的应用不会崩溃。
- 使用扩展运算符 (
...
) 灵活地传递多个参数。
注册事件监听器
...
停止进一步监听器:
如果监听器返回 false
,则该事件的后续监听器将不会执行。这允许你基于特定条件停止事件链。请记住,监听器的顺序很重要,因为第一个返回 false
的将阻止其余的运行。
示例:
Flight::onEvent('user.login', function ($username) {
if (isBanned($username)) {
logoutUser($username);
return false; // 停止后续监听器
}
});
Flight::onEvent('user.login', function ($username) {
sendWelcomeEmail($username); // 这不会被发送
});
重写事件方法
Flight::onEvent()
和 Flight::triggerEvent()
可以被扩展,这意味着你可以重新定义它们的工作原理。这对于想要自定义事件系统的高级用户很有好处,例如添加日志记录或改变事件的分发方式。
示例:自定义 onEvent
Flight::map('onEvent', function (string $event, callable $callback) {
// 记录每个事件注册
error_log("为新事件监听器添加:$event");
// 调用默认行为(假设有一个内部事件系统)
Flight::_onEvent($event, $callback);
});
现在,每次注册事件时,都会在继续之前进行日志记录。
为什么要重写?
- 添加调试或监控。
- 在某些环境中限制事件(例如,在测试时禁用)。
- 与其他事件库集成。
将事件放在哪里
作为初学者,你可能会想:我应该在哪里注册应用中的所有这些事件? Flight 的简洁性意味着没有严格的规则——你可以将它们放在任何适合你项目的地方。不过,保持它们的组织有助于你在应用增长时维护代码。以下是一些实用的选项和最佳实践,旨在符合 Flight 轻量级的特性:
选项 1:在主 index.php
中
对于小型应用或快速原型,你可以直接在 index.php
文件中注册事件,与路由共存。这使得一切保持在一个地方,对于优先考虑简洁性是可以的。
require 'vendor/autoload.php';
// 注册事件
Flight::onEvent('user.login', function ($username) {
error_log("$username 于 " . date('Y-m-d H:i:s') . " 登录");
});
// 定义路由
Flight::route('/login', function () {
$username = 'bob';
Flight::triggerEvent('user.login', $username);
echo "登录成功!";
});
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 于 " . date('Y-m-d H:i:s') . " 登录");
});
Flight::onEvent('user.registered', function ($email, $name) {
echo "发送邮件给 $email: 欢迎,$name!";
});
// index.php
require 'vendor/autoload.php';
require 'app/config/events.php';
Flight::route('/login', function () {
$username = 'bob';
Flight::triggerEvent('user.login', $username);
echo "登录成功!";
});
Flight::start();
- 优点:保持
index.php
专注于路由,逻辑组织事件,便于查找和编辑。 - 缺点:增加了一点结构,可能对非常小的应用来说显得过于复杂。
选项 3:靠近触发的位置
另一种方法是在触发事件的地方注册事件,例如在控制器或路由定义内。如果事件特定于应用的某一部分,这种方法效果很好。
Flight::route('/signup', function () {
// 在这里注册事件
Flight::onEvent('user.registered', function ($email) {
echo "欢迎邮件已发送给 $email!";
});
$email = 'jane@example.com';
Flight::triggerEvent('user.registered', $email);
echo "注册成功!";
});
- 优点:将相关代码保持在一起,适合孤立的特性。
- 缺点:事件注册分散,可能很难一目了然所有事件;如果不小心容易导致重复注册。
Flight 的最佳实践
- 从简单开始:对于小型应用,将事件放在
index.php
中。这样快捷且符合 Flight 的极简主义。 - 聪明地扩展:随着应用的扩大(例如,超过5-10个事件),使用
app/config/events.php
文件。这是自然的提升,就像组织路由一样,可以让你的代码整洁而不增加复杂的框架。 - 避免过度设计:除非你的应用规模很大,否则不要创建完整的“事件管理器”类或目录——Flight 追求简洁,保持轻量化。
提示:按目的分组
在 events.php
中,以注释分组相关事件(例如,把所有用户相关事件放在一起)以保持清晰度:
// app/config/events.php
// 用户事件
Flight::onEvent('user.login', function ($username) {
error_log("$username 登录");
});
Flight::onEvent('user.registered', function ($email) {
echo "欢迎到 $email!";
});
// 页面事件
Flight::onEvent('page.updated', function ($pageId) {
unset($_SESSION['pages'][$pageId]);
});
这种结构可扩展且对初学者友好。
初学者示例
让我们通过一些真实场景来演示事件的工作原理以及为什么它们有用。
示例 1:记录用户登录
// 步骤 1:注册监听器
Flight::onEvent('user.login', function ($username) {
$time = date('Y-m-d H:i:s');
error_log("$username 于 $time 登录");
});
// 步骤 2:在应用中触发它
Flight::route('/login', function () {
$username = 'bob'; // 假设这是来自表单
Flight::triggerEvent('user.login', $username);
echo "嗨,$username!";
});
为什么这有用:登录代码不需要知道日志记录的事情——它只是触发事件。你可以稍后添加更多监听器(例如,发送欢迎邮件),而无需更改路由。
示例 2:通知新用户
// 注册新注册的监听器
Flight::onEvent('user.registered', function ($email, $name) {
// 模拟发送电子邮件
echo "发送邮件给 $email: 欢迎,$name!";
});
// 在有人注册时触发它
Flight::route('/signup', function () {
$email = 'jane@example.com';
$name = 'Jane';
Flight::triggerEvent('user.registered', $email, $name);
echo "感谢注册!";
});
为什么这有用:注册逻辑专注于创建用户,而事件处理通知。你可以稍后添加更多监听器(例如,记录注册)而不需要更改逻辑。
示例 3:清除缓存
// 清除缓存的监听器
Flight::onEvent('page.updated', function ($pageId) {
unset($_SESSION['pages'][$pageId]); // 如果适用,清除会话缓存
echo "已清除页面 $pageId 的缓存。";
});
// 当页面被编辑时触发
Flight::route('/edit-page/(@id)', function ($pageId) {
// 假装我们更新了页面
Flight::triggerEvent('page.updated', $pageId);
echo "页面 $pageId 已更新。";
});
为什么这有用:编辑代码不需要关心缓存的事情——它只是发出更新的信号。应用的其他部分可以根据需要做出反应。
最佳实践
- 清晰命名事件:使用特定名称,如
'user.login'
或'page.updated'
,这样显而易见它们的作用。 - 保持监听器简单:不要在监听器中放入缓慢或复杂的任务——保持应用的快速。
- 测试你的事件:手动触发事件以确保监听器按预期工作。
- 明智使用事件:它们非常适合解耦,但过多可能使代码难以跟随——在合适的时候使用它们。
Flight PHP 中的事件系统以及 Flight::onEvent()
和 Flight::triggerEvent()
为你提供了一种简单而强大的方式来构建灵活的应用。通过让应用的不同部分通过事件进行通信,你可以保持代码组织良好、可重用且易于扩展。无论是记录操作、发送通知还是管理更新,事件都能帮助你做到这一点,而不让你的逻辑纠缠在一起。而且,能够重写这些方法使你有自由度来根据需要调整系统。从单个事件开始,看看它如何改变你应用的结构!
内置事件
Flight PHP 还附带了一些内置事件,你可以利用这些事件来挂钩到框架的生命周期。这些事件在请求/响应周期的特定时点触发,允许你在某些操作发生时执行自定义逻辑。
内置事件列表
flight.request.received
:当请求被接收、解析和处理时触发。flight.route.middleware.before
:在执行前中间件后触发。flight.route.middleware.after
:在执行后中间件后触发。flight.route.executed
:在路由被执行和处理后触发。flight.response.sent
:在响应被发送到客户端后触发。
Learn/views
视图
Flight默认提供一些基本的模板功能。要显示视图模板,请调用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>
自定义视图
Flight允许您通过注册自己的视图类简单地更换默认视图引擎。以下是如何为视图使用Smarty模板引擎的示例:
// 加载Smarty库
require './Smarty/libs/Smarty.class.php';
// 将Smarty注册为视图类
// 还要传递回调函数以在加载时配置Smarty
Flight::register('view', Smarty::class, [], function (Smarty $smarty) {
$smarty->setTemplateDir('./templates/');
$smarty->setCompileDir('./templates_c/');
$smarty->setConfigDir('./config/');
$smarty->setCacheDir('./cache/');
});
// 设置模板数据
Flight::view()->assign('name', 'Bob');
// 显示模板
Flight::view()->display('hello.tpl');
为了完整起见,您还应该覆盖Flight的默认render
方法:
Flight::map('render', function(string $template, array $data): void {
Flight::view()->assign($data);
Flight::view()->display($template);
});
Learn/templates
HTML 视图和模板
Flight 默认提供了一些基本的模板功能。
Flight 允许您通过注册自己的视图类来更换默认的视图引擎。向下滚动以查看如何使用 Smarty、Latte、Blade 等示例!
内置视图引擎
要显示视图模板,请调用 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 模板引擎:
// 加载 Smarty 库
require './Smarty/libs/Smarty.class.php';
// 注册 Smarty 作为视图类
// 还传递一个回调函数在加载时配置 Smarty
Flight::register('view', Smarty::class, [], function (Smarty $smarty) {
$smarty->setTemplateDir('./templates/');
$smarty->setCompileDir('./templates_c/');
$smarty->setConfigDir('./config/');
$smarty->setCacheDir('./cache/');
});
// 分配模板数据
Flight::view()->assign('name', 'Bob');
// 显示模板
Flight::view()->display('hello.tpl');
为了完整性,您还应该重写 Flight 的默认渲染方法:
Flight::map('render', function(string $template, array $data): void {
Flight::view()->assign($data);
Flight::view()->display($template);
});
Latte
以下是如何在您的视图中使用 Latte 模板引擎:
// 注册 Latte 作为视图类
// 还传递一个回调函数在加载时配置 Latte
Flight::register('view', Latte\Engine::class, [], function (Latte\Engine $latte) {
// 这是 Latte 将缓存您的模板以加快速度的地方
// Latte 的一个好处是,当您对模板进行更改时,它会自动刷新缓存!
$latte->setTempDirectory(__DIR__ . '/../cache/');
// 告诉 Latte 您的视图的根目录在哪里
$latte->setLoader(new \Latte\Loaders\FileLoader(__DIR__ . '/../views/'));
});
// 并包裹起来,以便您可以正确使用 Flight::render()
Flight::map('render', function(string $template, array $data): void {
// 这就像 $latte_engine->render($template, $data);
echo Flight::view()->render($template, $data);
});
Blade
以下是如何在您的视图中使用 Blade 模板引擎:
首先,您需要通过 Composer 安装 BladeOne 库:
composer require eftec/bladeone
然后,您可以在 Flight 中配置 BladeOne 作为视图类:
<?php
// 加载 BladeOne 库
use eftec\bladeone\BladeOne;
// 注册 BladeOne 作为视图类
// 还传递一个回调函数在加载时配置 BladeOne
Flight::register('view', BladeOne::class, [], function (BladeOne $blade) {
$views = __DIR__ . '/../views';
$cache = __DIR__ . '/../cache';
$blade->setPath($views);
$blade->setCompiledPath($cache);
});
// 分配模板数据
Flight::view()->share('name', 'Bob');
// 显示模板
echo Flight::view()->run('hello', []);
为了完整性,您还应该重写 Flight 的默认渲染方法:
<?php
Flight::map('render', function(string $template, array $data): void {
echo Flight::view()->run($template, $data);
});
在这个例子中,hello.blade.php 模板文件可能看起来像这样:
<?php
Hello, {{ $name }}!
输出将是:
Hello, Bob!
通过遵循这些步骤,您可以将 Blade 模板引擎与 Flight 集成,并使用它来渲染您的视图。
Learn/flight_vs_fat_free
Flight vs 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具有内置的会话、缓存和本地化。Flight需要您使用第三方库,但在文档中有介绍。
- Fat-Free有一群小众创建的社区插件,可用于扩展框架。Flight在文档和示例页面中有涵盖一些。
- 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有一些实现测试,甚至有自己的测试类,非常基础。然而,它不像Flight那样完全进行单元测试。
- 您必须使用像Google这样的搜索引擎来实际搜索文档网站。
- Flight在其文档网站上有深色模式。(mic drop)
- Fat-Free有一些模块是令人遗憾地未维护。
- Flight有一个简单的PdoWrapper,比Fat-Free内置的
DB\SQL
类简单一些。 - Flight有一个权限插件,可以用来保护应用程序。Slim要求您使用第三方库。
- Flight有一个名为active-record的ORM,感觉更像ORM,而不是Fat-Free的Mapper。
active-record
的附加好处是,您可以定义记录之间的关系,以实现自动连接,而Fat-Free的Mapper则要求您创建SQL视图。 - 令人惊讶的是,Fat-Free没有根命名空间。Flight从头到尾都有命名空间,以避免与您自己的代码冲突。
Cache
类在这里是最大的问题。 - Fat-Free没有中间件。相反,可以使用
beforeroute
和afterroute
钩子来过滤控制器中的请求和响应。 - Fat-Free无法分组路由。
- Fat-Free有一个依赖注入容器处理程序,但文档非常稀少,不清楚如何使用它。
- 调试可能会有些棘手,因为基本上一切都存储在所谓的
HIVE
中。
Learn/extending
扩展
Flight 被设计为一个可扩展的框架。该框架提供了一组默认方法和组件,但它允许你映射自己的方法,注册自己的类,甚至覆盖现有的类和方法。
如果你在寻找 DIC(依赖注入容器),请查看 Dependency Injection Container 页面。
映射方法
要映射你自己的简单自定义方法,可以使用 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
函数:
// 注册你的类
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 非常容易。以下是使用 Monolog 库的示例:
// index.php 或 bootstrap.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('这是一个警告信息');
这将把消息记录到你指定的日志文件中。如果你想在发生错误时记录某些内容,可以使用 error
方法:
// 在你的控制器或路由中
Flight::map('error', function(Throwable $ex) {
Flight::log()->error($ex->getMessage());
// 显示你的自定义错误页面
include 'errors/500.html';
});
你还可以使用 before
和 after
方法创建一个基本的 APM(应用程序性能监控)系统:
// 在你的引导文件中
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('请求 '.Flight::request()->url.' 花费 ' . round($end - $start, 4) . ' 秒');
// 你还可以将请求或响应头添加到日志中(小心,因为如果你有很多请求,这会产生大量数据)
Flight::log()->info('请求头: ' . json_encode(Flight::request()->headers));
Flight::log()->info('响应头: ' . json_encode(Flight::response()->headers));
});
覆盖框架方法
Flight 允许你覆盖其默认功能以满足自己的需求,而无需修改任何代码。你可以在 这里 查看所有可以覆盖的方法。
例如,当 Flight 无法将 URL 匹配到路由时,它会调用 notFound
方法并发送通用的 HTTP 404
响应。你可以使用 map
方法覆盖此行为:
Flight::map('notFound', function() {
// 显示自定义 404 页面
include 'errors/404.html';
});
Flight 还允许你替换框架的核心组件。例如,你可以用自己的自定义类替换默认的 Router 类:
// 注册你的自定义类
Flight::register('router', MyRouter::class);
// 当 Flight 加载 Router 实例时,它将加载你的类
$myrouter = Flight::router();
但是,像 map
和 register
这样的框架方法无法被覆盖。如果你尝试这样做,将会出现错误。
Learn/json
JSON
Flight提供发送JSON和JSONP响应的支持。要发送JSON响应,您需要传递一些数据进行JSON编码:
Flight::json(['id' => 123]);
对于JSONP请求,您可以选择传递用于定义回调函数的查询参数名称:
Flight::jsonp(['id' => 123], 'q');
因此,当使用 ?q=my_func
发出GET请求时,您应该收到以下输出:
my_func({"id":123});
如果您没有传递查询参数名称,它将默认为 jsonp
。
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()
方法來指定要從哪個目錄加載類。
基本示例
假設我們有如下目錄樹:
# 示例路徑
/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
*/
// 不需要命名空間
// 建議將所有自動加載的類命名為帕斯卡命名法(每個單詞首字母大寫,沒有空格)
// 截至 3.7.2 版本,您可以通過運行 Loader::setV2ClassLoading(false); 來使用帕斯卡_蛇_命名法命名您的類
class MyController {
public function index() {
// 做一些事情
}
}
命名空間
如果您有命名空間,實際上實現這一點變得非常容易。您應該使用Flight::path()
方法來指定應用程序的根目錄(而不是文檔根目錄或public/
文件夾)。
/**
* public/index.php
*/
// 添加一個路徑給自動加載程序
Flight::path(__DIR__.'/../');
現在您的控制器可能看起來像這樣。查看下面的示例,但請注意評注中的重要信息。
/**
* app/controllers/MyController.php
*/
// 必須有命名空間
// 命名空間與目錄結構相同
// 命名空間必須遵循與目錄結構相同的大小寫
// 命名空間和目錄不能有任何下劃線(除非 Loader::setV2ClassLoading(false) 已設置)
namespace app\controllers;
// 建議將所有自動加載的類命名為帕斯卡命名法(每個單詞首字母大寫,沒有空格)
// 截至 3.7.2 版本,您可以通過運行 Loader::setV2ClassLoading(false); 來使用帕斯卡_蛇_命名法命名您的類
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);
,來使用帕斯卡_蛇_命名法命名您的類。這將允許您在類名中使用下劃線。這不建議使用,但對於需要的用戶來說是可用的。
/**
* 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() {
// 做一些事情
}
}
Learn/troubleshooting
故障排除
这个页面将帮助您解决在使用Flight时可能遇到的常见问题。
常见问题
404未找到或意外路由行为
如果您看到404未找到错误(但您发誓它确实存在,而且没有拼写错误),实际上可能是因为您在路由终点返回一个值而不是仅仅输出它。这样做的原因是故意的,但可能会让一些开发人员感到意外。
Flight::route('/hello', function(){
// 这可能导致404未找到错误
return 'Hello World';
});
// 您可能想要的是
Flight::route('/hello', function(){
echo 'Hello World';
});
造成这种情况的原因是路由器中内置的特殊机制,它将返回输出视为“继续下一个路由”。您可以在路由部分中查看这种行为的文档。
类未找到(自动加载不起作用)
可能有几个原因导致这种情况发生。以下是一些示例,但请确保您还查看了自动加载部分。
文件名不正确
最常见的情况是类名与文件名不匹配。
如果您有一个名为 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__.'/../');
Install
安装
下载文件
如果您正在使用Composer,可以运行以下命令:
composer require flightphp/core
或者您可以直接下载文件 并将其提取到您的 web 目录中。
配置您的 Web 服务器
Apache
对于 Apache,请编辑您的 .htaccess
文件如下:
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L]
注意: 如果您需要在子目录中使用 Flight,请在
RewriteEngine On
之后添加RewriteBase /subdir/
。注意: 如果您希望保护所有服务器文件,例如 db 或 env 文件, 请将以下内容放入您的
.htaccess
文件中:
RewriteEngine On
RewriteRule ^(.*)$ index.php
Nginx
对于 Nginx,请将以下内容添加到您的服务器声明中:
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();
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?
Flight 是一个快速、简单、可扩展的 PHP 框架。它相当灵活,可以用于构建任何类型的 web 应用程序。它的设计简单易懂。
Flight 是初学者学习 PHP 的绝佳框架,适合那些想要学习如何构建 web 应用程序的人。对于希望对其 web 应用程序有更多控制的经验丰富的开发者来说,它也是一个很好的框架。它的设计可以轻松构建 RESTful API、简单的 web 应用程序或复杂的 web 应用程序。
快速入门
首先通过 Composer 安装
composer require flightphp/core
或者你可以在 这里 下载 repo 的 zip 文件。然后你会有一个基本的 index.php
文件,如下所示:
<?php
// 如果通过 composer 安装
require 'vendor/autoload.php';
// 或者如果通过 zip 文件手动安装
// require 'flight/Flight.php';
Flight::route('/', function() {
echo '你好,世界!';
});
Flight::route('/json', function() {
Flight::json(['hello' => '世界']);
});
Flight::start();
就这样!你拥有了一个基本的 Flight 应用程序。你可以用 php -S localhost:8000
运行这个文件,并在浏览器中访问 http://localhost:8000
查看输出。
它快吗?
是的!Flight 是快速的。它是可用的最快的 PHP 框架之一。你可以在 TechEmpower 查看所有的基准测试。
请查看下面的基准测试,与一些其他流行的 PHP 框架进行比较。
框架 | 明文请求/秒 | JSON 请求/秒 |
---|---|---|
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 框架。请访问 flightphp/skeleton 获取入门说明!你还可以访问 examples 页面,获得一些你可以使用 Flight 完成的事情的灵感。
社区
我们在 Matrix 聊天
以及 Discord
贡献
有两种方式可以为 Flight 做出贡献:
- 你可以通过访问 core repository 为核心框架做出贡献。
- 你可以为文档做出贡献。该文档网站托管在 Github。如果你发现错误或者想更好地阐述某个内容,欢迎你修改并提交拉取请求!我们努力跟上这些事情,但欢迎更新和语言翻译。
要求
Flight 需要 PHP 7.4 或更高版本。
注意: PHP 7.4 是被支持的,因为在写作时(2024 年)PHP 7.4 是一些 LTS Linux 发行版的默认版本。强制转向 PHP >8 将给那些用户带来很多麻烦。该框架也支持 PHP >8。
许可证
Flight 在 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
轻量、简单且独立的 PHP 文件缓存类
优点
- 轻量、独立且简单
- 所有代码在一个文件中 - 没有无意义的驱动程序。
- 安全 - 每个生成的缓存文件都有带有 die 的 PHP 头,即使有人知道路径并且您的服务器配置不正确,也无法直接访问
- 文档齐全且经过测试
- 通过 flock 正确处理并发
- 支持 PHP 7.4+
- 在 MIT 许可下免费
此文档网站正在使用此库来缓存每个页面!
点击 这里 查看代码。
安装
通过 composer 安装:
composer require flightphp/cache
使用
使用相当简单。这样会在缓存目录中保存缓存文件。
use flight\Cache;
$app = Flight::app();
// 您将将缓存存储的目录传递给构造函数
$app->register('cache', Cache::class, [ __DIR__ . '/../cache/' ], function(Cache $cache) {
// 这确保只有在生产模式下才使用缓存
// ENVIRONMENT 是在引导文件或应用的其他地方设置的常量
$cache->setDevMode(ENVIRONMENT === 'development');
});
然后您可以像这样在代码中使用它:
// 获取缓存实例
$cache = Flight::cache();
$data = $cache->refreshIfExpired('simple-cache-test', function () {
return date("H:i:s"); // 返回要缓存的数据
}, 10); // 10 秒
// 或
$data = $cache->retrieve('simple-cache-test');
if(empty($data)) {
$data = date("H:i:s");
$cache->store('simple-cache-test', $data, 10); // 10 秒
}
文档
访问 https://github.com/flightphp/cache 获取完整文档,并确保查看 examples 文件夹。
Awesome-plugins/permissions
FlightPHP/Permissions
这是一个权限模块,可以在您的项目中使用,如果您的应用程序中有多个角色,并且每个角色有稍微不同的功能。此模块允许您为每个角色定义权限,然后检查当前用户是否具有权限访问某个页面或执行某个操作。
单击此处查看 GitHub 上的存储库。
安装
运行 composer require flightphp/permissions
,您就可以开始了!
用法
首先,您需要设置权限,然后告诉您的应用程序这些权限的含义。最终,您将使用 $Permissions->has()
、->can()
或 is()
检查权限。has()
和 can()
具有相同的功能,但命名不同,以使您的代码更易读。
基本示例
假设您的应用程序中有一个功能,用于检查用户是否已登录。您可以像这样创建一个权限对象:
// index.php
require 'vendor/autoload.php';
// 一些代码
// 然后您可能有一些内容告诉您当前角色是谁
// 可能您有一些内容从会话变量中提取当前角色
// 以定义此内容
// 在某人登录后,否则他们将拥有 'guest' 或 'public' 角色。
$current_role = 'admin';
// 设置权限
$permission = new \flight\Permission($current_role);
$permission->defineRule('loggedIn', function($current_role) {
return $current_role !== 'guest';
});
// 您可能希望在 Flight 中某处持久化此对象
Flight::set('permission', $permission);
然后在某个控制器中,您可能会有如下代码。
<?php
// 某个控制器
class SomeController {
public function someAction() {
$permission = Flight::get('permission');
if ($permission->has('loggedIn')) {
// 做某事
} else {
// 做其他事
}
}
}
您还可以使用此功能跟踪用户在应用程序中是否具有执行某些操作的权限。 例如,如果您的应用程序允许用户与软件上的帖子进行交互,您可以 检查他们是否有权限执行某些操作。
$current_role = 'admin';
// 设置权限
$permission = new \flight\Permission($current_role);
$permission->defineRule('post', function($current_role) {
if($current_role === 'admin') {
$permissions = ['create', 'read', 'update', 'delete'];
} else if($current_role === 'editor') {
$permissions = ['create', 'read', 'update'];
} else if($current_role === 'author') {
$permissions = ['create', 'read'];
} else if($current_role === 'contributor') {
$permissions = ['create'];
} else {
$permissions = [];
}
return $permissions;
});
Flight::set('permission', $permission);
然后在某个控制器中...
class PostController {
public function create() {
$permission = Flight::get('permission');
if ($permission->can('post.create')) {
// 做某事
} else {
// 做其他事
}
}
}
注入依赖项
您可以将依赖项注入定义权限的闭包中。如果您有某种切换、ID 或任何其他要检查的数据点,这很有用。对于 Class->Method 类型的调用同样适用,只是您需要在方法中定义参数。
闭包
$Permission->defineRule('order', function(string $current_role, MyDependency $MyDependency = null) {
// ... 代码
});
// 在您的控制器文件中
public function createOrder() {
$MyDependency = Flight::myDependency();
$permission = Flight::get('permission');
if ($permission->can('order.create', $MyDependency)) {
// 做某事
} else {
// 做其他事
}
}
类
namespace MyApp;
class Permissions {
public function order(string $current_role, MyDependency $MyDependency = null) {
// ... 代码
}
}
使用类设置权限的快捷方式
您还可以使用类来定义您的权限。如果您有很多权限要保持代码整洁,这很有用。您可以这样做:
<?php
// 启动代码
$Permissions = new \flight\Permission($current_role);
$Permissions->defineRule('order', 'MyApp\Permissions->order');
// myapp/Permissions.php
namespace MyApp;
class Permissions {
public function order(string $current_role, int $user_id) {
// 假设您先前设置好了这一点
/** @var \flight\database\PdoWrapper $db */
$db = Flight::db();
$allowed_permissions = [ 'read' ]; // 每个人都可以查看订单
if($current_role === 'manager') {
$allowed_permissions[] = 'create'; // 管理员可以创建订单
}
$some_special_toggle_from_db = $db->fetchField('SELECT some_special_toggle FROM settings WHERE id = ?', [ $user_id ]);
if($some_special_toggle_from_db) {
$allowed_permissions[] = 'update'; // 如果用户有特殊开关,他们可以更新订单
}
if($current_role === 'admin') {
$allowed_permissions[] = 'delete'; // 管理员可以删除订单
}
return $allowed_permissions;
}
}
很酷的部分是,还有一个简便方法可以使用(也可以被缓存!),您只需告诉权限类将类中的所有方法映射到权限中。因此,如果您有一个名为 order()
和一个名为 company()
的方法,这些将自动映射,因此您只需运行 $Permissions->has('order.read')
或 $Permissions->has('company.read')
即可正常工作。定义这些非常困难,所以请跟着我学习。您只需要执行以下操作:
创建要分组的权限类。
class MyPermissions {
public function order(string $current_role, int $order_id = 0): array {
// 决定权限的代码
return $permissions_array;
}
public function company(string $current_role, int $company_id): array {
// 决定权限的代码
return $permissions_array;
}
}
然后使用此库使权限可发现。
$Permissions = new \flight\Permission($current_role);
$Permissions->defineRulesFromClassMethods(MyApp\Permissions::class);
Flight::set('permissions', $Permissions);
最后,在您的代码库中调用权限以检查用户是否被允许执行给定的权限。
class SomeController {
public function createOrder() {
if(Flight::get('permissions')->can('order.create') === false) {
die('您无法创建订单。抱歉!');
}
}
}
缓存
要启用缓存,请参阅简单的 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/index
令人惊叹的插件
Flight非常可扩展。有许多插件可以用于向您的Flight应用程序添加功能。其中一些得到了Flight团队的官方支持,而其他一些是微型/轻量级库,可帮助您入门。
缓存
缓存是加速应用程序的绝佳方法。有许多缓存库可与Flight一起使用。
- Wruczek/PHP-File-Cache - 轻巧、简单且独立的PHP文件缓存类
调试
在本地环境中进行开发时,调试至关重要。有一些插件可以提升您的调试体验。
- tracy/tracy - 这是一个功能齐全的错误处理程序,可以与Flight一起使用。它具有多个面板,可以帮助您调试应用程序。扩展和添加自定义面板也非常简单。
- flightphp/tracy-extensions - 与Tracy错误处理程序一起使用,此插件添加了一些额外的面板,专门用于Flight项目的调试。
数据库
数据库是大多数应用程序的核心。这是您存储和检索数据的方式。有些数据库库只是用来编写查询的包装器,而有些是完整的ORM。
- flightphp/core PdoWrapper - Flight官方PDO包装器,是核心的一部分。这是一个简单的包装器,帮助简化编写查询和执行查询的过程。它不是ORM。
- flightphp/active-record - Flight官方ActiveRecord ORM/Mapper。非常适合轻松检索和存储数据库中的数据的小型库。
会话
对于API来说,会话实际上并不那么有用,但对于构建Web应用程序来说,会话可以对保持状态和登录信息至关重要。
- Ghostff/Session - PHP会话管理器(非阻塞,闪存,分段,会话加密)。使用PHP open_ssl进行可选的会话数据加密/解密。
模板
模板是任何具有UI的Web应用程序的核心。有许多模板引擎可与Flight一起使用。
- flightphp/core View - 这是Flight核心的一个非常基本的模板引擎。如果项目中有多个页面,则不建议使用它。
- latte/latte - Latte是一个功能齐全的模板引擎,非常易于使用,更接近于PHP语法而不是Twig或Smarty。扩展和添加自定义过滤器和功能也非常简单。
贡献
有插件想要分享吗?提交拉取请求将其添加到列表中!
Awesome-plugins/ghost_session
Ghostff/Session
PHP 会话管理器(非阻塞,闪存,段,会话加密)。使用 PHP open_ssl 进行可选的会话数据加密/解密。支持文件、MySQL、Redis 和 Memcached。
点击 这里 查看代码。
安装
使用 Composer 安装。
composer require ghostff/session
基本配置
您不需要传递任何内容即可使用会话的默认设置。您可以在 Github 读我 中了解更多设置。
use Ghostff\Session\Session;
require 'vendor/autoload.php';
$app = Flight::app();
$app->register('session', Session::class);
// 需要记住的一点是,您必须在每个页面加载时提交您的会话
// 否则,您需要在配置中运行 auto_commit。
简单示例
这是一个如何使用此功能的简单示例。
Flight::route('POST /login', function() {
$session = Flight::session();
// 在这里执行您的登录逻辑
// 验证密码等。
// 如果登录成功
$session->set('is_logged_in', true);
$session->set('user', $user);
// 每次写入会话时,您必须故意提交它。
$session->commit();
});
// 此检查可以在受限页面逻辑中,或与中间件一起包装。
Flight::route('/some-restricted-page', function() {
$session = Flight::session();
if(!$session->get('is_logged_in')) {
Flight::redirect('/login');
}
// 在这里执行您的受限页面逻辑
});
// 中间件版本
Flight::route('/some-restricted-page', function() {
// 常规页面逻辑
})->addMiddleware(function() {
$session = Flight::session();
if(!$session->get('is_logged_in')) {
Flight::redirect('/login');
}
});
更复杂的示例
这是一个更复杂的示例,您可以如何使用此功能。
use Ghostff\Session\Session;
require 'vendor/autoload.php';
$app = Flight::app();
// 设置会话配置文件的自定义路径,并为会话 ID 提供随机字符串
$app->register('session', Session::class, [ 'path/to/session_config.php', bin2hex(random_bytes(32)) ], function(Session $session) {
// 或者您可以手动覆盖配置选项
$session->updateConfiguration([
// 如果您希望将会话数据存储在数据库中(如果您想要类似“从所有设备注销”功能)
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);
// 每次写入会话时,您必须故意提交它。
$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 读我 获取完整文档。配置选项在 default_config.php 文件中有良好的文档。如果您想自己浏览此包,代码非常简单易懂。
Awesome-plugins/pdo_wrapper
PdoWrapper PDO 辅助类
Flight 自带一个用于 PDO 的辅助类。它允许您轻松地查询您的数据库,使用所有准备/执行/获取所有结果的功能。它极大简化了您查询数据库的方式。每一行结果都作为 Flight 集合类返回,允许您通过数组语法或对象语法访问您的数据。
注册 PDO 辅助类
// 注册 PDO 辅助类
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
]
]);
用法
此对象扩展了 PDO,因此所有正常的 PDO 方法都可用。以下方法已添加以使查询数据库更容易:
runQuery(string $sql, array $params = []): PDOStatement
用于INSERTS、UPDATES,或者如果您打算在 while 循环中使用 SELECT
$db = Flight::db();
$statement = $db->runQuery("SELECT * FROM table WHERE something = ?", [ $something ]);
while($row = $statement->fetch()) {
// ...
}
// 或写入数据库
$db->runQuery("INSERT INTO table (name) VALUES (?)", [ $name ]);
$db->runQuery("UPDATE table SET name = ? WHERE id = ?", [ $name, $id ]);
fetchField(string $sql, array $params = []): mixed
从查询中获取第一个字段
$db = Flight::db();
$count = $db->fetchField("SELECT COUNT(*) FROM table WHERE something = ?", [ $something ]);
fetchRow(string $sql, array $params = []): array
从查询中获取一行
$db = Flight::db();
$row = $db->fetchRow("SELECT id, name FROM table WHERE id = ?", [ $id ]);
echo $row['name'];
// 或
echo $row->name;
fetchAll(string $sql, array $params = []): array
从查询中获取所有行
$db = Flight::db();
$rows = $db->fetchAll("SELECT id, name FROM table WHERE something = ?", [ $something ]);
foreach($rows as $row) {
echo $row['name'];
// 或
echo $row->name;
}
注意 IN()
语法
这还有一个有用的 IN()
语句包装器。您可以简单地传递一个问号作为 IN()
的占位符,然后是一个值数组。以下是这种用法的示例:
$db = Flight::db();
$name = 'Bob';
$company_ids = [1,2,3,4,5];
$rows = $db->fetchAll("SELECT id, name FROM table WHERE name = ? AND company_id IN (?)", [ $name, $company_ids ]);
完整示例
// 示例路由及如何使用该包装器
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'];
// or echo $user->name;
}
// 获取单个用户
$user = Flight::db()->fetchRow('SELECT * FROM users WHERE id = ?', [123]);
// 获取单个值
$count = Flight::db()->fetchField('SELECT COUNT(*) FROM users');
// 特殊 IN() 语法以帮助你(确保 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();
});
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', // 会话文件目录
'encryption_key' => 'a-secure-32-byte-key-here', // 启用加密(建议 AES-256-CBC 的 32 字节密钥)
'auto_commit' => false, // 禁用自动提交以进行手动控制
'start_session' => true, // 自动启动会话(默认:true)
'test_mode' => false // 启用开发测试模式
]);
配置选项
选项 | 描述 | 默认值 |
---|---|---|
save_path |
存储会话文件的目录 | sys_get_temp_dir() . '/flight_sessions' |
encryption_key |
AES-256-CBC 加密的密钥(可选) | null (不加密) |
auto_commit |
在关闭时自动保存会话数据 | true |
start_session |
自动启动会话 | true |
test_mode |
在不影响 PHP 会话的情况下以测试模式运行 | false |
test_session_id |
测试模式的自定义会话 ID(可选) | 如果未设置则随机生成 |
高级用法
手动提交
如果您禁用自动提交,您必须手动提交更改:
$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, '访问被拒绝');
}
});
这只是如何在中间件中使用此功能的一个简单示例。有关更深入的示例,请参阅 中间件 文档。
方法
Session
类提供以下方法:
set(string $key, $value)
:将值存储在会话中。get(string $key, $default = null)
:检索值,如果键不存在,则可选默认值。delete(string $key)
:从会话中移除特定键。clear()
:删除所有会话数据。commit()
:将当前会话数据保存到文件系统。id()
:返回当前会话 ID。regenerate(bool $deleteOld = false)
:再生会话 ID,可选择删除旧数据。
除了 get()
和 id()
之外,所有方法都返回 Session
实例以便于链接调用。
为什么使用这个插件?
- 轻量级:没有外部依赖——仅仅是文件。
- 非阻塞:默认使用
read_and_close
避免会话锁定。 - 安全:支持 AES-256-CBC 加密敏感数据。
- 灵活:提供自动提交、测试模式和手动控制选项。
- Flight 原生:专门为 Flight 框架构建。
技术细节
- 存储格式:会话文件以
sess_
前缀开头,存储在配置的save_path
中。加密数据使用E
前缀,明文使用P
。 - 加密:在提供
encryption_key
时,使用 AES-256-CBC,且每次会话写入时采用随机 IV。 - 垃圾收集:实现 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 Panel Extensions
=====
这是一组扩展,可以让与 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;
// 引导代码
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());
}
// 更多代码
Flight::start();
额外配置
会话数据
如果您有自定义会话处理程序(例如 ghostff/session),您可以将任何会话数据数组传递给 Tracy,它将自动为您输出。您可以在 TracyExtensionLoader
构造函数的第二个参数中的 session_data
键中传递它。
use Ghostff\Session\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() ]);
}
// 路由和其他事物...
Flight::start();
Latte
如果您在项目中安装了 Latte,您可以使用 Latte 面板来分析您的模板。您可以将 Latte 实例传递给 TracyExtensionLoader
构造函数的第二个参数中的 latte
键。
use Latte\Engine;
require 'vendor/autoload.php';
$app = Flight::app();
$app->register('latte', Engine::class, [], function($latte) {
$latte->setTempDirectory(__DIR__ . '/temp');
// 这是您向 Tracy 添加 Latte 面板的位置
$latte->addExtension(new Latte\Bridges\Tracy\TracyExtension);
});
if(Debugger::$showBar === true) {
// 这需为false,否则 Tracy 无法实际渲染 :(
Flight::set('flight.content_length', false);
new TracyExtensionLoader(Flight::app());
}
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
拿铁
拿铁 是一个功能齐全的模板引擎,非常易于使用,比Twig或Smarty更接近于PHP语法。它也非常容易扩展,添加自己的过滤器和函数。
安装
使用composer安装。
composer require latte/latte
基本配置
有一些基本配置选项可供开始使用。您可以在Latte文档中阅读更多内容。
use Latte\Engine as LatteEngine;
require 'vendor/autoload.php';
$app = Flight::app();
$app->register('latte', LatteEngine::class, [], function(LatteEngine $latte) use ($app) {
// 这是Latte将缓存模板以加快速度的位置
// Latte的一个好处是,当您更改模板时,它会自动刷新您的缓存!
$latte->setTempDirectory(__DIR__ . '/../cache/');
// 告诉Latte您的视图的根目录在哪里
// $app->get('flight.views.path') 在config.php文件中设置
// 您也可以这样做 `__DIR__ . '/../views/'`
$latte->setLoader(new \Latte\Loaders\FileLoader($app->get('flight.views.path')));
});
简单布局示例
这是一个布局文件的简单示例。这个文件将用于包装所有其他视图。
<!-- app/views/layout.latte -->
<!doctype html>
<html lang="zh-CN">
<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::latte()->render('home.latte', [
'title' => '主页'
]);
});
// 或者,如果您正在使用控制器
Flight::route('/', [HomeController::class, 'index']);
// HomeController.php
class HomeController
{
public function index()
{
Flight::latte()->render('home.latte', [
'title' => '主页'
]);
}
}
查看Latte文档以获取有关如何充分利用Latte的更多信息!
Awesome-plugins/awesome_plugins
精彩的插件
Flight 拥有强大的扩展性。有许多插件可以用来为你的 Flight 应用程序添加功能。其中一些是 Flight 团队正式支持的,其他的是轻量级库,可以帮助你入门。
API 文档
API 文档对于任何 API 都至关重要。它帮助开发者理解如何与你的 API 交互以及期望返回什么。有几种工具可供你为你的 Flight 项目生成 API 文档。
- FlightPHP OpenAPI 生成器 - Daniel Schreiber 撰写的博客文章,介绍如何将 OpenAPI 规范与 FlightPHP 一起使用,以 API 优先的方法构建你的 API。
- SwaggerUI - Swagger UI 是一种很好的工具,可以帮助你为你的 Flight 项目生成 API 文档。它非常易于使用,并且可以根据你的需求进行自定义。这是一个帮助你生成 Swagger 文档的 PHP 库。
身份验证/授权
身份验证和授权对于任何需要控制访问的应用程序至关重要。
- official flightphp/permissions - 官方 Flight 权限库。这个库提供了一种简单的方式来为你的应用程序添加用户和应用级别的权限。
缓存
缓存是加速你的应用程序的一个很好的方式。有许多缓存库可以与 Flight 一起使用。
- official flightphp/cache - 轻量、简单且独立的 PHP 文件内缓存类
CLI
CLI 应用程序是与您的应用程序交互的一种很好的方式。你可以用它们来生成控制器、显示所有路由等。
- official flightphp/runway - Runway 是一个 CLI 应用程序,帮助你管理你的 Flight 应用程序。
Cookies
Cookies 是在客户端存储小数据块的很好方式。它们可以用来存储用户偏好、应用程序设置等。
- overclokk/cookie - PHP Cookie 是一个 PHP 库,为管理 cookies 提供了一种简单而有效的方法。
调试
调试在你开发本地环境时至关重要。有一些插件可以提升你的调试体验。
- tracy/tracy - 这是一个功能齐全的错误处理程序,可以与 Flight 一起使用。它有许多面板可以帮助你调试应用程序。它也非常容易扩展,并添加自己的面板。
- flightphp/tracy-extensions - 与 Tracy 错误处理程序一起使用,这个插件添加了一些额外的面板,专门帮助调试 Flight 项目。
数据库
数据库是大多数应用程序的核心。这是你存储和检索数据的方式。一些数据库库只是包裹器,方便编写查询,而一些则是完整的 ORM。
- official flightphp/core PdoWrapper - 官方 Flight PDO 包装器,属于核心的一部分。这是一个简单的包装器,帮助简化编写和执行查询的过程。它不是 ORM。
- official flightphp/active-record - 官方 Flight ActiveRecord ORM/映射器。是一个轻量级库,用于轻松检索和存储数据库中的数据。
- byjg/php-migration - 插件,用于跟踪项目的所有数据库更改。
加密
加密对任何存储敏感数据的应用程序至关重要。加密和解密数据并不难,但正确存储加密密钥 可以 很 困难。最重要的是绝不要将你的加密密钥存储在公共目录中或将其提交到代码库中。
- defuse/php-encryption - 这是一个可以用来加密和解密数据的库。启动并运行加密和解密数据相对简单。
工作队列
工作队列对于异步处理任务非常有帮助。这可以是发送电子邮件、处理图像,或者任何不需要实时完成的任务。
- n0nag0n/simple-job-queue - 简单的工作队列是一个可以用于异步处理工作的库。它可以与 beanstalkd、MySQL/MariaDB、SQLite 和 PostgreSQL 一起使用。
会话
会话对于 API 来说并不是特别有用,但对于构建 Web 应用程序,保持状态和登录信息的会话非常关键。
- official flightphp/session - 官方 Flight 会话库。这是一个简单的会话库,可用于存储和检索会话数据。它使用 PHP 内置的会话处理。
- Ghostff/Session - PHP 会话管理器(非阻塞、闪存、段、会话加密)。使用 PHP open_ssl 可选地加密/解密会话数据。
模板
模板是任何具有 UI 的 Web 应用程序的核心。有许多模板引擎可以与 Flight 一起使用。
- deprecated flightphp/core View - 这是核心的一部分,非常基本的模板引擎。如果你的项目中有超过几页,不推荐使用。
- latte/latte - Latte 是一个功能齐全的模板引擎,使用起来非常简单,并且比 Twig 或 Smarty 更接近 PHP 语法。它也非常容易扩展并添加自己的过滤器和函数。
贡献
有插件想要分享吗?提交一个拉取请求,将其添加到列表中!
Media
媒体
我们试图跟踪我们能够找到的关于 Flight 的各种媒体类型。请参阅下面的不同资源,以了解更多关于 Flight 的信息。
文章和写作
- 定义、生成和实现:使用 OpenAPI Generator 和 FlightPHP 的 API 首先方法 由 Daniel Schreiber (2025)
- 2024 年最佳 PHP 微框架 由 n0nag0n (2024)
- 使用 Flight Framework 创建 RESTful API 由 n0nag0n (2024)
- 使用 Flight 第 2 部分构建简单博客 由 n0nag0n (2024)
- 使用 Flight 第 1 部分构建简单博客 由 n0nag0n (2024)
- 🚀 使用 Flight Framework 在 PHP 中构建简单的 CRUD API 由 soheil-khaledabadi (2024)
- 使用 Flight 微框架构建 PHP Web 应用程序 由 Arthur C. Codex (2023)
- 2024 年 Web 开发的最佳 PHP 框架 由 Ravikiran A S (2023)
- 2023 年最佳 12 个 PHP 框架:全面指南 由 marketing kbk (2023)
- 你(可能)从未听说过的 5 个 PHP 框架 由 n0nag0n (2022)
- 2023 年 Web 开发者考虑的 12 个顶级 PHP 框架 由 Anna Monus (2022)
- 云服务器上最佳的 PHP 微框架 由 Shahzeb Ahmed (2021)
- PHP 框架:适合您的 Web 开发的 15 个强大框架 由 AHT Tech (2020)
- 使用 FlightPHP 进行简单的 PHP 路由 由 Lucas Conceição (2019)
- 尝试新的 PHP 框架(Flight) 由 Leon (2017)
- 设置 FlightPHP 以与 Backbonejs 一起使用 由 Timothy Tocci (2015)
视频和教程
- 使用 PHP 和 FlightPHP 为 IoT 设备创建 REST API - ESP32 API 由 IoT Craft Hub (2024)
- PHP Flight Framework 简单介绍视频 由 n0nag0n (2024)
- 在 Flightphp 中设置 HTTP 头代码(3 种解决方案!) 由 Roel Van de Paar (2024)
- PHP Flight Framework 教程。超级简单的 API 项目! 由 n0nag0n (2022)
- 使用 php 和 mysql 以及 bootstrap 的 CRUD Web 应用 由 Devlopteca - Oscar Uh (2021)
- DevOps 和 SysAdmins:Flight PHP 微框架的 Lighttpd 重写规则 由 Roel Van de Paar (2021)
- REST API Flight PHP 教程 #PART2 插入表格信息 #代码 (塔加路) 由 Info Singkat Official (2020)
- REST API Flight PHP 教程 #PART1 信息 #代码 (塔加路) 由 Info Singkat Official (2020)
- 如何在 PHP 中创建 JSON REST API - 第 2 部分 由 Codewife (2018)
- 如何在 PHP 中创建 JSON REST API - 第 1 部分 由 Codewife (2018)
- 测试微框架 PHP - Flight PHP, Lumen, Slim 3 和 Laravel 由 Codemarket (2016)
- 教程 1 Flight PHP - 安装 由 absagg (2014)
- 教程 2 Flight PHP - 路由 第 1 部分 由 absagg (2014)
Examples
需要快速入门吗?
您有两种选择可以开始一个新的 Flight 项目:
社区贡献的示例:
- flightravel:带有 PHP 工具 + GH Actions 的 FlightPHP 和 Laravel 目录
- fleact - 带有 ReactJS 集成的 FlightPHP 启动工具包。
- flastro - 带有 Astro 集成的 FlightPHP 启动工具包。
- velt - Velt 是一个快速简单的 Svelte 启动模板,带有 FlightPHP 后端。
需要一些灵感吗?
虽然这些不是官方由 Flight 团队赞助的,但它们可以给您一些构建自己的 Flight 项目结构的想法!
- Decay - Flight v3,使用 HTMX 和 SleekDB,主题为僵尸! (演示)
- Flight 示例博客 - Flight v3,使用中间件、控制器、活动记录和 Latte。
- Flight CRUD RESTful API - 使用 Flight 框架的简单 CRUD API 项目,为新用户提供一个基本结构,以快速设置带有 CRUD 操作和数据库连接的 PHP 应用。该项目演示了如何使用 Flight 进行 RESTful API 开发,使其成为初学者的理想学习工具,以及对于更有经验的开发人员来说是一个有用的启动工具包。
- Flight 学校管理系统 - Flight v3
- 带评论的 Paste Bin - Flight v3
- 基本骨架应用
- 示例 Wiki
- IT创新者 PHP 框架应用
- LittleEducationalCMS (西班牙语)
- 意大利黄页 API
- 通用内容管理系统(带有……非常少的文档)
- 基于 Flight 和 medoo 的小型 PHP 框架。
- 示例 MVC 应用
想要分享您自己的示例吗?
如果您有一个项目想要分享,请提交拉取请求以将其添加到此列表中!
Install/install
安装
下载文件
确保您的系统上已安装PHP。如果没有,请单击这里获取有关如何为您的系统安装它的说明。
如果您使用Composer,可以运行以下命令:
composer require flightphp/core
或者您可以下载文件并将其直接提取到您的web目录中。
配置您的 Web 服务器
内置 PHP 开发服务器
这是迄今为止最简单的启动方式。您可以使用内置服务器来运行应用程序,甚至可以使用SQLite作为数据库(只要您的系统上安装了sqlite3)而无需进行太多设置!只需在安装了PHP后运行以下命令:
php -S localhost:8000
然后在浏览器中打开http://localhost:8000
。
如果您想将项目的文档根目录设置为不同的目录(例如:您的项目是~/myproject
,但您的文档根目录是~/myproject/public/
),则可以在进入~/myproject
目录后运行以下命令:
php -S localhost:8000 -t public/
然后在浏览器中打开http://localhost:8000
。
Apache
确保Apache已经安装在您的系统上。如果没有,请搜索如何在您的系统上安装Apache。
对于Apache,请使用以下内容编辑您的.htaccess
文件:
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L]
注意:如果您需要在子目录中使用flight,请在
RewriteEngine On
之后添加一行RewriteBase /subdir/
。注意:如果要保护所有服务器文件,例如数据库或env文件。请将以下内容放入您的
.htaccess
文件:
RewriteEngine On
RewriteRule ^(.*)$ index.php
Nginx
确保Nginx已经安装在您的系统上。如果没有,请搜索如何在您的系统上安装Nginx。
对于Nginx,请将以下内容添加到您的服务器声明中:
server {
location / {
try_files $uri $uri/ /index.php;
}
}
创建您的 index.php
文件
<?php
// If you're using Composer, require the autoloader.
require 'vendor/autoload.php';
// 如果您没有使用Composer,请直接加载框架
// require 'flight/Flight.php';
// 然后定义一个路由,并分配一个处理请求的函数。
Flight::route('/', function () {
echo 'hello world!';
});
// 最后,启动框架。
Flight::start();
安装 PHP
如果您的系统上已安装php
,请跳过这些说明并转到下载部分
当然!以下是在macOS、Windows 10/11、Ubuntu 和 Rocky Linux上安装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
- 安装最新版本:
-
在不同版本之间切换:
- 解除当前版本的链接并链接所需的版本:
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 repository:
- 打开终端并运行:
sudo dnf install epel-release
- 打开终端并运行:
-
安装 Remi's repository:
- 运行:
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 处理模板
- 为帖子实现路由
- 存储和检索数据
- 处理表单提交
- 基本错误处理
这个教程非常适合想要了解真实应用程序中所有组件如何结合在一起的初学者。
非官方指南
虽然这些指南并未由 Flight 团队正式维护,但它们是社区创建的有价值资源。它们涵盖各种主题和用例,提供了有关使用 Flight PHP 的额外见解。
使用 Flight 框架创建 RESTful API
本指南将引导您使用 Flight PHP 框架创建一个 RESTful API。它涵盖了设置 API、定义路由和返回 JSON 响应的基础知识。
构建一个简单的博客
本指南将引导您使用 Flight PHP 框架创建一个基本的博客。它实际上分为两部分:一部分涵盖基础知识,另一部分涵盖更多高级主题和为生产就绪的博客所做的改进。
- 构建一个简单的博客与 Flight - 第 1 部分 - 开始创建一个简单的博客。
- 构建一个简单的博客与 Flight - 第 2 部分 - 为生产完善博客。
在 PHP 中构建一个宝可梦 API:初学者指南
这个有趣的指南将引导您使用 Flight PHP 创建一个简单的宝可梦 API。它涵盖了设置 API、定义路由和返回 JSON 响应的基础知识。
贡献
有指南的想法吗?发现了错误?我们欢迎贡献!我们的指南在 FlightPHP 文档库 中维护。
如果您使用 Flight 构建了一些有趣的东西并想以指南的形式分享,请提交拉取请求。分享您的知识有助于 Flight 社区成长。
寻找 API 文档?
如果您正在寻找有关 Flight 核心功能和方法的具体信息,请查看我们文档的学习部分。