Introduction To NoClass™

NoClass™ is a procedural PHP MVC framework designed for simplicity and performance. Unlike traditional OOP frameworks, NoClass uses functions instead of classes, making it lightweight and easy to understand while maintaining a clean MVC structure.

Why NoClass? Perfect for developers who want a minimal footprint without sacrificing essential features, with built-in caching, security, and a flexible routing system.

Key Advantages

NoClass™ Philosophy and Conventions

NoClass is intentionally procedural: no controllers or models as classes, no service container, and no “magic” objects. The framework aims to be easy to read, easy to debug, and simple to deploy.

Important: The conventions below are recommended for clarity and teamwork, but they are not mandatory. Teams may adopt different styles as long as they remain consistent within a project.

File naming philosophy (Capitalised vs lowercase)

NoClass uses file naming to communicate intent, not object-orientation. Capitalised filenames identify framework-invoked “entry point” files (controllers, middleware, core system files). Lowercase filenames are plain function collections (models and libraries).

Layer Folder Recommended filename style Examples Why
Controllers controllers/ Capitalised Blog.php, User.php Public entry points (route targets)
Middleware middleware/ Capitalised Auth.php, Csrf.php Cross-cutting control logic
Core system system/ Capitalised Route.php, Respond.php Framework components invoked internally
Models models/ lowercase user.php, blog.php Business logic as function collections
Libraries lib/ lowercase email.php, security.php Reusable helpers and integrations
Views views/ lowercase blog/index.php Templates map naturally to URL paths

Note: Linux file systems are case-sensitive. Keep your naming consistent to avoid “works on Windows but fails on Linux” issues.

Controller and action naming

Middleware naming

Middleware files are Capitalised (e.g. middleware/Auth.php) because they are called by name from routing/config. Middleware functions inside can remain procedural and explicit.

Function prefixes for organisation

Because NoClass does not use objects, it helps to group functionality using function prefixes. This makes discovery and team collaboration easier.

Example naming
// models/user.php
function user_findById(int $id): ?array { /* ... */ }
function user_create(array $data): int { /* ... */ }

// lib/email.php
function email_send(string $to, string $subject, string $html): bool { /* ... */ }
function email_template(string $name, array $vars = []): string { /* ... */ }

Cross-reference

See Minimal App Walkthrough (End-To-End) for a full example following these conventions.

Changelog

v1.0.0 – Stable (2026-01-20)

Pre‑v1.0 (Development)

Quick Start (For The Impatient)

1. Installation

Terminal Commands
# Clone or extract NoClass to your web directory
            cd /var/www/html
            git clone https://github.com/your-repo/noclass.git
            cd noclass

            # If using file cache, ensure cache dir exists and is writable by the web user
            mkdir -p cache
            chmod 755 public/
            chmod 755 cache/
            

2. Basic Configuration

config/config.php
<?php

            // Debug and Environment
            define('DEBUG', true); // Set to false in production
            define('BASE_URL', 'http://localhost/noclass');
            define('BASE_URI', '/noclass');

            // Caching (choose one)
            define('CACHING', CACHE_ENGINE); // Or CACHE_FILE for file-based
            define('CACHE_TTL_DB', 300); // 5 minutes cache for queries
            define('CACHE_TTL_ROUTE', 60); // 1 minute for routes

            // Database
            define('USE_DB', true); // Set false if not using database

3. Create Your First Controller

controllers/Hello.php
<?php

        function index() {
            data([
                'message' => 'Hello NoClass!',
                'time'    => date('H:i:s')
            ]);

            // Return null to allow default view rendering (views/hello/index.php)
            return null;
        }

        function api_test() {
            // Auto JSON response for AJAX / API style calls
            return ['status' => 'success', 'message' => 'API working'];
        }

4. Define a Route

config/routes.php
<?php

        return [
            'hello' => [
                'controller'  => 'Hello',
                'action'      => ['index', 'api_test'],
                'middleware'  => [] // Optional middleware
            ]
        ];

5. Create a View

views/hello/index.php
<?php require view_path('partials/header.php'); ?>

        

Default message

Current time: N/A

That's it! Visit http://localhost/noclass/hello to see your first NoClass page.

Project Structure

NoClass projects follow a predictable folder structure. The separation of concerns is simple: controllers handle requests, models handle data logic, and views render output.

Directory Layout
noclass_project/

        ├── config/
        │   ├── config.php        # Framework configuration
        │   ├── database.php      # Database credentials
        │   └── routes.php        # URL routing definitions
        ├── controllers/          # Controller files (functions-as-actions)
        ├── models/               # Model files (procedural functions, optional)
        ├── views/                # View templates (PHP)
        │   ├── partials/         # Reusable components (header/footer, etc.)
        │   └── controller/action.php
        ├── lib/                  # Reusable libraries (email.php, security.php, etc.)
        ├── middleware/           # Middleware functions
        ├── system/               # Core framework (avoid editing)
        │   ├── setup.php         # Bootstrap
        │   ├── Route.php         # Routing logic
        │   ├── db.php            # Database helpers
        │   ├── cache.php         # Caching system
        │   └── ...               # Other core files
        ├── cache/                # File cache storage (when using CACHE_FILE)
        ├── public/               # Web root (DocumentRoot)
        │   ├── index.php         # Front controller
        │   ├── .htaccess         # Apache rewrites
        │   └── assets/           # CSS, JS, images
        ├── storage/              # Uploads, logs (best kept outside web root)
        └── vendor/               # Third-party libraries (autoloaded)

Routing System

NoClass uses a powerful pattern-based routing system that's both flexible and secure.

Basic Route Definition

config/routes.php
<?php

        return [
            'hello' => [
                'controller'  => 'Hello',
                'action'      => ['index', 'api_test'],
                'middleware'  => [] // Optional middleware
            ]
        ];

Route Pattern Types

Pattern Matches Example
{num} Numbers only /user/123
{alpha} or {name} Letters only /category/news
{alnum} Alphanumeric characters /post/abc123
{slug} URL-friendly slugs (letters, numbers, hyphens) /post/my-awesome-post
{email} Valid email addresses /verify/user@example.com
{uuid} UUID format /resource/550e8400-e29b-41d4-a716-446655440000
{any} Any non-empty string /file/somefile.txt

Alias Routes with Special Actions

Advanced Alias Example
<?php

        return [
        'user' => [
        'controller' => 'User',
        'action' => ['index', 'show/{num}', 'edit/{num}']
        ],
        'u' => [    // Alias with different/extra actions
            'controller' => 'User',
            'action' => [
                '{num}',                    // /u/123 → User controller, show(123)
                'profile',                  // /u/profile → User::profile()
                'settings/{tab}'            // /u/settings/security
            ],
            'overwrite_actions' => false,   // Combine with User's actions
            'middleware' => ['ApiAuth']     // Different middleware for API
        ]

        ];
Note: When overwrite_actions is false, alias actions are added to the original controller's actions. When true, only the alias actions are allowed for that URL prefix.

Request Lifecycle (How Noclass Works)

Understanding the request lifecycle makes NoClass feel predictable. NoClass follows an MVC-style flow, but implemented procedurally: a URL maps to a controller file, then an action function, then a view.

  1. Web server rewrite: All requests are rewritten to public/index.php.
  2. Bootstrap: NoClass loads config, helpers, error handling, and connects to the database (if enabled).
  3. Routing: The router parses the URL into {controller}/{action}/{params...}.
  4. Middleware (optional): Middleware runs before the action (e.g., auth checks).
  5. Controller action: The action function runs and may call models/libs.
  6. Return handling:
    • null → auto-render the default view (e.g., views/blog/index.php)
    • array → auto JSON response (useful for AJAX)
    • string → treated as raw output or a custom response (depending on your response helper)
  7. View render: Values passed via data() are extracted into the view scope.
Debugging tip: When something fails, trace it in this order: rewrite → routing → middleware → controller → view.

Common Anti-Patterns (What To Avoid)

NoClass is simple by design. These patterns make apps harder to debug or less secure.

1) Putting business logic inside views

2) Using $_GET/$_POST everywhere

3) Allowing undeclared actions in production

4) Using chmod 777

5) Treating cache like a database

Migration Guide (Laravel / Codeigniter / Vanilla Php)

NoClass will feel familiar if you have used MVC frameworks, but it removes class-based structure and containers. Use this mapping to translate concepts quickly.

Concept Laravel / CI NoClass
Controller Class functions Controller file with action functions
Routes Route definitions config/routes.php + convention-based controller/action
Views Blade / templating Plain PHP templates
Passing data return view('x', [...]) data([...]); return null;
JSON endpoints return response()->json() return ['status' => 'ok'];
Middleware Class middleware Middleware functions in middleware/
database helpers / Query Builder Eloquent / Builder db_* helpers or procedural query builder helpers
Important: NoClass intentionally avoids service containers and auto-injection. Keep dependencies explicit (require files, call functions).

