# System Overrides & Boot Hooks (the escape hatch)

> The last-resort customization mechanism: replacing a framework/helper class that has no per-type loader, and running your own PHP at boot — defining global h…

Source: CBX documentation, version 4.0 preview (unreleased). Canonical page: https://docs.configbox.at/docs/4.0-preview/customization/system_overrides_and_boot_hooks. Last updated 2026-08-14.

---
The **last-resort** customization mechanism: replacing a framework/helper **class** that has no per-type
loader, and running your own **PHP at boot** (or at DB-connect) — defining global hook functions, registering
RequireJS paths, seeding settings. These are the most powerful and the most dangerous tools in the layer,
because a class override replaces the *whole* class and you inherit the job of keeping it in sync with every
core update.

Reach for this only when a targeted mechanism doesn't fit. The earlier guides route here for one core
need: **changing the behavior of an existing core class** — a controller, model, or view — which the
per-type `controllers/`/`models/`/`views/` folders can't do (they're core-first, so a same-named file
there is never loaded). It is also where the **boot-time hook functions** other guides reference live
(`cbGetCustomRequirePaths`, `getPostDbConnectQueries`, …).

Read `com_configbox_customization_overview.md` first. All paths are relative to the component root
`docroot/components/com_configbox/`. Source references are point-in-time (component **3.4.1**) — verify
against the code.

---

## 1. Why this is the escape hatch (use a targeted mechanism first)

Before using anything here, confirm none of these fits — they're smaller and don't drift:

| To change… | Prefer | Not this |
|---|---|---|
| Add a **new** controller (no core file of that name) | customization `controllers/` | system_overrides |
| A model's **data/fields** | `model_property_customization/` | system_overrides |
| A model's behavior by **reacting** to store/copy/delete | an **observer** | system_overrides |
| A screen's markup | a **template** override | system_overrides |
| Displayed text | `language_overrides/` | system_overrides |

`system_overrides/` is justified when the thing you must change is a **framework or helper class** (e.g. a
`Kenedo*` class, a `Configbox*Helper`) that no per-type loader covers, **and** reacting/extending isn't
enough. Even then, prefer asking the vendor for a hook.

---

## 2. How the boot override works (and its one hard limit)

CBX's class autoloader is **lazy**: `init.php` only *registers* class→path mappings
(`KenedoAutoload::registerClass()`), and a class file is `require`d **the first time that class is
referenced** (`KenedoAutoload::loadClass()`, `external/kenedo/classes/KenedoAutoload.php:6-14`). Nothing
core is loaded just by booting.

At the end of boot, `init.php` fires `onConfigboxInitialized` (`helpers/init.php:98`), which runs
`ObserverSystem::onConfigboxInitialized()` → **`ConfigboxOverridesHelper::loadOverrideFiles()`**
(`observers/System.php:14`). That method **eagerly `require_once`'s** your override files
(`helpers/overrides.php:4-44`). So a class you *define* in a `system_overrides/` file is **already in
memory** before core code first references that class name — and because the class now exists, PHP's
autoloader is **never invoked for it**, so the core file that would have defined it is **never loaded**.
That's the whole trick: **you occupy the class name first.**

```php
// helpers/overrides.php (abridged) — runs at onConfigboxInitialized
$folder = KenedoPlatform::p()->getDirCustomizationSettings();          // 1) settings dir first
foreach (sorted($folder/*.php) as $file) require_once($file);

$folder = KenedoPlatform::p()->getDirCustomization().'/system_overrides';  // 2) then system_overrides
foreach (sorted($folder/*.php) as $file) require_once($file);          // (display_errors on for these)
```

> ⚠️ **The hard limit: a class can't already be loaded.** Eager pre-emption only works for classes **not
> yet referenced** when `loadOverrideFiles()` runs. If some code touched the core class *before*
> `onConfigboxInitialized` (so its file was already `require`d), then defining the same class name in
> `system_overrides/` causes a **fatal "cannot redeclare class"**. In practice most domain/helper classes
> are loaded well after boot, so this works — but **verify** your target isn't loaded during early boot,
> and test.

Two ways to define the winning class (§3): a **wholesale replacement** (declare the class from scratch,
reproducing core methods) or — when you can locate the core file — a **subclass**: `require` the core
file, `class_alias()` it to a side name, then declare the original name extending the alias. The subclass
form keeps `parent::` available and is much thinner; it works well for component classes
(controllers/models/views) whose file path you know. Use wholesale replacement for framework/helper classes
with no convenient core file to require.

### 2.1 Load order & the `_` prefix

Within each folder, files are sorted with **underscore-prefixed names first**, then case-insensitive alpha
(`helpers/overrides.php:13-18,28-33`). Use a leading `_` on a file that must load before the others (e.g. a
base class your other overrides extend). The **settings dir loads before `system_overrides/`**, so settings
files can define things the overrides rely on.

---

## 3. Overriding a class via `system_overrides/`

