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.
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.
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.
Learn how to autoload your own classes in your application.
Learn how to manage routes for your web application. This also includes grouping routes, route parameters and middleware.
Learn how to use middleware to filter requests and responses in your application.
Learn how to handle requests and responses in your application.
Learn how to send responses to your users.
Learn how to use the built-in view engine to render your HTML templates.
Learn how to secure your application from common security threats.
Learn how to configure the framework for your application.
Learn how to extend the framework to with adding your own methods and classes.
Learn how to use the event system to add hooks to your methods and internal framework methods.
Learn how to use dependency injection containers (DIC) to manage your application's dependencies.
Learn about the core methods of the framework.
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.
There are some common issues that you may run into when using Flight. This page will help you troubleshoot those issues.
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.
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.
// 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>'; });
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.
flight.v2.output_buffering
true
// 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
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.
Dispatcher
Dispatcher::invokeMethod()
Dispatcher::execute()
$result = $class->$method(...$params);
call_user_func_array()
halt()
stop()
redirect()
error()
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().
Flight::response()->clear()
You can customize certain behaviors of Flight by setting configuration values through the set method.
set
Flight::set('flight.log_errors', true);
The following is a list of all the available configuration settings:
?string
bool
string
Content-Length
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;
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.
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.
error
HTTP 500 Internal Server Error
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:
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.
notFound
HTTP 404 Not Found
Flight::map('notFound', function () { // Handle not found });
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.
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.
You can manually add these headers by using the header method on the Flight\Response object.
header
Flight\Response
// 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.
bootstrap.php
index.php
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=()'); });
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) 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.
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>
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.
csrf()
<form method="post"> {csrf()} <!-- other form fields --> </form>
Short and simple right?
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) 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: <script>alert("XSS")</script> // 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 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.
PDO
// 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.
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.
Flight::start()
// app/utils/CorsUtil.php namespace app\utils; class CorsUtil { public function set(array $params): void { $request = Flight::request(); $response = Flight::response(); if ($request->getVar('HTTP_ORIGIN') !== '') { $this->allowOrigins(); $response->header('Access-Control-Allow-Credentials', 'true'); $response->header('Access-Control-Max-Age', '86400'); } if ($request->method === 'OPTIONS') { if ($request->getVar('HTTP_ACCESS_CONTROL_REQUEST_METHOD') !== '') { $response->header( 'Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD' ); } if ($request->getVar('HTTP_ACCESS_CONTROL_REQUEST_HEADERS') !== '') { $response->header( "Access-Control-Allow-Headers", $request->getVar('HTTP_ACCESS_CONTROL_REQUEST_HEADERS') ); } $response->status(200); $response->send(); exit; } } private function allowOrigins(): void { // 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' ]);
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.
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.
The callback can be any object that is callable. So you can use a regular function:
function hello() { echo 'hello world!'; } Flight::route('/', 'hello');
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');
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();
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();
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.
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. :'(
()
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.
NULL
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 });
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 });
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.
/admin/users/@id
/admin/users/5
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'
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);
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:
/api/v1
group
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 }); }); });
You can still use route grouping with the Engine object in the following way:
Engine
$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 }); });
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.
streamWithHeaders()
Note: Streaming responses is only available if you have flight.v2.output_buffering set to false.
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.
stream()
header()
Flight::response()->setRealHeader()
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();
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 ]);
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.
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 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 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 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.
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.
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.
UserController
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!
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!
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.
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'); } }
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');
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.
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:
Flight::redirect()
function($params) { ... }
public function before($params) {}
flight\Engine
__construct()
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!"
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:
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 } }
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; } } }
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.']); } } }
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() ]);
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:
false
Flight::before('start', function (array &$params, string &$output): bool { echo 'one'; return true; }); Flight::before('start', function (array &$params, string &$output): bool { echo 'two'; // 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.
map
register
Flight encapsulates the HTTP request into a single object, which can be accessed by doing:
$request = Flight::request();
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.
$_GET
$_POST
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 });
The request object provides the following properties:
$_SERVER
HTTP_CLIENT_IP
HTTP_X_FORWARDED_FOR
HTTP_X_FORWARDED
HTTP_X_CLUSTER_CLIENT_IP
HTTP_FORWARDED_FOR
HTTP_FORWARDED
You can access the query, data, cookies, and files properties as arrays or objects.
query
data
cookies
files
So, to get a query string parameter, you can do:
$id = Flight::request()->query['id'];
Or you can do:
$id = Flight::request()->query->id;
To get the raw HTTP request body, for example when dealing with PUT requests, you can do:
$body = Flight::request()->getBody();
If you send a request with the type application/json and the data {"id": 123} it will be available from the data property:
application/json
{"id": 123}
$id = Flight::request()->data->id;
You can access the $_GET array via the query property:
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'];
There is a shortcut available to access the $_SERVER array via the getVar() method:
getVar()
$host = Flight::request()->getVar['HTTP_HOST'];
$_FILES
You can access uploaded files via the files property:
$uploadedFile = Flight::request()->files['myFile'];
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.
You can access request headers using the getHeader() or getHeaders() method:
getHeader()
getHeaders()
// 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();
You can access the raw request body using the getBody() method:
getBody()
You can access the request method using the method property or the getMethod() method:
method
getMethod()
$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.
$_SERVER['REQUEST_METHOD']
$_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']
$_REQUEST['_method']
There are a couple helper methods to piece together parts of a URL for your convenience.
You can access the full request URL using the getFullUrl() method:
getFullUrl()
$url = Flight::request()->getFullUrl(); // https://example.com/some/path?foo=bar
You can access the base URL using the getBaseUrl() method:
getBaseUrl()
$url = Flight::request()->getBaseUrl(); // Notice, no trailing slash. // https://example.com
You can pass a URL to the parseQuery() method to parse the query string into an associative array:
parseQuery()
$query = Flight::request()->parseQuery('https://example.com/some/path?foo=bar'); // ['foo' => 'bar']
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.
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
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.
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.
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.
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.
/user/1234
/users
It might work something like this:
http://example.com/user/1234
Flight::route('/user/@id', [ 'UserController', 'viewUserProfile' ]);
viewUserProfile($id)
1234
$id
viewUserProfile()
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:
user_view
id
/admin/user/1234
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.
if
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!
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.
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.
Flight::request()
Request
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.
Flight::response()
Response
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.
Flight
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.
echo
print
// 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.
write()
// 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(); });
You can set the status code of the response by using the status method:
status
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
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.
write
Flight::route('/', function() { Flight::response()->write("Hello, World!"); }); // same as Flight::route('/', function() { echo "Hello, World!"; });
If you want to clear the response body, you can use the clearBody method:
clearBody
Flight::route('/', function() { if($someCondition) { Flight::response()->write("Hello, World!"); } else { Flight::response()->clearBody(); } });
You can run a callback on the response body by using the addResponseBodyCallback method:
addResponseBodyCallback
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.
[ $class, 'method' ]
$strReplace = function($body) { str_replace('hi', 'there', $body); };
'minify'
Note: Route callbacks will not work if you are using the flight.v2.output_buffering configuration option.
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); }); });
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() ]);
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!"; });
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]);
You can also pass in a status code as the second argument:
Flight::json(['id' => 123], 201);
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::json()
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);
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.
jsonHalt
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 });
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:
?q=my_func
my_func({"id":123});
If you don't pass in a query parameter name it will default to jsonp.
jsonp
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);
You can stop the framework at any point by calling the halt method:
halt
Flight::halt();
You can also specify an optional HTTP status code and message:
HTTP
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:
stop
Flight::stop();
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.
clear()
200
Flight::response()->clear();
If you only want to clear the response body, you can use the clearBody() method:
clearBody()
// This will still keep any headers set on the response() object. Flight::response()->clearBody();
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.
304 Not Modified
If you want to cache your whole response, you can use the cache() method and pass in time to cache.
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.'; });
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.
lastModified
Flight::route('/news', function () { Flight::lastModified(1234567890); echo 'This content will be cached.'; });
ETag caching is similar to Last-Modified, except you can specify any id you want for the resource:
ETag
Last-Modified
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.
etag
HTTP 304
There is a helper method to download a file. You can use the download method and pass in the path.
download
Flight::route('/download', function () { Flight::download('/path/to/file.txt'); });
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.
To display a view template call the render method with the name of the template file and optional template data:
render
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.php
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:
name
Flight::render('hello');
Note that when specifying the name of the template in the render method, you can leave out the .php extension.
.php
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:
views
Flight::set('flight.views.path', '/path/to/views');
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:
headerContent
bodyContent
Flight::render('layout', ['title' => 'Home Page']);
If the template files looks like this:
header.php:
header.php
<h1><?= $heading ?></h1>
body.php:
body.php
<div><?= $body ?></div>
layout.php:
layout.php
<html> <head> <title><?= $title ?></title> </head> <body> <?= $headerContent ?> <?= $bodyContent ?> </body> </html>
<html> <head> <title>Home Page</title> </head> <body> <h1>Hello</h1> <div>World</div> </body> </html>
Flight allows you to swap out the default view engine simply by registering your own view class.
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); });
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); });
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.
GET
DB\SQL
active-record
Cache
beforeroute
afterroute
HIVE
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.
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.
register()
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.
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:
HTTP 404
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.
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.
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.
require
include
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.
Flight::path()
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 } }
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/
/** * 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 } }
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.
Loader::setV2ClassLoading(false);
/** * 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 } }
This page will help you troubleshoot common issues that you may run into when using Flight.
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.
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.
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.
MyClass
MyClass.php
myclass.php
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()
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.
config.php
// Add a path to the autoloader Flight::path(__DIR__.'/../');
Copyright © 2024 @mikecao, @n0nag0n
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.
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.
<?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!
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.
We're on Matrix Chat with us at #flight-php-framework:matrix.org.
There are two ways you can contribute to Flight:
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.
Flight is released under the MIT license.
overclokk/cookie is a simple library for managing cookies within your app.
Installation is simple with composer.
composer require overclokk/cookie
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'); } } }
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.
composer require defuse/php-encryption
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.
app/config/config.php
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; });
Light, simple and standalone PHP in-file caching class
Advantages
Click here to view the code.
Install via composer:
composer require wruczek/php-file-cache
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 }
Visit https://github.com/Wruczek/PHP-File-Cache for full documentation and make sure you see the examples folder.
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.
Run composer require flightphp/permissions and you're on your way!
composer require flightphp/permissions
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.
$Permissions->has()
->can()
is()
has()
can()
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 } } }
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.
$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 } }
namespace MyApp; class Permissions { public function order(string $current_role, MyDependency $MyDependency = null) { // ... code } }
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:
order()
company()
$Permissions->has('order.read')
$Permissions->has('company.read')
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!'); } } }
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!
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.
// 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 ] ]);
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; }
IN()
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 ]);
// 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(); });
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.
Install with composer.
composer require ghostff/session
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.
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'); } });
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 ] ]); } );
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.
$session->commit()
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.
auto_commit
$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.
Flight::after('start', function() { Flight::session()->commit(); });
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.
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.
composer require flightphp/runway
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.
.runway.json
Runway has a number of commands that you can use to manage your Flight application. There are two easy ways to use Runway.
php runway [command]
vendor/bin/runway [command]
For any command, you can pass in the --help flag to get more information on how to use the command.
--help
php runway routes --help
Here are a few examples:
Based on the configuration in your .runway.json file, the default location will generate a controller for you in the app/controllers/ directory.
app/controllers/
php runway make:controller MyController
Based on the configuration in your .runway.json file, the default location will generate a controller for you in the app/records/ directory.
app/records/
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:
users
email
created_at
updated_at
app/records/UserRecord.php
<?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'); } }
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.
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.
src/commands/
flight/commands/
app/commands/
commands/
To create a command, you simple extend the AbstractBaseCommand class, and implement at a minimum a __construct method and an execute method.
AbstractBaseCommand
__construct
execute
<?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!
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:
.runway-config.json
{ // 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" ] }
This is a set of extensions to make working with Flight a little richer.
$_SESSION
This is the Panel
And each panel displays very helpful information about your application!
Run composer require flightphp/tracy-extensions --dev and you're on your way!
composer require flightphp/tracy-extensions --dev
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();
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.
session_data
TracyExtensionLoader
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();
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.
latte
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()); }
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.
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
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); }
When you are debugging your code, there are some very helpful functions to output data for you.
bdump($var)
dumpe($var)
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.
User
$user
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!
Simply install with Composer
composer require flightphp/active-record
This can be used as a standalone library or with the Flight PHP Framework. Completely up to you.
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!
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
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:
UserRecord.php
<?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'); } }
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
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();
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.
rowid
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
This is an alias for the dirty() method. It's a little more clear what you are doing.
dirty()
$user->copyFrom([ 'name' => 'something', 'password' => password_hash('a different password') ]); $user->update(); // both name and password are updated.
isDirty(): boolean
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
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.
find()
findAll()
insert()
update()
save()
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.
$user->where("id = '{$id}' AND name = '{$name}'")->find();
where()
$user->eq('id', $id)->eq('name', $name)->find();
$pdo->quote($var)
quote()
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();
equal(string $field, mixed $value) / eq(string $field, mixed $value)
Where field = $value
field = $value
$user->eq('id', 1)->find();
notEqual(string $field, mixed $value) / ne(string $field, mixed $value)
Where field <> $value
field <> $value
$user->ne('id', 1)->find();
isNull(string $field)
Where field IS NULL
field IS NULL
$user->isNull('id')->find();
isNotNull(string $field) / notNull(string $field)
Where field IS NOT NULL
field IS NOT NULL
$user->isNotNull('id')->find();
greaterThan(string $field, mixed $value) / gt(string $field, mixed $value)
Where field > $value
field > $value
$user->gt('id', 1)->find();
lessThan(string $field, mixed $value) / lt(string $field, mixed $value)
Where field < $value
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
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
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
field LIKE $value
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)
field IN($value)
field NOT IN($value)
$user->in('id', [1, 2])->find();
between(string $field, array $values)
Where field BETWEEN $value AND $value1
field BETWEEN $value AND $value1
$user->between('id', [1, 2])->find();
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.
$relations
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?
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.
setCustomData()
$user->setCustomData('page_view_count', $page_view_count);
And then you simply reference it like a normal object property.
echo $user->page_view_count;
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)
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!
afterFind()
class User extends flight\ActiveRecord { public function __construct($database_connection) { parent::__construct($database_connection, 'users'); } protected function afterFindAll(array $results) { foreach($results as $self) { // 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:'; } }
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).
$config['connection']
setDatabaseConnection()
$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!
$database_connection
// 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).
$your_record->setDatabaseConnection($pdo_connection)
Please do. :D
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).
composer test-coverage
Also make sure you run composer beautify and composer phpcs to fix any linting errors.
composer beautify
composer phpcs
MIT
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.
composer require latte/latte
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'))); });
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"> © 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!
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 and Authorization are crucial for any application that requires controls to be in place for who can access what.
Caching is a great way to speed up your application. There are a number of caching libraries that can be used with Flight.
CLI applications are a great way to interact with your application. You can use them to generate controllers, display all routes, and more.
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 is crucial when you are developing in your local environment. There are a few plugins that can elevate your debugging experience.
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 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.
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 is core to any web application with a UI. There are a number of templating engines that can be used with Flight.
Got a plugin you'd like to share? Submit a pull request to add it to the list!
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.
You have two options to get started with Flight:
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!
If you have a project you want to share, please submit a pull request to add it to this list!
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.
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.
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:
~/myproject
~/myproject/public/
php -S localhost:8000 -t public/
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:
.htaccess
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:
Note: If you need to use flight in a subdirectory add the line RewriteBase /subdir/ just after RewriteEngine On.
RewriteBase /subdir/
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
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; } }
<?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();
If you already have php installed on your system, go ahead and skip these instructions and move to the download section
php
Install Homebrew (if not already installed):
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
Install PHP:
brew install php
brew tap shivammathur/php brew install shivammathur/php/php@8.1
Switch between PHP versions:
brew unlink php brew link --overwrite --force php@8.1
php -v
Download PHP:
Extract PHP:
C:\php
Add PHP to the system PATH:
Configure PHP:
php.ini-development
php.ini
extension_dir
Verify PHP installation:
Repeat the above steps for each version, placing each in a separate directory (e.g., C:\php7, C:\php8).
C:\php7
C:\php8
Switch between versions by adjusting the system PATH variable to point to the desired version directory.
Update package lists:
sudo apt update
sudo apt install php
sudo apt install php8.1
Install additional modules (optional):
sudo apt install php8.1-mysql
update-alternatives
sudo update-alternatives --set php /usr/bin/php8.1
Verify the installed version:
Enable the EPEL repository:
sudo dnf install epel-release
Install Remi's repository:
sudo dnf install https://rpms.remirepo.net/enterprise/remi-release-8.rpm sudo dnf module reset php
sudo dnf install php
sudo dnf module install php:remi-7.4
dnf
sudo dnf module reset php sudo dnf module enable php:remi-8.0 sudo dnf install php