Minimal App Walkthrough (End-To-End)

This walkthrough builds a tiny “Blog list” page end-to-end using NoClass’s procedural MVC style: route → controller → model → view.

Naming conventions: see NoClass Philosophy and Conventions.

1) Add a route

In config/routes.php map a URL to a controller + action:

config/routes.php
<?php

return [
  'blog' => [
    'controller' => 'Blog',
    'action'     => 'index',
  ],
];

2) Create the controller

Create controllers/Blog.php and implement an action function (e.g. indexAction):

controllers/Blog.php
<?php

function indexAction()
{
  $posts = blog_getAllPublished();

  // Pass data to the view (Option A: extracted into variables)
  data([
    'title' => 'Blog',
    'posts' => $posts,
  ]);

  // render_view() is automatically called unless overridden
}

3) Create the model

Models are plain PHP files with functions. In NoClass, model functions commonly use a prefix like blog_* for clarity and easy discovery.

models/Blog.php
<?php

function blog_getAllPublished(): array
{
  return db_select('posts', ['id','title','published_at'], ['status' => 'published'], [
    'order_by' => 'published_at DESC',
    'limit'    => 20,
  ]);
}

4) Create a view

Create the view file that matches the default mapping: views/blog/index.php. Because NoClass uses Option A, values set via data() are automatically available as variables (e.g. $title, $posts).

views/blog/index.php
<h1><?php echo e($title ?? 'Blog'); ?></h1>

<ul>
<?php foreach (($posts ?? []) as $p): ?>
  <li>
    <strong><?php echo e($p['title'] ?? ''); ?></strong>
    <small>(<?php echo e($p['published_at'] ?? ''); ?>)</small>
  </li>
<?php endforeach; ?>
</ul>

5) Visit the page

Navigate to /blog. NoClass will route the request to BlogindexAction(), load posts via the model function, and render views/blog/index.php.

Controllers

Controllers are plain PHP files containing action functions. Each function maps directly to a route and may return data, arrays (JSON), or null to auto-render views.

Controllers in NoClass are simple PHP files containing functions. Each function corresponds to an action.

Basic Controller Structure

controllers/Product.php
<?php

// index() handles /product
function index() {
$products = db_select('products', ['status' => 'active']);
data(['products' => $products, 'title' => 'All Products']);
return null; // Render default view
}

// show() handles /product/show/123
function show($id) {
$product = db_select('products', '*', ['id' => $id], '', '1');
if (empty($product)) {
    notFoundPage('Product not found');
    return;
}

data(['product' => $product[0], 'title' => $product[0]['name']]);

}

// create() handles /product/create (POST only)
function create() {
if (!isPost()) {
notFoundPage();
return;
}
// Validate CSRF
if (!csrf_verify(input_post('csrf_token'))) {
    return err('Invalid CSRF token', 403);
}

$id = db_insert('products', [
    'name' => input_post('name'),
    'price' => input_post('price'),
    'description' => input_post('description'),
    'created_at' => date('Y-m-d H:i:s')
]);

// Redirect after POST
redirect('/product/show/' . $id);

}

// API endpoint (auto JSON response)
function api_list() {
$products = db_select('products', ['status' => 'active']);
return ok($products); // Returns {'ok': true, 'data': [...]}
}

Controller Helper Functions

Function Purpose Example
isPost() Check if request is POST if (isPost()) { ... }
isGet() Check if request is GET if (isGet()) { ... }
is_ajax() Check if AJAX request if (is_ajax()) { return ok($data); }
redirect($url) Redirect to URL redirect('/dashboard');
ok($data) Return success JSON return ok(['id' => 123]);
err($message) Return error JSON return err('Invalid input', 400);

Views & Templates

Views are plain PHP files that receive data from controllers and generate HTML.

Basic View with Layout

views/product/index.php
<?php

// Access controller data
$products = $content['products'] ?? [];
$title = $content['title'] ?? 'Products';

// Include header
require view_path('partials/header.php');
?>

NoClass™ Documentation

<?php if (empty($products)): ?> <p class="empty">No products found.</p> <?php else: ?> <div class="product-grid"> <?php foreach ($products as $product): ?> <div class="product-card"> <h3><?php e($product['name']); ?></h3> <p>Price: $<?php e(number_format($product['price'], 2)); ?></p> <a href="<?php echo url('product/show/' . $product['id']); ?>"> View Details </a> </div> <?php endforeach; ?> </div> <?php endif; ?>

View Helpers

Function Description Usage
e($value) HTML escape output <?php e($user['name']); ?>
url($path) Generate internal URL <a href="<?php echo url('user/profile'); ?>">
base_url($path) Generate absolute URL <img src="<?php echo base_url('assets/logo.png'); ?>">
asset($file) Link to static asset <link href="<?php echo asset('css/app.css'); ?>" />
view_path($file) Get full path to view require view_path('partials/header.php');

Partial Templates (Components)

Partials (also called components) are reusable view fragments such as headers, footers, menus, and alerts. Keep partials inside views/partials/ and include them from your views using standard PHP includes.

views/partials/header.php
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <title><?php echo e($title ?? 'NoClass App'); ?></title>

  <!-- Assets -->
  <link rel="stylesheet" href="<?php echo asset('css/app.css'); ?>">

  <!-- CSRF token for AJAX/forms -->
  <meta name="csrf-token" content="<?php echo csrf_token(); ?>">
</head>
<body>

<header class="site-header">
  <h1><?php echo e($heading ?? 'Welcome'); ?></h1>
</header>
views/partials/footer.php
<footer class="site-footer">
  <p>&copy; <?php echo date('Y'); ?> NoClass</p>
</footer>

</body>
</html>
Include partials from a view
<?php
// In: views/home/index.php
$title   = 'Home';
$heading = 'Hello from NoClass';

include BASE_PATH . '/views/partials/header.php';
?>

<main>
  <p>This is the home page.</p>
</main>

<?php include BASE_PATH . '/views/partials/footer.php'; ?>

The data() Function

data() is NoClass’s official way to pass values from controllers (and middleware/helpers) into views. Because NoClass avoids classes and service containers, data() provides a simple, predictable alternative to view models or dependency injection.

What data() does

  • Stores key–value pairs for the current request.
  • Makes those values available to the view that is rendered.
  • Supports multiple incremental calls (values are merged).
  • Is request-scoped (it does not persist between requests).
Official rule: Use data() for view data only. Do not use it as a global state store for business logic.

Set values (controller)

controllers/Home.php
<?php

function indexAction()
{
  data('title', 'Home');
  data('heading', 'Welcome to NoClass');
  data('user', current_user());

  // render_view() runs automatically (unless overridden)
}

Set multiple values at once

Batch assignment
data([
  'title'   => 'Dashboard',
  'stats'   => $stats,
  'notices' => $notices,
]);

Incremental assignment (merge behaviour)

Calling data() multiple times merges values. This makes it safe for middleware or helper functions to add extra fields without breaking controllers.

Incremental calls
data('title', 'Blog');

// later, maybe in a helper or middleware:
data('csrf', csrf_token());
data('flash', flash_get_all());
Important: If you set the same key twice, the later value overwrites the earlier one. Keep keys stable and predictable (e.g., title, user, csrf, flash).

Read values inside a view

Option A (official): before a view is rendered, NoClass automatically extracts values set via data() into the view scope. That means data('title', 'Home') becomes a $title variable inside the view.

Recommended view pattern: use safe defaults so views do not break if a key was not set.

views/home/index.php
<h1><?php echo e($heading ?? 'Welcome'); ?></h1>
<p>Page title: <?php echo e($title ?? 'NoClass'); ?></p>

Using data('key') as a getter (supported)

NoClass also supports calling data('key') to read a value. This is useful in non-view contexts such as:

  • API endpoints that return JSON (no view rendering)
  • Middleware or helpers that need to read data that was set earlier in the request
  • Debugging or conditional logic in controllers
Getter examples
// set somewhere earlier
data('user', current_user());

// read later (controller / middleware / API handler)
$user = data('user');

// API response example (no views)
respond_json([
  'ok'   => true,
  'user' => $user,
]);

Passing arrays and lists

Controller
data('posts', [
  ['id' => 1, 'title' => 'Hello'],
  ['id' => 2, 'title' => 'World'],
]);
View
<ul>
<?php foreach (($posts ?? []) as $p): ?>
  <li><?php echo e($p['title']); ?></li>
<?php endforeach; ?>
</ul>

Best practices (official)

  • Controllers should set view data via data() and keep business logic in models/lib functions.
  • Use stable, predictable key names across the app (title, user, csrf, flash).
  • Prefer batch assignment for related data and incremental assignment for cross-cutting concerns (middleware/helpers).
  • Views should use defaults ($value ?? default) to avoid undefined variable notices.

