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 相比的优点

与 Flight 相比的缺点

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

可用配置设置

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

加载器配置

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

// 启用带下划线的类加载
// 默认为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.comobservatory.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.phpindex.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);
// 这将输出:&lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;

// 如果您使用像 Latte 这样的视图类,它也会自动转义。
Flight::view()->render('template', ['name' => $name]);

SQL 注入

SQL 注入是一种攻击,其中恶意用户可以向您的数据库注入 SQL 代码。这可以用于窃取数据库中的信息或对数据库执行操作。同样,您绝对不应信任用户的输入!始终假设他们是怀着恶意的。您可以在 PDO 对象中使用预处理语句来防止 SQL 注入。

// 假设您已经将 Flight::db() 注册为您的 PDO 对象
$statement = Flight::db()->prepare('SELECT * FROM users WHERE username = :username');
$statement->execute([':username' => $username]);
$users = $statement->fetchAll();

// 如果您使用 PdoWrapper 类,那么可以轻松通过一行代码完成
$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();

但是,像 mapregister 这样的框架方法不能被覆盖。如果您尝试这样做,将会收到错误提示。

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.indexusers.create 等。如果您想更改别名,请将 aliasBase 设置为您想要的值。

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

仅和排除

您还可以通过使用 onlyexcept 选项来指定要创建的路由。

Flight::resource('/users', UsersController::class, [ 'only' => [ 'index', 'show' ] ]);
Flight::resource('/users', UsersController::class, [ 'except' => [ 'create', 'store', 'edit', 'update', 'destroy' ] ]);

这些实际上是白名单和黑名单选项,以便您可以指定要创建的路由。

中间件

您还可以指定在 resource 方法创建的每个路由上运行的中间件。

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

流式传输

您现在可以使用 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 相比的优势

与 Flight 相比的缺点

Learn/flight_vs_another_framework

将Flight与另一个框架进行比较

如果您正在从另一个框架(如Laravel、Slim、Fat-Free或Symfony)迁移到Flight,则此页面将帮助您了解两者之间的区别。

Laravel

Laravel是一个功能齐全的框架,拥有所有功能和令人惊叹的开发人员专注生态系统,但需要在性能和复杂性方面付出代价。

查看Laravel和Flight之间的比较.

Slim

Slim是一个微框架,类似于Flight。它旨在轻量且易于使用,但可能比Flight复杂一些。

查看Slim和Flight之间的比较.

Fat-Free

Fat-Free是一个体积更小的全栈框架。虽然它拥有所有工具,但其数据架构可能使一些项目比必要复杂。

查看Fat-Free和Flight之间的比较.

Symfony

Symfony是一个模块化的企业级框架,旨在灵活且可扩展。对于较小的项目或新手开发人员,Symfony可能有些令人生畏。

查看Symfony和Flight之间的比较.

Learn/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!”

在您使用中间件之前,请务必了解一些非常重要的内容:

中间件类

中间件也可以注册为类。如果需要“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!”

处理中间件错误

假设您有一个授权中间件,并希望如果用户没有经过身份验证,则将用户重定向到登录页面。您有几个选择:

  1. 您可以从中间件函数中返回false,Flight将自动返回403 Forbidden错误,但无法自定义。
  2. 您可以使用Flight::redirect()将用户重定向到登录页面。
  3. 您可以在中间件中创建自定义错误,并停止路由的执行。

基本示例

这是一个简单的返回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;
});

请注意,mapregister 等核心方法无法进行过滤,因为它们是直接调用而不是动态调用的。

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 保存到数据库或其他内容
});

请求对象属性

请求对象提供以下属性:

您可以将 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 响应。

mapregister 添加的任何自定义方法也可进行筛选。

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) // 触发事件。

任何使用 mapregister 添加的自定义方法也可以被过滤。有关如何映射这些方法的示例,请参见 扩展 Flight 指南。

Learn/why_frameworks

为什么使用一个框架?

一些程序员强烈反对使用框架。他们认为框架臃肿、缓慢且难以学习。 他们说框架是不必要的,你可以不用框架编写更好的代码。 关于使用框架的缺点有一些合理的观点。然而,使用框架也有许多优势。

使用框架的原因

下面是您可能考虑使用框架的一些原因:

Flight 是一个微框架。这意味着它又小又轻量。它的功能不及像 Laravel 或 Symfony 这样的大型框架多。 然而,它确实提供了构建 Web 应用程序所需的许多功能。而且学习和使用它也很容易。 这使其成为迅速轻松构建 Web 应用程序的不错选择。如果您对框架还不熟悉,Flight 是一个很好的开始框架。 它将帮助您了解使用框架的优势,而不会让您在太复杂的内容中迷失方向。 在您有了使用 Flight 的经验后,将更容易转向像 Laravel 或 Symfony 这样更复杂的框架, 但 Flight 仍然可以构建成功的强大应用程序。

什么是路由?

路由是 Flight 框架的核心,但究竟是什么呢?路由是将 URL 与代码中的特定功能匹配的过程。 这是您可以根据被请求的 URL 使您的网站执行不同操作的方法。例如,当用户访问 /user/1234 时,您可能希望显示用户的个人资料, 但当他们访问 /users 时显示所有用户的列表。所有这些都通过路由完成。

可能像这样运作:

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

为什么重要?

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

可能您熟悉逐个脚本的方式创建网站。您可能有一个名为 index.php 的文件,其中包含一堆 if 语句,以检查 URL,然后根据 URL 运行特定函数。 这是一种形式的路由,但不够有组织并且很快就会失控。Flight 的路由系统是一种更有组织和强大的处理路由的方式。

这样?


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

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

// 等等...

还是这样?


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

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

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

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

请求与响应

Flight 提供了一种简单易用的方式处理请求和响应。这是 Web 框架的核心。它接收来自用户浏览器的请求, 处理它,然后发送回响应。这就是您可以构建显示用户资料、让用户登录或让用户发布新博客文章等功能的 Web 应用程序的方法。

请求

当用户访问您的网站时,请求是用户的浏览器发送到服务器的内容。这个请求包含关于用户想要执行的操作的信息。 例如,它可能包含关于用户想要访问的 URL、用户想要发送到您的服务器或从用户那里接收的数据的信息。重要的是要知道请求是只读的。 您无法更改请求,但可以从中读取数据。

Flight 提供了一种简单的方法来访问有关请求的信息。您可以使用 Flight::request() 方法来访问请求的信息。 此方法返回一个包含请求信息的 Request 对象。您可以使用这个对象来访问请求的信息,例如 URL、方法或用户发送到您的服务器的数据。

响应

当用户访问您的网站时,服务器发送给用户浏览器的内容就是响应。这个响应包含关于服务器要执行的操作的信息。 例如,它可能包含关于服务器想要发送给用户的数据、服务器想要从用户那里接收的数据或服务器想要存储在用户计算机上的数据的信息。

