naluz / naluzphp
NaluzPHP application skeleton — clone, composer install, and start building. The framework itself is the naluz/framework package.
Requires
- php: ^8.2
- naluz/framework: ^1.1
Requires (Dev)
- phpunit/phpunit: ^11.0
- squizlabs/php_codesniffer: ^3.10
Suggests
- ext-pcntl: Lets queue:work shut down gracefully on SIGTERM/SIGINT.
- ext-pdo_sqlsrv: Microsoft SQL Server (with the Microsoft ODBC driver).
- ext-redis: Not required: NaluzPHP ships its own Redis client.
- mongodb/mongodb: MongoDB driver for NoSQL (needs ext-mongodb); the file and memory NoSQL drivers need nothing.
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-03 02:11:35 UTC
README
A modern, secure PHP framework for monolithic apps and REST APIs, with a built-in ORM and an independent query builder. Created by Mark Anthony Naluz.
// routes/api.php $router->apiResource('posts', PostController::class); // app/Http/Controllers/Api/PostController.php public function index(): Paginator { return Post::published()->with('author')->latest()->paginate(15); }
- PHP 8.2+ (developed and tested on 8.3; CI matrix covers 8.2 – 8.5),
declare(strict_types=1)everywhere - PSR-native: PSR-1, 3, 4, 6, 7, 11, 12, 13, 14, 15, 16, 17, 18 (Guzzle) and 20 — each one tested (see architecture)
- Secure by default: parameterised SQL with identifier allow-listing, mass-assignment protection, CSRF, hardened headers, Argon2id, authenticated encryption, strict sessions, rate limiting, CORS allow-list
- Batteries included, still small: ORM + query builder, compiled auto-escaping templates, queues, mail, scheduler,
file storage, Redis cache/session/queue, factories & seeders, route cache — ~11k lines, three runtime packages beyond
the PSR interfaces (
nyholm/psr7,nyholm/psr7-server,guzzlehttp/guzzle)
Quick start
git clone https://github.com/taliffsss/framework.git naluzphp && cd naluzphp composer install php naluz new my-app # scaffolds a fresh project: .env, APP_KEY, JWT_SECRET, composer install cd ../my-app php naluz migrate && php naluz run:server # http://127.0.0.1:8000 # …or work directly in the clone: cp .env.example .env && php naluz key:generate --jwt && touch storage/database.sqlite
Requirements: PHP ≥ 8.2 with pdo, mbstring, openssl, sodium (plus pdo_sqlite, pdo_mysql or pdo_pgsql).
Point your web server's document root at public/ — never at the project root.
Try it:
curl localhost:8000/api/ping curl -X POST localhost:8000/api/posts -H 'Content-Type: application/json' -d '{"title":"x"}' # 422 + errors php naluz route:list
Project layout
app/ your code (PSR-4 namespace App\) Http/Controllers, Http/Middleware, Models
bootstrap/ creates the Application
config/ app, database, auth, session, security
database/ migrations
public/ the only web-accessible directory (index.php)
resources/views plain-PHP templates with layouts
routes/ web.php (sessions + CSRF) and api.php (stateless, /api prefix, rate limited)
vendor/naluz/framework the framework itself (namespace Naluz\), installed by Composer from https://github.com/taliffsss/naluz-framework
storage/ logs, cache, sessions, sqlite file
tests/ PHPUnit suite
naluz command-line entry point
A tour
Routing & controllers
$router->get('/users/{id}', [UserController::class, 'show'])->where('id', '[0-9]+')->name('users.show'); $router->get('/files/{path?}', fn (?string $path = null) => ['path' => $path]); $router->prefix('admin')->middleware(['auth', 'throttle:30,1'])->group(function ($router) { $router->resource('users', UserController::class); });
Controllers are resolved by the container (constructor and method injection). Route parameters are cast to the
declared type (int $id); a non-numeric id is a 404 and never reaches your code. Return a PSR-7 response, an array /
model / paginator (→ JSON), a string (→ HTML) or null (→ 204).
Query builder (standalone)
$db = app(Naluz\Database\DatabaseManager::class)->connection(); $rows = $db->table('orders') ->join('users', 'users.id', '=', 'orders.user_id') ->select('users.name', 'orders.total') ->where('orders.total', '>', 100) ->whereIn('users.role', ['admin', 'staff']) ->orderBy('orders.total', 'desc') ->paginate(20, page: 2); $db->table('users')->upsert([['email' => 'a@x.io', 'name' => 'A']], uniqueBy: ['email'], update: ['name']); $db->transaction(function ($db) { // nested calls use savepoints $db->table('accounts')->where('id', 1)->decrement('balance', 50); $db->table('accounts')->where('id', 2)->increment('balance', 50); });
ORM
class Post extends Model { use SoftDeletes; protected array $fillable = ['user_id', 'title', 'body']; // mass assignment is OFF until you list fields protected array $casts = ['published' => 'bool', 'meta' => 'array', 'status' => Status::class]; public function author(): BelongsTo { return $this->belongsTo(User::class, 'user_id'); } public function tags(): BelongsToMany { return $this->belongsToMany(Tag::class); } public function scopePublished(Builder $q): void { $q->where('published', true); } } $post = Post::create(['user_id' => 1, 'title' => 'Hello', 'body' => '…']); $posts = Post::published()->with('author', 'tags')->latest()->paginate(10); // 3 queries, not 1+2N $post->tags()->sync([1, 2, 3]);
Relationships: hasOne, hasMany, belongsTo, belongsToMany (with attach/detach/sync). Plus eager loading with
constraints and nesting (with('posts.comments')), casts (int float bool string array json datetime date enum encrypted hashed),
accessors/mutators, dirty tracking, timestamps, soft deletes, query scopes, model events and chunk()/cursor().
Validation
$data = Validator::make(Request::input($request), [ 'email' => 'required|email|unique:users,email', 'items.*.qty' => 'required|integer|min:1', ], db: $connection)->validate(); // throws ValidationException → 422 JSON, or redirect-back for browsers
Authentication
Session guard (Naluz\Auth\Auth, middleware auth) for web apps; stateless HS256 JWT (Naluz\Security\Jwt,
middleware jwt) for APIs.
Templates (resources/views/*.naluz.php)
@extends('layouts/app')
@section('content')
<h1>{{ $post->title }}</h1> {{-- escaped automatically; {!! $html !!} for raw --}}
@forelse ($post->comments as $c) <p>{{ $c->body }}</p> @empty <p>No comments.</p> @endforelse
<form method="POST">@csrf @method('PUT') …</form>
@endsection
Compiled to plain PHP and cached. See docs/templates.md.
Queues, mail, scheduler, storage
SendWelcomeEmail::dispatch($user->id); // queue (sync | database | redis), encrypted payloads, retries $mailer->queue($mailer->message()->to($email)->subject('Hi')->text('…')); // SMTP / log / array transports $schedule->job(new PruneOldRecords())->dailyAt('03:00')->withoutOverlapping(); // routes/console.php + one cron line $path = Uploads::store($file, storage('public'), 'avatars', Uploads::IMAGES); // content-sniffed, random name, allow-list required
Model caching — one .env flag
MODEL_CACHING=true MODEL_CACHE_DRIVER=redis # or local
Every model query is cached for 5 minutes and, when data is created, updated or deleted, the affected entries are
invalidated and re-cached automatically (including query-builder writes, pivots, soft deletes, cascades, transactions).
Falls back to the database if the cache store is down. Per-model opt-out with protected bool $cache = false;. See docs/model-cache.md.
Databases: SQLite, MySQL, PostgreSQL, SQL Server, read/write replica splitting, and NoSQL document stores (file, memory, MongoDB) — see docs/database.md and docs/nosql.md.
Factories & seeders
Post::factory(5)->published()->create(['user_id' => $user->id]); php naluz migrate:fresh --seed
CLI
| Command | |
|---|---|
new <name> |
scaffold a new project (--name=vendor/pkg --no-install --dir=…) |
run:server [--port=8001 --host --workers=N] |
development server (default http://127.0.0.1:8000) |
key:generate [--jwt] [--show] |
create APP_KEY / JWT_SECRET |
migrate · migrate:rollback [--step=N] · migrate:status · migrate:fresh [--seed] · db:seed |
database |
make:controller / model / middleware / migration / factory / seeder / job / provider Name |
generators |
queue:work · queue:failed · queue:retry <id|all> · queue:flush |
queues |
messaging:consume <topic> · messaging:declare · messaging:publish · make:subscriber |
optional event streaming (Redis Streams / RabbitMQ / Kafka) |
schedule:run · schedule:list |
scheduler |
route:list · route:cache · route:clear · view:clear · model-cache:flush · model-cache:prune |
routing & caches |
Security defaults
| Threat | Built-in protection |
|---|---|
| SQL injection | Prepared statements with real server-side binding (ATTR_EMULATE_PREPARES=false); column/table names that are not plain identifiers, unknown operators and bad sort directions throw instead of being escaped; Connection::raw() is the explicit escape hatch; bound values never appear in exception messages |
| Mass assignment | $fillable is empty by default; fill() / create() silently drop everything else; validators return only the fields that have rules |
| CSRF | Synchroniser token in every web route + Origin check; hash_equals comparison |
| XSS | e() / $this->e() in views; JSON responses are encoded with JSON_HEX_*; error pages escape messages; hardened CSP default |
| Session attacks | HttpOnly + SameSite=Lax (+ Secure on HTTPS) cookies, 160-bit IDs, unknown client IDs are discarded (no fixation), ID regeneration on login, JSON (not unserialize) storage |
| Passwords | Argon2id (bcrypt fallback), transparent rehash, constant-time-ish login path that hashes even for unknown users, 4 KB input cap |
| Encryption | XChaCha20-Poly1305 (libsodium) with key rotation; encrypted model cast |
| Tokens | JWT with pinned algorithm (alg:none rejected), hash_equals, exp/nbf/iss checks, ≥32-byte secret enforced |
| Clickjacking / sniffing / referrer leaks | X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, COOP, HSTS on HTTPS |
| Brute force / abuse | throttle:max,minutes middleware (api group: 60/min) |
| CORS | Explicit origin allow-list, never reflects arbitrary origins, never * with credentials |
| Open redirect / header injection | validation redirects only to same-host paths; redirect() rejects CR/LF |
| Info leaks | debug=false unless enabled; production errors are generic and logged; X-Powered-By removed; log-forging newlines stripped |
| Unsafe deserialisation | file cache uses allowed_classes => false |
| Path traversal | strict view-name, session-id, cache-key and generator-name allow-lists; storage disks reject ../symlink escapes; cache files are hashed |
| Queue / job tampering | payloads are encrypted JSON (no unserialize) and must decode to a Job subclass |
| Mail header injection | CR/LF/NUL in addresses, subjects, headers → exception before sending |
| Malicious uploads | content-sniffed type, mandatory allow-list, extension from your list, random file name |
| N+1 queries | lazy-load guard throws in local/testing |
See docs/security.md for details and what the framework deliberately does not do for you.
Testing
composer install
vendor/bin/phpunit # ~300 tests: unit, database, HTTP, security, console, queue, mail, storage, schedule
Tests run against in-memory SQLite and boot the real application, so HTTP tests exercise the whole middleware stack;
Redis tests start a throw-away redis-server (skipped if it isn't installed) and the mail tests talk to a scripted fake SMTP server.
See docs/testing.md.
Documentation
The documentation lives in its own repository and is published as a searchable site:
https://taliffsss.github.io/naluz-framework-docs/ (source: naluz-framework-docs).
- Installation · Getting started · Configuration
- Routing · Controllers · Middleware · Validation
- Database · Models · Model caching · NoSQL
- Security · Queues · GraphQL · CLI · Deployment
License
MIT © Mark Anthony Naluz