Drop a file under `data/customization/system_overrides/` that **declares the class with the same name** as
the core one. You have two patterns — prefer the subclass form whenever you can require the core file.

### 3.1 Subclass core (recommended for controllers/models/views)

When the core class lives in a file you can `require` by path, keep all core behavior and override only what
you need: require the core file, alias its class to a side name, then declare the original name extending
the alias. Because your class is defined first, the autoloader never loads core under that name again.

```php
// data/customization/system_overrides/ConfigboxControllerCart.php
<?php
defined('CB_VALID_ENTRY') or die();

require_once KenedoPlatform::p()->getComponentDir('com_configbox').'/controllers/cart.php';
class_alias('ConfigboxControllerCart', 'ConfigboxControllerCartCore');   // keep a handle on core

class ConfigboxControllerCart extends ConfigboxControllerCartCore {
    function add() {
        // pre-step …
        parent::add();          // ← core behavior still available
        // post-step …
    }
}
```

This is the thinnest, lowest-drift way to change a core **controller, model, or view** — you track only the
method signatures you touch, not the whole class. Swap the `require` path for `models/<name>.php` or
`views/<view>/view.html.php` as needed.

### 3.2 Wholesale replacement (framework/helper classes)

When there's no convenient core file to require — a `Kenedo*` framework class, a `Configbox*Helper` — declare
the class from scratch with the same name. There's no `parent::`, so reproduce the methods you don't change:
**start from the core class** and edit a copy, keeping the diff small.

```php
// data/customization/system_overrides/ConfigboxCurrencyHelper.php
<?php
defined('CB_VALID_ENTRY') or die();

// Same class name as core → this definition wins (loaded before core would autoload it).
class ConfigboxCurrencyHelper {

    // … copy the core class body, change only what you need …

    public static function getFormatted($price, $symbol = true, $emptyOnZero = false, $decimals = 2) {
        // your altered formatting
    }

    // … all other core methods reproduced verbatim …
}
```

> **You now own this class.** Every core bug fix or change to `ConfigboxCurrencyHelper` in future updates
> will *not* reach your install — your copy shadows it. **Re-diff against the core class after every
> component update.** Keep the override as thin as the language allows; if the class is large, consider
> whether an observer or a different seam avoids the copy.

---

## 4. Boot hook functions (run code, register hooks)

`system_overrides/` files (and settings-dir files) are plain PHP run at boot, so they're also where you
**define the global functions** other parts of CBX look for. The framework calls a function *if it
exists*:

| Function | Read by | Purpose | Guide |
|---|---|---|---|
| `cbGetCustomRequirePaths()` | `helpers/view.php:107` | add RequireJS module paths (`appConfig.customPaths`) | `com_configbox_assets_and_amd.md` §5 |
| `cbGetCustomRequireShims()` | `helpers/view.php:112` | add RequireJS shims | `com_configbox_assets_and_amd.md` §5 |
| `getPostDbConnectQueries()` | `KenedoDatabase.php:83-90` | SQL to run right after each DB connect (§5) | this guide |
| `postMakeSelection(&$response)` | `controllers/configuratorpage.php` `sendResponse()` | append data to EVERY makeSelection response — the seam for anything a widget must update live per selection change (a composed product code, a stock indicator). The parameter is by reference; add keys, never replace the array. A frontend module reads them off the `serverResponseReceived` event | `com_configbox_custom_question_types.md` (the code-composition pattern) |
| `getReleaseNumber()` | `helpers/view.php:20-21` | append a custom token to the asset cache-buster (`?version=`), so customization CSS/JS updates invalidate browser caches — bump the returned string whenever customization assets change | `com_configbox_assets_and_amd.md` |

```php
// data/customization/system_overrides/_hooks.php  (leading _ → loads early)
<?php
defined('CB_VALID_ENTRY') or die();

function cbGetCustomRequirePaths() {
    return array('mylib' => KenedoPlatform::p()->getUrlCustomizationAssets().'/javascript/vendor/mylib.min');
}
```

> These are **plain global functions**, not classes — so they don't hit the redeclare limit (§2). Defining
> them in `system_overrides/` (or the settings dir) is the supported way to register boot-time hooks.

---

## 5. The DB-connect hook — `getInitQueries.php`

Separate from the `loadOverrideFiles()` flow and fired **earlier**: right after each database connection,
`KenedoDatabase` includes `system_overrides/getInitQueries.php` (if present) and runs the queries returned
by `getPostDbConnectQueries()` (`external/kenedo/classes/KenedoDatabase.php:83-90`):

```php
// data/customization/system_overrides/getInitQueries.php
<?php
defined('CB_VALID_ENTRY') or die();

function getPostDbConnectQueries() {
    return array(
        "SET SESSION sql_mode = ''",          // e.g. relax a strict mode for legacy data
        "SET SESSION group_concat_max_len = 1000000",
    );
}
```

Use it for **session-level DB settings** the whole request needs (SQL mode, timezone, concat limits). It
runs on **every** connection — keep the list short and side-effect-free. This file is special-cased by
name; it does not need a class.