Flight 提供了一种简单的方法将响应发送给用户的浏览器。您可以使用 Flight::response() 方法发送响应。 此方法接受一个 Response 对象作为参数,并将响应发送给用户的浏览器。您可以使用这个对象将响应发送给用户的浏览器,例如 HTML、JSON 或文件。 Flight 可以帮助您自动生成响应的某些部分,使事情变得容易,但最终您可以控制发送给用户的内容。

Learn/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 '此内容将被缓存。';
});

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

Learn/responses

响应

Flight 帮助生成部分响应头,但你对返回给用户的内容拥有大部分控制权。有时你可以直接访问 Response 对象,但大多数时候你会使用 Flight 实例来发送响应。

发送基本响应

Flight 使用 ob_start() 来缓冲输出。这意味着你可以使用 echoprint 将响应发送给用户,Flight 会捕获它并将其连同适当的头部发送回用户。


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

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

作为替代,你可以调用 write() 方法来添加到响应体中。


// 这将向用户的浏览器发送 "Hello, World!"
Flight::route('/', function() {
    // 虽然冗长,但在某些情况下确实能派上用场
    Flight::response()->write("Hello, World!");

    // 如果你想检索在此时设置的响应体
    // 你可以像这样做
    $body = Flight::response()->getBody();
});

状态码

你可以通过使用 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_ERRORJSON_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 '此内容将被缓存。';
});

请记住,调用 lastModifiedetag 都将设置并检查缓存值。如果请求之间的缓存值相同,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

工作原理

你通过告诉 Flight 事件发生时该做什么来“订阅”一个事件。回调可以接受事件触发时传递的参数。

Flight 的事件系统是同步的,这意味着每个事件监听器按顺序执行,一个接一个。当你触发一个事件时,该事件所有注册的监听器将在代码继续之前运行完成。这一点很重要,因为与异步事件系统不同,异步事件系统的监听器可能会并行运行或在稍后的时间运行。

简单示例

Flight::onEvent('user.login', function ($username) {
    echo "欢迎回来,$username!";
});

在这里,当触发 'user.login' 事件时,它会以用户的名字向其问候。

关键点

触发事件

要使事件发生,请使用 Flight::triggerEvent()。这告诉 Flight 运行所有为该事件注册的监听器,并传递你提供的任何数据。

语法

Flight::triggerEvent(string $event, ...$args): void

简单示例

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

选项 3:靠近触发的位置

另一种方法是在触发事件的地方注册事件,例如在控制器或路由定义内。如果事件特定于应用的某一部分,这种方法效果很好。

Flight::route('/signup', function () {
    // 在这里注册事件
    Flight::onEvent('user.registered', function ($email) {
        echo "欢迎邮件已发送给 $email!";
    });

    $email = 'jane@example.com';
    Flight::triggerEvent('user.registered', $email);
    echo "注册成功!";
});

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 已更新。";
});

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

最佳实践

Flight PHP 中的事件系统以及 Flight::onEvent()Flight::triggerEvent() 为你提供了一种简单而强大的方式来构建灵活的应用。通过让应用的不同部分通过事件进行通信,你可以保持代码组织良好、可重用且易于扩展。无论是记录操作、发送通知还是管理更新,事件都能帮助你做到这一点,而不让你的逻辑纠缠在一起。而且,能够重写这些方法使你有自由度来根据需要调整系统。从单个事件开始,看看它如何改变你应用的结构!

内置事件

Flight PHP 还附带了一些内置事件,你可以利用这些事件来挂钩到框架的生命周期。这些事件在请求/响应周期的特定时点触发,允许你在某些操作发生时执行自定义逻辑。

内置事件列表

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

然后,您的视图将保存名为headerContentbodyContent的变量。然后,您可以通过执行以下操作来呈现您的布局:

Flight::render('layout', ['title' => 'Home Page']);

如果模板文件如下所示:

header.php

<h1><?= $heading ?></h1>

body.php

<div><?= $body ?></div>

layout.php

<html>
  <head>
    <title><?= $title ?></title>
  </head>
  <body>
    <?= $headerContent ?>
    <?= $bodyContent ?>
  </body>
</html>

输出将是:

<html>
  <head>
    <title>Home Page</title>
  </head>
  <body>
    <h1>Hello</h1>
    <div>World</div>
  </body>
</html>

自定义视图

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

您的视图将保存称为 headerContentbodyContent 的变量。然后,您可以通过执行以下操作来呈现布局:

Flight::render('layout', ['title' => 'Home Page']);

如果模板文件看起来像这样:

header.php

<h1><?= $heading ?></h1>

body.php

<div><?= $body ?></div>

layout.php

<html>
  <head>
    <title><?= $title ?></title>
  </head>
  <body>
    <?= $headerContent ?>
    <?= $bodyContent ?>
  </body>
</html>

输出将是:

<html>
  <head>
    <title>Home Page</title>
  </head>
  <body>
    <h1>Hello</h1>
    <div>World</div>
  </body>
</html>

Smarty

以下是如何在您的视图中使用 Smarty 模板引擎:

// 加载 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相比的优势

与Flight相比的缺点

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

你还可以使用 beforeafter 方法创建一个基本的 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();

但是,像 mapregister 这样的框架方法无法被覆盖。如果你尝试这样做,将会出现错误。

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相比的优点

与Flight相比的缺点

Learn/autoloading

自動加載

自動加載是 PHP 中的一個概念,在這裡您指定要從哪些目錄加載類。這比使用requireinclude來加載類要好得多。這也是使用 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 的简单性和灵活性。到最后,您将拥有一个功能性博客,包含主页、单独的帖子页面和创建表单。

先决条件

第一步:设置您的项目

首先创建一个新的项目目录并通过 Composer 安装 Flight。

  1. 创建目录

    mkdir flight-blog
    cd flight-blog
  2. 安装 Flight

    composer require flightphp/core
  3. 创建公共目录: Flight 使用单个入口点 (index.php)。为其创建 public/ 文件夹:

    mkdir public
  4. 基本的 index.php: 创建 public/index.php,添加简单的“你好,世界”路由:

    <?php
    require '../vendor/autoload.php';
    
    Flight::route('/', function () {
       echo '你好,Flight!';
    });
    
    Flight::start();
  5. 运行内置服务器: 使用 PHP 的开发服务器测试您的设置:

    php -S localhost:8000 -t public/

    访问 http://localhost:8000 查看“你好,Flight!”。

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

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

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

第三步:安装和配置 Latte

