少湖说 | 科技自媒体

互联网,科技,数码,鸿蒙

PSR

PSR 是 PHP Standard Recommendations 的简写,由 PHP FIG 组织制定的 PHP 规范,是 PHP 开发的实践标准。

PHP编码遵循PSR的建议

主要包括以下几个方面:

PSR-1 基础编码规范

PSR-2 编码风格规范

PSR-3 志接口规范

PSR-4 自动加载规范

PSR-6 缓存接口规范

PSR-7 HTTP 消息接口规范

详细内容见: https://psr.phphub.org/https://github.com/PizzaLiu/PHP-FIG

注意

代码审核时将遵守以上规范

RESTful接口规范 [1.0]

更新日志

协议

使用https协议

版本

将API的版本号放入URL,如https://mqtt.lianluo.com/v1/

HTTP动词

  • GET(SELECT):从服务器取出资源(一项或多项)。
  • POST(CREATE):在服务器新建一个资源。
  • PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
  • PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
  • DELETE(DELETE):从服务器删除资源。

路径

  • 每个网址代表一种资源(resource),所以网址中不能有动词,只能有名词
  • URI中的名词表示资源集合,使用复数形式。
  • 不用大写;
  • 用中杠-不用下杠_;
  • 参数列表要encode;

状态码

  • 200 OK - [GET]:服务器成功返回用户请求的数据,该操作是幂等的(Idempotent)。
  • 201 CREATED - [POST/PUT/PATCH]:用户新建或修改数据成功。
  • 202 Accepted - [*]:表示一个请求已经进入后台排队(异步任务)
  • 204 NO CONTENT - [DELETE]:用户删除数据成功。
  • 400 INVALID REQUEST - [POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据的操作,该操作是幂等的。
  • 401 Unauthorized - [*]:表示用户没有权限(令牌、用户名、密码错误)。
  • 403 Forbidden - [*] 表示用户得到授权(与401错误相对),但是访问是被禁止的。
  • 404 NOT FOUND - [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的。
  • 406 Not Acceptable - [GET]:用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)。
  • 410 Gone -[GET]:用户请求的资源被永久删除,且不会再得到的。
  • 422 Unprocesable entity - [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误。
  • 500 INTERNAL SERVER ERROR - [*]:服务器发生错误,用户将无法判断发出的请求是否成功。

1xx范围的状态码是保留给底层HTTP功能使用的,并且估计在你的职业生涯里面也用不着手动发送这样一个状态码出来。
2xx范围的状态码是保留给成功消息使用的,你尽可能的确保服务器总发送这些状态码给用户。
3xx范围的状态码是保留给重定向用的。大多数的API不会太常使用这类状态码,但是在新的超媒体样式的API中会使用更多一些。
4xx范围的状态码是保留给客户端错误用的。例如,客户端提供了一些错误的数据或请求了不存在的内容。这些请求应该是幂等的,不会改变任何服务器的状态。
5xx范围的状态码是保留给服务器端错误用的。这些错误常常是从底层的函数抛出来的,并且开发人员也通常没法处理。发送这类状态码的目的是确保客户端能得到一些响应。收到5xx响应后,客户端没办法知道服务器端的状态,所以这类状态码是要尽可能的避免。

国际化

  1. 在HTTP请求Header中, 携带Accept-Language字段, 表明当前要使用的语言, 如
1
2
Accept-Language: zh-CN

以下是常用语言:

字段 说明
Accept-Language zh-CN 简体中文
Accept-Language zh-TW 繁体中文
Accept-Language en-US 英文

处理错误

所有错误可以在HTTP请求处, 统一处理

  1. 发生错误时, 状态码大于400; 只有当状态码位于200-300区间时, 请求才成功
  2. 状态码为400, 404, 403, 406, 500 返回的JSON信息中, message为错误信息,如
1
2
3
4
{
"message":"验证失败_内容不能为空"
}

3.状态码为 422(验证失败)时返回的信息为一个对象数组, 每个对象中包含field和message信息, 如

1
2
3
4
5
6
7
8
9
10
[
{
"field": "verify_code",
"message": "短信验证码不正确"
},
{
"field": "phone",
"message": "手机号已被注册"
}
]

调试

所有API请求结果的Header中携带X-Debug-Tag参数,该参数为请求的id
调试时, 打开/debug网址, 然后使用debug-tag过滤找到相应的请求

返回结果

  • GET /collection:返回资源对象的列表(数组)
  • GET /collection/resource:返回单个资源对象
  • POST /collection:返回新生成的资源对象
  • PUT /collection/resource:返回完整的资源对象
  • PATCH /collection/resource:返回完整的资源对象
  • DELETE /collection/resource:返回一个空文档

数据格式

只用以下常见的3种body format:

  • Content-Type: application/json (API使用的格式)
  • Content-Type: application/x-www-form-urlencoded (浏览器POST表单用的格式)

API携带超链接

返回结果中提供链接,连向其他API方法,使得用户不查文档,也知道下一步应该做什么

1
2
3
4
5
6
{"link": {
"rel": "collection https://www.example.com/zoos",
"href": "https://api.example.com/zoos",
"title": "List of zoos",
"type": "application/vnd.yourformat+json"
}}

上面代码表示,文档中有一个link属性,用户读取这个属性就知道下一步该调用什么API了。rel表示这个API与当前网址的关系(collection关系,并给出该collection的网址),href表示API的路径,title表示API的标题,type表示返回类型。

参考链接

问题

在安装完 docker 后, 我们常常安装 docker-compose 来简化 docker 的日常维护,
但是由于 GitHub 在国内较慢, 经常安装不了,所以使用 DaoCloud 提供的镜像来快速安装

官方的安装方法

  1. 安装 docker yum install docker
  2. 安装 docker-compose
1
2
3
4
$ curl -L "https://github.com/docker/compose/releases/download/1.10.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

$ chmod +x /usr/local/bin/docker-compose

使用DaoCloud镜像安装 docker-compose

  1. 安装 docker yum install docker
  2. 安装 docker-compose
1
2
3
4
5
$ curl -L https://get.daocloud.io/docker/compose/releases/download/1.11.1/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose

$ chmod +x /usr/local/bin/docker-compose

Docker 镜像加速

由于下载镜像较慢, 可以使用 DaoCloud 提供的镜像对 Docker 进行加速

1
2
$ curl -sSL https://get.daocloud.io/daotools/set_mirror.sh | sh -s http://0835afe2.m.daocloud.io

环境准备

首先需要安装swoole

可以使用pecl进行安装 ,如 pecl install swool, 注意加上版本号

或者使用构建好的docker镜像,这里使用构建好的 zacksleo/php:7.1-alpine-fpm-swoole 镜像

使用 compose 安装依赖库

1
2
composer require jesusslim/mqttclient

编写业务逻辑代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

<?php

namespace console\controllers;

use yii;
use console\components\mqtt\Logger;
use console\components\mqtt\Store;
use yii\console\Controller;
use mqttclient\src\swoole\MqttClient;
use mqttclient\src\subscribe\Topic;

class MqttController extends Controller
{
public function actionClient()
{
$r = new MqttClient(getenv('TOKEN_MQTT_HOST'), getenv('TOKEN_MQTT_PORT'), 'push-server-client');
$r->setAuth(getenv('TOKEN_MQTT_USERNAME'), getenv('TOKEN_MQTT_PASSWORD'));
$r->setKeepAlive(60);
$r->setLogger(new Logger());
$r->setStore(new Store());
$r->setTopics(
[
//消息回执
new Topic('user-auth/create', function (MqttClient $client, $msg) {
//$msg 为获取到的消息体
}),
//消息打开
new Topic('user-auth/delete', function (MqttClient $client, $msg) {
//$msg 为获取到的消息体
})
]
);
$r->connect();
}
}

执行命令

由于该客户端需要常驻内存,所以需要在 terminal 运行,如

1
2
./yii mqtt/client

进程保活

为了防止进程被杀死,获取因为异常退出,可以使用进程管理工具进行管理,如 supervisor

更多内容,参见 [[使用supervisor管理进程]]

普遍适用的接口授权及认证规则

简介

接口授权和认证有许多处理方案, 其中最简单的一种使用appkey和appsecret的方式进行验证

另外还有一种是通过对接口中的参数排序并按照一定加密算法求得哈希值, 作为授权验证的令牌

本文介绍基于OAuth2的通用授权认证规则

简言之, 客户端首先通过一定方式获取访问令牌, 然后在每次调用接口时, 携带该令牌, 服务端验证该令牌

为什么要使用

  • 部分接口目前没有使用接口认证授权, 接口一旦泄露, 将存在很大安全隐患
  • 不同项目的接口认证机制不统一, 加重了开发人员的工作负担
  • 一旦接口泄露, 服务器亦不能及时对相关接口进行封锁

流程

  • 用户中心获取访问令牌, 访问令牌会同步到公用Redis缓存中, 以便其他项目访问
  • 客户端调用相关查询接口
  • 服务器通过查询Redis中的令牌, 验证该令牌是否合法, 如果不合法, 提醒客户端重新登录
  • 令牌验证通过, 服务器执行后续操作, 返回结果

特点

  • 访问令牌存储于公用Redis缓存中, 所有客户端共用一套授权认证机制
  • 用户在修改/重置密码后, 令牌自动失效, 所有客户端会强制下线, 保证安全性
  • 用户中心深度结合, 统一注册和登录, 统一授权认证
  • Redis可以使用集群进行扩展, 加速授权和访问
  • 用户信息也将缓存在Redis中, 方便各个项目统一, 快速读取用户信息
  • 基于OAuth2实现, 服务端可以方便的控制接口的访问权限, 例如一旦client_id和client_secret泄露, 可立即封杀

如何自动部署

原理

  • GitLab有预制的钩子, 在代码提交/合并等事件中,会自动调用WebHoos, 即向该URL发送POST请求
  • 在布署服务器上监听该POST, 验证通过后执行相关的布置Shell脚本, 即可完成自动布署

配置环境

    1. 安装Python和Pip
  • 2.如果需要, 安装python的requests模块和argparse模块
1
2
pip install requests
easy_install argparse
    1. 下载监听脚本
1
curl https://raw.githubusercontent.com/zacksleo/docker-hook/master/docker-hook > /usr/local/bin/docker-hook; chmod +x /usr/local/bin/docker-hook
  • 4.脚本安装完成后即可使用docker-hook 命令, 默认监听8555端口
1
nohup docker-hook  -t  <auth-token>  -c  <command> &

其中, auth-token 替换为授权token, command替换为要执行的命令, 例如
auth-token为auto-deploy-pushserver,command为sh /mnt/pushserver/deploy.sh
则执行命令: docker-hook -t auto-deploy-pushserver -c sh /mnt/pushserver/deploy.sh

deploy.sh的内容为:

1
git push origin dev

nohup+&命令为该进程设置为守护进程, 防止进程退出

  • 5.在GitLab的项目设置里面,设置Webhooks, 本例子中则为139.198.9.141:8555/audo-deploy-pushserver

    1. 注意, 如果需要部署多个hooks, 则需要通过–port配置不同的端口, 例如
1
nohup docker-hook  -t  <auth-token2>  -c  <command2>  --port 8556 &

参考

CI项目自动上报Bug

原理

通过重写CI_Exceptions, 当程序出错时, 通过调用GitLab中提交issue的API, 将相关信息自动提交

如果是Yii2项目,见 Yii自动上报Bug

步骤

  • 获取项目id和自己的id
    可以通过GitLab的project接口, 从中拿到project_id和assignee_id

在此提供一个方法, 给自己提交一个issue, 提交时审查HTML, 可以找到 issue_assignee_id和 data-project-id

  • 创建配置文件 config/gitlab.php
1
2
3
4
5
6
// to enable, set this to true
$config['gitlab'] = true;
$config['gitlab_project_id'] =xx; //找到项目的id
$config['gitlab_assignee_id'] = xx; //找到自己的id
$config['gitlab_api'] = 'http://demo.com/api/v3/projects/'.$config['gitlab_project_id'].'/issues';
$config['gitlab_private_token'] = 'xxx';
  • 在项目中添加core/MY_Exceptions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
<?php if (!defined('BASEPATH')) {
exit('No direct script access allowed');
}

/**
* Extend exceptions to email me on exception
*
* @author Mike Funk
* @email mfunk@christianpublishing.com
*
* @file MY_Exceptions.php
*/

/**
* MY_Exceptions class.
*
* @extends CI_Exceptions
*/
class MY_Exceptions extends CI_Exceptions
{

// --------------------------------------------------------------------------

/**
* extend log_exception to add emailing of php errors.
*
* @access public
* @param string $severity
* @param string $message
* @param string $filepath
* @param int $line
* @return void
*/
function log_exception($severity, $message, $filepath, $line)
{
$ci =& get_instance();
$ci->config->load('gitlab');
if (config_item('gitlab')) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, config_item('gitlab_api'));
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'PRIVATE-TOKEN: ' . config_item('gitlab_private_token'),
));
curl_setopt($ch, CURLOPT_POSTFIELDS, [
'title' => $message,
'description' => '<pre>' . $message . '</pre>',
'assignee_id' => config_item('gitlab_assignee_id'),
'labels' => '捕虫器,错误',
]);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_VERBOSE, false);
$response = curl_exec($ch);
curl_close($ch);
}

