Learn

Learn About Flight

Flight is a fast, simple, extensible framework for PHP. It is quite versatile and can be used for building any kind of web application. It is built with simplicity in mind and is written in a way that is easy to understand and use.

Important Framework Concepts

Why a Framework?

Here's a short article on why you should use a framework. It's a good idea to understand the benefits of using a framework before you start using one.

Additionally an excellent tutorial has been created by @lubiana. While it doesn't go into great detail about Flight specifically, this guide will help you understand some of the major concepts surrounding a framework and why they are beneficial to use. You can find the tutorial here.

Flight Compared to Other Frameworks

If you are migrating from another framework such as Laravel, Slim, Fat-Free, or Symfony to Flight, this page will help you understand the differences between the two.

Core Topics

Autoloading

Learn how to autoload your own classes in your application.

Routing

Learn how to manage routes for your web application. This also includes grouping routes, route parameters and middleware.

Middleware

Learn how to use middleware to filter requests and responses in your application.

Requests

Learn how to handle requests and responses in your application.

Responses

Learn how to send responses to your users.

HTML Templates

Learn how to use the built-in view engine to render your HTML templates.

Security

Learn how to secure your application from common security threats.

Configuration

Learn how to configure the framework for your application.

Extending Flight

Learn how to extend the framework to with adding your own methods and classes.

Events and Filtering

Learn how to use the event system to add hooks to your methods and internal framework methods.

Dependency Injection Container

Learn how to use dependency injection containers (DIC) to manage your application's dependencies.

Framework API

Learn about the core methods of the framework.

Migrating to v3

Backwards compatibility has for the most part been maintained, but there are some changes that you should be aware of when migrating from v2 to v3.

Troubleshooting

There are some common issues that you may run into when using Flight. This page will help you troubleshoot those issues.

Learn/flight_vs_laravel

Flight vs Laravel

What is Laravel?

Laravel is a full-featured framework that has all the bells and whistles and an amazing developer focused ecosystem, but at a cost in performance and complexity. The goal of Laravel is for the developer to have the highest level of productivity and to make common tasks easy. Laravel is a great choice for developers who are looking to build a full-featured, enterprise web application. That comes with some tradeoffs, specifically in terms of performance and complexity. Learning the beginnings of Laravel can be easy, but gaining proficiency in the framework can take some time.

There are also so many Laravel modules that developers often feel like the only way to solve problems is through these modules, when you actually could just use another library or write your own code.

Pros compared to Flight

Cons compared to Flight

Learn/migrating_to_v3

Migrating to v3

Backwards compatibility has for the most part been maintained, but there are some changes that you should be aware of when migrating from v2 to v3.

Output Buffering Behavior (3.5.0)

Output buffering is the process where the output generated by a PHP script is stored in a buffer (internal to PHP) before being sent to the client. This allows you to modify the output before it is sent to the client.

In an MVC application, the Controller is the "manager" and it manages what the view does. Having output generated outside of the controller (or in Flights case sometimes an anonymous function) breaks the MVC pattern. This change is to be more in line with the MVC pattern and to make the framework more predictable and easier to use.

In v2, output buffering was handled in a way where it wasn't consistently closing it's own output buffer and which made unit testing and streaming more difficult. For the majority of users, this change may not actually affect you. However if you are echoing content outside of callables and controllers (for example in a hook), you likely are going to run into trouble. Echoing out content in hooks, and prior to the framework actually executing may have worked in the past, but it won't work moving forward.

Where you might have problems

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

// just an example
define('START_TIME', microtime(true));

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

Flight::map('hello', 'hello');
Flight::after('hello', function(){
    // this will actually be fine
    echo '<p>This Hello World phrase was brought to you by the letter "H"</p>';
});

Flight::before('start', function(){
    // things like this will cause an error
    echo '<html><head><title>My Page</title></head><body>';
});

Flight::route('/', function(){
    // this is actually just fine
    echo 'Hello World';

    // This should be just fine as well
    Flight::hello();
});

Flight::after('start', function(){
    // this will cause an error
    echo '<div>Your page loaded in '.(microtime(true) - START_TIME).' seconds</div></body></html>';
});

Turning on v2 Rendering Behavior

Can you still keep your old code the way it is without doing a rewrite to make it work with v3? Yes, you can! You can turn on v2 rendering behavior by setting the flight.v2.output_buffering configuration option to true. This will allow you to continue to use the old rendering behavior, but it is recommended to fix it moving forward. In v4 of the framework, this will be removed.

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

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

Flight::before('start', function(){
    // Now this will be just fine
    echo '<html><head><title>My Page</title></head><body>';
});

// more code 

Dispatcher Changes (3.7.0)

If you have directly been calling static methods for Dispatcher such as Dispatcher::invokeMethod(), Dispatcher::execute(), etc. you will need to update your code to not directly call these methods. Dispatcher has been converted to be more object oriented so that Dependency Injection Containers can be used in an easier way. If you need to invoke a method similar to how Dispatcher did, you can manually use something like $result = $class->$method(...$params); or call_user_func_array() instead.

halt() stop() redirect() and error() Changes (3.10.0)

Default behavior before 3.10.0 was to clear both the headers and the response body. This was changed to only clear the response body. If you need to clear the headers as well, you can use Flight::response()->clear().

Learn/configuration

Configuration

You can customize certain behaviors of Flight by setting configuration values through the set method.

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

Available Configuration Settings

The following is a list of all the available configuration settings:

Loader Configuration

There is additionally another configuration setting for the loader. This will allow you to autoload classes with _ in the class name.

// Enable class loading with underscores
// Defaulted to true
Loader::$v2ClassLoading = false;

Variables

Flight allows you to save variables so that they can be used anywhere in your application.

// Save your variable
Flight::set('id', 123);

// Elsewhere in your application
$id = Flight::get('id');

To see if a variable has been set you can do:

if (Flight::has('id')) {
  // Do something
}

You can clear a variable by doing:

// Clears the id variable
Flight::clear('id');

// Clears all variables
Flight::clear();

Flight also uses variables for configuration purposes.

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

Error Handling

Errors and Exceptions

All errors and exceptions are caught by Flight and passed to the error method. The default behavior is to send a generic HTTP 500 Internal Server Error response with some error information.

You can override this behavior for your own needs:

Flight::map('error', function (Throwable $error) {
  // Handle error
  echo $error->getTraceAsString();
});

By default errors are not logged to the web server. You can enable this by changing the config:

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

Not Found

When a URL can't be found, Flight calls the notFound method. The default behavior is to send an HTTP 404 Not Found response with a simple message.

You can override this behavior for your own needs:

Flight::map('notFound', function () {
  // Handle not found
});

Learn/security

Security

Security is a big deal when it comes to web applications. You want to make sure that your application is secure and that your users' data is safe. Flight provides a number of features to help you secure your web applications.

Headers

HTTP headers are one of the easiest ways to secure your web applications. You can use headers to prevent clickjacking, XSS, and other attacks. There are several ways that you can add these headers to your application.

Two great websites to check for the security of your headers are securityheaders.com and observatory.mozilla.org.

Add By Hand

You can manually add these headers by using the header method on the Flight\Response object.

// Set the X-Frame-Options header to prevent clickjacking
Flight::response()->header('X-Frame-Options', 'SAMEORIGIN');

// Set the Content-Security-Policy header to prevent XSS
// Note: this header can get very complex, so you'll want
//  to consult examples on the internet for your application
Flight::response()->header("Content-Security-Policy", "default-src 'self'");

// Set the X-XSS-Protection header to prevent XSS
Flight::response()->header('X-XSS-Protection', '1; mode=block');

// Set the X-Content-Type-Options header to prevent MIME sniffing
Flight::response()->header('X-Content-Type-Options', 'nosniff');

// Set the Referrer-Policy header to control how much referrer information is sent
Flight::response()->header('Referrer-Policy', 'no-referrer-when-downgrade');

// Set the Strict-Transport-Security header to force HTTPS
Flight::response()->header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');

// Set the Permissions-Policy header to control what features and APIs can be used
Flight::response()->header('Permissions-Policy', 'geolocation=()');

These can be added at the top of your bootstrap.php or index.php files.

Add as a Filter

You can also add them in a filter/hook like the following:

// Add the headers in a filter
Flight::before('start', function() {
    Flight::response()->header('X-Frame-Options', 'SAMEORIGIN');
    Flight::response()->header("Content-Security-Policy", "default-src 'self'");
    Flight::response()->header('X-XSS-Protection', '1; mode=block');
    Flight::response()->header('X-Content-Type-Options', 'nosniff');
    Flight::response()->header('Referrer-Policy', 'no-referrer-when-downgrade');
    Flight::response()->header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
    Flight::response()->header('Permissions-Policy', 'geolocation=()');
});

Add as a Middleware

You can also add them as a middleware class. This is a good way to keep your code clean and organized.

// 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 or wherever you have your routes
// FYI, this empty string group acts as a global middleware for
// all routes. Of course you could do the same thing and just add
// this only to specific routes.
Flight::group('', function(Router $router) {
    $router->get('/users', [ 'UserController', 'getUsers' ]);
    // more routes
}, [ new SecurityHeadersMiddleware() ]);

Cross Site Request Forgery (CSRF)

Cross Site Request Forgery (CSRF) is a type of attack where a malicious website can make a user's browser send a request to your website. This can be used to perform actions on your website without the user's knowledge. Flight does not provide a built-in CSRF protection mechanism, but you can easily implement your own by using middleware.

Setup

First you need to generate a CSRF token and store it in the user's session. You can then use this token in your forms and check it when the form is submitted.

// Generate a CSRF token and store it in the user's session
// (assuming you've created a session object at attached it to Flight)
// see the session documentation for more information
Flight::register('session', \Ghostff\Session\Session::class);

// You only need to generate a single token per session (so it works 
// across multiple tabs and requests for the same user)
if(Flight::session()->get('csrf_token') === null) {
    Flight::session()->set('csrf_token', bin2hex(random_bytes(32)) );
}
<!-- Use the CSRF token in your form -->
<form method="post">
    <input type="hidden" name="csrf_token" value="<?= Flight::session()->get('csrf_token') ?>">
    <!-- other form fields -->
</form>

Using Latte

You can also set a custom function to output the CSRF token in your Latte templates.

// Set a custom function to output the CSRF token
// Note: View has been configured with Latte as the view engine
Flight::view()->addFunction('csrf', function() {
    $csrfToken = Flight::session()->get('csrf_token');
    return new \Latte\Runtime\Html('<input type="hidden" name="csrf_token" value="' . $csrfToken . '">');
});

And now in your Latte templates you can use the csrf() function to output the CSRF token.

<form method="post">
    {csrf()}
    <!-- other form fields -->
</form>

Short and simple right?

Check the CSRF Token

You can check the CSRF token using event filters:

// This middleware checks if the request is a POST request and if it is, it checks if the CSRF token is valid
Flight::before('start', function() {
    if(Flight::request()->method == 'POST') {

        // capture the csrf token from the form values
        $token = Flight::request()->data->csrf_token;
        if($token !== Flight::session()->get('csrf_token')) {
            Flight::halt(403, 'Invalid CSRF token');
            // or for a JSON response
            Flight::jsonHalt(['error' => 'Invalid CSRF token'], 403);
        }
    }
});

Or you can use a middleware class:

// 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 or wherever you have your routes
Flight::group('', function(Router $router) {
    $router->get('/users', [ 'UserController', 'getUsers' ]);
    // more routes
}, [ new CsrfMiddleware() ]);

Cross Site Scripting (XSS)

Cross Site Scripting (XSS) is a type of attack where a malicious website can inject code into your website. Most of these opportunities come from form values that your end users will fill out. You should never trust output from your users! Always assume all of them are the best hackers in the world. They can inject malicious JavaScript or HTML into your page. This code can be used to steal information from your users or perform actions on your website. Using Flight's view class, you can easily escape output to prevent XSS attacks.

// Let's assume the user is clever as tries to use this as their name
$name = '<script>alert("XSS")</script>';

// This will escape the output
Flight::view()->set('name', $name);
// This will output: &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;

// If you use something like Latte registered as your view class, it will also auto escape this.
Flight::view()->render('template', ['name' => $name]);

SQL Injection

SQL Injection is a type of attack where a malicious user can inject SQL code into your database. This can be used to steal information from your database or perform actions on your database. Again you should never trust input from your users! Always assume they are out for blood. You can use prepared statements in your PDO objects will prevent SQL injection.

// Assuming you have Flight::db() registered as your PDO object
$statement = Flight::db()->prepare('SELECT * FROM users WHERE username = :username');
$statement->execute([':username' => $username]);
$users = $statement->fetchAll();

// If you use the PdoWrapper class, this can easily be done in one line
$users = Flight::db()->fetchAll('SELECT * FROM users WHERE username = :username', [ 'username' => $username ]);

// You can do the same thing with a PDO object with ? placeholders
$statement = Flight::db()->fetchAll('SELECT * FROM users WHERE username = ?', [ $username ]);

// Just promise you will never EVER do something like this...
$users = Flight::db()->fetchAll("SELECT * FROM users WHERE username = '{$username}' LIMIT 5");
// because what if $username = "' OR 1=1; -- "; 
// After the query is build it looks like this
// SELECT * FROM users WHERE username = '' OR 1=1; -- LIMIT 5
// It looks strange, but it's a valid query that will work. In fact,
// it's a very common SQL injection attack that will return all users.

CORS

Cross-Origin Resource Sharing (CORS) is a mechanism that allows many resources (e.g., fonts, JavaScript, etc.) on a web page to be requested from another domain outside the domain from which the resource originated. Flight does not have built in functionality, but this can easily be handled with a hook to run before the Flight::start() method is called.

// app/utils/CorsUtil.php

namespace app\utils;

class CorsUtil
{
    public function set(array $params): void
    {
        $request = Flight::request();
        $response = Flight::response();
        if ($request->getVar('HTTP_ORIGIN') !== '') {
            $this->allowOrigins();
            $response->header('Access-Control-Allow-Credentials', 'true');
            $response->header('Access-Control-Max-Age', '86400');
        }

        if ($request->method === 'OPTIONS') {
            if ($request->getVar('HTTP_ACCESS_CONTROL_REQUEST_METHOD') !== '') {
                $response->header(
                    'Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD'
                );
            }
            if ($request->getVar('HTTP_ACCESS_CONTROL_REQUEST_HEADERS') !== '') {
                $response->header(
                    "Access-Control-Allow-Headers",
                    $request->getVar('HTTP_ACCESS_CONTROL_REQUEST_HEADERS')
                );
            }

            $response->status(200);
            $response->send();
            exit;
        }
    }

