Overview

Zieex Framework

A lightweight, AJAX-first PHP framework built for shared hosting โ€” with MVC, REST API, authentication, and a zero-dependency frontend layer.

โšก
Zero Dependencies
Raw PHP, raw SMTP sockets. No bloat, no composer bundles beyond your own.
๐Ÿ—
MVC + REST
Full Model-View-Controller with first-class resource API routing.
๐Ÿ”„
AJAX-First
Every link and form works without page reloads via Interaction.js.
๐Ÿ”’
Secure by Default
Auto CSRF on all POST, bcrypt auth, JWT, rate limiting built in.
๐Ÿ—„
Multi-Database
MySQL, PostgreSQL, SQLite, MongoDB, Firebase โ€” swap via .env.
๐Ÿ“ง
Flexible Mail
SMTP, Resend, PHP mail(), or API โ€” mix drivers per use-case.

Requirements

Dependency Version Notes
PHP 8.2+ Older versions are not supported
Composer Latest For autoloading & packages
MySQL / PostgreSQL / SQLite Any MySQL is the default
Apache mod_rewrite โ€” Required for clean URLs

Installation

Web Installer (Recommended)

Best for shared hosting โ€” no terminal access needed.

  1. Upload all framework files to your public_html
  2. Visit your domain โ€” the installer launches automatically
  3. Fill in app name, database credentials, and admin account on one page
  4. Done โ€” the installer locks itself after setup

CLI Installer

bash
composer install
cp .env.example .env
php core/CLI/zx key:generate
php core/CLI/zx migrate

.env Setup

.env
APP_NAME="My App"
APP_ENV=local
APP_KEY=
APP_URL=http://localhost

DB_DRIVER=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=myapp
DB_USERNAME=root
DB_PASSWORD=

Directory Structure

tree
โ”œโ”€โ”€ app/
โ”‚   โ”œโ”€โ”€ Controllers/        # Your controllers
โ”‚   โ”œโ”€โ”€ Models/             # Your models
โ”‚   โ””โ”€โ”€ Middleware/         # Custom middleware
โ”œโ”€โ”€ core/                   # Framework internals (do not edit)
โ”‚   โ”œโ”€โ”€ Router/
โ”‚   โ”œโ”€โ”€ Http/
โ”‚   โ”œโ”€โ”€ Auth/
โ”‚   โ”œโ”€โ”€ Database/
โ”‚   โ”œโ”€โ”€ Template/
โ”‚   โ”œโ”€โ”€ Mail/
โ”‚   โ”œโ”€โ”€ Cache/
โ”‚   โ”œโ”€โ”€ CLI/zx
โ”‚   โ””โ”€โ”€ helpers.php
โ”œโ”€โ”€ config/                 # app, database, mail, middleware
โ”œโ”€โ”€ routes/
โ”‚   โ”œโ”€โ”€ web/index.php       # Web routes
โ”‚   โ””โ”€โ”€ api/index.php       # API & mobile routes
โ”œโ”€โ”€ resources/views/        # .ze.php templates
โ”‚   โ”œโ”€โ”€ layouts/
โ”‚   โ””โ”€โ”€ errors/
โ”œโ”€โ”€ storage/                # Logs, cache, sessions
โ”œโ”€โ”€ public/assets/          # JS, CSS
โ”œโ”€โ”€ install/                # Web installer (auto-locks after use)
โ””โ”€โ”€ index.php               # Entry point

Configuration

All config files live in config/ and are accessible via the config() helper.

php
config('app.name');       // App name from config/app.php
config('database.driver'); // DB driver from config/database.php
config('mail.driver');     // Mail driver from config/mail.php

env('APP_ENV', 'local');   // Read directly from .env

Routing

Basic Routes

routes/web/index.php
Router::get('/', [HomeController::class, 'index']);
Router::post('/contact', [ContactController::class, 'store']);
Router::put('/posts/{id}', [PostController::class, 'update']);
Router::delete('/posts/{id}', [PostController::class, 'destroy']);
Router::any('/webhook', [WebhookController::class, 'handle']);

Route Groups

php
Router::group(['prefix' => '/admin', 'middleware' => ['auth', 'role:admin']], function () {
    Router::get('/users', [UserController::class, 'index']);
    Router::get('/settings', [SettingsController::class, 'index']);
});

// JWT-protected API group
Router::group(['prefix' => '/api', 'middleware' => ['jwt']], function () {
    Router::resource('/posts', PostController::class);
});

Resource Routes

A single Router::resource() registers 5 routes automatically:

php
Router::resource('/api/posts', PostController::class);

