Coding Conventions
The codebase is written one way on purpose. Code that follows it reads like the file it sits in, instead of announcing that someone else wrote it.
Overview
Most of these rules exist because the opposite caused a real failure: a value read without a default, a truthiness test that swallowed a legitimate zero, an unescaped input in a query.
They apply to anything you write inside an installation: modules, themes, hook listeners and their supporting code.
Reference
Input and Output
Five helpers stand in front of the builtins you would otherwise reach for. All are static and all have defaults, so read the signature, not the name.
// coremio/classes/Filter.php
public static function init($arg = null, $mod = false, $special = false);
public static function GET($arg = '');
public static function POST($arg = '');
public static function REQUEST($arg = '');
public static function FILES($arg = '');
public static function SERVER($arg = '');
public static function html_clear($arg = null, $allow = '');
public static function route($arg = '', $special = '');
public static function phone($arg = '');
// coremio/classes/Utility.php
public static function jencode($string = '', $flags = 0): string|false;
public static function jdecode($string = '', $mode = false);
public static function strtolower($str, $encode = 'UTF-8');
public static function strtoupper($str, $encode = 'utf8');
public static function AppAdress($prefix = false);
// coremio/classes/Language.php - coremio/classes/LinkGenerator.php
public static function g($key = '', $replaces = [], $slang = ''): string|int|array|bool;
public static function gc($name = '', $replaces = [], $slang = ''): string|int|array|bool;
public static function selected(): string;
public static function admin($route = '', $params = [], $lang = '', $wqs = []): string|bool;
public static function client($route = '', $params = [], $lang = '');
public static function wQS(null|string|bool $url, string|array $params = []): string;
Filter::init, Parameter by Parameter
The first argument names the source and the key. A prefix of GET/, POST/, REQUEST/, FILES/ or SERVER/ selects the array, and further slashes walk into it. Without a prefix the argument is a literal value to clean.
$id = Filter::init('POST/id', 'rnumbers'); // $_POST['id'] -> int
$name = Filter::init('POST/user/name', 'hclear'); // $_POST['user']['name']
$op = Filter::init('REQUEST/operation', 'route'); // $_REQUEST['operation']
$ip = Filter::init('SERVER/REMOTE_ADDR', 'ip'); // $_SERVER['REMOTE_ADDR']
$safe = Filter::init($someString, 'hclear'); // no prefix: clean a value you already hold
The second argument is a closed vocabulary, not a free string. It decides what survives, what type comes back, and what a missing key turns into. The last column matters: the answer is not one value, so one guard does not cover every mode.
| $mod | What survives | $special | Missing key returns |
|---|---|---|---|
| omitted | the value untouched | used on its own | false |
hclear | tags stripped, entities decoded first | ignored | '' |
dtext | same as hclear | ignored | '' |
text | tags stripped, then quotes turned into entities | ignored | '' |
letters | a-zA-Z plus the active language's own letters | added | '' |
letters_numbers | 0-9a-zA-Z | added | false |
noun | 0-9a-zA-Z, comma, period, space | added | '' |
numbers | digits and the minus sign | added | false |
rnumbers | the above, cast to int | added | 0 |
amount | digits, minus, period, comma; stays a string | added | '' |
rate | comma becomes a period, extra periods dropped, cast to float | added | 0.0 |
identity | digits and the minus sign | ignored | false |
ip | 0-9a-zA-Z, minus, period, colon | added | false |
email | 0-9a-zA-Z and @ . + _ - | ignored | '' |
domain | letters, digits, period, minus; lowercased | ignored | '' |
folder | 0-9a-zA-Z and / - _ . | added | '' |
file | 0-9a-zA-Z, language letters and - _ . | added | '' |
route | 0-9a-zA-Z and - _ ., with ../ removed first | added | '' |
password | nothing removed, the raw value comes back | ignored | false |
The third argument is not a default value. It is a fragment spliced into the negated character class the mode builds. It widens the filter by whatever characters you name.
$_POST['name'] = ' <b>Ada</b> Lovelace ';
Filter::init('POST/name', 'letters_numbers'); // 'AdaLovelace'
Filter::init('POST/name', 'letters_numbers', ' '); // ' Ada Lovelace ' space allowed back in
Filter::init('POST/name', 'password'); // ' <b>Ada</b> Lovelace ' untouched
$_POST['rate'] = '1.234,50';
Filter::init('POST/rate', 'amount'); // '1.234,50' string
Filter::init('POST/rate', 'rate'); // 1234.5 float
Translations and Links
Both helpers take a key that resolves against a file. Both also accept a second array, and it is not the language code.
For a translation the array is a straight search and replace. The keys are the placeholders exactly as they appear in the stored string, braces included. Nothing is inferred from the name.
// Written like this:
$text = Language::gc('admin/services/cancel-credit-log-desc', ['{service_id}' => 41]);
// 'Cancellation refund for service #41'
// Read from this, in coremio/locale/en/cm/admin/services.php:
return [
'cancel-credit-log-desc' => 'Cancellation refund for service #{service_id}',
];
// gc() addresses a component file: <folder>/<file>/<key> under coremio/locale/<lang>/cm/.
// g() addresses a package file: <file>/<key> under coremio/locale/<lang>/.
$hint = Language::g('needs/password-needs-special', ['{characters}' => '! @ # $']);
For a link the array fills the placeholders of a route pattern, positionally. A route with no placeholder ignores the array rather than appending to the path. Query strings never go in that slot; they are the fourth argument.
// coremio/locale/en/admin-routes.php ['url pattern', 'controller path']
return ['admin-routes' => [
'services' => ['services', 'services'],
'users-1' => ['users/(?)', 'users/(1)'],
'financial-2' => ['financial/(?)/(?)', 'financial/(1)/(2)'],
]];
LinkGenerator::admin('services'); // {admin}/services
LinkGenerator::admin('services', ['detail', 5]); // {admin}/services arguments dropped, no (?)
LinkGenerator::admin('users-1', ['detail']); // {admin}/users/detail
LinkGenerator::admin('financial-2', ['coupons', 'add']); // {admin}/financial/coupons/add
// Query string is the fourth argument; the third is the language.
LinkGenerator::admin('users-1', ['detail'], '', ['id' => 42, 'tab' => 'invoices']);
// {admin}/users/detail?id=42&tab=invoices
// On a URL you already hold, use wQS(); it picks ? or & for you.
LinkGenerator::wQS($url, ['tab' => 'cards']);
Reading Values
Read every array key with a default. This is not defensive habit, it is measured cost. A missing key raises a warning, and the error handler turns each warning into a disk write.
// Correct: null-safe, and identical for "0", "1", 0, 1 and null.
if ((int) ($data['enabled'] ?? 0) === 1) { }
$name = $data['name'] ?? '';
$limit = (int) ($data['limit'] ?? 0);
// Wrong: empty("0") is true, so the value "0" disappears.
// if (!empty($data['enabled'])) { }
// Wrong: verbose, and it says nothing the cast does not.
// if (isset($data['enabled']) && (int) $data['enabled'] === 1) { }
Shape of the Code
| Rule | Write | Not |
|---|---|---|
| Opening tag | <?php, and <?= in templates | the short open tag |
| Arrays | [] | array() |
| Defaults | $x ?? $y | isset($x) ? $x : $y |
| Single statement | no braces | a three-line block for one line |
| Deep nesting | return or throw early | nested conditions |
| Signatures | parameter and return types | untyped |
| Multi-line lists | trailing comma | none |
| Strings | single quotes without interpolation | double quotes everywhere |
| Arrow functions | concise body for one expression | braces around one expression |
Comments
The default is none. Before writing one, ask whether the code is unreadable without it. A comment on readable code is noise that drifts out of date. When one is warranted it explains why, never what, and it is written in English.
Three kinds never belong in code. Notes about where something was copied from, notes about the development process, and restatements of a line that already says it.
Example
An operation, and the two files that answer the keys it hands out.
public function update(Operation $operation): bool
{
$operation->demo();
$id = Filter::init('POST/id', 'rnumbers');
if (!$id) throw new Exception(Language::gc('admin/example/error-id-required'));
$name = Filter::init('POST/name', 'hclear');
if ($name === '') throw new Exception(Language::gc('admin/example/error-name-required'));
$data = [
'name' => $name,
'enabled' => Filter::init('POST/enabled', 'rnumbers'),
];
// Retry attempts are stored as failures; there is no success column to update.
if (!$this->model->update($id, $data))
throw new Exception(Language::gc('admin/example/error-save-failed', ['{id}' => $id]));
return $operation->output([
'status' => 'successful',
'redirect' => LinkGenerator::admin('users-1', ['detail'], '', ['id' => $id]),
]);
}
// coremio/locale/en/cm/admin/example.php
return [
'error-id-required' => 'A record must be selected.',
'error-name-required' => 'Name is required.',
'error-save-failed' => 'Record {id} could not be saved.',
];
// coremio/locale/en/admin-routes.php
return ['admin-routes' => [
'users-1' => ['users/(?)', 'users/(1)'],
]];
Pitfalls
LinkGenerator::admin($route, $params, $lang, $wqs) puts the query string fourth. An array written third silently becomes a language code, and the parameters vanish. The static translation helpers take replacements second and the language third. The instance method behind them reverses the two: Language::g($key, $replaces, $slang) against $lang->get($key, $slang, $replaces). Read the signature you are calling, not the one you remember.
Depending on the mode it is false, '', 0 or 0.0, and never null. So ?? $default after a filter call is dead code, and a presence check written as !== null is always true. Compare against the value the mode actually returns.
The value "0" is a real answer for a setting, a quantity or a status, and a truthiness test throws it away. Compare explicitly, or cast and compare.
Where a local pattern disagrees with this page, the local pattern usually wins. Consistency inside one file is worth more than consistency with a document.
Related Articles
Thanks for your feedback!
Our support team is here around the clock for anything you can't find above.