// do the rest of the codeigniter stuff
parent::log_exception($severity, $message, $filepath, $line);
}

// --------------------------------------------------------------------------

/**
* replace short tags with values.
*
* @access private
* @param string $content
* @param string $severity
* @param string $message
* @param string $filepath
* @param int $line
* @return string
*/
private function _replace_short_tags($content, $severity, $message, $filepath, $line)
{
$content = str_replace('{{severity}}', $severity, $content);
$content = str_replace('{{message}}', $message, $content);
$content = str_replace('{{filepath}}', $filepath, $content);
$content = str_replace('{{line}}', $line, $content);

return $content;
}

// --------------------------------------------------------------------------
}

Yii2 项目自动上报Bug

原理

yii2在程序报错时, 会执行指定action, 通过重写ErrorAction, 实现Bug自动提交至GitLab的issue

步骤

  • 配置SiteController中的actions方法
1
2
3
4
5
6
7
8
public function actions()
{
return [
'error' => [
'class' => 'app\helpers\web\ErrorAction',
],
];
}
  • 重写ErrorAction, 位于app\helpers\web\ErrorAction, 并修改常量URL,PRIVATE_TOKEN和ASSIGNEE_ID

如何获取project_id和assignee_id见 WIKI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
namespace app\helpers\web;

