# Code style

Derived from the existing codebase. When in doubt, mirror the file you are editing.

## Principles

**DRY, SOLID, clean code** — in that order of everyday relevance. Formatting rules are the easy part; these are
what a review actually checks.

### DRY

- Before writing anything, look for the thing that already does it. This codebase is dense with shared
  machinery, and duplicating it is the most common mistake here:
  [`app\db\ActiveRecord`](../db/ActiveRecord.php) (schema, slugs, timestamps, translations, photos,
  breadcrumb, SEO, search indexing), [`app\base\Widget`](../base/Widget.php) (render, cache, naming),
  the admin `Field` / `Action` classes under [`modules/admin/`](../modules/admin), the static services in
  [`services/`](../services), the global helpers in [`functions.php`](../functions.php), and the stateless
  classes in [`helpers/`](../helpers).
- Duplicated *knowledge* is the problem, not duplicated characters. Two similar-looking blocks that will change
  for different reasons should stay separate.
- Extend the base class when the behaviour belongs to every model/widget/screen. Add a helper when it is a
  stateless utility. Add a service when it is a read path over a table.

### SOLID

- **Single responsibility** — controllers resolve and delegate; widgets assemble a fragment; services read;
  models own their data and schema; helpers are stateless. A controller building HTML, or a model issuing HTTP
  requests, is in the wrong place.
- **Open/closed** — the framework layers here are meant to be extended, not edited. New behaviour arrives as a
  new `Field` type, a new `Action`, a new `Widget`, a new service — not as another `if` in the admin
  `BaseController`.
- **Liskov** — an override must honour the parent's contract. Overriding `getUrl()` or `getColumns()` has to
  keep returning something usable for every input the parent accepted.
- **Interface segregation** — the marker interfaces ([`SearchableInterface`](../models/SearchableInterface.php),
  [`HasNotifications`](../modules/admin/contracts/HasNotifications.php),
  [`GateServiceInterface`](../services/GateServiceInterface.php)) exist so a class opts into exactly one
  capability. Keep new contracts that small.
- **Dependency inversion** — depend on the abstraction: `ActiveRecord`, `Field`, `SearchableInterface`, a
  service's public API. Reaching into another class's internals or querying a table a service already owns
  couples the wrong things together.

### Clean code

- Names say what the thing is. The codebase is consistent about this; keep it that way.
- Small methods, one level of abstraction each.
- Guard clauses and early returns over nesting — this is the prevailing style here.
- No magic values. Named constants on the model (`Storage::PHOTO_SIZE_SMALL`), env constants for configuration,
  translation keys for text.
- Dead code, commented-out code and speculative abstractions get deleted, not kept "just in case".
- Errors are handled where they can be handled. Note that `ActiveRecord::save()` and `ActiveRecord::delete()`
  deliberately swallow and log ([`db/ActiveRecord.php:387`](../db/ActiveRecord.php),
  [`:400`](../db/ActiveRecord.php)) — so **check return values**; a silent `false` is the failure mode here.

## Language

Everything is **English**: class, method, property and variable names, database tables and columns,
comments, view files, JS, SCSS, documentation, commit messages. The only Hungarian text lives in
`messages/hu/*.php` and in seed data.

### User-facing copy

- A frontend label is **always multilingual** — add it to all three of
  `messages/{hu,en,de}/frontend.php`, never just one.
- The **admin is Hungarian only** — an admin label needs only the entry in `messages/hu/admin.php`.
- **Voice: informal.** Hungarian copy addresses the visitor with *tegeződés* ("te"), never "Ön"; German copy
  uses "du", never "Sie". This applies to every user-facing string, including e-mails and validation messages.

## Imports

**Always `use` classes at the top of the file. Never write an FQCN inside the code body.**

```php
use app\models\Storage;

$storage = new Storage();               // yes
$storage = new \app\models\Storage();   // no
```

The only accepted exception is a name collision with the class being defined
(e.g. `class ActiveRecord extends \yii\db\ActiveRecord`).

**`use` statements are sorted by line length, ascending.** Equal lengths keep their relative order.
`use Yii;` therefore always comes first.

```php
use Yii;
use app\helpers\Url;
use yii\web\Response;
use app\models\Article;
use yii\db\ActiveQuery;
use app\models\BreadcrumbItem;
use app\services\ArticleService;
use yii\web\NotFoundHttpException;
```

## Comments

**Comment only when it is justified, and then explain the *why*, never the *what* or the *how*.**
The code already says what it does. No references back to the conversation that produced the code.

```php
// no
// Increment the counter by one.
$counter++;

// yes
// Invalidate translations whose Hungarian source changed.
```

PHPDoc blocks are the exception — they are structural, not explanatory, and are expected on every class,
property, constant and method: a one-line description plus `@param`, `@return` and `@throws` where they apply.
Use `@inheritdoc` when the parent already documents it, and annotate only the differences.

