Humblee

A humble PHP framework & CMS

Extending Humblee

Humblee is designed to be extended, not forked. The /application/ directory is your workspace — everything there is autoloaded under the App\ namespace and takes precedence over its framework equivalent when a naming collision exists. The framework itself (/humblee/) should generally not be modified.


The Application Directory

application/
├── Controller/     # App\Controller\ — custom and overriding controllers
├── middleware/     # App\Middleware\ — discovered and run by the Kernel on every request
├── Model/          # App\Model\ — custom models
└── views/          # PHP include templates — not autoloaded

The App\ namespace is autoloaded by Composer PSR-4. Any class you place in application/Controller/, application/Model/, or any subdirectory under application/ with a matching namespace is immediately available everywhere.

Views are PHP include files, not classes. They are loaded by Core::view() using absolute paths — PSR-4 autoloading does not apply.


Adding a Custom Model

Create a class in application/Model/. Models are static-method collections — there is no base class to extend.

<?php
declare(strict_types=1);

namespace App\Model;

use Humblee\Foundation\Core;

class Product
{
    public static function getAll(): array
    {
        return \ORM::for_table('app_products')
            ->order_by_asc('name')
            ->find_many()
            ->as_array();
    }

    public static function getById(int $id): object|false
    {
        return \ORM::for_table('app_products')->find_one($id);
    }

    public static function save(array $data): bool
    {
        $record = $data['id']
            ? \ORM::for_table('app_products')->find_one((int) $data['id'])
            : \ORM::for_table('app_products')->create();

        if (!$record) {
            return false;
        }

        $record->name  = trim($data['name']);
        $record->price = (float) $data['price'];
        $record->save();

        return true;
    }
}

Table names for application tables are not covered by the framework's _table_* constants. Define your own constants in humblee/configuration/env_*.php, or use string literals for tables you own:

// In env_*.php:
define('_table_products', 'app_products');

Always use the ORM — never write raw SQL or concatenate user input into query strings. See Security.


Adding a Custom Controller

Public-facing page controller

When a CMS template's page type is set to controller, the framework instantiates the named class from the App\Controller\ namespace and calls the specified method. This is how you render a page that needs to fetch data before displaying.

<?php
declare(strict_types=1);

namespace App\Controller;

use Humblee\Foundation\Core;
use App\Model\Product;

class Catalog
{
    public function index(array $templateData): string
    {
        $content  = $templateData['content'];
        $page     = $templateData['page'];
        $products = Product::getAll();

        return Core::view(
            _app_server_path . 'application/views/catalog.php',
            ['content' => $content, 'page' => $page, 'products' => $products]
        );
    }
}

The method receives the CMS page context as $templateData and returns an HTML string. Pass whatever variables the view needs through Core::view().

Application AJAX endpoints

application/Controller/Request.php is your AJAX controller. It extends Humblee\Controller\Xhr and handles all URIs under request/. The second URI segment maps to the method:

POST /request/save-product  →  App\Controller\Request::saveProduct()
<?php
declare(strict_types=1);

namespace App\Controller;

use Humblee\Foundation\Core;
use Humblee\Controller\Xhr;
use Humblee\Middleware\Package;
use Humblee\Model\Crypto;
use App\Model\Product;

class Request extends Xhr
{
    public function saveProduct(): void
    {
        $this->require_hmac();
        $this->require_role(['admin', 'developer']);

        $package = Package::current();
        $data    = [
            'id'    => (int) $package->get('id', 0),
            'name'  => trim((string) $package->get('name', '')),
            'price' => (float) $package->get('price', 0),
        ];

        if (!Product::save($data)) {
            Core::json(['error' => 'Save failed'], 500);
        }

        Core::json(['status' => 'ok', 'csrf' => Crypto::get_hmac_pair()]);
    }
}

See Controllers for the complete pattern and check ordering.

Overriding a framework controller

Because the router checks App\Controller\ before Humblee\Controller\ for some routes, you can override framework behavior by creating a class with the same name in the application namespace. This is an advanced technique — be careful to preserve any behavior you are not intentionally replacing.


