# Coding Conventions

**Analysis Date:** 2026-04-21

## Naming Patterns

**Files:**
- Controllers: PascalCase, directory structure mirrors URL path. Example: `application/admin/controller/user/User.php` -> `/admin/user/user`
- Models: PascalCase in parallel directory structure. Example: `application/admin/model/User.php`
- Validates: PascalCase. Example: `application/admin/validate/User.php`
- Libraries: PascalCase. Example: `application/admin/library/Auth.php`, `application/common/library/Upload.php`
- Traits: PascalCase. Example: `application/admin/library/traits/Backend.php`
- Behaviors: PascalCase. Example: `application/admin/behavior/AdminLog.php`
- Language files: lowercase `zh-cn.php`, mirroring controller directory structure
- View templates: lowercase `.html`, matching action names. Example: `application/admin/view/user/user/index.html`
- JS files: lowercase, mirroring controller path. Example: `public/assets/js/backend/user/user.js`
- CSS: lowercase `.css` with `.min.css` variants
- Less: lowercase `.less` (source files in `public/assets/less/`)
- Common helper functions: snake_case. Example: `build_select()`, `cdnurl()`, `datetime()`, `letter_avatar()`

**Functions/Methods:**
- Controller action methods: lowercase with underscores. Examples: `index()`, `add()`, `edit()`, `del()`, `recyclebin()`, `get_field_list()`, `get_controller_list()`
- Controller protected methods: camelCase. Examples: `buildparams()`, `selectpage()`, `getDataLimitAdminIds()`, `loadlang()`, `assignconfig()`
- Model accessors/mutators: ThinkPHP convention `get{FieldName}Attr()` / `set{FieldName}Attr()`. Example: `getPrevtimeTextAttr()`, `setJointimeAttr()`, `getAvatarAttr()`, `setBirthdayAttr()`
- Model list methods: PascalCase `getStatusList()`, `getGenderList()`
- Model static methods: camelCase. Example: `User::money()`, `User::score()`, `User::nextlevel()`
- Library methods: camelCase. Example: `Auth::login()`, `Auth::getUserinfo()`, `Upload::init()`
- Global functions: snake_case, wrapped in `if (!function_exists('...'))`. Example: `__()`, `format_bytes()`, `check_cors_request()`, `xss_clean()`
- Addon methods: camelCase following ThinkPHP convention

**Variables:**
- Controller properties: camelCase. Examples: `$noNeedLogin`, `$model`, `$searchFields`, `$relationSearch`, `$dataLimit`
- Library private properties: underscore prefix + camelCase. Examples: `$_error`, `$_logined`, `$_user`, `$_token` (in `application/common/library/Auth.php`)
- Library protected properties: camelCase, no prefix. Examples: `$keeptime`, `$requestUri`, `$allowFields`
- Local variables: camelCase. Examples: `$tableList`, `$fieldlist`, `$insert_data`, `$changedata`
- Database fields: lowercase with underscores. Examples: `createtime`, `updatetime`, `group_id`, `loginip`
- Request input: via `$this->request->post('row/a')` (array), `$this->request->request('keyField')` (single)