    private function allowOrigins(): void
    {
        // customize your allowed hosts here.
        $allowed = [
            'capacitor://localhost',
            'ionic://localhost',
            'http://localhost',
            'http://localhost:4200',
            'http://localhost:8080',
            'http://localhost:8100',
        ];

        $request = Flight::request();

        if (in_array($request->getVar('HTTP_ORIGIN'), $allowed, true) === true) {
            $response = Flight::response();
            $response->header("Access-Control-Allow-Origin", $request->getVar('HTTP_ORIGIN'));
        }
    }
}

// index.php or wherever you have your routes
$CorsUtil = new CorsUtil();

// This needs to be run before start runs.
Flight::before('start', [ $CorsUtil, 'setupCors' ]);

Conclusion

Security is a big deal and it's important to make sure your web applications are secure. Flight provides a number of features to help you secure your web applications, but it's important to always be vigilant and make sure you're doing everything you can to keep your users' data safe. Always assume the worst and never trust input from your users. Always escape output and use prepared statements to prevent SQL injection. Always use middleware to protect your routes from CSRF and CORS attacks. If you do all of these things, you'll be well on your way to building secure web applications.

Learn/routing

Routing

Note: Want to understand more about routing? Check out the "why a framework?" page for a more in-depth explanation.

Basic routing in Flight is done by matching a URL pattern with a callback function or an array of a class and method.

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

Routes are matched in the order they are defined. The first route to match a request will be invoked.

Callbacks/Functions

The callback can be any object that is callable. So you can use a regular function:

function hello() {
    echo 'hello world!';
}

Flight::route('/', 'hello');

Classes

You can use a static method of a class as well:

class Greeting {
    public static function hello() {
        echo 'hello world!';
    }
}

Flight::route('/', [ 'Greeting','hello' ]);

Or by creating an object first and then calling the method:


// Greeting.php
class Greeting
{
    public function __construct() {
        $this->name = 'John Doe';
    }

    public function hello() {
        echo "Hello, {$this->name}!";
    }
}

// index.php
$greeting = new Greeting();

Flight::route('/', [ $greeting, 'hello' ]);
// You also can do this without creating the object first
// Note: No args will be injected into the constructor
Flight::route('/', [ 'Greeting', 'hello' ]);
// Additionally you can use this shorter syntax
Flight::route('/', 'Greeting->hello');
// or
Flight::route('/', Greeting::class.'->hello');

Dependency Injection via DIC (Dependency Injection Container)

If you want to use dependency injection via a container (PSR-11, PHP-DI, Dice, etc), the only type of routes where that is available is either directly creating the object yourself and using the container to create your object or you can use strings to defined the class and method to call. You can go to the Dependency Injection page for more information.

Here's a quick example:


use flight\database\PdoWrapper;

// Greeting.php
class Greeting
{
    protected PdoWrapper $pdoWrapper;
    public function __construct(PdoWrapper $pdoWrapper) {
        $this->pdoWrapper = $pdoWrapper;
    }

    public function hello(int $id) {
        // do something with $this->pdoWrapper
        $name = $this->pdoWrapper->fetchField("SELECT name FROM users WHERE id = ?", [ $id ]);
        echo "Hello, world! My name is {$name}!";
    }
}

// index.php

// Setup the container with whatever params you need
// See the Dependency Injection page for more information on PSR-11
$dice = new \Dice\Dice();

// Don't forget to reassign the variable with '$dice = '!!!!!
$dice = $dice->addRule('flight\database\PdoWrapper', [
    'shared' => true,
    'constructParams' => [ 
        'mysql:host=localhost;dbname=test', 
        'root',
        'password'
    ]
]);

// Register the container handler
Flight::registerContainerHandler(function($class, $params) use ($dice) {
    return $dice->create($class, $params);
});

// Routes like normal
Flight::route('/hello/@id', [ 'Greeting', 'hello' ]);
// or
Flight::route('/hello/@id', 'Greeting->hello');
// or
Flight::route('/hello/@id', 'Greeting::hello');

Flight::start();

Method Routing

By default, route patterns are matched against all request methods. You can respond to specific methods by placing an identifier before the URL.

Flight::route('GET /', function () {
  echo 'I received a GET request.';
});

Flight::route('POST /', function () {
  echo 'I received a POST request.';
});

// You cannot use Flight::get() for routes as that is a method 
//    to get variables, not create a route.
// Flight::post('/', function() { /* code */ });
// Flight::patch('/', function() { /* code */ });
// Flight::put('/', function() { /* code */ });
// Flight::delete('/', function() { /* code */ });

You can also map multiple methods to a single callback by using a | delimiter:

Flight::route('GET|POST /', function () {
  echo 'I received either a GET or a POST request.';
});

Additionally you can grab the Router object which has some helper methods for you to use:


$router = Flight::router();

// maps all methods
$router->map('/', function() {
    echo 'hello world!';
});

// GET request
$router->get('/users', function() {
    echo 'users';
});
// $router->post();
// $router->put();
// $router->delete();
// $router->patch();

Regular Expressions

You can use regular expressions in your routes:

Flight::route('/user/[0-9]+', function () {
  // This will match /user/1234
});

Although this method is available, it is recommended to use named parameters, or named parameters with regular expressions, as they are more readable and easier to maintain.

Named Parameters

You can specify named parameters in your routes which will be passed along to your callback function.

Flight::route('/@name/@id', function (string $name, string $id) {
  echo "hello, $name ($id)!";
});

You can also include regular expressions with your named parameters by using the : delimiter:

Flight::route('/@name/@id:[0-9]{3}', function (string $name, string $id) {
  // This will match /bob/123
  // But will not match /bob/12345
});

Note: Matching regex groups () with named parameters isn't supported. :'(

Optional Parameters

You can specify named parameters that are optional for matching by wrapping segments in parentheses.

Flight::route(
  '/blog(/@year(/@month(/@day)))',
  function(?string $year, ?string $month, ?string $day) {
    // This will match the following URLS:
    // /blog/2012/12/10
    // /blog/2012/12
    // /blog/2012
    // /blog
  }
);

Any optional parameters that are not matched will be passed in as NULL.

Wildcards

Matching is only done on individual URL segments. If you want to match multiple segments you can use the * wildcard.

Flight::route('/blog/*', function () {
  // This will match /blog/2000/02/01
});

To route all requests to a single callback, you can do:

Flight::route('*', function () {
  // Do something
});

Passing

You can pass execution on to the next matching route by returning true from your callback function.

Flight::route('/user/@name', function (string $name) {
  // Check some condition
  if ($name !== "Bob") {
    // Continue to next route
    return true;
  }
});

Flight::route('/user/*', function () {
  // This will get called
});

Route Aliasing

You can assign an alias to a route, so that the URL can dynamically be generated later in your code (like a template for instance).

Flight::route('/users/@id', function($id) { echo 'user:'.$id; }, false, 'user_view');

// later in code somewhere
Flight::getUrl('user_view', [ 'id' => 5 ]); // will return '/users/5'

This is especially helpful if your URL happens to change. In the above example, lets say that users was moved to /admin/users/@id instead. With aliasing in place, you don't have to change anywhere you reference the alias because the alias will now return /admin/users/5 like in the example above.

Route aliasing still works in groups as well:

Flight::group('/users', function() {
    Flight::route('/@id', function($id) { echo 'user:'.$id; }, false, 'user_view');
});

// later in code somewhere
Flight::getUrl('user_view', [ 'id' => 5 ]); // will return '/users/5'

Route Info

If you want to inspect the matching route information, you can request for the route object to be passed to your callback by passing in true as the third parameter in the route method. The route object will always be the last parameter passed to your callback function.

Flight::route('/', function(\flight\net\Route $route) {
  // Array of HTTP methods matched against
  $route->methods;

  // Array of named parameters
  $route->params;

  // Matching regular expression
  $route->regex;

  // Contains the contents of any '*' used in the URL pattern
  $route->splat;

  // Shows the url path....if you really need it
  $route->pattern;

  // Shows what middleware is assigned to this
  $route->middleware;

  // Shows the alias assigned to this route
  $route->alias;
}, true);

Route Grouping

There may be times when you want to group related routes together (such as /api/v1). You can do this by using the group method:

Flight::group('/api/v1', function () {
  Flight::route('/users', function () {
    // Matches /api/v1/users
  });

  Flight::route('/posts', function () {
    // Matches /api/v1/posts
  });
});

You can even nest groups of groups:

Flight::group('/api', function () {
  Flight::group('/v1', function () {
    // Flight::get() gets variables, it doesn't set a route! See object context below
    Flight::route('GET /users', function () {
      // Matches GET /api/v1/users
    });

    Flight::post('/posts', function () {
      // Matches POST /api/v1/posts
    });

    Flight::put('/posts/1', function () {
      // Matches PUT /api/v1/posts
    });
  });
  Flight::group('/v2', function () {

    // Flight::get() gets variables, it doesn't set a route! See object context below
    Flight::route('GET /users', function () {
      // Matches GET /api/v2/users
    });
  });
});

Grouping with Object Context

You can still use route grouping with the Engine object in the following way:

$app = new \flight\Engine();
$app->group('/api/v1', function (Router $router) {

  // user the $router variable
  $router->get('/users', function () {
    // Matches GET /api/v1/users
  });

  $router->post('/posts', function () {
    // Matches POST /api/v1/posts
  });
});

Resource Routing

You can create a set of routes for a resource using the resource method. This will create a set of routes for a resource that follows the RESTful conventions.

To create a resource, do the following:

Flight::resource('/users', UsersController::class);

And what will happen in the background is it will create the following routes:

[
      'index' => 'GET ',
      'create' => 'GET /create',
      'store' => 'POST ',
      'show' => 'GET /@id',
      'edit' => 'GET /@id/edit',
      'update' => 'PUT /@id',
      'destroy' => 'DELETE /@id'
]

And your controller will look like this:

class UsersController
{
    public function index(): void
    {
    }

    public function show(string $id): void
    {
    }

    public function create(): void
    {
    }

    public function store(): void
    {
    }

    public function edit(string $id): void
    {
    }

    public function update(string $id): void
    {
    }

    public function destroy(string $id): void
    {
    }
}

Note: You can view the newly added routes with runway by running php runway routes.

Customizing Resource Routes

There are a few options to configure the resource routes.

Alias Base

You can configure the aliasBase. By default the alias is the last part of the URL specified. For example /users/ would result in an aliasBase of users. When these routes are created, the aliases are users.index, users.create, etc. If you want to change the alias, set the aliasBase to the value you want.

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

Only and Except

You can also specify which routes you want to create by using the only and except options.

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

These are basically whitelisting and blacklisting options so you can specify which routes you want to create.

Middleware

You can also specify middleware to be run on each of the routes created by the resource method.

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

Streaming

You can now stream responses to the client using the streamWithHeaders() method. This is useful for sending large files, long running processes, or generating large responses. Streaming a route is handled a little differently than a regular route.

Note: Streaming responses is only available if you have flight.v2.output_buffering set to false.

Stream with Manual Headers

You can stream a response to the client by using the stream() method on a route. If you do this, you must set all the methods by hand before you output anything to the client. This is done with the header() php function or the Flight::response()->setRealHeader() method.

Flight::route('/@filename', function($filename) {

    // obviously you would sanitize the path and whatnot.
    $fileNameSafe = basename($filename);

    // If you have additional headers to set here after the route has executed
    // you must define them before anything is echoed out.
    // They must all be a raw call to the header() function or 
    // a call to Flight::response()->setRealHeader()
    header('Content-Disposition: attachment; filename="'.$fileNameSafe.'"');
    // or
    Flight::response()->setRealHeader('Content-Disposition', 'attachment; filename="'.$fileNameSafe.'"');

    $fileData = file_get_contents('/some/path/to/files/'.$fileNameSafe);

    // Error catching and whatnot
    if(empty($fileData)) {
        Flight::halt(404, 'File not found');
    }

    // manually set the content length if you'd like
    header('Content-Length: '.filesize($filename));

    // Stream the data to the client
    echo $fileData;

// This is the magic line here
})->stream();

Stream with Headers

You can also use the streamWithHeaders() method to set the headers before you start streaming.

Flight::route('/stream-users', function() {

    // you can add any additional headers you want here
    // you just must use header() or Flight::response()->setRealHeader()

    // however you pull your data, just as an example...
    $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 ',';
        }

        // This is required to send the data to the client
        ob_flush();
    }
    echo '}';

// This is how you'll set the headers before you start streaming.
})->streamWithHeaders([
    'Content-Type' => 'application/json',
    'Content-Disposition' => 'attachment; filename="users.json"',
    // optional status code, defaults to 200
    'status' => 200
]);

Learn/flight_vs_symfony

Flight vs Symfony

What is Symfony?

Symfony is a set of reusable PHP components and a PHP framework for web projects.

The standard foundation on which the best PHP applications are built. Choose any of the 50 stand-alone components available for your own applications.

Speed up the creation and maintenance of your PHP web applications. End repetitive coding tasks and enjoy the power of controlling your code.

Pros compared to Flight

Cons compared to Flight

Learn/flight_vs_another_framework

Comparing Flight to Another Framework

If you are migrating from another framework such as Laravel, Slim, Fat-Free, or Symfony to Flight, this page will help you understand the differences between the two.

Laravel

Laravel is a full-featured framework that has all the bells and whistles and an amazing developer focused ecosystem, but at a cost in performance and complexity.

See the comparison between Laravel and Flight.

Slim

Slim is a micro-framework that is similar to Flight. It is designed to be lightweight and easy to use, but can be a bit more complex than Flight.

See the comparison between Slim and Flight.

Fat-Free

Fat-Free is a full-stack framework in a much smaller package. While it has all the tools in the toolbox, it does have a data architecture that can make some projects more complex than they need to be.

See the comparison between Fat-Free and Flight.

Symfony

Symfony is a modular, enterprise level framework that is designed to be flexible and scalable. For smaller projects or newer developers, Symfony can be a bit overwhelming.

See the comparison between Symfony and Flight.

Learn/dependency_injection_container

Dependency Injection Container

Introduction

The Dependency Injection Container (DIC) is a powerful tool that allows you to manage your application's dependencies. It is a key concept in modern PHP frameworks and is used to manage the instantiation and configuration of objects. Some examples of DIC libraries are: Dice, Pimple, PHP-DI, and league/container.

A DIC a fancy way of saying that it allows you to create and manage your classes in a centralized location. This is useful for when you need to pass the same object to multiple classes (like your controllers). A simple example might help this make more sense.

Basic Example

The old way of doing things might look like this:


require 'vendor/autoload.php';

// class to manage users from the database
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();

You can see from the above code that we are creating a new PDO object and passing it to our UserController class. This is fine for a small application, but as your application grows, you will find that you are creating the same PDO object in multiple places. This is where a DIC comes in handy.

Here is the same example using a DIC (using Dice):


require 'vendor/autoload.php';