use yii;
use yii\base\Action;
use yii\base\Exception;
use yii\base\UserException;
use yii\web\HttpException;

class ErrorAction extends \yii\web\ErrorAction
{
const URL = '{host}/api/v3/projects/{project_id}/issues'; // host替换为主机地址, project_id为项目id
const PRIVATE_TOKEN = 'tD3Te-ctECeGwEHH7-ec';
const ASSIGNEE_ID = 21;

public function run()
{
if (($exception = Yii::$app->getErrorHandler()->exception) === null) {
$exception = new HttpException(404, Yii::t('yii', 'Page not found.'));
}

if ($exception instanceof HttpException) {
$code = $exception->statusCode;
} else {
$code = $exception->getCode();
}
if ($exception instanceof Exception) {
$name = $exception->getName();
} else {
$name = $this->defaultName ?: Yii::t('yii', 'Error');
}
$preCode = $code;
if ($code) {
$name .= " (#$code)";
}

if ($exception instanceof UserException) {
$message = $exception->getMessage();
} else {
$message = $this->defaultMessage ?: Yii::t('yii', 'An internal server error occurred.');
}
if ($code != '404') {
//自动向GitLab提交Bug
$url = self::URL;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'PRIVATE-TOKEN: '.self::PRIVATE_TOKEN,
));

curl_setopt($ch, CURLOPT_POSTFIELDS, [
'title' => $message,
'description' => '<blockquote>'.Yii::$app->request->getReferrer().'</blockquote>'. '<blockquote>' . Yii::$app->request->absoluteUrl . '</blockquote><br/><pre>' . $exception . '</pre>',
'assignee_id' => self::ASSIGNEE_ID,
'labels' => '捕虫器,' . $name,
]);
curl_setopt($ch, CURLOPT_HEADER, false);
// Pass TRUE or 1 if you want to wait for and catch the response against the request made
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// For Debug mode; shows up any error encountered during the operation
curl_setopt($ch, CURLOPT_VERBOSE, false);
$response = curl_exec($ch);
curl_close($ch);
}
if (Yii::$app->getRequest()->getIsAjax() || strpos($_SERVER['REQUEST_URI'], '/api/') > -1) {
\Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
return [
'message' => $message
];
} else {
return $this->controller->render($this->view ?: $this->id, [
'name' => $name,
'message' => $message,
'exception' => $exception,
]);
}
}
}

