Swoft 基于Swoole

Swoft: 首个基于 Swoole 原生协程的新时代 PHP 高性能协程全栈框架

首个基于 Swoole 原生协程的新时代 PHP 高性能协程全栈框架,内置协程网络服务器及常用的协程客户端,
常驻内存,不依赖传统的 PHP-FPM,全异步非阻塞 IO 实现,以类似于同步客户端的写法实现异步客户端的
使用,没有复杂的异步回调,没有繁琐的 yield, 有类似 Go 语言的协程、灵活的注解、强大的全局
依赖注入容器、完善的服务治理、灵活强大的 AOP、标准的 PSR 规范实现等等,可以用于构建高性能的Web系统
、API、中间件、基础服务等等。

特性

  • 基于 Swoole扩展
  • 内置协程网络服务器
  • WebSocket服务器
  • MVC分层设计
  • 高性能路由
  • 强大的AOP(面向切面编程)
  • 灵活的注解功能
  • 全局的依赖注入容器
  • 基于 PSR-7HTTP 消息实现
  • 基于 PSR-11 的容器规范实现
  • 基于 PSR-14 的事件管理器
  • 基于 PSR-15 的中间件
  • 基于 PSR-16 的缓存设计
  • 可扩展的高性能 RPC
  • RESTful 支持
  • 国际化(i18n)支持
  • 快速灵活的参数验证器
  • 完善的服务治理,熔断、降级、负载、注册与发现
  • 通用连接池 MysqlRedisRPC
  • 数据库 ORM
  • 协程、异步任务投递
  • 自定义用户进程
  • 协程和同步阻塞客户端无缝自动切换
  • 别名机制
  • 跨平台热更新自动 Reload
  • 强大的日志系统

基础信息

环境要求

必须安装的

  • PHP > 7.0, 推荐7.1+
  • Composer
  • Redis的异步客户端 hiredis
  • 连接迭代器依赖 pcre
  • Swoole扩展, 版本>=2.1
    • Swoole开启协程和异步Redis
  • PDO扩展

有冲突的

下面列出一些已知的和swoole有冲突的php扩展,请使用swoft时不要安装或禁用它们:

  • xdebug
  • xhprof
  • blackfire
  • zend
  • trace
  • uopz

推荐环境配置

可以查看 swoftDockerfile 文件

安装Swoft

Composer安装

1
composer create-project swoft/swoft swoft

手动安装

1
2
3
4
5
git clone https://github.com/swoft-cloud/swoft
cd swoft
composer install --no-dev # 不安装 dev 依赖会更快一些
cp .env.example .env
vim .env # 根据需要调整启动参数

Docker安装

1
docker run -p 80:80 swoft/swoft

Docker-Compose 安装

1
2
3
git clone https://github.com/swoft-cloud/swoft
cd swoft
docker-compose up

