Learn

了解 Flight

Flight 是一个快速、简单、可扩展的 PHP 框架。它非常多才多艺,可用于构建任何类型的 Web 应用程序。它的设计简单易懂,并且写法简洁明了。

框架重要概念

为什么使用框架?

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

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

核心主题

自动加载

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

路由

学习如何管理 Web 应用程序的路由。这还包括对路由进行分组、路由参数和中间件。

中间件

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

请求

学习如何处理应用程序中的请求和响应。

响应

学习如何向用户发送响应。

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

迁移到 v3

大部分情况下都保留了向后兼容性,但在从 v2 迁移到 v3 时有一些变化需要注意。

输出缓冲行为(3.5.0)

输出缓冲 是 PHP 脚本生成的输出被存储在缓冲区(PHP 内部)中,然后再发送到客户端的过程。这允许您在发送到客户端之前修改输出。

在 MVC 应用程序中,控制器是“管理器”,负责管理视图的操作。在控制器外部生成输出(或在 Flight 的情况下有时是匿名函数)会破坏 MVC 模式。这一变化旨在更符合 MVC 模式,以使框架更可预测和更易于使用。

在 v2 中,输出缓冲的处理方式不一致,导致它没有始终关闭自己的输出缓冲,这使得 单元测试流式处理 更加困难。对于大多数用户,这种变化实际上可能不会影响您。但是,如果您在回调函数和控制器之外输出内容(例如在 hook 中),您很可能会遇到问题。在过去,在框架实际执行之前在 hook 中输出内容以及 output 之外的内容可能是有效的,但在未来不会有效。

可能会出现问题的地方

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

// 一个例子
define('START_TIME', microtime(true));

function hello() {
    echo 'Hello World';
}

Flight::map('hello', 'hello');
Flight::after('hello', function(){
    // 这实际上没问题
    echo '<p>This Hello World phrase was brought to you by the letter "H"</p>';
});

Flight::before('start', function(){
    // 这样做将导致错误
    echo '<html><head><title>My Page</title></head><body>';
});

Flight::route('/', function(){
    // 这实际上没问题
    echo 'Hello World';

    // 这也应该没问题
    Flight::hello();
});

Flight::after('start', function(){
    // 这将导致错误
    echo '<div>Your page loaded in '.(microtime(true) - START_TIME).' seconds</div></body></html>';
});

开启 v2 渲染行为

您是否能保留旧代码不进行重写以使其在 v3 中运行?是的,您可以!您可以通过将 flight.v2.output_buffering 配置选项设置为 true 来开启 v2 渲染行为。这将允许您继续使用旧的渲染行为,但建议在未来修复它。在框架的 v4 中,这将被移除。

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

Flight::set('flight.v2.output_buffering', true);

Flight::before('start', function(){
    // 现在这将是没问题的
    echo '<html><head><title>My Page</title></head><body>';
});

// 更多代码

调度器更改(3.7.0)

如果您直接调用了 Dispatcher 的静态方法,例如 Dispatcher::invokeMethod()Dispatcher::execute()等,您需要更新您的代码,不要直接调用这些方法。Dispatcher 已转换为更具面向对象性质,以便更轻松地使用依赖注入容器。如果需要调用类似 Dispatcher 的方法,您可以手动使用类似 $result = $class->$method(...$params);call_user_func_array()

Learn/configuration

配置

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

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

可用的配置设置

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

变量

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内部服务器错误响应。

您可以根据自己的需求覆盖此行为:

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

安全

在涉及 Web 应用程序时,安全性至关重要。您希望确保您的应用程序是安全的,用户的数据是安全的。Flight 提供了许多功能来帮助您保护您的 Web 应用程序。

头部

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

用于检查头部安全性的两个很棒的网站是 securityheaders.comobservatory.mozilla.org

手动添加

您可以通过在 Flight\Response 对象上使用 header 方法手动添加这些头部。