Latte 是一个轻量级的模板引擎,与 Flight 很好地集成。

  1. 安装 Latte

    composer require latte/latte
  2. 在 Flight 中配置 Latte: 更新 public/index.php 以将 Latte 注册为视图引擎:

    <?php
    require '../vendor/autoload.php';
    
    use Latte\Engine;
    
    Flight::register('view', Engine::class, [], function ($latte) {
       $latte->setTempDirectory(__DIR__ . '/../cache/');
       $latte->setLoader(new \Latte\Loaders\FileLoader(__DIR__ . '/../app/views/'));
    });
    
    Flight::route('/', function () {
       Flight::view()->render('home.latte', ['title' => '我的博客']);
    });
    
    Flight::start();
  3. 创建布局模板:在 app/views/layout.latte

    <!DOCTYPE html>
    <html>
    <head>
    <title>{$title}</title>
    </head>
    <body>
    <header>
        <h1>我的博客</h1>
        <nav>
            <a href="/">首页</a> | 
            <a href="/create">创建帖子</a>
        </nav>
    </header>
    <main>
        {block content}{/block}
    </main>
    <footer>
        <p>&copy; {date('Y')} Flight 博客</p>
    </footer>
    </body>
    </html>
  4. 创建首页模板: 在 app/views/home.latte

    {extends 'layout.latte'}
    
    {block content}
        <h2>{$title}</h2>
        <ul>
        {foreach $posts as $post}
            <li><a href="/post/{$post['slug']}">{$post['title']}</a></li>
        {/foreach}
        </ul>
    {/block}

    如果退出服务器,请重新启动,并访问 http://localhost:8000 查看渲染页面。

  5. 创建数据文件

    使用 JSON 文件模拟数据库以简化操作。

    data/posts.json

    [
       {
           "slug": "first-post",
           "title": "我的第一篇帖子",
           "content": "这是我用 Flight PHP 撰写的第一篇博客帖子!"
       }
    ]

第四步:定义路由

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

  1. 创建 routes.php: 在 app/config/routes.php

    <?php
    Flight::route('/', function () {
       Flight::view()->render('home.latte', ['title' => '我的博客']);
    });
    
    Flight::route('/post/@slug', function ($slug) {
       Flight::view()->render('post.latte', ['title' => '帖子:' . $slug, 'slug' => $slug]);
    });
    
    Flight::route('GET /create', function () {
       Flight::view()->render('create.latte', ['title' => '创建帖子']);
    });
  2. 更新 index.php: 包含路由文件:

    <?php
    require '../vendor/autoload.php';
    
    use Latte\Engine;
    
    Flight::register('view', Engine::class, [], function ($latte) {
       $latte->setTempDirectory(__DIR__ . '/../cache/');
       $latte->setLoader(new \Latte\Loaders\FileLoader(__DIR__ . '/../app/views/'));
    });
    
    require '../app/config/routes.php';
    
    Flight::start();

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

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

  1. 添加帖子方法: 在 index.php 中,添加一个加载帖子的的方法:

    Flight::map('posts', function () {
       $file = __DIR__ . '/../data/posts.json';
       return json_decode(file_get_contents($file), true);
    });
  2. 更新路由: 修改 app/config/routes.php 以使用帖子:

    <?php
    Flight::route('/', function () {
       $posts = Flight::posts();
       Flight::view()->render('home.latte', [
           'title' => '我的博客',
           'posts' => $posts
       ]);
    });
    
    Flight::route('/post/@slug', function ($slug) {
       $posts = Flight::posts();
       $post = array_filter($posts, fn($p) => $p['slug'] === $slug);
       $post = reset($post) ?: null;
       if (!$post) {
           Flight::notFound();
           return;
       }
       Flight::view()->render('post.latte', [
           'title' => $post['title'],
           'post' => $post
       ]);
    });
    
    Flight::route('GET /create', function () {
       Flight::view()->render('create.latte', ['title' => '创建帖子']);
    });

第六步:创建模板

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

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

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

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

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

  1. 创建表单 (app/views/create.latte)

    {extends 'layout.latte'}
    
    {block content}
        <h2>{$title}</h2>
        <form method="POST" action="/create">
            <div class="form-group">
                <label for="title">标题:</label>
                <input type="text" name="title" id="title" required>
            </div>
            <div class="form-group">
                <label for="content">内容:</label>
                <textarea name="content" id="content" required></textarea>
            </div>
            <button type="submit">保存帖子</button>
        </form>
    {/block}
  2. 添加 POST 路由: 在 app/config/routes.php

    Flight::route('POST /create', function () {
       $request = Flight::request();
       $title = $request->data['title'];
       $content = $request->data['content'];
       $slug = strtolower(str_replace(' ', '-', $title));
    
       $posts = Flight::posts();
       $posts[] = ['slug' => $slug, 'title' => $title, 'content' => $content];
       file_put_contents(__DIR__ . '/../../data/posts.json', json_encode($posts, JSON_PRETTY_PRINT));
    
       Flight::redirect('/');
    });
  3. 测试它

    • 访问 http://localhost:8000/create
    • 提交新的帖子(例如,“第二篇帖子”,以及一些内容)。
    • 检查主页以查看其是否已列出。

第八步:增强错误处理

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

index.php

Flight::map('notFound', function () {
    Flight::view()->render('404.latte', ['title' => '未找到页面']);
});

创建 app/views/404.latte

{extends 'layout.latte'}

{block content}
    <h2>404 - {$title}</h2>
    <p>抱歉,该页面不存在!</p>
{/block}

下一步

结论

您已使用 Flight PHP 构建了一个简单的博客!本指南展示了核心功能,如路由、使用 Latte 进行模板处理和处理表单提交——同时保持轻量化。探索 Flight 的文档以获取更多高级功能以进一步提升您的博客!

License

MIT 许可证

版权所有 © 2024 @mikecao, @n0nag0n

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

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

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

About

什么是 Flight?

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

Matrix

以及 Discord

贡献

有两种方式可以为 Flight 做出贡献:

  1. 你可以通过访问 core repository 为核心框架做出贡献。
  2. 你可以为文档做出贡献。该文档网站托管在 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 文件缓存类

优点

此文档网站正在使用此库来缓存每个页面!

点击 这里 查看代码。

安装

通过 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

主要配置选项:

使用 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一起使用。

调试

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

数据库

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

会话

对于API来说,会话实际上并不那么有用,但对于构建Web应用程序来说,会话可以对保持状态和登录信息至关重要。

模板

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

贡献

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

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 脚本

脚本分为三组脚本:

脚本目录如下:

 <root dir>
     |
     +-- base.sql
     |
     +-- /migrations
              |
              +-- /up
                   |
                   +-- 00001.sql
                   +-- 00002.sql
              +-- /down
                   |
                   +-- 00000.sql
                   +-- 00001.sql

多开发环境

如果您与多个开发人员和多个分支工作,确定下一个数字将会非常困难。

在这种情况下,您可以在版本号后面加上后缀 "-dev"。

看看这个场景:

在这两种情况下,开发人员将创建一个名为 43-dev.sql 的文件。两个开发人员将毫无问题地进行 UP 和 DOWN 迁移,您的本地版本将是 43。

但是开发人员 1 合并了您的更改并创建了最终版本 43.sql (git mv 43-dev.sql 43.sql)。如果开发人员 2 更新您的本地分支,他将拥有一个 43.sql 文件(来自开发人员 1)和您的 43-dev.sql 文件。 如果他试图进行 UP 或 DOWN 迁移,迁移脚本将出现问题并警告他存在两个版本 43。在这种情况下,开发人员 2 将需要更新他的文件为 44-dev.sql,并继续工作,直到合并您的更改并生成最终版本。

