API 版本控制

开发APP时,随着业务需求的不断变化,服务端接口返回的数据也就会有各种变动,但由于APP无法动态更新请求接口,那么服务端接口变动时就需要兼容旧版本,所以服务端这边就需要对接口做版本控制

版本控制方式(这里使用 Symfony 框架实现了第一种和第四种):

  1. 使用一个 URI 参数:https://example.com/api/v1/login
  2. 使用查询参数:https://example.com/api/login?version=v1
  3. 自定义 Accept mime-type: Accept: application/json; version=1
  4. 自定义 Header:VERSION: 1

目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Bundles/
ApiBundle/
Controller
V1/
User/
Traits/
LoginTrait.php
CenterController.php
V2/
User/
Traits/
LoginTrait.php
CenterController.php
BaseController.php
Traits/
User/
LoginAbstractTrait.php User模块通用方法
ApiTrait.php
接口基类
1
2
3
4
5
6
7
8
9
10
11
12
// BaseController.php
<?php

namespace Bundles\ApiBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Bundles\ApiBundle\Traits\AbstractTrait;

class BaseController extends Controller
{
use AbstractTrait;
}
接口相关代码
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
// V1 版本
<?php

namespace Bundles\ApiBundle\Controller\V1\User;

use Bundles\ApiBundle\Controller\BaseController;
use Bundles\ApiBundle\Traits\User\LoginAbstractTrait;
use Bundles\ApiBundle\Controller\V1\User\Traits\LoginTrait;

class CenterController extends BaseController
{
use LoginAbstractTrait, LoginTrait;

// V1.0
public function loginAction()
{
die('版本V1');
}

// V1.1
public function loginM1()
{
die('版本V1.1');
}
}

// V2 版本
<?php

namespace Bundles\ApiBundle\Controller\V2\User;

use Bundles\ApiBundle\Controller\BaseController;
use Bundles\ApiBundle\Traits\User\LoginAbstractTrait;
use Bundles\ApiBundle\Controller\V2\User\Traits\LoginTrait;

class CenterController extends BaseController
{
use LoginAbstractTrait, LoginTrait;

// V2.0
public function loginAction()
{
die('版本V2');
}

// V2.1
public function loginM1()
{
die('版本V2.1');
}
}
注册监听器服务
1
2
3
4
5
6
7
8
9
services:
_defaults:
# ... be sure autowiring is enabled
autowire: true

api_request_listener:
class: Bundles\ApiBundle\Listener\ApiRequestListener
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest, priority: 10 }

自定义HEADER参数
实现原理:获取HEADER的自定义参数VERSION, 根据版本号调用不同方法
API接口:

1. localhost:8080/user/login    HEADER:VERSION = 1
2. localhost:8080/user/login    HEADER:VERSION = 1.1
3. localhost:8080/user/login    HEADER:VERSION = 2
4. localhost:8080/user/login    HEADER:VERSION = 2.1
路由配置
1
2
3
4
user_login:
path: /login
defaults: { _controller: ApiBundle:V1\User\Center:login }
methods: [GET, POST]
请求监听器
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
<?php

namespace Bundles\ApiBundle\Listener;

use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\DependencyInjection\ContainerInterface;

class ApiRequestListener
{
/**
* @var ContainerInterface
*/
protected $container;

private static $controllerNameSpace = 'Bundles\ApiBundle\Controller\\';

public function __construct(ContainerInterface $container)
{
$this->container = $container;
}

/**
* @param GetResponseEvent $event
*/
public function onKernelRequest(GetResponseEvent $event)
{
if (!$event->isMasterRequest()) {
return;
}

$request = $event->getRequest();
$attributes = $request->attributes;
$version = $request->server->get('HTTP_VERSION');
if (is_numeric($version) && $version) {
$controller = $attributes->get('_controller');
$delimiter = '::';
if (strpos($controller, $delimiter) !== false) {
if (strpos($version, '.') !== false) {
$major = (int)substr($version, 0, strpos($version, '.'));
$minor = (int)substr($version, strpos($version, '.') + 1);
} else {
$major = (int)$version;
$minor = null;
}

if (empty($major) || $major === 1 && empty($minor)) {
return;
}

list($controllerName, $actionName) = explode($delimiter, $controller, 2);
$controllerName = str_replace(self::$controllerNameSpace, '', $controllerName);
$controllerName = self::$controllerNameSpace . "V{$major}\\" . substr($controllerName, strpos($controllerName, '\\') + 1);
if (!empty($minor)) {
$actionName = substr($actionName, 0, -6) . "M{$minor}";
}

$request->attributes->set('_controller', $controllerName . $delimiter . $actionName);
}
}
}
}

URI参数, 动态匹配接口版本号, 根据版本号调用不同方法
API接口:

1. localhost:8080/user/login
2. localhost:8080/user/v1.1/login
3. localhost:8080/user/v2/login
4. localhost:8080/user/v2.1/login
接口路由
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#=================================版本V1.0 API START===================================#
user_login:
path: /login
defaults: { _controller: ApiBundle:V1\User\Center:login }
methods: [GET, POST]

v1_user_login:
path: /{version}/login
defaults: { _controller: ApiBundle:V1\User\Center:login }
methods: [GET, POST]
requirements:
version: 'v1\.\d+'

=================================版本V2.0 API START===================================#
v2_user_login:
path: /{version}/login
defaults: { _controller: ApiBundle:V2\User\Center:login }
methods: [GET, POST]
requirements:
version: 'v2(\.\d+)?'
请求监听器
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
<?php

namespace Bundles\ApiBundle\Listener;

use Symfony\Component\HttpKernel\Event\GetResponseEvent;

class ApiRequestListener
{
public function onKernelRequest(GetResponseEvent $event)
{
if (!$event->isMasterRequest()) {
return;
}

$request = $event->getRequest();
$attributes = $request->attributes;
$version = isset($attributes->get('_route_params')['version']) ? $attributes->get('_route_params')['version'] : '';
if (!empty($version)) {
$controller = $attributes->get('_controller');
$delimiter = '::';
if (strpos($controller, $delimiter) !== false) {
if (strpos($version, '.') !== false) {
$major = (int)substr($version, 1, strpos($version, '.'));
$minor = (int)substr($version, strpos($version, '.') + 1);
} else {
$major = (int)$version;
$minor = null;
}

if (empty($major) || $major === 1 && empty($minor)) {
return;
}

list($controllerName, $actionName) = explode($delimiter, $controller, 2);
if (!empty($minor)) {
$actionName = substr($actionName, 0, -6) . "M{$minor}";
}

$request->attributes->set('_controller', $controllerName . $delimiter . $actionName);
}
}
}
}
0%