v0.1

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