Advanced PHP framework with zero external dependencies.
Inspired by the Lapa Framework, Routsy elevates the concept of a minimalist framework with native integration of Artificial Intelligence, robust security, multi-driver cache, queue system, migrations, template engine, intelligent error detection, multi-channel log system, extensible plugins, OAuth2 authentication (Google, Microsoft, GitHub), S3-compatible storage (AWS, MinIO, R2), and an admin dashboard — all without relying on external dependencies.
GitHub Organization: https://github.com/routsy
- Why Routsy?
- Requirements
- Installation
- Quick Start
- Directory Structure
- Core Features
- Configuration
- Security Best Practices
- Deploy in Production
- License
| Feature | Lapa | Routsy |
|---|---|---|
| Router with parameters | Yes | Yes + named routes + REST resource |
| Subdomains | Yes (vhost) | Yes (multi-host with pattern) |
| Database | Medoo (wrapper) | Native fluent Query Builder + Schema + Migrations |
| Security | Basic CORS | CSRF, JWT, AES-256-GCM, Rate Limiting, Sanitizer |
| Artificial Intelligence | No | OpenAI / compatible, Chat, Vision, Embeddings |
| Cache | Simple file | Multi-driver (File, Redis, Memory) with tags |
| Validation | Basic | 30+ rules, customizable messages |
| Templates | Plain PHP with extract | Blade-like engine (layouts, sections, partials) |
| PHPMailer (ext. dep.) | Native SMTP (zero dependencies) | |
| Queues | No | Yes (File + Database drivers) |
| CLI / Console | No | Yes (serve, migrate, scaffold) |
| External dependencies | 3 (Medoo, PHPMailer, JSON) | Zero — native PHP extensions only |
| Error Detection | Simple message | Intelligent debug mode with exact line, fix suggestions, syntax highlighting, stack trace, environment and request info |
| Log System | Simple file | Multi-channel (file, database, syslog) with rotation, PSR-like levels, filters, and query |
| Plugins | No | Complete system — discovery, lifecycle, hooks/events, marketplace-ready |
| Authentication | No | Full auth — login, register, roles, password reset, remember-me, throttle |
| OAuth2 / Social Login | No | Google, Microsoft, GitHub — full flow |
| Cloud Storage | No | S3-compatible — AWS S3, MinIO, Cloudflare R2, DigitalOcean, Backblaze |
| Admin Dashboard | No | Admin panel — login, user CRUD, logs, metrics |
- PHP 8.1 or higher
- Extensions:
pdo,json,mbstring,openssl,curl
composer create-project routsy/framework my-project
cd my-project
# Set up environment
cp .env.example .env
# Generate encryption key
php bin/routsy key:generate
# Start development server
php bin/routsy serve<?php
// public/index.php
require __DIR__ . '/../vendor/autoload.php';
$app = new Routsy\Routsy([
'debug' => true,
'base_path' => dirname(__DIR__),
]);
$app->router->get('/', function ($request, $response) {
return $response->json(['hello' => 'world']);
});
$app->run();$config = require __DIR__ . '/../config/app.php';
$config['base_path'] = dirname(__DIR__);
$app = new Routsy\Routsy($config);
$app->run();my-project/
├── bin/
│ └── routsy # CLI (composer, migrations, scaffold)
├── config/
│ └── app.php # Main configuration
├── public/
│ ├── index.php # HTTP entry point
│ └── .htaccess # URL rewriting
├── routes/
│ ├── web.php # Web routes (pages, views)
│ └── api.php # API routes (JSON)
├── src/ # Framework core
│ ├── Routsy.php # Main application class
│ ├── Router/ # Routing system
│ ├── Database/ # Query builder, schema, migrations
│ ├── Security/ # CSRF, JWT, encryption, sanitizer, rate limiter
│ ├── Ai/ # OpenAI client, prompts, embeddings
│ ├── Cache/ # File, Redis, Memory drivers
│ ├── Validation/ # Validator with 30+ rules
│ ├── Session/ # Session + flash management
│ ├── View/ # Template engine
│ ├── Mail/ # Native SMTP
│ ├── Queue/ # Queue system
│ ├── Console/ # CLI commands
│ ├── Http/ # Request, Response, Middleware
│ ├── ErrorHandler/ # Intelligent error detector
│ ├── Log/ # Multi-channel logger
│ ├── Auth/ # Authentication system
│ ├── Storage/ # S3-compatible storage
│ ├── FileManager/ # File & media management
│ ├── Plugin/ # Plugin system
│ ├── Integrations/ # OAuth2 providers
│ └── Helpers/ # Global helper functions
├── storage/
│ ├── cache/
│ ├── logs/
│ ├── uploads/
│ └── temp/
├── views/
│ ├── layouts/
│ ├── partials/
│ └── home.php
├── admin/ # Admin dashboard routes and views
├── plugins/ # Installed plugins directory
├── .env.example
├── .htaccess
└── composer.json
use Routsy\Http\Request;
use Routsy\Http\Response;
// GET
$router->get('/users', function (Request $req, Response $res) {
return $res->json(['users' => []]);
});
// POST
$router->post('/users', function (Request $req, Response $res) {
$data = $req->input();
return $res->success($data, 'Created', 201);
});
// PUT, PATCH, DELETE, OPTIONS
$router->put('/users/{id}', fn(Request $req, Response $res) =>
$res->success(['updated' => $req->param('id')])
);
// Any method
$router->any('/webhook', fn() => 'ok');
// Multiple methods
$router->match(['GET', 'POST'], '/form', fn() => 'ok');// Lapa style: :param
$router->get('/users/:id', function (Request $req, Response $res) {
$id = $req->param('id');
return $res->success(['user' => $id]);
});
// Laravel style: {param}
$router->get('/posts/{slug}', function (Request $req, Response $res) {
return $res->success(['slug' => $req->param('slug')]);
});
// With constraints (where)
$router->get('/users/{id}', fn() => ...)
->where(['id' => '\d+']); // numbers only$router->group('/admin', function ($router) {
$router->get('/dashboard', fn() => 'Admin Dashboard');
$router->get('/users', fn() => 'Manage Users');
// Nested groups
$router->group('/settings', function ($router) {
$router->get('/profile', fn() => 'Profile Settings');
$router->get('/security', fn() => 'Security Settings');
});
});// API subdomain
$router->host('api.mysite.com', function ($router) {
$router->get('/v1/status', fn() => ['version' => '1.0']);
});
// Admin subdomain
$router->host('admin.mysite.com', function ($router) {
$router->get('/', fn() => 'Admin Area');
});// Auto-generates: index, create, store, show, edit, update, destroy
$router->resource('posts', App\Controllers\PostController::class);
// API resource (no create/edit)
$router->apiResource('posts', App\Controllers\PostApiController::class);$router->get('/profile', fn() => ...)->name('profile');
$router->get('/users/{id}', fn() => ...)->name('users.show');
// Generate URL
$url = $router->url('users.show', ['id' => 5]); // /users/5
// Global helper
$url = route('profile');$auth = function (Request $req, Closure $next) {
if (!$req->bearerToken()) {
return (new Response())->error('Unauthorized', 401);
}
return $next($req);
};
$router->group('/admin', function ($router) {
$router->get('/dashboard', fn() => 'OK');
})->middleware($auth);// config/app.php
'database' => [
'driver' => 'mysql', // mysql, pgsql, sqlite
'host' => 'localhost',
'database' => 'my_database',
'username' => 'root',
'password' => '',
'charset' => 'utf8mb4',
],$db = $app->db();
// SELECT
$users = $db->table('users')
->select(['id', 'name', 'email'])
->where('active', 1)
->where('created_at', '>', '2024-01-01')
->orderBy('name')
->limit(10)
->get();
// First result
$user = $db->table('users')->where('id', 1)->first();
// Count
$total = $db->table('users')->where('active', 1)->count();
// EXISTS
$exists = $db->table('users')->where('email', 'test@test.com')->exists();
// Pagination
$result = $db->table('users')->paginate(15, 1);
// Returns: ['data' => [...], 'total' => 100, 'page' => 1, 'perPage' => 15, 'lastPage' => 7]// Insert
$id = $db->table('users')->insert([
'name' => 'John Doe',
'email' => 'john@email.com',
]);
// Insert many
$db->table('users')->insertMany([
['name' => 'A', 'email' => 'a@a.com'],
['name' => 'B', 'email' => 'b@b.com'],
]);
// Update
$db->table('users')->where('id', 1)->update(['name' => 'New Name']);
// Delete
$db->table('users')->where('id', 1)->delete();$orders = $db->table('orders', 'o')
->select(['o.id', 'o.total', 'u.name'])
->join('users', 'o.user_id', '=', 'u.id', 'INNER')
->leftJoin('payments', 'o.id', '=', 'p.order_id')
->where('o.status', 'paid')
->get();$total = $db->table('orders')->sum('amount');
$avg = $db->table('orders')->avg('amount');
$min = $db->table('products')->min('price');
$max = $db->table('products')->max('price');$app->connection()->transaction(function ($conn) use ($db) {
$db->table('accounts')->where('id', 1)->update(['balance -=' => 100]);
$db->table('accounts')->where('id', 2)->update(['balance +=' => 100]);
});$results = $db->raw("SELECT * FROM users WHERE created_at > ?", ['2024-01-01']);
$db->rawExecute("UPDATE users SET active = 0 WHERE last_login < ?", ['2023-01-01']);// Create table programmatically
$app->schema()->createTable('products', function ($table) {
$table->id();
$table->string('name', 200);
$table->text('description')->nullable();
$table->decimal('price', 10, 2);
$table->integer('stock')->default(0);
$table->boolean('active')->default(true);
$table->foreign('category_id', 'id', 'categories');
$table->timestamps();
$table->softDeletes();
});
// Migrations via CLI
// php bin/routsy make:migration CreateProductsTable
// php bin/routsy migrate
// php bin/routsy migrate:rollback
// php bin/routsy migrate:status// Generate token (in forms)
echo csrf_field(); // <input type="hidden" name="_csrf_token" value="...">
echo csrf_token(); // Token value only
// Validate token (in controller)
$app->csrf->validate($request->post('_csrf_token'));$encrypted = encrypt('secret data');
$original = decrypt($encrypted);
// Password hashing (Argon2id)
$hash = bcrypt('my-password');// Generate token
$token = $app->jwt()->encode([
'user_id' => 42,
'role' => 'admin',
], 3600); // 1 hour
// Validate token
try {
$payload = $app->jwt()->decode($token);
$userId = $payload['user_id'];
} catch (\RuntimeException $e) {
// Invalid or expired token
}
// Refresh token
$newToken = $app->jwt()->refresh($token);$ip = $request->ip();
$limiter = $app->rateLimiter;
if ($limiter->tooManyAttempts('login:' . $ip, 5, 60)) {
return $response->error('Too many attempts. Try again in ' .
$limiter->availableIn('login:' . $ip, 5, 60) . 's', 429);
}
// Clear attempts after success
$limiter->clear('login:' . $ip);$clean = $app->sanitizer->string($input); // Remove HTML, escape
$email = $app->sanitizer->email($input); // Sanitize email
$num = $app->sanitizer->int($input); // Safe integer
$url = $app->sanitizer->url($input); // Safe URL
$file = $app->sanitizer->filename($input); // Safe filename
$arr = $app->sanitizer->array($data); // Recursively sanitize arrayCompatible client for OpenAI, Azure OpenAI, Anthropic, Ollama, and any compatible API.
AI_API_KEY=sk-your-key-here
AI_BASE_URL=https://api.openai.com/v1
AI_MODEL=gpt-4o$ai = $app->ai();
// Simple question
$answer = $ai->ask('What is the capital of France?');
// With system prompt
$answer = $ai->ask(
'What is PHP?',
'You are a programming expert. Answer concisely.'
);
// Conversation with history
$messages = [
['role' => 'system', 'content' => 'You are a helpful assistant.'],
['role' => 'user', 'content' => 'Who are you?'],
];
$result = $ai->chat($messages);
echo $result['choices'][0]['message']['content'];
// Streaming (real-time)
$fullText = $ai->chatStream($messages, function ($chunk) {
echo $chunk;
ob_flush(); flush();
});$description = $ai->vision(
'https://example.com/photo.jpg',
'Describe what you see in this image.'
);// Create semantic index
$embeddings = new Routsy\Ai\Embeddings($ai);
$embeddings->index('doc1', 'Routsy is a modern PHP framework.');
$embeddings->index('doc2', 'Laravel is a popular PHP framework.');
$embeddings->index('doc3', 'React is a JavaScript library for UIs.');
// Search
$results = $embeddings->search('frameworks for web development', 2);
// Results sorted by cosine similarity// Register template
Routsy\Ai\Prompt::register('translator',
'Translate the following text from {{from}} to {{to}}: {{text}}'
);
// Use template
$result = Routsy\Ai\Prompt::use('translator', [
'from' => 'English',
'to' => 'French',
'text' => 'Hello, how are you?',
]);// File driver (default)
$app->cache->set('key', 'value', 3600); // Store
$value = $app->cache->get('key'); // Retrieve
$exists = $app->cache->has('key'); // Check
$app->cache->delete('key'); // Remove
$app->cache->clear(); // Clear all
// Increment / Decrement
$app->cache->increment('visits');
$app->cache->decrement('stock');
// Remember (cache + callback)
$users = $app->cache->remember('users.list', 300, function () use ($db) {
return $db->table('users')->get();
});
// Global helper
cache('key', 'value', 600);
$value = cache('key');// File (default)
$cache = $app->cache->driver('file');
// Redis (requires ext-redis)
$cache = $app->cache->driver('redis');
// Memory (request lifetime only)
$cache = $app->cache->driver('memory');use Routsy\Validation\Validator;
use Routsy\Validation\ValidationException;
$data = $request->input();
try {
$validated = Validator::validate($data, [
'name' => 'required|min:3|max:100',
'email' => 'required|email|unique:users,email',
'age' => 'required|integer|between:1,120',
'password' => 'required|min:8|confirmed',
'role' => 'required|in:admin,user,editor',
'bio' => 'nullable|string|max:500',
'website' => 'nullable|url',
'avatar' => 'nullable|file|image|mimes:jpeg,png|size:2048',
]);
// $validated contains only the validated fields
$db->table('users')->insert($validated);
} catch (ValidationException $e) {
return $response->error('Validation failed', 422, $e->errors());
}| Rule | Description |
|---|---|
required |
Field is required |
nullable |
Field may be null (skips remaining rules) |
email |
Valid email address |
min:N |
Minimum value (string: length, number: value) |
max:N |
Maximum value |
between:MIN,MAX |
Value between MIN and MAX |
numeric |
Numeric value |
integer |
Integer value |
string |
Must be a string |
array |
Must be an array |
in:val1,val2 |
Value must be in the list |
not_in:val1,val2 |
Value must not be in the list |
regex:/pattern/ |
Regex validation |
url |
Valid URL |
date |
Valid date |
date_format:Y-m-d |
Date in specific format |
confirmed |
Confirmation (field_confirmation must match) |
same:other_field |
Must equal another field |
different:other_field |
Must differ from another field |
starts_with:abc |
Starts with... |
ends_with:xyz |
Ends with... |
alpha |
Letters only |
alphanumeric |
Letters and numbers only |
boolean |
true/false/0/1 |
json |
Valid JSON |
file |
Uploaded file |
image |
Valid image |
mimes:jpeg,png |
Allowed MIME types |
size:2048 |
Maximum size in KB |
unique:table,column |
Value must be unique in DB |
exists:table,column |
Value must exist in DB |
$validator = new Validator($data, $rules);
$validator->setMessages([
'email.required' => 'The email is required!',
'email.email' => 'Invalid email format.',
'required' => 'The :field field is required.',
]);
if (!$validator->validate()) {
$errors = $validator->errors();
}{{-- Layout with extension --}}
@extends('default')
@section('content')
<h1>{{ $title }}</h1>
@if($user)
<p>Welcome, {{ $user['name'] }}!</p>
@else
<p>Please log in.</p>
@endif
@foreach($posts as $post)
<div class="post">
<h2>{{ $post['title'] }}</h2>
<p>{!! $post['body'] !!}</p>
</div>
@endforeach
@include('partials.sidebar', ['categories' => $categories])
<form method="POST">
@csrf
@method('PUT')
<input name="name" value="{{ old('name') }}">
</form>
@endsection// Render view
return $app->view->render('home', [
'title' => 'Home Page',
'user' => ['name' => 'John'],
]);
// With specific layout
return $app->view->render('blog/post', $data, 'admin');
// Global helper
return view('welcome', ['name' => 'World']);
// Partial
return $app->view->partial('header', ['title' => 'My Site']);| Directive | Description |
|---|---|
@extends('layout') |
Defines parent layout |
@section('name') ... @endsection |
Defines a section |
@yield('name') |
Renders a section |
@include('view') |
Includes a partial |
@if / @elseif / @else / @endif |
Conditionals |
@foreach / @endforeach |
Foreach loop |
@for / @endfor |
For loop |
@while / @endwhile |
While loop |
@isset / @endisset |
Checks if variable exists |
@empty / @endempty |
Checks if variable is empty |
@auth / @endauth |
User is authenticated |
@guest / @endguest |
User is not authenticated |
@csrf |
CSRF field |
@method('PUT') |
Method spoofing |
{{ $var }} |
Safe echo (htmlspecialchars) |
{!! $var !!} |
Raw echo |
$app->mailer()->send(
'recipient@email.com',
'Email Subject',
'<h1>Hello!</h1><p>Email body in <strong>HTML</strong>.</p>',
[
'cc' => 'cc@email.com',
'bcc' => 'bcc@email.com',
'reply_to' => 'reply@email.com',
'alt_body' => 'Plain text version for non-HTML clients.',
]
);
// With attachments
$app->mailer()
->attach('/path/file.pdf', 'Report.pdf')
->send('recipient@email.com', 'Report', '<p>Attached.</p>');class SendWelcomeEmail
{
public function handle(array $payload): void
{
$email = $payload['email'];
// Send welcome email...
}
}// File driver
$app->queue()->push(SendWelcomeEmail::class, [
'email' => 'user@example.com',
'name' => 'John',
]);
// With 5-minute delay
$app->queue()->push(SendWelcomeEmail::class, $data, 300);# Process 1 job
php bin/routsy queue:work
# Process 10 jobs
php bin/routsy queue:work --max=10
# Pending jobs
php bin/routsy queue:pending# Development server
php bin/routsy serve
php bin/routsy serve 0.0.0.0 8080
# Generate application key
php bin/routsy key:generate
# Clear cache
php bin/routsy cache:clear
# Scaffolding
php bin/routsy make:migration CreateUsersTable
php bin/routsy make:controller UserController
php bin/routsy make:middleware AuthMiddleware
# Migrations
php bin/routsy migrate
php bin/routsy migrate:rollback
php bin/routsy migrate:status
# Queues
php bin/routsy queue:work
php bin/routsy queue:pending
# List routes
php bin/routsy routes:list// In bin/routsy or bootstrap
$console = new Routsy\Console\Console($argv, $basePath);
$console->register('my:command', function ($args) {
echo "Command executed with arguments: " . implode(', ', $args) . "\n";
return 0;
}, 'Description of my command');| Helper | Description |
|---|---|
app() |
Application instance |
config('database.host') |
Config value (dot notation) |
env('APP_KEY') |
Environment variable |
view('home', $data) |
Render view |
route('users.show', ['id' => 5]) |
Generate URL by route name |
redirect('/login') |
Redirect |
back() |
Go back to previous page |
session('user_id') |
Get session value |
flash('success', 'Message!') |
Flash message |
csrf_field() |
CSRF hidden field |
csrf_token() |
CSRF token |
old('email') |
Old input (after validation) |
error('email') |
First validation error |
errors() |
All validation errors |
cache('key') |
Get/set cache |
encrypt($data) |
Encrypt data |
decrypt($payload) |
Decrypt data |
bcrypt('password') |
Password hash |
slug('Hello World!') |
Generate slug |
str_random(32) |
Random string |
str_limit('long text', 10) |
Truncate string |
now() |
Current DateTime |
ago('2024-01-01') |
"X time ago" |
dd($var) |
Dump and Die |
dump($var) |
Dump (non-fatal) |
abort(404) |
Abort with HTTP code |
base_path('config') |
Absolute path |
storage_path('uploads') |
Path to storage |
public_path('css/app.css') |
Path to public |
response(['ok' => true]) |
Create Response |
Routsy includes an intelligent error detection system that, in debug mode, shows exactly:
- The exact line where the error occurred, highlighted with an animated
← ERROR HEREarrow - Code context (10 lines before and after) with syntax highlighting
- Error classification (Syntax Error, Type Error, Database Error...)
- Automatic fix suggestions based on the error type
- Formatted stack trace with argument previews for each call
- Environment information (PHP version, OS, memory, loaded extensions)
- Request data (URL, method, headers, IP, user agent)
- Timeline (execution time up to the error)
- Dark/light mode auto-detection
┌ MAIN ERROR src/Controllers/UserController.php (lines 32-52 of 120)
30 │ public function show(Request $req, Response $res): mixed
31 │ {
32 │► $user = $this->db->table('users')->where('id', $id)->first(); ← ERROR HERE
33 │ return $res->success($user);
34 │ }
💡 Fix Suggestion: The variable was not defined before being used. Check that you initialized the variable or that the name is correct. In this case, $id should be $req->param('id').
With APP_DEBUG=false, the user sees a friendly, generic error page. Full details are always logged to storage/logs/error.log.
Multi-channel logger compatible with PSR-3. Supports levels (emergency, alert, critical, error, warning, notice, info, debug), multiple channels, and automatic rotation.
// Available channels: file, database, syslog
$app->logger->info('User {user} logged in', ['user' => 'john@email.com']);
$app->logger->error('DB connection failed: {error}', ['error' => $e->getMessage()]);
$app->logger->warning('API limit nearly reached: {remaining}', ['remaining' => 5]);
// Specific channel
$app->logger->error('Critical error!', [], ['error']); // writes to error channel only
// Global context
$app->logger->withContext(['app_version' => '1.0', 'env' => 'production']);
// Query logs (for dashboards)
$recentErrors = $app->logger->query('error', 50, 0, 'database');
// Prune old logs
$app->logger->prune(30); // remove logs older than 30 daysChannels:
| Channel | Description |
|---|---|
file |
File with automatic size-based rotation (default) |
database |
logs table in the database |
syslog |
Operating system syslog |
null |
Discards logs (for testing) |
Complete plugin system with automatic discovery, lifecycle management, and hooks. Plugins can be installed directly from GitHub repositories through the admin dashboard.
$plugins = $app->plugins();
// Discover plugins
$all = $plugins->discover();
// Install / Enable / Disable
$plugins->install('MyPlugin');
$plugins->enable('MyPlugin');
$plugins->disable('MyPlugin');
$plugins->uninstall('MyPlugin');
// Hooks — fire events
$plugins->fire('user.created', $user);
// Hooks — listen to events
$plugins->on('user.created', function ($user) {
// Send welcome email...
});
// Filters — transformation chain
$plugins->on('content.render', function ($content) {
return str_replace('{year}', date('Y'), $content);
});
$parsed = $plugins->filter('content.render', $rawContent);
// Check if plugin is active
if ($plugins->isActive('MyPlugin')) { ... }Plugin structure:
plugins/MyPlugin/
├── manifest.json # {"name":"MyPlugin","version":"1.0","requires":[]}
├── Plugin.php # Class with boot(), install(), uninstall()
├── routes.php # Plugin routes
├── views/ # Plugin views
└── migrations/ # Plugin migrations
manifest.json:
{
"name": "MyPlugin",
"version": "1.0.0",
"description": "Plugin description",
"author": "Author Name",
"requires": ["AnotherPlugin"],
"class": "Plugin\\MyPlugin\\Plugin"
}Installation via Admin Dashboard: Plugins can be installed directly through the admin dashboard at /admin/plugins by providing the GitHub repository URL (e.g., https://github.com/username/routsy-plugin). The system automatically clones the repository, resolves dependencies, registers the plugin, and activates it.
Complete authentication system with roles, throttle, and remember-me.
$auth = $app->auth();
// Login
$user = $auth->attempt('email@example.com', 'password', remember: true);
if (!$user) { /* invalid credentials */ }
// Check authentication
if ($auth->check()) {
$user = $auth->user();
$id = $auth->id();
}
// Logout
$auth->logout();
// Register
$userId = $auth->register([
'name' => 'John Doe',
'email' => 'john@email.com',
'password' => 'secret123',
'role' => 'user',
]);
// Roles
if ($auth->hasRole('admin')) { ... }
if ($auth->hasRole(['admin', 'editor'])) { ... }
if ($auth->can('delete')) { ... }
// Password Reset
$token = $auth->createResetToken('john@email.com');
// Send email with link: /reset?token=$token&email=...
$success = $auth->resetPassword('john@email.com', $token, 'new-password');
// User CRUD (admin)
$users = $auth->listUsers(page: 1, perPage: 15, filters: ['role' => 'admin']);
$user = $auth->findUser(1);
$auth->createUser([...]);
$auth->updateUser(1, ['role' => 'admin']);
$auth->deleteUser(1);Auth middleware:
use Routsy\Auth\AuthMiddleware;
// Require login
$router->get('/profile', fn() => ...)->middleware(
AuthMiddleware::auth($app)
);
// Require specific role
$router->get('/admin', fn() => ...)->middleware(
AuthMiddleware::role($app, 'admin')
);
// Redirect if already authenticated
$router->get('/login', fn() => ...)->middleware(
AuthMiddleware::guest($app)
);Login with Google, Microsoft, and GitHub. Generic OAuth 2.0 client with full flow support.
use Routsy\Integrations\OAuth2;
// Configuration
$oauthConfig = [
'client_id' => 'YOUR_CLIENT_ID',
'client_secret' => 'YOUR_CLIENT_SECRET',
'redirect_uri' => 'https://mysite.com/auth/google/callback',
];
$oauth = new OAuth2($app, 'google', $oauthConfig);
// Route: initiate login
$router->get('/auth/google', function () use ($oauth) {
$oauth->redirect();
});
// Route: callback (process return)
$router->get('/auth/google/callback', function () use ($oauth, $app) {
try {
$user = $oauth->callback();
// $user = ['provider' => 'google', 'email' => '...', 'name' => '...', 'avatar' => '...']
// Create/authenticate local user
$localUser = $app->db()->table('users')->where('email', $user['email'])->first();
if (!$localUser) {
$id = $app->auth()->register([
'name' => $user['name'],
'email' => $user['email'],
'password' => bin2hex(random_bytes(16)),
'oauth_provider' => 'google',
'oauth_id' => $user['provider_id'],
]);
$localUser = $app->auth()->findUser($id);
}
$app->auth()->login($localUser);
return $app->response->redirect('/dashboard');
} catch (\RuntimeException $e) {
return $app->response->error('Authentication failed: ' . $e->getMessage());
}
});
// Microsoft
$oauth = new OAuth2($app, 'microsoft', ['client_id' => '...', 'client_secret' => '...', 'redirect_uri' => '...', 'tenant' => 'common']);
// GitHub
$oauth = new OAuth2($app, 'github', ['client_id' => '...', 'client_secret' => '...', 'redirect_uri' => '...']);Supported providers: Google, Microsoft (Azure AD), GitHub
S3-compatible driver for AWS S3, MinIO, Cloudflare R2, DigitalOcean Spaces, Backblaze B2, and Wasabi.
use Routsy\Storage\S3Driver;
// Configuration
$s3 = new S3Driver([
'key' => 'YOUR_ACCESS_KEY',
'secret' => 'YOUR_SECRET_KEY',
'region' => 'us-east-1',
'bucket' => 'my-bucket',
'endpoint' => 'https://s3.amazonaws.com', // AWS
// 'endpoint' => 'https://minio.mysite.com', // MinIO
// 'endpoint' => 'https://ACCOUNT.r2.cloudflarestorage.com', // Cloudflare R2
// 'path_style' => true, // For MinIO and R2
]);
// Basic operations
$s3->put('folder/photo.jpg', $imageContent, 'image/jpeg');
$s3->putFile('docs/report.pdf', '/local/path/file.pdf', 'application/pdf');
$content = $s3->get('folder/photo.jpg');
$exists = $s3->exists('folder/photo.jpg');
$size = $s3->size('folder/photo.jpg');
$mime = $s3->mimeType('folder/photo.jpg');
$s3->delete('folder/photo.jpg');
// URLs
$url = $s3->url('folder/photo.jpg'); // Public URL
$signedUrl = $s3->signedUrl('folder/photo.jpg', 3600); // Pre-signed URL (1h)
// Listing
$files = $s3->list('folder/', 100);
// [['key' => 'folder/a.jpg', 'size' => 12345, 'last_modified' => '...', 'etag' => '...'], ...]
// Copy / Move
$s3->copy('source.txt', 'destination.txt');
$s3->move('source.txt', 'destination.txt');Authentication: AWS Signature V4 calculated natively, with zero external dependencies.
Complete file upload, storage, and processing system. Ideal for blogs, CMS, and galleries.
$fm = $app->fileManager();
// Upload with validation
$file = $fm->upload('photo', [
'max_size' => '5MB',
'types' => ['image/jpeg', 'image/png', 'image/webp'],
'min_width' => 800,
], 'blog/posts');
// Upload raw content
$doc = $fm->put('docs/report.pdf', $pdfContent, 'application/pdf');
// URLs
echo $file->url(); // /storage/uploads/blog/posts/abc123.jpg
echo $file->signedUrl(300); // Temporary URL (5 min)
// Read / Download
$content = $fm->read($file);
$fm->download($file, 'profile-photo.jpg');
// Thumbnails (GD, zero dependencies)
$thumb = $fm->thumbnail($file, 400, 300, 'cover'); // center crop
$thumb = $fm->thumbnail($file, 800, 0, 'contain'); // proportional
$fm->images()->crop($file, 0, 0, 200, 200); // manual crop
$fm->images()->convert($file, 'webp'); // convert format
$fm->images()->watermarkText($file, '© My Site'); // watermark
$fm->images()->strip($file); // remove EXIF
// Serve image with on-the-fly resize
// GET /files/uploads/blog/posts/photo.jpg?w=400&h=300&fit=cover
// Media Library (blog/CMS)
$library = $fm->library();
$media = $library->list('blog/post-1', page: 1, perPage: 20);
$images = $library->list('blog/post-1', type: 'image');
$library->attachToPost($file->id, $postId, ['alt_text' => 'Article photo']);
$library->setFeatured($postId, $file->id);
$featured = $library->getFeatured($postId);
$gallery = $library->getPostFiles($postId);
// Folders and statistics
$folders = $library->folders();
$stats = $library->stats(); // total, images, size, foldersSupported drivers:
| Driver | Config | Usage |
|---|---|---|
local |
Local disk (default) | $fm->driver('local') |
s3 |
AWS S3, MinIO, R2... | $fm->addDriver('s3', new S3Driver([...])) |
Image capabilities (native GD, no Imagick):
- Resize with fit modes:
cover,contain,fill,stretch - Manual crop
- Format conversion (JPEG ↔ PNG ↔ WebP ↔ GIF)
- Text watermark
- EXIF metadata stripping
- Cached thumbnails (processed once)
Built-in admin panel accessible at /admin.
// Routes are auto-loaded from admin/routes.php
// Access: http://localhost:8000/admin
// Create first admin:
$app->auth()->register([
'name' => 'Admin',
'email' => 'admin@example.com',
'password' => 'admin123',
'role' => 'admin',
]);Dashboard features:
- Administrative login with throttle
- Dashboard with metrics (users, logs, storage, PHP)
- User CRUD with search, role filter, and pagination
- Log viewer with level filters and text search
- Plugin manager — install plugins directly from GitHub repositories
- Automatic dark/light mode
- Protected by auth + role middleware
APP_NAME=Routsy
APP_DEBUG=true
APP_TIMEZONE=UTC
APP_KEY=base64:...
DB_DRIVER=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=routsy
DB_USERNAME=root
DB_PASSWORD=
CACHE_DRIVER=file
QUEUE_DRIVER=file
LOG_DRIVER=file
LOG_LEVEL=debug
MAIL_HOST=smtp.example.com
MAIL_PORT=587
MAIL_ENCRYPTION=tls
MAIL_USERNAME=user@example.com
MAIL_PASSWORD=secret
MAIL_FROM_NAME=Routsy
MAIL_FROM_ADDRESS=noreply@example.com
AI_API_KEY=sk-...
AI_BASE_URL=https://api.openai.com/v1
AI_MODEL=gpt-4o
CORS_ENABLED=false
CORS_ORIGINS=https://mysite.com
CORS_METHODS=GET,POST,PUT,DELETE
CORS_HEADERS=Content-Type,Authorization
CORS_CREDENTIALS=true- APP_KEY — Generate with
php bin/routsy key:generateand never commit the.envfile - CSRF — Use
@csrfin all POST/PUT/DELETE forms - JWT — Use short-lived tokens; refresh as needed
- Rate Limiting — Apply to sensitive endpoints (login, API)
- Sanitization — Always sanitize user input
- CORS — Configure specific origins in production
- HTTPS — Enable
FORCE_HTTPS=truein production - Security Headers — X-Frame-Options, X-Content-Type-Options, X-XSS-Protection (enabled by default)
# 1. Install dependencies without dev
composer install --no-dev --optimize-autoloader
# 2. Configure environment
cp .env.example .env
php bin/routsy key:generate
# 3. Tune .env for production
APP_DEBUG=false
FORCE_HTTPS=true
CORS_ENABLED=true
# 4. Set permissions
chmod -R 755 storage
chmod -R 755 publicMIT License. See the LICENSE file for details.
Routsy Framework — Built with passion. Visit us at https://github.com/routsy.