Humblee

A humble PHP framework & CMS

Controllers

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.


Class Hierarchy

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


Admin Page Controllers

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.

How data reaches the view

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.

Adding a Svelte SPA to an admin page

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.


XHR Controllers

Humblee\Controller\Xhr is the base class for all JSON endpoints. Its constructor sets no-cache response headers automatically.

Inherited methods

$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.


Adding Application AJAX Endpoints

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(),
        ]);
    }
}

Required pattern for every XHR endpoint

  1. require_hmac() first — validate the CSRF token before reading any POST data
  2. require_role() — check authorization
  3. Validate and cast all input — cast integers, trim strings, check for required fields
  4. Return a fresh CSRF pair — include 'csrf' => Crypto::get_hmac_pair() in every successful response so the frontend can make the next request

See Security for the full rationale on check ordering.


Reading Request Data

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.


Template Controller (Public Pages)

Humblee\Controller\Template handles all URIs not claimed by another prefix. It:

  1. Parses the URI into a slug (stripping any i18n prefix if configured)
  2. Queries the database for a matching page record
  3. Loads content blocks via Content::findContent() with the resolved personalization ID
  4. Renders the page using the template's configured view file or controller

You do not typically extend or modify this controller. CMS-managed pages are configured through the admin interface.