// Registers:
// GET    /api/posts          โ†’ index()
// GET    /api/posts/{id}     โ†’ show()
// POST   /api/posts          โ†’ store()
// PUT    /api/posts/{id}     โ†’ update()
// DELETE /api/posts/{id}     โ†’ destroy()

Rate Limiting & Named Routes

php
Router::post('/login', [AuthController::class, 'login'])
    ->rateLimit(5, 60)         // 5 attempts per 60 seconds
    ->name('auth.login');      // Named route

Controllers

app/Controllers/PostController.php
<?php
declare(strict_types=1);

namespace App\Controllers;

use Zieex\Http\Controller;
use Zieex\Http\Request;
use Zieex\Http\Response;
use App\Models\Post;

class PostController extends Controller
{
    public function index(Request $request): Response
    {
        $posts = Post::all();
        return $this->view('posts.index', compact('posts'));
    }

    public function store(Request $request): Response
    {
        $data = $this->validate($request, [
            'title'   => 'required|max:255',
            'content' => 'required',
            'status'  => 'required|in:draft,published',
        ]);

        $post = Post::create($data);
        return $this->success($post, 'Post created.', 201);
    }
}

Available Controller Methods

Method Description
$this->view('name', $data) Render a .ze.php template
$this->validate($request, $rules) Validate request and return typed data
$this->success($data, $msg, $code) Return a consistent JSON success response
$this->error($msg, $code) Return a JSON error response
$this->redirect('/path') HTTP redirect

Models & ORM

app/Models/Post.php
<?php
declare(strict_types=1);

namespace App\Models;

use Zieex\Database\Model;

class Post extends Model
{
    protected static string $table = 'posts';
}

Model Methods

php
// Fetch
Post::all();
Post::find(1);
Post::where('status', 'published')->get();
Post::where('role', 'admin')->orderBy('created_at', 'DESC')->limit(10)->get();
Post::where('category', 'news')->paginate(15, $page);

// Write
Post::create(['title' => 'Hello', 'content' => 'World']);
Post::update(['id' => 1], ['title' => 'Updated']);
Post::delete(['id' => 1]);

// Existence check
Post::where('slug', $slug)->exists();
Post::count();

Templates (.ze.php)

Zieex uses a Blade-inspired template engine. Files use the .ze.php extension and are cached automatically in production.

Layout

resources/views/layouts/app.ze.php
<!DOCTYPE html>
<html>
<head>
  <title>{{ $title ?? config('app.name') }}</title>
</head>
<body>
  @flash('success')
  @flash('error')
  @yield('content')
</body>
</html>

Child View

resources/views/posts/index.ze.php
@extends('layouts.app')

@section('content')
  <h1>{{ $title }}</h1>

  @foreach($posts as $post)
    <article>
      <h2>{{ $post['title'] }}</h2>
      <p>{!! $post['html_content'] !!}</p>
    </article>
  @endforeach

  @auth
    <a href='/posts/create'>New Post</a>
  @endauth

  @guest
    <a href='/login'>Login to post</a>
  @endguest
@endsection

Template Directives

Directive Description
{{ $var }} Echo escaped output
{!! $var !!} Echo raw/unescaped HTML
@extends('layout') Extend a layout
@section('name') โ€ฆ @endsection Define a section
@yield('name') Output a section in the layout
@include('partial') Include another view
@if / @elseif / @else / @endif Conditionals
@foreach / @endforeach Loop over arrays
@auth / @endauth Render if user is logged in
@guest / @endguest Render if user is a guest
@csrf Insert CSRF hidden field
@method('PUT') Spoof HTTP method
@flash('success') Show flash message
@dd($var) Dump and die
@php โ€ฆ @endphp Raw PHP block

Middleware

Middleware intercepts the request before it reaches your controller. Apply to individual routes or groups.

Built-in Middleware

Key Class Description
auth AuthMiddleware Requires session login
jwt JwtMiddleware Requires valid JWT Bearer token
csrf CsrfMiddleware Validates CSRF token on POST (auto-applied)
role:admin RoleMiddleware Requires user to have a role
throttle ThrottleMiddleware Rate limiting

Custom Middleware

app/Middleware/VerifySubscription.php
class VerifySubscription
{
    public function __construct(private ?string $params = null) {}

    public function handle(Request $request, callable $next): Response
    {
        $user = Auth::user();

        if (!$user || $user['subscribed'] !== 1) {
            return redirect('/upgrade');
        }

        return $next();
    }
}

Register in config/middleware.php:

config/middleware.php
return [
    'auth'         => \Zieex\Middleware\AuthMiddleware::class,
    'jwt'          => \Zieex\Middleware\JwtMiddleware::class,
    'subscribed'   => \App\Middleware\VerifySubscription::class,
];