原理

先解析markdown为HTML, 然后解析出h1-h10标签, 根据h标签的前后大小, 决定ul的层级, 以此生成ul序列的嵌套

  • 如果当前H标签和前面H标签相同, 则生成<li></li>,
  • 如果比前面的大, 则生成<ul>
  • 如果比前面的小, 生成 </ul><ul><li></li>

上手

  • 选择一个markdown Parser库, 将markdown解析

$file为文件路径, $docs为解析后的html字串

1
2
3
4
$content = file_get_contents($file);
require_once('/var/www/html/vendor/erusev/parsedown/Parsedown.php');
$parsedown = new \Parsedown();
$docs = $parsedown->text($content);
  • 解析DOM, 根据H标签的前后顺序, 生成TOC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
$dom = new \DOMDocument();
$docs = mb_convert_encoding($docs, 'HTML-ENTITIES', "UTF-8");
$dom->loadHTML($docs);

// The toc being generated.
$toc = '';
$curr = $last = 0;
$type = "ul";
$xpath = new \DomXPath($dom);
$t = $xpath->query('//h1|//h2|//h3|//h4|//h5|//h6');
foreach ($t as $key => $item) {
$level = ltrim($item->nodeName, 'h');
sscanf($item->nodeName, 'h%u', $curr);
// If the current level is greater than the last level indent one level
if ($curr > $last) {
if ($last == 0) {
$toc .= '<' . $type . " class='nav doc-menu affix-top' id='doc-menu' data-spy='affix'>\n";
} else {
$toc .= '<' . $type . " class='nav doc-sub-menu'>\n";
}
} // If the current level is less than the last level go up appropriate amount.
elseif ($curr < $last) {
$toc .= str_repeat('</li></' . $type . ">\n", $last - $curr) . "</li>\n";
} // If the current level is equal to the last.
else {
$toc .= "</li>\n";
}
// Get and/or set an id
if ($item->hasAttribute('id')) {
$id = $item->getAttribute('id');
} else {
//auto generate id for the tag
$id = uniqid();
$t[$key]->setAttribute('id', $id);
}
if ($last == 0) {
$toc .= '<li class="active"><a class="scrollto" href="#' . $id . '">' . $item->nodeValue . "</a>\n";
} else {
$toc .= '<li><a class="scrollto" href="#' . $id . '">' . $item->nodeValue . "</a>\n";
}
$last = $curr;
}
// 将修改后的DOM 赋值为docs
$docs = $dom->saveHTML();
  • 渲染页面
    $docs为html主体内容
    $toc为生成的目录, 形如