**Types/Namespaces:**
- Namespace convention: lowercase `app\admin\controller`, `app\common\model`, `app\api\library`
- Class names: PascalCase. Example: `class User extends Backend`
- Addon namespace: `addons\{name}\` mapped via PSR-4 in `composer.json`
- Fast tools namespace: `fast\` maps to `extend/fast/` directory

## Code Style

**Formatting:**
- 4-space indentation throughout (no tabs)
- Opening brace on same line for classes/functions: `class User extends Backend\n{`
- Opening brace on next line for control structures in some files, same line in others (inconsistent)
- Closing PHP tag `?>` omitted at end of files
- Files start with `<?php` followed by a blank line
- Blank line after namespace declaration
- Blank line after last `use` statement before class declaration

**PHP Version:** PHP >= 7.4.0 (per `composer.json`)
- Union types in catch blocks: `catch (ValidateException|PDOException|Exception $e)`
- Null coalescing: `$value ?? ''`
- Match expressions: Not used
- Arrow functions: Not used
- Typed properties: Not used

**Linting/Formatting Tools:**
- No project-level `.php-cs-fixer.php`, `phpcs.xml`, `.editorconfig`, or `biome.json`
- No ESLint or stylelint configured
- PhpStorm `.idea/` directory present with PHP 7.4 language level configured
- Static analysis tools (PHPStan, PHPCS, MessDetector) configured in IDE but not activated (`transferred=true`)

## Import Organization

**Order in PHP files:**
1. `namespace app\admin\controller;`
2. `use` statements for app classes: `use app\common\controller\Backend;`
3. `use` statements for ThinkPHP classes: `use think\Db;`, `use think\Config;`, `use think\Exception;`
4. `use` statements for vendor/third-party classes: `use fast\Tree;`
5. No grouping separators between use statement categories

**Example from `application/admin/controller/user/User.php`:**
```php
namespace app\admin\controller\user;

use app\common\controller\Backend;
use app\common\library\Auth;
```

**Example from `application/common/controller/Backend.php`:**
```php
namespace app\common\controller;

use app\admin\library\Auth;
use think\Config;
use think\Controller;
use think\Hook;
use think\Lang;
use think\Loader;
use think\Model;
use think\Session;
use fast\Tree;
use think\Validate;
```

**Path Aliases:**
- No PSR-4 path aliases configured beyond `addons\\` -> `addons/` in `composer.json`
- ThinkPHP autoload handles `app\` namespace mapping automatically
- `fast\` namespace handled by ThinkPHP custom autoloader -> `extend/fast/`

## ThinkPHP Conventions

**Model Table Naming:**
- `$name` property defines the table name without prefix. Example: `protected $name = 'user';` maps to `{prefix}user`
- Table prefix configured in `application/config.php` database section
- All models extend `think\Model`

**Timestamp Convention:**
- Auto timestamp type: `protected $autoWriteTimestamp = 'int';` (Unix integer timestamps)
- Create field name: `protected $createTime = 'createtime';`
- Update field name: `protected $updateTime = 'updatetime';`

**Controller Initialization:**
- `_initialize()` method called by ThinkPHP before each action
- Always calls `parent::_initialize()` first
- Sets `$this->model` instance in `_initialize()`
- Assigns view data: `$this->view->assign("statusList", ...)`

**Magic Model Methods:**
- Dynamic finders via `@method` PHPDoc annotations
- `getBy{Field}()` auto-generated by ThinkPHP for any column
- Example: `@method static mixed getByUsername($str)` in `application/common/model/User.php`
- Usage: `\app\common\model\User::getByMobile($mobile)`

**AJAX Detection Pattern:**
```php
if ($this->request->isAjax()) {
    // Return JSON response
    return json(['total' => $list->total(), 'rows' => $list->items()]);
}
return $this->view->fetch();
```

## FastAdmin Patterns

**Backend Base Class (`app\common\controller\Backend`):**
- All admin controllers extend this class
- Provides via trait `app\admin\library\traits\Backend`:
  - `index()` - List with pagination and filtering
  - `add()` - Create record
  - `edit($ids)` - Update record
  - `del($ids)` - Soft delete
  - `destroy($ids)` - Hard delete (from recycle bin)
  - `restore($ids)` - Restore from recycle bin
  - `multi($ids)` - Batch update
  - `recyclebin()` - Recycle bin list
  - `import()` - Excel/CSV import
  - `selectpage()` - SelectPage dropdown data
- Key configurable properties:
  - `$noNeedLogin = []` - Methods skipping authentication entirely
  - `$noNeedRight = []` - Methods skipping permission check (still need login)
  - `$model = null` - Associated model instance
  - `$searchFields = 'id'` - Fields for quick search
  - `$relationSearch = false` - Whether to use table alias in queries
  - `$dataLimit = false` - Data scope: `auth`/`personal`/`false`
  - `$dataLimitField = 'admin_id'` - Field for data restriction
  - `$dataLimitFieldAutoFill = true` - Auto-fill restriction field
  - `$modelValidate = false` - Enable model-level validation
  - `$modelSceneValidate = false` - Enable scene-based validation
  - `$multiFields = 'status'` - Fields allowed in batch operations
  - `$selectpageFields = '*'` - Fields shown in SelectPage
  - `$excludeFields = ""` - Fields to exclude from form submission
  - `$importHeadType = 'comment'` - Import header type: `comment`/`name`
  - `$layout = 'default'` - Template layout name

**Standard CRUD Controller Pattern:**
```php
/**
 * 会员管理
 *
 * @icon fa fa-user
 */
class User extends Backend
{
    protected $relationSearch = true;
    protected $searchFields = 'id,username,nickname';

    /**
     * @var \app\admin\model\User
     */
    protected $model = null;

    public function _initialize()
    {
        parent::_initialize();
        $this->model = new \app\admin\model\User;
    }
}
```

**CRUD Generation:**
- Via `php think crud` command
- Flags: `--table`, `--controller`, `--model`, `--fields`, `--force`, `--delete`, `--menu`
- Extended flags: `--setcheckboxsuffix`, `--enumradiosuffix`, `--imagefield`, `--filefield`, etc.

**Auth Patterns:**
- Admin auth: `app\admin\library\Auth` - backend admin user authentication
- User auth: `app\common\library\Auth` - frontend/member user authentication
- Both use singleton: `Auth::instance()`
- Permission check: `$this->auth->check($path)` where `$path = 'controller/action'`
- Login check: `$this->auth->isLogin()`
- Super admin check: `$this->auth->isSuperAdmin()`

**API Controller Pattern (`app\common\controller\Api`):**
- API controllers extend this (NOT `think\Controller`)
- Does NOT extend `think\Controller` - standalone class with `__construct()`
- Response format: `{'code': 1|0, 'msg': '', 'time': <timestamp>, 'data': ...}`
- Success: `$this->success($msg, $data)` (code=1, HTTP 200)
- Error: `$this->error($msg, $data, $httpCode)` (code=0)
- HTTP status codes: 401 for unauth, 403 for forbidden
- API annotations for doc generation: `@ApiMethod(POST)`, `@ApiParams(name="...", type="string", required=true, description="...")`
- Default request filter: `trim,strip_tags,htmlspecialchars`

**Frontend Controller Pattern (`app\common\controller\Frontend`):**
- Index module controllers extend this
- Similar to Backend but for public-facing pages
- Uses `app\common\library\Auth` for member auth
- Layout can be empty string for no layout: `protected $layout = '';`

## Language File Structure

**Directory Layout:**
```
application/{module}/lang/zh-cn.php                     # Module-level translations
application/{module}/lang/zh-cn/controller.php           # Controller translations
application/{module}/lang/zh-cn/sub/controller.php       # Nested controller translations
application/{module}/lang/{locale}/controller.php        # Other locales
```

**Observed locales:**
- `zh-cn` (Simplified Chinese) - primary language for all modules
- `en` - only exists for `application/index/lang/en/index.php`

**File Format:**
```php
return [
    'Id'             => 'ID',
    'Group_id'       => '组别ID',
    'Username'       => '用户名',
    'Leave password blank if dont want to change' => '不修改密码请留空',
];
```

**Usage Pattern:**
- `__('Key')` - Simple translation
- `__('Key %s', $value)` - Parameterized translation
- Keys use PascalCase for field names (matching DB column names)
- Keys use sentence case for messages
- Language auto-loaded by controller in `loadlang()` method
- Language detection: `$this->request->langset()` with regex validation, defaults to `zh-cn`

## Template Syntax

**Template Engine:** ThinkPHP built-in template engine
**File Extension:** `.html`

**Template Tags Used:**

| Tag | Example |
|-----|---------|
| Output | `{$variable}`, `{$var\|htmlentities}`, `{$var\|default='default'}` |
| Function | `{:__('Dashboard')}`, `{:build_heading()}`, `{:build_toolbar('refresh,edit,del')}` |
| Condition | `{if condition="$auth->check('dashboard')"}`, `{if !IS_DIALOG}`, `{/if}` |
| If-else shorthand | `{:defined('IS_DIALOG') && IS_DIALOG ? 'is-dialog' : ''}` |
| Loop | `{foreach $breadcrumb as $vo}`, `{/foreach}` |
| Include | `{include file="common/meta" /}` |
| Layout placeholder | `{__CONTENT__}` |
| ThinkPHP config | `{$Think.config.fastadmin.breadcrumb}` |
| Inline PHP | `{:$auth->check('user/user/multi')?'':'hide'}` |

**Layout Structure:**
```
application/{module}/view/layout/default.html     # Layout wrapper with {__CONTENT__}
application/{module}/view/{controller}/index.html # Page content
application/{module}/view/common/meta.html        # Shared <head> section
application/{module}/view/common/script.html      # Shared JS loading
application/{module}/view/common/header.html      # Header fragment
application/{module}/view/common/menu.html        # Sidebar menu
application/{module}/view/common/control.html     # Control bar fragment
```

**CSS Framework:** Bootstrap 3 with AdminLTE theme
**Common Classes:**
- Layout: `.panel`, `.panel-default`, `.panel-intro`, `.panel-heading`, `.panel-body`
- Table: `.table`, `.table-striped`, `.table-bordered`, `.table-hover`, `.table-nowrap`
- Buttons: `.btn`, `.btn-primary`, `.btn-success`, `.btn-danger`, `.btn-info`, `.btn-xs`
- Form: `.form-control`, `.selectpicker`, `.form-group`, `.control-label`
- FastAdmin custom: `.btn-dialog`, `.btn-addtabs`, `.btn-ajax`, `.btn-click`, `.searchit`

## JS AMD Module Pattern (RequireJS)

**Module Definition Pattern:**
```javascript
define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
    var Controller = {
        index: function () {
            Table.api.init({
                extend: {
                    index_url: 'user/user/index',
                    add_url: 'user/user/add',
                    edit_url: 'user/user/edit',
                    del_url: 'user/user/del',
                    multi_url: 'user/user/multi',
                    table: 'user',
                }
            });
            var table = $("#table");
            table.bootstrapTable({
                url: $.fn.bootstrapTable.defaults.extend.index_url,
                pk: 'id',
                sortName: 'user.id',
                columns: [[
                    {checkbox: true},
                    {field: 'id', title: __('Id'), sortable: true},
                    {field: 'operate', title: __('Operate'), table: table,
                     events: Table.api.events.operate,
                     formatter: Table.api.formatter.operate}
                ]]
            });
            Table.api.bindevent(table);
        },
        add: function () {
            Controller.api.bindevent();
        },
        edit: function () {
            Controller.api.bindevent();
        },
        api: {
            bindevent: function () {
                Form.api.bindevent($("form[role=form]"));
            }
        }
    };
    return Controller;
});
```

**RequireJS Configuration Files:**
- `public/assets/js/require-backend.js` - Backend module config (minified as `require-backend.min.js`)
- `public/assets/js/require-frontend.js` - Frontend module config (minified)
- `public/assets/js/require-table.js` - Bootstrap-table wrapper module
- `public/assets/js/require-form.js` - Form handling module
- `public/assets/js/require-upload.js` - Upload module

**Core JS Modules:**
- `Fast` / `Fast.api` - Core API: `ajax()`, `open()`, `cdnurl()`, `fixurl()`, `selectedids()`
- `Backend` - Admin: sidebar badges, tab management, dialog/_ajax handlers
- `Frontend` - Frontend: captcha sending, touch swipe for sidebar
- `Table` - Bootstrap-table wrapper: `api.init()`, `api.bindevent()`, formatters, events
- `Form` - Form validation and submission: `api.bindevent()`
- `Template` - art-template JS template engine
- `Moment` - Date/time formatting (with `moment/locale/zh-cn`)

**Global Objects (set on `window`):**
- `window.Backend` - Backend namespace
- `window.Frontend` - Frontend namespace
- `window.Config` - Server-rendered config object
- `window.Toastr` - Toastr notification library
- `window.Layer` - Layer popup library (layui-based)
- `window.Table` - (via require) Table module
- `window.Form` - (via require) Form module
- `window.Template` - Template engine
- `window.Moment` - Moment.js

**Controller JS File Convention:**
- Location: `public/assets/js/backend/{controller_path}.js`
- Standard methods: `Controller.index()`, `Controller.add()`, `Controller.edit()`
- Sub-controller: `public/assets/js/backend/auth/admin.js`
- Each controller JS is loaded dynamically based on `Config.jsname`

**Button/Action Patterns in JS:**
- `.btn-dialog` / `.dialogit` - Opens URL in Layer dialog
- `.btn-addtabs` / `.addtabsit` - Opens URL in new tab
- `.btn-ajax` / `.ajaxit` - Sends AJAX request
- `.btn-click` / `.clickit` - Custom click handler
- `.searchit` - Triggers table search with field/value

**Table Formatter Patterns:**
- `Table.api.formatter.image` / `.images` - Image display
- `Table.api.formatter.datetime` - Date formatting
- `Table.api.formatter.status` - Status badge
- `Table.api.formatter.normal` - Generic label with color
- `Table.api.formatter.search` - Clickable search link
- `Table.api.formatter.operate` - Action buttons (edit/del)
- `Table.api.formatter.toggle` - Toggle switch
- `Table.api.formatter.flag` / `.label` - Multi-value flags

## CSS Class Naming

**Convention:** Bootstrap 3 + AdminLTE + FastAdmin extensions
**CSS Source:** Less files compiled to CSS
- `public/assets/less/backend.less` -> `public/assets/css/backend.css`
- `public/assets/less/frontend.less` -> `public/assets/css/frontend.css`
- `public/assets/less/bootstrap.less` -> `public/assets/css/bootstrap.css`
- `public/assets/css/fastadmin.css` - FastAdmin base overrides
- `public/assets/css/index.css` - Index page styles
- `public/assets/css/user.css` - User center styles

**Common Panel Pattern:**
```html
<div class="panel panel-default panel-intro">
    {:build_heading()}
    <div class="panel-body">
        <div id="toolbar" class="toolbar">
            {:build_toolbar('refresh,edit,del')}
        </div>
        <table id="table" class="table table-striped table-bordered table-hover table-nowrap"
               data-operate-edit="{:$auth->check('user/user/edit')}"
               data-operate-del="{:$auth->check('user/user/del')}"
               width="100%">
        </table>
    </div>