Validation

Zieex validation returns typed, sanitized data. Failed validation throws a ValidationException which is caught automatically by the framework.

php
$data = $this->validate($request, [
    'name'     => 'required|string|max:255',
    'email'    => 'required|email|unique:users,email',
    'age'      => 'required|integer|min:18',
    'password' => 'required|min:8|confirmed',
    'role'     => 'required|in:admin,editor,viewer',
    'website'  => 'nullable|url',
    'bio'      => 'nullable|string|max:1000',
    'tags'     => 'nullable|array',
]);
// $data is typed: age is (int), confirmed passwords pass, etc.

Validation Rules

Rule Description
required Field must be present and non-empty
string Must be a string
email Valid email format
integer Must be an integer (cast automatically)
numeric Must be numeric
boolean Must be boolean (cast automatically)
url Valid URL
date Parseable date string
min:N Minimum length or value
max:N Maximum length or value
in:a,b,c Value must be in list
not_in:a,b Value must not be in list
confirmed Must match field_confirmation
unique:table,col Must not exist in DB
exists:table,col Must exist in DB
regex:/pattern/ Match a regex
json Valid JSON string
array Must be an array (or valid JSON array)
nullable Allow null / empty

Authentication

Session Auth

php
use Zieex\Auth\Auth;

// Login
$success = Auth::attempt($email, $password); // returns bool

// Check status
Auth::check();   // true if logged in
Auth::guest();   // true if NOT logged in
Auth::user();    // returns user array or null
Auth::id();      // returns user ID or null

// Logout
Auth::logout();

// Password utilities
$hash   = Auth::hash($password);          // bcrypt
$valid  = Auth::verify($password, $hash); // verify

JWT Auth

php
use Zieex\Auth\JWT;

// Generate a token
$token = JWT::encode(['user_id' => $user['id'], 'role' => $user['role']]);

// Decode and verify
$payload = JWT::decode($token); // throws on invalid/expired

// Access payload in controller
$payload = $request->jwtPayload();
$userId  = $payload['user_id'];

Database & Query Builder

Raw Query Builder

php
use Zieex\Database\DB;

// Select
DB::table('users')->where('role', 'admin')->get();
DB::table('users')->where('active', 1)->orderBy('name')->limit(20)->get();
DB::table('users')->select(['id', 'name', 'email'])->where('role', 'editor')->get();

// Pagination
$result = DB::table('posts')->paginate(15, $currentPage);
// Returns: ['data' => [...], 'total' => N, 'per_page' => 15, ...]

// Transactions
DB::transaction(function () {
    DB::table('orders')->insert([...]);
    DB::table('inventory')->where('id', $itemId)->update([...]);
});

Migrations

database/migrations/create_posts_table.php
DB::schema()->create('posts', function ($table) {
    $table->uuid('id')->primary();
    $table->string('title', 255);
    $table->text('content');
    $table->enum('status', ['draft', 'published'])->default('draft');
    $table->uuid('user_id');
    $table->timestamps(); // created_at, updated_at
});
UUID by Default โ€” The users table ships with UUID primary keys. Use uuid() in your migrations for consistency, and the uuid() helper to generate IDs.

Mail

Zieex supports multiple mail drivers simultaneously. Use the right driver for each context.

php
use Zieex\Mail\Mail;

// Transactional - Resend API
Mail::driver('resend')
    ->to('user@example.com')
    ->subject('Your OTP Code')
    ->view('emails.otp', ['code' => $code])
    ->send();

// Bulk - SMTP
Mail::driver('smtp')
    ->from('newsletter@myapp.com', 'My App')
    ->to('subscriber@example.com')
    ->subject('Monthly Update')
    ->html('<h1>Hello!</h1>')
    ->cc('manager@myapp.com')
    ->send();

Mail Drivers

Driver Best for Config key
smtp Bulk, newsletters MAIL_SMTP_*
resend Auth emails, OTPs RESEND_API_KEY
api Notifications MAIL_API_*
mail Transactional (simple) โ€”

Events

php
use Zieex\Event;

// Register listeners
Event::on('user.registered', function ($user) {
    Mail::driver('resend')
        ->to($user['email'])
        ->subject('Welcome!')
        ->view('emails.welcome', ['user' => $user])
        ->send();
});

// Emit events
Event::emit('user.registered', $user);
Event::emit('order.placed', $order);

Logging

Logs are written to storage/logs/. In production, errors are never exposed to the browser โ€” only logged.

php
use Zieex\Log;