Database

NoClass provides a procedural database layer built around simple helper functions and prepared statements. The goal is to keep data access explicit, readable, and consistent across controllers and models.

Database configuration

Define your database settings in config/database.php:

config/database.php
define('DB_HOST', '127.0.0.1'); // Use 127.0.0.1, not localhost
define('DB_USER', 'root');
define('DB_PASS', '');
define('DB_NAME', 'noclass');

// Optional:
define('DB_PORT', 3306);
define('DB_CHARSET', 'utf8mb4');

Parameter order rule (official)

All NoClass DB helper functions follow this order:

DB helper order
table → columns → conditions → options

This mirrors SQL (SELECT columns FROM table WHERE ...) and keeps common queries readable. Avoid inventing alternative calling styles—follow the order above consistently.

Core helpers

Available DB helpers
// Reads
db_select(string $table, array $columns = ['*'], array $where = [], array $opts = []): array
db_select_one(string $table, array $columns = ['*'], array $where = [], array $opts = []): ?array
db_exists(string $table, array $where): bool
db_count(string $table, array $where = []): int

// Writes (require conditions for safety)
db_insert(string $table, array $data): int
db_update(string $table, array $data, array $where, array $opts = []): int
db_delete(string $table, array $where): int

Quick examples

Select examples
// SELECT * FROM users
$rows = db_select('users');

// SELECT id,email FROM users
$rows = db_select('users', ['id', 'email']);

// SELECT id,email FROM users WHERE status='active' ORDER BY id DESC LIMIT 20
$rows = db_select('users', ['id', 'email'], ['status' => 'active'], [
  'order_by' => 'id DESC',
  'limit'    => 20,
]);
Write examples
// INSERT
$id = db_insert('users', [
  'email' => 'john@example.com',
  'name'  => 'John'
]);

// UPDATE (requires conditions)
db_update('users', ['name' => 'John Doe'], ['id' => $id]);

// DELETE (requires conditions)
db_delete('users', ['id' => $id]);

Tip: Put DB calls in models/ functions for reuse. Controllers should coordinate requests and responses.

Advanced Database Examples

This section shows official, copy-pasteable patterns for more advanced queries using the NoClass DB helpers and the official parameter order rule: table → columns → conditions → options.

Ordering, limit, offset

Order + limit + offset
$rows = db_select('users', ['id', 'email'], ['status' => 'active'], [
  'order_by' => 'id DESC',
  'limit'    => 20,
  'offset'   => 40,
]);

Joins

Use $opts['joins'] for simple join cases, or switch to the fluent query builder for more complex join logic.

Join via options
$rows = db_select('users u', ['u.id', 'u.email', 'p.avatar'], ['u.status' => 'active'], [
  'joins' => [
    ['LEFT', 'profiles p', 'p.user_id = u.id'],
  ],
  'order_by' => 'u.id DESC',
  'limit'    => 50,
]);

Where operators (official style)

When using associative conditions, NoClass supports a simple operator suffix on keys. This keeps conditions readable and consistent.

Operators
// WHERE id > 10
$rows = db_select('users', ['id', 'email'], ['id >' => 10]);

// WHERE email LIKE '%@gmail.com'
$rows = db_select('users', ['id', 'email'], ['email LIKE' => '%@gmail.com']);

// WHERE id IN (1,2,3)
$rows = db_select('users', ['id'], ['id IN' => [1, 2, 3]]);

// WHERE deleted_at IS NULL
$rows = db_select('users', ['id'], ['deleted_at IS' => null]);

Transactions

Use transactions when multiple writes must succeed or fail together.

Transaction example
db_transaction(function () use ($fromId, $toId, $amount) {
  db_update('wallets', ['balance' => ['__raw__' => '`balance` - ' . (int)$amount]], ['id' => $fromId]);
  db_update('wallets', ['balance' => ['__raw__' => '`balance` + ' . (int)$amount]], ['id' => $toId]);
});

Concurrency & locks (quick pointer)

For multi-step flows where concurrent requests must not overlap (money, inventory, one-time jobs), use advisory locks. See Cache, DB & Locks for the full guidance.

Models

Models are procedural helpers that encapsulate database access and domain logic. They are optional but recommended for keeping controllers thin.

A model in NoClass is simply a collection of related functions. There is no base class, no database helpers object, and no hidden state. This keeps the code easy to test and easy to debug.

Recommended rules:

  • One file per domain: models/User.php, models/Blog.php, etc.
  • Prefix functions: user_*, blog_* to avoid naming collisions.
  • Keep controllers thin: controllers coordinate requests; models talk to the DB.
  • No HTML in models: return arrays/scalars only.

While NoClass is procedural, you can organize database logic into model files for better code organization.

Creating a Model

models/User.php
<?php

// Convention: prefix functions with the model name
// File: models/User.php

function user_getAll() {
    return db_select('users', [], '*', 'id DESC');
}

function user_findById(int $id) {
    $rows = db_select('users', ['id' => $id], '*', '', 1);
    return $rows[0] ?? null;
}

function user_findByEmail(string $email) {
    $rows = db_select('users', ['email' => $email], '*', '', 1);
    return $rows[0] ?? null;
}

function user_create(array $data) {
    // Safe fallback: ensure passwords are stored hashed
    if (isset($data['password'])) {
        $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
    }

    $data['created_at'] = $data['created_at'] ?? date('Y-m-d H:i:s');

    return db_insert('users', $data);
}

function user_updateById(int $id, array $data) {
    return db_update('users', $data, ['id' => $id]);
}

function user_deleteById(int $id) {
    return db_delete('users', ['id' => $id]);
}

?>

Using Models in Controllers

Controller using Model
<?php

// File: controllers/User.php
// (Models are usually auto-loaded in setup.php if you follow the folder conventions)

function profile($id) {
    $user = user_findById((int)$id);

    if (!$user) {
        return error_notFound('User not found');
    }

    data([
        'title' => 'User Profile',
        'user'  => $user
    ]);

    return null; // views/user/profile.php
}

?>

Query Builder

NoClass includes an internal query builder that helps with complex queries (joins, grouped conditions, pagination) while keeping the framework procedural. For most applications, prefer the DB helpers (db_select(), db_insert(), db_update(), db_delete()).

When to use what

  • DB helpers (recommended): simple CRUD, predictable patterns, easy to read and standardised.
  • Query builder (advanced): complex queries where a single db_select() call becomes hard to express.

About low-level query_* helpers

The query_* functions are building blocks used by the framework. They are available for advanced use, but to keep codebases consistent, NoClass recommends using them in a single, standard pattern: build a query object (array), apply steps, then execute.

Standard pattern (NoClass style)

Build → refine → execute
$q = query_init('users');
$q = query_select($q, ['id', 'email', 'name']);
$q = query_where($q, 'status', '=', 'active');
$q = query_order($q, 'id', 'DESC');
$q = query_limit($q, 20);

$rows = query_get($q);

Official query_* function list

The following query_* functions form the official query builder API in NoClass. Developers should use only these functions and avoid inventing custom variants.

Function Purpose
query_init($table)Initialise a query for a table
query_select($q, $columns)Select columns
query_where($q, $col, $op, $val)Add a WHERE condition
query_where_in($q, $col, array $vals)WHERE IN (...) condition
query_join($q, $type, $table, $on)Add JOIN clause
query_group($q, $expr)GROUP BY
query_having($q, $expr, $op, $val)HAVING clause
query_order($q, $col, $dir)ORDER BY
query_limit($q, $limit, $offset = 0)LIMIT / OFFSET
query_get($q)Execute SELECT and return rows
query_update($q, $data)Execute UPDATE
query_delete($q)Execute DELETE

Recommendation: If you expose query-builder logic to controllers, wrap it inside model functions so the rest of the application continues to depend on stable APIs.

Join example (advanced)

Join + select + filters
$q = query_init('users u');
$q = query_join($q, 'LEFT', 'profiles p', 'p.user_id = u.id');
$q = query_select($q, ['u.id', 'u.email', 'p.avatar']);
$q = query_where($q, 'u.status', '=', 'active');
$q = query_order($q, 'u.id', 'DESC');
$q = query_limit($q, 50);

$rows = query_get($q);

Update example

For simple updates, prefer db_update(). Use the query builder only when you need complex conditions or joins.

Update with conditions
// recommended for most apps:
$affected = db_update('users', ['name' => 'John Doe'], ['id' => 5]);

// builder style (advanced use):
$q = query_init('users');
$q = query_where($q, 'id', '=', 5);
$affected = query_update($q, ['name' => 'John Doe']);

Recommendation: if you use the query builder in your project, keep its usage consistent—wrap repeated queries inside model functions (e.g., models/User.php).