// same class as above. Nothing changed
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());
    }
}

// create a new container
$container = new \Dice\Dice;
// don't forget to reassign it to itself like below!
$container = $container->addRule('PDO', [
    // shared means that the same object will be returned each time
    'shared' => true,
    'constructParams' => ['mysql:host=localhost;dbname=test', 'user', 'pass' ]
]);

// This registers the container handler so Flight knows to use it.
Flight::registerContainerHandler(function($class, $params) use ($container) {
    return $container->create($class, $params);
});

// now we can use the container to create our UserController
Flight::route('/user/@id', [ 'UserController', 'view' ]);
// or alternatively you can define the route like this
Flight::route('/user/@id', 'UserController->view');
// or
Flight::route('/user/@id', 'UserController::view');

Flight::start();

I bet you might be thinking that there was a lot of extra code added to the example. The magic comes from when you have another controller that needs the PDO object.


// If all your controllers have a constructor that needs a PDO object
// each of the routes below will automatically have it injected!!!
Flight::route('/company/@id', 'CompanyController->view');
Flight::route('/organization/@id', 'OrganizationController->view');
Flight::route('/category/@id', 'CategoryController->view');
Flight::route('/settings', 'SettingsController->view');

The added bonus of utilizing a DIC is that unit testing becomes much easier. You can create a mock object and pass it to your class. This is a huge benefit when you are writing tests for your application!

PSR-11

Flight can also use any PSR-11 compliant container. This means that you can use any container that implements the PSR-11 interface. Here is an example using League's PSR-11 container:


require 'vendor/autoload.php';

// same UserController class as above

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

This can be a little more verbose than the previous Dice example, it still gets the job done with the same benefits!

Custom DIC Handler

You can also create your own DIC handler. This is useful if you have a custom container that you want to use that is not PSR-11 (Dice). See the basic example for how to do this.

Additionally, there are some helpful defaults that will make your life easier when using Flight.

Engine Instance

If you are using the Engine instance in your controllers/middleware, here is how you would configure it:


// Somewhere in your bootstrap file
$engine = Flight::app();

$container = new \Dice\Dice;
$container = $container->addRule('*', [
    'substitutions' => [
        // This is where you pass in the instance
        Engine::class => $engine
    ]
]);

$engine->registerContainerHandler(function($class, $params) use ($container) {
    return $container->create($class, $params);
});

// Now you can use the Engine instance in your controllers/middleware

class MyController {
    public function __construct(Engine $app) {
        $this->app = $app;
    }

    public function index() {
        $this->app->render('index');
    }
}

Adding Other Classes

If you have other classes that you want to add to the container, with Dice it's easy as they will be automatically resolved by the container. Here is an example:


$container = new \Dice\Dice;
// If you don't need to inject anything into your class
// you don't need to define anything!
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

Route Middleware

Flight supports route and group route middleware. Middleware is a function that is executed before (or after) the route callback. This is a great way to add API authentication checks in your code, or to validate that the user has permission to access the route.

Basic Middleware

Here's a basic example:

// If you only supply an anonymous function, it will be executed before the route callback. 
// there are no "after" middleware functions except for classes (see below)
Flight::route('/path', function() { echo ' Here I am!'; })->addMiddleware(function() {
    echo 'Middleware first!';
});

Flight::start();

// This will output "Middleware first! Here I am!"

There are some very important notes about middleware that you should be aware of before you use them:

Middleware Classes

Middleware can be registered as a class as well. If you need the "after" functionality, you must use a class.

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

// This will display "Middleware first! Here I am! Middleware last!"

Handling Middleware Errors

Let's say you have an auth middleware and you want to redirect the user to a login page if they are not authenticated. You have a couple of options at your disposal:

  1. You can return false from the middleware function and Flight will automatically return a 403 Forbidden error, but have no customization.
  2. You can redirect the user to a login page using Flight::redirect().
  3. You can create a custom error within the middleware and halt execution of the route.

Basic Example

Here is a simple return false; example:

class MyMiddleware {
    public function before($params) {
        if (isset($_SESSION['user']) === false) {
            return false;
        }

        // since it's true, everything just keeps on going
    }
}

Redirect Example

Here is an example of redirecting the user to a login page:

class MyMiddleware {
    public function before($params) {
        if (isset($_SESSION['user']) === false) {
            Flight::redirect('/login');
            exit;
        }
    }
}

Custom Error Example

Let's say you need to throw a JSON error because you're building an API. You can do that like this:

class MyMiddleware {
    public function before($params) {
        $authorization = Flight::request()->headers['Authorization'];
        if(empty($authorization)) {
            Flight::jsonHalt(['error' => 'You must be logged in to access this page.'], 403);
            // or
            Flight::json(['error' => 'You must be logged in to access this page.'], 403);
            exit;
            // or
            Flight::halt(403, json_encode(['error' => 'You must be logged in to access this page.']);
        }
    }
}

Grouping Middleware

You can add a route group, and then every route in that group will have the same middleware as well. This is useful if you need to group a bunch of routes by say an Auth middleware to check the API key in the header.


// added at the end of the group method
Flight::group('/api', function() {

    // This "empty" looking route will actually match /api
    Flight::route('', function() { echo 'api'; }, false, 'api');
    // This will match /api/users
    Flight::route('/users', function() { echo 'users'; }, false, 'users');
    // This will match /api/users/1234
    Flight::route('/users/@id', function($id) { echo 'user:'.$id; }, false, 'user_view');
}, [ new ApiAuthMiddleware() ]);

If you want to apply a global middleware to all your routes, you can add an "empty" group:


// added at the end of the group method
Flight::group('', function() {

    // This is still /users
    Flight::route('/users', function() { echo 'users'; }, false, 'users');
    // And this is still /users/1234
    Flight::route('/users/@id', function($id) { echo 'user:'.$id; }, false, 'user_view');
}, [ new ApiAuthMiddleware() ]);

Learn/filtering

Filtering

Flight allows you to filter methods before and after they are called. There are no predefined hooks you need to memorize. You can filter any of the default framework methods as well as any custom methods that you've mapped.

A filter function looks like this:

function (array &$params, string &$output): bool {
  // Filter code
}

Using the passed in variables you can manipulate the input parameters and/or the output.

You can have a filter run before a method by doing:

Flight::before('start', function (array &$params, string &$output): bool {
  // Do something
});

You can have a filter run after a method by doing:

Flight::after('start', function (array &$params, string &$output): bool {
  // Do something
});

You can add as many filters as you want to any method. They will be called in the order that they are declared.

Here's an example of the filtering process:

// Map a custom method
Flight::map('hello', function (string $name) {
  return "Hello, $name!";
});

// Add a before filter
Flight::before('hello', function (array &$params, string &$output): bool {
  // Manipulate the parameter
  $params[0] = 'Fred';
  return true;
});

// Add an after filter
Flight::after('hello', function (array &$params, string &$output): bool {
  // Manipulate the output
  $output .= " Have a nice day!";
  return true;
});

// Invoke the custom method
echo Flight::hello('Bob');

This should display:

Hello Fred! Have a nice day!

If you have defined multiple filters, you can break the chain by returning false in any of your filter functions:

Flight::before('start', function (array &$params, string &$output): bool {
  echo 'one';
  return true;
});

Flight::before('start', function (array &$params, string &$output): bool {
  echo 'two';

  // This will end the chain
  return false;
});

// This will not get called
Flight::before('start', function (array &$params, string &$output): bool {
  echo 'three';
  return true;
});

Note, core methods such as map and register cannot be filtered because they are called directly and not invoked dynamically.

Learn/requests

Requests

Flight encapsulates the HTTP request into a single object, which can be accessed by doing:

$request = Flight::request();

Typical Use Cases

When you are working with a request in a web application, typically you'll want to pull out a header, or a $_GET or $_POST parameter, or maybe even the raw request body. Flight provides a simple interface to do all of these things.

Here's an example getting a query string parameter:

Flight::route('/search', function(){
    $keyword = Flight::request()->query['keyword'];
    echo "You are searching for: $keyword";
    // query a database or something else with the $keyword
});

Here's an example of maybe a form with a POST method:

Flight::route('POST /submit', function(){
    $name = Flight::request()->data['name'];
    $email = Flight::request()->data['email'];
    echo "You submitted: $name, $email";
    // save to a database or something else with the $name and $email
});

Request Object Properties

The request object provides the following properties:

You can access the query, data, cookies, and files properties as arrays or objects.

So, to get a query string parameter, you can do:

$id = Flight::request()->query['id'];

Or you can do:

$id = Flight::request()->query->id;

RAW Request Body

To get the raw HTTP request body, for example when dealing with PUT requests, you can do:

$body = Flight::request()->getBody();

JSON Input

If you send a request with the type application/json and the data {"id": 123} it will be available from the data property:

$id = Flight::request()->data->id;

$_GET

You can access the $_GET array via the query property:

$id = Flight::request()->query['id'];

$_POST

You can access the $_POST array via the data property:

$id = Flight::request()->data['id'];

$_COOKIE

You can access the $_COOKIE array via the cookies property:

$myCookieValue = Flight::request()->cookies['myCookieName'];

$_SERVER

There is a shortcut available to access the $_SERVER array via the getVar() method:


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

Accessing Uploaded Files via $_FILES

You can access uploaded files via the files property:

$uploadedFile = Flight::request()->files['myFile'];

Processing File Uploads (v3.12.0)

You can process file uploads using the framework with some helper methods. It basically boils down to pulling the file data from the request, and moving it to a new location.

Flight::route('POST /upload', function(){
    // If you had an input field like <input type="file" name="myFile">
    $uploadedFileData = Flight::request()->getUploadedFiles();
    $uploadedFile = $uploadedFileData['myFile'];
    $uploadedFile->moveTo('/path/to/uploads/' . $uploadedFile->getClientFilename());
});

If you have multiple files uploaded, you can loop through them:

Flight::route('POST /upload', function(){
    // If you had an input field like <input type="file" name="myFiles[]">
    $uploadedFiles = Flight::request()->getUploadedFiles()['myFiles'];
    foreach ($uploadedFiles as $uploadedFile) {
        $uploadedFile->moveTo('/path/to/uploads/' . $uploadedFile->getClientFilename());
    }
});

Security Note: Always validate and sanitize user input, especially when dealing with file uploads. Always validate the type of extensions you'll allow to be uploaded, but you should also validate the "magic bytes" of the file to ensure it's actually the type of file the user claims it is. There are articles and libraries available to help with this.

Request Headers

You can access request headers using the getHeader() or getHeaders() method:


// Maybe you need Authorization header
$host = Flight::request()->getHeader('Authorization');
// or
$host = Flight::request()->header('Authorization');

// If you need to grab all headers
$headers = Flight::request()->getHeaders();
// or
$headers = Flight::request()->headers();

Request Body

You can access the raw request body using the getBody() method:

$body = Flight::request()->getBody();

Request Method

You can access the request method using the method property or the getMethod() method:

$method = Flight::request()->method; // actually calls getMethod()
$method = Flight::request()->getMethod();

Note: The getMethod() method first pulls the method from $_SERVER['REQUEST_METHOD'], then it can be overwritten by $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] if it exists or $_REQUEST['_method'] if it exists.

Request URLs

There are a couple helper methods to piece together parts of a URL for your convenience.

Full URL

You can access the full request URL using the getFullUrl() method:

$url = Flight::request()->getFullUrl();
// https://example.com/some/path?foo=bar

Base URL

You can access the base URL using the getBaseUrl() method:

$url = Flight::request()->getBaseUrl();
// Notice, no trailing slash.
// https://example.com

Query Parsing

You can pass a URL to the parseQuery() method to parse the query string into an associative array:

$query = Flight::request()->parseQuery('https://example.com/some/path?foo=bar');
// ['foo' => 'bar']

Learn/api

Framework API Methods

Flight is designed to be easy to use and understand. The following is the complete set of methods for the framework. It consists of core methods, which are regular static methods, and extensible methods, which are mapped methods that can be filtered or overridden.

Core Methods

These methods are core to the framework and cannot be overridden.

Flight::map(string $name, callable $callback, bool $pass_route = false) // Creates a custom framework method.
Flight::register(string $name, string $class, array $params = [], ?callable $callback = null) // Registers a class to a framework method.
Flight::unregister(string $name) // Unregisters a class to a framework method.
Flight::before(string $name, callable $callback) // Adds a filter before a framework method.
Flight::after(string $name, callable $callback) // Adds a filter after a framework method.
Flight::path(string $path) // Adds a path for autoloading classes.
Flight::get(string $key) // Gets a variable set by Flight::set().
Flight::set(string $key, mixed $value) // Sets a variable within the Flight engine.
Flight::has(string $key) // Checks if a variable is set.
Flight::clear(array|string $key = []) // Clears a variable.
Flight::init() // Initializes the framework to its default settings.
Flight::app() // Gets the application object instance
Flight::request() // Gets the request object instance
Flight::response() // Gets the response object instance
Flight::router() // Gets the router object instance
Flight::view() // Gets the view object instance

Extensible Methods

