mirror of
https://gitee.com/mix-php/mix.git
synced 2024-11-29 18:28:00 +08:00
feat:mix:add examples/
This commit is contained in:
parent
b780c3db7b
commit
f31bdadb1b
16
examples/api-skeleton/.env
Normal file
16
examples/api-skeleton/.env
Normal file
@ -0,0 +1,16 @@
|
||||
# APP
|
||||
APP_DEBUG=true
|
||||
|
||||
# DATABASE
|
||||
DATABASE_DSN='mysql:host=127.0.0.1;port=3306;charset=utf8;dbname=test'
|
||||
DATABASE_USERNAME=root
|
||||
DATABASE_PASSWORD=123456
|
||||
|
||||
# REDIS
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PORT=6379
|
||||
REDIS_DATABASE=0
|
||||
REDIS_PASSWORD=
|
||||
|
||||
# JWT
|
||||
JWT_KEY=my_secret_key
|
26
examples/api-skeleton/.gitignore
vendored
Normal file
26
examples/api-skeleton/.gitignore
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
# phpstorm project files
|
||||
.idea
|
||||
|
||||
# netbeans project files
|
||||
nbproject
|
||||
|
||||
# zend studio for eclipse project files
|
||||
.buildpath
|
||||
.project
|
||||
.settings
|
||||
|
||||
# windows thumbnail cache
|
||||
Thumbs.db
|
||||
|
||||
# composer itself is not needed
|
||||
composer.phar
|
||||
composer.lock
|
||||
vendor/
|
||||
|
||||
# Mac DS_Store Files
|
||||
.DS_Store
|
||||
|
||||
# phpunit itself is not needed
|
||||
phpunit.phar
|
||||
# local phpunit config
|
||||
/phpunit.xml
|
232
examples/api-skeleton/README.md
Normal file
232
examples/api-skeleton/README.md
Normal file
@ -0,0 +1,232 @@
|
||||
# API development skeleton
|
||||
|
||||
帮助你快速搭建 API 项目骨架,并指导你如何使用该骨架的细节,骨架默认开启了 SQL、Redis 日志,压测前请先关闭 `.env` 的 `APP_DEBUG`
|
||||
|
||||
## 安装
|
||||
|
||||
> 需要先安装 [Swoole](https://wiki.swoole.com/#/environment) 或者 [WorkerMan](http://doc.workerman.net/install/requirement.html)
|
||||
|
||||
```
|
||||
composer create-project --prefer-dist mix/api-skeleton api
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
启动 [cli-server](https://www.php.net/manual/zh/features.commandline.webserver.php) 开发服务
|
||||
|
||||
```
|
||||
composer run-script --timeout=0 cliserver:start
|
||||
```
|
||||
|
||||
启动 Swoole 多进程服务
|
||||
|
||||
```
|
||||
composer run-script --timeout=0 swoole:start
|
||||
```
|
||||
|
||||
启动 Swoole 协程服务
|
||||
|
||||
```
|
||||
composer run-script --timeout=0 swooleco:start
|
||||
```
|
||||
|
||||
启动 WorkerMan 多进程服务
|
||||
|
||||
```
|
||||
composer run-script --timeout=0 workerman:start
|
||||
```
|
||||
|
||||
## 执行脚本
|
||||
|
||||
- `composer run-script` 命令中的 `--timeout=0` 参数是防止 composer [执行超时](https://getcomposer.org/doc/06-config.md#process-timeout)
|
||||
- `composer.json` 定义了命令执行脚本,对应上面的执行命令
|
||||
|
||||
```json
|
||||
"scripts": {
|
||||
"cliserver:start": "php -S localhost:8000 public/index.php",
|
||||
"swoole:start": "php bin/swoole.php",
|
||||
"swooleco:start": "php bin/swooleco.php",
|
||||
"workerman:start": "php bin/workerman.php start",
|
||||
"cli:clearcache": "php bin/cli.php clearcache"
|
||||
},
|
||||
```
|
||||
|
||||
当然也可以直接下面这样启动,效果是一样的,但是 `scripts` 能帮你记录到底有哪些可用的命令,同时在IDE中调试更加方便。
|
||||
|
||||
```
|
||||
php bin/swoole.php start
|
||||
```
|
||||
|
||||
## 部署
|
||||
|
||||
- CLI
|
||||
|
||||
线上部署启动时,修改 `shell/server.sh` 脚本中的绝对路径和参数
|
||||
|
||||
```
|
||||
php=/usr/local/bin/php
|
||||
file=/project/bin/swoole.php
|
||||
cmd=start
|
||||
numprocs=1
|
||||
```
|
||||
|
||||
启动管理
|
||||
|
||||
```
|
||||
sh shell/server.sh start
|
||||
sh shell/server.sh stop
|
||||
sh shell/server.sh restart
|
||||
```
|
||||
|
||||
使用 `nginx` 或者 `SLB` 代理到服务器端口即可
|
||||
|
||||
```
|
||||
server {
|
||||
server_name www.domain.com;
|
||||
listen 80;
|
||||
root /data/project/public;
|
||||
|
||||
location / {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "keep-alive";
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header Scheme $scheme;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
if (!-f $request_filename) {
|
||||
proxy_pass http://127.0.0.1:9501;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- PHP-FPM
|
||||
|
||||
和 Laravel、ThinkPHP 部署方法完全一致,将 `public/index.php` 在 `nginx` 配置 `rewrite` 重写即可
|
||||
|
||||
```
|
||||
server {
|
||||
server_name www.domain.com;
|
||||
listen 80;
|
||||
root /data/project/public;
|
||||
index index.html index.php;
|
||||
|
||||
location / {
|
||||
if (!-e $request_filename) {
|
||||
rewrite ^/(.*)$ /index.php/$1 last;
|
||||
}
|
||||
}
|
||||
|
||||
location ~ ^(.+\.php)(.*)$ {
|
||||
fastcgi_pass 127.0.0.1:9000;
|
||||
fastcgi_split_path_info ^(.+\.php)(.*)$;
|
||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
include fastcgi_params;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 编写一个 API 接口
|
||||
|
||||
首先修改根目录 `.env` 文件的数据库信息
|
||||
|
||||
然后在 `routes/index.php` 定义一个新的路由
|
||||
|
||||
```php
|
||||
$vega->handle('/users/{id}', [new Users(), 'index'])->methods('GET');
|
||||
```
|
||||
|
||||
路由里使用了 `Users` 控制器,我们需要创建他
|
||||
|
||||
- 如何配置路由:[mix-php/vega](https://github.com/mix-php/vega#readme)
|
||||
- 如何调用数据库:[mix-php/database](https://github.com/mix-php/database#readme)
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Container\DB;
|
||||
use Mix\Vega\Context;
|
||||
|
||||
class Users
|
||||
{
|
||||
|
||||
/**
|
||||
* @param Context $ctx
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function index(Context $ctx)
|
||||
{
|
||||
$row = DB::instance()->table('users')->where('id = ?', $ctx->param('id'))->first();
|
||||
if (!$row) {
|
||||
throw new \Exception('User not found');
|
||||
}
|
||||
$ctx->JSON(200, [
|
||||
'code' => 0,
|
||||
'message' => 'ok',
|
||||
'data' => $row
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
重新启动服务器后方可测试新开发的接口
|
||||
|
||||
> 实际开发中使用 PhpStorm 的 Run 功能,只需要点击一下重启按钮即可
|
||||
|
||||
```
|
||||
// 查找进程 PID
|
||||
ps -ef | grep swoole
|
||||
|
||||
// 通过 PID 停止进程
|
||||
kill PID
|
||||
|
||||
// 重新启动进程
|
||||
composer run-script swoole:start
|
||||
|
||||
// curl 测试
|
||||
curl http://127.0.0.1:9501/users/1
|
||||
```
|
||||
|
||||
## 使用容器中的对象
|
||||
|
||||
容器采用了一个简单的单例模式,你可以修改为更加适合自己的方式。
|
||||
|
||||
- 数据库
|
||||
|
||||
```
|
||||
DB::instance()
|
||||
```
|
||||
|
||||
文档:[mix-php/database](https://github.com/mix-php/database#readme)
|
||||
|
||||
- Redis
|
||||
|
||||
```
|
||||
RDS::instance()
|
||||
```
|
||||
|
||||
文档:[mix-php/redis](https://github.com/mix-php/redis#readme)
|
||||
|
||||
- 日志
|
||||
|
||||
```
|
||||
Logger::instance()
|
||||
```
|
||||
|
||||
文档:[monolog/monolog](https://seldaek.github.io/monolog/doc/01-usage.html)
|
||||
|
||||
- 配置
|
||||
|
||||
```
|
||||
Config::instance()
|
||||
```
|
||||
|
||||
文档:[hassankhan/config](https://github.com/hassankhan/config#getting-values)
|
||||
|
||||
## License
|
||||
|
||||
Apache License Version 2.0, http://www.apache.org/licenses/
|
15
examples/api-skeleton/bin/cli.php
Normal file
15
examples/api-skeleton/bin/cli.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use Dotenv\Dotenv;
|
||||
|
||||
Dotenv::createUnsafeImmutable(__DIR__ . '/../', '.env')->load();
|
||||
define("APP_DEBUG", env('APP_DEBUG'));
|
||||
|
||||
App\Error::register();
|
||||
|
||||
switch ($argv[1]) {
|
||||
case 'clearcache';
|
||||
(new \App\Command\ClearCache())->exec();
|
||||
break;
|
||||
}
|
35
examples/api-skeleton/bin/swoole.php
Normal file
35
examples/api-skeleton/bin/swoole.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use App\Container\Logger;
|
||||
use App\Vega;
|
||||
use Dotenv\Dotenv;
|
||||
|
||||
Dotenv::createUnsafeImmutable(__DIR__ . '/../', '.env')->load();
|
||||
define("APP_DEBUG", env('APP_DEBUG'));
|
||||
|
||||
App\Error::register();
|
||||
|
||||
/**
|
||||
* 多进程默认开启了协程
|
||||
* 关闭协程只需关闭 `enable_coroutine` 配置并注释数据库的 `::enableCoroutine()` 即可退化为多进程同步模式
|
||||
*/
|
||||
|
||||
$vega = Vega::new();
|
||||
$http = new Swoole\Http\Server('0.0.0.0', 9501);
|
||||
$http->on('Request', $vega->handler());
|
||||
$http->on('WorkerStart', function ($server, $workerId) {
|
||||
// swoole 协程不支持 set_exception_handler 需要手动捕获异常
|
||||
try {
|
||||
App\Container\DB::enableCoroutine();
|
||||
App\Container\RDS::enableCoroutine();
|
||||
} catch (\Throwable $ex) {
|
||||
App\Error::handle($ex);
|
||||
}
|
||||
});
|
||||
$http->set([
|
||||
'enable_coroutine' => true,
|
||||
'worker_num' => 4,
|
||||
]);
|
||||
Logger::instance()->info('Start swoole server');
|
||||
$http->start();
|
30
examples/api-skeleton/bin/swooleco.php
Normal file
30
examples/api-skeleton/bin/swooleco.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use App\Container\Logger;
|
||||
use App\Vega;
|
||||
use Dotenv\Dotenv;
|
||||
|
||||
Dotenv::createUnsafeImmutable(__DIR__ . '/../', '.env')->load();
|
||||
define("APP_DEBUG", env('APP_DEBUG'));
|
||||
|
||||
App\Error::register();
|
||||
|
||||
Swoole\Coroutine\run(function () {
|
||||
$vega = Vega::new();
|
||||
$server = new Swoole\Coroutine\Http\Server('0.0.0.0', 9502, false, false);
|
||||
$server->handle('/', $vega->handler());
|
||||
|
||||
App\Container\DB::enableCoroutine();
|
||||
App\Container\RDS::enableCoroutine();
|
||||
|
||||
foreach ([SIGHUP, SIGINT, SIGTERM] as $signal) {
|
||||
Swoole\Process::signal($signal, function () use ($server) {
|
||||
Logger::instance()->info('Shutdown swoole coroutine server');
|
||||
$server->shutdown();
|
||||
});
|
||||
}
|
||||
|
||||
Logger::instance()->info('Start swoole coroutine server');
|
||||
$server->start();
|
||||
});
|
18
examples/api-skeleton/bin/workerman.php
Normal file
18
examples/api-skeleton/bin/workerman.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use App\Container\Logger;
|
||||
use App\Vega;
|
||||
use Dotenv\Dotenv;
|
||||
|
||||
Dotenv::createUnsafeImmutable(__DIR__ . '/../', '.env')->load();
|
||||
define("APP_DEBUG", env('APP_DEBUG'));
|
||||
|
||||
App\Error::register();
|
||||
|
||||
$vega = Vega::new();
|
||||
$http = new Workerman\Worker("http://0.0.0.0:2345");
|
||||
$http->onMessage = $vega->handler();
|
||||
$http->count = 4;
|
||||
Logger::instance()->info('Start workerman server');
|
||||
Workerman\Worker::runAll();
|
35
examples/api-skeleton/composer.json
Normal file
35
examples/api-skeleton/composer.json
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "mix/api-skeleton",
|
||||
"description": "API development skeleton",
|
||||
"type": "project",
|
||||
"homepage": "https://openmix.org/mix-php",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"cliserver:start": "php -S localhost:8000 public/index.php",
|
||||
"swoole:start": "php bin/swoole.php",
|
||||
"swooleco:start": "php bin/swooleco.php",
|
||||
"workerman:start": "php bin/workerman.php start",
|
||||
"cli:clearcache": "php bin/cli.php clearcache"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "src/"
|
||||
},
|
||||
"files": [
|
||||
"src/functions.php"
|
||||
]
|
||||
},
|
||||
"require": {
|
||||
"workerman/workerman": "^4.0",
|
||||
"mix/vega": "~3.0",
|
||||
"mix/database": "~3.0",
|
||||
"mix/redis": "~3.0",
|
||||
"vlucas/phpdotenv": "^5.3",
|
||||
"hassankhan/config": "^2.2",
|
||||
"monolog/monolog": "^2.3",
|
||||
"firebase/php-jwt": "^5.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"swoole/ide-helper": "^4.6"
|
||||
}
|
||||
}
|
3
examples/api-skeleton/conf/config.json
Normal file
3
examples/api-skeleton/conf/config.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"foo": "bar"
|
||||
}
|
16
examples/api-skeleton/public/index.php
Normal file
16
examples/api-skeleton/public/index.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
/**
|
||||
* PHP-FPM, cli-server 模式专用
|
||||
*/
|
||||
|
||||
use App\Vega;
|
||||
use Dotenv\Dotenv;
|
||||
|
||||
Dotenv::createUnsafeImmutable(__DIR__ . '/../', '.env')->load();
|
||||
define("APP_DEBUG", env('APP_DEBUG'));
|
||||
|
||||
App\Error::register();
|
||||
|
||||
return Vega::new()->run();
|
11
examples/api-skeleton/routes/index.php
Normal file
11
examples/api-skeleton/routes/index.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
use App\Controller\Auth;
|
||||
use App\Controller\Hello;
|
||||
use App\Controller\Users;
|
||||
|
||||
return function (Mix\Vega\Engine $vega) {
|
||||
$vega->handle('/hello', [new Hello(), 'index'])->methods('GET');
|
||||
$vega->handle('/users/{id}', [new Users(), 'index'])->methods('GET');
|
||||
$vega->handle('/auth', [new Auth(), 'index'])->methods('GET');
|
||||
};
|
2
examples/api-skeleton/runtime/.gitignore
vendored
Normal file
2
examples/api-skeleton/runtime/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
75
examples/api-skeleton/shell/server.sh
Normal file
75
examples/api-skeleton/shell/server.sh
Normal file
@ -0,0 +1,75 @@
|
||||
#!/bin/sh
|
||||
echo "============`date +%F' '%T`==========="
|
||||
|
||||
php=/usr/local/bin/php
|
||||
file=/project/bin/swoole.php
|
||||
cmd=start
|
||||
numprocs=1
|
||||
|
||||
getpid()
|
||||
{
|
||||
docmd=`ps aux | grep ${file} | grep ${cmd} | grep -v 'grep' | grep -v '\.sh' | awk '{print $2}' | xargs`
|
||||
echo $docmd
|
||||
}
|
||||
|
||||
getmpid()
|
||||
{
|
||||
docmd=`ps -ef | grep ${file} | grep ${cmd} | grep -v 'grep' | grep -v '\.sh' | grep ' 1 ' | awk '{print $2}' | xargs`
|
||||
echo $docmd
|
||||
}
|
||||
|
||||
start()
|
||||
{
|
||||
pidstr=`getpid`
|
||||
if [ -n "$pidstr" ];then
|
||||
echo "running with pids $pidstr"
|
||||
else
|
||||
if [ $numprocs -eq 1 ];then
|
||||
$php $file $cmd > /dev/null 2>&1 &
|
||||
else
|
||||
i=0
|
||||
while(( $i<$numprocs ))
|
||||
do
|
||||
$php $file $cmd > /dev/null 2>&1 &
|
||||
let "i++"
|
||||
done
|
||||
fi
|
||||
sleep 1
|
||||
pidstr=`getpid`
|
||||
echo "start with pids $pidstr"
|
||||
fi
|
||||
}
|
||||
|
||||
stop()
|
||||
{
|
||||
pidstr=`getpid`
|
||||
if [ ! -n "$pidstr" ];then
|
||||
echo "not executed!"
|
||||
return
|
||||
fi
|
||||
mpidstr=`getmpid`
|
||||
if [ -n "$mpidstr" ];then
|
||||
pidstr=$mpidstr
|
||||
fi
|
||||
echo "kill $pidstr"
|
||||
kill $pidstr
|
||||
}
|
||||
|
||||
restart()
|
||||
{
|
||||
stop
|
||||
sleep 1
|
||||
start
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
start
|
||||
;;
|
||||
stop)
|
||||
stop
|
||||
;;
|
||||
restart)
|
||||
restart
|
||||
;;
|
||||
esac
|
20
examples/api-skeleton/src/Command/ClearCache.php
Normal file
20
examples/api-skeleton/src/Command/ClearCache.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Container\RDS;
|
||||
|
||||
/**
|
||||
* Class ClearCache
|
||||
* @package App\Command
|
||||
*/
|
||||
class ClearCache
|
||||
{
|
||||
|
||||
public function exec(): void
|
||||
{
|
||||
RDS::instance()->del('foo_cache');
|
||||
print 'ok';
|
||||
}
|
||||
|
||||
}
|
28
examples/api-skeleton/src/Container/Config.php
Normal file
28
examples/api-skeleton/src/Container/Config.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Container;
|
||||
|
||||
/**
|
||||
* Class Config
|
||||
* @package App\Container
|
||||
*/
|
||||
class Config
|
||||
{
|
||||
|
||||
/**
|
||||
* @var \Noodlehaus\Config
|
||||
*/
|
||||
static private $instance;
|
||||
|
||||
/**
|
||||
* @return \Noodlehaus\Config
|
||||
*/
|
||||
public static function instance(): \Noodlehaus\Config
|
||||
{
|
||||
if (!isset(self::$instance)) {
|
||||
self::$instance = new \Noodlehaus\Config(__DIR__ . '/../../conf');
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
}
|
41
examples/api-skeleton/src/Container/DB.php
Normal file
41
examples/api-skeleton/src/Container/DB.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Container;
|
||||
|
||||
use Mix\Database\Database;
|
||||
|
||||
class DB
|
||||
{
|
||||
|
||||
/**
|
||||
* @var Database
|
||||
*/
|
||||
static private $instance;
|
||||
|
||||
/**
|
||||
* @return Database
|
||||
*/
|
||||
public static function instance(): Database
|
||||
{
|
||||
if (!isset(self::$instance)) {
|
||||
$dsn = $_ENV['DATABASE_DSN'];
|
||||
$username = $_ENV['DATABASE_USERNAME'];
|
||||
$password = $_ENV['DATABASE_PASSWORD'];
|
||||
$db = new Database($dsn, $username, $password);
|
||||
APP_DEBUG and $db->setLogger(new DBLogger());
|
||||
self::$instance = $db;
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public static function enableCoroutine()
|
||||
{
|
||||
$maxOpen = 30; // 最大开启连接数
|
||||
$maxIdle = 10; // 最大闲置连接数
|
||||
$maxLifetime = 3600; // 连接的最长生命周期
|
||||
$waitTimeout = 0.0; // 从池获取连接等待的时间, 0为一直等待
|
||||
self::instance()->startPool($maxOpen, $maxIdle, $maxLifetime, $waitTimeout);
|
||||
\Swoole\Runtime::enableCoroutine(); // 必须放到最后,防止触发协程调度导致异常
|
||||
}
|
||||
|
||||
}
|
17
examples/api-skeleton/src/Container/DBLogger.php
Normal file
17
examples/api-skeleton/src/Container/DBLogger.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Container;
|
||||
|
||||
/**
|
||||
* Class DBLogger
|
||||
* @package App\Container
|
||||
*/
|
||||
class DBLogger implements \Mix\Database\LoggerInterface
|
||||
{
|
||||
|
||||
public function trace(float $time, string $sql, array $bindings, int $rowCount, ?\Throwable $exception): void
|
||||
{
|
||||
Logger::instance()->debug(sprintf('SQL: %sms %s %s %d', $time, $sql, json_encode($bindings), $rowCount));
|
||||
}
|
||||
|
||||
}
|
61
examples/api-skeleton/src/Container/Logger.php
Normal file
61
examples/api-skeleton/src/Container/Logger.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Container;
|
||||
|
||||
use Monolog\Formatter\LineFormatter;
|
||||
use Monolog\Handler\HandlerInterface;
|
||||
use Monolog\Handler\RotatingFileHandler;
|
||||
|
||||
/**
|
||||
* Class Logger
|
||||
* @package App\Container
|
||||
*/
|
||||
class Logger implements HandlerInterface
|
||||
{
|
||||
|
||||
static private $instance;
|
||||
|
||||
/**
|
||||
* @return \Monolog\Logger
|
||||
*/
|
||||
public static function instance(): \Monolog\Logger
|
||||
{
|
||||
if (!isset(self::$instance)) {
|
||||
$logger = new \Monolog\Logger('MIX');
|
||||
$rotatingFileHandler = new RotatingFileHandler(__DIR__ . '/../../runtime/logs/mix.log', 7);
|
||||
$rotatingFileHandler->setFormatter(new LineFormatter("[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n", 'Y-m-d H:i:s.u'));
|
||||
$logger->pushHandler($rotatingFileHandler);
|
||||
$logger->pushHandler(new Logger());
|
||||
self::$instance = $logger;
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function isHandling(array $record): bool
|
||||
{
|
||||
return $record['level'] >= \Monolog\Logger::DEBUG;
|
||||
}
|
||||
|
||||
public function handle(array $record): bool
|
||||
{
|
||||
$message = sprintf("%s %s %s\n", $record['datetime']->format('Y-m-d H:i:s.u'), $record['level_name'], $record['message']);
|
||||
switch (PHP_SAPI) {
|
||||
case 'cli':
|
||||
case 'cli-server':
|
||||
file_put_contents("php://stdout", $message);
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function handleBatch(array $records): void
|
||||
{
|
||||
// TODO: Implement handleBatch() method.
|
||||
}
|
||||
|
||||
public function close(): void
|
||||
{
|
||||
// TODO: Implement close() method.
|
||||
}
|
||||
|
||||
}
|
42
examples/api-skeleton/src/Container/RDS.php
Normal file
42
examples/api-skeleton/src/Container/RDS.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Container;
|
||||
|
||||
use Mix\Redis\Redis;
|
||||
|
||||
class RDS
|
||||
{
|
||||
|
||||
/**
|
||||
* @var Redis
|
||||
*/
|
||||
static private $instance;
|
||||
|
||||
/**
|
||||
* @return Redis
|
||||
*/
|
||||
public static function instance(): Redis
|
||||
{
|
||||
if (!isset(self::$instance)) {
|
||||
$host = $_ENV['REDIS_HOST'];
|
||||
$port = $_ENV['REDIS_PORT'];
|
||||
$password = $_ENV['REDIS_PASSWORD'];
|
||||
$database = $_ENV['REDIS_DATABASE'];
|
||||
$rds = new Redis($host, $port, $password, $database);
|
||||
APP_DEBUG and $rds->setLogger(new RDSLogger());
|
||||
self::$instance = $rds;
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public static function enableCoroutine()
|
||||
{
|
||||
$maxOpen = 30; // 最大开启连接数
|
||||
$maxIdle = 10; // 最大闲置连接数
|
||||
$maxLifetime = 3600; // 连接的最长生命周期
|
||||
$waitTimeout = 0.0; // 从池获取连接等待的时间, 0为一直等待
|
||||
self::instance()->startPool($maxOpen, $maxIdle, $maxLifetime, $waitTimeout);
|
||||
\Swoole\Runtime::enableCoroutine(); // 必须放到最后,防止触发协程调度导致异常
|
||||
}
|
||||
|
||||
}
|
17
examples/api-skeleton/src/Container/RDSLogger.php
Normal file
17
examples/api-skeleton/src/Container/RDSLogger.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Container;
|
||||
|
||||
/**
|
||||
* Class RDSLogger
|
||||
* @package App\Container
|
||||
*/
|
||||
class RDSLogger implements \Mix\Redis\LoggerInterface
|
||||
{
|
||||
|
||||
public function trace(float $time, string $cmd, array $args, ?\Throwable $exception): void
|
||||
{
|
||||
Logger::instance()->debug(sprintf('RDS: %sms %s %s', $time, $cmd, json_encode($args)));
|
||||
}
|
||||
|
||||
}
|
34
examples/api-skeleton/src/Controller/Auth.php
Normal file
34
examples/api-skeleton/src/Controller/Auth.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use Firebase\JWT\JWT;
|
||||
use Mix\Vega\Context;
|
||||
|
||||
class Auth
|
||||
{
|
||||
|
||||
/**
|
||||
* @param Context $ctx
|
||||
*/
|
||||
public function index(Context $ctx)
|
||||
{
|
||||
$time = time();
|
||||
$payload = [
|
||||
"iss" => "http://example.org", // 签发人
|
||||
'iat' => $time, // 签发时间
|
||||
'exp' => $time + 7200, // 过期时间
|
||||
'uid' => 100008,
|
||||
];
|
||||
$token = JWT::encode($payload, $_ENV['JWT_KEY'], 'HS256');
|
||||
$ctx->JSON(200, [
|
||||
'code' => 0,
|
||||
'message' => 'ok',
|
||||
'data' => [
|
||||
'access_token' => $token,
|
||||
'expire_in' => 7200,
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
18
examples/api-skeleton/src/Controller/Hello.php
Normal file
18
examples/api-skeleton/src/Controller/Hello.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use Mix\Vega\Context;
|
||||
|
||||
class Hello
|
||||
{
|
||||
|
||||
/**
|
||||
* @param Context $ctx
|
||||
*/
|
||||
public function index(Context $ctx)
|
||||
{
|
||||
$ctx->string(200, 'hello, world!');
|
||||
}
|
||||
|
||||
}
|
28
examples/api-skeleton/src/Controller/Users.php
Normal file
28
examples/api-skeleton/src/Controller/Users.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Container\DB;
|
||||
use Mix\Vega\Context;
|
||||
|
||||
class Users
|
||||
{
|
||||
|
||||
/**
|
||||
* @param Context $ctx
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function index(Context $ctx)
|
||||
{
|
||||
$row = DB::instance()->table('users')->where('id = ?', $ctx->param('id'))->first();
|
||||
if (!$row) {
|
||||
throw new \Exception('User not found');
|
||||
}
|
||||
$ctx->JSON(200, [
|
||||
'code' => 0,
|
||||
'message' => 'ok',
|
||||
'data' => $row
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
81
examples/api-skeleton/src/Error.php
Normal file
81
examples/api-skeleton/src/Error.php
Normal file
@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use App\Container\Logger;
|
||||
|
||||
/**
|
||||
* Class Error
|
||||
* @package App
|
||||
*/
|
||||
class Error
|
||||
{
|
||||
|
||||
/**
|
||||
* @var Error
|
||||
*/
|
||||
static private $instance;
|
||||
|
||||
public static function register(): void
|
||||
{
|
||||
if (!isset(self::$instance)) {
|
||||
self::$instance = new Error();
|
||||
set_error_handler([self::$instance, 'error']);
|
||||
set_exception_handler([self::$instance, 'exception']); // swoole 协程不支持该函数
|
||||
register_shutdown_function([self::$instance, 'shutdown']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $errno
|
||||
* @param $errstr
|
||||
* @param string $errfile
|
||||
* @param int $errline
|
||||
*/
|
||||
public function error($errno, $errstr, $errfile = '', $errline = 0): void
|
||||
{
|
||||
if (error_reporting() & $errno) {
|
||||
// 委托给异常处理
|
||||
$isFatalWarning = function ($errno, $errstr) {
|
||||
if ($errno == E_WARNING && strpos($errstr, 'require') === 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
if ($isFatalWarning($errno, $errstr)) {
|
||||
$this->exception(new \Error(sprintf('%s in %s on line %d', $errstr, $errfile, $errline), $errno));
|
||||
return;
|
||||
}
|
||||
// 转换为异常抛出
|
||||
throw new \Error(sprintf('%s in %s on line %d', $errstr, $errfile, $errline), $errno);
|
||||
}
|
||||
}
|
||||
|
||||
public function shutdown(): void
|
||||
{
|
||||
$isFatal = function ($errno) {
|
||||
return in_array($errno, [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE]);
|
||||
};
|
||||
if (!is_null($error = error_get_last()) && $isFatal($error['type'])) {
|
||||
// 委托给异常处理
|
||||
$this->exception(new \Error(sprintf('%s in %s on line %d', $error['message'], $error['file'], $error['line']), $error['type']));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Throwable $ex
|
||||
*/
|
||||
public function exception(\Throwable $ex): void
|
||||
{
|
||||
Logger::instance()->error(sprintf('%s in %s on line %d', $ex->getMessage(), $ex->getFile(), $ex->getLine()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Throwable $ex
|
||||
*/
|
||||
public static function handle(\Throwable $ex)
|
||||
{
|
||||
self::$instance->exception($ex);
|
||||
}
|
||||
|
||||
}
|
35
examples/api-skeleton/src/Middleware/Auth.php
Normal file
35
examples/api-skeleton/src/Middleware/Auth.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Middleware;
|
||||
|
||||
use Firebase\JWT\JWT;
|
||||
use Mix\Vega\Context;
|
||||
|
||||
/**
|
||||
* Class Auth
|
||||
* @package App\Middleware
|
||||
*/
|
||||
class Auth
|
||||
{
|
||||
|
||||
/**
|
||||
* @return \Closure
|
||||
*/
|
||||
public function middleware(): \Closure
|
||||
{
|
||||
return function (Context $ctx) {
|
||||
try {
|
||||
list(, $token) = explode(' ', $ctx->header('authorization'));
|
||||
$payload = JWT::decode($token, $_ENV['JWT_KEY'], ['HS256']);
|
||||
} catch (\Throwable $e) {
|
||||
$ctx->abortWithStatus(403);
|
||||
}
|
||||
|
||||
// 把 Payload 放入上下文,方便其他位置调用
|
||||
$ctx->set('payload', $payload);
|
||||
|
||||
$ctx->next();
|
||||
};
|
||||
}
|
||||
|
||||
}
|
31
examples/api-skeleton/src/Middleware/Cors.php
Normal file
31
examples/api-skeleton/src/Middleware/Cors.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Middleware;
|
||||
|
||||
use Mix\Vega\Context;
|
||||
|
||||
/**
|
||||
* Class Cors
|
||||
* @package App\Middleware
|
||||
*/
|
||||
class Cors
|
||||
{
|
||||
|
||||
/**
|
||||
* @return \Closure
|
||||
*/
|
||||
public function middleware(): \Closure
|
||||
{
|
||||
return function (Context $ctx) {
|
||||
$ctx->setHeader('Access-Control-Allow-Origin', '*');
|
||||
$ctx->setHeader('Access-Control-Allow-Headers', 'Origin, Accept, Keep-Alive, User-Agent, Cache-Control, Content-Type, X-Requested-With, Authorization');
|
||||
$ctx->setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS');
|
||||
if ($ctx->request->getMethod() == 'OPTIONS') {
|
||||
$ctx->abortWithStatus(200);
|
||||
}
|
||||
|
||||
$ctx->next();
|
||||
};
|
||||
}
|
||||
|
||||
}
|
56
examples/api-skeleton/src/Vega.php
Normal file
56
examples/api-skeleton/src/Vega.php
Normal file
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use App\Container\Logger;
|
||||
use Mix\Vega\Abort;
|
||||
use Mix\Vega\Context;
|
||||
use Mix\Vega\Engine;
|
||||
use Mix\Vega\Exception\NotFoundException;
|
||||
|
||||
class Vega
|
||||
{
|
||||
|
||||
/**
|
||||
* @return Engine
|
||||
*/
|
||||
public static function new(): Engine
|
||||
{
|
||||
$vega = new Engine();
|
||||
|
||||
// 500
|
||||
$vega->use(function (Context $ctx) {
|
||||
try {
|
||||
$ctx->next();
|
||||
} catch (\Throwable $ex) {
|
||||
if ($ex instanceof Abort || $ex instanceof NotFoundException) {
|
||||
throw $ex;
|
||||
}
|
||||
Logger::instance()->error(sprintf('%s in %s on line %d', $ex->getMessage(), $ex->getFile(), $ex->getLine()));
|
||||
$ctx->string(500, 'Internal Server Error');
|
||||
$ctx->abort();
|
||||
}
|
||||
});
|
||||
|
||||
// debug
|
||||
if (APP_DEBUG) {
|
||||
$vega->use(function (Context $ctx) {
|
||||
$ctx->next();
|
||||
Logger::instance()->debug(sprintf(
|
||||
'%s|%s|%s|%s',
|
||||
$ctx->request->getMethod(),
|
||||
$ctx->uri(),
|
||||
$ctx->response->getStatusCode(),
|
||||
$ctx->remoteIP()
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
// routes
|
||||
$routes = require __DIR__ . '/../routes/index.php';
|
||||
$routes($vega);
|
||||
|
||||
return $vega;
|
||||
}
|
||||
|
||||
}
|
38
examples/api-skeleton/src/functions.php
Normal file
38
examples/api-skeleton/src/functions.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
if (!function_exists('env')) {
|
||||
/**
|
||||
* @param string $key
|
||||
* @param null $default
|
||||
* @return array|bool|string|null
|
||||
*/
|
||||
function env(string $key, $default = null)
|
||||
{
|
||||
$value = getenv($key);
|
||||
|
||||
if ($value === false) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
switch (strtolower($value)) {
|
||||
case 'true':
|
||||
case '(true)':
|
||||
return true;
|
||||
case 'false':
|
||||
case '(false)':
|
||||
return false;
|
||||
case 'empty':
|
||||
case '(empty)':
|
||||
return '';
|
||||
case 'null':
|
||||
case '(null)':
|
||||
return null;
|
||||
}
|
||||
|
||||
if (($valueLength = strlen($value)) > 1 && $value[0] === '"' && $value[$valueLength - 1] === '"') {
|
||||
return substr($value, 1, -1);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
13
examples/grpc-skeleton/.env
Normal file
13
examples/grpc-skeleton/.env
Normal file
@ -0,0 +1,13 @@
|
||||
# APP
|
||||
APP_DEBUG=true
|
||||
|
||||
# DATABASE
|
||||
DATABASE_DSN='mysql:host=127.0.0.1;port=3306;charset=utf8;dbname=test'
|
||||
DATABASE_USERNAME=root
|
||||
DATABASE_PASSWORD=123456
|
||||
|
||||
# REDIS
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PORT=6379
|
||||
REDIS_DATABASE=0
|
||||
REDIS_PASSWORD=
|
26
examples/grpc-skeleton/.gitignore
vendored
Normal file
26
examples/grpc-skeleton/.gitignore
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
# phpstorm project files
|
||||
.idea
|
||||
|
||||
# netbeans project files
|
||||
nbproject
|
||||
|
||||
# zend studio for eclipse project files
|
||||
.buildpath
|
||||
.project
|
||||
.settings
|
||||
|
||||
# windows thumbnail cache
|
||||
Thumbs.db
|
||||
|
||||
# composer itself is not needed
|
||||
composer.phar
|
||||
composer.lock
|
||||
vendor/
|
||||
|
||||
# Mac DS_Store Files
|
||||
.DS_Store
|
||||
|
||||
# phpunit itself is not needed
|
||||
phpunit.phar
|
||||
# local phpunit config
|
||||
/phpunit.xml
|
178
examples/grpc-skeleton/README.md
Normal file
178
examples/grpc-skeleton/README.md
Normal file
@ -0,0 +1,178 @@
|
||||
# gRPC development skeleton
|
||||
|
||||
帮助你快速搭建 gRPC 项目骨架,并指导你如何使用该骨架的细节,骨架默认开启了 SQL、Redis 日志,压测前请先关闭 `.env` 的 `APP_DEBUG`
|
||||
|
||||
## 安装
|
||||
|
||||
> 需要先安装 [Swoole](https://wiki.swoole.com/#/environment)
|
||||
|
||||
- Swoole >= 4.4.4: https://wiki.swoole.com/#/environment
|
||||
- 需要开启 `--enable-http2`
|
||||
|
||||
```
|
||||
composer create-project --prefer-dist mix/grpc-skeleton grpc
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
启动 Swoole 多进程服务
|
||||
|
||||
```
|
||||
composer run-script --timeout=0 swoole:start
|
||||
```
|
||||
|
||||
启动 Swoole 协程服务
|
||||
|
||||
```
|
||||
composer run-script --timeout=0 swooleco:start
|
||||
```
|
||||
|
||||
## 执行脚本
|
||||
|
||||
- `composer run-script` 命令中的 `--timeout=0` 参数是防止 composer [执行超时](https://getcomposer.org/doc/06-config.md#process-timeout)
|
||||
- `composer.json` 定义了命令执行脚本,对应上面的执行命令
|
||||
|
||||
```json
|
||||
"scripts": {
|
||||
"swoole:start": "php bin/swoole.php",
|
||||
"swooleco:start": "php bin/swooleco.php",
|
||||
"cli:clearcache": "php bin/cli.php clearcache"
|
||||
},
|
||||
```
|
||||
|
||||
当然也可以直接下面这样启动,效果是一样的,但是 `scripts` 能帮你记录到底有哪些可用的命令,同时在IDE中调试更加方便。
|
||||
|
||||
```
|
||||
php bin/swoole.php start
|
||||
```
|
||||
|
||||
## 部署
|
||||
|
||||
线上部署启动时,修改 `shell/server.sh` 脚本中的绝对路径和参数
|
||||
|
||||
```
|
||||
php=/usr/local/bin/php
|
||||
file=/project/bin/swoole.php
|
||||
cmd=start
|
||||
numprocs=1
|
||||
```
|
||||
|
||||
启动管理
|
||||
|
||||
```
|
||||
sh shell/server.sh start
|
||||
sh shell/server.sh stop
|
||||
sh shell/server.sh restart
|
||||
```
|
||||
|
||||
gRPC 通常都是内部使用,使用内网 `SLB` 代理到服务器IP或者直接使用 IP:PORT 调用
|
||||
|
||||
## 编写一个 gRPC 接口
|
||||
|
||||
首先修改根目录 `.env` 文件的数据库信息
|
||||
|
||||
然后在 `proto` 目录创建 `greeter.proto` 文件,并根据 [使用说明](https://github.com/mix-php/grpc#%E9%80%9A%E8%BF%87-proto-%E7%94%9F%E6%88%90-php-%E4%BB%A3%E7%A0%81) 将 .proto 文件生成为 PHP 代码
|
||||
|
||||
```
|
||||
protoc --php_out=. --mix_out=. greeter.proto
|
||||
```
|
||||
|
||||
然后创建一个新的服务 `src/Service/Say.php`
|
||||
|
||||
- `Say` 类实现了代码生成器生成的 `Php\Micro\Grpc\Greeter\SayInterface` 接口
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use Mix\Grpc\Context;
|
||||
use Php\Micro\Grpc\Greeter\Request;
|
||||
use Php\Micro\Grpc\Greeter\Response;
|
||||
|
||||
/**
|
||||
* Class Say
|
||||
* @package App\Service
|
||||
*/
|
||||
class Say implements \Php\Micro\Grpc\Greeter\SayInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* @param Context $context
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function Hello(Context $context, Request $request): Response
|
||||
{
|
||||
$response = new Response();
|
||||
$response->setMsg(sprintf('hello, %s', $request->getName()));
|
||||
return $response;
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
在 `src/Grpc.php` 中将服务注册到服务器
|
||||
|
||||
```php
|
||||
$server->register(Say::class);
|
||||
```
|
||||
|
||||
重新启动服务器后方可测试新开发的接口
|
||||
|
||||
> 实际开发中使用 PhpStorm 的 Run 功能,只需要点击一下重启按钮即可
|
||||
|
||||
```
|
||||
// 查找进程 PID
|
||||
ps -ef | grep swoole
|
||||
|
||||
// 通过 PID 停止进程
|
||||
kill PID
|
||||
|
||||
// 重新启动进程
|
||||
composer run-script swoole:start
|
||||
```
|
||||
|
||||
## 如何使用 gRPC 客户端
|
||||
|
||||
- [mix-php/grpc#客户端调用一个 gRPC 服务](https://github.com/mix-php/grpc#%E5%AE%A2%E6%88%B7%E7%AB%AF%E8%B0%83%E7%94%A8%E4%B8%80%E4%B8%AA-grpc-%E6%9C%8D%E5%8A%A1)
|
||||
|
||||
## 使用容器中的对象
|
||||
|
||||
容器采用了一个简单的单例模式,你可以修改为更加适合自己的方式。
|
||||
|
||||
- 数据库
|
||||
|
||||
```
|
||||
DB::instance()
|
||||
```
|
||||
|
||||
文档:[mix-php/database](https://github.com/mix-php/database#readme)
|
||||
|
||||
- Redis
|
||||
|
||||
```
|
||||
RDS::instance()
|
||||
```
|
||||
|
||||
文档:[mix-php/redis](https://github.com/mix-php/redis#readme)
|
||||
|
||||
- 日志
|
||||
|
||||
```
|
||||
Logger::instance()
|
||||
```
|
||||
|
||||
文档:[monolog/monolog](https://seldaek.github.io/monolog/doc/01-usage.html)
|
||||
|
||||
- 配置
|
||||
|
||||
```
|
||||
Config::instance()
|
||||
```
|
||||
|
||||
文档:[hassankhan/config](https://github.com/hassankhan/config#getting-values)
|
||||
|
||||
## License
|
||||
|
||||
Apache License Version 2.0, http://www.apache.org/licenses/
|
15
examples/grpc-skeleton/bin/cli.php
Normal file
15
examples/grpc-skeleton/bin/cli.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use Dotenv\Dotenv;
|
||||
|
||||
Dotenv::createUnsafeImmutable(__DIR__ . '/../', '.env')->load();
|
||||
define("APP_DEBUG", env('APP_DEBUG'));
|
||||
|
||||
App\Error::register();
|
||||
|
||||
switch ($argv[1]) {
|
||||
case 'clearcache';
|
||||
(new \App\Command\ClearCache())->exec();
|
||||
break;
|
||||
}
|
37
examples/grpc-skeleton/bin/swoole.php
Normal file
37
examples/grpc-skeleton/bin/swoole.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use App\Container\Logger;
|
||||
use App\Grpc;
|
||||
use Dotenv\Dotenv;
|
||||
|
||||
Dotenv::createUnsafeImmutable(__DIR__ . '/../', '.env')->load();
|
||||
define("APP_DEBUG", env('APP_DEBUG'));
|
||||
|
||||
App\Error::register();
|
||||
|
||||
/**
|
||||
* 多进程默认开启了协程
|
||||
* 关闭协程只需关闭 `enable_coroutine` 配置并注释数据库的 `::enableCoroutine()` 即可退化为多进程同步模式
|
||||
*/
|
||||
|
||||
$grpc = Grpc::new();
|
||||
$http = new Swoole\Http\Server('0.0.0.0', 9501);
|
||||
$http->on('Request', $grpc->handler());
|
||||
$http->on('WorkerStart', function ($server, $workerId) {
|
||||
// swoole 协程不支持 set_exception_handler 需要手动捕获异常
|
||||
try {
|
||||
App\Container\DB::enableCoroutine();
|
||||
App\Container\RDS::enableCoroutine();
|
||||
} catch (\Throwable $ex) {
|
||||
App\Error::handle($ex);
|
||||
}
|
||||
});
|
||||
$http->set([
|
||||
'enable_coroutine' => true,
|
||||
'worker_num' => 4,
|
||||
'open_http2_protocol' => true,
|
||||
'http_compression' => false,
|
||||
]);
|
||||
Logger::instance()->info('Start swoole server');
|
||||
$http->start();
|
34
examples/grpc-skeleton/bin/swooleco.php
Normal file
34
examples/grpc-skeleton/bin/swooleco.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use App\Container\Logger;
|
||||
use App\Grpc;
|
||||
use Dotenv\Dotenv;
|
||||
|
||||
Dotenv::createUnsafeImmutable(__DIR__ . '/../', '.env')->load();
|
||||
define("APP_DEBUG", env('APP_DEBUG'));
|
||||
|
||||
App\Error::register();
|
||||
|
||||
Swoole\Coroutine\run(function () {
|
||||
$grpc = Grpc::new();
|
||||
$server = new Swoole\Coroutine\Http\Server('0.0.0.0', 9502, false, false);
|
||||
$server->handle('/', $grpc->handler());
|
||||
$server->set([
|
||||
'open_http2_protocol' => true,
|
||||
'http_compression' => false,
|
||||
]);
|
||||
|
||||
App\Container\DB::enableCoroutine();
|
||||
App\Container\RDS::enableCoroutine();
|
||||
|
||||
foreach ([SIGHUP, SIGINT, SIGTERM] as $signal) {
|
||||
Swoole\Process::signal($signal, function () use ($server) {
|
||||
Logger::instance()->info('Shutdown swoole coroutine server');
|
||||
$server->shutdown();
|
||||
});
|
||||
}
|
||||
|
||||
Logger::instance()->info('Start swoole coroutine server');
|
||||
$server->start();
|
||||
});
|
35
examples/grpc-skeleton/composer.json
Normal file
35
examples/grpc-skeleton/composer.json
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "mix/grpc-skeleton",
|
||||
"description": "gRPC development skeleton",
|
||||
"type": "project",
|
||||
"homepage": "https://openmix.org/mix-php",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"swoole:start": "php bin/swoole.php",
|
||||
"swooleco:start": "php bin/swooleco.php",
|
||||
"cli:clearcache": "php bin/cli.php clearcache"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "src/",
|
||||
"GPBMetadata\\": "proto/GPBMetadata/",
|
||||
"Php\\": "proto/Php/"
|
||||
},
|
||||
"files": [
|
||||
"src/functions.php"
|
||||
]
|
||||
},
|
||||
"require": {
|
||||
"workerman/workerman": "^4.0",
|
||||
"mix/vega": "~3.0",
|
||||
"mix/grpc": "~3.0",
|
||||
"mix/database": "~3.0",
|
||||
"mix/redis": "~3.0",
|
||||
"vlucas/phpdotenv": "^5.3",
|
||||
"hassankhan/config": "^2.2",
|
||||
"monolog/monolog": "^2.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"swoole/ide-helper": "^4.6"
|
||||
}
|
||||
}
|
3
examples/grpc-skeleton/conf/config.json
Normal file
3
examples/grpc-skeleton/conf/config.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"foo": "bar"
|
||||
}
|
24
examples/grpc-skeleton/proto/GPBMetadata/Greeter.php
Normal file
24
examples/grpc-skeleton/proto/GPBMetadata/Greeter.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: greeter.proto
|
||||
|
||||
namespace GPBMetadata;
|
||||
|
||||
class Greeter
|
||||
{
|
||||
public static $is_initialized = false;
|
||||
|
||||
public static function initOnce() {
|
||||
$pool = \Google\Protobuf\Internal\DescriptorPool::getGeneratedPool();
|
||||
|
||||
if (static::$is_initialized == true) {
|
||||
return;
|
||||
}
|
||||
$pool->internalAddGeneratedFile(hex2bin(
|
||||
"0ab6010a0d677265657465722e70726f746f12167068702e6d6963726f2e677270632e6772656574657222170a0752657175657374120c0a046e616d6518012001280922170a08526573706f6e7365120b0a036d736718012001280932530a03536179124c0a0548656c6c6f121f2e7068702e6d6963726f2e677270632e677265657465722e526571756573741a202e7068702e6d6963726f2e677270632e677265657465722e526573706f6e73652200620670726f746f33"
|
||||
), true);
|
||||
|
||||
static::$is_initialized = true;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: greeter.proto
|
||||
|
||||
namespace Php\Micro\Grpc\Greeter;
|
||||
|
||||
use Google\Protobuf\Internal\GPBType;
|
||||
use Google\Protobuf\Internal\RepeatedField;
|
||||
use Google\Protobuf\Internal\GPBUtil;
|
||||
|
||||
/**
|
||||
* Generated from protobuf message <code>php.micro.grpc.greeter.Request</code>
|
||||
*/
|
||||
class Request extends \Google\Protobuf\Internal\Message
|
||||
{
|
||||
/**
|
||||
* Generated from protobuf field <code>string name = 1;</code>
|
||||
*/
|
||||
protected $name = '';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param array $data {
|
||||
* Optional. Data for populating the Message object.
|
||||
*
|
||||
* @type string $name
|
||||
* }
|
||||
*/
|
||||
public function __construct($data = NULL) {
|
||||
\GPBMetadata\Greeter::initOnce();
|
||||
parent::__construct($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generated from protobuf field <code>string name = 1;</code>
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generated from protobuf field <code>string name = 1;</code>
|
||||
* @param string $var
|
||||
* @return $this
|
||||
*/
|
||||
public function setName($var)
|
||||
{
|
||||
GPBUtil::checkString($var, True);
|
||||
$this->name = $var;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: greeter.proto
|
||||
|
||||
namespace Php\Micro\Grpc\Greeter;
|
||||
|
||||
use Google\Protobuf\Internal\GPBType;
|
||||
use Google\Protobuf\Internal\RepeatedField;
|
||||
use Google\Protobuf\Internal\GPBUtil;
|
||||
|
||||
/**
|
||||
* Generated from protobuf message <code>php.micro.grpc.greeter.Response</code>
|
||||
*/
|
||||
class Response extends \Google\Protobuf\Internal\Message
|
||||
{
|
||||
/**
|
||||
* Generated from protobuf field <code>string msg = 1;</code>
|
||||
*/
|
||||
protected $msg = '';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param array $data {
|
||||
* Optional. Data for populating the Message object.
|
||||
*
|
||||
* @type string $msg
|
||||
* }
|
||||
*/
|
||||
public function __construct($data = NULL) {
|
||||
\GPBMetadata\Greeter::initOnce();
|
||||
parent::__construct($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generated from protobuf field <code>string msg = 1;</code>
|
||||
* @return string
|
||||
*/
|
||||
public function getMsg()
|
||||
{
|
||||
return $this->msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generated from protobuf field <code>string msg = 1;</code>
|
||||
* @param string $var
|
||||
* @return $this
|
||||
*/
|
||||
public function setMsg($var)
|
||||
{
|
||||
GPBUtil::checkString($var, True);
|
||||
$this->msg = $var;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
# Generated by the protocol buffer compiler (https://github.com/mix-php/grpc). DO NOT EDIT!
|
||||
# source: greeter.proto
|
||||
|
||||
namespace Php\Micro\Grpc\Greeter;
|
||||
|
||||
use Mix\Grpc;
|
||||
use Mix\Grpc\Context;
|
||||
|
||||
class SayClient extends Grpc\Client\AbstractClient
|
||||
{
|
||||
/**
|
||||
* @param Context $context
|
||||
* @param Request $request
|
||||
* @param array $options
|
||||
* @return Response
|
||||
*
|
||||
* @throws Grpc\Exception\RuntimeException
|
||||
*/
|
||||
public function Hello(Context $context, Request $request): Response
|
||||
{
|
||||
return $this->_simpleRequest('/php.micro.grpc.greeter.Say/Hello', $context, $request, new Response());
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
# Generated by the protocol buffer compiler (https://github.com/mix-php/grpc). DO NOT EDIT!
|
||||
# source: greeter.proto
|
||||
|
||||
namespace Php\Micro\Grpc\Greeter;
|
||||
|
||||
use Mix\Grpc;
|
||||
use Mix\Grpc\Context;
|
||||
|
||||
interface SayInterface extends Grpc\ServiceInterface
|
||||
{
|
||||
// GRPC specific service name.
|
||||
public const NAME = "php.micro.grpc.greeter.Say";
|
||||
|
||||
/**
|
||||
* @param Context $context
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*
|
||||
* @throws Grpc\Exception\RuntimeException
|
||||
*/
|
||||
public function Hello(Context $context, Request $request): Response;
|
||||
}
|
15
examples/grpc-skeleton/proto/greeter.proto
Normal file
15
examples/grpc-skeleton/proto/greeter.proto
Normal file
@ -0,0 +1,15 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package php.micro.grpc.greeter;
|
||||
|
||||
service Say {
|
||||
rpc Hello(Request) returns (Response) {}
|
||||
}
|
||||
|
||||
message Request {
|
||||
string name = 1;
|
||||
}
|
||||
|
||||
message Response {
|
||||
string msg = 1;
|
||||
}
|
2
examples/grpc-skeleton/runtime/.gitignore
vendored
Normal file
2
examples/grpc-skeleton/runtime/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
75
examples/grpc-skeleton/shell/server.sh
Normal file
75
examples/grpc-skeleton/shell/server.sh
Normal file
@ -0,0 +1,75 @@
|
||||
#!/bin/sh
|
||||
echo "============`date +%F' '%T`==========="
|
||||
|
||||
php=/usr/local/bin/php
|
||||
file=/project/bin/swoole.php
|
||||
cmd=start
|
||||
numprocs=1
|
||||
|
||||
getpid()
|
||||
{
|
||||
docmd=`ps aux | grep ${file} | grep ${cmd} | grep -v 'grep' | grep -v '\.sh' | awk '{print $2}' | xargs`
|
||||
echo $docmd
|
||||
}
|
||||
|
||||
getmpid()
|
||||
{
|
||||
docmd=`ps -ef | grep ${file} | grep ${cmd} | grep -v 'grep' | grep -v '\.sh' | grep ' 1 ' | awk '{print $2}' | xargs`
|
||||
echo $docmd
|
||||
}
|
||||
|
||||
start()
|
||||
{
|
||||
pidstr=`getpid`
|
||||
if [ -n "$pidstr" ];then
|
||||
echo "running with pids $pidstr"
|
||||
else
|
||||
if [ $numprocs -eq 1 ];then
|
||||
$php $file $cmd > /dev/null 2>&1 &
|
||||
else
|
||||
i=0
|
||||
while(( $i<$numprocs ))
|
||||
do
|
||||
$php $file $cmd > /dev/null 2>&1 &
|
||||
let "i++"
|
||||
done
|
||||
fi
|
||||
sleep 1
|
||||
pidstr=`getpid`
|
||||
echo "start with pids $pidstr"
|
||||
fi
|
||||
}
|
||||
|
||||
stop()
|
||||
{
|
||||
pidstr=`getpid`
|
||||
if [ ! -n "$pidstr" ];then
|
||||
echo "not executed!"
|
||||
return
|
||||
fi
|
||||
mpidstr=`getmpid`
|
||||
if [ -n "$mpidstr" ];then
|
||||
pidstr=$mpidstr
|
||||
fi
|
||||
echo "kill $pidstr"
|
||||
kill $pidstr
|
||||
}
|
||||
|
||||
restart()
|
||||
{
|
||||
stop
|
||||
sleep 1
|
||||
start
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
start
|
||||
;;
|
||||
stop)
|
||||
stop
|
||||
;;
|
||||
restart)
|
||||
restart
|
||||
;;
|
||||
esac
|
20
examples/grpc-skeleton/src/Command/ClearCache.php
Normal file
20
examples/grpc-skeleton/src/Command/ClearCache.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Container\RDS;
|
||||
|
||||
/**
|
||||
* Class ClearCache
|
||||
* @package App\Command
|
||||
*/
|
||||
class ClearCache
|
||||
{
|
||||
|
||||
public function exec(): void
|
||||
{
|
||||
RDS::instance()->del('foo_cache');
|
||||
print 'ok';
|
||||
}
|
||||
|
||||
}
|
28
examples/grpc-skeleton/src/Container/Config.php
Normal file
28
examples/grpc-skeleton/src/Container/Config.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Container;
|
||||
|
||||
/**
|
||||
* Class Config
|
||||
* @package App\Container
|
||||
*/
|
||||
class Config
|
||||
{
|
||||
|
||||
/**
|
||||
* @var \Noodlehaus\Config
|
||||
*/
|
||||
static private $instance;
|
||||
|
||||
/**
|
||||
* @return \Noodlehaus\Config
|
||||
*/
|
||||
public static function instance(): \Noodlehaus\Config
|
||||
{
|
||||
if (!isset(self::$instance)) {
|
||||
self::$instance = new \Noodlehaus\Config(__DIR__ . '/../../conf');
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
}
|
41
examples/grpc-skeleton/src/Container/DB.php
Normal file
41
examples/grpc-skeleton/src/Container/DB.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Container;
|
||||
|
||||
use Mix\Database\Database;
|
||||
|
||||
class DB
|
||||
{
|
||||
|
||||
/**
|
||||
* @var Database
|
||||
*/
|
||||
static private $instance;
|
||||
|
||||
/**
|
||||
* @return Database
|
||||
*/
|
||||
public static function instance(): Database
|
||||
{
|
||||
if (!isset(self::$instance)) {
|
||||
$dsn = $_ENV['DATABASE_DSN'];
|
||||
$username = $_ENV['DATABASE_USERNAME'];
|
||||
$password = $_ENV['DATABASE_PASSWORD'];
|
||||
$db = new Database($dsn, $username, $password);
|
||||
APP_DEBUG and $db->setLogger(new DBLogger());
|
||||
self::$instance = $db;
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public static function enableCoroutine()
|
||||
{
|
||||
$maxOpen = 30; // 最大开启连接数
|
||||
$maxIdle = 10; // 最大闲置连接数
|
||||
$maxLifetime = 3600; // 连接的最长生命周期
|
||||
$waitTimeout = 0.0; // 从池获取连接等待的时间, 0为一直等待
|
||||
self::instance()->startPool($maxOpen, $maxIdle, $maxLifetime, $waitTimeout);
|
||||
\Swoole\Runtime::enableCoroutine(); // 必须放到最后,防止触发协程调度导致异常
|
||||
}
|
||||
|
||||
}
|
17
examples/grpc-skeleton/src/Container/DBLogger.php
Normal file
17
examples/grpc-skeleton/src/Container/DBLogger.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Container;
|
||||
|
||||
/**
|
||||
* Class DBLogger
|
||||
* @package App\Container
|
||||
*/
|
||||
class DBLogger implements \Mix\Database\LoggerInterface
|
||||
{
|
||||
|
||||
public function trace(float $time, string $sql, array $bindings, int $rowCount, ?\Throwable $exception): void
|
||||
{
|
||||
Logger::instance()->debug(sprintf('SQL: %sms %s %s %d', $time, $sql, json_encode($bindings), $rowCount));
|
||||
}
|
||||
|
||||
}
|
61
examples/grpc-skeleton/src/Container/Logger.php
Normal file
61
examples/grpc-skeleton/src/Container/Logger.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Container;
|
||||
|
||||
use Monolog\Formatter\LineFormatter;
|
||||
use Monolog\Handler\HandlerInterface;
|
||||
use Monolog\Handler\RotatingFileHandler;
|
||||
|
||||
/**
|
||||
* Class Logger
|
||||
* @package App\Container
|
||||
*/
|
||||
class Logger implements HandlerInterface
|
||||
{
|
||||
|
||||
static private $instance;
|
||||
|
||||
/**
|
||||
* @return \Monolog\Logger
|
||||
*/
|
||||
public static function instance(): \Monolog\Logger
|
||||
{
|
||||
if (!isset(self::$instance)) {
|
||||
$logger = new \Monolog\Logger('MIX');
|
||||
$rotatingFileHandler = new RotatingFileHandler(__DIR__ . '/../../runtime/logs/mix.log', 7);
|
||||
$rotatingFileHandler->setFormatter(new LineFormatter("[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n", 'Y-m-d H:i:s.u'));
|
||||
$logger->pushHandler($rotatingFileHandler);
|
||||
$logger->pushHandler(new Logger());
|
||||
self::$instance = $logger;
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function isHandling(array $record): bool
|
||||
{
|
||||
return $record['level'] >= \Monolog\Logger::DEBUG;
|
||||
}
|
||||
|
||||
public function handle(array $record): bool
|
||||
{
|
||||
$message = sprintf("%s %s %s\n", $record['datetime']->format('Y-m-d H:i:s.u'), $record['level_name'], $record['message']);
|
||||
switch (PHP_SAPI) {
|
||||
case 'cli':
|
||||
case 'cli-server':
|
||||
file_put_contents("php://stdout", $message);
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function handleBatch(array $records): void
|
||||
{
|
||||
// TODO: Implement handleBatch() method.
|
||||
}
|
||||
|
||||
public function close(): void
|
||||
{
|
||||
// TODO: Implement close() method.
|
||||
}
|
||||
|
||||
}
|
42
examples/grpc-skeleton/src/Container/RDS.php
Normal file
42
examples/grpc-skeleton/src/Container/RDS.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Container;
|
||||
|
||||
use Mix\Redis\Redis;
|
||||
|
||||
class RDS
|
||||
{
|
||||
|
||||
/**
|
||||
* @var Redis
|
||||
*/
|
||||
static private $instance;
|
||||
|
||||
/**
|
||||
* @return Redis
|
||||
*/
|
||||
public static function instance(): Redis
|
||||
{
|
||||
if (!isset(self::$instance)) {
|
||||
$host = $_ENV['REDIS_HOST'];
|
||||
$port = $_ENV['REDIS_PORT'];
|
||||
$password = $_ENV['REDIS_PASSWORD'];
|
||||
$database = $_ENV['REDIS_DATABASE'];
|
||||
$rds = new Redis($host, $port, $password, $database);
|
||||
APP_DEBUG and $rds->setLogger(new RDSLogger());
|
||||
self::$instance = $rds;
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public static function enableCoroutine()
|
||||
{
|
||||
$maxOpen = 30; // 最大开启连接数
|
||||
$maxIdle = 10; // 最大闲置连接数
|
||||
$maxLifetime = 3600; // 连接的最长生命周期
|
||||
$waitTimeout = 0.0; // 从池获取连接等待的时间, 0为一直等待
|
||||
self::instance()->startPool($maxOpen, $maxIdle, $maxLifetime, $waitTimeout);
|
||||
\Swoole\Runtime::enableCoroutine(); // 必须放到最后,防止触发协程调度导致异常
|
||||
}
|
||||
|
||||
}
|
17
examples/grpc-skeleton/src/Container/RDSLogger.php
Normal file
17
examples/grpc-skeleton/src/Container/RDSLogger.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Container;
|
||||
|
||||
/**
|
||||
* Class RDSLogger
|
||||
* @package App\Container
|
||||
*/
|
||||
class RDSLogger implements \Mix\Redis\LoggerInterface
|
||||
{
|
||||
|
||||
public function trace(float $time, string $cmd, array $args, ?\Throwable $exception): void
|
||||
{
|
||||
Logger::instance()->debug(sprintf('RDS: %sms %s %s', $time, $cmd, json_encode($args)));
|
||||
}
|
||||
|
||||
}
|
81
examples/grpc-skeleton/src/Error.php
Normal file
81
examples/grpc-skeleton/src/Error.php
Normal file
@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use App\Container\Logger;
|
||||
|
||||
/**
|
||||
* Class Error
|
||||
* @package App
|
||||
*/
|
||||
class Error
|
||||
{
|
||||
|
||||
/**
|
||||
* @var Error
|
||||
*/
|
||||
static private $instance;
|
||||
|
||||
public static function register(): void
|
||||
{
|
||||
if (!isset(self::$instance)) {
|
||||
self::$instance = new Error();
|
||||
set_error_handler([self::$instance, 'error']);
|
||||
set_exception_handler([self::$instance, 'exception']); // swoole 协程不支持该函数
|
||||
register_shutdown_function([self::$instance, 'shutdown']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $errno
|
||||
* @param $errstr
|
||||
* @param string $errfile
|
||||
* @param int $errline
|
||||
*/
|
||||
public function error($errno, $errstr, $errfile = '', $errline = 0): void
|
||||
{
|
||||
if (error_reporting() & $errno) {
|
||||
// 委托给异常处理
|
||||
$isFatalWarning = function ($errno, $errstr) {
|
||||
if ($errno == E_WARNING && strpos($errstr, 'require') === 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
if ($isFatalWarning($errno, $errstr)) {
|
||||
$this->exception(new \Error(sprintf('%s in %s on line %d', $errstr, $errfile, $errline), $errno));
|
||||
return;
|
||||
}
|
||||
// 转换为异常抛出
|
||||
throw new \Error(sprintf('%s in %s on line %d', $errstr, $errfile, $errline), $errno);
|
||||
}
|
||||
}
|
||||
|
||||
public function shutdown(): void
|
||||
{
|
||||
$isFatal = function ($errno) {
|
||||
return in_array($errno, [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE]);
|
||||
};
|
||||
if (!is_null($error = error_get_last()) && $isFatal($error['type'])) {
|
||||
// 委托给异常处理
|
||||
$this->exception(new \Error(sprintf('%s in %s on line %d', $error['message'], $error['file'], $error['line']), $error['type']));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Throwable $ex
|
||||
*/
|
||||
public function exception(\Throwable $ex): void
|
||||
{
|
||||
Logger::instance()->error(sprintf('%s in %s on line %d', $ex->getMessage(), $ex->getFile(), $ex->getLine()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Throwable $ex
|
||||
*/
|
||||
public static function handle(\Throwable $ex)
|
||||
{
|
||||
self::$instance->exception($ex);
|
||||
}
|
||||
|
||||
}
|
25
examples/grpc-skeleton/src/Grpc.php
Normal file
25
examples/grpc-skeleton/src/Grpc.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use App\Service\Say;
|
||||
use Mix\Grpc\Server;
|
||||
|
||||
/**
|
||||
* Class Grpc
|
||||
* @package App
|
||||
*/
|
||||
class Grpc
|
||||
{
|
||||
|
||||
/**
|
||||
* @return Server
|
||||
*/
|
||||
public static function new(): Server
|
||||
{
|
||||
$server = new Server();
|
||||
$server->register(Say::class);
|
||||
return $server;
|
||||
}
|
||||
|
||||
}
|
28
examples/grpc-skeleton/src/Service/Say.php
Normal file
28
examples/grpc-skeleton/src/Service/Say.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use Mix\Grpc\Context;
|
||||
use Php\Micro\Grpc\Greeter\Request;
|
||||
use Php\Micro\Grpc\Greeter\Response;
|
||||
|
||||
/**
|
||||
* Class Say
|
||||
* @package App\Service
|
||||
*/
|
||||
class Say implements \Php\Micro\Grpc\Greeter\SayInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* @param Context $context
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function Hello(Context $context, Request $request): Response
|
||||
{
|
||||
$response = new Response();
|
||||
$response->setMsg(sprintf('hello, %s', $request->getName()));
|
||||
return $response;
|
||||
}
|
||||
|
||||
}
|
38
examples/grpc-skeleton/src/functions.php
Normal file
38
examples/grpc-skeleton/src/functions.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
if (!function_exists('env')) {
|
||||
/**
|
||||
* @param string $key
|
||||
* @param null $default
|
||||
* @return array|bool|string|null
|
||||
*/
|
||||
function env(string $key, $default = null)
|
||||
{
|
||||
$value = getenv($key);
|
||||
|
||||
if ($value === false) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
switch (strtolower($value)) {
|
||||
case 'true':
|
||||
case '(true)':
|
||||
return true;
|
||||
case 'false':
|
||||
case '(false)':
|
||||
return false;
|
||||
case 'empty':
|
||||
case '(empty)':
|
||||
return '';
|
||||
case 'null':
|
||||
case '(null)':
|
||||
return null;
|
||||
}
|
||||
|
||||
if (($valueLength = strlen($value)) > 1 && $value[0] === '"' && $value[$valueLength - 1] === '"') {
|
||||
return substr($value, 1, -1);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
13
examples/web-skeleton/.env
Normal file
13
examples/web-skeleton/.env
Normal file
@ -0,0 +1,13 @@
|
||||
# APP
|
||||
APP_DEBUG=true
|
||||
|
||||
# DATABASE
|
||||
DATABASE_DSN='mysql:host=127.0.0.1;port=3306;charset=utf8;dbname=test'
|
||||
DATABASE_USERNAME=root
|
||||
DATABASE_PASSWORD=123456
|
||||
|
||||
# REDIS
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PORT=6379
|
||||
REDIS_DATABASE=0
|
||||
REDIS_PASSWORD=
|
26
examples/web-skeleton/.gitignore
vendored
Normal file
26
examples/web-skeleton/.gitignore
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
# phpstorm project files
|
||||
.idea
|
||||
|
||||
# netbeans project files
|
||||
nbproject
|
||||
|
||||
# zend studio for eclipse project files
|
||||
.buildpath
|
||||
.project
|
||||
.settings
|
||||
|
||||
# windows thumbnail cache
|
||||
Thumbs.db
|
||||
|
||||
# composer itself is not needed
|
||||
composer.phar
|
||||
composer.lock
|
||||
vendor/
|
||||
|
||||
# Mac DS_Store Files
|
||||
.DS_Store
|
||||
|
||||
# phpunit itself is not needed
|
||||
phpunit.phar
|
||||
# local phpunit config
|
||||
/phpunit.xml
|
233
examples/web-skeleton/README.md
Normal file
233
examples/web-skeleton/README.md
Normal file
@ -0,0 +1,233 @@
|
||||
# Web development skeleton
|
||||
|
||||
帮助你快速搭建 Web 项目骨架,并指导你如何使用该骨架的细节,骨架默认开启了 SQL、Redis 日志,压测前请先关闭 `.env` 的 `APP_DEBUG`
|
||||
|
||||
## 安装
|
||||
|
||||
> 需要先安装 [Swoole](https://wiki.swoole.com/#/environment) 或者 [WorkerMan](http://doc.workerman.net/install/requirement.html)
|
||||
|
||||
```
|
||||
composer create-project --prefer-dist mix/web-skeleton web
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
启动 [cli-server](https://www.php.net/manual/zh/features.commandline.webserver.php) 开发服务
|
||||
|
||||
```
|
||||
composer run-script --timeout=0 cliserver:start
|
||||
```
|
||||
|
||||
启动 Swoole 多进程服务
|
||||
|
||||
```
|
||||
composer run-script --timeout=0 swoole:start
|
||||
```
|
||||
|
||||
启动 Swoole 协程服务
|
||||
|
||||
```
|
||||
composer run-script --timeout=0 swooleco:start
|
||||
```
|
||||
|
||||
启动 WorkerMan 多进程服务
|
||||
|
||||
```
|
||||
composer run-script --timeout=0 workerman:start
|
||||
```
|
||||
|
||||
## 执行脚本
|
||||
|
||||
- `composer run-script` 命令中的 `--timeout=0` 参数是防止 composer [执行超时](https://getcomposer.org/doc/06-config.md#process-timeout)
|
||||
- `composer.json` 定义了命令执行脚本,对应上面的执行命令
|
||||
|
||||
```json
|
||||
"scripts": {
|
||||
"cliserver:start": "php -S localhost:8000 -t public",
|
||||
"swoole:start": "php bin/swoole.php",
|
||||
"swooleco:start": "php bin/swooleco.php",
|
||||
"workerman:start": "php bin/workerman.php start",
|
||||
"cli:clearcache": "php bin/cli.php clearcache"
|
||||
},
|
||||
```
|
||||
|
||||
当然也可以直接下面这样启动,效果是一样的,但是 `scripts` 能帮你记录到底有哪些可用的命令,同时在IDE中调试更加方便。
|
||||
|
||||
```
|
||||
php bin/swoole.php start
|
||||
```
|
||||
|
||||
## 部署
|
||||
|
||||
- CLI
|
||||
|
||||
线上部署启动时,修改 `shell/server.sh` 脚本中的绝对路径和参数
|
||||
|
||||
```
|
||||
php=/usr/local/bin/php
|
||||
file=/project/bin/swoole.php
|
||||
cmd=start
|
||||
numprocs=1
|
||||
```
|
||||
|
||||
启动管理
|
||||
|
||||
```
|
||||
sh shell/server.sh start
|
||||
sh shell/server.sh stop
|
||||
sh shell/server.sh restart
|
||||
```
|
||||
|
||||
使用 `nginx` 或者 `SLB` 代理到服务器端口即可
|
||||
|
||||
```
|
||||
server {
|
||||
server_name www.domain.com;
|
||||
listen 80;
|
||||
root /data/project/public;
|
||||
|
||||
location / {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "keep-alive";
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header Scheme $scheme;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
if (!-f $request_filename) {
|
||||
proxy_pass http://127.0.0.1:9501;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- PHP-FPM
|
||||
|
||||
和 Laravel、ThinkPHP 部署方法完全一致,将 `public/index.php` 在 `nginx` 配置 `rewrite` 重写即可
|
||||
|
||||
```
|
||||
server {
|
||||
server_name www.domain.com;
|
||||
listen 80;
|
||||
root /data/project/public;
|
||||
index index.html index.php;
|
||||
|
||||
location / {
|
||||
if (!-e $request_filename) {
|
||||
rewrite ^/(.*)$ /index.php/$1 last;
|
||||
}
|
||||
}
|
||||
|
||||
location ~ ^(.+\.php)(.*)$ {
|
||||
fastcgi_pass 127.0.0.1:9000;
|
||||
fastcgi_split_path_info ^(.+\.php)(.*)$;
|
||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
include fastcgi_params;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 编写一个 Web 页面
|
||||
|
||||
首先修改根目录 `.env` 文件的数据库信息
|
||||
|
||||
然后在 `routes/index.php` 定义一个新的路由
|
||||
|
||||
```php
|
||||
$vega->handle('/', [new Hello(), 'index'])->methods('GET');
|
||||
```
|
||||
|
||||
路由里使用了 `Hello` 控制器,我们需要创建他
|
||||
|
||||
- 如何配置路由:[mix-php/vega](https://github.com/mix-php/vega#readme)
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use Mix\Vega\Context;
|
||||
|
||||
class Hello
|
||||
{
|
||||
|
||||
/**
|
||||
* @param Context $ctx
|
||||
*/
|
||||
public function index(Context $ctx)
|
||||
{
|
||||
$ctx->HTML(200, 'index', [
|
||||
'title' => 'Hello, World!'
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
在 `views` 目录创建 `index.php` 视图文件
|
||||
|
||||
```php
|
||||
<html>
|
||||
<h1>
|
||||
<?= $title ?>
|
||||
</h1>
|
||||
</html>
|
||||
```
|
||||
|
||||
重新启动服务器后方可测试新开发的接口
|
||||
|
||||
> 实际开发中使用 PhpStorm 的 Run 功能,只需要点击一下重启按钮即可
|
||||
|
||||
```
|
||||
// 查找进程 PID
|
||||
ps -ef | grep swoole
|
||||
|
||||
// 通过 PID 停止进程
|
||||
kill PID
|
||||
|
||||
// 重新启动进程
|
||||
composer run-script swoole:start
|
||||
|
||||
// curl 测试
|
||||
curl http://127.0.0.1:9501/
|
||||
```
|
||||
|
||||
## 使用容器中的对象
|
||||
|
||||
容器采用了一个简单的单例模式,你可以修改为更加适合自己的方式。
|
||||
|
||||
- 数据库
|
||||
|
||||
```
|
||||
DB::instance()
|
||||
```
|
||||
|
||||
文档:[mix-php/database](https://github.com/mix-php/database#readme)
|
||||
|
||||
- Redis
|
||||
|
||||
```
|
||||
RDS::instance()
|
||||
```
|
||||
|
||||
文档:[mix-php/redis](https://github.com/mix-php/redis#readme)
|
||||
|
||||
- 日志
|
||||
|
||||
```
|
||||
Logger::instance()
|
||||
```
|
||||
|
||||
文档:[monolog/monolog](https://seldaek.github.io/monolog/doc/01-usage.html)
|
||||
|
||||
- 配置
|
||||
|
||||
```
|
||||
Config::instance()
|
||||
```
|
||||
|
||||
文档:[hassankhan/config](https://github.com/hassankhan/config#getting-values)
|
||||
|
||||
## License
|
||||
|
||||
Apache License Version 2.0, http://www.apache.org/licenses/
|
15
examples/web-skeleton/bin/cli.php
Normal file
15
examples/web-skeleton/bin/cli.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use Dotenv\Dotenv;
|
||||
|
||||
Dotenv::createUnsafeImmutable(__DIR__ . '/../', '.env')->load();
|
||||
define("APP_DEBUG", env('APP_DEBUG'));
|
||||
|
||||
App\Error::register();
|
||||
|
||||
switch ($argv[1]) {
|
||||
case 'clearcache';
|
||||
(new \App\Command\ClearCache())->exec();
|
||||
break;
|
||||
}
|
35
examples/web-skeleton/bin/swoole.php
Normal file
35
examples/web-skeleton/bin/swoole.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use App\Container\Logger;
|
||||
use App\Vega;
|
||||
use Dotenv\Dotenv;
|
||||
|
||||
Dotenv::createUnsafeImmutable(__DIR__ . '/../', '.env')->load();
|
||||
define("APP_DEBUG", env('APP_DEBUG'));
|
||||
|
||||
App\Error::register();
|
||||
|
||||
/**
|
||||
* 多进程默认开启了协程
|
||||
* 关闭协程只需关闭 `enable_coroutine` 配置并注释数据库的 `::enableCoroutine()` 即可退化为多进程同步模式
|
||||
*/
|
||||
|
||||
$vega = Vega::new();
|
||||
$http = new Swoole\Http\Server('0.0.0.0', 9501);
|
||||
$http->on('Request', $vega->handler());
|
||||
$http->on('WorkerStart', function ($server, $workerId) {
|
||||
// swoole 协程不支持 set_exception_handler 需要手动捕获异常
|
||||
try {
|
||||
App\Container\DB::enableCoroutine();
|
||||
App\Container\RDS::enableCoroutine();
|
||||
} catch (\Throwable $ex) {
|
||||
App\Error::handle($ex);
|
||||
}
|
||||
});
|
||||
$http->set([
|
||||
'enable_coroutine' => true,
|
||||
'worker_num' => 4,
|
||||
]);
|
||||
Logger::instance()->info('Start swoole server');
|
||||
$http->start();
|
30
examples/web-skeleton/bin/swooleco.php
Normal file
30
examples/web-skeleton/bin/swooleco.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use App\Container\Logger;
|
||||
use App\Vega;
|
||||
use Dotenv\Dotenv;
|
||||
|
||||
Dotenv::createUnsafeImmutable(__DIR__ . '/../', '.env')->load();
|
||||
define("APP_DEBUG", env('APP_DEBUG'));
|
||||
|
||||
App\Error::register();
|
||||
|
||||
Swoole\Coroutine\run(function () {
|
||||
$vega = Vega::new();
|
||||
$server = new Swoole\Coroutine\Http\Server('0.0.0.0', 9502, false, false);
|
||||
$server->handle('/', $vega->handler());
|
||||
|
||||
App\Container\DB::enableCoroutine();
|
||||
App\Container\RDS::enableCoroutine();
|
||||
|
||||
foreach ([SIGHUP, SIGINT, SIGTERM] as $signal) {
|
||||
Swoole\Process::signal($signal, function () use ($server) {
|
||||
Logger::instance()->info('Shutdown swoole coroutine server');
|
||||
$server->shutdown();
|
||||
});
|
||||
}
|
||||
|
||||
Logger::instance()->info('Start swoole coroutine server');
|
||||
$server->start();
|
||||
});
|
18
examples/web-skeleton/bin/workerman.php
Normal file
18
examples/web-skeleton/bin/workerman.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use App\Container\Logger;
|
||||
use App\Vega;
|
||||
use Dotenv\Dotenv;
|
||||
|
||||
Dotenv::createUnsafeImmutable(__DIR__ . '/../', '.env')->load();
|
||||
define("APP_DEBUG", env('APP_DEBUG'));
|
||||
|
||||
App\Error::register();
|
||||
|
||||
$vega = Vega::new();
|
||||
$http = new Workerman\Worker("http://0.0.0.0:2345");
|
||||
$http->onMessage = $vega->handler();
|
||||
$http->count = 4;
|
||||
Logger::instance()->info('Start workerman server');
|
||||
Workerman\Worker::runAll();
|
34
examples/web-skeleton/composer.json
Normal file
34
examples/web-skeleton/composer.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "mix/web-skeleton",
|
||||
"description": "Web development skeleton",
|
||||
"type": "project",
|
||||
"homepage": "https://openmix.org/mix-php",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"cliserver:start": "php -S localhost:8000 -t public",
|
||||
"swoole:start": "php bin/swoole.php",
|
||||
"swooleco:start": "php bin/swooleco.php",
|
||||
"workerman:start": "php bin/workerman.php start",
|
||||
"cli:clearcache": "php bin/cli.php clearcache"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "src/"
|
||||
},
|
||||
"files": [
|
||||
"src/functions.php"
|
||||
]
|
||||
},
|
||||
"require": {
|
||||
"workerman/workerman": "^4.0",
|
||||
"mix/vega": "~3.0",
|
||||
"mix/database": "~3.0",
|
||||
"mix/redis": "~3.0",
|
||||
"vlucas/phpdotenv": "^5.3",
|
||||
"hassankhan/config": "^2.2",
|
||||
"monolog/monolog": "^2.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"swoole/ide-helper": "^4.6"
|
||||
}
|
||||
}
|
3
examples/web-skeleton/conf/config.json
Normal file
3
examples/web-skeleton/conf/config.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"foo": "bar"
|
||||
}
|
BIN
examples/web-skeleton/public/favicon.ico
Normal file
BIN
examples/web-skeleton/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
16
examples/web-skeleton/public/index.php
Normal file
16
examples/web-skeleton/public/index.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
/**
|
||||
* PHP-FPM, cli-server 模式专用
|
||||
*/
|
||||
|
||||
use App\Vega;
|
||||
use Dotenv\Dotenv;
|
||||
|
||||
Dotenv::createUnsafeImmutable(__DIR__ . '/../', '.env')->load();
|
||||
define("APP_DEBUG", env('APP_DEBUG'));
|
||||
|
||||
App\Error::register();
|
||||
|
||||
return Vega::new()->run();
|
0
examples/web-skeleton/public/static/index.html
Normal file
0
examples/web-skeleton/public/static/index.html
Normal file
7
examples/web-skeleton/routes/index.php
Normal file
7
examples/web-skeleton/routes/index.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Controller\Hello;
|
||||
|
||||
return function (Mix\Vega\Engine $vega) {
|
||||
$vega->handle('/', [new Hello(), 'index'])->methods('GET');
|
||||
};
|
2
examples/web-skeleton/runtime/.gitignore
vendored
Normal file
2
examples/web-skeleton/runtime/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
75
examples/web-skeleton/shell/server.sh
Normal file
75
examples/web-skeleton/shell/server.sh
Normal file
@ -0,0 +1,75 @@
|
||||
#!/bin/sh
|
||||
echo "============`date +%F' '%T`==========="
|
||||
|
||||
php=/usr/local/bin/php
|
||||
file=/project/bin/swoole.php
|
||||
cmd=start
|
||||
numprocs=1
|
||||
|
||||
getpid()
|
||||
{
|
||||
docmd=`ps aux | grep ${file} | grep ${cmd} | grep -v 'grep' | grep -v '\.sh' | awk '{print $2}' | xargs`
|
||||
echo $docmd
|
||||
}
|
||||
|
||||
getmpid()
|
||||
{
|
||||
docmd=`ps -ef | grep ${file} | grep ${cmd} | grep -v 'grep' | grep -v '\.sh' | grep ' 1 ' | awk '{print $2}' | xargs`
|
||||
echo $docmd
|
||||
}
|
||||
|
||||
start()
|
||||
{
|
||||
pidstr=`getpid`
|
||||
if [ -n "$pidstr" ];then
|
||||
echo "running with pids $pidstr"
|
||||
else
|
||||
if [ $numprocs -eq 1 ];then
|
||||
$php $file $cmd > /dev/null 2>&1 &
|
||||
else
|
||||
i=0
|
||||
while(( $i<$numprocs ))
|
||||
do
|
||||
$php $file $cmd > /dev/null 2>&1 &
|
||||
let "i++"
|
||||
done
|
||||
fi
|
||||
sleep 1
|
||||
pidstr=`getpid`
|
||||
echo "start with pids $pidstr"
|
||||
fi
|
||||
}
|
||||
|
||||
stop()
|
||||
{
|
||||
pidstr=`getpid`
|
||||
if [ ! -n "$pidstr" ];then
|
||||
echo "not executed!"
|
||||
return
|
||||
fi
|
||||
mpidstr=`getmpid`
|
||||
if [ -n "$mpidstr" ];then
|
||||
pidstr=$mpidstr
|
||||
fi
|
||||
echo "kill $pidstr"
|
||||
kill $pidstr
|
||||
}
|
||||
|
||||
restart()
|
||||
{
|
||||
stop
|
||||
sleep 1
|
||||
start
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
start
|
||||
;;
|
||||
stop)
|
||||
stop
|
||||
;;
|
||||
restart)
|
||||
restart
|
||||
;;
|
||||
esac
|
20
examples/web-skeleton/src/Command/ClearCache.php
Normal file
20
examples/web-skeleton/src/Command/ClearCache.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Container\RDS;
|
||||
|
||||
/**
|
||||
* Class ClearCache
|
||||
* @package App\Command
|
||||
*/
|
||||
class ClearCache
|
||||
{
|
||||
|
||||
public function exec(): void
|
||||
{
|
||||
RDS::instance()->del('foo_cache');
|
||||
print 'ok';
|
||||
}
|
||||
|
||||
}
|
28
examples/web-skeleton/src/Container/Config.php
Normal file
28
examples/web-skeleton/src/Container/Config.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Container;
|
||||
|
||||
/**
|
||||
* Class Config
|
||||
* @package App\Container
|
||||
*/
|
||||
class Config
|
||||
{
|
||||
|
||||
/**
|
||||
* @var \Noodlehaus\Config
|
||||
*/
|
||||
static private $instance;
|
||||
|
||||
/**
|
||||
* @return \Noodlehaus\Config
|
||||
*/
|
||||
public static function instance(): \Noodlehaus\Config
|
||||
{
|
||||
if (!isset(self::$instance)) {
|
||||
self::$instance = new \Noodlehaus\Config(__DIR__ . '/../../conf');
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
}
|
41
examples/web-skeleton/src/Container/DB.php
Normal file
41
examples/web-skeleton/src/Container/DB.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Container;
|
||||
|
||||
use Mix\Database\Database;
|
||||
|
||||
class DB
|
||||
{
|
||||
|
||||
/**
|
||||
* @var Database
|
||||
*/
|
||||
static private $instance;
|
||||
|
||||
/**
|
||||
* @return Database
|
||||
*/
|
||||
public static function instance(): Database
|
||||
{
|
||||
if (!isset(self::$instance)) {
|
||||
$dsn = $_ENV['DATABASE_DSN'];
|
||||
$username = $_ENV['DATABASE_USERNAME'];
|
||||
$password = $_ENV['DATABASE_PASSWORD'];
|
||||
$db = new Database($dsn, $username, $password);
|
||||
APP_DEBUG and $db->setLogger(new DBLogger());
|
||||
self::$instance = $db;
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public static function enableCoroutine()
|
||||
{
|
||||
$maxOpen = 30; // 最大开启连接数
|
||||
$maxIdle = 10; // 最大闲置连接数
|
||||
$maxLifetime = 3600; // 连接的最长生命周期
|
||||
$waitTimeout = 0.0; // 从池获取连接等待的时间, 0为一直等待
|
||||
self::instance()->startPool($maxOpen, $maxIdle, $maxLifetime, $waitTimeout);
|
||||
\Swoole\Runtime::enableCoroutine(); // 必须放到最后,防止触发协程调度导致异常
|
||||
}
|
||||
|
||||
}
|
17
examples/web-skeleton/src/Container/DBLogger.php
Normal file
17
examples/web-skeleton/src/Container/DBLogger.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Container;
|
||||
|
||||
/**
|
||||
* Class DBLogger
|
||||
* @package App\Container
|
||||
*/
|
||||
class DBLogger implements \Mix\Database\LoggerInterface
|
||||
{
|
||||
|
||||
public function trace(float $time, string $sql, array $bindings, int $rowCount, ?\Throwable $exception): void
|
||||
{
|
||||
Logger::instance()->debug(sprintf('SQL: %sms %s %s %d', $time, $sql, json_encode($bindings), $rowCount));
|
||||
}
|
||||
|
||||
}
|
61
examples/web-skeleton/src/Container/Logger.php
Normal file
61
examples/web-skeleton/src/Container/Logger.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Container;
|
||||
|
||||
use Monolog\Formatter\LineFormatter;
|
||||
use Monolog\Handler\HandlerInterface;
|
||||
use Monolog\Handler\RotatingFileHandler;
|
||||
|
||||
/**
|
||||
* Class Logger
|
||||
* @package App\Container
|
||||
*/
|
||||
class Logger implements HandlerInterface
|
||||
{
|
||||
|
||||
static private $instance;
|
||||
|
||||
/**
|
||||
* @return \Monolog\Logger
|
||||
*/
|
||||
public static function instance(): \Monolog\Logger
|
||||
{
|
||||
if (!isset(self::$instance)) {
|
||||
$logger = new \Monolog\Logger('MIX');
|
||||
$rotatingFileHandler = new RotatingFileHandler(__DIR__ . '/../../runtime/logs/mix.log', 7);
|
||||
$rotatingFileHandler->setFormatter(new LineFormatter("[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n", 'Y-m-d H:i:s.u'));
|
||||
$logger->pushHandler($rotatingFileHandler);
|
||||
$logger->pushHandler(new Logger());
|
||||
self::$instance = $logger;
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function isHandling(array $record): bool
|
||||
{
|
||||
return $record['level'] >= \Monolog\Logger::DEBUG;
|
||||
}
|
||||
|
||||
public function handle(array $record): bool
|
||||
{
|
||||
$message = sprintf("%s %s %s\n", $record['datetime']->format('Y-m-d H:i:s.u'), $record['level_name'], $record['message']);
|
||||
switch (PHP_SAPI) {
|
||||
case 'cli':
|
||||
case 'cli-server':
|
||||
file_put_contents("php://stdout", $message);
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function handleBatch(array $records): void
|
||||
{
|
||||
// TODO: Implement handleBatch() method.
|
||||
}
|
||||
|
||||
public function close(): void
|
||||
{
|
||||
// TODO: Implement close() method.
|
||||
}
|
||||
|
||||
}
|
42
examples/web-skeleton/src/Container/RDS.php
Normal file
42
examples/web-skeleton/src/Container/RDS.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Container;
|
||||
|
||||
use Mix\Redis\Redis;
|
||||
|
||||
class RDS
|
||||
{
|
||||
|
||||
/**
|
||||
* @var Redis
|
||||
*/
|
||||
static private $instance;
|
||||
|
||||
/**
|
||||
* @return Redis
|
||||
*/
|
||||
public static function instance(): Redis
|
||||
{
|
||||
if (!isset(self::$instance)) {
|
||||
$host = $_ENV['REDIS_HOST'];
|
||||
$port = $_ENV['REDIS_PORT'];
|
||||
$password = $_ENV['REDIS_PASSWORD'];
|
||||
$database = $_ENV['REDIS_DATABASE'];
|
||||
$rds = new Redis($host, $port, $password, $database);
|
||||
APP_DEBUG and $rds->setLogger(new RDSLogger());
|
||||
self::$instance = $rds;
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public static function enableCoroutine()
|
||||
{
|
||||
$maxOpen = 30; // 最大开启连接数
|
||||
$maxIdle = 10; // 最大闲置连接数
|
||||
$maxLifetime = 3600; // 连接的最长生命周期
|
||||
$waitTimeout = 0.0; // 从池获取连接等待的时间, 0为一直等待
|
||||
self::instance()->startPool($maxOpen, $maxIdle, $maxLifetime, $waitTimeout);
|
||||
\Swoole\Runtime::enableCoroutine(); // 必须放到最后,防止触发协程调度导致异常
|
||||
}
|
||||
|
||||
}
|
17
examples/web-skeleton/src/Container/RDSLogger.php
Normal file
17
examples/web-skeleton/src/Container/RDSLogger.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Container;
|
||||
|
||||
/**
|
||||
* Class RDSLogger
|
||||
* @package App\Container
|
||||
*/
|
||||
class RDSLogger implements \Mix\Redis\LoggerInterface
|
||||
{
|
||||
|
||||
public function trace(float $time, string $cmd, array $args, ?\Throwable $exception): void
|
||||
{
|
||||
Logger::instance()->debug(sprintf('RDS: %sms %s %s', $time, $cmd, json_encode($args)));
|
||||
}
|
||||
|
||||
}
|
20
examples/web-skeleton/src/Controller/Hello.php
Normal file
20
examples/web-skeleton/src/Controller/Hello.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use Mix\Vega\Context;
|
||||
|
||||
class Hello
|
||||
{
|
||||
|
||||
/**
|
||||
* @param Context $ctx
|
||||
*/
|
||||
public function index(Context $ctx)
|
||||
{
|
||||
$ctx->HTML(200, 'index', [
|
||||
'title' => 'Hello, World!'
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
81
examples/web-skeleton/src/Error.php
Normal file
81
examples/web-skeleton/src/Error.php
Normal file
@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use App\Container\Logger;
|
||||
|
||||
/**
|
||||
* Class Error
|
||||
* @package App
|
||||
*/
|
||||
class Error
|
||||
{
|
||||
|
||||
/**
|
||||
* @var Error
|
||||
*/
|
||||
static private $instance;
|
||||
|
||||
public static function register(): void
|
||||
{
|
||||
if (!isset(self::$instance)) {
|
||||
self::$instance = new Error();
|
||||
set_error_handler([self::$instance, 'error']);
|
||||
set_exception_handler([self::$instance, 'exception']); // swoole 协程不支持该函数
|
||||
register_shutdown_function([self::$instance, 'shutdown']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $errno
|
||||
* @param $errstr
|
||||
* @param string $errfile
|
||||
* @param int $errline
|
||||
*/
|
||||
public function error($errno, $errstr, $errfile = '', $errline = 0): void
|
||||
{
|
||||
if (error_reporting() & $errno) {
|
||||
// 委托给异常处理
|
||||
$isFatalWarning = function ($errno, $errstr) {
|
||||
if ($errno == E_WARNING && strpos($errstr, 'require') === 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
if ($isFatalWarning($errno, $errstr)) {
|
||||
$this->exception(new \Error(sprintf('%s in %s on line %d', $errstr, $errfile, $errline), $errno));
|
||||
return;
|
||||
}
|
||||
// 转换为异常抛出
|
||||
throw new \Error(sprintf('%s in %s on line %d', $errstr, $errfile, $errline), $errno);
|
||||
}
|
||||
}
|
||||
|
||||
public function shutdown(): void
|
||||
{
|
||||
$isFatal = function ($errno) {
|
||||
return in_array($errno, [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE]);
|
||||
};
|
||||
if (!is_null($error = error_get_last()) && $isFatal($error['type'])) {
|
||||
// 委托给异常处理
|
||||
$this->exception(new \Error(sprintf('%s in %s on line %d', $error['message'], $error['file'], $error['line']), $error['type']));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Throwable $ex
|
||||
*/
|
||||
public function exception(\Throwable $ex): void
|
||||
{
|
||||
Logger::instance()->error(sprintf('%s in %s on line %d', $ex->getMessage(), $ex->getFile(), $ex->getLine()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Throwable $ex
|
||||
*/
|
||||
public static function handle(\Throwable $ex)
|
||||
{
|
||||
self::$instance->exception($ex);
|
||||
}
|
||||
|
||||
}
|
63
examples/web-skeleton/src/Vega.php
Normal file
63
examples/web-skeleton/src/Vega.php
Normal file
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use App\Container\Logger;
|
||||
use Mix\Vega\Abort;
|
||||
use Mix\Vega\Context;
|
||||
use Mix\Vega\Engine;
|
||||
use Mix\Vega\Exception\NotFoundException;
|
||||
|
||||
class Vega
|
||||
{
|
||||
|
||||
/**
|
||||
* @return Engine
|
||||
*/
|
||||
public static function new(): Engine
|
||||
{
|
||||
$vega = new Engine();
|
||||
|
||||
// 500
|
||||
$vega->use(function (Context $ctx) {
|
||||
try {
|
||||
$ctx->next();
|
||||
} catch (\Throwable $ex) {
|
||||
if ($ex instanceof Abort || $ex instanceof NotFoundException) {
|
||||
throw $ex;
|
||||
}
|
||||
Logger::instance()->error(sprintf('%s in %s on line %d', $ex->getMessage(), $ex->getFile(), $ex->getLine()));
|
||||
$ctx->string(500, 'Internal Server Error');
|
||||
$ctx->abort();
|
||||
}
|
||||
});
|
||||
|
||||
// debug
|
||||
if (APP_DEBUG) {
|
||||
$vega->use(function (Context $ctx) {
|
||||
$ctx->next();
|
||||
Logger::instance()->debug(sprintf(
|
||||
'%s|%s|%s|%s',
|
||||
$ctx->request->getMethod(),
|
||||
$ctx->uri(),
|
||||
$ctx->response->getStatusCode(),
|
||||
$ctx->remoteIP()
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
// 静态文件处理
|
||||
$vega->static('/static', __DIR__ . '/../public/static');
|
||||
$vega->staticFile('/favicon.ico', __DIR__ . '/../public/favicon.ico');
|
||||
|
||||
// 视图
|
||||
$vega->withHTMLRoot(__DIR__ . '/../views');
|
||||
|
||||
// routes
|
||||
$routes = require __DIR__ . '/../routes/index.php';
|
||||
$routes($vega);
|
||||
|
||||
return $vega;
|
||||
}
|
||||
|
||||
}
|
38
examples/web-skeleton/src/functions.php
Normal file
38
examples/web-skeleton/src/functions.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
if (!function_exists('env')) {
|
||||
/**
|
||||
* @param string $key
|
||||
* @param null $default
|
||||
* @return array|bool|string|null
|
||||
*/
|
||||
function env(string $key, $default = null)
|
||||
{
|
||||
$value = getenv($key);
|
||||
|
||||
if ($value === false) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
switch (strtolower($value)) {
|
||||
case 'true':
|
||||
case '(true)':
|
||||
return true;
|
||||
case 'false':
|
||||
case '(false)':
|
||||
return false;
|
||||
case 'empty':
|
||||
case '(empty)':
|
||||
return '';
|
||||
case 'null':
|
||||
case '(null)':
|
||||
return null;
|
||||
}
|
||||
|
||||
if (($valueLength = strlen($value)) > 1 && $value[0] === '"' && $value[$valueLength - 1] === '"') {
|
||||
return substr($value, 1, -1);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
5
examples/web-skeleton/views/index.php
Normal file
5
examples/web-skeleton/views/index.php
Normal file
@ -0,0 +1,5 @@
|
||||
<html>
|
||||
<h1>
|
||||
<?= $title ?>
|
||||
</h1>
|
||||
</html>
|
13
examples/websocket-skeleton/.env
Normal file
13
examples/websocket-skeleton/.env
Normal file
@ -0,0 +1,13 @@
|
||||
# APP
|
||||
APP_DEBUG=true
|
||||
|
||||
# DATABASE
|
||||
DATABASE_DSN='mysql:host=127.0.0.1;port=3306;charset=utf8;dbname=test'
|
||||
DATABASE_USERNAME=root
|
||||
DATABASE_PASSWORD=123456
|
||||
|
||||
# REDIS
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PORT=6379
|
||||
REDIS_DATABASE=0
|
||||
REDIS_PASSWORD=
|
26
examples/websocket-skeleton/.gitignore
vendored
Normal file
26
examples/websocket-skeleton/.gitignore
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
# phpstorm project files
|
||||
.idea
|
||||
|
||||
# netbeans project files
|
||||
nbproject
|
||||
|
||||
# zend studio for eclipse project files
|
||||
.buildpath
|
||||
.project
|
||||
.settings
|
||||
|
||||
# windows thumbnail cache
|
||||
Thumbs.db
|
||||
|
||||
# composer itself is not needed
|
||||
composer.phar
|
||||
composer.lock
|
||||
vendor/
|
||||
|
||||
# Mac DS_Store Files
|
||||
.DS_Store
|
||||
|
||||
# phpunit itself is not needed
|
||||
phpunit.phar
|
||||
# local phpunit config
|
||||
/phpunit.xml
|
251
examples/websocket-skeleton/README.md
Normal file
251
examples/websocket-skeleton/README.md
Normal file
@ -0,0 +1,251 @@
|
||||
# WebSocket development skeleton
|
||||
|
||||
帮助你快速搭建 WebSocket 项目骨架,并指导你如何使用该骨架的细节,骨架默认开启了 SQL、Redis 日志,压测前请先关闭 `.env` 的 `APP_DEBUG`
|
||||
|
||||
## 安装
|
||||
|
||||
> 需要先安装 [Swoole](https://wiki.swoole.com/#/environment)
|
||||
|
||||
- Swoole >= 4.4.15: https://wiki.swoole.com/#/environment
|
||||
|
||||
```
|
||||
composer create-project --prefer-dist mix/websocket-skeleton websocket
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
启动 Swoole 协程服务
|
||||
|
||||
```
|
||||
composer run-script --timeout=0 swooleco:start
|
||||
```
|
||||
|
||||
## 执行脚本
|
||||
|
||||
- `composer run-script` 命令中的 `--timeout=0` 参数是防止 composer [执行超时](https://getcomposer.org/doc/06-config.md#process-timeout)
|
||||
- `composer.json` 定义了命令执行脚本,对应上面的执行命令
|
||||
|
||||
```json
|
||||
"scripts": {
|
||||
"swooleco:start": "php bin/swooleco.php",
|
||||
"cli:clearcache": "php bin/cli.php clearcache"
|
||||
},
|
||||
```
|
||||
|
||||
|
||||
当然也可以直接下面这样启动,效果是一样的,但是 `scripts` 能帮你记录到底有哪些可用的命令,同时在IDE中调试更加方便。
|
||||
|
||||
```
|
||||
php bin/swooleco.php start
|
||||
```
|
||||
|
||||
## 部署
|
||||
|
||||
线上部署启动时,修改 `shell/server.sh` 脚本中的绝对路径和参数
|
||||
|
||||
```
|
||||
php=/usr/local/bin/php
|
||||
file=/project/bin/swooleco.php
|
||||
cmd=start
|
||||
numprocs=1
|
||||
```
|
||||
|
||||
启动管理
|
||||
|
||||
```
|
||||
sh shell/server.sh start
|
||||
sh shell/server.sh stop
|
||||
sh shell/server.sh restart
|
||||
```
|
||||
|
||||
使用 `nginx` 或者 `SLB` 代理到服务器端口即可
|
||||
|
||||
```
|
||||
location /websocket {
|
||||
proxy_pass http://127.0.0.1:9502;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
}
|
||||
```
|
||||
|
||||
## 编写一个 WebSocket 服务
|
||||
|
||||
首先修改根目录 `.env` 文件的数据库信息
|
||||
|
||||
然后在 `routes/index.php` 定义一个新的路由
|
||||
|
||||
```php
|
||||
$vega->handle('/websocket', [new WebSocket(), 'index'])->methods('GET');
|
||||
```
|
||||
|
||||
路由里使用了 `WebSocket` 控制器,我们需要创建他
|
||||
|
||||
- 如何配置路由:[mix-php/vega](https://github.com/mix-php/vega#readme)
|
||||
- 如何使用 WebSocket 升级器:[mix-php/websocket](https://github.com/mix-php/websocket#readme)
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Container\Upgrader;
|
||||
use App\Service\Session;
|
||||
use Mix\Vega\Context;
|
||||
|
||||
class WebSocket
|
||||
{
|
||||
|
||||
/**
|
||||
* @param Context $ctx
|
||||
*/
|
||||
public function index(Context $ctx)
|
||||
{
|
||||
$conn = Upgrader::instance()->upgrade($ctx->request, $ctx->response);
|
||||
$session = new Session($conn);
|
||||
$session->start();
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
控制器中使用了一个 `Session` 类来处理连接事务
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Handler\Hello;
|
||||
use Mix\WebSocket\Connection;
|
||||
use Swoole\Coroutine\Channel;
|
||||
|
||||
class Session
|
||||
{
|
||||
|
||||
/**
|
||||
* @var Connection
|
||||
*/
|
||||
protected $conn;
|
||||
|
||||
/**
|
||||
* @var Channel
|
||||
*/
|
||||
protected $writeChan;
|
||||
|
||||
/**
|
||||
* Session constructor.
|
||||
* @param Connection $conn
|
||||
*/
|
||||
public function __construct(Connection $conn)
|
||||
{
|
||||
$this->conn = $conn;
|
||||
$this->writeChan = new Channel(10);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $data
|
||||
*/
|
||||
public function send(string $data): void
|
||||
{
|
||||
$this->writeChan->push($data);
|
||||
}
|
||||
|
||||
public function start(): void
|
||||
{
|
||||
// 接收消息
|
||||
go(function () {
|
||||
while (true) {
|
||||
$frame = $this->conn->recv();
|
||||
$message = $frame->data;
|
||||
|
||||
(new Hello($this))->index($message);
|
||||
}
|
||||
});
|
||||
|
||||
// 发送消息
|
||||
go(function () {
|
||||
while (true) {
|
||||
$data = $this->writeChan->pop();
|
||||
if (!$data) {
|
||||
return;
|
||||
}
|
||||
$frame = new \Swoole\WebSocket\Frame();
|
||||
$frame->data = $data;
|
||||
$frame->opcode = WEBSOCKET_OPCODE_TEXT; // or WEBSOCKET_OPCODE_BINARY
|
||||
$this->conn->send($frame);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
在接收消息处,使用了 `src/Handler/Hello.php` 处理器对当前发送的消息做逻辑处理,我们只需根据自己的需求增加新的处理器来处理不同消息即可。
|
||||
|
||||
```
|
||||
(new Hello($this))->index($message);
|
||||
```
|
||||
|
||||
重新启动服务器后方可测试新开发的接口
|
||||
|
||||
> 实际开发中使用 PhpStorm 的 Run 功能,只需要点击一下重启按钮即可
|
||||
|
||||
```
|
||||
// 查找进程 PID
|
||||
ps -ef | grep swoole
|
||||
|
||||
// 通过 PID 停止进程
|
||||
kill PID
|
||||
|
||||
// 重新启动进程
|
||||
composer run-script swoole:start
|
||||
```
|
||||
|
||||
使用测试工具测试
|
||||
|
||||
- [WEBSOCKET 在线测试工具](http://www.easyswoole.com/wstool.html)
|
||||
|
||||
## 如何使用 WebSocket 客户端
|
||||
|
||||
- [mix-php/websocket#客户端-client](https://github.com/mix-php/websocket#%E5%AE%A2%E6%88%B7%E7%AB%AF-client)
|
||||
|
||||
## 使用容器中的对象
|
||||
|
||||
容器采用了一个简单的单例模式,你可以修改为更加适合自己的方式。
|
||||
|
||||
- 数据库
|
||||
|
||||
```
|
||||
DB::instance()
|
||||
```
|
||||
|
||||
文档:[mix-php/database](https://github.com/mix-php/database#readme)
|
||||
|
||||
- Redis
|
||||
|
||||
```
|
||||
RDS::instance()
|
||||
```
|
||||
|
||||
文档:[mix-php/redis](https://github.com/mix-php/redis#readme)
|
||||
|
||||
- 日志
|
||||
|
||||
```
|
||||
Logger::instance()
|
||||
```
|
||||
|
||||
文档:[monolog/monolog](https://seldaek.github.io/monolog/doc/01-usage.html)
|
||||
|
||||
- 配置
|
||||
|
||||
```
|
||||
Config::instance()
|
||||
```
|
||||
|
||||
文档:[hassankhan/config](https://github.com/hassankhan/config#getting-values)
|
||||
|
||||
## License
|
||||
|
||||
Apache License Version 2.0, http://www.apache.org/licenses/
|
15
examples/websocket-skeleton/bin/cli.php
Normal file
15
examples/websocket-skeleton/bin/cli.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use Dotenv\Dotenv;
|
||||
|
||||
Dotenv::createUnsafeImmutable(__DIR__ . '/../', '.env')->load();
|
||||
define("APP_DEBUG", env('APP_DEBUG'));
|
||||
|
||||
App\Error::register();
|
||||
|
||||
switch ($argv[1]) {
|
||||
case 'clearcache';
|
||||
(new \App\Command\ClearCache())->exec();
|
||||
break;
|
||||
}
|
32
examples/websocket-skeleton/bin/swooleco.php
Normal file
32
examples/websocket-skeleton/bin/swooleco.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use App\Container\Logger;
|
||||
use App\Container\Upgrader;
|
||||
use App\Vega;
|
||||
use Dotenv\Dotenv;
|
||||
|
||||
Dotenv::createUnsafeImmutable(__DIR__ . '/../', '.env')->load();
|
||||
define("APP_DEBUG", env('APP_DEBUG'));
|
||||
|
||||
App\Error::register();
|
||||
|
||||
Swoole\Coroutine\run(function () {
|
||||
$vega = Vega::new();
|
||||
$server = new Swoole\Coroutine\Http\Server('0.0.0.0', 9502, false, false);
|
||||
$server->handle('/', $vega->handler());
|
||||
|
||||
App\Container\DB::enableCoroutine();
|
||||
App\Container\RDS::enableCoroutine();
|
||||
|
||||
foreach ([SIGHUP, SIGINT, SIGTERM] as $signal) {
|
||||
Swoole\Process::signal($signal, function () use ($server) {
|
||||
Logger::instance()->info('Shutdown swoole coroutine server');
|
||||
$server->shutdown();
|
||||
Upgrader::instance()->closeAll();
|
||||
});
|
||||
}
|
||||
|
||||
Logger::instance()->info('Start swoole coroutine server');
|
||||
$server->start();
|
||||
});
|
31
examples/websocket-skeleton/composer.json
Normal file
31
examples/websocket-skeleton/composer.json
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "mix/websocket-skeleton",
|
||||
"description": "WebSocket development skeleton",
|
||||
"type": "project",
|
||||
"homepage": "https://openmix.org/mix-php",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"swooleco:start": "php bin/swooleco.php",
|
||||
"cli:clearcache": "php bin/cli.php clearcache"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "src/"
|
||||
},
|
||||
"files": [
|
||||
"src/functions.php"
|
||||
]
|
||||
},
|
||||
"require": {
|
||||
"mix/vega": "~3.0",
|
||||
"mix/websocket": "~3.0",
|
||||
"mix/database": "~3.0",
|
||||
"mix/redis": "~3.0",
|
||||
"vlucas/phpdotenv": "^5.3",
|
||||
"hassankhan/config": "^2.2",
|
||||
"monolog/monolog": "^2.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"swoole/ide-helper": "^4.6"
|
||||
}
|
||||
}
|
3
examples/websocket-skeleton/conf/config.json
Normal file
3
examples/websocket-skeleton/conf/config.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"foo": "bar"
|
||||
}
|
7
examples/websocket-skeleton/routes/index.php
Normal file
7
examples/websocket-skeleton/routes/index.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Controller\WebSocket;
|
||||
|
||||
return function (Mix\Vega\Engine $vega) {
|
||||
$vega->handle('/websocket', [new WebSocket(), 'index'])->methods('GET');
|
||||
};
|
2
examples/websocket-skeleton/runtime/.gitignore
vendored
Normal file
2
examples/websocket-skeleton/runtime/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
75
examples/websocket-skeleton/shell/server.sh
Normal file
75
examples/websocket-skeleton/shell/server.sh
Normal file
@ -0,0 +1,75 @@
|
||||
#!/bin/sh
|
||||
echo "============`date +%F' '%T`==========="
|
||||
|
||||
php=/usr/local/bin/php
|
||||
file=/project/bin/swooleco.php
|
||||
cmd=start
|
||||
numprocs=1
|
||||
|
||||
getpid()
|
||||
{
|
||||
docmd=`ps aux | grep ${file} | grep ${cmd} | grep -v 'grep' | grep -v '\.sh' | awk '{print $2}' | xargs`
|
||||
echo $docmd
|
||||
}
|
||||
|
||||
getmpid()
|
||||
{
|
||||
docmd=`ps -ef | grep ${file} | grep ${cmd} | grep -v 'grep' | grep -v '\.sh' | grep ' 1 ' | awk '{print $2}' | xargs`
|
||||
echo $docmd
|
||||
}
|
||||
|
||||
start()
|
||||
{
|
||||
pidstr=`getpid`
|
||||
if [ -n "$pidstr" ];then
|
||||
echo "running with pids $pidstr"
|
||||
else
|
||||
if [ $numprocs -eq 1 ];then
|
||||
$php $file $cmd > /dev/null 2>&1 &
|
||||
else
|
||||
i=0
|
||||
while(( $i<$numprocs ))
|
||||
do
|
||||
$php $file $cmd > /dev/null 2>&1 &
|
||||
let "i++"
|
||||
done
|
||||
fi
|
||||
sleep 1
|
||||
pidstr=`getpid`
|
||||
echo "start with pids $pidstr"
|
||||
fi
|
||||
}
|
||||
|
||||
stop()
|
||||
{
|
||||
pidstr=`getpid`
|
||||
if [ ! -n "$pidstr" ];then
|
||||
echo "not executed!"
|
||||
return
|
||||
fi
|
||||
mpidstr=`getmpid`
|
||||
if [ -n "$mpidstr" ];then
|
||||
pidstr=$mpidstr
|
||||
fi
|
||||
echo "kill $pidstr"
|
||||
kill $pidstr
|
||||
}
|
||||
|
||||
restart()
|
||||
{
|
||||
stop
|
||||
sleep 1
|
||||
start
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
start
|
||||
;;
|
||||
stop)
|
||||
stop
|
||||
;;
|
||||
restart)
|
||||
restart
|
||||
;;
|
||||
esac
|
20
examples/websocket-skeleton/src/Command/ClearCache.php
Normal file
20
examples/websocket-skeleton/src/Command/ClearCache.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Container\RDS;
|
||||
|
||||
/**
|
||||
* Class ClearCache
|
||||
* @package App\Command
|
||||
*/
|
||||
class ClearCache
|
||||
{
|
||||
|
||||
public function exec(): void
|
||||
{
|
||||
RDS::instance()->del('foo_cache');
|
||||
print 'ok';
|
||||
}
|
||||
|
||||
}
|
28
examples/websocket-skeleton/src/Container/Config.php
Normal file
28
examples/websocket-skeleton/src/Container/Config.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Container;
|
||||
|
||||
/**
|
||||
* Class Config
|
||||
* @package App\Container
|
||||
*/
|
||||
class Config
|
||||
{
|
||||
|
||||
/**
|
||||
* @var \Noodlehaus\Config
|
||||
*/
|
||||
static private $instance;
|
||||
|
||||
/**
|
||||
* @return \Noodlehaus\Config
|
||||
*/
|
||||
public static function instance(): \Noodlehaus\Config
|
||||
{
|
||||
if (!isset(self::$instance)) {
|
||||
self::$instance = new \Noodlehaus\Config(__DIR__ . '/../../conf');
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
}
|
41
examples/websocket-skeleton/src/Container/DB.php
Normal file
41
examples/websocket-skeleton/src/Container/DB.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Container;
|
||||
|
||||
use Mix\Database\Database;
|
||||
|
||||
class DB
|
||||
{
|
||||
|
||||
/**
|
||||
* @var Database
|
||||
*/
|
||||
static private $instance;
|
||||
|
||||
/**
|
||||
* @return Database
|
||||
*/
|
||||
public static function instance(): Database
|
||||
{
|
||||
if (!isset(self::$instance)) {
|
||||
$dsn = $_ENV['DATABASE_DSN'];
|
||||
$username = $_ENV['DATABASE_USERNAME'];
|
||||
$password = $_ENV['DATABASE_PASSWORD'];
|
||||
$db = new Database($dsn, $username, $password);
|
||||
APP_DEBUG and $db->setLogger(new DBLogger());
|
||||
self::$instance = $db;
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public static function enableCoroutine()
|
||||
{
|
||||
$maxOpen = 30; // 最大开启连接数
|
||||
$maxIdle = 10; // 最大闲置连接数
|
||||
$maxLifetime = 3600; // 连接的最长生命周期
|
||||
$waitTimeout = 0.0; // 从池获取连接等待的时间, 0为一直等待
|
||||
self::instance()->startPool($maxOpen, $maxIdle, $maxLifetime, $waitTimeout);
|
||||
\Swoole\Runtime::enableCoroutine(); // 必须放到最后,防止触发协程调度导致异常
|
||||
}
|
||||
|
||||
}
|
17
examples/websocket-skeleton/src/Container/DBLogger.php
Normal file
17
examples/websocket-skeleton/src/Container/DBLogger.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Container;
|
||||
|
||||
/**
|
||||
* Class DBLogger
|
||||
* @package App\Container
|
||||
*/
|
||||
class DBLogger implements \Mix\Database\LoggerInterface
|
||||
{
|
||||
|
||||
public function trace(float $time, string $sql, array $bindings, int $rowCount, ?\Throwable $exception): void
|
||||
{
|
||||
Logger::instance()->debug(sprintf('SQL: %sms %s %s %d', $time, $sql, json_encode($bindings), $rowCount));
|
||||
}
|
||||
|
||||
}
|
61
examples/websocket-skeleton/src/Container/Logger.php
Normal file
61
examples/websocket-skeleton/src/Container/Logger.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Container;
|
||||
|
||||
use Monolog\Formatter\LineFormatter;
|
||||
use Monolog\Handler\HandlerInterface;
|
||||
use Monolog\Handler\RotatingFileHandler;
|
||||
|
||||
/**
|
||||
* Class Logger
|
||||
* @package App\Container
|
||||
*/
|
||||
class Logger implements HandlerInterface
|
||||
{
|
||||
|
||||
static private $instance;
|
||||
|
||||
/**
|
||||
* @return \Monolog\Logger
|
||||
*/
|
||||
public static function instance(): \Monolog\Logger
|
||||
{
|
||||
if (!isset(self::$instance)) {
|
||||
$logger = new \Monolog\Logger('MIX');
|
||||
$rotatingFileHandler = new RotatingFileHandler(__DIR__ . '/../../runtime/logs/mix.log', 7);
|
||||
$rotatingFileHandler->setFormatter(new LineFormatter("[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n", 'Y-m-d H:i:s.u'));
|
||||
$logger->pushHandler($rotatingFileHandler);
|
||||
$logger->pushHandler(new Logger());
|
||||
self::$instance = $logger;
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function isHandling(array $record): bool
|
||||
{
|
||||
return $record['level'] >= \Monolog\Logger::DEBUG;
|
||||
}
|
||||
|
||||
public function handle(array $record): bool
|
||||
{
|
||||
$message = sprintf("%s %s %s\n", $record['datetime']->format('Y-m-d H:i:s.u'), $record['level_name'], $record['message']);
|
||||
switch (PHP_SAPI) {
|
||||
case 'cli':
|
||||
case 'cli-server':
|
||||
file_put_contents("php://stdout", $message);
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function handleBatch(array $records): void
|
||||
{
|
||||
// TODO: Implement handleBatch() method.
|
||||
}
|
||||
|
||||
public function close(): void
|
||||
{
|
||||
// TODO: Implement close() method.
|
||||
}
|
||||
|
||||
}
|
42
examples/websocket-skeleton/src/Container/RDS.php
Normal file
42
examples/websocket-skeleton/src/Container/RDS.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Container;
|
||||
|
||||
use Mix\Redis\Redis;
|
||||
|
||||
class RDS
|
||||
{
|
||||
|
||||
/**
|
||||
* @var Redis
|
||||
*/
|
||||
static private $instance;
|
||||
|
||||
/**
|
||||
* @return Redis
|
||||
*/
|
||||
public static function instance(): Redis
|
||||
{
|
||||
if (!isset(self::$instance)) {
|
||||
$host = $_ENV['REDIS_HOST'];
|
||||
$port = $_ENV['REDIS_PORT'];
|
||||
$password = $_ENV['REDIS_PASSWORD'];
|
||||
$database = $_ENV['REDIS_DATABASE'];
|
||||
$rds = new Redis($host, $port, $password, $database);
|
||||
APP_DEBUG and $rds->setLogger(new RDSLogger());
|
||||
self::$instance = $rds;
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public static function enableCoroutine()
|
||||
{
|
||||
$maxOpen = 30; // 最大开启连接数
|
||||
$maxIdle = 10; // 最大闲置连接数
|
||||
$maxLifetime = 3600; // 连接的最长生命周期
|
||||
$waitTimeout = 0.0; // 从池获取连接等待的时间, 0为一直等待
|
||||
self::instance()->startPool($maxOpen, $maxIdle, $maxLifetime, $waitTimeout);
|
||||
\Swoole\Runtime::enableCoroutine(); // 必须放到最后,防止触发协程调度导致异常
|
||||
}
|
||||
|
||||
}
|
17
examples/websocket-skeleton/src/Container/RDSLogger.php
Normal file
17
examples/websocket-skeleton/src/Container/RDSLogger.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Container;
|
||||
|
||||
/**
|
||||
* Class RDSLogger
|
||||
* @package App\Container
|
||||
*/
|
||||
class RDSLogger implements \Mix\Redis\LoggerInterface
|
||||
{
|
||||
|
||||
public function trace(float $time, string $cmd, array $args, ?\Throwable $exception): void
|
||||
{
|
||||
Logger::instance()->debug(sprintf('RDS: %sms %s %s', $time, $cmd, json_encode($args)));
|
||||
}
|
||||
|
||||
}
|
28
examples/websocket-skeleton/src/Container/Upgrader.php
Normal file
28
examples/websocket-skeleton/src/Container/Upgrader.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Container;
|
||||
|
||||
/**
|
||||
* Class Upgrader
|
||||
* @package App\Container
|
||||
*/
|
||||
class Upgrader
|
||||
{
|
||||
|
||||
/**
|
||||
* @var \Mix\WebSocket\Upgrader
|
||||
*/
|
||||
static private $instance;
|
||||
|
||||
/**
|
||||
* @return \Mix\WebSocket\Upgrader
|
||||
*/
|
||||
public static function instance(): \Mix\WebSocket\Upgrader
|
||||
{
|
||||
if (!isset(self::$instance)) {
|
||||
self::$instance = new \Mix\WebSocket\Upgrader();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user