Caching System

NoClass includes a powerful caching system with automatic database query caching and version-based invalidation.

Cache Configuration

config/config.php
<?php

// Caching engine
// Options: CACHE_FILE (file cache), CACHE_ENGINE (cache server/engine)
define('CACHING', CACHE_ENGINE);

// Cache TTLs (seconds)
define('CACHE_TTL_DB', 300);      // 5 minutes for DB result caching
define('CACHE_TTL_HTML', 900);    // 15 minutes for rendered HTML fragments

// File cache location (if using CACHE_FILE)
define('CACHE_PATH', BASE_PATH . '/cache');

?>

Automatic Query Caching

Database queries are automatically cached when using db_select(), db_aggregate(), etc.

How Auto-Caching Works
<?php

// Example: Auto-caching DB results (conceptual)
//
// First call: hits DB, stores result in cache
$users = db_select('users', ['status' => 'active']);

// Later call with the same query: cache hit returns quickly
$users = db_select('users', ['status' => 'active']);

// Under the hood, a key is generated from:
//
// 1) table name + current table version (for invalidation)
// 2) where/columns/order/limit (hashed for compactness)
//
// Example key format:
// "users.v{version}.{hash}"

?>

Manual Cache Control

Direct Cache Functions
<?php

// Set cache
cache_set('homepage_html', $renderedHTML, CACHE_TTL_HTML);

// Get cache (returns null if missing/expired)
$html = cache_get('homepage_html');

// Delete cache
cache_del('homepage_html');

// "Remember" helper: compute once, cache, return value
$stats = cache_remember('stats:dashboard', 60, function () {
    $row = db_raw("SELECT COUNT(*) AS total FROM users");
    return $row[0] ?? ['total' => 0];
});

?>

Version-Based Cache (Advanced)

Smart Cache Invalidation
<?php

// Smart invalidation with versioned keys
//
// Versioned keys automatically invalidate when you bump a table version.
// This is faster and safer than scanning/deleting thousands of keys.

$key = cache_key_versioned('users', 'list:active');

cache_set($key, json_encode($users), CACHE_TTL_DB);

$cached = cache_get($key);

if ($cached) {
    $users = json_decode($cached, true);
}

// When users table changes (insert/update/delete), bump version once:
cache_bumpver('users');

?>

Cache Transactions

Atomic Cache Operations
<?php

// Atomic operations (cache server / engine mode)
//
// If you are using the NoClass cache server, you can wrap multiple operations
// so they are applied together.

cache_tx_begin();

cache_set('key1', 'value1');
cache_set('key2', 'value2');

// This invalidates all versioned 'users' keys at once:
cache_bumpver('users');

cache_tx_commit();

// If something goes wrong:
// cache_tx_rollback();

?>
Common pitfalls:
  • Forgetting invalidation: if you cache a list, decide when and how it becomes stale.
  • Key collisions: always include a namespace/table/version in keys.
  • Over-caching: caching everything can hide bugs and make data “feel” inconsistent.

Cache, DB & Locks

NoClass provides a clear, explicit, and procedural approach to database access, caching, and concurrency. This section defines the official patterns developers must follow when working with: database reads/writes, read-through caching, stale-while-revalidate (SWR), advisory locks, and transactions.

Important: follow the patterns in this section exactly. Do not invent alternative styles or abstractions.

The NoClass mental model

Responsibilities
Reads:          db_select(), db_select_one()
Writes:         db_insert(), db_update(), db_delete()
Invalidation:   table-versioned cache (cache_getver / cache_bumpver)
Fast reads:     SWR (stale-while-revalidate) + dedup
Concurrency:    db_lock() / db_unlock()
Atomicity:      db_transaction(fn() => ...)

Database access

Parameter order (mandatory):

DB helper order
table → columns → where → options

This mirrors SQL and keeps common queries readable.

Example
$users = db_select(
    'users',
    ['id', 'email'],
    ['status' => 'active'],
    [
        'order_by' => 'id DESC',
        'limit'    => 20,
    ]
);

Table-versioned caching (automatic invalidation)

NoClass uses table versioning to ensure cached data is never silently stale. Each table has a version number, and any write bumps the version. Cached SELECT keys include table versions, so writes automatically invalidate older cached reads.

Versioning primitives
// Read current version
$v = cache_getver('users');

// Bump after writes (NoClass does this automatically after insert/update/delete)
cache_bumpver('users');

Read-through caching for db_select()

When enabled, db_select() first checks cache, returns cached data if available, and otherwise hits the DB and stores the result.

Enable globally
define('DB_SELECT_CACHE', true);
define('DB_SELECT_CACHE_TTL', 30);
Per-query override
$rows = db_select('users', ['id'], [], [
    'cache'     => true,
    'cache_ttl' => 60,
]);

Stale-while-revalidate (SWR)

With SWR, NoClass can return stale cached data immediately while refreshing it in the background. This improves performance under load and avoids slow responses when cached items expire.

Enable SWR + dedup window
define('DB_SELECT_CACHE_SWR', true);
define('CACHE_REFRESH_DEDUP_WINDOW', 10);

Dedup happens at the cache-key level (inside cache_get_stale()), so only one refresh is scheduled per key per window.

Advisory locks

Advisory locks are named, cooperative locks provided by the database. They prevent concurrent execution of a logical critical section. They are connection-bound and automatically released if the DB connection closes.

Lock API
db_lock(string $key, int $timeoutSeconds = 5): bool
db_unlock(string $key): void

Naming convention (mandatory): <context>:<entity>:<identifier>

Lock naming examples
user:42
wallet:user:42
order:checkout:8891
cron:daily_reports

Transactions vs advisory locks

Transactions protect data integrity (atomicity / rollback). Advisory locks protect who may enter a multi-step flow. In many critical flows you will use both: lock to guard entry, transaction to guarantee correctness.

Recommended combined pattern
if (!db_lock('wallet:user:42', 3)) {
    throw new Exception('Resource busy, try again');
}

try {
    db_transaction(function () {
        db_update('wallets',
            ['balance' => ['__raw__' => '`balance` - 10']],
            ['user_id' => 42]
        );
    });
} finally {
    db_unlock('wallet:user:42');
}

Official rules

  • Do use db_select() for reads and let NoClass handle invalidation via table versions.
  • Do use advisory locks for multi-step critical flows (money, inventory, one-time jobs).
  • Do use transactions for atomic writes.
  • Do not manually clear DB select cache keys — table versioning makes this unnecessary.
  • Do not invent new where/operator styles. Use the documented array patterns.

Forms & Validation

NoClass provides a complete form handling system with CSRF protection, validation, and file uploads.

Basic Form Creation

Creating a Form
<?php

echo form_open('/contact/submit', 'POST', [
    'class' => 'contact-form'
]);

echo csrf_field();

echo form_input('name', old('name'), [
    'placeholder' => 'Your name',
    'required'    => true
]);

echo form_input('email', old('email'), [
    'type'        => 'email',
    'placeholder' => 'Your email',
    'required'    => true
]);

echo form_textarea('message', old('message'), [
    'placeholder' => 'Your message',
    'required'    => true,
    'rows'        => 6
]);

// Show validation errors (if any)
echo form_error_summary();

echo form_submit('send', 'Send Message', [
    'class' => 'btn btn-primary'
]);

echo form_close();

?>

Form Processing in Controller

Handling Form Submission
<?php

function contact_submit() {

    if (!isPost()) {
        return notFoundPage();
    }

    // CSRF validation
    if (!csrf_verify(input_post('csrf_token'))) {
        return error_forbidden('Invalid CSRF token');
    }

    // Validate input
    $rules = [
        'name'    => ['required', 'min:2', 'max:80'],
        'email'   => ['required', 'email', 'max:120'],
        'message' => ['required', 'min:10', 'max:2000']
    ];

    $result = validate(input_all_post(), $rules);

    if (!$result['ok']) {
        // Preserve old input + validation errors
        set_errors($result['errors']);
        set_old(input_all_post());
        return redirect(url('contact/index'));
    }

    // Use sanitized values
    $data = $result['data'];

    // Example: save to DB or send email
    db_insert('contact_messages', [
        'name'       => $data['name'],
        'email'      => $data['email'],
        'message'    => $data['message'],
        'created_at' => date('Y-m-d H:i:s')
    ]);

    flash('success', 'Message sent successfully.');
    return redirect(url('contact/index'));
}

?>

File Uploads

Handling File Uploads
<?php

// In form (don't forget enctype!)
echo form_open('/upload', 'POST', [
    'enctype' => 'multipart/form-data',
    'class'   => 'upload-form'
]);

echo csrf_field();
echo form_file('photo', ['accept' => 'image/*']);
echo form_submit('upload', 'Upload');
echo form_close();

