mawsis / nebula-php
A PHP 8 MVC framework built from scratch: DI container, router, ORM, migrations, middleware, validation, and JWT auth
Requires
- php: >=8.0
- firebase/php-jwt: ^6.11
- monolog/monolog: ^3.8
- vlucas/phpdotenv: ^5.6
README
Nebula is a PHP 8 MVC framework written from scratch to understand how modern frameworks actually work under the hood (dependency injection, routing, an ORM, middleware, validation) without hiding any of it behind magic. The entire core is ~2,600 lines across 50 focused classes with only three runtime dependencies (dotenv, Monolog, firebase/php-jwt), so it is small enough to read in an afternoon and real enough to build a JSON API or a server-rendered app with. It is installable via Composer as mawsis/nebula-php.
Features
- DI container:
bind/singleton/instance/alias, with reflection-based auto-wiring for constructors, controller methods, and route closures - Router: static and
{param}routes, closure or[Controller::class, 'method']callbacks, per-route middleware - ORM: active-record style
DbModel(findOne,create,save) on top of a fluentQueryBuilder(where / joins / groupBy / having / orderBy / limit / offset, all with bound parameters), plushasOne/hasMany/belongsTorelationships and eager loading viawith() - Migrations: file-based migrations with an applied-migrations table and rollback support
- HTTP layer:
Requestwith input sanitization, JSON body parsing, and helpers (input,only,except,bearerToken);Responsewith a consistent JSON envelope andsuccess/error/validationErrorhelpers - Validation: Laravel-style form requests: declare
rules()on aRequestsubclass, use string rules ('min:8','unique:users:email') or rule objects, callvalidated() - Auth: session-based authentication with a configurable user model, plus stateless JWT (HS256) via
JwtHelper - Middleware: Auth, JWT, CORS, CSRF, and JSON middleware included; custom middleware is a single
execute()method - Error handling: typed exceptions (
NotFoundException,UnauthorizedException,ForbiddenException,ValidationException, ...) mapped by a central handler to JSON for API requests or rendered error views - Extras: facades (
Route,DB,Auth,Session,Logger), aTransformlayer for shaping API resources, pagination, flash messages, Monolog logging, dotenv config
Requirements
- PHP >= 8.0
- Composer
Quick Start
Scaffold a new application with the Nebula Installer:
composer global require mawsis/nebula-installer nebula new my-app
Or pull the core into an existing project:
composer require mawsis/nebula-php
A minimal front controller looks like this:
// public/index.php require_once __DIR__ . '/../vendor/autoload.php'; Dotenv\Dotenv::createImmutable(dirname(__DIR__))->safeLoad(); Nebula\Core\Config::load(dirname(__DIR__) . '/config'); $app = new Nebula\Core\Application(dirname(__DIR__)); require_once __DIR__ . '/../routes/main.php'; $app->run();
See Nebula-Example for a complete application skeleton (config, providers, routes, controllers, migrations, views, Docker setup).
Usage
Routing
Routes take a closure or a [Controller::class, 'method'] pair, plus an optional list of middleware aliases:
use Nebula\Core\Facades\Route; use App\Controllers\AuthController; use App\Controllers\UserController; Route::get('/users', [UserController::class, 'listUsers'], ['auth']); Route::get('/users/{id}', [UserController::class, 'showUser'], ['auth']); Route::post('/login', [AuthController::class, 'loginStore']);
Controller methods are resolved through the container, so dependencies are injected by type hint:
public function listUsers(Request $request, Response $response) { $users = Paginator::paginate(User::query()->orderBy('id', 'ASC'), $response); return $response->json(['users' => $users]); }
Models and queries
A model declares its table, attributes, and primary key; the base class does the rest:
namespace App\Models; use Nebula\Core\DbModel; class Post extends DbModel { public static function tableName(): string { return 'posts'; } public static function primaryKey(): string { return 'id'; } public static function attributes(): array { return ['title', 'body', 'user_id']; } public function user() { return $this->belongsTo(User::class, 'user_id'); } }
$post = Post::findOne(['id' => 42]); $posts = Post::query() ->where('user_id', '=', $userId) ->orderBy('created_at', 'DESC') ->limit(10) ->get(); $posts = Post::with(['user'])->where('title', 'LIKE', '%nebula%')->get(); // eager loading Post::create(['title' => 'Hello', 'body' => '...', 'user_id' => 1]);
Validation
Declare rules on a Request subclass. String rules are resolved through config/validations.php, and anything after : is passed to the rule's constructor; you can also pass rule objects or write your own by extending BaseValidation:
namespace App\Requests; use Nebula\Core\Request; class RegisterRequest extends Request { public function rules(): array { return [ 'username' => ['required', 'min:3', 'max:20'], 'email' => ['required', 'email', 'unique:users:email'], 'password' => ['required', 'min:8'], ]; } }
Type-hint the request in a controller action and call validated(); on failure it throws a ValidationException, which the central handler turns into a 422 JSON response with per-field errors:
public function registerStore(RegisterRequest $request, Response $response) { $data = $request->validated(); User::create([ 'username' => $data['username'], 'email' => $data['email'], 'password' => password_hash($data['password'], PASSWORD_DEFAULT), ]); return $response->success(['registered' => true], 201); }
Middleware
Middleware aliases are defined once in config/middlewares.php and referenced by name in routes:
return [ 'auth' => Nebula\Core\Middlewares\AuthMiddleware::class, 'jwt' => Nebula\Core\Middlewares\JwtMiddleware::class, 'csrf' => Nebula\Core\Middlewares\CsrfMiddleware::class, 'cors' => Nebula\Core\Middlewares\CorsMiddleware::class, 'json' => Nebula\Core\Middlewares\JsonMiddleware::class, ];
A custom middleware is one class with one method; it halts the request by throwing a typed exception:
use Nebula\Core\Facades\Auth; use Nebula\Core\Middlewares\BaseMiddleware; use Nebula\Core\Exceptions\ForbiddenException; class AdminMiddleware extends BaseMiddleware { public function execute() { if (Auth::isGuest() || !Auth::user()->is_admin) { throw new ForbiddenException(); } } }
Design
Everything in Nebula is deliberately hand-rolled to expose the mechanics that larger frameworks abstract away:
- Container (
Container): a static service container withbind/singleton/instance/alias. Auto-wiring uses reflection to resolve constructor, controller-method, and closure dependencies, with sensible fallbacks for defaults and nullable parameters. - Router (
Router+Routefacade): routes are stored per HTTP method;{param}segments are compiled to regexes at match time. Route middleware is resolved from config aliases (or passed as instances) and executed before the action. - ORM (
DbModel+QueryBuilder): models describe their schema in three static methods; the query builder assembles parameterized SQL and hydrates results back into model instances. Relationships are plain methods built on the same query builder, andwith()eager-loads them onto results. - HTTP + errors (
Request,Response,Handler): requests sanitize input and parse JSON bodies; responses emit a consistent{success, status_code, data | error}envelope. A central handler maps typed exceptions to JSON for API requests or rendered error views, and fatal errors get a dedicated error page. - Validation: rule classes implement a two-method
BaseValidationcontract (validate,getErrorMessage); string rules map to classes through config, so applications can register their own rules the same way the built-ins work. - Auth: a session-backed
Authservice (login,logout,user,isGuest) with the user model class supplied via config, plus JWT issuance/verification for stateless APIs. - Facades: a ~10-line
__callStaticbase class that proxies to container-resolved services, which is all a facade actually is.
Ecosystem
| Repository | Description |
|---|---|
| Nebula-Installer | nebula new CLI for scaffolding new applications |
| Nebula-Example | Full example application built on the framework |
License
Released under the MIT License.