spark-php / framework
Spark — a lightweight, Laravel-inspired PHP framework with routing, ORM, templating, and CLI
Requires
- php: ^8.1
- ext-json: *
- ext-mbstring: *
- ext-pdo: *
Requires (Dev)
- phpunit/phpunit: ^10.5 || ^11.0
README
A lightweight, Laravel-inspired PHP framework. No Symfony dependencies, no heavy abstractions — just routing, ORM, templating, middleware, DI, migrations, and a CLI in clean PHP 8.1+ code.
Quick Start
composer create-project spark-php/skeleton my-app
cd my-app
php spark serve
Open http://localhost:8000.
No extra configuration needed — the app key is generated automatically and SQLite is configured out of the box.
Requirements
- PHP 8.1+
- Extensions:
pdo,mbstring,json - No other dependencies
Routing
routes/web.php:
$router->get('/', [HomeController::class, 'index']); $router->get('/users/{id}', [UserController::class, 'show']); $router->get('/posts/{slug?}', [PostController::class, 'show']); // optional param $router->post('/users', [UserController::class, 'store']) ->middleware(App\Middleware\Auth::class); $router->group(['prefix' => 'admin', 'middleware' => [Auth::class]], function ($router) { $router->get('/dashboard', [DashboardController::class, 'index']); }); $router->get('/ping', fn() => ['pong' => true]); // API routes (routes/api.php) — no CSRF, prefixed /api $router->get('/users', [UserController::class, 'index'])->name('api.users'); // Webhook — opt out of CSRF individually $router->post('/webhook/github', [WebhookController::class, 'handle'])->withoutCsrf();
Route params are injected into controller methods by name.
Controllers
namespace App\Controllers; use Spark\Http\Request; use Spark\Http\Response; class UserController { public function show(Request $request, string $id): Response { $user = User::find($id); return json($user); } public function store(Request $request): Response { $user = User::create($request->only(['name', 'email'])); return json($user, 201); } }
Return a Response, an array/object (auto-JSON), or a string (HTML).
Models (ORM)
namespace App\Models; use Spark\Database\Model; class User extends Model { protected static array $fillable = ['name', 'email']; protected static bool $timestamps = true; } // CRUD $user = User::create(['name' => 'Ada', 'email' => 'ada@x.com']); $all = User::all(); $one = User::find(1); $adults = User::where('age', '>', 18)->orderBy('name')->limit(10)->get(); $user->update(['name' => 'Ada Lovelace']); $user->delete();
Relationships: hasOne, hasMany, belongsTo.
Migrations
use Spark\Database\{Migration, Schema, Blueprint}; return new class extends Migration { public function up(): void { Schema::create('posts', function (Blueprint $t) { $t->id(); $t->string('title'); $t->text('body'); $t->foreignId('user_id'); $t->timestamps(); }); } public function down(): void { Schema::dropIfExists('posts'); } };
Supports SQLite, MySQL, and PostgreSQL.
php spark migrate php spark migrate:rollback
Blade-lite Views (.spark.php)
@extends('layout') @section('title', 'Home') @section('content') <h1>{{ $title }}</h1> @foreach($items as $item) <li>{{ $item }}</li> @endforeach @if($user) Welcome, {{ $user->name }} @endif @endsection
Templates compile once to plain PHP in storage/cache/views/. Subsequent requests use the cached version.
Directives: @extends @section @endsection @yield @include @if @elseif @else @endif @foreach @endforeach @for @while @isset @empty @unless @php @endphp
Echo: {{ $var }} (HTML-escaped), {!! $html !!} (raw)
Middleware
namespace App\Middleware; use Closure; use Spark\Http\Request; use Spark\Http\Response; class Auth { public function handle(Request $request, Closure $next): Response { if (!$request->header('authorization')) { abort(401, 'Token required'); } return $next($request); } }
Built-in: StartSession, VerifyCsrfToken, Cors, ForceHttps.
Service Providers
namespace App\Providers; use Spark\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { public function register(): void { $this->app->singleton(PaymentGateway::class, fn() => new StripeGateway( config('services.stripe.key') )); } public function boot(): void { // Runs after all providers are registered } }
Register in bootstrap/app.php:
$app->register(\App\Providers\AppServiceProvider::class);
Dependency Injection
Constructor-inject any class; the container resolves dependencies automatically:
class ReportController { public function __construct(private UserRepository $repo) {} public function index(): Response { return json($this->repo->all()); } }
CLI
php spark serve # dev server (localhost:8000) php spark make:controller Foo # scaffold controller php spark make:model Foo php spark make:middleware Foo php spark make:migration create_foo_table php spark migrate php spark migrate:rollback php spark route:list php spark key:generate
Security defaults
Every response gets these headers automatically:
| Header | Value |
|---|---|
X-Content-Type-Options |
nosniff |
X-Frame-Options |
SAMEORIGIN |
Referrer-Policy |
strict-origin-when-cross-origin |
Content-Security-Policy |
strict + nonce for inline scripts/styles |
Strict-Transport-Security |
set on HTTPS |
Permissions-Policy |
geolocation=(), microphone=(), camera=() |
CSRF tokens, secure session cookies, SQL prepared statements, mass-assignment protection, and open-redirect prevention are all on by default.
Logging
Spark includes a built-in file logger. Logs are written to storage/logs/spark.log.
// Log at info level logger('User registered', ['user_id' => $user->id]); // Get the Logger instance for any level logger()->debug('Cache miss', ['key' => $cacheKey]); logger()->warning('Slow query detected', ['ms' => 850]); logger()->error('Payment failed', ['order' => $orderId]);
Log format:
[2026-04-17 10:30:00] INFO: User registered {"user_id":42}
[2026-04-17 10:30:01] ERROR: Payment failed {"order":99}
The minimum log level is controlled by LOG_LEVEL in your .env file (default: debug).
Valid levels: debug, info, warning, error.
You can also inject the logger via the container:
use Spark\Support\Logger; class OrderController { public function __construct(private Logger $logger) {} public function store(Request $request): Response { // ... $this->logger->info('Order created', ['id' => $order->id]); return json($order, 201); } }
Helpers
app(), config(), env(), base_path(), storage_path(), view(), json(), response(), redirect(), abort(), csrf_token(), csrf_field(), bcrypt(), csp_nonce(), e(), dd(), logger()
License
MIT — Copyright (c) 2026 Pawan More