使用 PHP API 并将其集成到您的项目中

基本用法是

查看一个示例:

<?php
// 创建连接 URI
// 了解更多: https://github.com/byjg/anydataset#connection-based-on-uri
$connectionUri = new \ByJG\Util\Uri('mysql://migrateuser:migratepwd@localhost/migratedatabase');

// 注册可处理该 URI 的数据库:
\ByJG\DbMigration\Migration::registerDatabase(\ByJG\DbMigration\Database\MySqlDatabase::class);

// 创建 Migration 实例
$migration = new \ByJG\DbMigration\Migration($connectionUri, '.');

// 添加一个回调进度函数以接收执行信息
$migration->addCallbackProgress(function ($action, $currentVersion, $fileInfo) {
    echo "$action, $currentVersion, ${fileInfo['description']}\n";
});

// 使用 "base.sql" 脚本恢复数据库
// 并运行所有现有脚本以将数据库版本提升到最新版本
$migration->reset();

// 运行所有现有脚本以向上或向下迁移数据库版本
// 从当前版本到 $version 编号;
// 如果未指定版本编号,则迁移到最后的数据库版本
$migration->update($version = null);

迁移对象控制数据库版本。

在您的项目中创建版本控制

<?php
// 注册可处理该 URI 的数据库:
\ByJG\DbMigration\Migration::registerDatabase(\ByJG\DbMigration\Database\MySqlDatabase::class);

// 创建 Migration 实例
$migration = new \ByJG\DbMigration\Migration($connectionUri, '.');

// 此命令将在您的数据库中创建版本表
$migration->createVersion();

获取当前版本

<?php
$migration->getCurrentVersion();

添加回调以控制进度

<?php
$migration->addCallbackProgress(function ($command, $version, $fileInfo) {
    echo "执行命令:$command 在版本 $version - ${fileInfo['description']}, ${fileInfo['exists']}, ${fileInfo['file']}, ${fileInfo['checksum']}\n";
});

获取 Db 驱动实例

<?php
$migration->getDbDriver();

要使用它,请访问: https://github.com/byjg/anydataset-db

避免部分迁移(不适用于 MySQL)

部分迁移是指因为错误或手动中断而在过程中中断迁移脚本。

迁移表将处于状态 partial uppartial down,需要手动修复才能再次迁移。

为了避免这种情况,您可以指定迁移将在事务上下文中运行。 如果迁移脚本失败,事务将被回滚,迁移表将标记为 complete,版本将是导致错误的脚本之前的立即前一个版本。

要启用此功能,您需要调用方法 withTransactionEnabled,并传递 true 作为参数:

<?php
$migration->withTransactionEnabled(true);

注意:此功能在 MySQL 中不可用,因为它不支持事务中的 DDL 命令。 如果您在 MySQL 中使用此方法,迁移将悄默忽略它。 更多信息:https://dev.mysql.com/doc/refman/8.0/en/cannot-roll-back.html

编写 Postgres SQL 迁移的提示

创建触发器和 SQL 函数

-- DO
CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$
    BEGIN
        -- 检查 empname 和薪水是否已给出
        IF NEW.empname IS NULL THEN
            RAISE EXCEPTION 'empname 不能为空'; -- 这些注释是否为空并不重要
        END IF; --
        IF NEW.salary IS NULL THEN
            RAISE EXCEPTION '% 不能有空薪水', NEW.empname; --
        END IF; --

        -- 谁为我们工作时必须为此支付?
        IF NEW.salary < 0 THEN
            RAISE EXCEPTION '% 不能有负薪水', NEW.empname; --
        END IF; --

        -- 记住谁在何时更改工资单
        NEW.last_date := current_timestamp; --
        NEW.last_user := current_user; --
        RETURN NEW; --
    END; --
$emp_stamp$ LANGUAGE plpgsql;

-- DON'T
CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$
    BEGIN
        -- 检查 empname 和薪水是否已给出
        IF NEW.empname IS NULL THEN
            RAISE EXCEPTION 'empname 不能为空';
        END IF;
        IF NEW.salary IS NULL THEN
            RAISE EXCEPTION '% 不能有空薪水', NEW.empname;
        END IF;

        -- 谁为我们工作时必须为此支付?
        IF NEW.salary < 0 THEN
            RAISE EXCEPTION '% 不能有负薪水', NEW.empname;
        END IF;

        -- 记住谁在何时更改工资单
        NEW.last_date := current_timestamp;
        NEW.last_user := current_user;
        RETURN NEW;
    END;
$emp_stamp$ LANGUAGE plpgsql;

由于 PDO 数据库抽象层无法运行SQL语句批处理, 当 byjg/migration 读取迁移文件时,必须在分号处分割整个 SQL 文件的内容,并逐个运行语句。然而,有一种语句可以在其主体中包含多个分号:函数。

为了正确解析函数,byjg/migration 从 2.1.0 版本开始,在分号 + 行结束符 (EOL) 的序列分割迁移文件,而不仅仅是分号。这样,如果您在每个函数定义的内部分号后附加一个空注释,byjg/migration 将能够解析它。

遗憾的是,如果您忘记添加任何这些注释,库将把 CREATE FUNCTION 语句分成多个部分,迁移将失败。

