├── .env.example ├── .github └── workflows │ ├── build.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .gitlab-ci.yml ├── .php_cs ├── .phpstorm.meta.php ├── Dockerfile ├── README.md ├── app ├── Chat │ ├── Constants.php │ ├── Handler │ │ ├── ErrorMessageHandler.php │ │ ├── SendMessageHandler.php │ │ └── UserListHandler.php │ ├── HandlerInterface.php │ ├── InitNodeListener.php │ └── Node.php ├── Command │ ├── DebugCommand.php │ └── UserRegistCommand.php ├── Constants │ └── ErrorCode.php ├── Controller │ ├── Controller.php │ └── IndexController.php ├── Exception │ ├── BusinessException.php │ └── Handler │ │ └── BusinessExceptionHandler.php ├── Kernel │ ├── ClassMap │ │ ├── Coroutine.php │ │ └── ResolverDispatcher.php │ ├── Context │ │ └── Coroutine.php │ ├── Functions.php │ ├── Http │ │ ├── Response.php │ │ └── WorkerStartListener.php │ └── Log │ │ ├── AppendRequestIdProcessor.php │ │ └── LoggerFactory.php ├── Listener │ ├── AutoDeleteChannelListener.php │ ├── DbQueryExecutedListener.php │ └── QueueHandleListener.php ├── Model │ ├── Model.php │ └── User.php ├── Nsq │ └── Consumer │ │ └── SendMessageConsumer.php └── Service │ ├── Dao │ └── UserDao.php │ ├── Formatter │ └── UserFormatter.php │ ├── Obj │ └── UserObj.php │ ├── Redis │ └── UserCollection.php │ ├── Service.php │ └── UserService.php ├── bin └── hyperf.php ├── chat.sql ├── composer.json ├── config ├── autoload │ ├── annotations.php │ ├── aspects.php │ ├── async_queue.php │ ├── cache.php │ ├── commands.php │ ├── databases.php │ ├── dependencies.php │ ├── devtool.php │ ├── exceptions.php │ ├── listeners.php │ ├── logger.php │ ├── middlewares.php │ ├── nsq.php │ ├── processes.php │ ├── redis.php │ └── server.php ├── config.php ├── container.php └── routes.php ├── deploy.test.yml ├── phpstan.neon ├── phpunit.xml └── test ├── Cases └── ExampleTest.php ├── HttpTestCase.php └── bootstrap.php /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME=chat-api 2 | 3 | # Mysql 4 | DB_DRIVER=mysql 5 | DB_HOST=127.0.0.1 6 | DB_PORT=3306 7 | DB_DATABASE=hyperf 8 | DB_USERNAME=root 9 | DB_PASSWORD= 10 | DB_CHARSET=utf8mb4 11 | DB_COLLATION=utf8mb4_unicode_ci 12 | DB_PREFIX= 13 | 14 | # Redis 15 | REDIS_HOST=127.0.0.1 16 | REDIS_AUTH=(null) 17 | REDIS_PORT=6379 18 | REDIS_DB=0 19 | 20 | # Nsq 21 | NSQ_HOST=127.0.0.1 22 | NSQ_PORT=4150 23 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Docker 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout code 10 | uses: actions/checkout@v2 11 | - name: Build 12 | run: docker build -t biz-skeleton . 13 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | # Sequence of patterns matched against refs/tags 4 | tags: 5 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 6 | 7 | name: Release 8 | 9 | jobs: 10 | release: 11 | name: Release 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v2 16 | - name: Create Release 17 | id: create_release 18 | uses: actions/create-release@v1 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | with: 22 | tag_name: ${{ github.ref }} 23 | release_name: Release ${{ github.ref }} 24 | draft: false 25 | prerelease: false 26 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: PHPUnit for Hyperf 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | SW_VERSION: '4.5.6' 7 | 8 | jobs: 9 | ci: 10 | name: Test on PHP ${{ matrix.php-version }} 11 | runs-on: '${{ matrix.os }}' 12 | strategy: 13 | matrix: 14 | os: [ubuntu-latest] 15 | php-version: ['7.2', '7.3', '7.4'] 16 | max-parallel: 3 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v2 20 | - name: Setup PHP 21 | uses: shivammathur/setup-php@v2 22 | with: 23 | php-version: ${{ matrix.php-version }} 24 | tools: phpize 25 | ini-values: extension=swoole, opcache.enable_cli=1, swoole.use_shortname='Off' 26 | coverage: none 27 | - name: Setup Swoole 28 | run: | 29 | wget https://github.com/swoole/swoole-src/archive/v${SW_VERSION}.tar.gz -O swoole.tar.gz 30 | mkdir -p swoole 31 | tar -xf swoole.tar.gz -C swoole --strip-components=1 32 | rm swoole.tar.gz 33 | cd swoole 34 | phpize 35 | ./configure --enable-openssl --enable-mysqlnd --enable-http2 36 | make -j$(nproc) 37 | sudo make install 38 | - name: Setup Packages 39 | run: composer update -o 40 | - name: Run Test Cases 41 | run: | 42 | composer analyse 43 | composer test 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .buildpath 2 | .settings/ 3 | .project 4 | *.patch 5 | .idea/ 6 | .git/ 7 | runtime/ 8 | vendor/ 9 | .phpintel/ 10 | .env 11 | .DS_Store 12 | *.lock 13 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | # usermod -aG docker gitlab-runner 2 | 3 | stages: 4 | - build 5 | - deploy 6 | 7 | variables: 8 | PROJECT_NAME: hyperf 9 | REGISTRY_URL: registry-docker.org 10 | 11 | build_test_docker: 12 | stage: build 13 | before_script: 14 | # - git submodule sync --recursive 15 | # - git submodule update --init --recursive 16 | script: 17 | - docker build . -t $PROJECT_NAME 18 | - docker tag $PROJECT_NAME $REGISTRY_URL/$PROJECT_NAME:test 19 | - docker push $REGISTRY_URL/$PROJECT_NAME:test 20 | only: 21 | - test 22 | tags: 23 | - builder 24 | 25 | deploy_test_docker: 26 | stage: deploy 27 | script: 28 | - docker stack deploy -c deploy.test.yml --with-registry-auth $PROJECT_NAME 29 | only: 30 | - test 31 | tags: 32 | - test 33 | 34 | build_docker: 35 | stage: build 36 | before_script: 37 | # - git submodule sync --recursive 38 | # - git submodule update --init --recursive 39 | script: 40 | - docker build . -t $PROJECT_NAME 41 | - docker tag $PROJECT_NAME $REGISTRY_URL/$PROJECT_NAME:$CI_COMMIT_REF_NAME 42 | - docker tag $PROJECT_NAME $REGISTRY_URL/$PROJECT_NAME:latest 43 | - docker push $REGISTRY_URL/$PROJECT_NAME:$CI_COMMIT_REF_NAME 44 | - docker push $REGISTRY_URL/$PROJECT_NAME:latest 45 | only: 46 | - tags 47 | tags: 48 | - builder 49 | 50 | deploy_docker: 51 | stage: deploy 52 | script: 53 | - echo SUCCESS 54 | only: 55 | - tags 56 | tags: 57 | - builder 58 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | setRiskyAllowed(true) 14 | ->setRules([ 15 | '@PSR2' => true, 16 | '@Symfony' => true, 17 | '@DoctrineAnnotation' => true, 18 | '@PhpCsFixer' => true, 19 | 'header_comment' => [ 20 | 'commentType' => 'PHPDoc', 21 | 'header' => $header, 22 | 'separate' => 'none', 23 | 'location' => 'after_declare_strict', 24 | ], 25 | 'array_syntax' => [ 26 | 'syntax' => 'short' 27 | ], 28 | 'list_syntax' => [ 29 | 'syntax' => 'short' 30 | ], 31 | 'concat_space' => [ 32 | 'spacing' => 'one' 33 | ], 34 | 'blank_line_before_statement' => [ 35 | 'statements' => [ 36 | 'declare', 37 | ], 38 | ], 39 | 'general_phpdoc_annotation_remove' => [ 40 | 'annotations' => [ 41 | 'author' 42 | ], 43 | ], 44 | 'ordered_imports' => [ 45 | 'imports_order' => [ 46 | 'class', 'function', 'const', 47 | ], 48 | 'sort_algorithm' => 'alpha', 49 | ], 50 | 'single_line_comment_style' => [ 51 | 'comment_types' => [ 52 | ], 53 | ], 54 | 'yoda_style' => [ 55 | 'always_move_variable' => false, 56 | 'equal' => false, 57 | 'identical' => false, 58 | ], 59 | 'phpdoc_align' => [ 60 | 'align' => 'left', 61 | ], 62 | 'multiline_whitespace_before_semicolons' => [ 63 | 'strategy' => 'no_multi_line', 64 | ], 65 | 'constant_case' => [ 66 | 'case' => 'lower', 67 | ], 68 | 'class_attributes_separation' => true, 69 | 'combine_consecutive_unsets' => true, 70 | 'declare_strict_types' => true, 71 | 'linebreak_after_opening_tag' => true, 72 | 'lowercase_static_reference' => true, 73 | 'no_useless_else' => true, 74 | 'no_unused_imports' => true, 75 | 'not_operator_with_successor_space' => true, 76 | 'not_operator_with_space' => false, 77 | 'ordered_class_elements' => true, 78 | 'php_unit_strict' => false, 79 | 'phpdoc_separation' => false, 80 | 'single_quote' => true, 81 | 'standardize_not_equals' => true, 82 | 'multiline_comment_opening_closing' => true, 83 | ]) 84 | ->setFinder( 85 | PhpCsFixer\Finder::create() 86 | ->exclude('public') 87 | ->exclude('runtime') 88 | ->exclude('vendor') 89 | ->in(__DIR__) 90 | ) 91 | ->setUsingCache(false); 92 | -------------------------------------------------------------------------------- /.phpstorm.meta.php: -------------------------------------------------------------------------------- 1 | " version="1.0" license="MIT" app.name="Hyperf" 10 | 11 | ## 12 | # ---------- env settings ---------- 13 | ## 14 | # --build-arg timezone=Asia/Shanghai 15 | ARG timezone 16 | 17 | ENV TIMEZONE=${timezone:-"Asia/Shanghai"} \ 18 | APP_ENV=prod \ 19 | SCAN_CACHEABLE=(true) 20 | 21 | # update 22 | RUN set -ex \ 23 | # show php version and extensions 24 | && php -v \ 25 | && php -m \ 26 | && php --ri swoole \ 27 | # ---------- some config ---------- 28 | && cd /etc/php7 \ 29 | # - config PHP 30 | && { \ 31 | echo "upload_max_filesize=128M"; \ 32 | echo "post_max_size=128M"; \ 33 | echo "memory_limit=1G"; \ 34 | echo "date.timezone=${TIMEZONE}"; \ 35 | } | tee conf.d/99_overrides.ini \ 36 | # - config timezone 37 | && ln -sf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime \ 38 | && echo "${TIMEZONE}" > /etc/timezone \ 39 | # ---------- clear works ---------- 40 | && rm -rf /var/cache/apk/* /tmp/* /usr/share/man \ 41 | && echo -e "\033[42;37m Build Completed :).\033[0m\n" 42 | 43 | WORKDIR /opt/www 44 | 45 | # Composer Cache 46 | # COPY ./composer.* /opt/www/ 47 | # RUN composer install --no-dev --no-scripts 48 | 49 | COPY . /opt/www 50 | RUN composer install --no-dev -o && php bin/hyperf.php 51 | 52 | EXPOSE 9501 53 | 54 | ENTRYPOINT ["php", "/opt/www/bin/hyperf.php", "start"] 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 介绍 2 | 3 | Hyperf 是基于 `Swoole 4.5+` 实现的高性能、高灵活性的 PHP 持久化框架,内置协程服务器及大量常用的组件,性能较传统基于 `PHP-FPM` 的框架有质的提升,提供超高性能的同时,也保持着极其灵活的可扩展性,标准组件均以最新的 [PSR 标准](https://www.php-fig.org/psr) 实现,基于强大的依赖注入设计可确保框架内的绝大部分组件或类都是可替换的。 4 | 5 | 框架组件库除了常见的协程版的 `MySQL 客户端`、`Redis 客户端`,还为您准备了协程版的 `Eloquent ORM`、`GRPC 服务端及客户端`、`Zipkin (OpenTracing) 客户端`、`Guzzle HTTP 客户端`、`Elasticsearch 客户端`、`Consul 客户端`、`ETCD 客户端`、`AMQP 组件`、`Apollo 配置中心`、`基于令牌桶算法的限流器`、`通用连接池` 等组件的提供也省去了自己去实现对应协程版本的麻烦,并提供了 `依赖注入`、`注解`、`AOP 面向切面编程`、`中间件`、`自定义进程`、`事件管理器`、`简易的 Redis 消息队列和全功能的 RabbitMQ 消息队列` 等非常便捷的功能,满足丰富的技术场景和业务场景,开箱即用。 6 | 7 | # 框架初衷 8 | 9 | 尽管现在基于 PHP 语言开发的框架处于一个百花争鸣的时代,但仍旧没能看到一个优雅的设计与超高性能的共存的完美框架,亦没有看到一个真正为 PHP 微服务铺路的框架,此为 Hyperf 及其团队成员的初衷,我们将持续投入并为此付出努力,也欢迎你加入我们参与开源建设。 10 | 11 | # 设计理念 12 | 13 | `Hyperspeed + Flexibility = Hyperf`,从名字上我们就将 `超高速` 和 `灵活性` 作为 Hyperf 的基因。 14 | 15 | - 对于超高速,我们基于 Swoole 协程并在框架设计上进行大量的优化以确保超高性能的输出。 16 | - 对于灵活性,我们基于 Hyperf 强大的依赖注入组件,组件均基于 [PSR 标准](https://www.php-fig.org/psr) 的契约和由 Hyperf 定义的契约实现,达到框架内的绝大部分的组件或类都是可替换的。 17 | 18 | 基于以上的特点,Hyperf 将存在丰富的可能性,如实现 Web 服务,网关服务,分布式中间件,微服务架构,游戏服务器,物联网(IOT)等。 19 | 20 | # 文档 21 | 22 | [https://doc.hyperf.io/](https://hyperf.wiki/) 23 | 24 | # 使用 25 | 26 | ## 连接 27 | 28 | ``` 29 | ws://127.0.0.1:9501/?token=xxx 30 | ``` 31 | 32 | ## 协议 33 | 34 | 查看所有用户列表 35 | 36 | ``` 37 | {"protocal":"user.list","data":"Hello World"} 38 | ``` 39 | 40 | 向对方用户发送消息 41 | 42 | ``` 43 | {"protocal":"send.message","data":{"id":2,"message":"Hello World."}} 44 | ``` 45 | -------------------------------------------------------------------------------- /app/Chat/Constants.php: -------------------------------------------------------------------------------- 1 | '错误信息', 22 | * 'close' => false, // 是否前置关闭客户端 23 | * ] 24 | */ 25 | public function handle(Server $server, int $fd, $data) 26 | { 27 | $server->push($fd, json_encode($data)); 28 | 29 | if ($data['close'] ?? false) { 30 | $server->close($fd); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/Chat/Handler/SendMessageHandler.php: -------------------------------------------------------------------------------- 1 | 'send.message', 59 | * 'data' => [ 60 | * 'id' => 1, // 目标ID 61 | * 'message' => 'Hello World.' 62 | * ] 63 | * ] 64 | */ 65 | public function handle(Server $server, int $fd, $data) 66 | { 67 | $id = $data['data']['id'] ?? 0; 68 | $message = $data['data']['message'] ?? null; 69 | 70 | if ($id && ! is_null($message)) { 71 | $user = $this->dao->first($id); 72 | if (empty($user)) { 73 | $this->errorHandler->handle($server, $fd, [ 74 | 'message' => '目标用户不存在', 75 | ]); 76 | return; 77 | } 78 | 79 | $this->nsq->publish(Constants::SEND_MESSAGE, Json::encode([ 80 | 'token' => $user->token, 81 | 'data' => $data, 82 | ])); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /app/Chat/Handler/UserListHandler.php: -------------------------------------------------------------------------------- 1 | 'user.list' 39 | * ] 40 | */ 41 | public function handle(Server $server, int $fd, $data) 42 | { 43 | // 查询所有在线的用户 44 | $users = $this->dao->findOnline(); 45 | $mine = $this->service->find($fd); 46 | 47 | $result = []; 48 | foreach ($users as $user) { 49 | $item = UserFormatter::instance()->base($user); 50 | if ($mine->token == $user->token) { 51 | $item['own'] = true; 52 | } 53 | 54 | $result[] = $item; 55 | } 56 | 57 | $data['list'] = $result; 58 | $server->push($fd, json_encode($data)); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/Chat/HandlerInterface.php: -------------------------------------------------------------------------------- 1 | get(Node::class); 33 | 34 | $node->setId(uniqid()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Chat/Node.php: -------------------------------------------------------------------------------- 1 | id; 29 | } 30 | 31 | public function setId($id) 32 | { 33 | $this->id = $id; 34 | return $this; 35 | } 36 | 37 | public function getChannel(): string 38 | { 39 | return Constants::SEND_MESSAGE . '.' . $this->getId(); 40 | } 41 | 42 | public function heartbeat() 43 | { 44 | $redis = di()->get(Redis::class); 45 | $redis->set($this->getChannel(), '1', 3600); 46 | } 47 | 48 | public function clear() 49 | { 50 | $channelApi = di()->get(Channel::class); 51 | $client = di()->get(Api::class); 52 | $redis = di()->get(Redis::class); 53 | $conotents = $client->stats('json', Constants::SEND_MESSAGE)->getBody()->getContents(); 54 | $res = Json::decode($conotents); 55 | foreach ($res['topics'] ?? [] as $topic) { 56 | if (($topic['topic_name'] ?? null) !== Constants::SEND_MESSAGE) { 57 | continue; 58 | } 59 | 60 | $channels = $topic['channels'] ?? []; 61 | foreach ($channels as $channel) { 62 | if ($name = $channel['channel_name'] ?? null) { 63 | if (! $redis->exists($name)) { 64 | $channelApi->delete(Constants::SEND_MESSAGE, $name); 65 | } 66 | } 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/Command/DebugCommand.php: -------------------------------------------------------------------------------- 1 | container = $container; 36 | 37 | parent::__construct('debug:send-message'); 38 | } 39 | 40 | public function configure() 41 | { 42 | $this->setDescription('Send Message to websocket client.'); 43 | $this->addArgument('token', InputArgument::REQUIRED, '目标 TOKEN'); 44 | } 45 | 46 | public function handle() 47 | { 48 | $token = $this->input->getArgument('token'); 49 | 50 | $data = [ 51 | 'protocal' => 'text', 52 | 'data' => 'Hello World', 53 | ]; 54 | 55 | di()->get(Nsq::class)->publish(Constants::SEND_MESSAGE, Json::encode([ 56 | 'token' => $token, 57 | 'data' => $data, 58 | ])); 59 | 60 | Timer::clearAll(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/Command/UserRegistCommand.php: -------------------------------------------------------------------------------- 1 | container = $container; 34 | 35 | parent::__construct('user:regist'); 36 | } 37 | 38 | public function configure() 39 | { 40 | $this->setDescription('注册用户信息'); 41 | $this->addArgument('token', InputArgument::REQUIRED, '用户TOKEN'); 42 | $this->addOption('name', 'N', InputOption::VALUE_REQUIRED, '用户名'); 43 | } 44 | 45 | public function handle() 46 | { 47 | $token = $this->input->getArgument('token'); 48 | $name = $this->input->getOption('name'); 49 | if (empty($name)) { 50 | $this->error('请输入用户名'); 51 | return; 52 | } 53 | 54 | $user = new User(); 55 | $user->token = $token; 56 | $user->name = $name; 57 | $user->save(); 58 | 59 | $this->info(sprintf('用户创建完毕,请使用 %s 登录系统', $token)); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/Constants/ErrorCode.php: -------------------------------------------------------------------------------- 1 | container = $container; 38 | $this->response = $container->get(Response::class); 39 | $this->request = $container->get(RequestInterface::class); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/Controller/IndexController.php: -------------------------------------------------------------------------------- 1 | service->find($fd)) { 53 | $this->service->delete($obj); 54 | 55 | if ($user = $this->dao->firstByToken($obj->token)) { 56 | $user->is_online = User::OFFLINE; 57 | $user->save(); 58 | } 59 | } 60 | } 61 | 62 | public function onMessage(WebSocket\Server $server, Frame $frame): void 63 | { 64 | $fd = $frame->fd; 65 | $data = json_decode($frame->data, true); 66 | 67 | $protocal = 'protocal.' . $data['protocal'] ?? ''; 68 | if (! $this->container->has($protocal)) { 69 | $this->errorMessageHandler->handle($server, $fd, [ 70 | 'message' => 'The Protocal is invalid.', 71 | ]); 72 | return; 73 | } 74 | 75 | /** @var HandlerInterface $handler */ 76 | $handler = $this->container->get($protocal); 77 | $handler->handle($server, $fd, $data); 78 | } 79 | 80 | public function onOpen(WebSocket\Server $server, Request $request): void 81 | { 82 | $token = $this->request->input('token'); 83 | 84 | $user = $this->dao->firstByToken($token); 85 | if (empty($user)) { 86 | $this->errorMessageHandler->handle($server, $request->fd, [ 87 | 'message' => 'The Token is invalid.', 88 | 'close' => true, 89 | ]); 90 | return; 91 | } 92 | 93 | $this->dao->online($token, $user); 94 | $node = di()->get(Node::class)->getId(); 95 | 96 | $this->service->save(new UserObj($token, $request->fd, $node)); 97 | 98 | $user = $this->dao->findOnline()->toArray(); 99 | 100 | $result = [ 101 | 'protocal' => 'user.list', 102 | 'list' => $user, 103 | ]; 104 | 105 | $server->push($request->fd, json_encode($result)); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /app/Exception/BusinessException.php: -------------------------------------------------------------------------------- 1 | container = $container; 44 | $this->response = $container->get(Response::class); 45 | $this->logger = $container->get(StdoutLoggerInterface::class); 46 | } 47 | 48 | public function handle(Throwable $throwable, ResponseInterface $response) 49 | { 50 | if ($throwable instanceof HttpException) { 51 | return $this->response->handleException($throwable); 52 | } 53 | 54 | if ($throwable instanceof BusinessException) { 55 | $this->logger->warning(format_throwable($throwable)); 56 | 57 | return $this->response->fail($throwable->getCode(), $throwable->getMessage()); 58 | } 59 | 60 | $this->logger->error(format_throwable($throwable)); 61 | 62 | return $this->response->fail(ErrorCode::SERVER_ERROR, 'Server Error'); 63 | } 64 | 65 | public function isValid(Throwable $throwable): bool 66 | { 67 | return true; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/Kernel/ClassMap/Coroutine.php: -------------------------------------------------------------------------------- 1 | get(Co::class)->create($callable); 67 | } 68 | 69 | public static function inCoroutine(): bool 70 | { 71 | return Coroutine::id() > 0; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/Kernel/ClassMap/ResolverDispatcher.php: -------------------------------------------------------------------------------- 1 | container = $container; 42 | } 43 | 44 | /** 45 | * Resolve a definition to a value. 46 | * 47 | * @param DefinitionInterface $definition object that defines how the value should be obtained 48 | * @param array $parameters optional parameters to use to build the entry 49 | * @throws InvalidDefinitionException if the definition cannot be resolved 50 | * @return mixed value obtained from the definition 51 | */ 52 | public function resolve(DefinitionInterface $definition, array $parameters = []) 53 | { 54 | if ($definition instanceof SelfResolvingDefinitionInterface) { 55 | return $definition->resolve($this->container); 56 | } 57 | 58 | $resolver = $this->getDefinitionResolver($definition); 59 | return $resolver->resolve($definition, $parameters); 60 | } 61 | 62 | /** 63 | * Check if a definition can be resolved. 64 | * 65 | * @param DefinitionInterface $definition object that defines how the value should be obtained 66 | * @param array $parameters optional parameters to use to build the entry 67 | */ 68 | public function isResolvable(DefinitionInterface $definition, array $parameters = []): bool 69 | { 70 | if ($definition instanceof SelfResolvingDefinitionInterface) { 71 | return $definition->isResolvable($this->container); 72 | } 73 | 74 | $resolver = $this->getDefinitionResolver($definition); 75 | return $resolver->isResolvable($definition, $parameters); 76 | } 77 | 78 | /** 79 | * Returns a resolver capable of handling the given definition. 80 | * 81 | * @throws RuntimeException no definition resolver was found for this type of definition 82 | */ 83 | private function getDefinitionResolver(DefinitionInterface $definition): ResolverInterface 84 | { 85 | switch (true) { 86 | case $definition instanceof ObjectDefinition: 87 | if (! $this->objectResolver) { 88 | $this->objectResolver = new ObjectResolver($this->container, $this); 89 | } 90 | return $this->objectResolver; 91 | case $definition instanceof FactoryDefinition: 92 | if (! $this->factoryResolver) { 93 | $this->factoryResolver = new FactoryResolver($this->container, $this); 94 | } 95 | return $this->factoryResolver; 96 | default: 97 | throw new RuntimeException('No definition resolver was configured for definition of type ' . get_class($definition)); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /app/Kernel/Context/Coroutine.php: -------------------------------------------------------------------------------- 1 | container = $container; 43 | $this->logger = $container->get(StdoutLoggerInterface::class); 44 | if ($container->has(FormatterInterface::class)) { 45 | $this->formatter = $container->get(FormatterInterface::class); 46 | } 47 | } 48 | 49 | /** 50 | * @return int Returns the coroutine ID of the coroutine just created. 51 | * Returns -1 when coroutine create failed. 52 | */ 53 | public function create(callable $callable): int 54 | { 55 | $id = Utils\Coroutine::id(); 56 | $result = SwooleCoroutine::create(function () use ($callable, $id) { 57 | try { 58 | // Shouldn't copy all contexts to avoid socket already been bound to another coroutine. 59 | Utils\Context::copy($id, [ 60 | AppendRequestIdProcessor::REQUEST_ID, 61 | ServerRequestInterface::class, 62 | ]); 63 | call($callable); 64 | } catch (Throwable $throwable) { 65 | if ($this->formatter) { 66 | $this->logger->warning($this->formatter->format($throwable)); 67 | } else { 68 | $this->logger->warning((string) $throwable); 69 | } 70 | } 71 | }); 72 | return is_int($result) ? $result : -1; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/Kernel/Functions.php: -------------------------------------------------------------------------------- 1 | get($id); 28 | } 29 | 30 | return $container; 31 | } 32 | } 33 | 34 | if (! function_exists('format_throwable')) { 35 | /** 36 | * Format a throwable to string. 37 | */ 38 | function format_throwable(Throwable $throwable): string 39 | { 40 | return di()->get(FormatterInterface::class)->format($throwable); 41 | } 42 | } 43 | 44 | if (! function_exists('queue_push')) { 45 | /** 46 | * Push a job to async queue. 47 | */ 48 | function queue_push(JobInterface $job, int $delay = 0, string $key = 'default'): bool 49 | { 50 | $driver = di()->get(DriverFactory::class)->get($key); 51 | return $driver->push($job, $delay); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/Kernel/Http/Response.php: -------------------------------------------------------------------------------- 1 | container = $container; 37 | $this->response = $container->get(ResponseInterface::class); 38 | } 39 | 40 | public function success($data = []): PsrResponseInterface 41 | { 42 | return $this->response->json([ 43 | 'code' => 0, 44 | 'data' => $data, 45 | ]); 46 | } 47 | 48 | public function fail($code, $message = ''): PsrResponseInterface 49 | { 50 | return $this->response->json([ 51 | 'code' => $code, 52 | 'message' => $message, 53 | ]); 54 | } 55 | 56 | public function redirect($url, $status = 302): PsrResponseInterface 57 | { 58 | return $this->response() 59 | ->withAddedHeader('Location', (string) $url) 60 | ->withStatus($status); 61 | } 62 | 63 | public function cookie(Cookie $cookie) 64 | { 65 | $response = $this->response()->withCookie($cookie); 66 | Context::set(PsrResponseInterface::class, $response); 67 | return $this; 68 | } 69 | 70 | public function handleException(HttpException $throwable): PsrResponseInterface 71 | { 72 | return $this->response() 73 | ->withAddedHeader('Server', 'Hyperf') 74 | ->withStatus($throwable->getStatusCode()) 75 | ->withBody(new SwooleStream($throwable->getMessage())); 76 | } 77 | 78 | public function response(): PsrResponseInterface 79 | { 80 | return Context::get(PsrResponseInterface::class); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /app/Kernel/Http/WorkerStartListener.php: -------------------------------------------------------------------------------- 1 | get(StdoutLogger::class)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Kernel/Log/AppendRequestIdProcessor.php: -------------------------------------------------------------------------------- 1 | get(HyperfLoggerFactory::class)->make(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/Listener/AutoDeleteChannelListener.php: -------------------------------------------------------------------------------- 1 | container = $container; 33 | } 34 | 35 | public function listen(): array 36 | { 37 | return [ 38 | MainWorkerStart::class, 39 | ]; 40 | } 41 | 42 | public function process(object $event) 43 | { 44 | $node = $this->container->get(Node::class); 45 | while (true) { 46 | $node->heartbeat(); 47 | $node->clear(); 48 | sleep(10); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/Listener/DbQueryExecutedListener.php: -------------------------------------------------------------------------------- 1 | logger = $container->get(LoggerFactory::class)->get('sql'); 36 | } 37 | 38 | public function listen(): array 39 | { 40 | return [ 41 | QueryExecuted::class, 42 | ]; 43 | } 44 | 45 | /** 46 | * @param QueryExecuted $event 47 | */ 48 | public function process(object $event) 49 | { 50 | if ($event instanceof QueryExecuted) { 51 | $sql = $event->sql; 52 | if (! Arr::isAssoc($event->bindings)) { 53 | foreach ($event->bindings as $key => $value) { 54 | $sql = Str::replaceFirst('?', "'{$value}'", $sql); 55 | } 56 | } 57 | 58 | $this->logger->info(sprintf('[%s:%s] %s', $event->connectionName, $event->time, $sql)); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/Listener/QueueHandleListener.php: -------------------------------------------------------------------------------- 1 | logger = $container->get(LoggerFactory::class)->get('queue'); 35 | } 36 | 37 | public function listen(): array 38 | { 39 | return [ 40 | AfterHandle::class, 41 | BeforeHandle::class, 42 | FailedHandle::class, 43 | RetryHandle::class, 44 | ]; 45 | } 46 | 47 | public function process(object $event) 48 | { 49 | if ($event instanceof Event && $event->message->job()) { 50 | $job = $event->message->job(); 51 | $jobClass = get_class($job); 52 | if ($job instanceof AnnotationJob) { 53 | $jobClass = sprintf('Job[%s@%s]', $job->class, $job->method); 54 | } 55 | $date = date('Y-m-d H:i:s'); 56 | 57 | switch (true) { 58 | case $event instanceof BeforeHandle: 59 | $this->logger->info(sprintf('[%s] Processing %s.', $date, $jobClass)); 60 | break; 61 | case $event instanceof AfterHandle: 62 | $this->logger->info(sprintf('[%s] Processed %s.', $date, $jobClass)); 63 | break; 64 | case $event instanceof FailedHandle: 65 | $this->logger->error(sprintf('[%s] Failed %s.', $date, $jobClass)); 66 | $this->logger->error(format_throwable($event->getThrowable())); 67 | break; 68 | case $event instanceof RetryHandle: 69 | $this->logger->warning(sprintf('[%s] Retried %s.', $date, $jobClass)); 70 | break; 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/Model/Model.php: -------------------------------------------------------------------------------- 1 | 'integer', 'is_online' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime']; 48 | } 49 | -------------------------------------------------------------------------------- /app/Nsq/Consumer/SendMessageConsumer.php: -------------------------------------------------------------------------------- 1 | get(Node::class); 35 | 36 | $this->channel = $node->getChannel(); 37 | } 38 | 39 | public function consume(Message $payload): ?string 40 | { 41 | $data = Json::decode($payload->getBody()); 42 | 43 | $token = $data['token']; 44 | $data = $data['data']; 45 | 46 | $sender = $this->container->get(Sender::class); 47 | 48 | $obj = $this->container->get(UserCollection::class)->find($token); 49 | $node = $this->container->get(Node::class)->getId(); 50 | if ($obj && $obj->node === $node) { 51 | $sender->push($obj->fd, json_encode($data)); 52 | } 53 | 54 | return Result::ACK; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/Service/Dao/UserDao.php: -------------------------------------------------------------------------------- 1 | where('token', $token)->first(); 34 | } 35 | 36 | /** 37 | * 返回所有在线的用户. 38 | * @return User[] 39 | */ 40 | public function findOnline() 41 | { 42 | return User::query()->where('is_online', User::ONLINE)->get(); 43 | } 44 | 45 | public function online($token, User $user = null, $status = User::ONLINE) 46 | { 47 | if ($user === null) { 48 | $user = $this->firstByToken($token); 49 | } 50 | 51 | if ($user) { 52 | $user->is_online = $status; 53 | $user->save(); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/Service/Formatter/UserFormatter.php: -------------------------------------------------------------------------------- 1 | $model->id, 25 | 'name' => $model->name, 26 | 'is_online' => $model->is_online, 27 | 'created_at' => $model->created_at->toDateTimeString(), 28 | 'updated_at' => $model->updated_at->toDateTimeString(), 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Service/Obj/UserObj.php: -------------------------------------------------------------------------------- 1 | token = $token; 25 | $this->fd = $fd; 26 | $this->node = $node; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Service/Redis/UserCollection.php: -------------------------------------------------------------------------------- 1 | get(\Redis::class); 27 | } 28 | 29 | public function save(UserObj $obj) 30 | { 31 | $str = serialize($obj); 32 | 33 | return $this->set($obj->token, $str, null); 34 | } 35 | 36 | public function find(string $token): ?UserObj 37 | { 38 | $str = $this->get($token); 39 | 40 | if ($str) { 41 | return unserialize($str); 42 | } 43 | 44 | return null; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/Service/Service.php: -------------------------------------------------------------------------------- 1 | container = $container; 32 | $this->logger = $container->get(StdoutLoggerInterface::class); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/Service/UserService.php: -------------------------------------------------------------------------------- 1 | users[$obj->fd] = $obj; 31 | 32 | $this->col->save($obj); 33 | } 34 | 35 | public function find(int $fd): ?UserObj 36 | { 37 | return $this->users[$fd] ?? null; 38 | } 39 | 40 | public function delete(UserObj $obj) 41 | { 42 | unset($this->users[$obj->fd]); 43 | 44 | $this->col->delete($obj->token); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /bin/hyperf.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | get(\Hyperf\Contract\ApplicationInterface::class); 22 | $application->run(); 23 | })(); 24 | -------------------------------------------------------------------------------- /chat.sql: -------------------------------------------------------------------------------- 1 | # ************************************************************ 2 | # Sequel Pro SQL dump 3 | # Version 5438 4 | # 5 | # https://www.sequelpro.com/ 6 | # https://github.com/sequelpro/sequelpro 7 | # 8 | # Host: 127.0.0.1 (MySQL 5.7.27) 9 | # Database: chat 10 | # Generation Time: 2019-09-06 07:10:03 +0000 11 | # ************************************************************ 12 | 13 | 14 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 15 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; 16 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; 17 | /*!40101 SET NAMES utf8 */; 18 | SET NAMES utf8mb4; 19 | /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; 20 | /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; 21 | /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; 22 | 23 | 24 | # Dump of table user 25 | # ------------------------------------------------------------ 26 | 27 | DROP TABLE IF EXISTS `user`; 28 | 29 | CREATE TABLE `user` ( 30 | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, 31 | `name` varchar(64) NOT NULL DEFAULT '', 32 | `token` varchar(32) NOT NULL DEFAULT '', 33 | `is_online` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否在线', 34 | `created_at` datetime DEFAULT NULL, 35 | `updated_at` datetime DEFAULT NULL, 36 | PRIMARY KEY (`id`), 37 | UNIQUE KEY `UNIQUE_NAME` (`name`), 38 | UNIQUE KEY `UNIQUE_TOKEN` (`token`), 39 | KEY `INDEX_ONLINE` (`is_online`) 40 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; 41 | 42 | 43 | 44 | 45 | /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; 46 | /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; 47 | /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; 48 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; 49 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; 50 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; 51 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperf/biz-skeleton", 3 | "type": "project", 4 | "keywords": [ 5 | "php", 6 | "swoole", 7 | "framework", 8 | "hyperf", 9 | "microservice", 10 | "middleware" 11 | ], 12 | "description": "A coroutine framework that focuses on hyperspeed and flexible, specifically use for build microservices and middlewares.", 13 | "license": "MIT", 14 | "require": { 15 | "php": ">=7.2", 16 | "ext-json": "*", 17 | "ext-openssl": "*", 18 | "ext-pdo": "*", 19 | "ext-pdo_mysql": "*", 20 | "ext-redis": "*", 21 | "ext-swoole": ">=4.5", 22 | "hyperf/async-queue": "2.0.*", 23 | "hyperf/cache": "2.0.*", 24 | "hyperf/command": "2.0.*", 25 | "hyperf/config": "2.0.*", 26 | "hyperf/constants": "2.0.*", 27 | "hyperf/contract": "2.0.*", 28 | "hyperf/database": "2.0.*", 29 | "hyperf/db-connection": "2.0.*", 30 | "hyperf/di": "2.0.*", 31 | "hyperf/dispatcher": "2.0.*", 32 | "hyperf/event": "2.0.*", 33 | "hyperf/exception-handler": "2.0.*", 34 | "hyperf/framework": "2.0.*", 35 | "hyperf/guzzle": "2.0.*", 36 | "hyperf/http-server": "2.0.*", 37 | "hyperf/logger": "2.0.*", 38 | "hyperf/model-cache": "2.0.*", 39 | "hyperf/nsq": "2.0.*", 40 | "hyperf/pool": "2.0.*", 41 | "hyperf/process": "2.0.*", 42 | "hyperf/redis": "2.0.*", 43 | "hyperf/server": "2.0.*", 44 | "hyperf/utils": "2.0.*", 45 | "hyperf/websocket-server": "2.0.*", 46 | "limingxinleo/redis-collection": "^1.2" 47 | }, 48 | "require-dev": { 49 | "friendsofphp/php-cs-fixer": "^2.14", 50 | "hyperf/devtool": "2.0.*", 51 | "hyperf/testing": "2.0.*", 52 | "mockery/mockery": "^1.0", 53 | "phpstan/phpstan": "^0.12.18", 54 | "swoole/ide-helper": "dev-master", 55 | "symfony/var-dumper": "^5.1" 56 | }, 57 | "autoload": { 58 | "psr-4": { 59 | "App\\": "app/" 60 | }, 61 | "files": [ 62 | "app/Kernel/Functions.php" 63 | ] 64 | }, 65 | "autoload-dev": { 66 | "psr-4": { 67 | "HyperfTest\\": "test/" 68 | } 69 | }, 70 | "minimum-stability": "dev", 71 | "prefer-stable": true, 72 | "config": { 73 | "sort-packages": true 74 | }, 75 | "extra": [], 76 | "scripts": { 77 | "post-root-package-install": [ 78 | "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" 79 | ], 80 | "post-autoload-dump": [ 81 | "rm -rf runtime/container" 82 | ], 83 | "analyse": "phpstan analyse --memory-limit 512M -l 0 -c phpstan.neon ./app ./config", 84 | "cs-fix": "php-cs-fixer fix $1", 85 | "start": "php ./bin/hyperf.php start", 86 | "test": "co-phpunit -c phpunit.xml --colors=always" 87 | }, 88 | "repositories": { 89 | "packagist": { 90 | "type": "composer", 91 | "url": "https://mirrors.aliyun.com/composer" 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /config/autoload/annotations.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'paths' => [ 15 | BASE_PATH . '/app', 16 | ], 17 | 'ignore_annotations' => [ 18 | 'mixin', 19 | ], 20 | 'class_map' => [ 21 | // Hyperf\Utils\Coroutine::class => BASE_PATH . '/app/Kernel/ClassMap/Coroutine.php', 22 | Hyperf\Di\Resolver\ResolverDispatcher::class => BASE_PATH . '/app/Kernel/ClassMap/ResolverDispatcher.php', 23 | ], 24 | ], 25 | ]; 26 | -------------------------------------------------------------------------------- /config/autoload/aspects.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'driver' => Hyperf\AsyncQueue\Driver\RedisDriver::class, 15 | 'channel' => '{queue}', 16 | 'timeout' => 2, 17 | 'retry_seconds' => 5, 18 | 'handle_timeout' => 10, 19 | 'processes' => 1, 20 | 'concurrent' => [ 21 | 'limit' => 2, 22 | ], 23 | ], 24 | ]; 25 | -------------------------------------------------------------------------------- /config/autoload/cache.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'driver' => Hyperf\Cache\Driver\RedisDriver::class, 15 | 'packer' => Hyperf\Utils\Packer\PhpSerializerPacker::class, 16 | 'prefix' => 'c:', 17 | ], 18 | ]; 19 | -------------------------------------------------------------------------------- /config/autoload/commands.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'driver' => env('DB_DRIVER', 'mysql'), 15 | 'host' => env('DB_HOST', 'localhost'), 16 | 'port' => env('DB_PORT', 3306), 17 | 'database' => env('DB_DATABASE', 'hyperf'), 18 | 'username' => env('DB_USERNAME', 'root'), 19 | 'password' => env('DB_PASSWORD', ''), 20 | 'charset' => env('DB_CHARSET', 'utf8mb4'), 21 | 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), 22 | 'prefix' => env('DB_PREFIX', ''), 23 | 'pool' => [ 24 | 'min_connections' => 1, 25 | 'max_connections' => 10, 26 | 'connect_timeout' => 10.0, 27 | 'wait_timeout' => 3.0, 28 | 'heartbeat' => -1, 29 | 'max_idle_time' => (float) env('DB_MAX_IDLE_TIME', 60), 30 | ], 31 | 'cache' => [ 32 | 'handler' => Hyperf\ModelCache\Handler\RedisHandler::class, 33 | 'cache_key' => '{mc:%s:m:%s}:%s:%s', 34 | 'prefix' => 'default', 35 | 'ttl' => 3600 * 24, 36 | 'empty_model_ttl' => 600, 37 | 'load_script' => true, 38 | ], 39 | 'commands' => [ 40 | 'gen:model' => [ 41 | 'path' => 'app/Model', 42 | 'force_casts' => true, 43 | 'inheritance' => 'Model', 44 | 'uses' => '', 45 | 'refresh_fillable' => true, 46 | 'table_mapping' => [], 47 | ], 48 | ], 49 | ], 50 | ]; 51 | -------------------------------------------------------------------------------- /config/autoload/dependencies.php: -------------------------------------------------------------------------------- 1 | App\Kernel\Log\LoggerFactory::class, 17 | Hyperf\Server\Listener\AfterWorkerStartListener::class => App\Kernel\Http\WorkerStartListener::class, 18 | 'protocal.send.message' => SendMessageHandler::class, 19 | 'protocal.user.list' => UserListHandler::class, 20 | ]; 21 | -------------------------------------------------------------------------------- /config/autoload/devtool.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'amqp' => [ 15 | 'consumer' => [ 16 | 'namespace' => 'App\\Amqp\\Consumer', 17 | ], 18 | 'producer' => [ 19 | 'namespace' => 'App\\Amqp\\Producer', 20 | ], 21 | ], 22 | 'aspect' => [ 23 | 'namespace' => 'App\\Aspect', 24 | ], 25 | 'command' => [ 26 | 'namespace' => 'App\\Command', 27 | ], 28 | 'controller' => [ 29 | 'namespace' => 'App\\Controller', 30 | ], 31 | 'job' => [ 32 | 'namespace' => 'App\\Job', 33 | ], 34 | 'listener' => [ 35 | 'namespace' => 'App\\Listener', 36 | ], 37 | 'middleware' => [ 38 | 'namespace' => 'App\\Middleware', 39 | ], 40 | 'Process' => [ 41 | 'namespace' => 'App\\Processes', 42 | ], 43 | ], 44 | ]; 45 | -------------------------------------------------------------------------------- /config/autoload/exceptions.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'http' => [ 15 | App\Exception\Handler\BusinessExceptionHandler::class, 16 | ], 17 | ], 18 | ]; 19 | -------------------------------------------------------------------------------- /config/autoload/listeners.php: -------------------------------------------------------------------------------- 1 | [ 16 | 'handler' => [ 17 | 'class' => Monolog\Handler\StreamHandler::class, 18 | 'constructor' => [ 19 | 'stream' => BASE_PATH . '/runtime/logs/hyperf.log', 20 | 'level' => Monolog\Logger::INFO, 21 | ], 22 | ], 23 | 'formatter' => [ 24 | 'class' => Monolog\Formatter\LineFormatter::class, 25 | 'constructor' => [ 26 | 'format' => null, 27 | 'dateFormat' => 'Y-m-d H:i:s', 28 | 'allowInlineLineBreaks' => true, 29 | ], 30 | ], 31 | 'processors' => [ 32 | [ 33 | 'class' => Log\AppendRequestIdProcessor::class, 34 | ], 35 | ], 36 | ], 37 | ]; 38 | -------------------------------------------------------------------------------- /config/autoload/middlewares.php: -------------------------------------------------------------------------------- 1 | [ 14 | ], 15 | ]; 16 | -------------------------------------------------------------------------------- /config/autoload/nsq.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'enable' => true, 15 | 'host' => '127.0.0.1', 16 | 'port' => 4150, 17 | 'pool' => [ 18 | 'min_connections' => 1, 19 | 'max_connections' => 10, 20 | 'connect_timeout' => 10.0, 21 | 'wait_timeout' => 3.0, 22 | 'heartbeat' => -1, 23 | 'max_idle_time' => 60.0, 24 | ], 25 | 'nsqd' => [ 26 | 'port' => 4151, 27 | 'options' => [ 28 | ], 29 | ], 30 | ], 31 | ]; 32 | -------------------------------------------------------------------------------- /config/autoload/processes.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'host' => env('REDIS_HOST', 'localhost'), 15 | 'auth' => env('REDIS_AUTH', null), 16 | 'port' => (int) env('REDIS_PORT', 6379), 17 | 'db' => (int) env('REDIS_DB', 0), 18 | 'pool' => [ 19 | 'min_connections' => 1, 20 | 'max_connections' => 10, 21 | 'connect_timeout' => 10.0, 22 | 'wait_timeout' => 3.0, 23 | 'heartbeat' => -1, 24 | 'max_idle_time' => (float) env('REDIS_MAX_IDLE_TIME', 60), 25 | ], 26 | ], 27 | ]; 28 | -------------------------------------------------------------------------------- /config/autoload/server.php: -------------------------------------------------------------------------------- 1 | SWOOLE_BASE, 17 | 'servers' => [ 18 | [ 19 | 'name' => 'http', 20 | 'type' => Server::SERVER_WEBSOCKET, 21 | 'host' => '0.0.0.0', 22 | 'port' => 9501, 23 | 'sock_type' => SWOOLE_SOCK_TCP, 24 | 'callbacks' => [ 25 | SwooleEvent::ON_HAND_SHAKE => [Hyperf\WebSocketServer\Server::class, 'onHandShake'], 26 | SwooleEvent::ON_MESSAGE => [Hyperf\WebSocketServer\Server::class, 'onMessage'], 27 | SwooleEvent::ON_CLOSE => [Hyperf\WebSocketServer\Server::class, 'onClose'], 28 | ], 29 | ], 30 | ], 31 | 'settings' => [ 32 | 'enable_coroutine' => true, 33 | 'worker_num' => 4, 34 | 'pid_file' => BASE_PATH . '/runtime/hyperf.pid', 35 | 'open_tcp_nodelay' => true, 36 | 'max_coroutine' => 100000, 37 | 'open_http2_protocol' => true, 38 | 'max_request' => 0, 39 | 'socket_buffer_size' => 2 * 1024 * 1024, 40 | 'package_max_length' => 2 * 1024 * 1024, 41 | ], 42 | 'callbacks' => [ 43 | SwooleEvent::ON_BEFORE_START => [Hyperf\Framework\Bootstrap\ServerStartCallback::class, 'beforeStart'], 44 | SwooleEvent::ON_WORKER_START => [Hyperf\Framework\Bootstrap\WorkerStartCallback::class, 'onWorkerStart'], 45 | SwooleEvent::ON_PIPE_MESSAGE => [Hyperf\Framework\Bootstrap\PipeMessageCallback::class, 'onPipeMessage'], 46 | SwooleEvent::ON_WORKER_EXIT => [Hyperf\Framework\Bootstrap\WorkerExitCallback::class, 'onWorkerExit'], 47 | ], 48 | ]; 49 | -------------------------------------------------------------------------------- /config/config.php: -------------------------------------------------------------------------------- 1 | env('APP_NAME', 'skeleton'), 17 | 'app_env' => env('APP_ENV', 'dev'), 18 | 'scan_cacheable' => env('SCAN_CACHEABLE', false), 19 | StdoutLoggerInterface::class => [ 20 | 'log_level' => [ 21 | LogLevel::ALERT, 22 | LogLevel::CRITICAL, 23 | LogLevel::DEBUG, 24 | LogLevel::EMERGENCY, 25 | LogLevel::ERROR, 26 | LogLevel::INFO, 27 | LogLevel::NOTICE, 28 | LogLevel::WARNING, 29 | ], 30 | ], 31 | ]; 32 | -------------------------------------------------------------------------------- /config/container.php: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | test 14 | 15 | 16 | 17 | 18 | app 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /test/Cases/ExampleTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/HttpTestCase.php: -------------------------------------------------------------------------------- 1 | client = make(Testing\Client::class); 35 | // $this->client = make(Testing\HttpClient::class, ['baseUri' => 'http://127.0.0.1:9501']); 36 | } 37 | 38 | public function __call($name, $arguments) 39 | { 40 | return $this->client->{$name}(...$arguments); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/bootstrap.php: -------------------------------------------------------------------------------- 1 | get(Hyperf\Contract\ApplicationInterface::class); 27 | --------------------------------------------------------------------------------