Files
2026-04-21 23:02:15 +08:00

600 lines
23 KiB
Markdown

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