Adding Custom Middleware

Drop a file in application/middleware/ implementing Humblee\Middleware\Contract. The Kernel discovers it automatically — no registration needed.

<?php
declare(strict_types=1);

namespace App\Middleware;

use Humblee\Middleware\Contract;
use Humblee\Middleware\Package;

class MaintenanceMode implements Contract
{
    public function handle(Package $package): void
    {
        if (getenv('MAINTENANCE') === 'true') {
            http_response_code(503);
            include _app_server_path . 'application/views/maintenance.php';
            exit;
        }
    }
}

Middleware runs on every request, in filesystem order, before routing. Use it for concerns that apply globally: request logging, rate limiting, security headers, maintenance mode, A/B assignment, etc. See Routing & Middleware for the full pipeline.


Adding a View

Views are plain PHP files in application/views/. They receive variables from Core::view() and are responsible for rendering HTML. There is no templating engine — use PHP directly.

<?php
// application/views/catalog.php
declare(strict_types=1);

use Humblee\Foundation\Draw;
?>

<section class="catalog">
    <h1><?php Draw::content($content, 'page_title') ?></h1>

    <div class="product-grid">
        <?php foreach ($products as $product): ?>
            <div class="product-card">
                <h2><?php echo htmlspecialchars($product['name'], ENT_QUOTES, 'UTF-8') ?></h2>
                <p><?php echo number_format($product['price'], 2) ?></p>
            </div>
        <?php endforeach ?>
    </div>
</section>
  • Use Draw::content($content, 'key') for CMS-managed content blocks
  • Escape all user-controlled strings with htmlspecialchars()
  • Do not output raw HTML from untrusted sources

Views are loaded by path, not by class name. Pass the absolute path to Core::view():

Core::view(
    _app_server_path . 'application/views/catalog.php',
    ['products' => $products, 'content' => $content]
)

_app_server_path is a constant that resolves to the project root. You can also use subdirectories:

_app_server_path . 'application/views/products/detail.php'

Adding a New Admin Svelte App

When a new admin tool needs a rich frontend, create a Svelte app alongside the PHP controller changes.

  1. Copy an existing app from frontend/apps/ and rename the directory.

  2. Update vite.config.ts — change outDir to point to the new app name:

    outDir: resolve(__dirname, '../../../public/humblee/js/admin/my-new-app'),
  3. Add npm scripts to the root package.json:

    "build:my-new-app": "npm run build --workspace=frontend/apps/my-new-app",
    "dev:my-new-app":   "npm run dev   --workspace=frontend/apps/my-new-app"
  4. Build once to create the output directory:

    npm run build:my-new-app
  5. Wire the controller to load the compiled assets:

    // In humblee/src/Controller/Admin.php:
    public function myFeature(): void
    {
        $this->page_title       = 'My Feature';
        $this->extra_head_code  = '<link rel="stylesheet" href="' . _app_path . 'humblee/js/admin/my-new-app/index.css">';
        $this->extra_head_code .= '<script type="module" src="' . _app_path . 'humblee/js/admin/my-new-app/index.js"></script>';
    }
  6. Add the view template at humblee/views/admin/my-feature.php with the config injection block:

    <script>
    window.__MY_FEATURE_CONFIG__ = {
        XHR_PATH: "<?php echo _app_path ?>core-request/",
        WEB_ROOT: "<?php echo _app_path ?>",
        csrf:     <?php echo json_encode(\Humblee\Model\Crypto::get_hmac_pair()) ?>
    };
    </script>
    <div id="app"></div>

See Frontend for the complete guide to the build system and the PHP→Svelte config injection pattern.


Key Constants

These are available everywhere — defined in humblee/configuration/env_*.php:

Constant Example Purpose
_app_path / or /myapp/ URL root path for links and asset URLs
_app_server_path /var/www/html/ Filesystem path to the project root
session_key humblee Key for $_SESSION namespace
_table_pages humblee_pages Always use _table_* constants — never hardcode table names