```php
/**
 * Get storages by location.
 * @param int $locationId
 * @param bool $publishedOnly
 * @return Storage[]
 */
```

Mark public API used only from views or config with `@api` so the IDE stops reporting it as unused — controller
actions carry it. Suppress false positives with targeted `@noinspection` annotations.

## PHP formatting

- **Tabs** for indentation.
- Opening brace on the **same line**, for classes too.
- **Spaces inside parentheses**: `function foo( string $a ) : void`, `if( $a === $b )`, `foreach( $x as $y )`.
- No space between the keyword and the parenthesis: `if(`, `foreach(`, `while(`, `match(`.
- Space before the return-type colon: `) : bool {`.
- No braces for single-statement `if` / `foreach` bodies.
- One blank line after the opening brace and before the closing brace of a multi-statement method body.
- Typed properties, typed parameters, typed returns everywhere.
- Array literals with spaces inside: `[ 'a' => 1, 'b' => 2 ]`. Array access too: `$row[ 'name' ]`.
- Prefer `match` over `switch`, arrow functions over closures where the body is a single expression,
  spread over `array_merge` for lists: `[ ...$a, ...$b ]`.
- Null-safe operator and null-coalescing assignment are idiomatic here: `$photo?->picture`,
  `$published ??= static::query()->all()`.

## Naming

- Classes `PascalCase`, methods and properties `camelCase`, constants `UPPER_SNAKE_CASE`.
- Global env constants are prefixed by domain: `APP_`, `DB_`, `CACHE_`, `MAILER_`, `TELL_`, `BILLINGO_`, …
- **Database columns are `camelCase`** (`orderNumber`, `isPublished`, `createdAt`, `nameInContract`).
- Boolean columns and properties read as predicates: `isPublished`, `isFeatured`, `isNew`, `isPremium`.
- Foreign keys are `<model>Id` (`locationId`, `storageTypeId`).
- Table names are whatever Yii generates from the model; indexes are
  `ix_<table>_<columnName1>_<columnNameN>`, which is what
  [`SchemaHelper`](../helpers/SchemaHelper.php) produces.
- Private backing fields are prefixed with an underscore.
- View files are kebab-case and mirror the widget name: `ArticleWidget` → `views/widgets/article-widget.php`.

## Global helper functions

Defined in [`functions.php`](../functions.php), use them instead of the long form. Before reaching for a
framework call, check whether a shorthand already exists.

| Function | Purpose |
|----------|---------|
| `t( $key, $params, $category, $language )` | frontend translation — **returns `''` when the key is missing**, not the key |
| `bt( $key, $params, $language )` | admin translation (the `admin` category) |
| `p( $name, $default )` | value from the [`Settings`](../models/Settings.php) model |
| `e( $text )` | HTML encode — use instead of `Html::encode()` |
| `f()` | the formatter component — use instead of `Yii::$app->formatter` |
| `num( $number )` | decimal number format |
| `amount( $amount )` | number plus the currency suffix |
| `i( $class, $style, $fw )` | FontAwesome icon class |
| `array_find( $array, $callback )` | first matching element or `null` — a PHP 8.4 polyfill, guarded by `function_exists` |

## Views

- Open with a docblock declaring every incoming variable via `@var`.
- `use` statements come after that docblock.
- Compute in PHP above the markup, then switch to HTML with alternative syntax
  (`<?php foreach( … ): ?>` … `<?php endforeach; ?>` — with the semicolon).
- Echo with the short tag and no spaces: `<?=$model->name?>`.
- Always escape untrusted output with `e()`.

## JavaScript

- ES modules, one class per file, default export.
- Frontend widget classes live in `resources/js/widgets/` and extend `Widget`, exposing a
  `static selector` and being initialized from an entry with `Widget.init()`.
- Entry points in `resources/js/entries/` follow `<controllerId><ActionId>.js`, import their own SCSS first,
  then their widgets, then initialize them.

## SCSS

- One file per widget in `resources/scss/widgets/`, mirroring the widget's CSS class.
- Each widget stylesheet imports `../bootstrap-core` and scopes everything under the widget's own class.
- Bootstrap 5 utilities first; only write custom CSS where utilities do not reach.
- Shared variables in `resources/scss/variables.scss`.

## Things that are deliberate here

- **Models describe their own schema** (`getColumns()` / `getIndexes()`); migrations call
  [`SchemaHelper`](../helpers/SchemaHelper.php) instead of spelling out columns. **Adding a column means
  editing the model *and* writing a migration** — `getColumns()` alone only affects fresh installs.
- Read paths go through the static services in [`services/`](../services), which memoize whole result sets
  for the duration of the request.
- Multilingual attributes are stored in a `translations` JSON column and swapped in transparently by
  `ActiveRecord::populateRecord()`.

Details of all three are in the [wiki](wiki/README.md).
