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

v2.2.2

v2.2.1

v2.2.0

v2.1.0

v2.0.0 — Breaking Changes

v1.7.0

v1.6.0

v1.5.1

v1.5.0

v1.4.0

v1.0.0 – Initial Release

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

app/.env (change from app/.env.example)
<?php

    # ---- App ----
    BASE_URL=http://localhost  # origin only — scheme + host + port. NO subfolder path.
    APP_ENV=production       # production or development
    APP_DEBUG=false
    APP_LOG=false

    # ---- Database ----
    # Default demo does not use a database.
    USE_DB=false
    DB_HOST=localhost
    DB_PORT=3306
    DB_NAME=noclass
    DB_USER=root
    DB_PASS=

    # ---- Cache ----
    # Options: NONE, FILE, ENGINE
    CACHE_DRIVER=NONE
    FILEMAP_CACHE=true
    FILEMAP_CACHE_AUTO_REBUILD=true
config/config.php
<?php

    // Environment
    define('WEBSITE_NAME',                  'NoClass™');  // Set Website name
    define('BASE_URL',                      env('BASE_URL', 'localhost'));
    define('APP_ENV',                       env('APP_ENV', 'development'));
    define('DEBUG',                         env_bool('APP_DEBUG', APP_ENV !== 'production'));
    define('APP_DEBUG',                     DEBUG);
    define('DEFAULT_CONTROLLER',            'Home');
    ...   more setting below

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

The view contains only the page-specific HTML. NoClass wraps it in the layout automatically — no header or footer includes needed.

app/views/hello/index.php
<!-- Just the page content. NoClass wraps this in the layout automatically. -->

<div class="card">
    <h1><?php e($message) ?></h1>
    <p>Current time: <?php e($time) ?></p>