1
2
3
4
5
6
7
8
9
<ul>
<li></li>
<ul>
<li></li>
<li></li>
</ul>
<li></li>
<li></li>
</ul>

安装步骤

    1. 安装部署Docker
      因为docker对内核版本有要求,所有要先升级系统(linux内核3.0+),这里用的CentOS 7
  • 2.拉取镜像 docker pull sameersbn/redmine:latest

docker镜像加速

1
curl -sSL https://get.daocloud.io/daotools/set_mirror.sh | sh -s http://0835afe2.m.daocloud.io
    1. 安装docker-compose, “由于网络原因,该步骤可能会下载失败,请尝试多下载几次,或者外挂代理”
1
curl -L https://github.com/docker/compose/releases/download/1.8.0/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose

修改权限

1
chmod +x /usr/local/bin/docker-compose
    1. 使用compose快速启动 可以需要外挂
1
2
wget https://raw.githubusercontent.com/sameersbn/docker-redmine/master/docker-compose.yml
docker-compose up
    1. 映射数据目录

注意事项

  • linux内核要最新 3.0+
  • redmine默认使用的端口是10083,如果启动成功后,无法打开,使用telnet 远程检测端口访问性,如果不能访问,检查两个方面:1.本机防火墙是否禁用端口(用telnet 检测);2.运营商/云端是否禁用
  • 注意使用volumn命令映射数据存储命令

参考链接

0%