A humble PHP framework & CMS
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.
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.
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.
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/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.
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.
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.
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>
Draw::content($content, 'key') for CMS-managed content blockshtmlspecialchars()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'
When a new admin tool needs a rich frontend, create a Svelte app alongside the PHP controller changes.
Copy an existing app from frontend/apps/ and rename the directory.
Update vite.config.ts — change outDir to point to the new app name:
outDir: resolve(__dirname, '../../../public/humblee/js/admin/my-new-app'),
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"
Build once to create the output directory:
npm run build:my-new-app
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>';
}
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.
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 |