Skip to content
LibxaFrame
Sign In

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=production in .env
  • Set APP_DEBUG=false in .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/storage and src/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:

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

  1. Entry Point - public/index.php boots the application
  2. Application Boot - Service providers are registered and booted
  3. HTTP Kernel - Handles the request through middleware
  4. Middleware Pipeline - Global and route-specific middleware execute
  5. Router Dispatch - Matches URL to controller action
  6. Controller Execution - Business logic runs
  7. Response Generation - Response object created
  8. 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
    }
});

Something missing or wrong?

Edit on GitHub