# 后端编码规范

## bcy-next

**0x00: 开发环境需求**

* PHP 7.4
* [Swoole 4.5](https://www.swoole.com/)
* Redis
* Composer

**0x01: IDE**

* 使用支持PHP 7.4版本的PHPStorm (2019.3或以上)并安装[PHP Annotation插件](https://plugins.jetbrains.com/plugin/7320-php-annotations)
* 设置[PHPStorm保存自动格式化](https://blog.csdn.net/jefry52/article/details/103149207)

**0x02: 启动**

* 完整阅读一遍[Hyperf文档](https://hyperf.wiki/2.0/)
* 执行`composer install`命令安装包
* 按照需要修改`.env`文件的环境变量配置
* 运行`composer run start`命令启动服务

注意, `composer run start`使用了Hyperf提供的watcher组件实现了热重启功能修改了代码后无需手动重启, 但是如果有删除文件的动作必须手动重启服务, 详参[热更新 Watcher](https://hyperf.wiki/2.0/#/zh-cn/watcher)

**0x03: 函数编码与PHPDoc**

```php
/**
 * 这是一个foo函数
 * @author 2m
 * @param $name string
 * @return string
*/
function foo(string $name): string {
    return "hello {$name}";
}
```

上例是一个标准的函数/方法

* 必须编写PHPDoc, 第一行简述此函数/方法的作用, `@author`为原始作者
* `foo(string $name): string`, 函数的参数列表类型和函数返回类型`必须`声明

**0x04: 代码基本分层**

* `Controller`控制器层主要负责接收验证客户端的数据, 调用`Service`层的代码返回数据给客户端
* `Service`逻辑层是主要逻辑代码的聚集, 接收来自控制器层或者其它Service层的调用, 禁止在Service层中获取请求参数或响应客户端
* `Model`模型层是数据表的实体表示, 以及描述表与表之间的关联关系, 模型层只能做`描述`动作, 禁止在模型层编写操作数据库的代码
* `Utils`工具层提供一系列与`业务无关联`的助手函数, 反例是编写一个使用$userid获取用户数据的方法, 这属于与业务有关联的方法, 统一放到Service层中

**0x05: 配置**

在项目中有以下三种配置来源, 各负责不同类型的配置, 如果需要增加配置项需要合理安排

* `config`目录下的配置文件, 配置一些非敏感和不区分环境的配置, 另外也为各种组件提供配置来源
* `.env`环境变量存放一些区分环境的配置, 比如`debug`选项, 在生产环境中是禁用的, 但是在开发环境中开启debug尤其重要
* `ACM`是阿里云的配置中心, 用于存放一些敏感的配置, 比如`access_token`之类

**0x06: 接口测试**

利用Hyperf提供的[自动化测试组件](https://hyperf.wiki/2.0/#/zh-cn/testing), 我们可以很方便的创建接口测试用例, 对于一些核心的接口测试覆盖率要求达到`100%`, 从一个Demo用例开始了解`test/Cases/DemoTest.php`, 执行`composer run test`运行测试

**0x07: 主要的环境变量**

在`.env.example`文件中有详细的可用环境变量列表已经默认的设置, 如果业务中有加入其它的环境变量, 一定要在此文件中写明作用以及默认值

* APP\_ENV 应用环境, 可用的值为`prod`生产环境, `dev`线上测试环境, `local`本地开发环境
* SERVER\_WORKER\_NUM 服务Worker进程数, 设置为`(null)`即自动根据CPU核心数设置, 在本地开发环境中设置为`1`可以减少资源消耗

**0x08: 模型**

可以通过Hyperf提供的`gen:model`命令快速生成模型类文件, 模型类文件生成后要检查`$table`属性是否和数据库表名一致, 注意模型类以下默认设置

```php
class Foo extends \Hyperf\Database\Model\Model {
    protected $table = 'foo';
    protected $dateFormat = 'U';

    const CREATED_AT = 'create_time';
    const UPDATED_AT = 'update_time';
}
```

这意味着每个数据表都要求有`create_time`和`update_time`字段并且字段类型为`int`

**0x09: 响应**

多数情况下使用`AbstractController::ok`和`AbstractController::error`方法可以完成响应动作, 但是如果需要修改响应头或者设置其它的HTTP状态码, 参考下例代码

```php
use App\Core\Response;

Response::header('x-foo', 'bar'); // 设置x-foo响应头
Response::statusCode(201); // 设置响应状态码为201
// 使用上例设置了响应后, 必须用过`ok`或`error`方法或下面两个方法结束请求才会生效

Response::send('Other Content'); // 发送自定义内容
Response::asJson(['foo' => 'bar']); // 发送自定义JSON内容
```

**0x10: 异常**

当前代码环境无法处理的异常一定要抛出到上一层让调用者知道发生了什么, 下例是反例

```php
class Service 
{
    public function getUser() {
        try {
            // some code...
            // throw exception!
        }catch(Throwable$exception){
            return false;
        }
    }
}

class Controller {
    public function index(Service $service) {
        if(!$service->getUser()) {
            // some code...
        }   
    }
}
```

上例`getUser`方法调用了其它的代码并发生了异常, 但是并没有处理这个异常而是直接返回了`false`, 这会增加debug成本, 如果这个异常在`getUser`函数内无法恢复, 就必须抛出到上一层让控制器知道发生了什么

```php
class Service 
{
    public function getUser() {
        try {
            // some code...
            // throw exception!
        }catch(Throwable $exception){
            throw $exception;
        }

        // some code...
    }
}

class Controller extends \App\Controller\AbstractController {
    public function index(Service $service) {
        list($data, $exception) = \App\Utils\Functions::pcall(fn() => $service->getUser());

        if(!$exception) {
            $this->error($exception);
        }   else{        
            $this->ok($data);
        }   
    }

    public function better(Service $service) {
        $this->pcall(fn() => $service->getUser());
    }   
}
```

上例使用了PHP 7.4的新特性箭头函数, 配合`pcall`助手方法可以把`try{}catch`代码块转换成`golang`风格的异常处理逻辑, `better`方法中提供了一种更方便的处理方法, 它与`index`的处理逻辑等效

**0x11: 表/模型关联**

表与表之间的关联操作会很频繁, Hyperf提供了非常强大的[模型关联](https://hyperf.wiki/2.0/#/zh-cn/db/relationship) 能力, 尽量把表与表之间的关联转换为模型与模型间的关联从而避免手动`join`连表, 在无法避免使用`join`时所有参与的字段`必须`加上表名

````php
use App\Model\Model;

class Order extends Model {
    public function user() {
        // 声明Order -> User模型间的关联关系
        return $this->belongsTo(User::class, 'user_id', 'user_id');
    }
}
class User extends Model {}

// Bad: 使用join连接表
Order::query()
    ->join('user', 'user.user_id', '=', 'order.user_id')
    ->where(...)
    ->select(['user.user_id', 'order.order_id'])->get();

// Better: 使用关联模型
Order::query()->where(...)->user;## bcy-next

#### 0x00: 开发环境需求
* PHP 7.4
* [Swoole 4.5](https://www.swoole.com/)
* Redis
* Composer

#### 0x01: IDE
* 使用支持PHP 7.4版本的PHPStorm (2019.3或以上)并安装[PHP Annotation插件](https://plugins.jetbrains.com/plugin/7320-php-annotations)
* 设置[PHPStorm保存自动格式化](https://blog.csdn.net/jefry52/article/details/103149207)

#### 0x02: 启动
* 完整阅读一遍[Hyperf文档](https://hyperf.wiki/2.0/)
* 执行`composer install`命令安装包
* 按照需要修改`.env`文件的环境变量配置
* 运行`composer run start`命令启动服务

注意, `composer run start`使用了Hyperf提供的watcher组件实现了热重启功能修改了代码后无需手动重启, 但是如果有删除文件的动作必须手动重启服务, 详参[热更新 Watcher](https://hyperf.wiki/2.0/#/zh-cn/watcher)

#### 0x03: 函数编码与PHPDoc
```php
/**
 * 这是一个foo函数
 * @author 2m
 * @param $name string
 * @return string
*/
function foo(string $name): string {
    return "hello {$name}";
}
````

上例是一个标准的函数/方法

* 必须编写PHPDoc, 第一行简述此函数/方法的作用, `@author`为原始作者
* `foo(string $name): string`, 函数的参数列表类型和函数返回类型`必须`声明

### 0x04: 代码基本分层

* `Controller`控制器层主要负责接收验证客户端的数据, 调用`Service`层的代码返回数据给客户端
* `Service`逻辑层是主要逻辑代码的聚集, 接收来自控制器层或者其它Service层的调用, 禁止在Service层中获取请求参数或响应客户端
* `Model`模型层是数据表的实体表示, 以及描述表与表之间的关联关系, 模型层只能做`描述`动作, 禁止在模型层编写操作数据库的代码
* `Utils`工具层提供一系列与`业务无关联`的助手函数, 反例是编写一个使用$userid获取用户数据的方法, 这属于与业务有关联的方法, 统一放到Service层中

### 0x05: 配置

在项目中有以下三种配置来源, 各负责不同类型的配置, 如果需要增加配置项需要合理安排

* `config`目录下的配置文件, 配置一些非敏感和不区分环境的配置, 另外也为各种组件提供配置来源
* `.env`环境变量存放一些区分环境的配置, 比如`debug`选项, 在生产环境中是禁用的, 但是在开发环境中开启debug尤其重要
* `ACM`是阿里云的配置中心, 用于存放一些敏感的配置, 比如`access_token`之类

### 0x06: 接口测试

利用Hyperf提供的[自动化测试组件](https://hyperf.wiki/2.0/#/zh-cn/testing), 我们可以很方便的创建接口测试用例, 对于一些核心的接口测试覆盖率要求达到`100%`, 从一个Demo用例开始了解`test/Cases/DemoTest.php`, 执行`composer run test`运行测试

### 0x07: 主要的环境变量

在`.env.example`文件中有详细的可用环境变量列表已经默认的设置, 如果业务中有加入其它的环境变量, 一定要在此文件中写明作用以及默认值

* APP\_ENV 应用环境, 可用的值为`prod`生产环境, `dev`线上测试环境, `local`本地开发环境
* SERVER\_WORKER\_NUM 服务Worker进程数, 设置为`(null)`即自动根据CPU核心数设置, 在本地开发环境中设置为`1`可以减少资源消耗

### 0x08: 模型

可以通过Hyperf提供的`gen:model`命令快速生成模型类文件, 模型类文件生成后要检查`$table`属性是否和数据库表名一致, 注意模型类以下默认设置

```php
class Foo extends \Hyperf\Database\Model\Model {
    protected $table = 'foo';
    protected $dateFormat = 'U';

    const CREATED_AT = 'create_time';
    const UPDATED_AT = 'update_time';
}
```

这意味着每个数据表都要求有`create_time`和`update_time`字段并且字段类型为`int`

### 0x09: 响应

多数情况下使用`AbstractController::ok`和`AbstractController::error`方法可以完成响应动作, 但是如果需要修改响应头或者设置其它的HTTP状态码, 参考下例代码

```php
use App\Core\Response;

Response::header('x-foo', 'bar'); // 设置x-foo响应头
Response::statusCode(201); // 设置响应状态码为201
// 使用上例设置了响应后, 必须用过`ok`或`error`方法或下面两个方法结束请求才会生效

Response::send('Other Content'); // 发送自定义内容
Response::asJson(['foo' => 'bar']); // 发送自定义JSON内容
```

### 0x10: 异常

当前代码环境无法处理的异常一定要抛出到上一层让调用者知道发生了什么, 下例是反例

```php
class Service 
{
    public function getUser() {
        try {
            // some code...
            // throw exception!
        }catch(Throwable$exception){
            return false;
        }
    }
}

class Controller {
    public function index(Service $service) {
        if(!$service->getUser()) {
            // some code...
        }   
    }
}
```

上例`getUser`方法调用了其它的代码并发生了异常, 但是并没有处理这个异常而是直接返回了`false`, 这会增加debug成本, 如果这个异常在`getUser`函数内无法恢复, 就必须抛出到上一层让控制器知道发生了什么

```php
class Service 
{
    public function getUser() {
        try {
            // some code...
            // throw exception!
        }catch(Throwable $exception){
            throw $exception;
        }

        // some code...
    }
}

class Controller extends \App\Controller\AbstractController {
    public function index(Service $service) {
        list($data, $exception) = \App\Utils\Functions::pcall(fn() => $service->getUser());

        if(!$exception) {
            $this->error($exception);
        }   else{        
            $this->ok($data);
        }   
    }

    public function better(Service $service) {
        $this->pcall(fn() => $service->getUser());
    }   
}
```

上例使用了PHP 7.4的新特性箭头函数, 配合`pcall`助手方法可以把`try{}catch`代码块转换成`golang`风格的异常处理逻辑, `better`方法中提供了一种更方便的处理方法, 它与`index`的处理逻辑等效

### 0x11: 表/模型关联

表与表之间的关联操作会很频繁, Hyperf提供了非常强大的[模型关联](https://hyperf.wiki/2.0/#/zh-cn/db/relationship) 能力, 尽量把表与表之间的关联转换为模型与模型间的关联从而避免手动`join`连表, 在无法避免使用`join`时所有参与的字段`必须`加上表名

```php
use App\Model\Model;

class Order extends Model {
    public function user() {
        // 声明Order -> User模型间的关联关系
        return $this->belongsTo(User::class, 'user_id', 'user_id');
    }
}
class User extends Model {}

// Bad: 使用join连接表
Order::query()
    ->join('user', 'user.user_id', '=', 'order.user_id')
    ->where(...)
    ->select(['user.user_id', 'order.order_id'])->get();

// Better: 使用关联模型
Order::query()->where(...)->user;
```

\`\`\`
