A humble PHP framework & CMS
Controllers handle incoming requests and produce responses. Humblee has two distinct controller roles: admin page controllers that render HTML views, and XHR controllers that respond with JSON. The framework provides base classes for both, and the application layer extends them.
Humblee\Controller\Xhr ← Base for all AJAX (JSON) controllers
├── Humblee\Controller\Request ← Core CMS AJAX (core-request/)
└── App\Controller\Request ← Your application AJAX (request/)
Humblee\Controller\Admin ← CMS admin panel pages (admin/)
Humblee\Controller\User ← Authentication pages (user/)
Humblee\Controller\Media ← File serving and decryption (media/)
Humblee\Controller\Template ← Public CMS pages (catch-all)
For most feature work you will interact with two of these: Admin (if you are adding an admin page) and App\Controller\Request (if you are adding an AJAX endpoint).
Humblee\Controller\Admin handles all URIs under admin/. Its constructor checks for the admin or developer role and redirects to the login page if the check fails.
Admin action methods set public properties on the controller instance. After the method returns, the framework calls Core::view(), which reads all public properties with get_object_vars($this) and passes them to the view template as variables.
// In Humblee\Controller\Admin (humblee/src/Controller/Admin.php):
public function pages(): void
{
$this->page_list = Pages::getAll();
$this->page_title = 'Pages';
// Core::view() is called automatically — do not call it here
}
The corresponding view at humblee/views/admin/pages.php receives $page_list and $page_title as local variables.
When an admin page needs a Svelte frontend tool, set $this->extra_head_code to load the compiled assets:
public function myFeature(): void
{
$this->page_title = 'My Feature';
$this->extra_head_code = '<link rel="stylesheet" href="' . _app_path . 'humblee/js/admin/my-feature/index.css">';
$this->extra_head_code .= '<script type="module" src="' . _app_path . 'humblee/js/admin/my-feature/index.js"></script>';
}
The admin layout template outputs $extra_head_code inside <head>, so the module loads before the page body is interactive.
Humblee\Controller\Xhr is the base class for all JSON endpoints. Its constructor sets no-cache response headers automatically.
$this->require_hmac(); // 401 + exit if CSRF token invalid
$this->require_role('admin'); // 403 + exit if role missing
$this->require_role(['admin', 'content']); // 403 + exit if user holds none of these
All JSON responses use the static Core::json() method, which is available everywhere:
Core::json(['status' => 'ok', 'data' => $result]); // 200
Core::json(['status' => 'created'], 201); // 201
Core::json(['error' => 'Not found'], 404); // 404
Core::json() sets Content-Type: application/json, JSON-encodes the array, and exits.
Your application's AJAX endpoints live in application/Controller/Request.php. This class extends Humblee\Controller\Xhr and handles all URIs under request/. The second URI segment maps to the method: POST /request/save-profile calls Request::saveProfile() (camelCase conversion applies — the router lowercases the segment and maps underscores to camelCase if needed; exact behavior depends on your method name).
<?php
declare(strict_types=1);
namespace App\Controller;
use Humblee\Foundation\Core;
use Humblee\Controller\Xhr;
use Humblee\Middleware\Package;
class Request extends Xhr
{
public function saveProfile(): void
{
$this->require_hmac();
$this->require_role('login');
$package = Package::current();
$user_id = (int) $package->get('user_id');
if ($user_id <= 0) {
Core::json(['error' => 'Invalid user ID'], 400);
}
$record = \ORM::for_table(_table_users)->find_one($user_id);
if (!$record) {
Core::json(['error' => 'Not found'], 404);
}
$record->display_name = trim((string) $package->get('name', ''));
$record->save();
Core::json([
'status' => 'ok',
'csrf' => \Humblee\Model\Crypto::get_hmac_pair(),
]);
}
}
require_hmac() first — validate the CSRF token before reading any POST datarequire_role() — check authorization'csrf' => Crypto::get_hmac_pair() in every successful response so the frontend can make the next requestSee Security for the full rationale on check ordering.
Always use Package::current() — never read $_POST, $_GET, or php://input directly.
use Humblee\Middleware\Package;
$package = Package::current();
$id = $package->get('id'); // null if missing
$id = $package->get('id', 0); // with default
$name = (string) $package->get('name', '');
$method = $package->method(); // 'GET' | 'POST' | 'PUT' | ...
$all = $package->all(); // all input as array
Package normalizes form data and JSON request bodies identically, so your controller code does not need to know whether the request came from a form or a JavaScript fetch() with Content-Type: application/json.
Humblee\Controller\Template handles all URIs not claimed by another prefix. It:
Content::findContent() with the resolved personalization IDYou do not typically extend or modify this controller. CMS-managed pages are configured through the admin interface.