前言
公司有个项目需要用到 微服务于是决定用 hyperf
框架(基于RPC多路复用),来解决微服务通信问题。
基础平台-用户服务
安装rpc多路复用
1 2 3 4
| # composer require hyperf/rpc-multiplex -W # composer require hyperf/rpc-log-listener
|
添加监听器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <?php
declare(strict_types=1);
use Hyperf\Command\Listener\FailToHandleListener; use Hyperf\ExceptionHandler\Listener\ErrorExceptionHandler;
return [ ErrorExceptionHandler::class, FailToHandleListener::class, Hyperf\RPCLogListener\RPCEventListener::class, ];
|
下面这个写法是为了让 composer
使用指定的 php
版本
which composer
只会指定到 PATH
最左边的的路径, 如果想用最新的需要调整 PATH
位置(请注意)
安装common(私用)
1 2
| # php81 $(which composer) require wlfpanda1012/plt-common
|
安装常用组件
1 2 3 4 5 6 7 8
| # composer require limingxinleo/hyperf-utiles -W # composer require hyperf/validation -W # composer require hyperf/swagger -W # composer require limingxinleo/i-encryption -W
|
安装微信组件
1 2 3 4
| # composer require limingxinleo/easywechat-classmap -W # composer require w7corp/easywechat -W
|
腾讯软件源
composer 镜像使用帮助
切换镜像指向
1
| composer config repo.packagist composer https://mirrors.tencent.com/composer/
|
composer 安装教程
1 2 3 4 5 6 7 8
| # php -r "copy('https://install.phpcomposer.com/installer', 'composer-setup.php');" # php -r "if (hash_file('sha384', 'composer-setup.php') === 'e0012edf3e80b6978849f5eff0d4b4e4c79ff1609dd1e613307e16318854d24ae64f26d17af3ef0bf7cfb710ca74755a') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" # php composer-setup.php # php -r "unlink('composer-setup.php');"
|
创建RPC服务
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
| <?php
declare(strict_types=1);
namespace App\RPC;
use Hyperf\RpcMultiplex\Constant; use Hyperf\RpcServer\Annotation\RpcService; use Wlfpanda1012\PltCommon\RPC\User\UserInterface;
#[RpcService(name: UserInterface::NAME, server: 'rpc', protocol: Constant::PROTOCOL_DEFAULT)] class UserService implements UserInterface { public function ping(): bool { return true; } }
|
安装数据库
1 2 3 4 5 6 7
| ```php # php81 bin/hyperf.php gen:migration create_some_table # php81 bin/hyperf.php migrate # php81 bin/hyperf.php gen:model
|
user id 非自增
1 2
| # php81 $(which composer) require "hyperf/snowflake:3.1.*" -W
|
微信小程序登录(可扩展)
配置文件 config/autoload/wechat.php
(需修改)
因为可能会需要对应多个appid所以在配置文件中 appid作为key,方便扩展。
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
declare(strict_types=1);
use function Hyperf\Support\env;
return [ env('WECHAT_APP_ID') => [
'app_id' => env('WECHAT_APP_ID'), 'secret' => env('WECHAT_APP_SECRET'), 'token' => 'your-token', 'aes_key' => '', 'use_stable_access_token' => false, 'http' => [ 'throw' => true, 'timeout' => 5.0,
'retry' => true, ], ], ];
|
WechatService
将上一个项目的WechatService.php
复制到当前项目的 app/Service
目录下
将 wechat 配置文件 注入进来。
再写一个 init
方法进行 configs 的初始化 因为使用了代理模式所以不会出现重复初始化的问题。
为了初始化,我们需要写一个监听类
1
| php bin/hyperf.php gen:listener MainCoroutineServerStartListener
|
通过阅读easywechat
源码
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
| <?php
declare(strict_types=1);
namespace EasyWeChat\Kernel\Traits;
use EasyWeChat\Kernel\Config; use EasyWeChat\Kernel\Contracts\Config as ConfigInterface; use EasyWeChat\Kernel\Exceptions\InvalidArgumentException;
use function is_array;
trait InteractWithConfig { protected ConfigInterface $config;
public function __construct(array|ConfigInterface $config) { $this->config = is_array($config) ? new Config($config) : $config; }
public function getConfig(): ConfigInterface { return $this->config; }
public function setConfig(ConfigInterface $config): static { $this->config = $config;
return $this; } }
|
在 config
的构造函数中并没有任何IO,所以不会出现非协程化的IO,使用协程导致的一系列问题。
所以我们可以在 MainCoroutineServerStartListener
中进行初始化
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
| <?php
declare(strict_types=1);
namespace App\Listener;
use App\Service\SubService\WeChatService; use Hyperf\Event\Annotation\Listener; use Hyperf\Framework\Event\BootApplication; use Psr\Container\ContainerInterface; use Hyperf\Event\Contract\ListenerInterface;
#[Listener] class MainCoroutineServerStartListener implements ListenerInterface { public function __construct(protected ContainerInterface $container) { }
public function listen(): array { return [ BootApplication::class ]; }
public function process(object $event): void { di()->get(WeChatService::class)->init(); } }
|
最终实现的 WeChatService
如下
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
| <?php
declare(strict_types=1);
namespace App\Service\SubService;
use App\Constants\ErrorCode; use App\Exception\BusinessException; use EasyWeChat\Kernel\Exceptions\HttpException; use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; use EasyWeChat\MiniApp\Application; use Han\Utils\Service; use Hyperf\Config\Annotation\Value; use JetBrains\PhpStorm\ArrayShape; use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface; use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface; use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface; use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
class WeChatService extends Service { #[Value(key: 'wechat')] protected array $configs;
protected array $apps = [];
public function init(): void { foreach ($this->configs as $key => $config) { $this->apps[$key] = new Application($config); } }
public function get(string $appid): Application { if (! isset($this->apps[$appid])) { throw new BusinessException(ErrorCode::WECHAT_MINI_APP_ID_NOT_EXIST); } return $this->apps[$appid]; }
#[ArrayShape(['openid' => 'string', 'session_key' => 'string'])] public function login(string $code, string $appid): array { $utils = $this->get($appid)->getUtils(); return $utils->codeToSession($code); } }
|
实现common中的 UserInterface
接口
目前只实现了 firstByCode
方法 目的只是实现微服务的调用
通过 code
获取到 openid
再通过 openid
获取到用户信息
如果用户不存在则创建用户
返回用户id
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
| <?php
declare(strict_types=1);
namespace App\RPC;
use App\Constants\ErrorCode; use App\Exception\BusinessException; use App\Service\Dao\UserDao; use App\Service\SubService\WeChatService; use EasyWeChat\Kernel\Exceptions\HttpException; use Hyperf\RpcMultiplex\Constant; use Hyperf\RpcServer\Annotation\RpcService; use JetBrains\PhpStorm\ArrayShape; use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface; use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface; use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface; use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; use Wlfpanda1012\PltCommon\Constant\OAuthType; use Wlfpanda1012\PltCommon\RPC\User\UserInterface;
#[RpcService(name: UserInterface::NAME, server: 'rpc', protocol: Constant::PROTOCOL_DEFAULT)] class UserService implements UserInterface { public function ping(): bool { return true; }
#[ArrayShape(['id' => 'int'])] public function firstByCode(string $code, string $appId, int|OAuthType $type = OAuthType::WECHAT_MINI_APP): array { if (is_int($type)) { $type = OAuthType::from($type); } $res = di()->get(WeChatService::class)->login($code, $appId); if (! isset($res['openid'])) { throw new BusinessException(ErrorCode::WECHAT_MINI_CODE_INVALID); } $user = di()->get(UserDao::class)->firstOrCreate($res['openid'], $appId, $type); return [ 'id' => $user->id, ]; } }
|
用户登录测试用例
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
| <?php
declare(strict_types=1);
namespace HyperfTest\Cases;
use App\RPC\UserService; use HyperfTest\HttpTestCase;
class UserTest extends HttpTestCase { public function testFirstByCode() { $user = di()->get(UserService::class)->firstByCode('123', 'xxx', 0); $this->assertTrue(isset($user['id'])); } }
|
因为实际上我们测试的时候并不会用到 code
所以我们需要对 WeChatService
进行mock
具体实现写在了 HttpTestCase
中
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
| <?php
declare(strict_types=1);
namespace HyperfTest;
use App\Service\SubService\WeChatService; use Hyperf\Testing; use Mockery; use PHPUnit\Framework\TestCase;
use function Hyperf\Support\make;
abstract class HttpTestCase extends TestCase {
protected $client;
protected static bool $init = false;
public function __construct(string $name) { parent::__construct($name); $this->client = make(Testing\Client::class); }
public function __call($name, $arguments) { return $this->client->{$name}(...$arguments); }
protected function setUp(): void { if (! static::$init) { static::$init = true; di()->set(WeChatService::class, $wx = Mockery::mock(WeChatService::class . '[login]', [di()])); $wx->shouldReceive('login')->withAnyArgs()->andReturn([ 'openid' => 'ce123', 'session_key' => 'ce456', ]); } }
protected function tearDown(): void { Mockery::close(); } }
|
当调用 WeChatService
的 login
方法时,会返回一个固定的 openid
和 session_key
修改 wechat-content-api
因为我们的 common
项目中已经定义了 UserInterface
接口
并且在 plt-user
实现了该接口
我们 wechat-content-api
对应的用户接口都由 plt-user
提供和实现
所以我们需要修改 wechat-content-api
项目
安装 common
1 2
| # php81 $(which composer) require wlfpanda1012/plt-common
|
修改 composer.json
1 2 3 4 5 6 7 8
| { "repositories": { "common": { "type": "path", "url": "../plt-common" } } }
|
安装 rpc-log 监听器
1 2
| # composer require hyperf/rpc-log-listener
|
配置 USER-RPC 服务器地址(生产环境一般用k8s,host用的是容器名)
1
| RPC_PLT_USER=127.0.0.1:9502
|
LoginService
由于 wechat-content-api
是rpc的消费方,所以我们需要在 LoginService
中注入 UserInterface
服务
并调用 firstByCode
方法
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
| <?php
declare(strict_types=1);
namespace App\Service;
use App\Schema\LoginSchema; use App\Service\Dao\UserDao; use App\Service\SubService\UserAuth; use Han\Utils\Service; use Hyperf\Di\Annotation\Inject; use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; use Wlfpanda1012\PltCommon\Constant\OAuthType; use Wlfpanda1012\PltCommon\RPC\User\UserInterface;
use function Hyperf\Support\env;
class LoginService extends Service { #[Inject] protected UserDao $userDao; #[Inject] protected UserInterface $userRpc;
public function login(string $code): LoginSchema { $result = $this->userRpc->firstByCode($code, env('WECHAT_APP_ID'), OAuthType::WECHAT_MINI_APP);
$user = $this->userDao->firstOrCreate($result['id']); $userAuth = UserAuth::instance()->init($user);
return new LoginSchema($userAuth->getToken()); } }
|