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.
Key Advantages
- No Classes, Pure Functions: Easy to learn and debug
- Built-in Caching: File-based and in-memory engine with automatic invalidation
- Security First: CSRF protection, XSS prevention, and secure session handling
- Flexible Routing: Pattern-based routes with parameter validation
- Procedural MVC: Clear separation of concerns without complex OOP hierarchies
- Zero Dependencies: Works out of the box with minimal setup
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.
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
- Controller file:
controllers/Blog.php - Action function:
index(),show(), etc. - Default view mapping:
views/blog/index.php
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.
- Models:
user_*,blog_*,order_*inmodels/ - Libraries:
email_*,security_*,http_*inlib/ - Middleware helpers:
auth_*style helpers inmiddleware/if needed
// 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
- New:
session()helper infunc.php— read/write/clear$_SESSIONkeys using the samedata()/flash()call style. - Docs:
APP_STRUCTURE.mdupdated with session() usage and login/logout patterns.
v2.2.1
- Fix:
base_url()now extracts origin only viaparse_url()— fixes doubled subfolder paths whenBASE_URLincluded the subfolder path. - New:
base_url_full()— returns the complete configuredBASE_URLfor webhooks and OAuth callbacks. - Fix:
url()now preserves query strings —url('route?key=val')works correctly. - Docs:
.env.exampleandAPP_STRUCTURE.mdupdated withBASE_URLorigin-only rule.
v2.2.0
- Fix:
csrf_verify()now sendsX-CSRF-Tokenresponse header after rotation — fixes every secondnc.post()call failing. - New:
migrate.batWindows convenience wrapper.
v2.1.0
- New:
noclass.js— zero-dependency HTTP client with automatic CSRF,nc.flash(),nc.redirect()and more. - Fix:
.env.examplemoved toapp/folder to match correctBASE_PATHlocation.
v2.0.0 — Breaking Changes
- Breaking:
db_raw_secure()removed — security now always on insidedb_raw(). - Breaking:
db_fetch_*()signatures changed from(mysqli_stmt)to(string $sql, array $params = []). - Breaking:
declare(strict_types=1)removed from framework system files. - New: Full database layer redesign — 27 public functions including
db_find,db_find_by,db_paginate,db_search,db_tx, and more. - New: Self-bootstrapping migration engine (
php app/system/migrate.php). - New: PHP 7.4+ compatibility enforced across all system files.
v1.7.0
- Fix:
{slug}route validation —ctype_alnum() === 1was always false, causing all slug routes to return 404. - Fix: All cache driver
lockrow()functions now return 1 — was causing "Could not acquire lock" on every update. - Fix:
db_raw_secure()regex scanner replaced with string-manipulation scanner (no false positives).
v1.6.0
- New:
init/auto-loader — files inapp/init/required at boot, alphabetically. - New:
db_fetch_all/one/value/column/mapresult helpers. - New:
APP_STRUCTURE.mddocumentation reference. - Fix: PHP parse errors in
config.phpandfunc.php.
v1.5.1
- Fix:
Route.phpfunction_existsguards to prevent redeclaration fatal errors withmodules.php.
v1.5.0
- New: Cache driver system — NONE, FILE, ENGINE drivers.
- New: Full HMVC module management system (
modules.php). - New:
php.iniandphp-development.iniOPcache configuration files.
v1.4.0
- Fix:
BASE_PATHguard moved before firstrequire_onceinsetup.php. - Fix: CSP made opt-in via
USE_CSPconstant. - Fix:
asset()routes throughurl()to respectBASE_URIin subfolder deployments. - Fix: Controller/action state moved from session to
$GLOBALS(no more cross-request bleed). - Fix:
csrf_verify()auto-extracts token from request (no manual pass required). - New: Layout, partial, and section slot system (
view_helpers.php).
v1.0.0 – Initial Release
- First official release of NoClass framework.
- Procedural MVC architecture.
- Routing, controllers, models, views, middleware.
- Database helpers, caching, security features.
Quick Start (For The Impatient)
1. Installation
# 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
<?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
<?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
<?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
<?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.
<!-- 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>
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
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
<?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
<?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
]
];
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.
- Web server rewrite: All requests are rewritten to
public/index.php. - Bootstrap: NoClass loads config, helpers, error handling, and connects to the database (if enabled).
- Routing: The router parses the URL into
{controller}/{action}/{params...}. - Middleware (optional): Middleware runs before the action (e.g., auth checks).
- Controller action: The action function runs and may call models/libs.
- 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)
- View render: Values passed via
data()are extracted into the view scope.
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
- Avoid: SQL queries, complex loops, or validation in view templates.
- Prefer: Keep views for output, move logic to controllers/models/libs.
2) Using $_GET/$_POST everywhere
- Avoid: Reading raw input in many places.
- Prefer: Centralize validation/sanitization inside controllers or a small input helper.
3) Allowing undeclared actions in production
- Avoid: Exposing every function in a controller as a public action.
- Prefer: Set
ALLOW_UNDECLARED_ACTIONS=falseand declare actions inroutes.php.
4) Using chmod 777
- Avoid: World-writable permissions.
- Prefer: Correct owner/group +
755/750, and only write access where required.
5) Treating cache like a database
- Avoid: Storing critical source-of-truth data only in cache.
- Prefer: DB is truth; cache is acceleration. Always plan invalidation.
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 |
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:
<?php
return [
'blog' => [
'controller' => 'Blog',
'action' => 'index',
],
];
2) Create the controller
Create controllers/Blog.php and implement an action function (e.g. indexAction):
<?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.
<?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).
<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 Blog → indexAction(),
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
<?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.
<html>,
not <head>, not <body>. The layout provides all of that.
How automatic layout wrapping works
When a controller action runs, NoClass:
- Renders the view file (
views/products/index.php) into a buffer - Resolves which layout to use (see Layout Resolution below)
- 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
<!-- 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.
<!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):
| Priority | How | Example |
|---|---|---|
| 1 (highest) | layout_off() called inside the view | No layout at all — raw view output |
| 2 | layout('name') called inside the view | Override to views/layouts/admin.php |
| 3 | 'layout' => false in routes.php | No layout for all actions on that route |
| 4 | 'layout' => 'portal' in routes.php | Named layout for all actions on that route |
| 5 | DEFAULT_LAYOUT constant in config.php | App-wide default layout name |
| 6 (lowest) | views/layouts/main.php convention | Fallback when nothing else is set |
<?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 -->
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.
<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.
<!-- 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>
<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>
<!-- 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.
<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>
<?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
| Function | Where called | Purpose |
|---|---|---|
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
- 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).
data() for view data only.
Do not use it as a global state store for business logic.
Set values (controller)
<?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
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.
data('title', 'Blog');
// later, maybe in a helper or middleware:
data('csrf', csrf_token());
data('flash', flash_get_all());
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.
<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
// 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
data('posts', [
['id' => 1, 'title' => 'Hello'],
['id' => 2, 'title' => 'World'],
]);
<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.
Session Helper — session()
session() reads and writes $_SESSION using the same call style as
data() and flash(). All three request-state helpers are consistent.
| Call | Effect |
|---|---|
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 |
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
<?php
function Auth(): bool
{
if (!session('user_id')) {
redirect(url('login'));
return false;
}
return true;
}
Login and logout patterns
session_regenerate_id(true); // prevent session fixation
session('user_id', $user['id']);
session('user_role', $user['role']);
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:
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:
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.
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=[])
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)
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
db_raw_secure() no longer exists.
Use db_fetch_all($sql, $params) for raw queries. Security is always on.
Quick 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);
$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]);
$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
$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.
$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.
// 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.
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
<?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
<?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
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
migrate.bat at the project root, or always use forward
slashes — backslashes collapse in Git Bash and cause path errors.
Migration file format
-- @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
- Applied migrations are recorded in the
nc_migrationstable. - Override the tracking table name with
define('NC_MIGRATIONS_TABLE', 'my_migrations'). freshis blocked whenAPP_ENV=production.- All activity is logged to
storage/logs/migrations.log.
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.
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
<?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.
<?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
<?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)
<?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
<?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();
?>
- 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
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):
table → columns → where → options
This mirrors SQL and keeps common queries readable.
$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.
// 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.
define('DB_SELECT_CACHE', true);
define('DB_SELECT_CACHE_TTL', 30);
$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.
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.
db_lock(string $key, int $timeoutSeconds = 5): bool
db_unlock(string $key): void
Naming convention (mandatory): <context>:<entity>:<identifier>
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.
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
<?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
<?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
<?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.
$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).
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:
define('TABLE_GRIDJS_INLINE', false);
Then attach Grid.js config as data-* attributes using table_gridjs_attrs() and initialise via an external JS file.
$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();
// 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 }).
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
<?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
e() when outputting user data in views!
<?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.
<?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
<?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.lockand 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).
<?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.
<?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.
// Example flags (adapt to your config file)
define('ASSET_CACHE_BUST', true);
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>)
<meta name="base-url" content="<?= url('') ?>">
<meta name="csrf-token" content="<?= csrf_token() ?>">
<script src="<?= asset('js/noclass.js') ?>"></script>
HTTP methods
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}.
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
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'); });
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
| Function | Purpose |
|---|---|
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
- 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/, andvendor/if misconfigured.
PHP
- Enable OPcache and set
DEBUGtofalse. - Set a strong
session.cookie_samesitepolicy and use HTTPS for authenticated apps. - Limit upload sizes and validate MIME type + extension for uploads.
App Settings
- Set
ALLOW_UNDECLARED_ACTIONStofalseto prevent accidental exposure of functions. - Use
CACHE_ENGINEwhere 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
- Wrong BASE_URI: If your app is in a subfolder, BASE_URI must match it exactly (e.g.,
/noclass). - DocumentRoot set to project root: This can expose config and system files.
- Permissions too open: Avoid
chmod 777. Prefer correct owner/group and755/750. - CDN misconfig: Set
CDN_URLonly for static assets, not for application routes.
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.
<?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.
<?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.
<?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).
.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 insidepublic/. - If your host forces document root to the project root, store
.envone 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.
# ---- 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
- 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 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.
<?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.
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.lockand upgrade vendors deliberately. - Never map or scan
vendor/in NoClass file-maps: Composer handles vendor autoloading.
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
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.
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:
<?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).
<?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
- Middleware should be fast and side-effect free where possible.
- Use NoClass helpers for input/sessions/responses instead of direct
$_POST/echowhere possible. - Middleware may return a response (redirect / JSON error) to stop the request, or return
nullto 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.
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.
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).
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.
<?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.
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
<?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()anddb_batchUpdate()
3. Use Stale-While-Revalidate Pattern
<?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
<?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
# 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 |
|
| Database connection errors |
|
| CSRF token errors |
|
| Cache not working |
|
| Views not rendering |
|
Debug Mode
Enable debug mode only on development/staging. In production, disable DEBUG and log errors instead of displaying them to users.
<?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.
<?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_atcolumn) - 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
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)
table → columns → conditions → options
Common 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
$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
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.
$balance = db_select_one('wallets', ['balance'], ['user_id' => 42], [
'cache' => false
]);
Locks + transactions
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.