</div>
```

## Error Handling

**Controller Layer:**
- `$this->error($message)` - Returns error, terminates execution
- `$this->success($message, $data)` - Returns success response
- Backend: throws `HttpResponseException` internally
- API: sets HTTP status codes (401/403) via header

**Transaction Pattern:**
```php
Db::startTrans();
try {
    // database operations
    Db::commit();
} catch (ValidateException|PDOException|Exception $e) {
    Db::rollback();
    $this->error($e->getMessage());
}
if ($result === false) {
    $this->error(__('No rows were inserted'));
}
$this->success();
```

**Model Event Hooks:**
```php
protected static function init()
{
    self::beforeWrite(function ($row) {
        $changed = $row->getChangedData();
        if (isset($changed['password'])) {
            $salt = \fast\Random::alnum();
            $row->password = \app\common\library\Auth::instance()->getEncryptPassword($changed['password'], $salt);
            $row->salt = $salt;
        }
    });
}
```

## Logging

**Framework:** ThinkPHP built-in logging
- Configurable driver (file, etc.) in `application/config.php`
- Custom log library: `application/common/library/Log.php`
- Admin log behavior: `application/admin/behavior/AdminLog.php` - auto-logs admin actions to database

## Comments

**JSDoc/TSDoc Pattern:**
- All public/protected methods have PHPDoc with Chinese descriptions
- Controller methods: brief action description
- API methods: `@ApiMethod` + `@ApiParams` annotations for API doc generation

**Controller Class Doc:**
```php
/**
 * 会员管理
 *
 * @icon fa fa-user
 */