避免冒号字符(:

-- DO
CREATE TABLE bookings (
  booking_id UUID PRIMARY KEY,
  booked_at  TIMESTAMPTZ NOT NULL CHECK (CAST(booked_at AS DATE) <= check_in),
  check_in   DATE NOT NULL
);

-- DON'T
CREATE TABLE bookings (
  booking_id UUID PRIMARY KEY,
  booked_at  TIMESTAMPTZ NOT NULL CHECK (booked_at::DATE <= check_in),
  check_in   DATE NOT NULL
);

由于 PDO 使用冒号字符作为准备语句中命名参数的前缀,因此在其他上下文中的使用会导致问题。

例如,PostgreSQL 语句可以使用 :: 在类型之间转换值。另一方面,PDO 将把这视为无效命名参数并在无效上下文中失败。

解决这种不一致的唯一方法是完全避免冒号(在这种情况下,PostgreSQL 还有一种替代语法:CAST(value AS type))。

使用 SQL 编辑器

最后,编写手动 SQL 迁移可能是繁琐的,但如果您使用能够理解 SQL 语法的编辑器,提供自动完成、检查当前数据库架构和/或自动格式化代码,这将简单得多。

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

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

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

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

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

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

运行单元测试

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

vendor/bin/phpunit

运行数据库测试

运行集成测试需要您确保数据库已启用并正在运行。我们提供了一个基本的 docker-compose.yml,您可以用它来启动测试数据库。

运行数据库

docker-compose up -d postgres mysql mssql

运行测试

vendor/bin/phpunit
vendor/bin/phpunit tests/SqliteDatabase*
vendor/bin/phpunit tests/MysqlDatabase*
vendor/bin/phpunit tests/PostgresDatabase*
vendor/bin/phpunit tests/SqlServerDblibDatabase*
vendor/bin/phpunit tests/SqlServerSqlsrvDatabase*

您可以选择设置单元测试使用的主机和密码

export MYSQL_TEST_HOST=localhost     # 默认为 localhost
export MYSQL_PASSWORD=newpassword    # 如果想有一个空密码,请使用 '.'
export PSQL_TEST_HOST=localhost      # 默认为 localhost
export PSQL_PASSWORD=newpassword     # 如果想有一个空密码,请使用 '.'
export MSSQL_TEST_HOST=localhost     # 默认为 localhost
export MSSQL_PASSWORD=Pa55word
export SQLITE_TEST_HOST=/tmp/test.db      # 默认为 /tmp/test.db

Awesome-plugins/session

FlightPHP 会话 - 轻量级文件基础会话处理器

这是一个轻量级的文件基础会话处理器插件,适用于 Flight PHP Framework。它提供了一种简单而强大的会话管理解决方案,具有非阻塞会话读取、可选加密、自动提交功能和开发测试模式等特性。会话数据存储在文件中,适合不需要数据库的应用程序。

如果您确实想使用数据库,可以查看 ghostff/session 插件,它具有许多相同的功能,但使用数据库后端。

访问 Github 仓库 获取完整源代码和详细信息。

安装

通过 Composer 安装插件:

composer require flightphp/session

基本用法

以下是如何在您的 Flight 应用程序中使用 flightphp/session 插件的简单示例:

require 'vendor/autoload.php';

use flight\Session;

$app = Flight::app();

// 注册会话服务
$app->register('session', Session::class);

// 示例路由,使用会话
Flight::route('/login', function() {
    $session = Flight::session();
    $session->set('user_id', 123);
    $session->set('username', 'johndoe');
    $session->set('is_admin', false);

    echo $session->get('username'); // 输出: johndoe
    echo $session->get('preferences', 'default_theme'); // 输出: default_theme

    if ($session->get('user_id')) {
        Flight::json(['message' => '用户已登录!', 'user_id' => $session->get('user_id')]);
    }
});

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

Flight::start();

关键点

配置

您可以在注册时传递选项数组来自定义会话处理器:

$app->register('session', Session::class, [
    'save_path' => '/custom/path/to/sessions',         // 会话文件目录
    '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 类提供以下方法:

除了 get()id() 之外,所有方法都返回 Session 实例以便于链接调用。

为什么使用这个插件?

技术细节

贡献

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

许可证

这个插件遵循 MIT 许可证。详情请参阅 Github 仓库

Awesome-plugins/runway

跑道

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

点击这里 查看代码。

安装

使用 composer 安装。

composer require flightphp/runway

基本配置

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

用法

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

  1. 如果您使用的是骨架项目,可以从项目的根目录运行 php runway [command]
  2. 如果您是通过composer安装的包来使用跑道,您可以从项目的根目录运行 vendor/bin/runway [command]

对于任何命令,您都可以传入 --help 标志以获取有关如何使用命令的更多信息。

php runway routes --help

以下是一些示例:

生成控制器

根据您的 .runway.json 文件中的配置,默认位置将为您在 app/controllers/ 目录中生成一个控制器。

php runway make:controller MyController

生成活动记录模型

根据您的 .runway.json 文件中的配置,默认位置将为您在 app/records/ 目录中生成一个控制器。

php runway make:record users

例如,如果您有以下架构的 users 表:idnameemailcreated_atupdated_at,则类似以下内容的文件将在 app/records/UserRecord.php 文件中创建:

<?php

declare(strict_types=1);

namespace app\records;

/**
 * users 表的ActiveRecord类。
 * @link https://docs.flightphp.com/awesome-plugins/active-record
 * 
 * @property int $id
 * @property string $name
 * @property string $email
 * @property string $created_at
 * @property string $updated_at
 * // 一旦在 $relations 数组中定义了关系,您也可以在此处添加关系
 * @property CompanyRecord $company 关系示例
 */
class UserRecord extends \flight\ActiveRecord
{
    /**
     * @var array $relations 为该模型设置关系
     *   https://docs.flightphp.com/awesome-plugins/active-record#relationships
     */
    protected array $relations = [];

    /**
     * 构造函数
     * @param mixed $databaseConnection 数据库连接
     */
    public function __construct($databaseConnection)
    {
        parent::__construct($databaseConnection, 'users');
    }
}

显示所有路由

这将显示当前在Flight中注册的所有路由。

php runway routes

如果您只想查看特定路由,您可以传入一个标志以过滤路由。

# 仅显示 GET 路由
php runway routes --get

# 仅显示 POST 路由
php runway routes --post

# 等等

自定义跑道

如果您要为Flight创建一个包,或者想要将自定义命令添加到您的项目中,您可以通过为您的项目/包创建一个 src/commands/, flight/commands/, app/commands/, 或 commands/ 目录来实现此目的。

要创建一个命令,您只需扩展 AbstractBaseCommand 类,并至少实现一个 __construct 方法和一个 execute 方法。

<?php

declare(strict_types=1);

namespace flight\commands;

class ExampleCommand extends AbstractBaseCommand
{
    /**
     * 构造函数
     *
     * @param array<string,mixed> $config 来自 .runway-config.json 的JSON配置
     */
    public function __construct(array $config)
    {
        parent::__construct('make:example', '为文档创建示例', $config);
        $this->argument('<funny-gif>', '滑稽gif的名称');
    }

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

        $io->info('创建示例...');

        // 在这里执行操作

        $io->ok('示例已创建!');
    }
}

有关如何将自定义命令构建到您的Flight应用程序中的更多信息,请参阅 adhocore/php-cli 文档

Awesome-plugins/tracy_extensions

Tracy Flight Panel Extensions

=====

这是一组扩展,可以让与 Flight 的工作变得更加丰富。

这是面板

Flight Bar

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

Flight Data Flight Database Flight Request

单击这里查看代码。

安装

运行 composer require flightphp/tracy-extensions --dev,您就开始了!

配置

您需要做很少的配置才能启动此功能。在使用此功能之前,您需要初始化 Tracy 调试器https://tracy.nette.org/en/guide

<?php

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

// 引导代码
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);
}

有用提示

当您调试代码时,有一些非常有用的函数可以为您输出数据。

Awesome-plugins/active_record

Flight 活跃记录

活跃记录是将数据库实体映射到 PHP 对象。通俗地讲,如果你的数据库中有一个用户表,你可以将该表中的一行“翻译”为 User 类和你代码库中的 $user 对象。请参见 基本示例

点击 这里 查看 GitHub 中的代码库。

基本示例

假设你有以下表格:

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

现在你可以设置一个新类来表示该表:

/**
 * 活跃记录类通常是单数形式
 * 
 * 强烈建议在这里添加表的属性作为注释
 * 
 * @property int    $id
 * @property string $name
 * @property string $password
 */ 