// In controller
function upload() {

    if (!isPost()) return notFoundPage();
    if (!csrf_verify(input_post('csrf_token'))) return error_forbidden('Invalid CSRF token');

    $file = input_file('photo');

    if (!$file || $file['error'] !== UPLOAD_ERR_OK) {
        flash('error', 'Upload failed.');
        return redirect(url('upload/index'));
    }

    // Validate file type + size (example)
    if (!is_allowed_mime($file['tmp_name'], ['image/jpeg', 'image/png'])) {
        flash('error', 'Only JPG/PNG allowed.');
        return redirect(url('upload/index'));
    }

    if ($file['size'] > 2 * 1024 * 1024) {
        flash('error', 'Max size is 2MB.');
        return redirect(url('upload/index'));
    }

    $destDir  = BASE_PATH . '/storage/uploads';
    $filename = uniqid('img_', true) . '.jpg';

    if (!is_dir($destDir)) mkdir($destDir, 0755, true);

    move_uploaded_file($file['tmp_name'], $destDir . '/' . $filename);

    flash('success', 'Uploaded successfully.');
    return redirect(url('upload/index'));
}

?>

Tables

NoClass includes simple procedural helpers for rendering HTML tables and enhancing them with JavaScript features like sorting/search/pagination via Grid.js. The goal is to give developers a single standard way to output tables and avoid everyone writing custom table markup.

Basic table rendering

Use render_table() for quick tables, or use table_open(), table_header(), and table_body() when you need finer control.

Basic table
$columns = ['ID', 'Email', 'Name'];

$rows = db_select('users', ['id','email','name'], ['status' => 'active'], [
  'order_by' => 'id DESC',
  'limit'    => 50,
]);

echo render_table($columns, $rows, ['id','email','name'], [
  'id'    => 'usersTable',
  'class' => 'table',
]);

Grid.js helper (client-side enhancement)

Grid.js can enhance a table with sorting, searching, and pagination. NoClass provides a helper to initialise Grid.js. Ensure Grid.js is loaded on the page (via your assets bundle or a vendor script).

Inline init (default)
echo render_table($columns, $rows, ['id','email','name'], [
  'id' => 'usersTable'
]);

// Default: outputs an inline script that runs on DOMContentLoaded
echo table_init_gridjs('usersTable', $columns);

Strict CSP mode (no inline scripts)

If your site uses a strict Content Security Policy (CSP) that forbids inline scripts, disable inline Grid.js init:

config/config.php
define('TABLE_GRIDJS_INLINE', false);

Then attach Grid.js config as data-* attributes using table_gridjs_attrs() and initialise via an external JS file.

CSP-safe table markup
$attrs = table_gridjs_attrs($columns);

echo table_open(array_merge(['id' => 'usersTable'], $attrs));
echo table_header($columns);
echo table_body($rows, ['id','email','name']);
echo table_close();
External JS bootstrap (example)
// Put this in your app.js (or a dedicated noclass-grid.js), not inline
window.NoClassGridInit = function () {
  var nodes = document.querySelectorAll('[data-noclass-grid="1"]');
  nodes.forEach(function (el) {
    var cfgStr = el.getAttribute('data-gridjs') || '{}';
    var cfg = {};
    try { cfg = JSON.parse(cfgStr); } catch (e) { cfg = {}; }
    new gridjs.Grid(cfg).render(el);
  });
};

document.addEventListener('DOMContentLoaded', function () {
  if (window.NoClassGridInit) window.NoClassGridInit();
});

Server-side JSON adapter for Grid.js

For large datasets, prefer server-side pagination/filtering. Grid.js can fetch JSON from your endpoint. NoClass includes export helpers like table_export_json(); for Grid.js, structure your response consistently (for example: { data: [...], total: 123 }).

Example endpoint (controllers/Users.php)
function gridAction()
{
  // read paging params from query string
  $page  = (int)($_GET['page'] ?? 1);
  $limit = (int)($_GET['limit'] ?? 10);
  if ($page < 1) $page = 1;
  if ($limit < 1 || $limit > 100) $limit = 10;
  $offset = ($page - 1) * $limit;

  $rows = db_select('users', ['id','email','name'], ['status' => 'active'], [
    'order_by' => 'id DESC',
    'limit'    => $limit,
    'offset'   => $offset,
    'cache'    => false, // server-side lists should usually be uncached
  ]);

  $total = db_count('users', ['status' => 'active']);

  // Standard JSON shape for Grid.js adapters
  respond_json([
    'data'  => $rows,
    'total' => $total,
    'page'  => $page,
    'limit' => $limit,
  ]);
}

Tip: If you need server-side search/sort, pass query params (q, sort, dir) and apply them with safe allow-lists.

Security Features

CSRF Protection

Automatic CSRF Protection
<?php

// In views/forms:
// If you use form_open(), CSRF may be included automatically (depending on your helper)
// Otherwise include it explicitly:
echo csrf_field();

// In controllers (POST handler):
if (!csrf_verify(input_post('csrf_token'))) {
    return error_forbidden('Invalid CSRF token');
}

// Tip: regenerate tokens periodically for sensitive actions
// csrf_regenerate();

?>

XSS Prevention

Always use e() when outputting user data in views!
Safe Output Examples
<?php

// SECURE (escaped output)
<h1><?= e($user['name']) ?></h1>
<p><?= e($user['bio']) ?></p>

// Alternative (built-in PHP)
<h1><?= htmlspecialchars($user['name'], ENT_QUOTES, 'UTF-8') ?></h1>

// INSECURE (vulnerable to XSS)
<h1><?= $user['name'] ?></h1>

?>

SQL Injection Protection

All NoClass database functions use prepared statements automatically.

Safe Database Usage
<?php

// SECURE: helper functions use prepared statements internally
$user = db_select('users', [
    'email'  => $email,
    'status' => 'active'
], '*', '', 1);

// SECURE: raw SQL with parameters (prepared)
$rows = db_raw(
    "SELECT * FROM users WHERE email = ? AND status = ?",
    [$email, 'active']
);

// INSECURE: concatenating user input into SQL (SQL injection risk)
$sql = "SELECT * FROM users WHERE email = '" . $_GET['email'] . "'";

?>

Secure Sessions

Session Security
<?php

// Start sessions securely (called during setup)
secure_session_start();

// Recommended session settings:
// - HttpOnly cookies (JS can't read)
// - Secure flag on HTTPS
// - SameSite protection
// - Session ID regeneration after login

function login() {

    // ... authenticate user ...

    session_regenerate_id(true);

    $_SESSION['user_id'] = $user['id'];

    return redirect(url('dashboard/index'));
}

?>

Vendor Isolation

NoClass supports third-party vendors (via Composer), but security and maintainability depend on keeping vendor usage isolated. Do not call vendor classes directly across the application. Instead, wrap vendor libraries inside small procedural integration functions (for example under lib/integrations/). This reduces the risk of exposing sensitive errors and makes vendor upgrades safer.

  • Keep vendor calls in adapters, not in controllers or views.
  • Catch exceptions inside integrations and return safe, consistent errors.
  • Validate all external inputs before passing data to vendor SDKs (webhooks, callbacks, uploads).
  • Pin and audit dependencies using composer.lock and upgrade deliberately.
  • Never expose secrets: load credentials from .env, and never log them.

Paths, URLs, and Assets

NoClass separates filesystem paths from web URLs. This lets you move the app between domains and subfolders without breaking links.

Core constants

Constant Meaning Example
BASE_PATH Absolute filesystem path to the project root /var/www/noclass
BASE_URI URL path where the app is mounted (subfolder-safe) / or /myapp
ASSET_PATH Public assets base path (relative to BASE_URI) /assets
CDN_URL Optional CDN base URL for assets https://cdn.example.com

URL helpers

Use these helpers to build internal links safely (works at domain root or in a subfolder).

URL helpers
<?php

// Internal link relative to BASE_URI (subfolder-safe)
echo url('blog');
// Example output: /myapp/blog

// Route helper (if you use named routes)
echo route('blog.show', ['id' => 5]);

// Current URL path (request path within the app)
echo current_path();

?>

Asset helpers

Asset helpers build URLs to files in your public assets directory. If CDN_URL is set, assets are served from the CDN. If cache-busting is enabled, NoClass appends a version query string.

Assets
<?php

// Standard asset (supports CDN + cache-busting when enabled)
echo asset('css/app.css');

// Raw asset (no cache-busting) — useful for third-party URLs or special cases
echo asset_raw('css/app.css');

// Force HTTPS (useful when the site can be visited on http + https)
echo secure_asset('js/app.js');

?>

Cache-busting

When enabled, NoClass can append a version string so browsers fetch new files after deployments. The common approach is using filemtime() (modification time) or a build hash.

