LibxaFrame Starter Application
Welcome to your new LibxaFrame application! This starter provides a clean, modern foundation for building web applications with PHP 8.3+.
About LibxaFrame
LibxaFrame is a modern, elegant, and lightning-fast PHP framework for the next generation of web applications. Built around developer happiness, performance, and scalability.
Requirements
- PHP >= 8.3
- Composer
- Node.js & NPM (for frontend assets)
- SQLite, MySQL, PostgreSQL, or SQL Server
Installation
The quick way
Install the LibxaFrame installer once:
irm https://raw.githubusercontent.com/libxa-framework/libxa-installer/main/scripts/install.ps1 | iex
curl -fsSL https://raw.githubusercontent.com/libxa-framework/libxa-installer/main/scripts/install.sh | sh
Then, for every project after that:
libxa new my-app
It asks which database you want, installs the skeleton, generates the
application key, configures .env and makes the first commit. The steps below
are what it does, for when you would rather do them yourself or are working on
an existing project.
1. Install Dependencies
composer create-project libxa/libxa my-app
Or, in a project you already have:
composer install
2. Environment Setup
Copy the example environment file and configure it:
cp .env.example .env
Generate an application key:
php libxa key:generate
3. Database Setup
Configure your database in .env:
DB_CONNECTION=sqlite
# Or for MySQL:
# DB_CONNECTION=mysql
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=your_database
# DB_USERNAME=your_username
# DB_PASSWORD=your_password
Run migrations:
php libxa migrate
4. Frontend Assets
Install and compile frontend assets:
npm install
npm run dev
For production:
npm run build
Quick Start
Start the development server:
php libxa serve
Open your browser and visit http://localhost:8000
Project Structure
your-app/
├── src/
│ ├── app/ # Application code
│ │ ├── Http/ # Controllers, Middleware, Requests
│ │ ├── Models/ # Eloquent models
│ │ ├── Services/ # Business logic
│ │ └── Providers/ # Service providers
│ ├── config/ # Configuration files
│ ├── database/ # Database files
│ │ ├── migrations/ # Migration files
│ │ └── seeds/ # Seed files
│ ├── public/ # Public assets
│ ├── resources/ # Frontend assets (JS, CSS, Views)
│ │ ├── views/ # Blade templates
│ │ ├── js/ # JavaScript files
│ │ └── css/ # CSS files
│ ├── routes/ # Route definitions
│ │ ├── web.php # Web routes
│ │ ├── api.php # API routes
│ │ └── console.php # Console routes
│ └── storage/ # Application storage
│ ├── app/ # Application generated files
│ ├── framework/ # Framework cache
│ └── logs/ # Log files
├── packages/ # Local packages
├── tests/ # Test files
├── composer.json # PHP dependencies
├── package.json # Node dependencies
└── libxa # Framework CLI tool
Available Commands
Application
php libxa serve # Start development server
php libxa key:generate # Generate application key
php libxa env # Display current environment
Database
php libxa migrate # Run database migrations
php libxa migrate:rollback # Rollback last migration
php libxa migrate:refresh # Rollback and re-run migrations
php libxa migrate:status # Show migration status
php libxa db:seed # Run database seeders
php libxa make:migration # Create a new migration
php libxa make:model # Create a new model
php libxa make:seeder # Create a new seeder
Code Generation
php libxa make:controller # Create a new controller
php libxa make:model # Create a new model
php libxa make:migration # Create a new migration
php libxa make:seeder # Create a new seeder
php libxa make:request # Create a form request
php libxa make:middleware # Create a new middleware
php libxa make:command # Create a new console command
php libxa make:provider # Create a new service provider
php libxa make:event # Create a new event
php libxa make:listener # Create a new event listener
Package Management
php libxa make:package # Create a new package
php libxa package:discover # Discover and register packages
php libxa vendor:publish # Publish package assets
Queue
php libxa queue:work # Process queue jobs
php libxa queue:listen # Listen for queue jobs
php libxa queue:restart # Restart queue workers
Cache
php libxa cache:clear # Clear application cache
php libxa config:clear # Clear configuration cache
php libxa route:clear # Clear route cache
php libxa view:clear # Clear view cache
Testing
php libxa test # Run all tests
php libxa test --filter # Run specific test
Routing
Routes are defined in src/routes/web.php for web routes and src/routes/api.php for API routes.
Basic Route
$router->get('/', function () {
return view('welcome');
});
Controller Route
$router->get('/users', [UserController::class, 'index']);
Route with Parameters
$router->get('/users/{id}', function ($id) {
return "User {$id}";
});
Route Groups
$router->group(['prefix' => 'admin', 'middleware' => 'auth'], function ($router) {
$router->get('/dashboard', [AdminController::class, 'dashboard']);
});
Controllers
Controllers are stored in src/app/Http/Controllers/.
<?php
namespace App\\Http\\Controllers;
use Libxa\\Http\\Request;
use Libxa\\Http\\Response;
class UserController extends Controller
{
public function index(): Response
{
return view('users.index');
}
}
Models
Models are stored in src/app/Models/ and extend the base Model class.
<?php
namespace App\\Models;
use Libxa\\Atlas\\Model;
class User extends Model
{
protected $fillable = ['name', 'email', 'password'];
}
Views
Views are stored in src/resources/views/ and use the Blade templating engine.
// resources/views/welcome.blade.php
<!DOCTYPE html>
<html>
<head>
<title>Welcome</title>
</head>
<body>
<h1>Welcome, {{ $name }}!</h1>
</body>
</html>
Configuration
Configuration files are located in src/config/. You can access configuration values using the config() helper:
$value = config('app.name');
Environment Variables
Environment variables are loaded from .env file. Access them using the env() helper:
$debug = env('APP_DEBUG', false);
Database
Query Builder
use Libxa\\Atlas\\DB;
$users = DB::table('users')->where('active', true)->get();
$user = DB::table('users')->where('id', 1)->first();
DB::table('users')->insert(['name' => 'John', 'email' => 'john@example.com']);
DB::table('users')->where('id', 1)->update(['name' => 'Jane']);
DB::table('users')->where('id', 1)->delete();
Eloquent ORM
// Get all users
$users = User::all();
// Find by ID
$user = User::find(1);
// Create
User::create(['name' => 'John', 'email' => 'john@example.com']);
// Update
$user = User::find(1);
$user->name = 'Jane';
$user->save();
// Delete
$user->delete();
Middleware
Middleware is stored in src/app/Http/Middleware/.
<?php
namespace App\\Http\\Middleware;
use Libxa\\Http\\Request;
use Libxa\\Http\\Response;
class CheckAge
{
public function handle(Request $request, callable $next): Response
{
if ($request->age < 18) {
return redirect('home');
}
return $next($request);
}
}
Security
Authentication
// Login
Auth::attempt(['email' => $email, 'password' => $password]);
// Get authenticated user
$user = Auth::user();
// Logout
Auth::logout();
Hashing
// Hash a password
$hashed = Hash::make('password');
// Verify a password
if (Hash::check('password', $hashed)) {
// Password matches
}
File Storage
use Libxa\\Support\\Facades\\Storage;
// Store a file
Storage::put('file.jpg', $contents);
// Get a file
$contents = Storage::get('file.jpg');
// Check if file exists
$exists = Storage::exists('file.jpg');
// Delete a file
Storage::delete('file.jpg');
// Get file URL
$url = Storage::url('file.jpg');
Cache
use Libxa\\Support\\Facades\\Cache;
// Store value
Cache::put('key', 'value', 3600);
// Get value
$value = Cache::get('key');
// Remember pattern
$value = Cache::remember('key', 3600, function () {
return DB::table('users')->get();
});
Queue
// Create a job
class SendEmail implements ShouldQueue
{
public function handle()
{
// Process job
}
}
// Dispatch job
SendEmail::dispatch();
// Process queue
php libxa queue:work
Testing
Tests are stored in tests/ directory.
<?php
namespace Tests\\Unit;
use Tests\\TestCase;
class ExampleTest extends TestCase
{
public function test_basic_test(): void
{
$this->assertTrue(true);
}
}
Run tests:
php libxa test
Frontend Assets
This starter uses Vite for frontend asset compilation.
JavaScript
Add your JavaScript in src/resources/js/app.js:
import './bootstrap';
console.log('LibxaFrame is ready!');
CSS
Add your styles in src/resources/css/app.css:
body {
font-family: sans-serif;
}
Building Assets
Development:
npm run dev
Production:
npm run build
Deployment
Production Checklist
- Set
APP_ENV=productionin.env - Set
APP_DEBUG=falsein.env - Generate application key:
php libxa key:generate - Run migrations:
php libxa migrate - Optimize Composer:
composer install --no-dev --optimize-autoloader - Build frontend assets:
npm run build - Set proper file permissions for
src/storageandsrc/public - Configure web server (Nginx/Apache)
The document root is src/public
Point the web server at src/public/, and send anything that is not a real
file to index.php. Both halves matter.
If the front-controller rule is missing, the home page loads and every other
route returns the web server's own 404, because Apache asked for /login looks
for a file called login and never starts PHP. / works only because
DirectoryIndex finds index.php.
That failure does not reproduce locally. php libxa serve passes
src/public/router.php to PHP's built-in server, and that shim emulates the
rewrite. A real server needs the rule written out.
Nginx Configuration
server {
listen 80;
server_name yourdomain.com;
root /var/www/your-app/src/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \\.php$ {
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
# .env holds the app key and the database password.
location ~ /\\. { deny all; }
}
The common mistake here is try_files $uri $uri/ =404;, copied from a
static-site configuration. It returns 404 for every route.
Apache Configuration
The starter kit ships src/public/.htaccess, so if your host reads .htaccess
files this already works. Two settings stop it: AllowOverride None, which
makes Apache ignore them entirely, and mod_rewrite not being enabled.
A virtual host that does not depend on .htaccess at all:
<VirtualHost *:80>
ServerName yourdomain.com
DocumentRoot /var/www/your-app/src/public
<Directory /var/www/your-app/src/public>
AllowOverride All
Require all granted
Options -MultiViews -Indexes
RewriteEngine On
RewriteCond %{HTTP:Authorization} .
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]
</Directory>
</VirtualHost>
-MultiViews is not optional. With content negotiation on, Apache answers
/about with about.php when that file exists, bypassing the router
completely. The HTTP_AUTHORIZATION line puts back a header that CGI and
FastCGI strip, without which API token authentication fails silently.
Shared hosting
When the document root is the project directory and cannot be moved, the root
.htaccess that ships with the kit rewrites requests into src/public/ and
refuses vendor/, src/app/, src/storage/ and .env. Confirm it took
effect by requesting /.env: you should get a 403 or a 404, never a download.
Subdirectories are not supported
example.com/my-app/ does not work. Routes are matched against the full
request path, so a request for /my-app/about is looked up as /my-app/about
and matches nothing. Use a subdomain, or point a virtual host at the project.
Checking it worked
curl -I https://yourdomain.com/ # 200
curl -I https://yourdomain.com/login # 200, not 404
curl -I https://yourdomain.com/.env # 403 or 404, never 200
If the second returns 404, the rewrite is not in effect.
Additional Resources
Support
For issues and questions:
- Check the documentation
- Search GitHub Issues
- Join our Discord community
License
The LibxaFrame framework is open-sourced software licensed under the MIT license.
Happy coding with LibxaFrame! 🚀
LibxaFrame
The modern, elegant, and lightning-fast PHP framework for the next generation of web applications. Built around developer happiness, performance, and scalability.
Features
- 🚀 Blazing Fast - Optimized for performance with minimal overhead
- 🧠 AI Integration - First-class AI/LLM integration out of the box
- ⚡ Async/Fibers - Native PHP 8.1+ Fiber concurrency without extensions
- 🔌 WebSockets - Real-time event-driven WebSocket ecosystem
- 🏗️ Modular Architecture - Package-based modular system
- 🎨 Elegant Syntax - Clean, expressive code following modern PHP standards
- 🛡️ Security - Built-in security features including LibxaSecure
- 📦 Package System - First-party and third-party package support
- 🗄️ ORM & Query Builder - Powerful Atlas ORM with AI-powered queries
- 🔄 Multi-Tenancy - Zero-config multi-tenancy support
- 📁 File Storage - Consistent filesystem API
- 🧵 Queue System - Asynchronous job processing
- 🌐 HTTP Client - Connection pooling for parallel requests
- 📝 Helpers - Extensive utility functions
Framework Lifecycle
The LibxaFrame request lifecycle follows this flow:
1. HTTP Request → Public/index.php
2. HttpKernel receives request
3. Middleware Pipeline executes
4. Router matches route
5. Controller/Closure executes
6. Response generated
7. Middleware Pipeline processes response
8. Response sent to client
Detailed Lifecycle
- Entry Point -
public/index.phpboots the application - Application Boot - Service providers are registered and booted
- HTTP Kernel - Handles the request through middleware
- Middleware Pipeline - Global and route-specific middleware execute
- Router Dispatch - Matches URL to controller action
- Controller Execution - Business logic runs
- Response Generation - Response object created
- Termination - Middleware cleanup and final actions
Core Features
AI Integration
LibxaFrame provides first-class AI integration with OpenAI and compatible services:
// Generate text
$response = AI::text("Write a short poem about coding.");
// Classification
$category = AI::classify("I love this framework!", ['positive', 'negative', 'neutral']);
// Embeddings
$vector = AI::embed("LibxaFrame is awesome.");
// Data extraction
$data = AI::extract("John Doe is a software engineer.", [
'name' => 'string',
'occupation' => 'string'
]);
// Natural language database queries
$result = DB::ask("total revenue from users in Paris last month");
Configuration:
OPENAI_API_KEY=your-api-key
AI_BASE_URL=https://api.openai.com/v1
ATLAS_AI_ENABLED=true
ATLAS_AI_PROVIDER=openai
ATLAS_AI_MODEL=gpt-4o-mini
Async & Fibers
True asynchronous behavior using native PHP 8.1+ Fibers:
use Libxa\\Async\\Parallel;
// Run tasks concurrently
[$users, $orders, $ads] = Parallel::run([
'users' => fn() => User::all(),
'orders' => fn() => Order::recent(),
'ads' => fn() => Http::get('https://api.ads.com/serve'),
]);
// HTTP connection pooling
use Libxa\\Http\\Client;
$responses = Client::pool([
fn() => (new Client())->get('https://api1.com/data'),
fn() => (new Client())->get('https://api2.com/data'),
fn() => (new Client())->get('https://api3.com/data'),
]);
// Fiber queue workers
Queue::fiber()
->batch($massiveArrayOfJobs)
->maxConcurrency(10)
->dispatch();
WebSockets
Event-driven WebSocket ecosystem built on Workerman:
php libxa ws:serve
WebSocket Controller:
use Libxa\\WebSockets\\WebSocketController;
class ChatController extends WebSocketController
{
public function onMessage($connection, $data)
{
$this->broadcast($data);
}
}
Modular Package System
First-class package support with automatic discovery:
php libxa package:discover
php libxa vendor:publish --provider="Vendor\\Package\\ServiceProvider"
Package Structure:
packages/
└── my-package/
├── src/
│ ├── MyServiceProvider.php
│ ├── Routes/
│ ├── Views/
│ └── Database/Migrations/
└── composer.json
Multi-Tenancy
Zero-config multi-tenancy support:
TENANCY_ENABLED=true
TENANCY_DRIVER=subdomain
// Automatic tenant resolution based on subdomain
tenant()->id; // Current tenant ID
tenant()->connection; // Tenant-specific database connection
Helpers & Utilities
Extensive helper functions:
// String helpers
str('Hello World')->slug(); // hello-world
str()->limit('Long text...', 10); // Long te...
// Array helpers
collect([1, 2, 3])->avg(); // 2
collect([1, 2, 3])->sum(); // 6
// Path helpers
app_path();
storage_path();
public_path();
// Time helpers
now()->format('Y-m-d');
now()->addDays(7);
HTTP Client
Powerful HTTP client with connection pooling:
use Libxa\\Http\\Client;
$client = new Client();
$response = $client->get('https://api.example.com/data');
$data = $response->json();
// POST request
$response = $client->post('https://api.example.com/users', [
'name' => 'John Doe',
'email' => 'john@example.com'
]);
Console Commands
Creating Commands
php libxa make:command SendEmails
<?php
namespace App\\Console\\Commands;
use Libxa\\Console\\Command;
class SendEmails extends Command
{
protected static $defaultName = 'emails:send';
protected function configure(): void
{
$this->setDescription('Send pending emails');
}
protected function execute(): int
{
// Your logic
return Command::SUCCESS;
}
}
Package Development
Creating a Package
php libxa make:package MyPackage
This creates:
packages/my-package/
├── src/
│ ├── MyPackageServiceProvider.php
│ ├── Routes/
│ ├── Views/
│ └── Database/Migrations/
└── composer.json
Package Discovery
// In MyPackageServiceProvider
class MyPackageServiceProvider extends ModuleServiceProvider
{
public function boot(): void
{
$this->loadRoutesFrom(__DIR__ . '/Routes/web.php');
$this->loadViewsFrom(__DIR__ . '/Resources/views', 'mypackage');
$this->loadMigrationsFrom(__DIR__ . '/Database/Migrations');
}
}
First-Party Packages
Three packages are maintained alongside the framework. Everything below is
running on this site — the newsletter signup form writes through
libxa/secure, the subscriber list is a LibAdmin resource, and the
Server-Timing header on this page comes from libxa/toolkit.
LibAdmin
An admin panel. Install it, point a resource at a model, and you get a table, a detail page, create and edit forms, and delete — with an allow-list you control.
composer require libxa/lib-admin
php libxa vendor:publish --tag=admin
php libxa migrate
php libxa admin:make-user "Your Name" you@example.com "a-long-password"
admin:make-user takes the password as its third argument. Without it the
command prompts, which will not work in a deploy script — pass
--no-interaction and all three arguments there.
Generate a resource and register it:
php libxa admin:make-resource Subscriber
namespace App\Admin\Resources;
use Libxa\Admin\Columns\BadgeColumn;
use Libxa\Admin\Columns\TextColumn;
use Libxa\Admin\Fields\TextInput;
use Libxa\Admin\Resources\AdminResource;
class SubscriberResource extends AdminResource
{
protected static string|null $model = \App\Models\Subscriber::class;
protected static string|null $pluralLabel = 'Subscribers';
protected static string $icon = 'mail';
protected static string $group = 'Newsletter';
public function columns(): array
{
return [
TextColumn::make('email')->sortable()->searchable()->copyable(),
BadgeColumn::make('unsubscribed_at')
->label('Status')
->formatUsing(fn ($value) => $value === null ? 'subscribed' : 'unsubscribed')
->colors(['subscribed' => 'emerald', 'unsubscribed' => 'slate']),
TextColumn::make('ip_address')->label('Signed up from'),
TextColumn::make('created_at')->dateTime('Y-m-d H:i'),
];
}
public function fields(): array
{
return [
TextInput::make('email')->required(),
TextInput::make('source'),
];
}
}
Register it in a provider, and add that provider to providers in
config/app.php:
namespace App\Providers;
use Libxa\Admin\Facades\Admin;
use Libxa\Container\ServiceProvider;
class AdminPanelProvider extends ServiceProvider
{
public function boot(): void
{
Admin::registerResources([\App\Admin\Resources\SubscriberResource::class]);
}
}
Registering a resource also puts it in the sidebar and in Quick Actions on the dashboard — do not add a navigation entry by hand as well, or the link appears twice.
columns() and fields() are two different lists, on purpose.
columns() is what can be read. It governs the table and the detail page
both, so a column you leave out is not shown anywhere.
fields() is what can be written. It is an allow-list, not a form hint: a
form that posts id, is_admin or ip_address writes none of them unless
the resource says those are editable. In the example above, ip_address and
the timestamps are visible and not writable, because they are the record of
when and from where somebody consented.
created_at and updated_at are maintained for you when the table has them.
Widgets
namespace App\Admin\Widgets;
use Libxa\Admin\Widgets\Stat;
use Libxa\Admin\Widgets\StatsOverviewWidget;
use Libxa\Atlas\DB;
class NewsletterStatsWidget extends StatsOverviewWidget
{
protected function getStats(): array
{
$rows = DB::select('SELECT COUNT(*) AS total FROM subscribers');
return [
Stat::make('Subscribers', (string) ($rows[0]['total'] ?? 0))
->description('Currently receiving the newsletter')
->icon('mail'),
];
}
}
Register with Admin::registerWidgets([...]). Count in SQL rather than
loading the table — a widget runs on every dashboard view.
Note that DB::select() returns associative arrays, while the query builder
returns stdClass.
Roles and permissions
Set them up once:
php libxa admin:sync-permissions
php libxa admin:assign-role you@example.com superadmin
admin:sync-permissions creates four roles — superadmin, admin, editor,
viewer — writes one permission per resource ability, and grants each role
what it is defined as having. Run it again after adding a resource. Re-running
only adds: a permission you revoked by hand stays revoked.
| Role | What it can do |
|---|---|
superadmin |
Everything, including resources added later |
admin |
Every ability on every synced resource, plus media and the audit trail |
editor |
View, create, update, export. No deleting, no audit trail |
viewer |
View only |
Permissions are named <resource>.<ability> — subscribers.update,
media.upload, audit.view. The resource half is the URL slug, so
/admin/resources/subscribers is governed by subscribers.* and the two
cannot drift apart.
Every resource action, the media library and the audit trail check a permission. Navigation, dashboard Quick Actions and the per-row buttons show only what the account can use — but that is presentation, not the control: each route checks for itself, because a POST does not have to come from a page the panel rendered.
Two things worth knowing before you rely on this.
An admin with no roles can do nothing — not "everything, because nobody has configured this yet". And if the tables are missing or a query fails, the answer is no. Failing open so as not to lock anyone out would turn a broken migration into an unprotected panel, silently.
superadmin is allowed everything by name rather than by holding every
permission, so a resource you add tomorrow is covered without another sync.
Manage roles from the command line:
php libxa admin:roles --permissions
php libxa admin:assign-role someone@example.com editor
php libxa admin:revoke-role someone@example.com editor
Revoking the last superadmin needs --force, since it otherwise leaves a
panel with nobody who can grant anything and no way back through the UI.
To exempt a resource from checks entirely, declare it:
protected static bool $authorize = false;
Resources are protected by default, so opting out is a decision made in the class rather than one made by forgetting.
The audit trail
Every write through the panel is recorded in audit_logs, with no wiring
needed: auth.login, auth.login_failed, auth.logout, resource.created,
resource.updated and resource.deleted, each with the acting admin, the
resource and id, the IP and user agent, and the values.
Updates keep both sides — the row as it was and the values written — and a delete keeps the whole record, which after the delete is the only copy of it anywhere.
password, remember_token, api_token and secret are stored as
[redacted]. An audit row is read by more people than the record it came
from.
Read it over the API:
GET /admin/api/audit-logs?event=resource.deleted&per_page=50
GET /admin/api/audit-logs/{id}
Filterable by event, resource_type and admin_user_id, paginated, capped
at 100 per page.
Failed logins are recorded with the attempted address and no actor — a run of them against one address is what a brute-force attempt looks like from here.
Recording never throws. A trail that can take a request down with it is one that gets switched off by the first person it inconveniences.
Plugins
Contributors can extend the panel without forking it. See the package's own README for the plugin contract.
Libxa Secure
Encryption, audit logging, and threat detection.
composer require libxa/secure
php libxa vendor:publish --tag=secure-config
php libxa migrate
Audit logging
Record actions worth being able to reconstruct later:
app('secure.audit')->record('newsletter.subscribed', [
'email' => $email,
'source' => 'website',
]);
The logger is built never to throw: an audit trail that can take the request down with it will be switched off by the first person it inconveniences. A write that fails is reported quietly rather than raised.
Encryption
$vault = app('secure.vault');
$payload = $vault->encrypt('card-token');
$plain = $vault->decrypt($payload);
AES-256-GCM, with the key id passed as additional authenticated data, so a payload cannot be re-pointed at a different key. Multiple keys are supported so you can rotate without a migration.
Libxa Toolkit
A debugger, a profiler, and code generators.
composer require libxa/toolkit
php libxa vendor:publish --tag=toolkit-config
Profiler
The profiler is a service, not automatic instrumentation — something has to start it and read it back. The usual place is a middleware:
namespace App\Http\Middleware;
use Libxa\Http\Request;
use Libxa\Http\Response;
use Libxa\Toolkit\Profiler\Profiler;
class ProfileRequest
{
public function __construct(private readonly Profiler $profiler)
{
}
public function handle(Request $request, callable $next): Response
{
if (! $this->profiler->enabled()) {
return $next($request);
}
$this->profiler->start('request');
$response = $next($request);
$this->profiler->stop('request');
$timing = $this->profiler->serverTiming();
return $timing === '' ? $response : $response->withHeader('Server-Timing', $timing);
}
}
Push it onto the global stack from a provider's boot():
$this->app->make(\Libxa\Foundation\HttpKernel::class)
->pushMiddleware(\App\Http\Middleware\ProfileRequest::class);
Timings then appear in the browser's network panel next to the request. The
profiler defaults to on in local, testing and development only, so
production costs one method call and gets no header; set TOOLKIT_PROFILER to
override that deliberately.
Measure a specific stretch with measure():
$rows = app('toolkit.profiler')->measure('subscribers.query', fn () => Subscriber::all());
Dumper
dump($value);
Depth, item count and string length are all capped, configurably, so dumping a model with a full object graph behind it does not produce a megabyte of HTML.
Performance Optimization
Query Optimization
// Eager loading
$users = User::with('posts')->get();
// Select specific columns
$users = User::select('id', 'name')->get();
// Chunking
User::chunk(100, function ($users) {
foreach ($users as $user) {
// Process
}
});