Flight::start() // Starts the framework.
Flight::stop() // Stops the framework and sends a response.
Flight::halt(int $code = 200, string $message = '') // Stop the framework with an optional status code and message.
Flight::route(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') // Maps a URL pattern to a callback.
Flight::post(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') // Maps a POST request URL pattern to a callback.
Flight::put(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') // Maps a PUT request URL pattern to a callback.
Flight::patch(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') // Maps a PATCH request URL pattern to a callback.
Flight::delete(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') // Maps a DELETE request URL pattern to a callback.
Flight::group(string $pattern, callable $callback) // Creates grouping for urls, pattern must be a string.
Flight::getUrl(string $name, array $params = []) // Generates a URL based on a route alias.
Flight::redirect(string $url, int $code) // Redirects to another URL.
Flight::download(string $filePath) // Downloads a file.
Flight::render(string $file, array $data, ?string $key = null) // Renders a template file.
Flight::error(Throwable $error) // Sends an HTTP 500 response.
Flight::notFound() // Sends an HTTP 404 response.
Flight::etag(string $id, string $type = 'string') // Performs ETag HTTP caching.
Flight::lastModified(int $time) // Performs last modified HTTP caching.
Flight::json(mixed $data, int $code = 200, bool $encode = true, string $charset = 'utf8', int $option) // Sends a JSON response.
Flight::jsonp(mixed $data, string $param = 'jsonp', int $code = 200, bool $encode = true, string $charset = 'utf8', int $option) // Sends a JSONP response.
Flight::jsonHalt(mixed $data, int $code = 200, bool $encode = true, string $charset = 'utf8', int $option) // Sends a JSON response and stops the framework.

Any custom methods added with map and register can also be filtered. For examples on how to map these methods, see the Extending Flight guide.

Learn/why_frameworks

Why a Framework?

Some programmers are vehemently opposed to using frameworks. They argue that frameworks are bloated, slow, and difficult to learn. They say that frameworks are unnecessary and that you can write better code without them. There are certainly some valid points to be made about the disadvantages of using frameworks. However, there are also many advantages to using frameworks.

Reasons to Use a Framework

Here are a few reasons why you might want to consider using a framework:

Flight is a micro-framework. This means that it is small and lightweight. It doesn't provide as much functionality as larger frameworks like Laravel or Symfony. However, it does provide a lot of the functionality that you need to build web applications. It is also easy to learn and use. This makes it a good choice for building web applications quickly and easily. If you are new to frameworks, Flight is a great beginner framework to start with. It will help you learn about the advantages of using frameworks without overwhelming you with too much complexity. After you have some experience with Flight, it will be easier to move onto more complex frameworks like Laravel or Symfony, however Flight can still make a successful robust application.

What is Routing?

Routing is the core of the Flight framework, but what is it exactly? Routing is the process of taking a URL and matching it to a specific function in your code. This is how you can make your website do different things based on the URL that is requested. For example, you might want to show a user's profile when they visit /user/1234, but show a list of all users when they visit /users. This is all done through routing.

It might work something like this:

And Why is it Important?

Having a proper centralized router can actually make your life dramatically easier! It just might be hard to see at first. Here are a few reasons why:

I'm sure you're familiar with the script by script way of creating a website. You might have a file called index.php that has a bunch of if statements to check the URL and then run a specific function based on the URL. This is a form of routing, but it's not very organized and it can get out of hand quickly. Flight's routing system is a much more organized and powerful way to handle routing.

This?


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

// etc...

Or this?


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

// In maybe your app/controllers/UserController.php
class UserController {
    public function viewUserProfile($id) {
        // do something
    }

    public function editUserProfile($id) {
        // do something
    }
}

Hopefully you can start to see the benefits of using a centralized routing system. It's a lot easier to manage and understand in the long run!

Requests and Responses

Flight provides a simple and easy way to handle requests and responses. This is the core of what a web framework does. It takes in a request from a user's browser, processes it, and then sends back a response. This is how you can build web applications that do things like show a user's profile, let a user log in, or let a user post a new blog post.

Requests

A request is what a user's browser sends to your server when they visit your website. This request contains information about what the user wants to do. For example, it might contain information about what URL the user wants to visit, what data the user wants to send to your server, or what kind of data the user wants to receive from your server. It's important to know that a request is read-only. You can't change the request, but you can read from it.

Flight provides a simple way to access information about the request. You can access information about the request using the Flight::request() method. This method returns a Request object that contains information about the request. You can use this object to access information about the request, such as the URL, the method, or the data that the user sent to your server.

Responses

A response is what your server sends back to a user's browser when they visit your website. This response contains information about what your server wants to do. For example, it might contain information about what kind of data your server wants to send to the user, what kind of data your server wants to receive from the user, or what kind of data your server wants to store on the user's computer.

Flight provides a simple way to send a response to a user's browser. You can send a response using the Flight::response() method. This method takes a Response object as an argument and sends the response to the user's browser. You can use this object to send a response to the user's browser, such as HTML, JSON, or a file. Flight helps you auto generate some parts of the response to make things easy, but ultimately you have control over what you send back to the user.

Learn/responses

Responses

Flight helps generate part of the response headers for you, but you hold most of the control over what you send back to the user. Sometimes you can access the Response object directly, but most of the time you'll use the Flight instance to send a response.

Sending a Basic Response

Flight uses ob_start() to buffer the output. This means you can use echo or print to send a response to the user and Flight will capture it and send it back to the user with the appropriate headers.


// This will send "Hello, World!" to the user's browser
Flight::route('/', function() {
    echo "Hello, World!";
});

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

As an alternative, you can call the write() method to add to the body as well.


// This will send "Hello, World!" to the user's browser
Flight::route('/', function() {
    // verbose, but gets the job sometimes when you need it
    Flight::response()->write("Hello, World!");

    // if you want to retrieve the body that you've set at this point
    // you can do so like this
    $body = Flight::response()->getBody();
});

Status Codes

You can set the status code of the response by using the status method:

Flight::route('/@id', function($id) {
    if($id == 123) {
        Flight::response()->status(200);
        echo "Hello, World!";
    } else {
        Flight::response()->status(403);
        echo "Forbidden";
    }
});

If you want to get the current status code, you can use the status method without any arguments:

Flight::response()->status(); // 200

Setting a Response Body

You can set the response body by using the write method, however, if you echo or print anything, it will be captured and sent as the response body via output buffering.

Flight::route('/', function() {
    Flight::response()->write("Hello, World!");
});

// same as

Flight::route('/', function() {
    echo "Hello, World!";
});

Clearing a Response Body

If you want to clear the response body, you can use the clearBody method:

Flight::route('/', function() {
    if($someCondition) {
        Flight::response()->write("Hello, World!");
    } else {
        Flight::response()->clearBody();
    }
});

Running a Callback on the Response Body

You can run a callback on the response body by using the addResponseBodyCallback method:

Flight::route('/users', function() {
    $db = Flight::db();
    $users = $db->fetchAll("SELECT * FROM users");
    Flight::render('users_table', ['users' => $users]);
});

// This will gzip all the responses for any route
Flight::response()->addResponseBodyCallback(function($body) {
    return gzencode($body, 9);
});

You can add multiple callbacks and they will be run in the order they were added. Because this can accept any callable, it can accept a class array [ $class, 'method' ], a closure $strReplace = function($body) { str_replace('hi', 'there', $body); };, or a function name 'minify' if you had a function to minify your html code for example.

Note: Route callbacks will not work if you are using the flight.v2.output_buffering configuration option.

Specific Route Callback

If you wanted this to only apply to a specific route, you could add the callback in the route itself:

Flight::route('/users', function() {
    $db = Flight::db();
    $users = $db->fetchAll("SELECT * FROM users");
    Flight::render('users_table', ['users' => $users]);

    // This will gzip only the response for this route
    Flight::response()->addResponseBodyCallback(function($body) {
        return gzencode($body, 9);
    });
});

Middleware Option

You can also use middleware to apply the callback to all routes via middleware:

// MinifyMiddleware.php
class MinifyMiddleware {
    public function before() {
        // Apply the callback here on the response() object.
        Flight::response()->addResponseBodyCallback(function($body) {
            return $this->minify($body);
        });
    }

    protected function minify(string $body): string {
        // minify the body somehow
        return $body;
    }
}

// index.php
Flight::group('/users', function() {
    Flight::route('', function() { /* ... */ });
    Flight::route('/@id', function($id) { /* ... */ });
}, [ new MinifyMiddleware() ]);

Setting a Response Header

You can set a header such as content type of the response by using the header method:


// This will send "Hello, World!" to the user's browser in plain text
Flight::route('/', function() {
    Flight::response()->header('Content-Type', 'text/plain');
    // or
    Flight::response()->setHeader('Content-Type', 'text/plain');
    echo "Hello, World!";
});

JSON

Flight provides support for sending JSON and JSONP responses. To send a JSON response you pass some data to be JSON encoded:

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

JSON with Status Code

You can also pass in a status code as the second argument:

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

JSON with Pretty Print

You can also pass in an argument to the last position to enable pretty printing:

Flight::json(['id' => 123], 200, true, 'utf-8', JSON_PRETTY_PRINT);

If you are changing options passed into Flight::json() and want a simpler syntax, you can just remap the JSON method:

Flight::map('json', function($data, $code = 200, $options = 0) {
    Flight::_json($data, $code, true, 'utf-8', $options);
}

// And now it can be used like this
Flight::json(['id' => 123], 200, JSON_PRETTY_PRINT);

JSON and Stop Execution (v3.10.0)

If you want to send a JSON response and stop execution, you can use the jsonHalt method. This is useful for cases where you are checking for maybe some type of authorization and if the user is not authorized, you can send a JSON response immediately, clear the existing body content and stop execution.

Flight::route('/users', function() {
    $authorized = someAuthorizationCheck();
    // Check if the user is authorized
    if($authorized === false) {
        Flight::jsonHalt(['error' => 'Unauthorized'], 401);
    }

    // Continue with the rest of the route
});

Before v3.10.0, you would have to do something like this:

Flight::route('/users', function() {
    $authorized = someAuthorizationCheck();
    // Check if the user is authorized
    if($authorized === false) {
        Flight::halt(401, json_encode(['error' => 'Unauthorized']));
    }

    // Continue with the rest of the route
});

JSONP

For JSONP requests you, can optionally pass in the query parameter name you are using to define your callback function:

Flight::jsonp(['id' => 123], 'q');

So, when making a GET request using ?q=my_func, you should receive the output:

my_func({"id":123});

If you don't pass in a query parameter name it will default to jsonp.

Redirect to another URL

You can redirect the current request by using the redirect() method and passing in a new URL:

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

By default Flight sends a HTTP 303 ("See Other") status code. You can optionally set a custom code:

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

Stopping

You can stop the framework at any point by calling the halt method:

Flight::halt();

You can also specify an optional HTTP status code and message:

Flight::halt(200, 'Be right back...');

Calling halt will discard any response content up to that point. If you want to stop the framework and output the current response, use the stop method:

Flight::stop();

Clearing Response Data

You can clear the response body and headers by using the clear() method. This will clear any headers assigned to the response, clear the response body, and set the status code to 200.

Flight::response()->clear();

Clearing Response Body Only

If you only want to clear the response body, you can use the clearBody() method:

// This will still keep any headers set on the response() object.
Flight::response()->clearBody();

HTTP Caching

Flight provides built-in support for HTTP level caching. If the caching condition is met, Flight will return an HTTP 304 Not Modified response. The next time the client requests the same resource, they will be prompted to use their locally cached version.

Route Level Caching

If you want to cache your whole response, you can use the cache() method and pass in time to cache.


// This will cache the response for 5 minutes
Flight::route('/news', function () {
  Flight::response()->cache(time() + 300);
  echo 'This content will be cached.';
});

// Alternatively, you can use a string that you would pass
// to the strtotime() method
Flight::route('/news', function () {
  Flight::response()->cache('+5 minutes');
  echo 'This content will be cached.';
});

Last-Modified

You can use the lastModified method and pass in a UNIX timestamp to set the date and time a page was last modified. The client will continue to use their cache until the last modified value is changed.

Flight::route('/news', function () {
  Flight::lastModified(1234567890);
  echo 'This content will be cached.';
});

ETag

ETag caching is similar to Last-Modified, except you can specify any id you want for the resource:

Flight::route('/news', function () {
  Flight::etag('my-unique-id');
  echo 'This content will be cached.';
});

Keep in mind that calling either lastModified or etag will both set and check the cache value. If the cache value is the same between requests, Flight will immediately send an HTTP 304 response and stop processing.

Download a File (v3.12.0)

There is a helper method to download a file. You can use the download method and pass in the path.

Flight::route('/download', function () {
  Flight::download('/path/to/file.txt');
});

Learn/templates

HTML Views and Templates

Flight provides some basic templating functionality by default.

If you need more complex templating needs, see the Smarty and Latte examples in the Custom Views section.

Default View Engine

To display a view template call the render method with the name of the template file and optional template data:

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

The template data you pass in is automatically injected into the template and can be reference like a local variable. Template files are simply PHP files. If the content of the hello.php template file is:

Hello, <?= $name ?>!

The output would be:

Hello, Bob!

You can also manually set view variables by using the set method:

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

The variable name is now available across all your views. So you can simply do:

Flight::render('hello');

Note that when specifying the name of the template in the render method, you can leave out the .php extension.

By default Flight will look for a views directory for template files. You can set an alternate path for your templates by setting the following config:

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

Layouts

It is common for websites to have a single layout template file with interchanging content. To render content to be used in a layout, you can pass in an optional parameter to the render method.

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

Your view will then have saved variables called headerContent and bodyContent. You can then render your layout by doing:

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

If the template files looks like this:

header.php:

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

body.php:

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

layout.php:

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

The output would be:

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

Custom View Engines

Flight allows you to swap out the default view engine simply by registering your own view class.

Smarty

Here's how you would use the Smarty template engine for your views:

// Load Smarty library
require './Smarty/libs/Smarty.class.php';

// Register Smarty as the view class
// Also pass a callback function to configure Smarty on load
Flight::register('view', Smarty::class, [], function (Smarty $smarty) {
  $smarty->setTemplateDir('./templates/');
  $smarty->setCompileDir('./templates_c/');
  $smarty->setConfigDir('./config/');
  $smarty->setCacheDir('./cache/');
});

// Assign template data
Flight::view()->assign('name', 'Bob');

// Display the template
Flight::view()->display('hello.tpl');

For completeness, you should also override Flight's default render method:

Flight::map('render', function(string $template, array $data): void {
  Flight::view()->assign($data);
  Flight::view()->display($template);
});

Latte

Here's how you would use the Latte template engine for your views:


// Register Latte as the view class
// Also pass a callback function to configure Latte on load
Flight::register('view', Latte\Engine::class, [], function (Latte\Engine $latte) {
  // This is where Latte will cache your templates to speed things up
    // One neat thing about Latte is that it automatically refreshes your
    // cache when you make changes to your templates!
    $latte->setTempDirectory(__DIR__ . '/../cache/');

    // Tell Latte where the root directory for your views will be at.
    $latte->setLoader(new \Latte\Loaders\FileLoader(__DIR__ . '/../views/'));
});

// And wrap it up so you can use Flight::render() correctly
Flight::map('render', function(string $template, array $data): void {
  // This is like $latte_engine->render($template, $data);
  echo Flight::view()->render($template, $data);
});

Learn/flight_vs_fat_free

Flight vs Fat-Free

What is Fat-Free?

Fat-Free (affectionately known as F3) is a powerful yet easy-to-use PHP micro-framework designed to help you build dynamic and robust web applications - fast!

Flight compares with Fat-Free in many ways and is probably the closest cousin in terms of features and simplicity. Fat-Free has a lot of features that Flight does not have, but it also has a lot of features that Flight does have. Fat-Free is starting to show its age and is not as popular as it once was.

Updates are becoming less frequent and the community is not as active as it once was. The code is simple enough, but sometimes the lack of syntax discipline can make it difficult to read and understand. It does work for PHP 8.3, but the code itself still looks like it lives in PHP 5.3.

Pros compared to Flight

Cons compared to Flight

Learn/extending

Extending

Flight is designed to be an extensible framework. The framework comes with a set of default methods and components, but it allows you to map your own methods, register your own classes, or even override existing classes and methods.

If you are looking for a DIC (Dependency Injection Container), hop over to the Dependency Injection Container page.

Mapping Methods

To map your own simple custom method, you use the map function:

// Map your method
Flight::map('hello', function (string $name) {
  echo "hello $name!";
});

// Call your custom method
Flight::hello('Bob');

While it is possible to make simple custom methods, it is recommended to just create standard functions in PHP. This has autocomplete in IDE's and is easier to read. The equivalent of the above code would be:

function hello(string $name) {
  echo "hello $name!";
}

hello('Bob');

This is used more when you need to pass variables into your method to get an expected value. Using the register() method like below is more for passing in configuration and then calling your pre-configured class.

Registering Classes

To register your own class and configure it, you use the register function:

// Register your class
Flight::register('user', User::class);

// Get an instance of your class
$user = Flight::user();

The register method also allows you to pass along parameters to your class constructor. So when you load your custom class, it will come pre-initialized. You can define the constructor parameters by passing in an additional array. Here's an example of loading a database connection:

// Register class with constructor parameters
Flight::register('db', PDO::class, ['mysql:host=localhost;dbname=test', 'user', 'pass']);

// Get an instance of your class
// This will create an object with the defined parameters
//
// new PDO('mysql:host=localhost;dbname=test','user','pass');
//
$db = Flight::db();

// and if you needed it later in your code, you just call the same method again
class SomeController {
  public function __construct() {
    $this->db = Flight::db();
  }
}

If you pass in an additional callback parameter, it will be executed immediately after class construction. This allows you to perform any set up procedures for your new object. The callback function takes one parameter, an instance of the new object.

// The callback will be passed the object that was constructed
Flight::register(
  'db',
  PDO::class,
  ['mysql:host=localhost;dbname=test', 'user', 'pass'],
  function (PDO $db) {
    $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
  }
);

By default, every time you load your class you will get a shared instance. To get a new instance of a class, simply pass in false as a parameter:

// Shared instance of the class
$shared = Flight::db();

// New instance of the class
$new = Flight::db(false);

Keep in mind that mapped methods have precedence over registered classes. If you declare both using the same name, only the mapped method will be invoked.

Overriding Framework Methods

Flight allows you to override its default functionality to suit your own needs, without having to modify any code. You can view all the methods you can override here.

For example, when Flight cannot match a URL to a route, it invokes the notFound method which sends a generic HTTP 404 response. You can override this behavior by using the map method:

Flight::map('notFound', function() {
  // Display custom 404 page
  include 'errors/404.html';
});

Flight also allows you to replace core components of the framework. For example you can replace the default Router class with your own custom class:

// Register your custom class
Flight::register('router', MyRouter::class);

// When Flight loads the Router instance, it will load your class
$myrouter = Flight::router();

Framework methods like map and register however cannot be overridden. You will get an error if you try to do so.

Learn/flight_vs_slim

Flight vs Slim

What is Slim?

Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs.

A lot of the inspiration for some of the v3 features of Flight actually came from Slim. Grouping routes, and executing middleware in a specific order are two features that were inspired by Slim. Slim v3 came out geared towards simplicity, but there has been mixed reviews regarding v4.

Pros compared to Flight

Cons compared to Flight

Learn/autoloading

Autoloading

Autoloading is a concept in PHP where you specific a directory or directories to load classes from. This is much more beneficial than using require or include to load classes. It is also a requirement for using Composer packages.

By default any Flight class is autoloaded for your automatically thanks to composer. However, if you want to autoload your own classes, you can use the Flight::path() method to specify a directory to load classes from.

Basic Example

Let's assume we have a directory tree like the following:

# Example path
/home/user/project/my-flight-project/
├── app
│   ├── cache
│   ├── config
│   ├── controllers - contains the controllers for this project
│   ├── translations
│   ├── UTILS - contains classes for just this application (this is all caps on purpose for an example later)
│   └── views
└── public
    └── css
    └── js
    └── index.php

You may have noticed that this is the same file structure as this documentation site.

You can specify each directory to load from like this:


/**
 * public/index.php
 */

// Add a path to the autoloader
Flight::path(__DIR__.'/../app/controllers/');
Flight::path(__DIR__.'/../app/utils/');

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

// no namespacing required

// All autoloaded classes are recommended to be Pascal Case (each word capitalized, no spaces)
// As of 3.7.2, you can use Pascal_Snake_Case for your class names by running Loader::setV2ClassLoading(false);
class MyController {

    public function index() {
        // do something
    }
}

Namespaces

If you do have namespaces, it actually becomes very easy to implement this. You should use the Flight::path() method to specify the root directory (not the document root or public/ folder) of your application.


/**
 * public/index.php
 */

// Add a path to the autoloader
Flight::path(__DIR__.'/../');

Now this is what your controller might look like. Look at the example below, but pay attention to the comments for important information.

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

// namespaces are required
// namespaces are the same as the directory structure
// namespaces must follow the same case as the directory structure
// namespaces and directories cannot have any underscores (unless Loader::setV2ClassLoading(false) is set)
namespace app\controllers;

// All autoloaded classes are recommended to be Pascal Case (each word capitalized, no spaces)
// As of 3.7.2, you can use Pascal_Snake_Case for your class names by running Loader::setV2ClassLoading(false);
class MyController {

    public function index() {
        // do something
    }
}

And if you wanted to autoload a class in your utils directory, you would do basically the same thing:


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

// namespace must match the directory structure and case (note the UTILS directory is all caps
//     like in the file tree above)
namespace app\UTILS;

class ArrayHelperUtil {

    public function changeArrayCase(array $array) {
        // do something
    }
}

Underscores in Class Names

As of 3.7.2, you can use Pascal_Snake_Case for your class names by running Loader::setV2ClassLoading(false);. This will allow you to use underscores in your class names. This is not recommended, but it is available for those who need it.


/**
 * public/index.php
 */

// Add a path to the autoloader
Flight::path(__DIR__.'/../app/controllers/');
Flight::path(__DIR__.'/../app/utils/');
Loader::setV2ClassLoading(false);

/**
 * app/controllers/My_Controller.php
 */

// no namespacing required

class My_Controller {

    public function index() {
        // do something
    }
}

Learn/troubleshooting

Troubleshooting

This page will help you troubleshoot common issues that you may run into when using Flight.

Common Issues

404 Not Found or Unexpected Route Behavior

If you are seeing a 404 Not Found error (but you swear on your life that it's really there and it's not a typo) this actually could be a problem with you returning a value in your route endpoint instead of just echoing it. The reason for this is intentional but could sneak up on some developers.


Flight::route('/hello', function(){
    // This might cause a 404 Not Found error
    return 'Hello World';
});

// What you probably want
Flight::route('/hello', function(){
    echo 'Hello World';
});

The reason for this is because of a special mechanism built into the router that handles the return output as a single to "go to the next route". You can see the behavior documented in the Routing section.

Class Not Found (autoloading not working)

There could be a couple reasons for this one not happening. Below are some examples but make sure you also check out the autoloading section.

Incorrect File Name

The most common is that the class name doesn't match the file name.

If you have a class named MyClass then the file should be named MyClass.php. If you have a class named MyClass and the file is named myclass.php then the autoloader won't be able to find it.

Incorrect Namespace

If you are using namespaces, then the namespace should match the directory structure.

// code

// if your MyController is in the app/controllers directory and it's namespaced
// this will not work.
Flight::route('/hello', 'MyController->hello');

// you'll need to pick one of these options
Flight::route('/hello', 'app\controllers\MyController->hello');
// or if you have a use statement up top

use app\controllers\MyController;

Flight::route('/hello', [ MyController::class, 'hello' ]);
// also can be written
Flight::route('/hello', MyController::class.'->hello');
// also...
Flight::route('/hello', [ 'app\controllers\MyController', 'hello' ]);

path() not defined

In the skeleton app, this is defined inside the config.php file, but in order for your classes to be found, you need to make sure that the path() method is defined (probably to the root of your directory) before you try to use it.


// Add a path to the autoloader
Flight::path(__DIR__.'/../');

License

The MIT License (MIT)

Copyright © 2024 @mikecao, @n0nag0n

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

About

What is Flight?

Flight is a fast, simple, extensible framework for PHP. It is quite versatile and can be used for building any kind of web application. It is built with simplicity in mind and is written in a way that is easy to understand and use.

Flight is a great beginner framework for those who are new to PHP and want to learn how to build web applications. It is also a great framework for experienced developers who want more control over their web applications. It is engineered to easily build a RESTful API, a simple web application, or a complex web application.

Quick Start

<?php

// if installed with composer
require 'vendor/autoload.php';
// or if installed manually by zip file
// require 'flight/Flight.php';

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

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

Flight::start();

Simple enough right? Learn more about Flight in the documentation!

Skeleton/Boilerplate App

There is an example app that can help you get started with the Flight Framework. Go to flightphp/skeleton for instructions on how to get started! You can also visit the examples page for inspiration on some of the things you can do with Flight.

Community

We're on Matrix Chat with us at #flight-php-framework:matrix.org.

Contributing

There are two ways you can contribute to Flight:

  1. You can contribute to the core framework by visiting the core repository.
  2. You can contribute to the documentation. This documentation website is hosted on Github. If you notice an error or want to flesh out something better, feel free to correct it and submit a pull request! We try to keep up on things, but updates and language translations are welcome.

Requirements

Flight requires PHP 7.4 or greater.

Note: PHP 7.4 is supported because at the current time of writing (2024) PHP 7.4 is the default version for some LTS Linux distributions. Forcing a move to PHP >8 would cause a lot of heartburn for those users. The framework also supports PHP >8.

License

Flight is released under the MIT license.

Awesome-plugins/php_cookie

Cookies

overclokk/cookie is a simple library for managing cookies within your app.

Installation

Installation is simple with composer.

composer require overclokk/cookie

Usage

Usage is as simple as registering a new method on the Flight class.


use Overclokk\Cookie\Cookie;

/*
 * Set in your bootstrap or public/index.php file
 */

Flight::register('cookie', Cookie::class);

/**
 * ExampleController.php
 */

class ExampleController {
    public function login() {
        // Set a cookie

        // you'll want this to be false so you get a new instance
        // use the below comment if you want autocomplete
        /** @var \Overclokk\Cookie\Cookie $cookie */
        $cookie = Flight::cookie(false);
        $cookie->set(
            'stay_logged_in', // name of the cookie
            '1', // the value you want to set it to
            86400, // number of seconds the cookie should last
            '/', // path that the cookie will be available to
            'example.com', // domain that the cookie will be available to
            true, // cookie will only be transmitted over a secure HTTPS connection
            true // cookie will only be available through the HTTP protocol
        );

        // optionally, if you want to keep the default values
        // and have a quick way to set a cookie for a long time
        $cookie->forever('stay_logged_in', '1');
    }

    public function home() {
        // Check if you have the cookie
        if (Flight::cookie()->has('stay_logged_in')) {
            // put them in the dashboard area for example.
            Flight::redirect('/dashboard');
        }
    }
}

Awesome-plugins/php_encryption

PHP Encryption

defuse/php-encryption is a library that can be used to encrypt and decrypt data. Getting up and running is fairly simple to start encrypting and decrypting data. They have a great tutorial that helps explain the basics of how to use the library as well as important security implications regarding encryption.

Installation

Installation is simple with composer.

composer require defuse/php-encryption

Setup

Then you'll need to generate an encryption key.

vendor/bin/generate-defuse-key

This will spit out a key that you'll need to keep safe. You could keep the key in your app/config/config.php file in the array at the bottom of the file. While it's not the perfect spot, it's at least something.

Usage

Now that you have the library and an encryption key, you can start encrypting and decrypting data.


use Defuse\Crypto\Crypto;
use Defuse\Crypto\Key;

/*
 * Set in your bootstrap or public/index.php file
 */

// Encryption method
Flight::map('encrypt', function($raw_data) {
    $encryption_key = /* $config['encryption_key'] or a file_get_contents of where you put the key */;
    return Crypto::encrypt($raw_data, Key::loadFromAsciiSafeString($encryption_key));
});

// Decryption method
Flight::map('decrypt', function($encrypted_data) {
    $encryption_key = /* $config['encryption_key'] or a file_get_contents of where you put the key */;
    try {
        $raw_data = Crypto::decrypt($encrypted_data, Key::loadFromAsciiSafeString($encryption_key));
    } catch (Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException $ex) {
        // An attack! Either the wrong key was loaded, or the ciphertext has
        // changed since it was created -- either corrupted in the database or
        // intentionally modified by Eve trying to carry out an attack.

        // ... handle this case in a way that's suitable to your application ...
    }
    return $raw_data;
});

Flight::route('/encrypt', function() {
    $encrypted_data = Flight::encrypt('This is a secret');
    echo $encrypted_data;
});

Flight::route('/decrypt', function() {
    $encrypted_data = '...'; // Get the encrypted data from somewhere
    $decrypted_data = Flight::decrypt($encrypted_data);
    echo $decrypted_data;
});

Awesome-plugins/php_file_cache

Wruczek/PHP-File-Cache

Light, simple and standalone PHP in-file caching class

Advantages

Click here to view the code.

Installation

Install via composer:

composer require wruczek/php-file-cache

Usage

Usage is fairly straightforward.

use Wruczek\PhpFileCache\PhpFileCache;

$app = Flight::app();

// You pass the directory the cache will be stored in into the constructor
$app->register('cache', PhpFileCache::class, [ __DIR__ . '/../cache/' ], function(PhpFileCache $cache) {

    // This ensures that the cache is only used when in production mode
    // ENVIRONMENT is a constant that is set in your bootstrap file or elsewhere in your app
    $cache->setDevMode(ENVIRONMENT === 'development');
});

Then you can use it in your code like this:


// Get cache instance
$cache = Flight::cache();
$data = $cache->refreshIfExpired('simple-cache-test', function () {
    return date("H:i:s"); // return data to be cached
}, 10); // 10 seconds

// or
$data = $cache->retrieve('simple-cache-test');
if(empty($data)) {
    $data = date("H:i:s");
    $cache->store('simple-cache-test', $data, 10); // 10 seconds
}

Documentation

Visit https://github.com/Wruczek/PHP-File-Cache for full documentation and make sure you see the examples folder.

Awesome-plugins/permissions

FlightPHP/Permissions

This is a permissions module that can be used in your projects if you have multiple roles in your app and each role has a little bit different functionality. This module allows you to define permissions for each role and then check if the current user has the permission to access a certain page or perform a certain action.

Click here for the repository in GitHub.

Installation

Run composer require flightphp/permissions and you're on your way!

Usage

First you need to setup your permissions, then you tell the your app what the permissions mean. Ultimately you will check your permissions with $Permissions->has(), ->can(), or is(). has() and can() have the same functionality, but are named differently to make your code more readable.

Basic Example

Let's assume you have a feature in your application that checks if a user is logged in. You can create a permissions object like this:

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

// some code 

// then you probably have something that tells you who the current role is of the person
// likely you have something where you pull the current role
// from a session variable which defines this
// after someone logs in, otherwise they will have a 'guest' or 'public' role.
$current_role = 'admin';

// setup permissions
$permission = new \flight\Permission($current_role);
$permission->defineRule('loggedIn', function($current_role) {
    return $current_role !== 'guest';
});

// You'll probably want to persist this object in Flight somewhere
Flight::set('permission', $permission);

Then in a controller somewhere, you might have something like this.

<?php

// some controller
class SomeController {
    public function someAction() {
        $permission = Flight::get('permission');
        if ($permission->has('loggedIn')) {
            // do something
        } else {
            // do something else
        }
    }
}

You can also use this to track if they have permission to do something in your application. For instance, if your have a way that users can interact with posting on your software, you can check if they have permission to perform certain actions.

$current_role = 'admin';

// setup permissions
$permission = new \flight\Permission($current_role);
$permission->defineRule('post', function($current_role) {
    if($current_role === 'admin') {
        $permissions = ['create', 'read', 'update', 'delete'];
    } else if($current_role === 'editor') {
        $permissions = ['create', 'read', 'update'];
    } else if($current_role === 'author') {
        $permissions = ['create', 'read'];
    } else if($current_role === 'contributor') {
        $permissions = ['create'];
    } else {
        $permissions = [];
    }
    return $permissions;
});
Flight::set('permission', $permission);

Then in a controller somewhere...

class PostController {
    public function create() {
        $permission = Flight::get('permission');
        if ($permission->can('post.create')) {
            // do something
        } else {
            // do something else
        }
    }
}

Injecting dependencies

You can inject dependencies into the closure that defines the permissions. This is useful if you have some sort of toggle, id, or any other data point that you want to check against. The same works for Class->Method type calls, except you define the arguments in the method.

Closures

$Permission->defineRule('order', function(string $current_role, MyDependency $MyDependency = null) {
    // ... code
});

// in your controller file
public function createOrder() {
    $MyDependency = Flight::myDependency();
    $permission = Flight::get('permission');
    if ($permission->can('order.create', $MyDependency)) {
        // do something
    } else {
        // do something else
    }
}

Classes

namespace MyApp;

class Permissions {

    public function order(string $current_role, MyDependency $MyDependency = null) {
        // ... code
    }
}

Shortcut to set permissions with classes

You can also use classes to define your permissions. This is useful if you have a lot of permissions and you want to keep your code clean. You can do something like this:

<?php

// bootstrap code
$Permissions = new \flight\Permission($current_role);
$Permissions->defineRule('order', 'MyApp\Permissions->order');

// myapp/Permissions.php
namespace MyApp;

class Permissions {

    public function order(string $current_role, int $user_id) {
        // Assuming you set this up beforehand
        /** @var \flight\database\PdoWrapper $db */
        $db = Flight::db();
        $allowed_permissions = [ 'read' ]; // everyone can view an order
        if($current_role === 'manager') {
            $allowed_permissions[] = 'create'; // managers can create orders
        }
        $some_special_toggle_from_db = $db->fetchField('SELECT some_special_toggle FROM settings WHERE id = ?', [ $user_id ]);
        if($some_special_toggle_from_db) {
            $allowed_permissions[] = 'update'; // if the user has a special toggle, they can update orders
        }
        if($current_role === 'admin') {
            $allowed_permissions[] = 'delete'; // admins can delete orders
        }
        return $allowed_permissions;
    }
}

The cool part is that there is also a shortcut that you can use (that can also be cached!!!) where you just tell the permissions class to map all methods in a class into permissions. So if you have a method named order() and a method named company(), these will automatically be mapped so you can just run $Permissions->has('order.read') or $Permissions->has('company.read') and it will work. Defining this is very difficult, so stay with me here. You just need to do this:

Create the class of permissions you want to group together.

class MyPermissions {
    public function order(string $current_role, int $order_id = 0): array {
        // code to determine permissions
        return $permissions_array;
    }

    public function company(string $current_role, int $company_id): array {
        // code to determine permissions
        return $permissions_array;
    }
}

Then make the permissions discoverable using this library.

$Permissions = new \flight\Permission($current_role);
$Permissions->defineRulesFromClassMethods(MyApp\Permissions::class);
Flight::set('permissions', $Permissions);

Finally, call the permission in your codebase to check if the user is allowed to perform a given permission.

class SomeController {
    public function createOrder() {
        if(Flight::get('permissions')->can('order.create') === false) {
            die('You can\'t create an order. Sorry!');
        }
    }
}

Caching

To enable caching, see the simple wruczak/phpfilecache library. An example of enabling this is below.


// this $app can be part of your code, or
// you can just pass null and it will
// pull from Flight::app() in the constructor
$app = Flight::app();

// For now it accepts this as a file cache. Others can easily
// be added in the future. 
$Cache = new Wruczek\PhpFileCache\PhpFileCache;

$Permissions = new \flight\Permission($current_role, $app, $Cache);
$Permissions->defineRulesFromClassMethods(MyApp\Permissions::class, 3600); // 3600 is how many seconds to cache this for. Leave this off to not use caching

And away you go!

Awesome-plugins/pdo_wrapper

PdoWrapper PDO Helper Class

Flight comes with a helper class for PDO. It allows you to easily query your database with all the prepared/execute/fetchAll() wackiness. It greatly simplifies how you can query your database. Each row result is returned as a Flight Collection class which allows you to access your data via array syntax or object syntax.

Registering the PDO Helper Class

// Register the PDO helper class
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
    ]
]);

Usage

This object extends PDO so all the normal PDO methods are available. The following methods are added to make querying the database easier:

runQuery(string $sql, array $params = []): PDOStatement

Use this for INSERTS, UPDATES, or if you plan on using a SELECT in a while loop

$db = Flight::db();
$statement = $db->runQuery("SELECT * FROM table WHERE something = ?", [ $something ]);
while($row = $statement->fetch()) {
    // ...
}

// Or writing to the database
$db->runQuery("INSERT INTO table (name) VALUES (?)", [ $name ]);
$db->runQuery("UPDATE table SET name = ? WHERE id = ?", [ $name, $id ]);

fetchField(string $sql, array $params = []): mixed

Pulls the first field from the query

$db = Flight::db();
$count = $db->fetchField("SELECT COUNT(*) FROM table WHERE something = ?", [ $something ]);

fetchRow(string $sql, array $params = []): array

Pulls one row from the query

$db = Flight::db();
$row = $db->fetchRow("SELECT id, name FROM table WHERE id = ?", [ $id ]);
echo $row['name'];
// or
echo $row->name;

fetchAll(string $sql, array $params = []): array

Pulls all rows from the query

$db = Flight::db();
$rows = $db->fetchAll("SELECT id, name FROM table WHERE something = ?", [ $something ]);
foreach($rows as $row) {
    echo $row['name'];
    // or
    echo $row->name;
}

Note with IN() syntax

This also has a helpful wrapper for IN() statements. You can simply pass a single question mark as a placeholder for IN() and then an array of values. Here is an example of what that might look like:

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

Full Example

// Example route and how you would use this wrapper
Flight::route('/users', function () {
    // Get all users
    $users = Flight::db()->fetchAll('SELECT * FROM users');

    // Stream all users
    $statement = Flight::db()->runQuery('SELECT * FROM users');
    while ($user = $statement->fetch()) {
        echo $user['name'];
        // or echo $user->name;
    }

    // Get a single user
    $user = Flight::db()->fetchRow('SELECT * FROM users WHERE id = ?', [123]);

    // Get a single value
    $count = Flight::db()->fetchField('SELECT COUNT(*) FROM users');

    // Special IN() syntax to help out (make sure IN is in caps)
    $users = Flight::db()->fetchAll('SELECT * FROM users WHERE id IN (?)', [[1,2,3,4,5]]);
    // you could also do this
    $users = Flight::db()->fetchAll('SELECT * FROM users WHERE id IN (?)', [ '1,2,3,4,5']);

    // Insert a new user
    Flight::db()->runQuery("INSERT INTO users (name, email) VALUES (?, ?)", ['Bob', 'bob@example.com']);
    $insert_id = Flight::db()->lastInsertId();

    // Update a user
    Flight::db()->runQuery("UPDATE users SET name = ? WHERE id = ?", ['Bob', 123]);

    // Delete a user
    Flight::db()->runQuery("DELETE FROM users WHERE id = ?", [123]);

    // Get the number of affected rows
    $statement = Flight::db()->runQuery("UPDATE users SET name = ? WHERE name = ?", ['Bob', 'Sally']);
    $affected_rows = $statement->rowCount();

});

Awesome-plugins/session

Ghostff/Session

PHP Session Manager (non-blocking, flash, segment, session encryption). Uses PHP open_ssl for optional encrypt/decryption of session data. Supports File, MySQL, Redis, and Memcached.

Click here to view the code.

Installation

Install with composer.

composer require ghostff/session

Basic Configuration

You aren't required to pass anything in to use the default settings with your session. You can read about more settings in the Github Readme.


use Ghostff\Session\Session;

require 'vendor/autoload.php';

$app = Flight::app();

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

// one thing to remember is that you must commit your session on each page load
// or you'll need to run auto_commit in your configuration.

Simple Example

Here's a simple example of how you might use this.

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

    // do your login logic here
    // validate password, etc.

    // if the login is successful
    $session->set('is_logged_in', true);
    $session->set('user', $user);

    // any time you write to the session, you must commit it deliberately.
    $session->commit();
});

// This check could be in the restricted page logic, or wrapped with middleware.
Flight::route('/some-restricted-page', function() {
    $session = Flight::session();

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

    // do your restricted page logic here
});

// the middleware version
Flight::route('/some-restricted-page', function() {
    // regular page logic
})->addMiddleware(function() {
    $session = Flight::session();

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

More Complex Example

Here's a more complex example of how you might use this.


use Ghostff\Session\Session;

require 'vendor/autoload.php';

$app = Flight::app();

// set a custom path to your session configuration file and give it a random string for the session id
$app->register('session', Session::class, [ 'path/to/session_config.php', bin2hex(random_bytes(32)) ], function(Session $session) {
        // or you can manually override configuration options
        $session->updateConfiguration([
            // if you want to store your session data in a database (good if you want something like, "log me out of all devices" functionality)
            Session::CONFIG_DRIVER        => Ghostff\Session\Drivers\MySql::class,
            Session::CONFIG_ENCRYPT_DATA  => true,
            Session::CONFIG_SALT_KEY      => hash('sha256', 'my-super-S3CR3T-salt'), // please change this to be something else
            Session::CONFIG_AUTO_COMMIT   => true, // only do this if it requires it and/or it's hard to commit() your session.
                                                   // additionally you could do Flight::after('start', function() { Flight::session()->commit(); });
            Session::CONFIG_MYSQL_DS         => [
                'driver'    => 'mysql',             # Database driver for PDO dns eg(mysql:host=...;dbname=...)
                'host'      => '127.0.0.1',         # Database host
                'db_name'   => 'my_app_database',   # Database name
                'db_table'  => 'sessions',          # Database table
                'db_user'   => 'root',              # Database username
                'db_pass'   => '',                  # Database password
                'persistent_conn'=> false,          # Avoid the overhead of establishing a new connection every time a script needs to talk to a database, resulting in a faster web application. FIND THE BACKSIDE YOURSELF
            ]
        ]);
    }
);

Help! My Session Data is Not Persisting!

Are you setting your session data and it's not persisting between requests? You might have forgotten to commit your session data. You can do this by calling $session->commit() after you've set your session data.

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

    // do your login logic here
    // validate password, etc.

    // if the login is successful
    $session->set('is_logged_in', true);
    $session->set('user', $user);

    // any time you write to the session, you must commit it deliberately.
    $session->commit();
});

The other way around this is when you setup your session service, you have to set auto_commit to true in your configuration. This will automatically commit your session data after each request.


$app->register('session', Session::class, [ 'path/to/session_config.php', bin2hex(random_bytes(32)) ], function(Session $session) {
        $session->updateConfiguration([
            Session::CONFIG_AUTO_COMMIT   => true,
        ]);
    }
);

Additionally you could do Flight::after('start', function() { Flight::session()->commit(); }); to commit your session data after each request.

Documentation

Visit the Github Readme for full documentation. The configuration options are well documented in the default_config.php file itself. The code is simple to understand if you wanted to peruse this package yourself.

Awesome-plugins/runway

Runway

Runway is a CLI application that helps you manage your Flight applications. It can generate controllers, display all routes, and more. It is based on the excellent adhocore/php-cli library.

Click here to view the code.

Installation

Install with composer.

composer require flightphp/runway

Basic Configuration

The first time you run Runway, it will run you through a setup process and create a .runway.json configuration file in the root of your project. This file will contain some necessary configurations for Runway to work properly.

Usage

Runway has a number of commands that you can use to manage your Flight application. There are two easy ways to use Runway.

  1. If you are using the skeleton project, you can run php runway [command] from the root of your project.
  2. If you are using Runway as a package installed via composer, you can run vendor/bin/runway [command] from the root of your project.

For any command, you can pass in the --help flag to get more information on how to use the command.

php runway routes --help

Here are a few examples:

Generate a Controller

Based on the configuration in your .runway.json file, the default location will generate a controller for you in the app/controllers/ directory.

php runway make:controller MyController

Generate an Active Record Model

Based on the configuration in your .runway.json file, the default location will generate a controller for you in the app/records/ directory.

php runway make:record users

If for instance you have the users table with the following schema: id, name, email, created_at, updated_at, a file similar to the following will be created in the app/records/UserRecord.php file:

<?php

declare(strict_types=1);

namespace app\records;

/**
 * ActiveRecord class for the users table.
 * @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
 * // you could also add relationships here once you define them in the $relations array
 * @property CompanyRecord $company Example of a relationship
 */
class UserRecord extends \flight\ActiveRecord
{
    /**
     * @var array $relations Set the relationships for the model
     *   https://docs.flightphp.com/awesome-plugins/active-record#relationships
     */
    protected array $relations = [];

    /**
     * Constructor
     * @param mixed $databaseConnection The connection to the database
     */
    public function __construct($databaseConnection)
    {
        parent::__construct($databaseConnection, 'users');
    }
}

Display All Routes

This will display all of the routes that are currently registered with Flight.

php runway routes

If you would like to only view specific routes, you can pass in a flag to filter the routes.

# Display only GET routes
php runway routes --get

# Display only POST routes
php runway routes --post

# etc.

Customizing Runway

If you are either creating a package for Flight, or want to add your own custom commands into your project, you can do so by creating a src/commands/, flight/commands/, app/commands/, or commands/ directory for your project/package. If you need further customization, see the section below on Configuration.

To create a command, you simple extend the AbstractBaseCommand class, and implement at a minimum a __construct method and an execute method.

<?php

declare(strict_types=1);

namespace flight\commands;

class ExampleCommand extends AbstractBaseCommand
{
    /**
     * Construct
     *
     * @param array<string,mixed> $config JSON config from .runway-config.json
     */
    public function __construct(array $config)
    {
        parent::__construct('make:example', 'Create an example for the documentation', $config);
        $this->argument('<funny-gif>', 'The name of the funny gif');
    }

    /**
     * Executes the function
     *
     * @return void
     */
    public function execute(string $controller)
    {
        $io = $this->app()->io();

        $io->info('Creating example...');

        // Do something here

        $io->ok('Example created!');
    }
}

See the adhocore/php-cli Documentation for more information on how to build your own custom commands into your Flight application!

Configuration

If you need to customize the configuration for Runway, you can create a .runway-config.json file in the root of your project. Below are some additional configurations that you can set:

{

    // This is where your application directory is located
    "app_root": "app/",

    // This is the directory where your root index file is located
    "index_root": "public/",

    // These are the paths to the roots of other projects
    "root_paths": [
        "/home/user/different-project",
        "/var/www/another-project"
    ],

    // Base paths most likely don't need to be configured, but it's here if you want it
    "base_paths": {
        "/includes/libs/vendor", // if you have a really unique path for your vendor directory or something
    },

    // Final paths are locations within a project to search for the command files
    "final_paths": {
        "src/diff-path/commands",
        "app/module/admin/commands",
    },

    // If you want to just add the full path, go right ahead (absolute or relative to project root)
    "paths": [
        "/home/user/different-project/src/diff-path/commands",
        "/var/www/another-project/app/module/admin/commands",
        "app/my-unique-commands"
    ]
}

Awesome-plugins/tracy_extensions

Tracy Flight Panel Extensions

This is a set of extensions to make working with Flight a little richer.

This is the Panel

Flight Bar

And each panel displays very helpful information about your application!

Flight Data Flight Database Flight Request

Click here to view the code.

Installation

Run composer require flightphp/tracy-extensions --dev and you're on your way!

Configuration

There is very little configuration you need to do to get this started. You will need to initiate the Tracy debugger prior to using this https://tracy.nette.org/en/guide:

<?php

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

// bootstrap code
require __DIR__ . '/vendor/autoload.php';

Debugger::enable();
// You may need to specify your environment with Debugger::enable(Debugger::DEVELOPMENT)

// if you use database connections in your app, there is a 
// required PDO wrapper to use ONLY IN DEVELOPMENT (not production please!)
// It has the same parameters as a regular PDO connection
$pdo = new PdoQueryCapture('sqlite:test.db', 'user', 'pass');
// or if you attach this to the Flight framework
Flight::register('db', PdoQueryCapture::class, ['sqlite:test.db', 'user', 'pass']);
// now whenever you make a query it will capture the time, query, and parameters

// This connects the dots
if(Debugger::$showBar === true) {
    // This needs to be false or Tracy can't actually render :(
    Flight::set('flight.content_length', false);
    new TracyExtensionLoader(Flight::app());
}

// more code

Flight::start();

Additional Configuration

Session Data

If you have a custom session handler (such as ghostff/session), you can pass any array of session data to Tracy and it will automatically output it for you. You pass it in with the session_data key in the second parameter of the TracyExtensionLoader constructor.


use Ghostff\Session\Session;

require 'vendor/autoload.php';

$app = Flight::app();

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

if(Debugger::$showBar === true) {
    // This needs to be false or Tracy can't actually render :(
    Flight::set('flight.content_length', false);
    new TracyExtensionLoader(Flight::app(), [ 'session_data' => Flight::session()->getAll() ]);
}

// routes and other things...

Flight::start();

Latte

If you have Latte installed in your project, you can use the Latte panel to analyze your templates. You can pass in the Latte instance to the TracyExtensionLoader constructor with the latte key in the second parameter.



use Latte\Engine;

require 'vendor/autoload.php';

$app = Flight::app();

$app->register('latte', Engine::class, [], function($latte) {
    $latte->setTempDirectory(__DIR__ . '/temp');

    // this is where you add the Latte Panel to Tracy
    $latte->addExtension(new Latte\Bridges\Tracy\TracyExtension);
});

if(Debugger::$showBar === true) {
    // This needs to be false or Tracy can't actually render :(
    Flight::set('flight.content_length', false);
    new TracyExtensionLoader(Flight::app());
}

Awesome-plugins/tracy

Tracy

Tracy is an amazing error handler that can be used with Flight. It has a number of panels that can help you debug your application. It's also very easy to extend and add your own panels. The Flight Team has created a few panels specifically for Flight projects with the flightphp/tracy-extensions plugin.

Installation

Install with composer. And you will actually want to install this without the dev version as Tracy comes with a production error handling component.

composer require tracy/tracy

Basic Configuration

There are some basic configuration options to get started. You can read more about them in the Tracy Documentation.


require 'vendor/autoload.php';

use Tracy\Debugger;

// Enable Tracy
Debugger::enable();
// Debugger::enable(Debugger::DEVELOPMENT) // sometimes you have to be explicit (also Debugger::PRODUCTION)
// Debugger::enable('23.75.345.200'); // you can also provide an array of IP addresses

// This where errors and exceptions will be logged. Make sure this directory exists and is writable.
Debugger::$logDirectory = __DIR__ . '/../log/';
Debugger::$strictMode = true; // display all errors
// Debugger::$strictMode = E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED; // all errors except deprecated notices
if (Debugger::$showBar) {
    $app->set('flight.content_length', false); // if Debugger bar is visible, then content-length can not be set by Flight

    // This is specific to the Tracy Extension for Flight if you've included that
    // otherwise comment this out.
    new TracyExtensionLoader($app);
}

Helpful Tips

When you are debugging your code, there are some very helpful functions to output data for you.

Awesome-plugins/active_record

Flight Active Record

An active record is mapping a database entity to a PHP object. Spoken plainly, if you have a users table in your database, you can "translate" a row in that table to a User class and a $user object in your codebase. See basic example.

Click here for the repository in GitHub.

Basic Example

Let's assume you have the following table:

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

Now you can setup a new class to represent this table:

/**
 * An ActiveRecord class is usually singular
 * 
 * It's highly recommended to add the properties of the table as comments here
 * 
 * @property int    $id
 * @property string $name
 * @property string $password
 */ 
class User extends flight\ActiveRecord {
    public function __construct($database_connection)
    {
        // you can set it this way
        parent::__construct($database_connection, 'users');
        // or this way
        parent::__construct($database_connection, null, [ 'table' => 'users']);
    }
}

Now watch the magic happen!

// for sqlite
$database_connection = new PDO('sqlite:test.db'); // this is just for example, you'd probably use a real database connection

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

// or mysqli
$database_connection = new mysqli('localhost', 'username', 'password', 'test_db');
// or mysqli with non-object based creation
$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();
// or $user->save();

echo $user->id; // 1

$user->name = 'Joseph Mamma';
$user->password = password_hash('some cool password again!!!');
$user->insert();
// can't use $user->save() here or it will think it's an update!

echo $user->id; // 2

And it was just that easy to add a new user! Now that there is a user row in the database, how do you pull it out?

$user->find(1); // find id = 1 in the database and return it.
echo $user->name; // 'Bobby Tables'

And what if you want to find all the users?

$users = $user->findAll();

What about with a certain condition?

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

See how much fun this is? Let's install it and get started!

Installation

Simply install with Composer

composer require flightphp/active-record 

Usage

This can be used as a standalone library or with the Flight PHP Framework. Completely up to you.

Standalone

Just makes sure you pass a PDO connection to the constructor.

$pdo_connection = new PDO('sqlite:test.db'); // this is just for example, you'd probably use a real database connection

$User = new User($pdo_connection);

Don't want to always set your database connection in the constructor? See Database Connection Management for other ideas!

Register as a method in Flight

If you are using the Flight PHP Framework, you can register the ActiveRecord class as a service, but you honestly don't have to.

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

// then you can use it like this in a controller, a function, etc.

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

runway Methods

runway is a CLI tool for Flight that has a custom command for this library.

# Usage
php runway make:record database_table_name [class_name]

# Example
php runway make:record users

This will create a new class in the app/records/ directory as UserRecord.php with the following content:

<?php

declare(strict_types=1);

namespace app\records;

/**
 * ActiveRecord class for the users table.
 * @link https://docs.flightphp.com/awesome-plugins/active-record
 *
 * @property int $id
 * @property string $username
 * @property string $email
 * @property string $password_hash
 * @property string $created_dt
 */
class UserRecord extends \flight\ActiveRecord
{
    /**
     * @var array $relations Set the relationships for the model
     *   https://docs.flightphp.com/awesome-plugins/active-record#relationships
     */
    protected array $relations = [
        // 'relation_name' => [ self::HAS_MANY, 'RelatedClass', 'foreign_key' ],
    ];

    /**
     * Constructor
     * @param mixed $databaseConnection The connection to the database
     */
    public function __construct($databaseConnection)
    {
        parent::__construct($databaseConnection, 'users');
    }
}

CRUD functions

find($id = null) : boolean|ActiveRecord

Find one record and assign in to current object. If you pass an $id of some kind it will perform a lookup on the primary key with that value. If nothing is passed, it will just find the first record in table.

Additionally you can pass it other helper methods to query your table.

// find a record with some conditions before hand
$user->notNull('password')->orderBy('id DESC')->find();

// find a record by a specific id
$id = 123;
$user->find($id);

findAll(): array<int,ActiveRecord>

Finds all records in the table that you specify.

$user->findAll();

isHydrated(): boolean (v0.4.0)

Returns true if the current record has been hydrated (fetched from the database).

$user->find(1);
// if a record is found with data...
$user->isHydrated(); // true

insert(): boolean|ActiveRecord

Inserts the current record into database.

$user = new User($pdo_connection);
$user->name = 'demo';
$user->password = md5('demo');
$user->insert();
Text Based Primary Keys

If you have a text based primary key (such as a UUID), you can set the primary key value before inserting in one of two ways.

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

or you can have the primary key automatically generated for you through events.

class User extends flight\ActiveRecord {
    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users', [ 'primaryKey' => 'uuid' ]);
        // you can also set the primaryKey this way instead of the array above.
        $this->primaryKey = 'uuid';
    }

    protected function beforeInsert(self $self) {
        $self->uuid = uniqid(); // or however you need to generated your unique ids
    }
}

