ThinkPHP Backend Development Standards
References:ThinkPHP 8.0 Development Standards | PSR-4 Autoloading
Trigger Conditions
- Develop ThinkPHP projects
- Implement REST APIs
- Use model data access
- Implement JWT authentication
- Implement permission control
Part 1: Technology Stack
| Layer |
Selection |
Description |
| Runtime Environment |
PHP 8.1+ |
Fibers, Enums, Readonly Properties |
| Framework |
ThinkPHP 8.x |
MVC Architecture, Built-in ORM |
| Database |
MySQL 8.x |
InnoDB Engine |
| Cache |
Redis 7.x |
Distributed Cache |
| Authentication |
firebase/php-jwt |
JWT Signing/Verification |
| API Documentation |
Swagger UI (swagger.json) |
Static Documentation |
Part 2: Directory Structure
The base directory is based on ThinkPHP 8 Official Directory Structure ;app/ On this basis, it is divided by business modules (auth、system , etc.), and each module contains controller/model/service/validate.
app/
├── BaseController.php # 基础控制器
├── ExceptionHandle.php # 全局异常处理
├── common.php, middleware.php # 公共函数 / 中间件定义
│
├── auth/ # 认证模块
│ ├── controller/{AuthController, WxMaAuthController}
│ └── service/{AuthService, WxMaAuthService}
│
├── system/ # 系统管理
│ ├── controller/{UserController, RoleController, MenuController, ...}
│ ├── model/{User, Role, Menu, ...}
│ ├── service/{UserService, RoleService, RolePermService, DataPermissionService, ...}
│ ├── validate/{UserValidate, RoleValidate}
│ └── enums/{ActionType, LogModule}
│
├── codegen/ # 代码生成器
├── file/ # 文件管理
│
├── common/ # 公共代码
│ ├── constants/ # RedisConstants, RedisKey
│ ├── enums/ # DataScopeEnum
│ ├── exception/ # BusinessException
│ ├── middleware/ # Auth, Perm, DataScope, Cors, Log, RateLimit, ConvertCase
│ ├── model/BaseModel.php
│ ├── traits/{AuthTrait, ParamsTrait}
│ ├── util/{CaseConverter, IdStringify, PageUtil, VerifyCodeHelper}
│ └── web/ # Result, PageResult, ResultCode, IResultCode
│
├── controller/BaseController.php # 基础控制器
│
extend/ # 扩展类库
├── jwt/{JwtTokenManager, TokenManager, ...}
├── redis/{RedisClient, KeyFormatter}
├── sse/{SseEmitter, SseService, ...}
└── http/HttpClient.php
config/ route/ public/ sql/
Design Principles:
common/ is a public component shared by all modules
extend/ is a third-party/framework extension (JWT/Redis/SSE) and does not depend on business logic
app/{module}/ Organized by module, each module contains controller/model/service/validate
Part 3: Naming Conventions
Follow ThinkPHP 8.0 Development Standards + PSR-4。
3.1 Files and Classes
| Rule |
Example |
| Directories use lowercase + underscores |
controller/, user_service/ |
| Class names use camel case (first letter uppercase) |
UserController, UserValidate |
| Class name matches file name |
UserController → UserController.php |
| Namespace matches directory path |
app\system\controller → app/system/controller/ |
3.2 Constants and Configuration
| Type |
Convention |
Example |
| Constant |
Uppercase letters and underscores |
APP_PATH, HAS_ONE |
| Configuration Parameters |
Lowercase letters and underscores |
url_route_on |
| Environment Variables |
Uppercase letters and underscores |
APP_DEBUG, DB_HOST |
3.3 Data Tables and Fields
| Rule |
Example |
| Data tables use lowercase + underscores |
sys_user, sys_role_menu |
| Fields use lowercase + underscores |
user_name, create_time |
| Camel case and Chinese naming are prohibited |
❌ userName, ❌ 用户表 |
3.4 Functions and Variables
| Type |
Convention |
Example |
| Method |
Camel case (first letter lowercase) |
getUserName() |
| Global Functions |
Lowercase letters and underscores |
get_client_ip() |
| Property |
Camel case (first letter lowercase) |
$tableName |
Part 4: RESTful API Standards
4.1 Standard CRUD Paths
| Operation |
Method |
Path |
| Paginated List |
GET |
/api/v1/users/page |
| Details |
GET |
/api/v1/users/:id |
| Create |
POST |
/api/v1/users |
| Update |
PUT |
/api/v1/users |
| Delete |
DELETE |
/api/v1/users/:id |
| Batch Delete |
DELETE |
/api/v1/users/batch |
| Dropdown Options |
GET |
/api/v1/users/options |
4.2 Controller Template
declare(strict_types=1);
namespace app\system\controller;
use app\BaseController;
use app\system\model\User;
use app\system\validate\UserValidate;
use app\common\exception\BusinessException;
use app\common\web\ResultCode;
final class UserController extends BaseController
{
protected bool $requireAuth = true;
/** 用户分页列表 */
public function page()
{
$params = $this->request->get();
$result = User::where(function ($q) use ($params) {
if (!empty($params['keywords']))
$q->whereLike('username|nickname', $params['keywords']);
if (isset($params['status']))
$q->where('status', $params['status']);
})->order('create_time', 'desc')
->paginate(['page' => $params['pageNum'] ?? 1, 'list_rows' => $params['pageSize'] ?? 20]);
return $this->successPaginate($result->items(), $result->total());
}
/** 新增用户 */
public function create()
{
$params = $this->request->post();
$this->validate($params, UserValidate::class, 'create');
if (User::where('username', $params['username'])->find())
throw new BusinessException(ResultCode::USER_ERROR, '用户名已存在');
$user = new User();
$user->username = $params['username'];
$user->password = password_hash($params['password'], PASSWORD_BCRYPT);
$user->save();
return $this->success(['id' => $user->id]);
}
}
Part 5: Response Format and Exception Handling
5.1 Unified Response
class Result
{
public static function success(mixed $data = null, string $msg = '成功'): array
{
return ['code' => '00000', 'msg' => $msg, 'data' => $data];
}
public static function page(array $list, int $total): array
{
return ['code' => '00000', 'msg' => '成功', 'data' => ['list' => $list, 'total' => $total]];
}
public static function failedWith(ResultCode $code, string $msg = ''): array
{
return ['code' => $code->value, 'msg' => $msg ?: $code->getMsg(), 'data' => null];
}
}
5.2 Base Controller Response Methods
abstract class BaseController
{
protected function success(mixed $data = null, string $msg = ''): Json
{
return json(Result::success($data, $msg ?: ResultCode::SUCCESS->getMsg()));
}
protected function successPaginate(array $list, int $total): Json
{
return json(Result::page($list, $total));
}
}
5.3 Business Exceptions
final class BusinessException extends RuntimeException
{
private ResultCode $resultCode;
public function __construct(ResultCode $resultCode, string $message = '')
{
$this->resultCode = $resultCode;
parent::__construct($message ?: $resultCode->getMsg());
}
public function getResultCode(): ResultCode { return $this->resultCode; }
}
5.4 Global Exception Handling
class ExceptionHandle extends Handle
{
public function render($request, Throwable $e): Response
{
if ($e instanceof BusinessException)
return json(Result::failedWith($e->getResultCode(), $e->getMessage()));
if ($e instanceof ValidateException)
return json(Result::failedWith(ResultCode::PARAM_ERROR, $e->getError()));
return parent::render($request, $e); // 生产环境隐藏详情
}
}
// config/app.php — 注册异常处理器
return ['exception_handle' => \app\ExceptionHandle::class];
Part 6: Model Standards
class BaseModel extends Model
{
protected $autoWriteTimestamp = true;
protected $createTime = 'create_time';
protected $updateTime = 'update_time';
use \think\model\concern\SoftDelete;
protected $deleteTime = 'delete_time';
protected $defaultSoftDelete = 0;
}
final class User extends BaseModel
{
protected $name = 'sys_user';
protected $pk = 'id';
protected $hidden = ['password', 'delete_time'];
public function dept()
{
return $this->belongsTo(Dept::class, 'dept_id');
}
public function roles()
{
return $this->belongsToMany(Role::class, UserRole::class, 'role_id', 'user_id');
}
}
Part 7: Routing Standards
use think\facade\Route;
// 认证接口(无需登录)
Route::group('api/v1/auth', function () {
Route::post('login', 'auth.controller.AuthController/login');
Route::post('logout', 'auth.controller.AuthController/logout');
Route::post('refresh-token', 'auth.controller.AuthController/refreshToken');
})->allowCrossDomain();
// 需要登录的接口
Route::group('api/v1', function () {
Route::group('users', function () {
Route::get('page', 'system.controller.UserController/page');
Route::get(':id', 'system.controller.UserController/detail');
Route::post('', 'system.controller.UserController/create');
Route::put('', 'system.controller.UserController/update');
Route::delete(':id', 'system.controller.UserController/delete');
Route::delete('batch', 'system.controller.UserController/batchDelete');
});
})->middleware(\app\middleware\Auth::class)->allowCrossDomain();
Part 8: Authentication Specification
8.1 JWT Service
final class JwtService
{
public static function generateAccessToken(array $payload): string
{
$payload['exp'] = time() + Config::get('jwt.access_ttl', 7200);
$payload['iat'] = time();
$payload['iss'] = Config::get('jwt.issuer', 'youlai-think');
return JWT::encode($payload, Config::get('jwt.secret'), 'HS256');
}
public static function parseToken(string $token): ?array
{
try {
return (array) JWT::decode($token, new Key(Config::get('jwt.secret'), 'HS256'));
} catch (\Exception) { return null; }
}
}
8.2 Authentication Middleware
final class Auth
{
public function handle(Request $request, Closure $next): Response
{
$token = str_replace('Bearer ', '', $request->header('Authorization', ''));
if (empty($token)) return json(Result::failedWith(ResultCode::ACCESS_TOKEN_INVALID));
$payload = JwtService::parseToken($token);
if (!$payload) return json(Result::failedWith(ResultCode::ACCESS_TOKEN_INVALID));
$request->userId = $payload['sub'];
$request->username = $payload['username'] ?? '';
return $next($request);
}
}
Part 9: Validator Specification
final class UserValidate extends Validate
{
protected $rule = [
'username' => 'require|max:50',
'password' => 'require|min:6|max:20',
'nickname' => 'require|max:50',
'mobile' => 'mobile',
'status' => 'in:0,1',
];
protected $scene = [
'create' => ['username', 'password', 'nickname', 'mobile', 'status'],
'update' => ['id', 'nickname', 'mobile', 'status'],
];
}
Part 10: Comment Specification
- All public methods use PHPDoc
/** */ comments
- Method comments include
@param/@return/@throws
declare(strict_types=1) on the first line of the file (PHP 8 enforces type checking)
/**
* 用户管理控制器。
*/
final class UserController extends BaseController
{
/**
* 创建新用户。
*
* @return Json {"code":"00000","data":{"id":1}}
* @throws BusinessException 用户名已存在
*/
public function create()
{
// ...
}
}
Part 11: Code Quality Checklist
- Directories use lowercase + underscores, class names use camel case
- Data tables and fields use lowercase + underscores
- Follow RESTful API path conventions
- Use a unified response format (
Result / ResultCode)
- Implement global exception handling (
ExceptionHandle)
- Business exceptions use
BusinessException + ResultCode
- Models inherit from
BaseModel, use soft deletes
- Use the
UserValidate validator to validate parameters
- Explicit route mapping
-
declare(strict_types=1) on the first line of the file
Common Anti-Patterns
| Anti-Pattern |
Correct Practice |
return json(['code' => 0]) Scattered everywhere |
Use Result::success() unified encapsulation |
| Direct SQL queries in the Controller |
Delegate to the Model / Service layer |
if (!$user) { exit('用户不存在'); } |
Throw BusinessException, go through global exception handling |
catch (\Exception $e) { } empty catch |
Log or rethrow BusinessException |
| Table names use camel case |
sys_user(lowercase + underscores), prohibit SysUser |