Log::info('User logged in', ['user_id' => $user['id']]);
Log::warning('Rate limit approaching', ['ip' => $ip, 'attempts' => $count]);
Log::error('Payment failed', ['order_id' => $orderId]);
Log::debug('Query executed', ['sql' => $sql]);

// Stored in: storage/logs/YYYY-MM-DD.log

Cache

File-based cache stored in storage/cache/. Disabled automatically in local environment.

php
use Zieex\Cache\Cache;

// Store for 600 seconds
Cache::set('user:42', $userData, 600);

// Retrieve
$user = Cache::get('user:42'); // null if expired

// Remember pattern (fetch + cache in one)
$posts = Cache::remember('posts:all', 300, fn() => Post::all());

// Delete / flush
Cache::delete('user:42');
Cache::flush();

Helpers

Global helper functions available everywhere in your application.

Helper Returns Description
env('KEY', 'default') mixed Read .env variable
config('app.name') mixed Read config value
view('home', $data) Response Render a template
redirect('/url') Response HTTP redirect
response() Response New response object
base_path('storage') string Absolute path from root
storage_path('logs') string Path inside storage/
asset('css/app.css') string Full URL to public asset
url('/posts') string Full URL for a path
uuid() string Generate a UUID v4
now('Y-m-d') string Current datetime
flash('success', 'Done!') void Set a flash message
flash_get('success') ?string Read & clear flash
old('email') mixed Re-populate form field
csrf_token() string Current CSRF token
csrf_field() string Hidden CSRF input HTML
back() Response Redirect to previous URL
abort(404) never Abort with error page
dd($var) never Dump variable and exit
dump($var) void Dump variable, continue
app() Application Application instance
cache() Cache Cache instance
auth() Auth Auth instance

Interaction.js

Zieex intercepts all links, forms, and buttons by default โ€” loading responses without a page reload, like Inertia or HTMX, but built from scratch with zero dependencies.

Default Behavior

html
<!-- Links: AJAX by default -->
<a href="/dashboard">Dashboard</a>

<!-- Opt out: full page load -->
<a href="/download" data-no-ajax>Download File</a>

<!-- Forms: AJAX submit by default -->
<form method="POST" action="/posts">
  @csrf
  <input name="title" />
  <button>Submit</button>
</form>

Button Actions

html
<!-- DELETE with confirm dialog -->
<button
  data-action="/posts/1"
  data-method="DELETE"
  data-confirm="Delete this post?">
  Delete
</button>

<!-- Target a specific element to update -->
<form data-target="#results" method="POST" action="/search">
  @csrf
  <input name="q" placeholder="Search..." />
  <button>Search</button>
</form>
<div id="results"></div>

Interaction.js Attributes

Attribute Element Description
data-no-ajax a, form Force full page load
data-action="/url" button URL to POST/DELETE to
data-method="DELETE" button Override HTTP method
data-confirm="msg" button, a Show confirmation dialog
data-target="#el" form Update this element with response
data-loading="..." button Label during request
data-on="event:method" any LiveComponent event binding

LiveComponent

Register reactive client-side components with local state, similar to Alpine.js but built into Zieex.

javascript
LiveComponent.register("counter", {
  data: () => ({ count: 0, step: 1 }),
  methods: {
    increment() { this.count += this.step; },
    decrement() { this.count -= this.step; },
    reset()     { this.count = 0; },
  },
  render() {
    return `
      <p>${this.count}</p>
      <button data-on="click:decrement">โˆ’</button>
      <button data-on="click:increment">+</button>
    `;
  }
});
html
<!-- Mount a component -->
<div data-live="counter"></div>

<!-- Mount with initial data -->
<div data-live="counter" data-props='{"step":5}'></div>

CLI (zx)

The zx CLI lives at core/CLI/zx. Run it with php core/CLI/zx <command>.

bash
# Generate files
php core/CLI/zx make:controller PostController
php core/CLI/zx make:model Post
php core/CLI/zx make:middleware VerifySubscription
php core/CLI/zx make:migration create_posts_table

# Database
php core/CLI/zx migrate

# App utilities
php core/CLI/zx key:generate
php core/CLI/zx cache:clear
php core/CLI/zx serve              # Start built-in dev server

Command Reference

Command Description
make:controller Name Create a new controller in app/Controllers/
make:model Name Create a new model in app/Models/
make:middleware Name Create a new middleware in app/Middleware/
make:migration name Create a timestamped migration file
migrate Run all pending migrations
key:generate Generate and write APP_KEY to .env
cache:clear Delete all cached views and data
serve Start PHP built-in web server on localhost:8000
help List all commands