If you don't set the primary key before inserting, it will be set to the rowid and the database will generate it for you, but it won't persist because that field may not exist in your table. This is why it's recommended to use the event to automatically handle this for you.

update(): boolean|ActiveRecord

Updates the current record into the database.

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

save(): boolean|ActiveRecord

Inserts or updates the current record into the database. If the record has an id, it will update, otherwise it will insert.

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

Note: If you have relationships defined in the class, it will recursively save those relations as well if they have been defined, instantiated and have dirty data to update. (v0.4.0 and above)

delete(): boolean

Deletes the current record from the database.

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

You can also delete multiple records executing a search before hand.

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

dirty(array $dirty = []): ActiveRecord

Dirty data refers to the data that has been changed in a record.

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

// nothing is "dirty" as of this point.

$user->email = 'test@example.com'; // now email is considered "dirty" since it's changed.
$user->update();
// now there is no data that is dirty because it's been updated and persisted in the database

$user->password = password_hash()'newpassword'); // now this is dirty
$user->dirty(); // passing nothing will clear all the dirty entries.
$user->update(); // nothing will update cause nothing was captured as dirty.

$user->dirty([ 'name' => 'something', 'password' => password_hash('a different password') ]);
$user->update(); // both name and password are updated.

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

This is an alias for the dirty() method. It's a little more clear what you are doing.