// 设置 X-Frame-Options 头部以防止点击劫持
Flight::response()->header('X-Frame-Options', 'SAMEORIGIN');

// 设置内容安全策略头部以防止 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)
// 您只需要为每个会话生成一个令牌 (以便在同一用户的多个选项卡和请求中起作用)
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, 'Invalid CSRF token');
        }
    }
});

或者您可以使用中间件类:

// 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, 'Invalid CSRF token');
            }
        }
    }
}

// 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();
Flight::before('start', [ $CorsUtil, 'setupCors' ]);

结论

安全性非常重要,确保您的 Web 应用程序是安全的至关重要。Flight 提供了许多功能来帮助您保护您的 Web 应用程序,但始终保持警惕并确保您尽力保护用户数据。始终假定最坏的情况,并且永远不要相信用户的输入。始终转义输出并使用准备语句以防止 SQL 注入。始终使用中间件保护您的路由免受 CSRF 和 CORS 攻击。如果您做到所有这些,您将为构建安全的 Web 应用程序迈出重要的一步。

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

# 路由

> **提示:** 想了解更多关于路由的内容吗?请查看["为什么选择框架?"](/learn/why-frameworks)页面,有更详尽的解释。

Flight中的基本路由是通过将URL模式与回调函数或类和方法的数组进行匹配来完成的。

