# Coding Conventions

https://dev.wisecp.com/es/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.

- **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.

```php
// 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.

```php
$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.

```php
$_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.

```php
// 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.

```php
// 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.

```php
// 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.

```php
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]),
    ]);
}
```

```php
// 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.

## Related Articles

- [Filtering User Input](https://dev.wisecp.com/en/filtering-user-input)
- [Error Handling](https://dev.wisecp.com/en/error-handling)
- [Translations and Language Files](https://dev.wisecp.com/en/translations-and-language-files)
- [Operations](https://dev.wisecp.com/en/operations)