Recommended config
// Example flags (adapt to your config file)
define('ASSET_CACHE_BUST', true);
Production tip: If you enable long cache lifetimes on static files, you should also enable cache-busting (query string version or hashed filenames) to prevent users seeing old assets after a deployment.

JavaScript Assets (app.js & https.js)

NoClass includes small, optional JavaScript helpers to cover common frontend needs without introducing a heavy framework. These helpers live in assets/js/ and work with plain (vanilla) JavaScript.

Philosophy: NoClass ships a “good default” for AJAX, but you are free to replace it with Axios, a different fetch wrapper, or a frontend framework if your project grows.

assets/js/https.js – lightweight fetch wrapper

https.js wraps the browser fetch() API to standardise JSON handling, headers, CSRF tokens, and error handling. This avoids repeating boilerplate in every request.

Supported API surface (recommended)

Keep the wrapper intentionally small. A common NoClass-friendly surface is:

Method Purpose Example
https.get(url, opts?) GET JSON https.get('/api/users')
https.post(url, data, opts?) POST JSON https.post('/api/users', {name:'A'})
https.put(url, data, opts?) PUT JSON https.put('/api/users/5', {...})
https.del(url, opts?) DELETE https.del('/api/users/5')
https.request(method, url, data?, opts?) Low-level escape hatch https.request('PATCH','/x', {...})

Standard response shape

To keep apps consistent, the wrapper should return a resolved promise with a predictable object shape, for example:

Response shape (recommended)
{
  ok: true,            // boolean
  status: 200,         // HTTP status
  data: {...},         // parsed JSON payload (or null)
  raw:  response       // optional: original Response object
}

When a request fails, reject with an Error-like object that contains at least message and status.

GET request
https.get('/api/users')
  .then(function (res) {
    console.log(res.data);
  })
  .catch(function (err) {
    alert(err.message);
  });
POST request
https.post('/api/users', {
  email: 'test@example.com',
  name: 'John'
})
.then(function (res) {
  console.log('Created', res.data);
});

CSRF integration

If your app uses CSRF protection, the simplest approach is to expose the token in a meta tag and have https.js automatically send it on state-changing requests (POST/PUT/PATCH/DELETE).

In your layout / header partial
<meta name="csrf-token" content="<?php echo csrf_token(); ?>">
https.js reads token (concept)
function csrfToken(){
  var m = document.querySelector('meta[name="csrf-token"]');
  return m ? m.getAttribute('content') : '';
}

Tip: If you build APIs for third-party clients, you may disable CSRF for those routes and use tokens/keys instead.

assets/js/app.js – application bootstrap

app.js is the recommended place for application-level JavaScript such as:

  • binding DOM event listeners
  • initialising UI widgets (Grid.js, date pickers, etc.)
  • calling helpers from https.js
Typical app.js pattern
document.addEventListener('DOMContentLoaded', function () {
  document.querySelectorAll('[data-delete]').forEach(function (btn) {
    btn.addEventListener('click', function () {
      if (!confirm('Delete item?')) return;

      https.post(btn.dataset.url)
        .then(function () {
          location.reload();
        });
    });
  });
});

Progressive enhancement (recommended)

NoClass encourages “progressive enhancement”: pages should still work without JavaScript where possible, and JS enhances the experience (faster actions, fewer full reloads).

  • Provide normal links/forms first (server-rendered behaviour)
  • Enhance with JS (AJAX, modals, in-page updates) when available
  • Keep endpoints reusable (HTML for browsers, JSON for AJAX where appropriate)

Deployment &Amp; Security Checklist

This checklist covers the most common production setup steps for NoClass applications.

Web Server

  • Point your document root to public/ (never to the project root).
  • Enable URL rewriting (Apache: mod_rewrite; Nginx: equivalent rewrite rules).
  • Block direct web access to config/, system/, storage/, and vendor/ if misconfigured.

PHP

  • Enable OPcache and set DEBUG to false.
  • Set a strong session.cookie_samesite policy and use HTTPS for authenticated apps.
  • Limit upload sizes and validate MIME type + extension for uploads.

App Settings

  • Set ALLOW_UNDECLARED_ACTIONS to false to prevent accidental exposure of functions.
  • Use CACHE_ENGINE where possible for speed (and ensure your cache engine supports the NoClass command set).
  • Store secrets outside the repo (environment variables or private config overrides).

Common Deployment Mistakes

  1. Wrong BASE_URI: If your app is in a subfolder, BASE_URI must match it exactly (e.g., /noclass).
  2. DocumentRoot set to project root: This can expose config and system files.
  3. Permissions too open: Avoid chmod 777. Prefer correct owner/group and 755/750.
  4. CDN misconfig: Set CDN_URL only for static assets, not for application routes.
Recommended: Keep storage/ outside public/ and serve uploads via controlled endpoints or a dedicated static host.

Configuration

Configuration in NoClass is file-based and explicit. The core files live under config/. Keep credentials private, and avoid changing system/ files unless you are upgrading the framework.

Main Configuration File

This file defines framework-wide defaults (debug, base URLs, caching, routing safety). Keep production settings strict: DEBUG=false, ALLOW_UNDECLARED_ACTIONS=false, and configure BASE_URI correctly if installed in a subfolder.

If you are deploying to multiple environments, consider loading secrets from environment variables or a private override file.

config/config.php (Complete)
<?php

// =====================================================
// NoClass main configuration (example)
// File: config/config.php
// =====================================================

// Environment
define('DEBUG', false);                 // true on dev, false on production
define('APP_ENV', 'production');        // 'development' | 'staging' | 'production'

// Base paths / URLs (subfolder safe)
define('BASE_URL', 'https://example.com/noclass');  // full origin + BASE_URI (recommended)
define('BASE_URI', '/noclass');                     // '/' if installed at domain root

// Public / filesystem paths (set in index.php/setup.php usually)
define('ASSET_PATH', 'assets');         // public/assets
define('CDN_URL', '');                  // optional: https://cdn.example.com

// Routing & security defaults
define('ALLOW_UNDECLARED_ACTIONS', false);  // keep false in production
define('DEFAULT_CONTROLLER', 'Home');
define('DEFAULT_ACTION', 'index');

// Caching
// Options: CACHE_FILE, CACHE_ENGINE
define('CACHING', CACHE_ENGINE);

define('CACHE_TTL_DB', 300);            // DB result caching (seconds)
define('CACHE_TTL_HTML', 900);          // HTML fragment caching (seconds)
define('CACHE_TTL_ROUTE', 300);         // route/page caching (seconds)

// Session / cookies (tune for your host)
define('SESSION_NAME', 'noclass_session');

// Optional hardening (useful for shared hosting)
define('TRUST_PROXY_HEADERS', false);   // set true only behind a trusted reverse proxy

?>

Database Configuration

Database settings live in config/database.php (or your central config file depending on your setup). Keep credentials out of version control where possible (environment variables or a private override file), and disable DB_DEBUG in production to avoid leaking SQL errors.

config/database.php
<?php

// MySQL / MariaDB configuration
define('DB_HOST', '127.0.0.1');       // or 'localhost'
define('DB_PORT', 3306);
define('DB_NAME', 'noclass_db');
define('DB_USER', 'noclass_user');
define('DB_PASS', 'strong_password_here');

// Optional settings
define('DB_CHARSET', 'utf8mb4');
define('DB_COLLATION', 'utf8mb4_unicode_ci');

// Debugging (disable in production)
define('DB_DEBUG', false);

?>

Routes Configuration

Routes can be defined explicitly to restrict exposed actions. This is recommended for production apps.

config/routes.php (Advanced Example)
<?php

return [

    // Public pages
    'home' => [
        'controller' => 'Home',
        'action'     => ['index']
    ],

    'blog' => [
        'controller' => 'Blog',
        'action'     => ['index', 'view']
    ],

    // Auth-protected area
    'dashboard' => [
        'controller' => 'Dashboard',
        'action'     => ['index', 'settings'],
        'middleware' => ['auth']
    ],

    // API endpoints (JSON responses)
    'api/users' => [
        'controller' => 'ApiUsers',
        'action'     => ['list', 'show'],
        'middleware' => ['auth', 'rate_limit']
    ]

];

?>