---

## 6. Boot-time settings files (`data/store/private/settings/`)

`loadOverrideFiles()` loads a **second** set of PHP files first: those in
`getDirCustomizationSettings()` = `data/store/private/settings/` (`general.php:1015`). Same eager
`require_once`, same `_`-first ordering, loaded **before** `system_overrides/`. This is a separate slot from
`data/customization/` (it lives under `data/store/private/`), intended for **install-local boot settings**
(e.g. defining constants/hook functions for one deployment). For shipped/versioned customization code,
prefer `system_overrides/`; use the settings dir for per-install boot tweaks that shouldn't travel with the
code. Both run at the same boot point and can define the hook functions in §4.

---

## 7. Deployment checklist

```
data/customization/
  system_overrides/
    _<base>.php             ← (optional) loads first (leading underscore) — e.g. a shared base
    <Class>.php             ← replace a core class (same class name; reproduce unchanged methods)
    _hooks.php              ← define cbGetCustomRequirePaths / cbGetCustomRequireShims, etc.
    getInitQueries.php      ← getPostDbConnectQueries() — SQL per DB connect (special-cased)
data/store/private/settings/
    *.php                   ← per-install boot settings (loaded before system_overrides, not shipped code)
```

1. **Exhaust targeted mechanisms first** (§1). Only override a class if nothing smaller fits.
2. **Confirm the target isn't loaded during early boot** (the redeclare limit, §2) — test on a real page.
3. **Define the class** in `system_overrides/<Class>.php` with the same name as core — **subclass** it
   (require+alias+extend) for a controller/model/view (§3.1), or **replace it wholesale** for a
   framework/helper class (§3.2). Use a leading `_` if another override must load after a shared base.
4. **For hooks/boot code**, define plain functions (`cbGetCustomRequirePaths`, `getPostDbConnectQueries`,
   …) — these are safe and don't hit the redeclare limit (§4–§5).
5. **For per-install boot tweaks**, use `data/store/private/settings/` instead of shipping a system override
   (§6).
6. **Verify manually** — exercise the affected behavior, watch the error log (system overrides run with
   `display_errors` on, `overrides.php:34`), and confirm DB-connect queries took effect. CBX has
   **zero automated tests**.
7. **After every component update, re-diff each overridden class** against the new core version — this is
   the standing maintenance cost of this mechanism.

---

## 8. Conventions & gotchas

- **Escape hatch, not default.** Prefer controllers/templates/property-injection/observers/language-overrides;
  override a class only when no per-type seam exists (§1).
- **Same class name wins by loading first** — but only if the class **isn't already loaded** when overrides
  run; otherwise it's a fatal redeclare (§2). For component classes you can `require`+alias+subclass to keep
  `parent::` (§3.1); for framework/helper classes you replace wholesale (§3.2).
- **You inherit maintenance.** A replaced class won't get core updates; re-diff after every upgrade. Keep
  overrides minimal.
- **`_`-prefixed files load first**, settings dir before `system_overrides/` (§2.1, §6).
- **Hook functions are plain functions** — safe to define here (`cbGetCustomRequirePaths`,
  `cbGetCustomRequireShims`, `getPostDbConnectQueries`) (§4–§5).
- **`getInitQueries.php` runs on every DB connect** — keep it minimal, session-level only (§5).
- **Settings dir is per-install, not shipped code** (`data/store/private/settings/`) — different intent from
  `data/customization/` (§6).
- **No top-level side effects beyond what you intend** — these files run at boot, eagerly, every request.
  Guard with `defined('CB_VALID_ENTRY') or die();`.
- **Escape/sanitize as everywhere** — `getQuoted()` backtick-quotes **identifiers**, `getEscaped()`
  escapes **values** (inside quotes you add) — or bind values with `setPreparedQuery($sql, $params)`
  and skip manual escaping.

---

## See also

- `com_configbox_customization_overview.md` — the extension-point map (this is the **eager pre-emption /
  boot-include** mechanism) and the per-kind precedence rules that route here.
- `com_configbox_overriding_controllers_and_models.md` — why changing an existing core **controller**,
  **model**, or view behavior lands here (the per-type folders are core-first).
- `com_configbox_assets_and_amd.md` §5 — `cbGetCustomRequirePaths`/`cbGetCustomRequireShims` (defined here).
- `com_configbox_events_and_observers.md` — usually a safer alternative to replacing a class.
- `helpers/overrides.php` — `loadOverrideFiles()` (`:4`): the two folders, `_`-first ordering, eager include.
- `helpers/init.php` (`:98`) + `observers/System.php` (`:14`) — the boot point that triggers overrides.
- `external/kenedo/classes/KenedoAutoload.php` — the lazy autoloader that makes pre-emption possible.
- `external/kenedo/classes/KenedoDatabase.php` (`:83`) — the `getInitQueries.php` / `getPostDbConnectQueries`
  hook.