class User extends flight\ActiveRecord {
    public function __construct($database_connection)
    {
        // 你可以这样设置
        parent::__construct($database_connection, 'users');
        // 或者这样
        parent::__construct($database_connection, null, [ 'table' => 'users']);
    }
}

现在看魔法发生吧!

// 用于 sqlite
$database_connection = new PDO('sqlite:test.db'); // 这只是示例,你可能会使用实际的数据库连接

// 用于 mysql
$database_connection = new PDO('mysql:host=localhost;dbname=test_db&charset=utf8bm4', 'username', 'password');

// 或者 mysqli
$database_connection = new mysqli('localhost', 'username', 'password', 'test_db');
// 或者通过非面向对象的方式创建 mysqli
$database_connection = mysqli_connect('localhost', 'username', 'password', 'test_db');

$user = new User($database_connection);
$user->name = 'Bobby Tables';
$user->password = password_hash('some cool password');
$user->insert();
// 或 $user->save();

echo $user->id; // 1

$user->name = 'Joseph Mamma';
$user->password = password_hash('some cool password again!!!');
$user->insert();
// 不能在这里使用 $user->save() 否则会认为这是一个更新!

echo $user->id; // 2

添加新用户就是这么简单!现在数据库中有一个用户行,你如何提取它?

$user->find(1); // 在数据库中查找 id = 1 并返回。
echo $user->name; // 'Bobby Tables'

如果你想查找所有用户呢?

$users = $user->findAll();

带有某个条件呢?

$users = $user->like('name', '%mamma%')->findAll();

看看这有多有趣?让我们安装它并开始吧!

安装

只需通过 Composer 安装

composer require flightphp/active-record 

用法

这可以作为独立库使用或与 Flight PHP 框架一起使用。完全由你决定。

独立使用

只需确保将 PDO 连接传递给构造函数。

$pdo_connection = new PDO('sqlite:test.db'); // 这只是示例,你可能会使用实际的数据库连接

$User = new User($pdo_connection);

不想在构造函数中总是设置你的数据库连接?请参见 数据库连接管理 以获取其他想法!

在 Flight 中注册为方法

如果你正在使用 Flight PHP 框架,可以将 ActiveRecord 类注册为服务,但你实际上不必这样做。

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

// 然后你可以在控制器、函数等中这样使用它。

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

runway 方法

runway 是一个用于 Flight 的 CLI 工具,它为该库有一个自定义命令。

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

# 示例
php runway make:record users

这将在 app/records/ 目录中创建一个名为 UserRecord.php 的新类,内容如下:

<?php

declare(strict_types=1);

namespace app\records;

/**
 * 用户表的活跃记录类。
 * @link https://docs.flightphp.com/awesome-plugins/active-record
 *
 * @property int $id
 * @property string $username
 * @property string $email
 * @property string $password_hash
 * @property string $created_dt
 */
class UserRecord extends \flight\ActiveRecord
{
    /**
     * @var array $relations 设置模型的关系
     *   https://docs.flightphp.com/awesome-plugins/active-record#relationships
     */
    protected array $relations = [
        // 'relation_name' => [ self::HAS_MANY, 'RelatedClass', 'foreign_key' ],
    ];

    /**
     * 构造函数
     * @param mixed $databaseConnection 到数据库的连接
     */
    public function __construct($databaseConnection)
    {
        parent::__construct($databaseConnection, 'users');
    }
}

CRUD 函数

find($id = null) : boolean|ActiveRecord

查找一条记录并分配给当前对象。如果你传递一个类型的 $id,它将使用该值在主键上执行查找。如果没有传递任何内容,它将只查找表中的第一条记录。

此外,你可以传递其他辅助方法来查询你的表。

// 预先查找满足某些条件的记录
$user->notNull('password')->orderBy('id DESC')->find();

// 按特定 id 查找记录
$id = 123;
$user->find($id);

findAll(): array<int,ActiveRecord>

查找你指定的表中的所有记录。

$user->findAll();

isHydrated(): boolean (v0.4.0)

如果当前记录已被填充(从数据库中获取),则返回 true

$user->find(1);
// 如果找到的数据记录...
$user->isHydrated(); // true

insert(): boolean|ActiveRecord

将当前记录插入数据库。

$user = new User($pdo_connection);
$user->name = 'demo';
$user->password = md5('demo');
$user->insert();
基于文本的主键

如果你有一个基于文本的主键(例如 UUID),你可以通过两种方式在插入之前设置主键值。

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

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

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

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

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

update(): boolean|ActiveRecord

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

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

save(): boolean|ActiveRecord

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

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

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

delete(): boolean

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

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

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

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

dirty(array $dirty = []): ActiveRecord

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

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

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

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

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

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

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

这是 dirty() 方法的别名。这样你所做的更加清晰。

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

isDirty(): boolean (v0.4.0)

如果当前记录已更改,则返回 true

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

reset(bool $include_query_data = true): ActiveRecord

将当前记录重置为其初始状态。这在循环行为中非常好用。 如果你传递 true,它还将重置用于查找当前对象的查询数据(默认行为)。

$users = $user->greaterThan('id', 0)->orderBy('id desc')->find();
$user_company = new UserCompany($pdo_connection);

foreach($users as $user) {
    $user_company->reset(); // 从干净的状态开始
    $user_company->user_id = $user->id;
    $user_company->company_id = $some_company_id;
    $user_company->insert();
}

getBuiltSql(): string (v0.4.1)

在你运行 find()findAll()insert()update()save() 方法后,你可以获取构建的 SQL,并将其用于调试目的。

SQL 查询方法

select(string $field1 [, string $field2 ... ])

如果你愿意,可以只选择表中的少数列(对于列很多的宽表来说,这更高效)

$user->select('id', 'name')->find();

from(string $table)

你也可以选择其他表!为什么不呢?

$user->select('id', 'name')->from('user')->find();

join(string $table_name, string $join_condition)

你甚至可以连接到数据库中的另一张表。

$user->join('contacts', 'contacts.user_id = users.id')->find();

where(string $where_conditions)

你可以设置一些自定义的 where 参数(你不能在此 where 语句中设置参数)

$user->where('id=1 AND name="demo"')->find();

安全注意 - 你可能会想做这样的事情 $user->where("id = '{$id}' AND name = '{$name}'")->find();。请不要这样做!!!这容易受到所谓的 SQL 注入攻击。网上有很多文章,请搜索“sql injection attacks php”,你会发现很多关于这个主题的文章。使用这个库时,处理此问题的正确方法是, вместо этого вы должны сделать что-то более подобное $user->eq('id', $id)->eq('name', $name)->find(); 如果你绝对不得不这样做,PDO 库有 $pdo->quote($var) 可以为你转义。只有在你使用 quote() 后,你才能在 where() 语句中使用它。

group(string $group_by_statement)/groupBy(string $group_by_statement)

根据特定条件对结果进行分组。