class User extends Backend
```

**Method Doc:**
```php
/**
 * 会员登录
 *
 * @ApiMethod (POST)
 * @ApiParams (name="account", type="string", required=true, description="账号")
 * @ApiParams (name="password", type="string", required=true, description="密码")
 */
public function login()
```

**Model Doc:**
```php
/**
 * 会员模型
 * @method static mixed getByUsername($str) 通过用户名查询用户
 * @method static mixed getByNickname($str) 通过昵称查询用户
 */
class User extends Model
```

**Function Doc:**
```php
/**
 * 将字节转换为可读文本
 * @param int    $size      大小
 * @param string $delimiter 分隔符
 * @param int    $precision 小数位数
 * @return string
 */
function format_bytes($size, $delimiter = '', $precision = 2)
```

## Function Design

**Size:**
- Controller action methods: 10-30 lines (when extending Backend, most logic is in trait)
- Backend trait methods: 30-80 lines (`index`, `add`, `edit`, `del`)
- Import method: ~130 lines (`application/admin/library/traits/Backend.php`)
- Library methods: 10-50 lines
- Global functions: 5-30 lines

**Parameters:**
- Primary key as method parameter: `edit($ids = null)`, `del($ids = "")`
- POST form data: `$this->request->post('row/a')` (returns associative array)
- Query params: `$this->request->request('key')`, `$this->request->get('filter')`

**Return Values:**
- AJAX: `json(['total' => N, 'rows' => [...]])`
- Non-AJAX: `$this->view->fetch()`
- API: Always JSON via `$this->success()` / `$this->error()`

## Module Design

**PHP Exports:**
- No explicit exports; PSR-4 autoloading handles class loading
- Language files: `return [...]` array
- Config files: `return [...]` array

**JS Exports:**
- AMD `define()` with `return Controller;` pattern
- No ES modules, no CommonJS

**No Barrel Files:** Direct imports throughout, no index/re-export files

**Addon Structure:**
```
addons/{addon_name}/
├── config.php              # Addon configuration (returns array)
├── Command.php             # Main addon class (extends \fast\Addons)
├── controller/Index.php    # Addon controller
├── library/Output.php      # Addon library
└── info.ini                # Addon metadata
```

## Where to Add New Code

**New Admin Feature (CRUD):**
- Controller: `application/admin/controller/{module}/{Name}.php`
- Model: `application/admin/model/{Name}.php`
- Validate: `application/admin/validate/{Name}.php`
- Language: `application/admin/lang/zh-cn/{module}/{name}.php`
- View: `application/admin/view/{module}/{name}/index.html`, `add.html`, `edit.html`
- JS: `public/assets/js/backend/{module}/{name}.js`
- Or auto-generate: `php think crud --table={table} --controller={name}`

**New API Endpoint:**
- Controller: `application/api/controller/{Name}.php` (extends `app\common\controller\Api`)
- Response via `$this->success()` / `$this->error()`
- Add `@ApiMethod` and `@ApiParams` annotations for doc generation

**New Common Library:**
- Location: `application/common/library/{Name}.php`
- Use singleton `instance()` pattern if needed

**New Global Function:**
- Location: `application/common.php`
- Wrap in `if (!function_exists('name')) { ... }`

**New Module-Level Function:**
- Location: `application/{module}/common.php`

**New Model:**
- Shared model: `application/common/model/{Name}.php`
- Admin-only model: `application/admin/model/{Name}.php`
- Extend `think\Model`, set `$name`, `$autoWriteTimestamp`, `$createTime`, `$updateTime`

**New Validate:**
- Location: `application/admin/validate/{Name}.php`
- Extend `think\Validate`, define `$rule`, `$field`, `$scene`

---

*Convention analysis: 2026-04-21*
