Facil Framework
A dev-friendly, insanely fast file-based routing PHP framework designed for rapid API and Fullstack development.
Forget heavy boilerplates. Drop a file in the routes/ folder, and your endpoint is ready. Built for developers who want to ship fast without sacrificing architecture, featuring built-in Auth, Security, Database, elegant MicroORM, Migrations, and Template rendering.
Installation & Setup
The easiest way to start a new Facil project is via Composer. This command will clone the skeleton and set up everything you need.
composer create-project rodrigocborges/facilphp my-app
cd my-app
Running the Server
Facil comes with built-in commands to easily spin up the PHP development server.
# For local development (localhost:8000)
composer run start-dev
# For production/network testing (0.0.0.0:80)
composer run start-prod
Environment (.env)
Manage your application secrets easily. Facil loads your .env file automatically at boot.
# .env
APP_NAME="My Micro SaaS"
DB_DSN="sqlite:../database/app.sqlite"
CORS_ORIGIN="*"
Retrieve variables anywhere in your code using the Env utility:
use Facil\Support\Env;
$dbPath = Env::get('DB_DSN', 'mysql:host=localhost;dbname=test');
$appName = Env::get('APP_NAME');
File-Based Routing
Facil uses a Next.js inspired file-based routing system. The file path automatically dictates the URL endpoint.
Static Routes
Create a file at routes/api/users.php to map to /api/users.
<?php
use Facil\Http\Response;
return [
'name' => 'api.users.index',
'GET' => function() {
return Response::json(['message' => 'All users fetched!']);
}
];
Dynamic Parameters
Use brackets to define dynamic parameters. Create a file at routes/users/[id].php.
<?php
use Facil\Http\Response;
return [
'name' => 'api.users.show',
'GET' => function(string $id) {
return Response::json(['user_id' => $id]);
}
];
Generating URLs (Named Routes)
Since your routes can have a name attribute, you can dynamically generate full URLs to them using the Router::url method. This prevents hardcoding URLs throughout your application.
use Facil\Routing\Router;
// Assuming a route named 'api.users.show' mapping to routes/users/[id].php
$url = Router::url('api.users.show', ['id' => 42]);
// Returns: http://localhost:8000/users/42
Request & Response
Handling incoming data and sending responses is fully static and incredibly clean.
use Facil\Http\Request;
use Facil\Http\Response;
use Facil\Http\HttpStatusCode;
return [
'POST' => function() {
$data = Request::body(); // Parses JSON or Form-Data
$id = Request::param('id'); // From URL /users/[id]
$tab = Request::query('tab', 'profile'); // From ?tab=profile
return Response::json(
['status' => 'Created successfully', 'data' => $data],
HttpStatusCode::CREATED
);
}
];
File Uploads
Handling $_FILES securely can be tedious. Facil provides a fluent DX for retrieving and storing uploaded files, automatically generating secure, unique filenames and preventing directory traversal.
Retrieving & Saving Files
Use Request::file() to grab the file payload and Upload::save() to store it. By default, it validates the extension/size and saves to your project's public/uploads directory.
use Facil\Http\Request;
use Facil\Http\Response;
use Facil\Filesystem\Upload;
return [
'POST' => function() {
// 1. Grab the file from the request
$file = Request::file('avatar');
// 2. Save it securely (Returns the new unique filename, or false on error)
$filename = Upload::save($file);
if (!$filename) {
return Response::json(['error' => 'Upload failed or invalid file.'], 400);
}
return Response::json([
'message' => 'File uploaded successfully!',
'url' => '/uploads/' . $filename
]);
}
];
Customizing Destination & Deleting
You can easily override the default destination, allowed extensions, and maximum file size (default is 2MB). You can also delete files just as easily.
use Facil\Filesystem\Upload;
// Custom destination, only PDF/DOCX, max 5MB
$customPath = dirname(__DIR__, 2) . '/private/documents';
$filename = Upload::save($file, $customPath, ['pdf', 'docx'], Upload::mb(5)); //or 5mb = 5242880 bytes
// Delete an existing file
Upload::delete($customPath . '/' . $filename);
Validation Rules
Validate payloads instantly using the pipe | syntax. Facil supports powerful rules like min, max, in, match, strong password checks, and native Brazilian standards (cpf and cnpj).
use Facil\Support\Validate;
use Facil\Http\Request;
use Facil\Http\Response;
use Facil\Http\HttpStatusCode;
return [
'POST' => function() {
$errors = Validate::check(Request::body(), [
'username' => 'required|min:4|max:20',
'email' => 'required|email',
'role' => 'required|in:admin,editor,viewer',
'password' => 'required|min:8|password',
'password_confirmation' => 'required|match:password',
'cpf' => 'cpf', // Optional field, validated only if present
'website' => 'url'
]);
if (!empty($errors)) {
return Response::json(['errors' => $errors], HttpStatusCode::UNPROCESSABLE_ENTITY);
}
return Response::json(['message' => 'Valid payload!']);
}
];
Database (Raw PDO)
Facil includes a simple wrapper around PDO heavily inspired by better-sqlite3. All queries automatically use prepared statements preventing SQL Injection.
use Facil\Database\Database;
// Run (Insert/Update/Delete - Returns affected rows)
Database::run("INSERT INTO users (name, email) VALUES (?, ?)", ['John', 'john@example.com']);
// Get (Fetch a single row)
$user = Database::get("SELECT * FROM users WHERE id = :id", ['id' => 5]);
// All (Fetch multiple rows)
$activeUsers = Database::all("SELECT * FROM users WHERE status = ?", ['active']);
// Get Last Insert ID
$newId = Database::id();
MicroORM (Query Builder)
Writing raw SQL is fine, but using an object-oriented Query Builder is faster and more elegant. Facil features a fluent, Laravel-inspired microORM that compiles to safe PDO queries.
Inserting Data
The insert method accepts an associative array and returns the newly inserted ID.
use Facil\Database\Query;
$newUserId = Query::table('users')->insert([
'name' => 'Digo',
'email' => 'digo@example.com',
'is_active' => 1
]);
Fetching Data (Select & Where)
Chain methods like where, orderBy, and select. Use get() to retrieve multiple rows or first() for a single row.
// Fetch a single user by email
$user = Query::table('users')->where('email', 'digo@example.com')->first();
// Fetch specific columns for multiple active users
$users = Query::table('users')
->select('id, name')
->where('is_active', 1)
->orderBy('created_at', 'DESC')
->get();
Updating Data
Use update to modify records matching the where clauses. It returns the number of affected rows.
$affected = Query::table('users')
->where('id', $newUserId)
->update(['name' => 'Rodrigo Borges']);
Deleting Data
Query::table('users')->where('is_active', 0)->delete();
Pagination (Built-in)
Say goodbye to writing LIMIT and OFFSET manually. The paginate() method automatically queries the database, calculates the total rows, and returns a standard JSON structure with data and metadata.
// Grabs the page from the URL query string (?page=2)
$page = (int) Request::query('page', 1);
$paginatedResult = Query::table('users')
->where('is_active', 1)
->paginate($page, pageSize: 15);
return Response::json($paginatedResult);
/*
Returns:
{
"data": [{...}, {...}],
"meta": { "total": 45, "page": 1, "page_size": 15, "last_page": 3, "has_more": true }
}
*/
Relationships (Eager Loading)
Fetch related data in a single extra query using the with() method to solve the N+1 problem instantly.
// Fetches active users and attaches their posts automatically
$usersWithPosts = Query::table('users')
->where('is_active', 1)
->with(table: 'posts', foreignKey: 'user_id', localKey: 'id')
->get();
Migrations (Schema Builder)
Facil comes with a Schema Builder focused on SQLite to help you create tables programmatically without writing raw `CREATE TABLE` statements.
Create a migrate.php file in the root of your project and run it via terminal: php migrate.php
<?php
require __DIR__ . '/vendor/autoload.php';
use Facil\Database\Database;
use Facil\Database\Schema;
use Facil\Database\Blueprint;
// Connect to SQLite
Database::connect('sqlite:' . __DIR__ . '/database/app.sqlite');
Schema::create('users', function(Blueprint $table) {
$table->id(); // Creates an auto-increment primary key
$table->string('name');
$table->string('email')->unique();
$table->string('password');
$table->boolean('is_active'); // Creates an integer field defaulting to 0
$table->timestamps(); // Creates created_at and updated_at
});
echo "Migrations completed!";
Authentication
Built-in session-based authentication. Manage logins, logouts, and user sessions flawlessly.
use Facil\Support\Auth;
use Facil\Http\Response;
return [
'POST' => function() {
// Attempts login (automatically hashes and compares the password)
if (Auth::attempt('john@example.com', 'secret123')) {
return Response::json([
'message' => 'Logged in!',
'user' => Auth::user() // Retrieves the user record
]);
}
return Response::json(['error' => 'Invalid credentials'], 401);
},
'GET' => function() {
// Checking if a user is logged in
if (!Auth::check()) {
return Response::json(['error' => 'Unauthorized'], 401);
}
return Response::json(['user_id' => Auth::id()]);
},
'DELETE' => function() {
Auth::logout();
return Response::json(['message' => 'Logged out!']);
}
];
Views & HTML
Building a Fullstack app? Facil comes with a built-in View engine that easily extracts variables into HTML/PHP templates.
Returning a View in a Route
use Facil\View\View;
return [
'GET' => function() {
// Looks for 'views/pages/home.php'
return View::render('pages.home', [
'title' => 'Welcome to Facil',
'user' => 'Rodrigo'
]);
}
];
The HTML File (views/pages/home.php)
<h1><?= $title ?></h1>
<p>Hello, <?= $user ?>!</p>
Security & CORS
Facil comes with out-of-the-box tools to secure your app against common vulnerabilities like XSS, Clickjacking, and CSRF.
Global Security Headers & CORS
Typically added to your public/index.php Front Controller:
use Facil\Http\Security;
// Sets anti-XSS, anti-clickjacking, and strict-transport-security headers
Security::setHeaders();
// Configure allowed domains for your API
Security::cors('https://my-frontend-app.com');
CSRF Protection
Protect your fullstack form submissions with ease using the built-in CSRF helpers.
<!-- Inside your HTML View -->
<form method="POST" action="/update-profile">
<?= \Facil\View\View::csrf() ?>
<input type="text" name="name">
<button type="submit">Save</button>
</form>
// Inside your Route to validate the incoming POST request
use Facil\Http\Request;
use Facil\Http\Response;
return [
'POST' => function() {
// Checks the body payload or 'X-CSRF-TOKEN' header
if (!Request::verifyCsrf()) {
return Response::json(['error' => 'Invalid CSRF token'], 403);
}
return Response::json(['message' => 'Secure payload accepted!']);
}
];