```php
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' ]);

通过DIC(Dependency Injection Container)进行依赖项注入

如果您想要通过容器(PSR-11、PHP-DI、Dice等)进行依赖项注入,那么 只有直接创建对象并使用容器创建对象的路由类型或者您可以使用字符串定义类和 要调用的方法。您可以前往Dependency Injection页面了解更多信息。

这里是一个简单示例:


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

提示: 不支持在命名参数中匹配正则表达式组 ()。\:(

可选参数

您可以通过将段落包装在括号中指定可选匹配的命名参数。

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 'user:'.$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 'user:'.$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
  });
});

流式传输

现在您可以使用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/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身份验证检查,或者验证用户是否有权限访问路由。

基本中间件

这里有一个基本示例:

// 如果你只提供一个匿名函数,它将在路由回调之前执行。
// 除了类(见下文)之外没有“后置”中间件函数。
Flight::route('/path', function() { echo ' Here I am!'; })->addMiddleware(function() {
    echo 'Middleware first!';
});

Flight::start();

// 这将输出“Middleware first! Here I am!”

在你使用中间件之前,有一些非常重要的注意事项需要注意:

中间件类

中间件也可以注册为类。如果你需要“后置”功能,你必须使用类。

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); // also ->addMiddleware([ $MyMiddleware, $MyMiddleware2 ]);

Flight::start();

// 这将显示“Middleware first! Here I am! Middleware last!”

处理中间件错误

假设你有一个auth中间件,如果用户未经身份验证,你希望将用户重定向到登录页面。你有几种选择:

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

分组中间件

你可以添加一个路由组,然后该组中的每个路由都将具有相同的中间件。如果你需要根据标头中的API密钥对一组路由进行分组,这将是有用的。


// 添加到group方法的末尾
Flight::group('/api', function() {

    // 这个“空”路由将实际匹配/api
    Flight::route('', function() { echo 'api'; }, false, 'api');
    Flight::route('/users', function() { echo 'users'; }, false, 'users');
    Flight::route('/users/@id', function($id) { echo 'user:'.$id; }, false, 'user_view');
}, [ new ApiAuthMiddleware() ]);

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


// 添加到group方法的末尾
Flight::group('', function() {
    Flight::route('/users', function() { echo 'users'; }, false, 'users');
    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();

请求对象提供以下属性:

你可以将querydatacookiesfiles属性视为数组或对象来访问。

因此,要获取查询字符串参数,可以这样做:

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

访问$_SERVER

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


$host = Flight::request()->getVar['HTTP_HOST'];

访问请求头

可以使用getHeader()getHeaders()方法访问请求头:


// 可能您需要Authorization头
$host = Flight::request()->getHeader('Authorization');

// 如果您需要获取所有头部信息
$headers = Flight::request()->getHeaders();

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 $名称, callable $回调, bool $传递路由 = false) // 创建自定义框架方法。
Flight::register(string $名称, string $类, array $参数 = [], ?callable $回调 = null) // 将类注册到框架方法。
Flight::unregister(string $名称) // 将类取消注册到框架方法。
Flight::before(string $名称, callable $回调) // 在框架方法之前添加过滤器。
Flight::after(string $名称, callable $回调) // 在框架方法之后添加过滤器。
Flight::path(string $路径) // 添加自动加载类的路径。
Flight::get(string $键) // 获取变量。
Flight::set(string $键, mixed $值) // 设置变量。
Flight::has(string $键) // 检查变量是否设置。
Flight::clear(array|string $键 = []) // 清除变量。
Flight::init() // 将框架初始化为其默认设置。
Flight::app() // 获取应用程序对象实例
Flight::request() // 获取请求对象实例
Flight::response() // 获取响应对象实例
Flight::router() // 获取路由器对象实例
Flight::view() // 获取视图对象实例

可扩展方法

Flight::start() // 启动框架。
Flight::stop() // 停止框架并发送响应。
Flight::halt(int $代码 = 200, string $消息 = '') // 停止框架,并可选择添加状态代码和消息。
Flight::route(string $模式, callable $回调, bool $传递路由 = false, string $别名 = '') // 将 URL 模式映射到回调。
Flight::post(string $模式, callable $回调, bool $传递路由 = false, string $别名 = '') // 将 POST 请求 URL 模式映射到回调。
Flight::put(string $模式, callable $回调, bool $传递路由 = false, string $别名 = '') // 将 PUT 请求 URL 模式映射到回调。
Flight::patch(string $模式, callable $回调, bool $传递路由 = false, string $别名 = '') // 将 PATCH 请求 URL 模式映射到回调。
Flight::delete(string $模式, callable $回调, bool $传递路由 = false, string $别名 = '') // 将 DELETE 请求 URL 模式映射到回调。
Flight::group(string $模式, callable $回调) // 为 URL 创建分组,模式必须是字符串。
Flight::getUrl(string $名称, array $参数 = []) // 基于路由别名生成 URL。
Flight::redirect(string $url, int $代码) // 重定向到另一个 URL。
Flight::render(string $文件, array $数据, ?string $键 = null) // 渲染模板文件。
Flight::error(Throwable $错误) // 发送 HTTP 500 响应。
Flight::notFound() // 发送 HTTP 404 响应。
Flight::etag(string $id, string $类型 = 'string') // 执行 ETag HTTP 缓存。
Flight::lastModified(int $时间) // 执行上次修改的 HTTP 缓存。
Flight::json(mixed $数据, int $代码 = 200, bool $编码 = true, string $字符集 = 'utf8', int $选项) // 发送 JSON 响应。
Flight::jsonp(mixed $数据, string $参数 = 'jsonp', int $代码 = 200, bool $编码 = true, string $字符集 = 'utf8', int $选项) // 发送 JSONP 响应。

通过 mapregister 添加的任何自定义方法也可以被过滤。

Learn/why_frameworks

为什么使用框架?

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

使用框架的理由

以下是您可能考虑使用框架的一些理由:

Flight 是一个微型框架。这意味着它很小且轻量级。它提供的功能不如 Laravel 或 Symfony 等较大的框架多。但是,它确实提供了构建 Web 应用程序所需的许多功能。而且,它易于学习和使用。这使它成为快速轻松构建 Web 应用程序的良好选择。如果您是框架新手,Flight 是一个很棒的入门框架。它将帮助您了解使用框架的优势,而不会让您陷入复杂性之中。在您对 Flight 有一些经验之后,更容易转向像 Laravel 或 Symfony 这样更复杂的框架,然而 Flight 仍然可以构建成功的强大应用程序。

什么是路由?

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

可能会像这样运作:

为什么它重要?

拥有一个适当的集中化路由器实际上可以使您的生活变得更加轻松!起初可能有点难以理解。以下是一些原因:

我相信您对逐个脚本的方式创建网站很熟悉。您可能有一个名为 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

在响应正文上运行回调

您可以使用 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]);

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

中间件选项

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

// MinifyMiddleware.php
class MinifyMiddleware {
    public function before() {
        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');
    echo "Hello, World!";
});

JSON

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

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

带状态码的 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 和停止执行

如果您想发送一个 JSON 响应并停止执行,您可以使用 jsonHalt 方法。这在您正在检查某种类型的授权并且用户未经授权时,可以立即发送 JSON 响应,清除现有正文内容并停止执行。

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

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

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 ("See Other") 状态码。您可以选择设置自定义代码:

Flight::redirect('/new/location', 401);

停止

您可以随时通过调用 halt 方法来停止框架:

Flight::halt();

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

Flight::halt(200, '马上回来...');

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

Flight::stop();

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响应并停止处理。

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

视图

默认情况下,Flight提供一些基本的模板功能。

如果需要更复杂的模板需求,请参阅自定义视图部分中的Smarty和Latte示例。

要显示一个视图模板,请使用render方法,指定模板文件的名称和可选的模板数据:

Flight::render('hello.php', ['name' => 'Bob']);

您传递的模板数据将自动注入到模板中,可以像本地变量一样引用。模板文件简单地是PHP文件。如果hello.php模板文件的内容是:

Hello, <?= $name ?>!

输出将是:

Hello, Bob!

您也可以通过使用set方法手动设置视图变量:

Flight::view()->set('name', 'Bob');

现在变量name可以在所有视图中使用。因此,您可以简单地这样做:

Flight::render('hello');

请注意,在render方法中指定模板名称时,可以省略.php扩展名。

默认情况下,Flight将查找views目录中的模板文件。您可以通过设置以下配置来为模板设置备用路径:

Flight::set('flight.views.path', '/path/to/views');

布局

网站通常有一个使用单个布局模板文件的常见模式。要呈现用于布局的内容,可以向render方法传递一个可选参数。

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

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

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

如果模板文件如下所示:

header.php:

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

body.php:

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

layout.php:

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

输出将是:

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

自定义视图

Flight允许您简单注册自己的视图类来替换默认视图引擎。

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默认的render方法:

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

Learn/extending

扩展

Flight旨在成为一个可扩展的框架。该框架提供了一组默认方法和组件,但允许您映射自己的方法,注册自己的类,甚至覆盖现有的类和方法。

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

映射方法

要映射自己简单的自定义方法,可以使用map函数:

// 映射您的方法
Flight::map('hello', function (string $name) {
  echo "你好 $name!";
});

// 调用您的自定义方法
Flight::hello('Bob');

当您需要将变量传递到方法中以获得预期值时,会更多地使用这种方法。像下面使用register()方法更多地是用于传递配置,然后调用您预先配置的类。

注册类

要注册自己的类并对其进行配置,可以使用register函数:

// 注册您的类
Flight::register('user', User::class);

// 获取您的类的实例
$user = Flight::user();

注册方法还允许您将参数传递给类的构造函数。因此,当您加载您的自定义类时,它将被预先初始化。您可以通过传递一个额外的数组来定义构造函数参数。以下是加载数据库连接的示例:

// 注册具有构造函数参数的类
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无法将URL匹配到路由时,它会调用notFound方法,该方法发送一个通用的HTTP 404响应。您可以通过使用map方法覆盖此行为:

Flight::map('notFound', function() {
  // 显示自定义404页面
  include 'errors/404.html';
});

Flight还允许您替换框架的核心组件。例如,您可以用自定义的路由器类替换默认的路由器类:

// 注册您的自定义类
Flight::register('router', MyRouter::class);

// 当Flight加载路由器实例时,它将加载您的类
$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/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);来使用Pascal_Snake_Case作为类名
class MyController {

    public function index() {
        // 做一些事情
    }
}

命名空间

如果您有命名空间,实际上实现这一点会变得非常容易。您应该使用Flight::path()方法指定应用程序的根目录(而不是文档根目录或public/文件夹)。


/**
 * public/index.php
 */

// 向自动加载器添加路径
Flight::path(__DIR__.'/../');

现在,您的控制器可能如下所示。查看下面的示例,但请留意重要信息的注释。

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

// 必须使用命名空间
// 命名空间与目录结构相同
// 命名空间必须遵循与目录结构相同的大小写
// 命名空间和目录不能有任何下划线(除非设置了Loader::setV2ClassLoading(false))
namespace app\controllers;

// 建议所有自动加载的类均为帕斯卡命名法(每个单词的首字母大写,没有空格)
// 从3.7.2开始,您可以通过运行Loader::setV2ClassLoading(false);来使用Pascal_Snake_Case作为类名
class MyController {

    public function index() {
        // 做一些事情
    }
}

如果您想要自动加载utils目录中的类,您将执行基本相同的操作:


/**
 * app/UTILS/ArrayHelperUtil.php
 */

// 命名空间必须与目录结构和大小写匹配(注意UTILS目录全部大写,与上面的文件树相同)
namespace app\UTILS;

class ArrayHelperUtil {

    public function changeArrayCase(array $array) {
        // 做一些事情
    }
}

类名中的下划线

从3.7.2开始,您可以通过运行Loader::setV2ClassLoading(false);来使用Pascal_Snake_Case作为类名。这将允许您在类名中使用下划线。虽然不建议这样做,但对那些需要的人是可用的。


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

这是由于路由器内置的特殊机制处理返回输出作为“继续下一个路由”的单个。您可以在路由部分中查看行为的文档。

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

License

MIT 許可證(MIT)
=====================

版權所有(C)`2023` `@mikecao, @n0nag0n`

特此免費授權給任何人免費獲得此軟件和相關文檔(以下簡稱“軟件”)的副本,而無需支付費用,以便在不受限制的情況下處理軟件,包括但不限於使用、複製、修改、合併、發布、分發、許可和/或出售軟件的副本,並允許將軟件提供給其所提供軟件的人員,但應受如下條件:

上述版權聲明和本許可聲明應包含在所有副本或主要部分中。

本軟件按“原樣”提供,不附帶任何明示或暗示的擔保,包括但不限於針對特定目的的商業性、適用性和非侵權性質的擔保。在任何情況下,作者或版權持有人均不對任何索賠、損害或其他責任承擔責任,無論是在合同、侵權行為或其他方面出現的,與軟件或使用或其他處理軟件有關的,或與軟件或使用或其他處理軟件有關的行為有關。

About

什么是Flight?

Flight 是一个快速、简单、可扩展的 PHP 框架。它非常灵活,可用于构建任何类型的 Web 应用程序。它专注于简单性,并且采用易于理解和使用的方式编写。

对于那些刚接触 PHP 并想要学习如何构建 Web 应用程序的初学者来说,Flight 是一个很好的入门框架。对于有经验的开发人员来说,Flight 也是一个很好的框架,可以更好地控制他们的 Web 应用程序。它设计用于轻松构建 RESTful API、简单的 Web 应用程序或复杂的 Web 应用程序。

快速开始

<?php

// 如果使用 Composer 安装
require 'vendor/autoload.php';
// 或者如果手动通过 zip 文件安装
// require 'flight/Flight.php';

Flight::route('/', function() {
  echo 'hello world!';
});

Flight::route('/json', function() {
  Flight::json(['hello' => 'world']);
});

Flight::start();

看起来很简单是吧?在文档中了解更多关于Flight的信息!

骨架/样板应用

有一个示例应用程序可以帮助您开始使用Flight框架。访问 flightphp/skeleton 获取开始说明!您还可以访问 examples 页面,以获取关于Flight能做什么的一些灵感。

社区

我们在Matrix上!在#flight-php-framework:matrix.org与我们交流。

贡献

有两种方式可以贡献给Flight:

  1. 您可以通过访问 核心仓库 贡献到核心框架。
  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

Wruczek/PHP-File-Cache

轻量、简单且独立的PHP内部文件缓存类

优势

安装

通过composer安装:

composer require wruczek/php-file-cache

用法

使用非常简单。

use Wruczek\PhpFileCache\PhpFileCache;

$app = Flight::app();

// 将存储缓存的目录传递给构造函数
$app->register('cache', PhpFileCache::class, [ __DIR__ . '/../cache/' ], function(PhpFileCache $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/Wruczek/PHP-File-Cache 获取完整文档,并确保查看examples文件夹。

Awesome-plugins/index

令人惊叹的插件

Flight非常可扩展。有许多插件可以用于向您的Flight应用程序添加功能。其中一些得到了Flight团队的官方支持,而其他一些是微型/轻量级库,可帮助您入门。

缓存

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

调试

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

数据库

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

会话

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

模板

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

贡献

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

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

Ghostff/Session

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

安装

使用 composer 进行安装。

composer require ghostff/session

基本配置

您不需要传入任何内容即可使用默认设置与会话。 您可以在 Github Readme 中阅读更多有关设置的信息。


use Ghostff\Session\Session;

require 'vendor/autoload.php';

$app = Flight::app();

$app->register('session', Session::class);

// 请记住的一件事是您必须在每个页面加载时提交您的会话
// 或者您需要在配置中运行 auto_commit。

简单示例

这是您可能如何使用此功能的简单示例。

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

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

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

    // 每次写入会话时,都必须有意识地提交。
    $session->commit();
});

// 此检查可以在受限页面逻辑中,或在中间层包装中。
Flight::route('/some-restricted-page', function() {
    $session = Flight::session();

    if(!$session->get('is_logged_in')) {
        Flight::redirect('/login');
    }

    // 在这里执行受限页面逻辑
});

// 中间件版本
Flight::route('/some-restricted-page', function() {
    // 常规页面逻辑
})->addMiddleware(function() {
    $session = Flight::session();

    if(!$session->get('is_logged_in')) {
        Flight::redirect('/login');
    }
});

更复杂的示例

这是您可能如何使用此功能的更复杂示例。


use Ghostff\Session\Session;

require 'vendor/autoload.php';

$app = Flight::app();

// 将一个自定义路径设置为会话配置文件,并为会话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 Readme 以获取完整文档。 配置选项在 default_config.php 文件中有很好的文档。 如果您希望自己查看此软件包,代码很容易理解。

Awesome-plugins/runway

飛道

飛道是一個CLI應用程序,可以幫助您管理您的Flight應用程序。它可以生成控制器,顯示所有路由等。它基於優秀的adhocore/php-cli庫。

安裝

使用composer安裝。

composer require flightphp/runway

基本配置

第一次運行飛道時,它將引導您完成一個設置過程並在項目的根目錄中創建一個.runway.json配置文件。該文件將包含一些Runway正常工作所需的必要配置。

用法

飛道有多個命令可用於管理您的Flight應用程序。有兩種簡單的方式可以使用飛道。

  1. 如果您使用骨架項目,您可以從項目的根目錄運行php runway [command]
  2. 如果您將Runway作為通過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;

/**
 * 用於用戶表的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
 */
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('示例已創建!');
    }
}

查看adhocore/php-cli文檔了解如何將自定義命令集成到您的Flight應用程序中的更多信息!

Awesome-plugins/tracy_extensions

Tracy Flight 面板扩展

这是一组扩展,使与 Flight 一起工作更加丰富。

这是面板

Flight Bar

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

Flight Data Flight Database Flight Request

安装

运行 composer require flightphp/tracy-extensions --dev 就可以开始了!

配置

您需要做很少的配置才能开始使用这个工具。您需要在使用此工具之前初始化 Tracy 调试器 https://tracy.nette.org/en/guide

<?php

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

// 启动代码
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 键将 Latte 实例传递给 TracyExtensionLoader 构造函数。



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

飞行活动记录

活动记录是将数据库实体映射到 PHP 对象。简单来说,如果你在数据库中有一个用户表,你可以将该表中的一行“转换”为一个User类和一个$user对象在你的代码库中。请参见基本示例

基本示例

假设你有以下表格:

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 PHP 框架

如果你在使用 Flight PHP 框架,你可以将 ActiveRecord 类注册为服务(但你实际上并不需要)。

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

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

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

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' ]);
        // 你也可以像这样设置主键而不是上面的数组。
        $this->primaryKey = 'uuid';
    }

    protected function beforeInsert(self $self) {
        $self->uuid = uniqid(); // 或者以其他方式生成你的唯一标识符
    }
}

如果在插入之前不设置主键,它将被设置为 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(); // name 和 password 都会被更新。

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

这是dirty()方法的别名。这样更清晰一些。

$user->copyFrom([ 'name' => 'something', 'password' => password_hash('a different password') ]);
$user->update(); // name 和 password 都会被更新。

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 语句中设置参数)。

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

安全提示 - 也许你会想做这样的事$user->where("id = '{$id}' AND name = '{$name}'")->find();。请不要这样做!这容易受到 SQL 注入攻击的影响。网上有许多文章,请搜索“sql 注入攻击 php”,你会找到很多相关文章。在这个库中处理这种情况的正确方式是,不要使用 where() 方法,而是使用类似 $user->eq('id', $id)->eq('name', $name)->find(); 的方式。如果确实必须这样做,PDO 库提供了 $pdo->quote($var) 来为你转义参数。只有在使用 quote() 后才能在 where() 语句中使用。

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

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

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

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

以某种方式排序返回的查询结果。

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

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

限制返回的记录数。如果给出第二个整数,它将是偏移量,就像 SQL 中的 limit 一样。

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

关系

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

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

protected array $relations = [
    // 你可以给 key 取任何名字。ActiveRecord 的名字可能很合适。比如:user, contact, client
    'user' => [
        // 必须
        // self::HAS_MANY, self::HAS_ONE, self::BELONGS_TO
        self::HAS_ONE, // 这是关系的类型

        // 必须
        'Some_Class', // 这是关联的“其他”ActiveRecord 类

        // 必须
        // 根据关系类型
        // self::HAS_ONE = 引用连接的外键
        // self::HAS_MANY = 引用连接的外键
        // self::BELONGS_TO = 引用连接的本地键
        'local_or_foreign_key',
        // 只是 FYI,这也连接到“其他”模型的主键

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

现在我们已完成整个文档的翻译,请查阅。如果有任何疑问或需要进一步翻译,请随时告诉我。

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您的视图的根目录将在哪里。
    $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 . ' - '}My App</title>
        <link rel="stylesheet" href="style.css">
    </head>
    <body>
        <header>
            <nav>
                <!-- 在这里放置您的导航元素 -->
            </nav>
        </header>
        <div id="content">
            <!-- 这就是神奇的所在 -->
            {block content}{/block}
        </div>
        <div id="footer">
            &copy; 版权所有
        </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 团队官方支持,而其他一些是微型/精简库,可帮助您入门。

缓存

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

命令行界面

CLI 应用程序是与应用程序进行交互的好方法。您可以使用它们生成控制器,显示所有路由等。

Cookies

Cookies 是在客户端存储小数据块的好方法。它们可用于存储用户首选项、应用程序设置等。

调试

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

数据库

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

加密

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

会话

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

模板

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

贡献

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

Examples

需要快速开始吗?

您有两个选项可以开始使用 Flight:

需要一些灵感吗?

虽然这些不是由 Flight 团队官方赞助的,但这些可能会给您关于如何构建使用 Flight 构建的项目结构的想法!

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

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