$user->copyFrom([ 'name' => 'something', 'password' => password_hash('a different password') ]);
$user->update(); // both name and password are updated.

isDirty(): boolean (v0.4.0)

Returns true if the current record has been changed.

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

reset(bool $include_query_data = true): ActiveRecord

Resets the current record to it's initial state. This is really good to use in loop type behaviors. If you pass true it will also reset the query data that was used to find the current object (default behavior).

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

foreach($users as $user) {
    $user_company->reset(); // start with a clean slate
    $user_company->user_id = $user->id;
    $user_company->company_id = $some_company_id;
    $user_company->insert();
}

getBuiltSql(): string (v0.4.1)

After you run a find(), findAll(), insert(), update(), or save() method you can get the SQL that was built and use it for debugging purposes.

SQL Query Methods

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

You can select only a few of the columns in a table if you'd like (it is more performant on really wide tables with many columns)

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

from(string $table)

You can technically choose another table too! Why the heck not?!

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

join(string $table_name, string $join_condition)

You can even join to another table in the database.

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

where(string $where_conditions)

You can set some custom where arguments (you cannot set params in this where statement)

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

Security Note - You might be tempted to do something like $user->where("id = '{$id}' AND name = '{$name}'")->find();. Please DO NOT DO THIS!!! This is susceptible to what is knows as SQL Injection attacks. There are lots of articles online, please Google "sql injection attacks php" and you'll find a lot of articles on this subject. The proper way to handle this with this library is instead of this where() method, you would do something more like $user->eq('id', $id)->eq('name', $name)->find(); If you absolutely have to do this, the PDO library has $pdo->quote($var) to escape it for you. Only after you use quote() can you use it in a where() statement.

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