Tip: Group related routes under a shared prefix (e.g. api/*) and apply middleware consistently. This keeps large applications manageable and reduces security risks.

.env & Environment Variables

NoClass supports environment-based configuration so you can keep secrets out of your repository. The recommended approach is: keep non-secret defaults in config/, and load secrets from environment variables (or a local .env file in development).

Important: Never commit .env files to git. Keep them outside the web root where possible.

Where to place the .env file

  • Recommended: project root (same level as config/, system/, vendor/) and not inside public/.
  • If your host forces document root to the project root, store .env one level above, or protect it via server rules.

How NoClass reads env values

NoClass uses PHP’s built-in getenv() / $_ENV for environment variables. In your setup/bootstrap, you can optionally parse a local .env file for development environments and populate $_ENV.

Typical .env keys
APP_ENV=development
APP_DEBUG=1

DB_HOST=127.0.0.1
DB_PORT=3306
DB_NAME=noclass
DB_USER=noclass_user
DB_PASS=change_me

APP_KEY=base64:change_me

Best practice

  • Validate required env keys during bootstrap and fail early with a clear error (especially in production).
  • Use allow-lists for env-driven “mode switches” (e.g., APP_ENV, CACHING), and avoid letting env control file paths directly.
  • Keep secrets in env (DB passwords, API keys, SMTP creds, Stripe/PayPal keys).

Vendors (Composer)

NoClass is a procedural-first framework by design. This keeps application code simple, explicit, and easy to reason about. At the same time, real-world projects often require third‑party libraries (payments, email, storage, etc.). NoClass fully supports third‑party vendor libraries—even when those libraries are class‑based—by using Composer’s autoloader.

Install a vendor

Composer install
composer require stripe/stripe-php
composer require aws/aws-sdk-php
composer require phpmailer/phpmailer

Load the Composer autoloader

In NoClass, the Composer autoloader is typically required during bootstrap (for example inside system/setup.php or public/index.php), before controllers run.

Autoload (NoClass)
<?php

// Composer autoloader (vendor libraries)
// This supports class-based third-party packages while NoClass stays procedural.
$autoload = BASE_PATH . '/vendor/autoload.php';
if (is_file($autoload)) {
    require_once $autoload;
}

Procedural-first, vendor-friendly

Vendors are often class-based. NoClass supports them via Composer, but your application code can remain procedural by isolating vendor usage behind small procedural wrappers (adapters). This keeps your controllers and models stable and consistent with the NoClass approach.

  • Keep vendor calls inside integration functions (for example under lib/integrations/).
  • Expose a procedural API to the rest of the app (example: stripe_checkout_create(), s3_upload(), send_mail()).
  • Read secrets from .env (never hard-code API keys).
  • Translate vendor exceptions into NoClass-friendly return arrays / error responses.
Strength through isolation: NoClass stays procedural and predictable, while still benefiting from the modern PHP ecosystem. Vendors remain replaceable because your app depends on your own procedural wrappers, not vendor internals.

Vendor Integration Guidelines

These guidelines help keep NoClass projects clean, secure, and easy to maintain when using third‑party vendors:

  • One integration file per vendor: lib/integrations/stripe.php, lib/integrations/paypal.php, lib/integrations/s3.php, etc.
  • No vendor classes in controllers: controllers should call only your procedural functions.
  • Centralise configuration: load vendor settings from config/services.php (values sourced from .env).
  • Return predictable results: standardise on ['ok' => true, 'data' => ...] and ['ok' => false, 'error' => ...].
  • Catch exceptions inside adapters: do not leak stack traces or raw vendor errors to end-users in production.
  • Pin versions: commit composer.lock and upgrade vendors deliberately.
  • Never map or scan vendor/ in NoClass file-maps: Composer handles vendor autoloading.
Note: Some vendors may provide procedural-friendly helper packages, but many are class-based. NoClass supports both. The recommended approach is always to wrap vendor usage behind your own procedural integration layer.

HMVC Modules

NoClass supports an HMVC-style modular structure for larger applications. A module is a self-contained area of the app with its own controllers, models, views, and routes. This helps teams scale codebases without introducing classes or service containers.

Recommended module layout

Module structure
app/
  modules/
    blog/
      controllers/
      models/
      views/
      config/
    admin/
      controllers/
      models/
      views/
      config/

How modules are routed

  • Modules are typically mounted under a prefix (example: /m/blog/*), then routed internally to the module’s controller/action.
  • Teams can also mount modules directly at the root (example: /blog/*) via root routes configuration.
Tip: Keep module routes explicit in production to avoid accidentally exposing internal functions. Use ALLOW_UNDECLARED_ACTIONS=false and declare actions in the module’s routes file.

When to use HMVC

  • When the app has multiple “sub-apps” (Admin, API, Public site, Partner portal).
  • When you want different middleware stacks per module (e.g., Admin requires Auth + Role:admin).
  • When teams work in parallel and need clear boundaries.

Middleware

Middleware runs before a controller action. Use it for cross-cutting concerns like authentication, role checks, CSRF enforcement (for APIs), rate limiting, and maintenance mode.

How to define middleware in routes

NoClass supports multiple middleware declaration styles:

Supported middleware formats
<?php

// 'middleware' => ['Auth']
// 'middleware' => ['Role:user,male']            // pass args after ":" (comma-separated)
// 'middleware' => [['Role', 'user', 'male']]    // same as above (array form)
// 'middleware' => ['Auth','Role:user']          // multiple middleware
// 'middleware' => ['Auth',['Role','user']]      // mixed styles

Middleware file and function

Middleware files live in middleware/ and are named in a clear, Capitalised style (e.g. Auth.php, Role.php). Each middleware file exposes a function with the same name (or the name your router expects).

middleware/Role.php
<?php

function Role($role = null, $extra = null)
{
  $user = current_user();
  if (!$user) return redirect(url('login'));

  // Simple example
  if ($role && (($user['role'] ?? '') !== $role)) {
    return err('Forbidden', 403);
  }

  // return null to continue request
  return null;
}

Official rules

  • Middleware should be fast and side-effect free where possible.
  • Use NoClass helpers for input/sessions/responses instead of direct $_POST/echo where possible.
  • Middleware may return a response (redirect / JSON error) to stop the request, or return null to continue.

Public Folder Layouts

NoClass can run in different hosting layouts depending on how your server is configured. The main goal is always the same: only your public files should be web-accessible. Your system/, config/, controllers/, and models/ folders should not be directly reachable by the browser.

Recommended Layout (Public Document Root)

This is the safest option. Your hosting points the domain’s document root to public/. Only public/ is exposed, while the application code stays above it.

Folder Structure (Recommended)
project/
  noclass_app/
    controllers/
    models/
    views/
    lib/
    system/
    config/
  public/
    index.php
    .htaccess
    assets/

In this layout, public/index.php bootstraps the framework and sets BASE_PATH to the project root (one level up). Your rewrite rule lives in public/.htaccess.

App-Folder - NoClass Default (application and system in app)

This is the popular project structure which NoClass defaults to. In that case, index.php may live in the root folder, while the application and system live in a protected app folder.

Folder Structure (Root index.php)
project/
  app/
    controllers/
    models/
    views/
    lib/
    system/
    config/
  assets/
  index.php
  .htaccess

Our default .htaccess helps to protect the app folder.

Single-Folder Hosting (Index.php in Root)

Some shared hosts force the document root to the project root. In that case, index.php may live in the root folder. This works, but you must ensure sensitive folders are protected (or moved outside web root if possible).

Folder Structure (Root index.php)
project/
  index.php
  .htaccess
  system/
  config/
  controllers/
  models/
  views/
  lib/
  vendor/
  assets/

If you are forced into this layout, strongly consider: (1) denying direct access to system/, config/, and other private folders via server rules, and (2) disabling directory listing. Our default .htaccess does this already.

Subfolder Installations

If your app is installed in a subfolder (e.g. https://example.com/noclass), set BASE_URI accordingly (for example /noclass). This ensures routing and helper functions (like base_url() and asset()) generate correct URLs.

Choosing BASE_PATH and BASE_URI

BASE_PATH is a filesystem path used for safe includes (controllers, models, views). BASE_URI is a URL path prefix used for generating links in the browser.

Example (public/index.php)
<?php

// public/index.php
define('BASE_PATH', dirname(__DIR__)); // project root
require BASE_PATH . '/system/setup.php';

// If installed at domain root:
define('BASE_URI', '/');

// If installed at /noclass:
// define('BASE_URI', '/noclass');

?>

Rewrite Rules (Apache)

If you are using Apache, route all requests through index.php. Keep the rewrite rules in the folder where index.php lives.

.htaccess (Typical)
RewriteEngine On

# If index.php is in this folder:
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php?url=$1 [QSA,L]

Recommendation: if your hosting supports it, prefer the public document root layout. It reduces accidental exposure and makes deployments more secure by default.

Performance Tips

Performance in NoClass is mostly about three things: caching, minimizing database work, and tuning your web server for static assets. Start by caching expensive reads and enabling OPcache.

Good baseline: OPcache on, debug off, cache where it matters, index your DB.

1. Enable CACHE_ENGINE for Production

Production Caching
<?php

// config/config.php (production example)

// Enable cache server/engine if available
define('CACHING', CACHE_ENGINE);

// Longer TTLs in production (tune per feature)
define('CACHE_TTL_DB', 1800);      // 30 minutes
define('CACHE_TTL_ROUTE', 300);    // 5 minutes (route/page fragments)
define('CACHE_TTL_HTML', 900);     // 15 minutes

// Disable debug in production
define('DEBUG', false);
define('DB_DEBUG', false);

?>

2. Optimize Database Queries

  • Use db_select() with specific columns instead of *
  • Add appropriate indexes to frequently queried columns
  • Use db_paginate() for large datasets
  • Batch operations with db_batch_insert() and db_batchUpdate()

3. Use Stale-While-Revalidate Pattern

High-Traffic Endpoints
<?php

function homepage() {

    // Serve cached content fast (stale-while-revalidate pattern)
    $content = cache_get_stale('homepage_content', 60);

    if ($content !== null) {
        return $content;
    }

    // Cache miss: generate fresh content
    $content = generate_homepage_content();

    cache_set('homepage_content', $content, 300);

    return $content;
}

?>

4. Optimistic Locking for High Concurrency

Preventing Update Conflicts
<?php

// Add a version column to tables (example):
// ALTER TABLE products
//   ADD COLUMN version INT UNSIGNED NOT NULL DEFAULT 1;

function update_product($id, array $data, int $expectedVersion) {

    // Optimistic lock: only update if version matches
    $data['version'] = $expectedVersion + 1;

    $affected = db_update(
        'products',
        $data,
        ['id' => $id, 'version' => $expectedVersion]
    );

    if ($affected === 0) {
        return [
            'ok'      => false,
            'message' => 'Update conflict. Reload and try again.'
        ];
    }

    return ['ok' => true];
}

?>

5. Production .htaccess Optimizations

.htaccess for Performance
# Enable compression
<IfModule mod_deflate.c>
    AddOutputFilterByType DEFLATE
        text/plain
        text/html
        text/xml
        text/css
        text/javascript
        application/javascript
        application/json
        application/xml
        image/svg+xml
</IfModule>

# Browser caching (static assets)
<IfModule mod_expires.c>
    ExpiresActive On

    ExpiresByType text/css "access plus 30 days"
    ExpiresByType application/javascript "access plus 30 days"
    ExpiresByType image/png "access plus 30 days"
    ExpiresByType image/jpeg "access plus 30 days"
    ExpiresByType image/svg+xml "access plus 30 days"
</IfModule>

# Disable directory listing
Options -Indexes

Troubleshooting

Common Issues and Solutions

Issue Solution
Routes not working
  1. Check .htaccess exists in public folder
  2. Enable Apache mod_rewrite: sudo a2enmod rewrite
  3. Restart Apache: sudo systemctl restart apache2
  4. Verify AllowOverride All in Apache config
Database connection errors
  1. Check credentials in config/database.php
  2. Verify MySQL is running: sudo systemctl status mysql
  3. Test connection: mysql -u username -p database
  4. Check firewall: sudo ufw allow 3306
CSRF token errors
  1. Ensure secure_session_start() is called
  2. Check SameSite cookie settings match your domain
  3. Verify form includes csrf_field()
  4. Check token expiration (default 15 minutes)
Cache not working
  1. Check CACHING constant is set correctly
  2. For file cache: ensure cache/ folder is writable (755)
  3. For engine cache: verify Redis is running on port 6379
  4. Clear cache: delete files in cache/ folder
Views not rendering
  1. Check view file exists in correct folder
  2. Verify controller calls data() to pass data
  3. Check for syntax errors in view file
  4. Ensure view returns HTML, not JSON for AJAX

Debug Mode

Enable debug mode only on development/staging. In production, disable DEBUG and log errors instead of displaying them to users.

Enable Detailed Errors
<?php

// config/config.php
define('DEBUG', true);

// Optional: show SQL errors in debug mode only
define('DB_DEBUG', true);

// Example: quick debugging inside a controller action
function debug_action() {

    var_dump($_POST);
    var_dump($_SESSION);

    // Check database connectivity
    $ping = db_raw("SELECT 1 AS ok");

    echo '<pre>';
    print_r($ping);
    echo '</pre>';
}

?>

Error Logging

Logging makes production issues diagnosable without exposing stack traces to users. Store logs in storage/logs (or a central log system) and include structured context in JSON.

Custom Error Handling
<?php

// Simple error logging helper (file-based example)
// Recommended: store logs outside public/

function log_error(string $message, array $data = []): void {

    $dir = BASE_PATH . '/storage/logs';
    if (!is_dir($dir)) {
        mkdir($dir, 0755, true);
    }

    $file = $dir . '/app-' . date('Y-m-d') . '.log';

    $line = date('Y-m-d H:i:s') . ' | ' . $message;

    if (!empty($data)) {
        $line .= ' | ' . json_encode($data, JSON_UNESCAPED_SLASHES);
    }

    $line .= PHP_EOL;

    // Append safely
    file_put_contents($file, $line, FILE_APPEND | LOCK_EX);
}

// Usage:
// log_error('Payment failed', ['user_id' => 5, 'order_id' => 991]);

?>

Best Practices

These recommendations are based on common issues seen during real deployments and maintenance. Following them will keep NoClass apps predictable, secure, and easy to debug.

1. Code Organization

  • Keep controllers thin - move business logic to models or libraries
  • Use models for database operations (prefix functions with model name)
  • Create libraries for reusable utilities (email sending, PDF generation, etc.)
  • Use partial views for reusable HTML components
  • Follow naming conventions: controllers in PascalCase, views in lowercase

2. Security Guidelines

  • Always escape output with e() in views
  • Validate ALL user input - both client and server side
  • Use HTTPS in production with proper SSL certificates
  • Implement rate limiting for authentication endpoints
  • Store secrets in environment variables, not in code
  • Regularly update PHP and NoClass framework files

3. Database Best Practices

  • Use transactions for multiple related operations
  • Add indexes on frequently queried columns
  • Normalize data but denormalize for performance when needed
  • Use optimistic locking for high-concurrency updates
  • Implement soft deletes (add deleted_at column)
  • Backup regularly and test restoration procedures

4. Performance Optimization

  • Enable OPcache in PHP configuration
  • Use CACHE_ENGINE (Redis) for production caching
  • Minify and combine CSS/JS assets
  • Use CDN for static assets in production
  • Implement pagination for large datasets
  • Optimize images before uploading

5. Development Workflow

  • Use version control (Git) from day one
  • Implement CI/CD pipeline for automated testing and deployment
  • Write documentation for complex business logic
  • Create database migrations for schema changes
  • Monitor errors and performance in production
  • Regularly review and refactor code
Remember: NoClass is designed to be simple but powerful. Start with the basics, then gradually incorporate more advanced features as your application grows. The framework scales with your needs.

Quick Reference

This one-page cheat sheet summarises the official NoClass patterns for DB, caching, SWR, and locks. Use this for onboarding and day-to-day development.

DB helper order (official)

Parameter order rule
table → columns → conditions → options

Common reads

Reads
db_select('users'); // SELECT *

db_select('users', ['id','email']); // columns

db_select('users', ['id'], ['status' => 'active'], [
  'order_by' => 'id DESC',
  'limit'    => 20,
]);

db_select_one('users', ['id','email'], ['id' => 5]);

db_exists('users', ['email' => $email]);

db_count('users', ['status' => 'active']);

Common writes

Writes
$id = db_insert('users', ['email' => $e, 'name' => $n]);

db_update('users', ['name' => 'X'], ['id' => $id]);

db_delete('users', ['id' => $id]);

Caching & SWR

Config
define('DB_SELECT_CACHE', true);
define('DB_SELECT_CACHE_TTL', 30);

define('DB_SELECT_CACHE_SWR', true);
define('CACHE_REFRESH_DEDUP_WINDOW', 10);

Do not use SWR for strict reads like balances or one-time codes; disable cache per query when needed.

Disable cache for strict reads
$balance = db_select_one('wallets', ['balance'], ['user_id' => 42], [
  'cache' => false
]);

Locks + transactions

Recommended combined pattern
if (!db_lock('wallet:user:42', 3)) {
  throw new Exception('Resource busy, try again');
}

try {
  db_transaction(function () use ($amount) {
    db_update('wallets',
      ['balance' => ['__raw__' => '`balance` - ' . (int)$amount]],
      ['user_id' => 42]
    );
  });
} finally {
  db_unlock('wallet:user:42');
}

Common mistakes (avoid)

  • Don’t invent your own DB parameter order. Always use table → columns → conditions → options.
  • Don’t manually delete DB select cache keys—writes bump table versions automatically.
  • Don’t rely on transactions to prevent concurrent entry; use advisory locks for multi-step flows.
  • Don’t use SWR for strict reads (balances, inventory, one-time codes). Disable cache per query.