Learn

了解 Flight

Flight 是一个快速、简单、可扩展的 PHP 框架。它非常灵活,可以用于构建任何类型的 Web 应用程序。它的设计简洁明了,易于理解和使用。

重要的框架概念

为什么要使用框架?

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

此外,@lubiana 创建了一份优秀的教程。虽然它并未深入讲解 Flight 框架,但这份指南将帮助您理解围绕框架的一些主要概念以及使用框架的好处。您可以在 这里 找到这个教程。

核心主题

自动加载

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

路由

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

中间件

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

请求

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

响应

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

HTML 模板

学习如何使用内置的视图引擎来呈现您的 HTML 模板。

安全

学习如何保护应用程序免受常见的安全威胁。

配置

学习如何为您的应用程序配置框架。

扩展 Flight

学习如何扩展框架,添加自己的方法和类。

事件和过滤

学习如何使用事件系统向您的方法和内部框架方法添加钩子。

依赖注入容器

学习如何使用依赖注入容器(DIC)来管理应用程序的依赖关系。

框架 API

了解框架的核心方法。

迁移到 v3

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

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 中,输出缓冲的处理方式并未一致关闭自身的输出缓冲区,这使得 单元测试流式传输 变得更加困难。对于大多数用户来说,此更改实际上可能不会影响您。但是,如果在不可调用和控制器之外输出内容 (例如在挂钩中),您很可能会遇到问题。在过去,可以在挂钩中输出内容并在框架实际执行之前运行,但在未来将无法正常工作。

您可能会遇到问题的地方

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

// 设置 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 或您设置路由的任何地方
// FYI,这个空字符串组充当全局中间件,以保护所有路由。当然,您也可以为特定路由执行同样的操作。
Flight::group('', function(Router $router) {
    $router->get('/users', [ 'UserController', 'getUsers' ]);
    // 更多路由
}, [ new SecurityHeadersMiddleware() ]);

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

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

如果你想通过容器(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('/你好/@id', [ 'Greeting', 'hello' ]);
// 或
Flight::route('/你好/@id', 'Greeting->hello');
// 或
Flight::route('/你好/@id', 'Greeting::hello');

Flight::start();

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

// 现在您可以在您的控制器/中间件中使用引擎实例

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

分组中间件

您可以添加一个路由组,然后该组中的每个路由也将具有相同的中间件。如果您需要按照标头中的API密钥对一堆路由进行分组,这将很有用。


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

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


// 添加到组方法的末尾
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

设置响应标头

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

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

HTTP 缓存

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

路由级别缓存

如果要缓存整个响应,可以使用 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还允许您替换框架的核心组件。例如,您可以用自己的自定义类替换默认的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/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
 */

// 不需要命名空间

// 所有自动加载的类都建议使用帕斯卡命名法(每个单词首字母大写,无空格)
// 不能在类名中使用下划线是一个要求
class MyController {

    public function index() {
        // 执行某些操作
    }
}

命名空间

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

/**
 * public/index.php
 */

// 添加一个路径到自动加载程序
Flight::path(__DIR__.'/../');

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

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

// 命名空间是必需的
// 命名空间与目录结构相同
// 命名空间必须与目录结构大小写一致
// 命名空间和目录都不能包含下划线
namespace app\controllers;

// 所有自动加载的类都建议使用帕斯卡命名法(每个单词首字母大写,无空格)
// 不能在类名中使用下划线是一个要求
class MyController {

    public function index() {
        // 执行某些操作
    }
}

如果您想要自动加载 utils 目录中的类,您可以做基本相同的操作:

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

// 命名空间必须与目录结构和大小写匹配(请注意文件树中 UTILS 目录是全大写的)
namespace app\UTILS;

class ArrayHelperUtil {

    public function changeArrayCase(array $array) {
        // 执行某些操作
    }
}

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的助手类。它允许您轻松地查询数据库,并处理所有准备/执行/fetchAll()的繁琐操作。它极大地简化了您查询数据库的方式。每一行结果都以Flight Collection类的形式返回,这使您可以通过数组语法或对象语法访问您的数据。

注册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'];
                // 或 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, // 只有在需要提交会话时才这样做,和/或者很难commit()您的会话。
                                                // 此外,您可以做 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,          # 避免每次脚本需要与数据库通信时建立新连接的开销,从而使网页应用更快。自行寻找缺点
            ]
        ]);
    }
);

文档

访问GitHub Readme获取完整文档。 默认_config.php文件中的配置选项有很好的文档。 如果您想自行查看此包,代码很容易理解。

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 实例传递给 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

飞行 Active Record

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

基本示例

假设您有以下表:

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

现在,您可以设置一个新类来表示此表:

/**
 * Active Record 类通常为单数形式
 * 
 * 强烈建议在此处添加表的属性作为注释
 * 
 * @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();

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 注入攻击 php”后会发现有很多关于这个主题的文章。使用此库的正确方法是,使用 eq('id', $id)->eq('name', $name)->find(); 这样的方法,而不是 where()。如果绝对必须这样做,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)

限制返回的记录数量。如果给出第二个 int,它将是偏移量,就像在 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();

关系

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

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

protected array $relations = [
    // 您可以为键指定任何名称。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');
    }
}

现在我们已经设置了引用,因此可以轻松使用它们!

$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; // 这是用户名称

很酷吧?

设置自定义数据

有时,您可能需要附加一些特殊内容到您的 ActiveRecord 中,例如某种自定义计算。这样可能更容易附加到对象中,然后传递到模板中。

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

如果需要刷新数据库连接,例如如果正在运行长时间运行的 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您的视图的根目录将在哪里。
    $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一起使用。

Cookies

Cookie是在客户端存储小数据片段的好方法。它们可用于存储用户偏好、应用程序设置等。

调试

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

数据库

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

加密

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

会话

对于API来说,会话并不是很有用,但对于构建Web应用程序,会话对于保持状态和登录信息至关重要。

模板

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

贡献

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

Examples

需要快速入门吗?

请前往 flightphp/skeleton 存储库开始!这是一个项目,包括一个单页面文件,其中包含运行应用所需的所有内容。它还包括一个更加完整的示例,其中包括控制器和视图。

需要一些灵感吗?

虽然这些并非 Flight 团队官方赞助的,但这些示例可以给您关于如何构建使用 Flight 的项目结构的想法!

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

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