$user->select('COUNT(*) as count')->groupBy('name')->findAll();

order(string $order_by_statement)/orderBy(string $order_by_statement)

以特定方式对返回的查询进行排序。

$user->orderBy('name DESC')->find();

limit(string $limit)/limit(int $offset, int $limit)

限制返回的记录数量。如果给定第二个整数,它将像 SQL 一样进行偏移和限制。

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

WHERE 条件

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

其中 field = $value

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

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

其中 field <> $value

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

isNull(string $field)

其中 field IS NULL

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

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

其中 field IS NOT NULL

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

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

其中 field > $value

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

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

其中 field < $value

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

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

其中 field >= $value

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

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

其中 field <= $value

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

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

其中 field LIKE $valuefield NOT LIKE $value

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

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

其中 field IN($value)field NOT IN($value)

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

between(string $field, array $values)

其中 field BETWEEN $value AND $value1

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

OR 条件

可以将您的条件包装在 OR 语句中。这是通过使用 startWrap()endWrap() 方法或通过在字段和值之后填充条件的第三个参数来完成的。

// 方法 1
$user->eq('id', 1)->startWrap()->eq('name', 'demo')->or()->eq('name', 'test')->endWrap('OR')->find();
// 这将计算为 `id = 1 AND (name = 'demo' OR name = 'test')`

// 方法 2
$user->eq('id', 1)->eq('name', 'demo', 'OR')->find();
// 这将计算为 `id = 1 OR name = 'demo'`

关系

你可以使用这个库设置多种类型的关系。你可以在表之间设置一对多和一对一的关系。这需要在类中进行一些额外的设置。

设置 $relations 数组并不困难,但猜测正确的语法可能会让人困惑。

protected array $relations = [
    // 你可以为键命名为任何你喜欢的名称。活跃记录的名称可能是一个不错的选择。例如:user,contact,client
    'user' => [
        // 必需
        // self::HAS_MANY,self::HAS_ONE,self::BELONGS_TO
        self::HAS_ONE, // 这是关系的类型

        // 必需
        'Some_Class', // 这是将被引用的“其他”活跃记录类

        // 必需
        // 取决于关系类型
        // self::HAS_ONE = 引用连接的外键
        // self::HAS_MANY = 引用连接的外键
        // self::BELONGS_TO = 引用连接的本地键
        'local_or_foreign_key',
        // 仅供参考,这仅连接到“其他”模型的主键

        // 可选
        [ 'eq' => [ 'client_id', 5 ], 'select' => 'COUNT(*) as count', 'limit' 5 ], // 你在连接关系时想要的额外条件
        // $record->eq('client_id', 5)->select('COUNT(*) as count')->limit(5))

        // 可选
        'back_reference_name' // 如果你想将此关系反向引用回自身,例如:$user->contact->user;
    ];
]
class User extends ActiveRecord{
    protected array $relations = [
        'contacts' => [ self::HAS_MANY, Contact::class, 'user_id' ],
        'contact' => [ self::HAS_ONE, Contact::class, 'user_id' ],
    ];

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

class Contact extends ActiveRecord{
    protected array $relations = [
        'user' => [ self::BELONGS_TO, User::class, 'user_id' ],
        'user_with_backref' => [ self::BELONGS_TO, User::class, 'user_id', [], 'contact' ],
    ];
    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'contacts');
    }
}

现在我们已经设置好引用,这样我们可以非常方便地使用它们!

$user = new User($pdo_connection);

// 查找最新的用户。
$user->notNull('id')->orderBy('id desc')->find();

// 通过使用关系获取联系人:
foreach($user->contacts as $contact) {
    echo $contact->id;
}

// 或者我们可以反过来。
$contact = new Contact();

// 查找一个联系人
$contact->find();

// 通过使用关系获取用户:
echo $contact->user->name; // 这是用户名

很酷吧?

设置自定义数据

有时你可能需要将某些唯一的东西附加到你的活跃记录中,例如一个自定义计算,这可能更容易附加到对象上,然后传递给模板。

setCustomData(string $field, mixed $value)

你可以使用 setCustomData() 方法附加自定义数据。

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

然后你可以像正常对象属性一样引用它。

echo $user->page_view_count;

事件

关于这个库的另一个超级棒的功能是事件。事件在你调用的某些方法的特定时间触发。它们在自动为你设置数据时非常有帮助。

onConstruct(ActiveRecord $ActiveRecord, array &config)

如果你需要设置默认连接或类似的东西,这个功能非常有用。

// index.php 或 bootstrap.php
Flight::register('db', 'PDO', [ 'sqlite:test.db' ]);

//
//
//

// User.php
class User extends flight\ActiveRecord {

    protected function onConstruct(self $self, array &$config) { // 不要忘记引用 &
        // 你可以这样自动设置连接
        $config['connection'] = Flight::db();
        // 或者这样
        $self->transformAndPersistConnection(Flight::db());

        // 你也可以通过这种方式设置表名称。
        $config['table'] = 'users';
    } 
}

beforeFind(ActiveRecord $ActiveRecord)

如果你每次需要查询操作,这可能只对你有用。

class User extends flight\ActiveRecord {

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

    protected function beforeFind(self $self) {
        // 如果这是你的习惯,总是运行 id >= 0
        $self->gte('id', 0); 
    } 
}

afterFind(ActiveRecord $ActiveRecord)

这个可能更有用,如果你需要每次获取此记录时运行一些逻辑。你需要解密某些东西吗?你需要每次运行自定义计数查询吗(虽然不高效,但也无所谓)?

class User extends flight\ActiveRecord {

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

    protected function afterFind(self $self) {
        // 解密某些东西
        $self->secret = yourDecryptFunction($self->secret, $some_key);

        // 也许存储一些自定义的数据,比如查询???
        $self->setCustomData('view_count', $self->select('COUNT(*) count')->from('user_views')->eq('user_id', $self->id)['count']); 
    } 
}

beforeFindAll(ActiveRecord $ActiveRecord)

这可能只对你每次需要查询操作时有用。

class User extends flight\ActiveRecord {

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

    protected function beforeFindAll(self $self) {
        // 如果这是你的习惯,总是运行 id >= 0
        $self->gte('id', 0); 
    } 
}

afterFindAll(array<int,ActiveRecord> $results)

afterFind() 类似,但你可以对所有记录执行此操作!

class User extends flight\ActiveRecord {

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

    protected function afterFindAll(array $results) {

        foreach($results as $self) {
            // 做一些酷的事情,像 afterFind()
        }
    } 
}

beforeInsert(ActiveRecord $ActiveRecord)

如果每次需要设置默认值,这非常有用。

class User extends flight\ActiveRecord {

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

    protected function beforeInsert(self $self) {
        // 设置一些合理的默认值
        if(!$self->created_date) {
            $self->created_date = gmdate('Y-m-d');
        }

        if(!$self->password) {
            $self->password = password_hash((string) microtime(true));
        }
    } 
}

afterInsert(ActiveRecord $ActiveRecord)

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