Group your results by a particular condition.

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

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

Sort the returned query a certain way.

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

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

Limit the amount of records returned. If a second int is given, it will be offset, limit just like in SQL.

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

WHERE conditions

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

Where field = $value

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

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

Where field <> $value

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

isNull(string $field)

Where field IS NULL

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

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

Where field IS NOT NULL

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

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

Where field > $value

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

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

Where field < $value

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

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

Where field >= $value

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

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

Where field <= $value

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

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

Where field LIKE $value or field NOT LIKE $value

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

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

Where field IN($value) or field NOT IN($value)

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

between(string $field, array $values)

Where field BETWEEN $value AND $value1

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

Relationships

You can set several kinds of relationships using this library. You can set one->many and one->one relationships between tables. This requires a little extra setup in the class beforehand.

Setting the $relations array is not hard, but guessing the correct syntax can be confusing.

protected array $relations = [
    // you can name the key anything you'd like. The name of the ActiveRecord is probably good. Ex: user, contact, client
    'user' => [
        // required
        // self::HAS_MANY, self::HAS_ONE, self::BELONGS_TO
        self::HAS_ONE, // this is the type of relationship

        // required
        'Some_Class', // this is the "other" ActiveRecord class this will reference

        // required
        // depending on the relationship type
        // self::HAS_ONE = the foreign key that references the join
        // self::HAS_MANY = the foreign key that references the join
        // self::BELONGS_TO = the local key that references the join
        'local_or_foreign_key',
        // just FYI, this also only joins to the primary key of the "other" model

        // optional
        [ 'eq' => [ 'client_id', 5 ], 'select' => 'COUNT(*) as count', 'limit' 5 ], // additional conditions you want when joining the relation
        // $record->eq('client_id', 5)->select('COUNT(*) as count')->limit(5))

        // optional
        'back_reference_name' // this is if you want to back reference this relationship back to itself Ex: $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');
    }
}