</div>
That's it! Visit http://localhost/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.NoClass can run in different hosting layouts depending on how your server is configured (See Public Folder Layouts).Below is the recommended but NoClass comes with the default as you can see below

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/ (optional)    # 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() api_ok($data) / api_err($msg)
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()) {
    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 & Layouts

Views are plain PHP files in app/views/{controller}/{action}.php. They receive variables from the controller via data() and contain only presentation logic — no database queries, no business logic.

You do not call any layout function from a view. NoClass wraps every view in its layout automatically. A view file contains only the page-specific HTML — not <html>, not <head>, not <body>. The layout provides all of that.

How automatic layout wrapping works

When a controller action runs, NoClass:

  1. Renders the view file (views/products/index.php) into a buffer
  2. Resolves which layout to use (see Layout Resolution below)
  3. Loads the layout file and calls view_content() inside it to output the buffered view

The view never needs to know which layout wraps it. The layout never needs to know which view it is wrapping.

A typical view file

app/views/products/index.php
<!-- This is the ENTIRE view file. No html/head/body tags needed. -->
<!-- NoClass wraps this automatically in the layout. -->

<h1><?php e($page_title) ?></h1>

<?php if (empty($products)): ?>
    <p>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>
                <a href="<?= url('products/show/' . $product['id']) ?>">
                    View Details
                </a>
            </div>
        <?php endforeach ?>
    </div>
<?php endif ?>

The layout file

The layout lives in app/views/layouts/main.php (or whichever layout name is configured). It wraps every view and calls view_content() where the rendered view output should appear.

app/views/layouts/main.php
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title><?php e($page_title ?? 'My App') ?></title>
    <meta name="base-url"   content="<?= url('') ?>">
    <meta name="csrf-token" content="<?= csrf_token() ?>">
    <link rel="stylesheet" href="<?= asset('css/app.css') ?>">
</head>
<body>
    <nav>...</nav>

    <main>
        <?= view_content() ?>    <!-- view output goes here -->
    </main>

    <footer>...</footer>
    <script src="<?= asset('js/noclass.js') ?>"></script>
</body>
</html>

Layout resolution — six priority levels

NoClass resolves which layout to use in this order (first match wins):

PriorityHowExample
1 (highest)layout_off() called inside the viewNo layout at all — raw view output
2layout('name') called inside the viewOverride to views/layouts/admin.php
3'layout' => false in routes.phpNo layout for all actions on that route
4'layout' => 'portal' in routes.phpNamed layout for all actions on that route
5DEFAULT_LAYOUT constant in config.phpApp-wide default layout name
6 (lowest)views/layouts/main.php conventionFallback when nothing else is set
Layout control from a view (when you need to override)
<?php layout_off(); ?>
<!-- This view renders with no layout at all (e.g. an AJAX partial response) -->

<?php layout('admin'); ?>
<!-- This view uses views/layouts/admin.php regardless of the route setting -->
Layout control from routes.php (applies to all actions on the route)
return [
    // API route — no layout
    'api' => [
        'controller' => 'Api',
        'action'     => ['validate'],
        'middleware' => [],
        'layout'     => false,
    ],

    // Admin route — named layout
    'dashboard' => [
        'controller' => 'Dashboard',
        'action'     => ['index'],
        'middleware' => ['AuthAdmin'],
        'layout'     => 'admin',    // uses views/layouts/admin.php
    ],
];

Partials — reusable view components

Partials are small, reusable view fragments — not layout files. Think of them as components: a breadcrumb bar, a product card, a search widget, an alert box, a pagination control. They are independent HTML fragments that can be dropped anywhere inside a view.

Partials are not the same as layouts. A layout provides the full page wrapper (<html>, <head>, <body>). A partial is a small reusable fragment placed inside the view content area. Never put <html>, <head>, or <body> in a partial.

Partials live in app/views/partials/. Use NoClass's partial() helper to include them — it handles path resolution automatically and passes data cleanly.

app/views/partials/breadcrumb.php
<!-- This is a PARTIAL — just a fragment, no html/head/body -->
<nav class="breadcrumb" aria-label="breadcrumb">
    <a href="<?= url('') ?>">Home</a>
    <?php foreach ($crumbs as $crumb): ?>
        <span> / </span>
        <?php if (!empty($crumb['url'])): ?>
            <a href="<?= url($crumb['url']) ?>"><?php e($crumb['label']) ?></a>
        <?php else: ?>
            <span><?php e($crumb['label']) ?></span>
        <?php endif ?>
    <?php endforeach ?>
</nav>
app/views/partials/product_card.php
<div class="product-card">
    <h3><?php e($product['name']) ?></h3>
    <p><?php e($product['description']) ?></p>
    <a href="<?= url('products/show/' . $product['id']) ?>">View</a>
</div>
Using partials from a view with partial()
<!-- app/views/products/index.php -->

<?php partial('breadcrumb', [
    'crumbs' => [
        ['label' => 'Products', 'url' => 'products'],
        ['label' => $page_title],   // last item has no url = current page
    ]
]) ?>

<h1><?php e($page_title) ?></h1>

<div class="product-grid">
    <?php foreach ($products as $product): ?>
        <?php partial('product_card', ['product' => $product]) ?>
    <?php endforeach ?>
</div>

partial('name', $data) looks for the file in app/views/partials/name.php (or the current module's views/partials/ folder for HMVC modules). The $data array is extracted into local variables inside the partial.

Section slots — injecting content into the layout

Section slots let a view inject content into named slots defined in the layout — useful for per-page titles, additional stylesheets, or page-specific scripts that belong in <head> but are decided by the view.

Layout — define named slots
<head>
    <title><?php e($page_title ?? 'My App') ?></title>
    <link rel="stylesheet" href="<?= asset('css/app.css') ?>">
    <?= yield_section('head_extras', '') ?>  <!-- optional per-page additions -->
</head>
<body>
    <main><?= view_content() ?></main>
    <?= yield_section('scripts', '') ?>       <!-- optional per-page scripts -->
</body>
View — fill named slots
<?php section('head_extras') ?>
    <link rel="stylesheet" href="<?= asset('css/charts.css') ?>">
<?php end_section() ?>

<h1>Dashboard</h1>
<div id="chart"></div>

<?php section('scripts') ?>
    <script src="<?= asset('js/charts.js') ?>"></script>
<?php end_section() ?>

View helpers reference

FunctionWhere calledPurpose
e($value) View HTML-escape and echo. Use for ALL user/DB output.
view_content() Layout only Outputs the rendered view content inside the layout.
partial('name', $data) View or layout Include a partial from views/partials/. Data extracted as variables.
layout_off() View Suppress layout for this view (e.g. AJAX partial responses).
layout('name') View Override to a named layout for this view only.
section('name') View Start capturing content for a named layout slot.
end_section() View End the current section capture.
yield_section('name', $default) Layout Output a named slot (or default if the view didn't fill it).
url($path) Anywhere Internal URL. Always use this for links — handles BASE_URI correctly.
asset($file) Anywhere Asset URL respecting BASE_URI, CDN_URL, and cache-busting.
view_path() is an internal path resolver — it is not intended for use in application views. Do not use require view_path('something.php') to load partials or layouts. Use partial('name') for partials. Layouts are handled automatically.

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

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:

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)

Session Helper — session()

session() reads and writes $_SESSION using the same call style as data() and flash(). All three request-state helpers are consistent.

CallEffect
session('user_id')Get — returns value or null if absent
session('user_id', 42)Set $_SESSION['user_id'] = 42
session('user_id', null)Clear — unsets the key
session()Get entire $_SESSION array
Note: session() does not start or destroy the session. The bootstrap always calls secure_session_start() first. Never call session_start() yourself in application code.

Middleware usage

middleware/Auth.php
<?php

function Auth(): bool
{
    if (!session('user_id')) {
        redirect(url('login'));
        return false;
    }
    return true;
}

Login and logout patterns

Login
session_regenerate_id(true);        // prevent session fixation
session('user_id',   $user['id']);
session('user_role', $user['role']);
Logout
session_destroy();
session_start();
session_regenerate_id(true);

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.

Public API — 27 functions

db_raw() is internal only. Application code always uses the 27 public functions below. Security (anomaly detection) is built in to every call — there is no separate "secure" variant.

Structured reads
db_select($table, $cols='*', $conds=[], $order='', $limit='', $joins=[])
db_find($table, $id, $keyCol='id')           // find by primary key → ?array
db_find_by($table, $col, $value)             // find by any column  → ?array
db_count($table, $conds=[])                  // → int
db_exists($table, $conds)                    // → bool
db_pluck($table, $col, $conds=[], $order='') // column values → array
db_aggregate($table, $func, $col, $conds=[]) // SUM, AVG, MAX, MIN
db_paginate($table, $cols, $conds, $page, $perPage, $order='', $joins=[])
db_search($table, $searchCols[], $term, $conds=[])
Structured writes
db_insert($table, $data)                     // → int (inserted ID)
db_batch_insert($table, $rows)               // → int (affected)
db_update($table, $data, $conds)             // → int (affected)
db_batch_update($table, $keyCol, $rows)
db_upsert($table, $data, $updateCols)
db_delete($table, $conds)                    // → int (affected)
db_increment($table, $col, $by, $conds)
Raw SQL (array params — not variadic spread)
db_fetch_all($sql, array $params=[])
db_fetch_one($sql, array $params=[])
db_fetch_value($sql, array $params=[], $default=null)
db_fetch_column($sql, array $params=[])
db_fetch_map($sql, array $params=[], $valueCol=null)
db_exec($sql)

// Streaming
db_chunk($sql, $params, $chunkSize, $callback)

// Transactions
db_tx($fn)          // auto commit/rollback
db_transaction($fn) // alias
Removed in v2.0.0: db_raw_secure() no longer exists. Use db_fetch_all($sql, $params) for raw queries. Security is always on.

Quick examples

Read examples
// Find by primary key
$user = db_find('users', $id);

// Find by any column
$user = db_find_by('users', 'email', $email);

// Select with conditions, order, limit
$rows = db_select('users', ['id', 'email'], ['status' => 'active'],
                  'id DESC', '20');

// Count, exists, pluck
$count   = db_count('users', ['status' => 'active']);
$exists  = db_exists('users', ['email' => $email]);
$emails  = db_pluck('users', 'email', ['role' => 'admin']);

// Paginate (returns [data, pagination])
$result  = db_paginate('users', '*', [], 1, 25);
Write examples
$id = db_insert('users', ['email' => 'jane@example.com', 'name' => 'Jane']);

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

db_increment('users', 'login_count', 1, ['id' => $id]);

db_delete('users', ['id' => $id]);
Raw SQL with array params
$rows = db_fetch_all(
    "SELECT u.*, p.avatar FROM users u
     LEFT JOIN profiles p ON p.user_id = u.id
     WHERE u.status = ? ORDER BY u.name ASC",
    ['active']
);

$total = db_fetch_value("SELECT COUNT(*) FROM orders WHERE user_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:

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
}

?>

Database Migrations

NoClass includes a self-bootstrapping migration engine. It runs from the CLI and is never loaded by the framework bootstrap — it is a deployment tool only.

CLI commands

From your project web root
php app/system/migrate.php              # run all pending
php app/system/migrate.php status       # show applied / pending
php app/system/migrate.php run 002      # run by prefix
php app/system/migrate.php rollback     # reverse last batch
php app/system/migrate.php fresh        # drop all + re-run (non-prod only)
php app/system/migrate.php make name    # create numbered file
Windows: Use migrate.bat at the project root, or always use forward slashes — backslashes collapse in Git Bash and cause path errors.

Migration file format

database/migrations/002_add_bio.sql
-- @up
ALTER TABLE users ADD COLUMN bio TEXT NULL;

-- @down
ALTER TABLE users DROP COLUMN bio;

Files without @up/@down markers are treated as @up entirely. The @down section is optional — migrations without it are skipped during rollback.

Tracking and safety

Running from an admin panel

Use nc_migrate_cmd_run() directly rather than exec(). exec() is unreliable on Windows and may be disabled on shared hosting.

Admin panel migration runner
require_once BASE_PATH . '/system/migrate.php';
ob_start();
$db = nc_migrate_connect();
nc_migrate_ensure_table($db);
nc_migrate_cmd_run($db, null);
$output = ob_get_clean();

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

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()) {
        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()) 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()) {
    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.

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.

noclass.js — HTTP Client

noclass.js ships with NoClass (v2.1.0+) — zero-dependency, vanilla ES6. It handles CSRF automatically, maps to api_ok()/api_err(), and provides helpers for flash messages, navigation, and form serialisation.

Setup (required in every layout <head>)

views/layouts/main.php
<meta name="base-url"   content="<?= url('') ?>">
<meta name="csrf-token" content="<?= csrf_token() ?>">
<script src="<?= asset('js/noclass.js') ?>"></script>

HTTP methods

API
nc.get(path, opts?)
nc.post(path, data?, opts?)    // plain object or FormData
nc.put(path, data?, opts?)
nc.patch(path, data?, opts?)
nc.delete(path, opts?)

All return a Promise: resolves with data on {ok:true}, rejects with Error on {ok:false}.

Critical: nc.post() sends Content-Type: application/json. $_POST is empty for JSON requests. Use input('key') not $_POST['key'] in AJAX-targeted controllers.

Full example

View JavaScript
nc.post('products/status/' + id, { status: 'live' })
  .then(function(data) {
      document.getElementById('status-badge').textContent = data.status;
      nc.flash(data.message, 'success');
  })
  .catch(function(err) { nc.flash(err.message, 'danger'); });
AJAX controller (PHP)
function status($id)
{
    if (!csrf_verify()) { api_err('Invalid request token.'); return; }
    $newStatus = input('status');   // NOT $_POST
    if (!in_array($newStatus, ['draft', 'live', 'archived'])) {
        api_err('Invalid status.'); return;
    }
    db_update('products', ['status' => $newStatus], ['id' => (int)$id]);
    api_ok(['status' => $newStatus, 'message' => 'Status updated.']);
}

Helpers

FunctionPurpose
nc.flash(msg, type, ttl)Inline flash — type: success/danger/warning/info
nc.redirect(path)Navigate to a route
nc.reload()Reload current page
nc.confirm(msg, fn)Confirm dialog then call fn
nc.serialize(form)<form> element → plain object
nc.url(path)Full URL respecting BASE_URI

Deployment &Amp; Security Checklist

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

Web Server

PHP

App Settings

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

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 ----
  BASE_URL=localhost       # set you domain or subfolder e.g noclass.org or noclass.org/subfolder
  APP_ENV=production       # production or development
  APP_DEBUG=false
  APP_LOG=false

  # ---- Database ----
  # Default demo does not use a database.
  USE_DB=false
  DB_HOST=localhost
  DB_PORT=3306
  DB_NAME=noclass
  DB_USER=root
  DB_PASS=

  # ---- Cache ----
  # Options: NONE, FILE, ENGINE
  CACHE_DRIVER=NONE
  FILEMAP_CACHE=true
  FILEMAP_CACHE_AUTO_REBUILD=true

Best practice

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.

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:

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

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

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)
{
    if (!session('user_id')) {
        redirect(url('login'));
        return false;
    }

    if ($role && (session('user_role') !== $role)) {
        api_err('Forbidden', 403);
        return false;
    }

    return true;
}

Official rules

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

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

2. Security Guidelines

3. Database Best Practices

4. Performance Optimization

5. Development Workflow

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
// Find by key or column
$user = db_find('users', $id);
$user = db_find_by('users', 'email', $email);

// Select with conditions
$rows = db_select('users', ['id','email'], ['status' => 'active'], 'id DESC', '20');

// Aggregates
$count  = db_count('users', ['status' => 'active']);
$emails = db_pluck('users', 'email', ['role' => 'admin']);
$result = db_paginate('users', '*', [], 1, 25);

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

Common writes

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

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

db_increment('users', 'login_count', 1, ['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)