开发准备

  • 安装PHP7.1, autoconf, openssl, redis
  • 下载安装Hiredis
  • 下载安装`Swoole

快速起步

服务启动与管理

帮助命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[root@caoxl swoft]# php bin/swoft -h
____ __ _
/ ___|_ _____ / _| |_
\___ \ \ /\ / / _ \| |_| __|
___) \ V V / (_) | _| |_
|____/ \_/\_/ \___/|_| \__|

Usage:
php bin/swoft -h {command} [arguments] [options]

Commands:
app There are some help command for application[built-in]
dev Some commands for application dev[built-in]
entity The group command list of database entity
gen Generate some common application template classes[built-in]
rpc The group command list of rpc server
server The group command list of HTTP-Server
test Test command
ws There are some commands for manage the webSocket server

Options:
-h, --help Display help information
-v, --version Display version information

HTTP服务器

是否同时启动RPC服务器取决于 .env 文件配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 启动服务,根据 .env 配置决定是否是守护进程
php bin/swoft start

// 守护进程启动,覆盖 .env 守护进程(DAEMONIZE)的配置
php bin/swoft start -d

// 重启
php bin/swoft restart

// 重新加载
php bin/swoft reload

// 关闭服务
php bin/swoft stop

WebSocket服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 启动服务,根据 .env 配置决定是否是守护进程
php bin/swoft ws:start

// 守护进程启动,覆盖 .env 守护进程(DAEMONIZE)的配置
php bin/swoft ws:start -d

// 重启
php bin/swoft ws:restart

// 重新加载
php bin/swoft ws:reload

// 关闭服务
php bin/swoft ws:stop

RPC服务器

使用独立的RPC服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 启动服务,根据 .env 配置决定是否是守护进程
php bin/swoft rpc:start

// 守护进程启动,覆盖 .env 守护进程(DAEMONIZE)的配置
php bin/swoft rpc:start -d

// 重启
php bin/swoft rpc:restart

// 重新加载
php bin/swoft rpc:reload

// 关闭服务
php bin/swoft rpc:stop

env环境配置

在执行 composer install 的时候程序会自动复制环境变量配置文件。 若没有,可手动复制项目根目录的 .env.example 并命名为 .env

可用配置项

下面是官方的 .env 文件支持的env配置项, 你也可以按需添加自己需要的配置

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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# Application
TIME_ZONE=Asia/Shanghai
LOG_ENABLE=false
APP_DEBUG=false

# Server
# pid 文件,要同时在一台机器上运行多个server时,注意要更改pid文件
PFILE=/tmp/swoft.pid
# 项目名,要同时在一台机器上运行多个server时,注意要更改名称以方便区分
PNAME=php-swoft
# 是否同时启动 RPC
TCPABLE=true
# 是否同时启动 定时任务
CRONABLE=false
# 是否启用热重载。仅推荐在开发时启用
AUTO_RELOAD=true
AUTO_REGISTER=false

# HTTP 服务器设置
HTTP_HOST=0.0.0.0
HTTP_PORT=80
HTTP_MODE=SWOOLE_PROCESS
HTTP_TYPE=SWOOLE_SOCK_TCP

# WebSocket 服务器设置(它是继承自http服务器的,跟http有相同的配置项,更多请查看对应组件文档)
WS_ENABLE_HTTP=true

# TCP
TCP_HOST=0.0.0.0
TCP_PORT=8099
TCP_MODE=SWOOLE_PROCESS
TCP_TYPE=SWOOLE_SOCK_TCP
TCP_PACKAGE_MAX_LENGTH=2048
TCP_OPEN_EOF_CHECK=false

# Crontab
CRONTAB_TASK_COUNT=1024
CRONTAB_TASK_QUEUE=2048

# Swoole Settings
WORKER_NUM=1
MAX_REQUEST=100000
DAEMONIZE=0
DISPATCH_MODE=2
TASK_IPC_MODE=1
MESSAGE_QUEUE_KEY=1879052289
TASK_TMPDIR=/tmp/
LOG_FILE=@runtime/logs/swoole.log
TASK_WORKER_NUM=1
PACKAGE_MAX_LENGTH=2048
OPEN_HTTP2_PROTOCOL=false
SSL_CERT_FILE=/path/to/ssl_cert_file
SSL_KEY_FILE=/path/to/ssl_key_file

# Database Master nodes
DB_NAME=dbMaster
DB_URI=127.0.0.1:3306/test?user=root&password=123456&charset=utf8,127.0.0.1:3306/test?user=root&password=123456&charset=utf8
DB_MIN_ACTIVE=5
DB_MAX_ACTIVE=10
DB_MAX_WAIT=20
DB_MAX_WAIT_TIME=3
DB_MAX_IDLE_TIME=60
DB_TIMEOUT=2

# Database Slave nodes
DB_SLAVE_NAME=dbSlave
DB_SLAVE_URI=127.0.0.1:3306/test?user=root&password=123456&charset=utf8,127.0.0.1:3306/test?user=root&password=123456&charset=utf8
DB_SLAVE_MIN_ACTIVE=5
DB_SLAVE_MAX_ACTIVE=10
DB_SLAVE_MAX_WAIT=20
DB_SLAVE_MAX_WAIT_TIME=3
DB_SLAVE_MAX_IDLE_TIME=60
DB_SLAVE_TIMEOUT=3

# Redis
REDIS_NAME=redis
REDIS_DB=2
REDIS_URI=127.0.0.1:6379,127.0.0.1:6379
REDIS_MIN_ACTIVE=5
REDIS_MAX_ACTIVE=10
REDIS_MAX_WAIT=20
REDIS_MAX_WAIT_TIME=3
REDIS_MAX_IDLE_TIME=60
REDIS_TIMEOUT=3
REDIS_SERIALIZE=1

# other redis node
REDIS_DEMO_REDIS_DB=6
REDIS_DEMO_REDIS_PREFIX=demo_redis_

# User service (demo service)
USER_POOL_NAME=user
USER_POOL_URI=127.0.0.1:8099,127.0.0.1:8099
USER_POOL_MIN_ACTIVE=5
USER_POOL_MAX_ACTIVE=10
USER_POOL_MAX_WAIT=20
USER_POOL_TIMEOUT=200
USER_POOL_MAX_WAIT_TIME=3
USER_POOL_MAX_IDLE_TIME=60
USER_POOL_USE_PROVIDER=false
USER_POOL_BALANCER=random
USER_POOL_PROVIDER=consul

# User service breaker (demo service)
USER_BREAKER_FAIL_COUNT = 3
USER_BREAKER_SUCCESS_COUNT = 6
USER_BREAKER_DELAY_TIME = 5000

# Consul
CONSUL_ADDRESS=http://127.0.0.1
CONSUL_PORT=8500
CONSUL_REGISTER_NAME=user
CONSUL_REGISTER_ETO=false
CONSUL_REGISTER_SERVICE_ADDRESS=127.0.0.1
CONSUL_REGISTER_SERVICE_PORT=8099
CONSUL_REGISTER_CHECK_NAME=user
CONSUL_REGISTER_CHECK_TCP=127.0.0.1:8099
CONSUL_REGISTER_CHECK_INTERVAL=10
CONSUL_REGISTER_CHECK_TIMEOUT=1

应用结构简介

一个完整的swoft应用可以包含:

  • HTTP服务(跟传统的框架差不多)
  • WebSocket服务
  • Rpc服务

swoft-cloud/swoft 即是一个完整应用的demo。当然,如果你只想使用一部分功能也是可以的

框架核心

生命周期

框架生命周期

Swoft 的一切都是建立在 Swoole 扩展之上运行的,想要了解整个生命周期就必须要了解 Swoole 的生命周期,此部分可查阅 Swoole文档 获得更多的资料。

在 Swoft 服务启动阶段,我们主要关注 OnWorkerStart 事件,此事件会在 Worker 启动的时候触发,这个过程也是 Swoft 众多机制实现的关键,这时 Swoft

  • 扫描目录
  • 读取配置文件
  • 收集注解信息
  • 收集事件监听器

然后根据扫描到的注解信息执行对应的功能逻辑,并储存在与注解相对应的 Collector 容器内,包括但不限于注册路由信息,注册事件监听器,注册中间件,注册过滤器等等。

请求生命周期

每一个请求的开始到结束,都是由 Swoole 本身的 onRequest()onReceive() 事件监听并委托给 Dispatcher 来处理并响应的,而 Dispatcher 的主要职责是负责调度请求生命周期内的各个参与者(组件)。

HTTP Server

HTTP Server 的情况下,将由 ServerDispatcher来负责调度,参与者主要包括 RequestContext, ExceptionHandler, RequestHandler

  • RequestionContext(请求上下文): 作为当前的请求信息的容器将贯穿整个请求生命周期,负责信息的储存和传递;
  • ExceptionHandler(异常处理器): 则是在遇到异常的情况下出来收拾场面的,确保在各种异常情况下依旧能给客户端返回一个预期内的结果
  • RequestHandler(请求处理器): 则是整个请求生命周期的核心组件,其实也就是 Middleware(中间件) ,该组件实现了 PSR-15 协议
    • 负责将 Request -> Router -> Controller -> Action -> Renderer -> Response 这整个请求流程贯穿起来,其实也就是从 RequestResponse 的过程
    • 只要在任意一个环节返回一个有效的 Response 对象便能对该请求做出响应并返回。

RPC Server

RPC Server 的情况下,由 ServiceDispatcher 来负责调度,与 HTTP Server 类似,
区别在于参与者只有 RequestContextRequestHandler,而 RequestHandler 内的 Middleware 也和 HTTP Server 的类似

比较明显的区别在于 RPC Server 下会有一个 ServicePacker(数据打包器) 来负责将服务方法或异常返回的数据打包成一个统一的数据格式,并返回给客户端。

配置

swoft 的配置与其他框架稍微有一些区别,有几份:

  • config/server.php: swoft 服务器配置,主要是对swoole的配置
  • config/beans/*: swoft beans 配置,用于配置一些通过数组方式创建的bean,关于bean的创建方式请查看相关章节。
  • config/properties/*: 这里面就是通常的 应用配置 数据了
    • 只会加载里面的 app.php 文件,其他文件都是在 app.php 内部 include进来的
  • config/define.php:定义了一些通用的常量和路径别名
1
2
3
4
5
! defined('DS') && define('DS', DIRECTORY_SEPARATOR);
// App name
! defined('APP_NAME') && define('APP_NAME', 'swoft');
// Project base path
! defined('BASE_PATH') && define('BASE_PATH', dirname(__DIR__, 1));

配置的使用

  • 应用配置对象是 Swoft\Core\Config 的实例。

  • 获取配置对象;

    • \Swoft::getProperties() OR \Swoft\App::getProperties()
    • \Swoft::getBean('config') OR \Swoft\App::getBean('config')
  • 获取配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 通过全局函数使用
$value = \config('key', 'default value');
// 通过 "." 符号获取子级配置
$value = \config('section.subkey', 'default value');

// 通过获取config对象来使用
$config = \Swoft::getBean('config');

$value = $config->get('key', 'default value');
$value = $config->get('section.subkey', 'default value');

// 设置值
$config->set('my-name', 'tom');
echo $config->get('my-name'); // tom

路径别名

路径别名 主要是配置了一些常用的路径,给他们取了一些别名以方便使用。

已有别名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// config/define.php
// Register alias
$aliases = [
'@root' => BASE_PATH, // 项目所在路径
'@env' => '@root',
'@app' => '@root/app',
'@res' => '@root/resources',
'@runtime' => '@root/runtime',
'@configs' => '@root/config',
'@resources' => '@root/resources',
'@beans' => '@configs/beans',
'@properties' => '@configs/properties',
'@console' => '@beans/console.php',
'@commands' => '@app/command',
'@vendor' => '@root/vendor',
];

\Swoft\App::setAliases($aliases);

别名使用

1
2
3
4
5
6
7
// 通过全局函数使用
$path = \alias('@app');
$path = \alias('@app/Controllers');

// 通过顶级类 \Swoft 使用
$path = \Swoft::getAlias('@app');
$path = \Swoft::getAlias('@app/Controllers');
  • 获取全部的别名:
1
$aliaes = \Swoft::getAliases();

Bean容器

Swoft 中一个 Bean 就是一个类的一个对象实例。 容器就是一个巨大的工厂,用于存放和管理 Bean 生命周期。

注解

@Bean

命名空间:\Swoft\Bean\Annotation\Bean

  • name定义Bean别名,缺省默认类名
  • scope注入Bean类型,默认单例, Scope::SINGLETON/Scope::PROTOTYPE(每次创建)
  • ref指定引入Bean,用于定义在接口上面,指定使用哪个接口实现.

@Inject

命名空间:\Swoft\Bean\Annotation\Inject

  • name定义属性注入的bean名称,缺省属性自动类型名称

定义Bean

bean有两种方式定义,注解和数组配置

数组定义

1
2
3
4
5
6
7
8
9
10
// 配置创建
$beanConfig = [
'class' => MyBean::class,
'pro1' => 'v1',
'pro2' => 'v2',
[ // 构造函数参数
'arg1',
'${beanName}'
]
];
  • 数组中必须要有class字段定义
  • pro1/pro1和类面的成员变量名称是一一对应的
  • 属性值和构造函数参数值,都可以通过 ${xxx}${config.xx}, 注入Bean和引用properties配置信息

注解定义

注解定义使用PHP文档注解,在类上做一些标记,通过解析类注解,实现不同的功能。

1
2
3
4
5
6
7
/**
* @\Swoft\Bean\Annotation\Bean("userData")
*/
class XxxBean
{

}

操作Bean

1
2
3
4
App::getBean("name");
ApplicationContext::getBean('name');
BeanFactory::getBean('name');
BeanFactory::hasBean("name");
  • App/AppplicaionContext/BeanFactory都可从容器中得到Bean
  • hasBean某个bean是否存在

实例

别名定义

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
/**
* @\Swoft\Bean\Annotation\Bean("userData")
*/
class UserData
{
public function getData()
{
return [];
}
}

/**
* @\Swoft\Bean\Annotation\Bean()
*/
class UserLogic
{
/**
* @\Swoft\Bean\Annotation\Inject("userData")
* @var \UserData
*/
private $userData;

private function getUser()
{
return $this->userData->getData();
}
}

缺省定义

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
/**
* @\Swoft\Bean\Annotation\Bean("userData")
*/
class UserData
{
public function getData()
{
return [];
}
}

/**
* @\Swoft\Bean\Annotation\Bean()
*/
class UserLogic
{
/**
* @\Swoft\Bean\Annotation\Inject()
* @var \UserData
*/
private $userData;

private function getUser()
{
return $this->userData->getData();
}
}

接口引用

  • 接口上面指定了使用的实现bean别名
  • 接口使用处,无需指定使用那个别名,会根据接口上面的引用注入不同的实例bean
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
/**
* @\Swoft\Bean\Annotation\Bean(ref="boy")
*/
interface UserInterface
{
public function getData();
}

/**
* @\Swoft\Bean\Annotation\Bean("boy")
*/
class UserBoy implements \UserInterface
{
public function getData()
{
return 'boy';
}
}

/**
* @\Swoft\Bean\Annotation\Bean("girl")
*/
class UserGirl implements \UserInterface
{
public function getData()
{
return 'girl';
}
}

/**
* @\Swoft\Bean\Annotation\Bean()
*/
class UserLogic
{
/**
* @\Swoft\Bean\Annotation\Inject()
* @var \UserInterface
*/
private $userData;

private function getUser()
{
return $this->userData->getData();
}
}

组件

swoft 应用由一个个组件构成,核心为 swoft-framework 组件。

组件注册

内部组件

app/ 目录和 vendor/swoft 会被自动的扫描,收集注解信息。

自定义组件

在配置文件 config/properties/app.php 添加如下配置

1
2
3
4
5
6
'components' => [
'custom' => [
// Your package namespace.
'Package\\Namespace',
],
],

之后,swoft启动时就会到对应的包里去扫描,收集信息

事件管理

swoft我们将事件分为三大类:

  • swoole server 的回调事件
  • swoft server的事件,基于swoole的回调处理,扩展了一些可用事件以增强自定义性
  • 应用内的自定义事件管理和使用, 也是我们通常了解和使用的事件管理了

swooleserver级别的事件监听器,应当放置在boot阶段。(即通常应放置于 App\Boot 空间下)

Swoole Server事件

  • TAG: @SwooleListener("event name")

用注解tag @SwooleListener("event name") 来注册swoole的回调事件监听, 支持所有swoole官网列出来的事件回调名
具体请查看 SwooleEvent::class 以及 swoole 官网。

请谨慎使用 @SwooleListener。 它是直接注册到 swoole server上的(监听相同事件将会被覆盖),操作不当可能导致出现问题。

Swoft Server 事件

  • TAG: @ServerListener("event name")

用注解tag @ServerListener("event name") 来注册服务器级别的事件监听。

它是对 @SwooleListener 的补充扩展,除了支持 swoole 的事件以外,还增加了一些额外的可用事件监听。

两者的区别是:

  • SwooleListener 中一个事件的监听器只允许一个,并且是直接注册到 swoole server 上的(监听相同事件将会被覆盖)
  • ServerListener 允许对swoole事件添加多个监听器, 会逐个通知
  • ServerListener 不影响基础swoole事件的监听

Examples

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

namespace App\Boot\Listener;

use Swoft\Bootstrap\Listeners\Interfaces\StartInterface;
use Swoft\Bean\Annotation\ServerListener;
use Swoft\Bootstrap\SwooleEvent;
use Swoole\Server;

/**
* Class MyServerListener
* @package App\Boot\Listener
* @ServerListener(event=SwooleEvent::ON_START)
*/
class MyServerListener implements StartInterface
{
public function onStart(Server $server)
{
\output()->writeln('TestStartListener');
var_dump('TestStartListener');
}
}

自定义事件

基本的事件注册与触发管理

  • implement the Psr 14 - Event dispatcher
  • 支持设置事件优先级
  • 支持快速的事件组注册
  • 支持通配符事件的监听

作为核心服务组件,事件管理会自动启用

1
2
3
'eventManager'    => [
'class' => \Swoft\Event\EventManager::class,
],

注册事件监听

用注解tag @Listener("event name") 来注册用户自定义的事件监听

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 应用加载事件
*
* @Listener(AppEvent::APPLICATION_LOADER)
*/
class ApplicationLoaderListener implements EventHandlerInterface
{
/**
* @param EventInterface $event 事件对象
*/
public function handle(EventInterface $event)
{
// do something ....
}
}

事件名称管理推荐放置在一个单独类的常量里面,方便管理和维护

  • 触发事件
1
2
\Swoft::trigger('event name', null, $arg0, $arg1);
// OR use \Swoft\App::tigger();

连接池

基础属性

  • name连接池节点名称,用于服务发现
  • uri连接地址信息
  • maxActive最大活跃连接
  • maxWait最大等待连接
  • minActive最小活跃链接数
  • maxIdleTime连接最大空闲时间,单位秒
  • maxWaitTime连接最大等待时间,单位秒
  • timeout超时时间,单位秒

具体使用

AOP切面编程

AOP 为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。

AOP用途

利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高开发的效率

  • 日志记录
  • 性能统计
  • 安全控制
  • 事务处理
  • 异常处理

组成部分

AOP 由 切面通知连接点切入点,四部分组成。

切面 (Aspect)

其实就是共有功能的实现。如日志切面、权限切面、事务切面等。在实际应用中通常是一个存放共有功能实现的普通PHP类(切面类),之所以能被AOP容器识别成切面,是在配置中指定的。

通知 (Advice)

是切面的具体实现。以 目标方法 (要被代理的方法)为参照点,根据放置的地方不同,
可分为前置通知(Before)后置通知(AfterReturning)异常通知(AfterThrowing)最终通知(After)环绕通知(Around)5种。
在实际应用中通常是指向切面类中的一个方法,具体属于哪类通知,同样是在配置中指定的。

连接点 (Joinpoint)

就是程序在运行过程中能够插入切面的地点。例如,方法调用、异常抛出或字段修改等,但Swoft只支持方法级的连接点。

切入点 (Pointcut)

用于定义通知应该切入到哪些连接点上。不同的通知通常需要切入到不同的连接点上,这种精准的匹配是由切入点的正则表达式来定义的。

注解Tag

定义切面类

@Aspect()

定义一个类为切面类

@PointBean()

定义bean切入点 - 这个bean类里的方法执行都会经过此切面类的代理

  • include 定义需要切入的实体名称集合
  • exclude 定义需要排除的实体名称集合
@PointAnnotation()

定义注解切入点 - 所有包含使用了对应注解的方法都会经过此切面类的代理

  • include定义需要切入的注解名称集合
  • exclude定义需要排除的注解集合
@PointExecution()

定义匹配切入点 - 指明要代理目标类的哪些方法

  • include 定义需要切入的匹配集合, 匹配的类方法,支持正则表达式
  • exclude 定义需要排序的匹配集合, 匹配的类方法,支持正则表达式

定义通知点

上面的几个tag,标明了切面类的作用范围。下面的几个tag则是进一步限制(或者说标记)要在那些点上进行切入

  • @Before() 标记方法为前置通知 - 在目标方法执行前先执行此方法
  • @After() 标记方法为后置通知 - 在目标方法执行后执行此方法
  • @AfterReturning() 标记方法为最终返回通知
  • @AfterThrowing() 标记方法为异常通知 - 在目标方法执行抛出异常时执行此方法
  • @Around() 标记方法为环绕通知 - 在目标方法执行前、后都执行此方法

如上所述,@Before() @After() 就跟我们平常使用的 beforeAction afterAction 功能类似。 :)

说明

  • 定义切面后,框架会自动在对应的切入点,查询方法
  • @PointBean@PointAnnotation@PointExecution 三种定义的关系是并集,三种里面定义的排除也是并集后在排除

提示: 为了便于理解和使用,一个切面类尽量只使用上面三个中的一个。

使用实例
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
/**
* the test of aspcet
*
* @Aspect()
*
* @PointBean(
* include={AopBean::class},
* )
* @PointAnnotation(
* include={
* Cacheable::class,
* CachePut::class
* }
* )
* @PointExecution(
* include={
* "Swoft\Testing\Aop\RegBean::reg.*",
* }
* )
*/
class AllPointAspect
{
/**
* @Before()
*/
public function before()
{
var_dump(' before1 ');
}

/**
* @After()
*/
public function after()
{
var_dump(' after1 ');
}

/**
* @AfterReturning()
*/
public function afterReturn(JoinPoint $joinPoint)
{
$result = $joinPoint->getReturn();
return $result.' afterReturn1 ';
}

/**
* @Around()
* @param ProceedingJoinPoint $proceedingJoinPoint
* @return mixed
*/
public function around(ProceedingJoinPoint $proceedingJoinPoint)
{
$this->test .= ' around-before1 ';
$result = $proceedingJoinPoint->proceed();
$this->test .= ' around-after1 ';
return $result.$this->test;
}

/**
* @AfterThrowing()
*/
public function afterThrowing()
{
echo "aop=1 afterThrowing !\n";
}
}

AOP实现原理

AOP 框架底层是通过动态代理模式实现。代理是一种常用的设计模式,其目的就是为其他对象提供一个代理以控制对某个对象的访问。代理类负责为委托类预处理消息,过滤消息并转发消息,以及进行消息被委托类执行后的后续处理。

  • 静态代理: 由程序员创建代理类或特定工具自动生成源代码再对其编译
  • 动态代理: 在程序运行时运用反射机制动态创建而成。

动态代理

Swoft 动态代理定义,继承Swoft\Proxy\Handler\HandlerInterface接口,实现invoke方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class TestHandler implements HandlerInterface
{
/**
* @var object
*/
private $target;

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

public function invoke($method, $parameters)
{
$before = 'before';
$result = $this->target->$method(...$parameters);
$after = 'after';
$result .= $before.$after;
return $result;
}
}

使用

1
2
3
4
5
$object  = new ProxyTest(1, 2);
$handler = new TestHandler($object);

/* @var ProxyTest $proxy 这个代理类具有ProxyTest所有功能,可以直接当ProxyTest实例使用,没有任何区别*/
$proxy = Proxy::newProxyInstance(ProxyTest::class, $handler);

异常处理

1. 编写的代码

首先我们在 app 目录下创建一个 Exception 目录,建立一个异常捕获的处理控制器 SwoftExceptionHandler

用到的注解:
@ExceptionHandler() class注解,声明当前类是异常处理类 @Handler() method注解,需要捕获的异常类; 例如Exception::class为php自带或者自己定义的异常处理类

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
/**
* @ExceptionHandler()
* @package App\Exception
*/
class SwoftExceptionHandler
{
/**
* @Handler(Exception::class)
*
* @param Response $response
* @param \Throwable $throwable
*
* @return Response
*/
public function handlerException(Response $response, \Throwable $throwable)
{
$file = $throwable->getFile();
$line = $throwable->getLine();
$code = $throwable->getCode();
$exception = $throwable->getMessage();

$data = ['msg' => $exception, 'file' => $file, 'line' => $line, 'code' => $code];
App::error(json_encode($data));
return $response->json($data);
}
}

2.配置能被Swoft扫描到

config/properties/app.php中 的beanScan中增加App\Exception, 配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
return [
'version' => '1.0',
'autoInitBean' => true,
'bootScan' => [
'App\Commands',
],
'beanScan' => [
'App\Controllers',
'App\Models',
'App\Middlewares',
'App\Exception',
],
'env' => 'Base',
'db' => require __DIR__ . DS . 'db.php',
'cache' => require __DIR__ . DS . 'cache.php',
];

修改完成后,重新启动swoft即可。

组件列表

认证管理

权限控制

现在支持:

  • BasicAuth
  • BearerToken(JWT)
  • ACL

安装

1
composer require swoft/auth

使用

本组件目前实现了 BasicAuthBearerToken 的验证,以及简单的 ACL,使用方法简单,
config/beans/base.php 中的 serverDispatcher.middlewares 里 添加 \Swoft\Auth\Middleware\AuthMiddleware::class 中间件,如下

1
2
3
4
5
'serverDispatcher' => [
'middlewares' => [
\Swoft\Auth\Middleware\AuthMiddleware::class,
]
],

然后在配置文件 config/properties/app.php 中添加

1
2
3
4
5
6
'auth' => [
'jwt' => [
'algorithm' => 'HS256',
'secret' => '1231231'
],
],
  • 注意 secret 不要使用上述值,修改为你自己的值

配置验证管理 AuthManagerInterface

AuthManager 是登录验证的核心,本类实现了 Token 的验证及缓存,你可以继承这个类实现多种方式登录(配合accountType实现),下面就是一个 basicAuth 的 Demo

首先实现一个 Swoft\Auth\Mapping\AccountTypeInterface 作为我们登录的通道

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
use Swoft\Auth\Mapping\AccountTypeInterface;
use Swoft\Auth\Bean\AuthResult;

/**
* @Bean()
*/
class AdminNormalAccount implements AccountTypeInterface
{
/**
* @Inject()
* @var AdminUserDAO
*/
protected $dao;

const ROLE = 'role';

/**
* @throws \Swoft\Db\Exception\DbException
*/
public function login(array $data) : AuthResult
{
$identity = $data['identity'];
$credential = $data['credential'];
$user = $this->dao::findOneByUsername($identity);
$result = new AuthResult();
if($user instanceof AdminUserBean && $user->verify($credential)){
$result->setExtendedData([self::ROLE => $user->getIsAdministrator()]);
$result->setIdentity($user->getId());
}
return $result;
}

/**
* @throws \Swoft\Db\Exception\DbException
*/
public function authenticate(string $identity) : bool
{
return $this->dao::issetUserById($identity);
}
}

然后在我们自己的 AuthManagerService 实现这个登录

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
se Swoft\Auth\AuthManager;
use Swoft\Auth\Mapping\AuthManagerInterface;
use Swoft\Auth\Bean\AuthSession;

/**
* @Bean()
*/
class AuthManagerService extends AuthManager implements AuthManagerInterface
{
/**
* @var string
*/
protected $cacheClass = Redis::class;

/**
* @var bool 开启缓存
*/
protected $cacheEnable = true;

public function adminBasicLogin(string $identity, string $credential) : AuthSession
{
return $this->login(AdminNormalAccount::class, [
'identity' => $identity,
'credential' => $credential
]);
}
}

然后在 config/beans/base.php 中把系统默认的 AuthManager Bean 替换为我们自己的 AuthManagerService,添加如下代码进行替换

1
2
3
\Swoft\Auth\Mapping\AuthManagerInterface::class => [
'class'=>App\Domain\User\Service\AuthManagerService::class
],

现在我们就可以在一个 Controller 中使用刚才实现的登录方式了

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
use Swoft\Auth\Constants\AuthConstants;
use Swoft\Http\Message\Server\Request;
use Swoft\Http\Server\Bean\Annotation\Controller;
use Swoft\Http\Server\Bean\Annotation\RequestMapping;
use Swoft\Http\Server\Bean\Annotation\RequestMethod;
use Swoft\Auth\Mapping\AuthManagerInterface;

/**
* @Controller(prefix="/oauth")
*/
class AuthorizationsResource
{
/**
* @RequestMapping(route="token", method={RequestMethod::POST})
*/
public function oauth(Request $request) : array
{
$identity = $request->getAttribute(AuthConstants::BASIC_USER_NAME) ?? '';
$credential = $request->getAttribute(AuthConstants::BASIC_PASSWORD) ?? '';
if(!$identity || !$credential){
return [
"code" => 400,
"message" => "Identity and Credential are required."
];
}
/** @var AuthManagerService $manager */
$manager = App::getBean(AuthManagerInterface::class);
/** @var AuthSession $session */
$session = $manager->adminBasicLogin($identity, $credential);
$data = [
'token' => $session->getToken(),
'expire' => $session->getExpirationTime()
];
return $data;
}
}

现在可以通过 Postman 或 其它请求方式 请求我们的登录接口了

// 待续…

命令行

HTTP服务

WebSocket服务

RPC服务

缓存

数据库

视图

Session会话

RPC客户端

HTTP客户端

任务

进程

国际化

日志

内存操作

服务治理

开发者工具

Powered by Hexo and Hexo-theme-hiker

Copyright © 2017 - 2023 Keep It Simple And Stupid All Rights Reserved.

访客数 : | 访问量 :