Now we have the references setup so we can use them very easily!

$user = new User($pdo_connection);

// find the most recent user.
$user->notNull('id')->orderBy('id desc')->find();

// get contacts by using relation:
foreach($user->contacts as $contact) {
    echo $contact->id;
}

// or we can go the other way.
$contact = new Contact();

// find one contact
$contact->find();

// get user by using relation:
echo $contact->user->name; // this is the user name

Pretty cool eh?

Setting Custom Data

Sometimes you may need to attach something unique to your ActiveRecord such as a custom calculation that might be easier to just attach to the object that would then be passed to say a template.

setCustomData(string $field, mixed $value)

You attach the custom data with the setCustomData() method.

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

And then you simply reference it like a normal object property.

echo $user->page_view_count;

Events

One more super awesome feature about this library is about events. Events are triggered at certain times based on certain methods you call. They are very very helpful in setting up data for you automatically.

onConstruct(ActiveRecord $ActiveRecord, array &config)

This is really helpful if you need to set a default connection or something like that.

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

//
//
//

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

    protected function onConstruct(self $self, array &$config) { // don't forget the & reference
        // you could do this to automatically set the connection
        $config['connection'] = Flight::db();
        // or this
        $self->transformAndPersistConnection(Flight::db());

        // You can also set the table name this way.
        $config['table'] = 'users';
    } 
}

beforeFind(ActiveRecord $ActiveRecord)

This is likely only useful if you need a query manipulation each time.

class User extends flight\ActiveRecord {

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

    protected function beforeFind(self $self) {
        // always run id >= 0 if that's your jam
        $self->gte('id', 0); 
    } 
}

afterFind(ActiveRecord $ActiveRecord)

This one is likely more useful if you always need to run some logic every time this record is fetched. Do you need to decrypt something? Do you need to run a custom count query each time (not performant but whatevs)?

class User extends flight\ActiveRecord {

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

    protected function afterFind(self $self) {
        // decrypting something
        $self->secret = yourDecryptFunction($self->secret, $some_key);

        // maybe storing something custom like a query???
        $self->setCustomData('view_count', $self->select('COUNT(*) count')->from('user_views')->eq('user_id', $self->id)['count']; 
    } 
}

beforeFindAll(ActiveRecord $ActiveRecord)

This is likely only useful if you need a query manipulation each time.

class User extends flight\ActiveRecord {

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

    protected function beforeFindAll(self $self) {
        // always run id >= 0 if that's your jam
        $self->gte('id', 0); 
    } 
}

afterFindAll(array<int,ActiveRecord> $results)

Similar to afterFind() but you get to do it to all the records instead!

class User extends flight\ActiveRecord {

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

    protected function afterFindAll(array $results) {

        foreach($results as $self) {
            // do something cool like afterFind()
        }
    } 
}

beforeInsert(ActiveRecord $ActiveRecord)

Really helpful if you need some default values set each time.

class User extends flight\ActiveRecord {

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

    protected function beforeInsert(self $self) {
        // set some sound defaults
        if(!$self->created_date) {
            $self->created_date = gmdate('Y-m-d');
        }

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

afterInsert(ActiveRecord $ActiveRecord)

Maybe you have a user case for changing data after it's inserted?

class User extends flight\ActiveRecord {

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

    protected function afterInsert(self $self) {
        // you do you
        Flight::cache()->set('most_recent_insert_id', $self->id);
        // or whatever....
    } 
}

beforeUpdate(ActiveRecord $ActiveRecord)

Really helpful if you need some default values set each time on an update.

class User extends flight\ActiveRecord {

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

    protected function beforeInsert(self $self) {
        // set some sound defaults
        if(!$self->updated_date) {
            $self->updated_date = gmdate('Y-m-d');
        }
    } 
}

afterUpdate(ActiveRecord $ActiveRecord)

Maybe you have a user case for changing data after it's updated?

class User extends flight\ActiveRecord {

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

    protected function afterInsert(self $self) {
        // you do you
        Flight::cache()->set('most_recently_updated_user_id', $self->id);
        // or whatever....
    } 
}

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

This is useful if you want events to happen both when inserts or updates happen. I'll spare you the long explanation, but I'm sure you can guess what it is.

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)

Not sure what you'd want to do here, but no judgments here! Go for it!

class User extends flight\ActiveRecord {

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

    protected function beforeDelete(self $self) {
        echo 'He was a brave soldier... :cry-face:';
    } 
}

Database Connection Management

When you are using this library, you can set the database connection in a few different ways. You can set the connection in the constructor, you can set it via a config variable $config['connection'] or you can set it via setDatabaseConnection() (v0.4.1).

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

If you want to avoid always setting a $database_connection every time you call an active record, there are ways around that!

// index.php or bootstrap.php
// Set this as a registered class in Flight
Flight::register('db', 'PDO', [ 'sqlite:test.db' ]);

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

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

// And now, no args required!
$user = new User();

Note: If you are planning on unit testing, doing it this way can add some challenges to unit testing, but overall because you can inject your connection with setDatabaseConnection() or $config['connection'] it's not too bad.

If you need to refresh the database connection, for instance if you are running a long running CLI script and need to refresh the connection every so often, you can re-set the connection with $your_record->setDatabaseConnection($pdo_connection).

Contributing

Please do. :D

Setup

When you contribute, make sure you run composer test-coverage to maintain 100% test coverage (this isn't true unit test coverage, more like integration testing).

Also make sure you run composer beautify and composer phpcs to fix any linting errors.

License

MIT

Awesome-plugins/latte

Latte

Latte is a full featured templating engine that is very easy to use and feels closer to a PHP syntax than Twig or Smarty. It's also very easy to extend and add your own filters and functions.

Installation

Install with composer.

composer require latte/latte

Basic Configuration

There are some basic configuration options to get started. You can read more about them in the Latte Documentation.


use Latte\Engine as LatteEngine;

require 'vendor/autoload.php';

$app = Flight::app();

$app->register('latte', LatteEngine::class, [], function(LatteEngine $latte) use ($app) {

    // This is where Latte will cache your templates to speed things up
    // One neat thing about Latte is that it automatically refreshes your
    // cache when you make changes to your templates!
    $latte->setTempDirectory(__DIR__ . '/../cache/');

    // Tell Latte where the root directory for your views will be at.
    // $app->get('flight.views.path') is set in the config.php file
    //   You could also just do something like `__DIR__ . '/../views/'`
    $latte->setLoader(new \Latte\Loaders\FileLoader($app->get('flight.views.path')));
});

Simple Layout Example

Here's a simple example of a layout file. This is the file that will be used to wrap all of your other views.

<!-- app/views/layout.latte -->
<!doctype html>
<html lang="en">
    <head>
        <title>{$title ? $title . ' - '}My App</title>
        <link rel="stylesheet" href="style.css">
    </head>
    <body>
        <header>
            <nav>
                <!-- your nav elements here -->
            </nav>
        </header>
        <div id="content">
            <!-- This is the magic right here -->
            {block content}{/block}
        </div>
        <div id="footer">
            &copy; Copyright
        </div>
    </body>
</html>

And now we have your file that's going to render inside that content block:

<!-- app/views/home.latte -->
<!-- This tells Latte that this file is "inside" the layout.latte file -->
{extends layout.latte}

<!-- This is the content that will be rendered inside the layout inside the content block -->
{block content}
    <h1>Home Page</h1>
    <p>Welcome to my app!</p>
{/block}

Then when you go to render this inside your function or controller, you would do something like this:

// simple route
Flight::route('/', function () {
    Flight::latte()->render('home.latte', [
        'title' => 'Home Page'
    ]);
});

// or if you're using a controller
Flight::route('/', [HomeController::class, 'index']);

// HomeController.php
class HomeController
{
    public function index()
    {
        Flight::latte()->render('home.latte', [
            'title' => 'Home Page'
        ]);
    }
}

See the Latte Documentation for more information on how to use Latte to it's fullest potential!

Awesome-plugins/awesome_plugins

Awesome Plugins

Flight is incredibly extensible. There are a number of plugins that can be used to add functionality to your Flight application. Some are officially supported by the Flight Team and others are micro/lite libraries to help you get started.

Authentication/Authorization

Authentication and Authorization are crucial for any application that requires controls to be in place for who can access what.

Caching

Caching is a great way to speed up your application. There are a number of caching libraries that can be used with Flight.

CLI

CLI applications are a great way to interact with your application. You can use them to generate controllers, display all routes, and more.

Cookies

Cookies are a great way to store small bits of data on the client side. They can be used to store user preferences, application settings, and more.

Debugging

Debugging is crucial when you are developing in your local environment. There are a few plugins that can elevate your debugging experience.

Databases

Databases are the core to most applications. This is how you store and retrieve data. Some database libraries are simply wrappers to write queries and some are full fledged ORMs.

Encryption

Encryption is crucial for any application that stores sensitive data. Encrypting and decrypting the data isn't terribly hard, but properly storing the encryption key can be difficult. The most important thing is to never store your encryption key in a public directory or to commit it to your code repository.

Session

Sessions aren't really useful for API's but for building out a web application, sessions can be crucial for maintaining state and login information.

Templating

Templating is core to any web application with a UI. There are a number of templating engines that can be used with Flight.

Contributing

Got a plugin you'd like to share? Submit a pull request to add it to the list!

Media

Media

We've tried to track down what we can of the various types of media around the internet around Flight. See below for different resources that you can use to learn more about Flight.

Articles

Videos

Examples

Need a quick start?

You have two options to get started with Flight:

Need Some Inspiration?

While these are not officially sponsored by the Flight Team, these could give you ideas on how to structure your own projects that are built with Flight!

Want to Share Your Own Example?

If you have a project you want to share, please submit a pull request to add it to this list!

Install/install

Installation

Download the files

Make sure you have PHP installed on your system. If not, click here for instructions on how to install it for your system.

If you're using Composer, you can run the following command:

composer require flightphp/core

OR you can download the files directly and extract them to your web directory.

Configure your Web Server

Built-in PHP Development Server

This is by far the simplest way to get up and running. You can use the built-in server to run your application and even use SQLite for a database (as long as sqlite3 is installed on your system) and not require much of anything! Just run the following command once PHP is installed:

php -S localhost:8000

Then open your browser and go to http://localhost:8000.

If you want to make the document root of your project a different directory (Ex: your project is ~/myproject, but your document root is ~/myproject/public/), you can run the following command once your in the ~/myproject directory:

php -S localhost:8000 -t public/

Then open your browser and go to http://localhost:8000.

Apache

Make sure Apache is already installed on your system. If not, google how to install Apache on your system.

For Apache, edit your .htaccess file with the following:

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L]

Note: If you need to use flight in a subdirectory add the line RewriteBase /subdir/ just after RewriteEngine On.

Note: If you want to protect all server files, like a db or env file. Put this in your .htaccess file:

RewriteEngine On
RewriteRule ^(.*)$ index.php

Nginx

Make sure Nginx is already installed on your system. If not, google how to Nginx Apache on your system.

For Nginx, add the following to your server declaration:

server {
  location / {
    try_files $uri $uri/ /index.php;
  }
}

Create your index.php file

<?php

// If you're using Composer, require the autoloader.
require 'vendor/autoload.php';
// if you're not using Composer, load the framework directly
// require 'flight/Flight.php';

// Then define a route and assign a function to handle the request.
Flight::route('/', function () {
  echo 'hello world!';
});

// Finally, start the framework.
Flight::start();

Installing PHP

If you already have php installed on your system, go ahead and skip these instructions and move to the download section

macOS

Installing PHP using Homebrew

  1. Install Homebrew (if not already installed):

    • Open Terminal and run:
      /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
  2. Install PHP:

    • Install the latest version:
      brew install php
    • To install a specific version, for example, PHP 8.1:
      brew tap shivammathur/php
      brew install shivammathur/php/php@8.1
  3. Switch between PHP versions:

    • Unlink the current version and link the desired version:
      brew unlink php
      brew link --overwrite --force php@8.1
    • Verify the installed version:
      php -v

Windows 10/11

Installing PHP manually

  1. Download PHP:

    • Visit PHP for Windows and download the latest or a specific version (e.g., 7.4, 8.0) as a non-thread-safe zip file.
  2. Extract PHP:

    • Extract the downloaded zip file to C:\php.
  3. Add PHP to the system PATH:

    • Go to System Properties > Environment Variables.
    • Under System variables, find Path and click Edit.
    • Add the path C:\php (or wherever you extracted PHP).
    • Click OK to close all windows.
  4. Configure PHP:

    • Copy php.ini-development to php.ini.
    • Edit php.ini to configure PHP as needed (e.g., setting extension_dir, enabling extensions).
  5. Verify PHP installation:

    • Open Command Prompt and run:
      php -v

Installing Multiple Versions of PHP

  1. Repeat the above steps for each version, placing each in a separate directory (e.g., C:\php7, C:\php8).

  2. Switch between versions by adjusting the system PATH variable to point to the desired version directory.

Ubuntu (20.04, 22.04, etc.)

Installing PHP using apt

  1. Update package lists:

    • Open Terminal and run:
      sudo apt update
  2. Install PHP:

    • Install the latest PHP version:
      sudo apt install php
    • To install a specific version, for example, PHP 8.1:
      sudo apt install php8.1
  3. Install additional modules (optional):

    • For example, to install MySQL support:
      sudo apt install php8.1-mysql
  4. Switch between PHP versions:

    • Use update-alternatives:
      sudo update-alternatives --set php /usr/bin/php8.1
  5. Verify the installed version:

    • Run:
      php -v

Rocky Linux

Installing PHP using yum/dnf

  1. Enable the EPEL repository:

    • Open Terminal and run:
      sudo dnf install epel-release
  2. Install Remi's repository:

    • Run:
      sudo dnf install https://rpms.remirepo.net/enterprise/remi-release-8.rpm
      sudo dnf module reset php
  3. Install PHP:

    • To install the default version:
      sudo dnf install php
    • To install a specific version, for example, PHP 7.4:
      sudo dnf module install php:remi-7.4
  4. Switch between PHP versions:

    • Use the dnf module command:
      sudo dnf module reset php
      sudo dnf module enable php:remi-8.0
      sudo dnf install php
  5. Verify the installed version:

    • Run:
      php -v

General Notes