class User extends flight\ActiveRecord {

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

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

beforeUpdate(ActiveRecord $ActiveRecord)

每次需要在更新时设置一些默认值,这真的很有用。

class User extends flight\ActiveRecord {

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

    protected function beforeInsert(self $self) {
        // 设置一些合理的默认值
        if(!$self->updated_date) {
            $self->updated_date = gmdate('Y-m-d');
        }
    } 
}

afterUpdate(ActiveRecord $ActiveRecord)

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

class User extends flight\ActiveRecord {

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

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

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

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

class User extends flight\ActiveRecord {

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

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

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

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

class User extends flight\ActiveRecord {

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

    protected function beforeDelete(self $self) {
        echo '他是一名勇敢的士兵... :cry-face:';
    } 
}

数据库连接管理

当你使用此库时,可以通过几种不同的方式设置数据库连接。你可以在构造函数中设置连接,可以通过配置变量 $config['connection'] 来设置,或者可以通过 setDatabaseConnection() 来设置(v0.4.1)。

$pdo_connection = new PDO('sqlite:test.db'); // 例如
$user = new User($pdo_connection);
// 或
$user = new User(null, [ 'connection' => $pdo_connection ]);
// 或
$user = new User();
$user->setDatabaseConnection($pdo_connection);

如果你想避免在每次调用活跃记录时设置 $database_connection,可以有其他方法!

// index.php 或 bootstrap.php
// 将其作为一个已注册的类设置在 Flight 中
Flight::register('db', 'PDO', [ 'sqlite:test.db' ]);

// User.php
class User extends flight\ActiveRecord {

    public function __construct(array $config = [])
    {
        $database_connection = $config['connection'] ?? Flight::db();
        parent::__construct($database_connection, 'users', $config);
    }
}

// 现在,不需要任何参数!
$user = new User();

注意: 如果你打算进行单元测试,这种方式可能会给单元测试带来一些挑战,但总体来说,因为可以通过 setDatabaseConnection()$config['connection'] 注入连接,所以还不错。

如果你需要刷新数据库连接,比如如果你正在运行一个长时间运行的 CLI 脚本,并且需要定期刷新连接,可以通过 $your_record->setDatabaseConnection($pdo_connection) 重新设置连接。

贡献

请这样做。 :D

设置

当你贡献时,请确保运行 composer test-coverage 以保持 100% 的测试覆盖率(这不是准确的单元测试覆盖率,更像是集成测试)。

还要确保运行 composer beautifycomposer phpcs 来修复任何语法错误。

许可证

MIT

Awesome-plugins/latte

拿铁

拿铁 是一个功能齐全的模板引擎,非常易于使用,比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">
            &copy; 版权所有
        </div>
    </body>
</html>

现在我们有一个将在该内容块内呈现的文件:

<!-- app/views/home.latte -->
<!-- 这告诉Latte,此文件位于layout.latte文件内部 -->
{extends layout.latte}

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

然后当您要在函数或控制器中呈现此内容时,您可以这样做:

// 简单路由
Flight::route('/', function () {
    Flight::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 文档。

身份验证/授权

身份验证和授权对于任何需要控制访问的应用程序至关重要。

缓存

缓存是加速你的应用程序的一个很好的方式。有许多缓存库可以与 Flight 一起使用。

CLI

CLI 应用程序是与您的应用程序交互的一种很好的方式。你可以用它们来生成控制器、显示所有路由等。

Cookies

Cookies 是在客户端存储小数据块的很好方式。它们可以用来存储用户偏好、应用程序设置等。

调试

调试在你开发本地环境时至关重要。有一些插件可以提升你的调试体验。

数据库

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

加密

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

工作队列

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

会话

会话对于 API 来说并不是特别有用,但对于构建 Web 应用程序,保持状态和登录信息的会话非常关键。

模板

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

贡献

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

Media

媒体

我们试图跟踪我们能够找到的关于 Flight 的各种媒体类型。请参阅下面的不同资源,以了解更多关于 Flight 的信息。

文章和写作

视频和教程

Examples

需要快速入门吗?

您有两种选择可以开始一个新的 Flight 项目:

社区贡献的示例:

需要一些灵感吗?

虽然这些不是官方由 Flight 团队赞助的,但它们可以给您一些构建自己的 Flight 项目结构的想法!

想要分享您自己的示例吗?

如果您有一个项目想要分享,请提交拉取请求以将其添加到此列表中!

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

  1. 安装 Homebrew(如果尚未安装):

    • 打开终端并运行:
      /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
  2. 安装 PHP

    • 安装最新版本:
      brew install php
    • 要安装特定版本,例如,PHP 8.1:
      brew tap shivammathur/php
      brew install shivammathur/php/php@8.1
  3. 在不同版本之间切换

    • 解除当前版本的链接并链接所需的版本:
      brew unlink php
      brew link --overwrite --force php@8.1
    • 验证已安装的版本:
      php -v

Windows 10/11

手动安装 PHP

  1. 下载 PHP

    • 访问PHP for Windows,下载最新版本或特定版本(例如,7.4、8.0)的非线程安全zip文件。
  2. 解压 PHP

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

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

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

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

安装多个 PHP 版本

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

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

Ubuntu(20.04、22.04等)

使用apt安装 PHP

  1. 更新软件包列表

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

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

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

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

    • 运行:
      php -v

Rocky Linux

使用yum/dnf安装 PHP

  1. 启用 EPEL repository

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

    • 运行:
      sudo dnf install https://rpms.remirepo.net/enterprise/remi-release-8.rpm
      sudo dnf module reset php
  3. 安装 PHP

    • 要安装默认版本:
      sudo dnf install php
    • 要安装特定版本,例如,PHP 7.4:
      sudo dnf module install php:remi-7.4
  4. 在PHP版本之间切换

    • 使用dnf模块命令:
      sudo dnf module reset php
      sudo dnf module enable php:remi-8.0
      sudo dnf install php
  5. 验证已安装的版本

    • 运行:
      php -v

一般说明

Guides

指南

Flight PHP 旨在简单而强大,我们的指南将帮助您一步步构建现实应用程序。这些实用的教程将通过完整的项目向您展示如何有效地使用 Flight。

官方指南

构建一个博客

学习如何使用 Flight PHP 创建一个功能齐全的博客应用程序。本指南将引导您完成以下内容:

这个教程非常适合想要了解真实应用程序中所有组件如何结合在一起的初学者。

非官方指南

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

使用 Flight 框架创建 RESTful API

本指南将引导您使用 Flight PHP 框架创建一个 RESTful API。它涵盖了设置 API、定义路由和返回 JSON 响应的基础知识。

构建一个简单的博客

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

在 PHP 中构建一个宝可梦 API:初学者指南

这个有趣的指南将引导您使用 Flight PHP 创建一个简单的宝可梦 API。它涵盖了设置 API、定义路由和返回 JSON 响应的基础知识。

贡献

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

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

寻找 API 文档?

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