Coding Conventions

7 views Markdown

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.

Filter::init() Every value that comes from a request. The superglobals are never read directly.
Utility::jencode() JSON encoding. Three flags are forced on top of yours, so output is stable across call sites.
Language::gc() Anything a human reads. Text is never written into a condition or an exception in one language.
LinkGenerator::admin() Panel addresses. A hardcoded path breaks the moment the admin directory or the route translation changes.
Utility::strtolower() Case conversion, because the language-aware form and the PHP builtin disagree on Turkish.
signatures
// 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.

the first argument
$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.

$modWhat survives$specialMissing key returns
omittedthe value untouchedused on its ownfalse
hcleartags stripped, entities decoded firstignored''
dtextsame as hclearignored''
texttags stripped, then quotes turned into entitiesignored''
lettersa-zA-Z plus the active language's own lettersadded''
letters_numbers0-9a-zA-Zaddedfalse
noun0-9a-zA-Z, comma, period, spaceadded''
numbersdigits and the minus signaddedfalse
rnumbersthe above, cast to intadded0
amountdigits, minus, period, comma; stays a stringadded''
ratecomma becomes a period, extra periods dropped, cast to floatadded0.0
identitydigits and the minus signignoredfalse
ip0-9a-zA-Z, minus, period, colonaddedfalse
email0-9a-zA-Z and @ . + _ -ignored''
domainletters, digits, period, minus; lowercasedignored''
folder0-9a-zA-Z and / - _ .added''
file0-9a-zA-Z, language letters and - _ .added''
route0-9a-zA-Z and - _ ., with ../ removed firstadded''
passwordnothing removed, the raw value comes backignoredfalse

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.

the third argument, with measured output
$_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

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.

writing side, then the file it reads
// 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.

route map, then the calls it answers
// 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.

values
// 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

RuleWriteNot
Opening tag<?php, and <?= in templatesthe short open tag
Arrays[]array()
Defaults$x ?? $yisset($x) ? $x : $y
Single statementno bracesa three-line block for one line
Deep nestingreturn or throw earlynested conditions
Signaturesparameter and return typesuntyped
Multi-line liststrailing commanone
Stringssingle quotes without interpolationdouble quotes everywhere
Arrow functionsconcise body for one expressionbraces 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.

the operation
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]),
    ]);
}
what the keys resolve against
// 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

The third argument is the language, in both families

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.

A missing key does not come back as null

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.

A truthiness check is not a presence check

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.

Match the file you are in

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.

Was this helpful?

Thanks for your feedback!

Still Need Help?

Our support team is here around the clock for anything you can't find above.